Files
slsa-verifier/verifiers/internal/gha/bundle.go
T
2024-08-13 15:42:58 +00:00

285 lines
9.5 KiB
Go

package gha
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse"
bundle_v1 "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
v1 "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1"
"github.com/sigstore/rekor/pkg/generated/models"
sigstoreRoot "github.com/sigstore/sigstore-go/pkg/root"
"google.golang.org/protobuf/encoding/protojson"
)
// Bundle specific errors.
var (
ErrorMismatchSignature = errors.New("bundle tlog entry does not match signature")
ErrorUnexpectedEntryType = errors.New("unexpected tlog entry type")
ErrorMissingCertInBundle = errors.New("missing signing certificate in bundle")
ErrorUnexpectedBundleContent = errors.New("expected DSSE bundle content")
)
// IsSigstoreBundle checks if the provenance is a Sigstore bundle.
func IsSigstoreBundle(bytes []byte) bool {
var bundle bundle_v1.Bundle
if err := protojson.Unmarshal(bytes, &bundle); err != nil {
return false
}
return true
}
// verifyRekorEntryFromBundle extracts and verifies the Rekor entry from the Sigstore
// bundle verification material, validating the SignedEntryTimestamp.
func verifyRekorEntryFromBundle(ctx context.Context, tlogEntry *v1.TransparencyLogEntry,
trustedRoot *sigstoreRoot.LiveTrustedRoot) (
*models.LogEntryAnon, error,
) {
canonicalBody := tlogEntry.GetCanonicalizedBody()
logID := hex.EncodeToString(tlogEntry.GetLogId().GetKeyId())
rekorEntry := &models.LogEntryAnon{
Body: canonicalBody,
IntegratedTime: &tlogEntry.IntegratedTime,
LogIndex: &tlogEntry.LogIndex,
LogID: &logID,
Verification: &models.LogEntryAnonVerification{
SignedEntryTimestamp: tlogEntry.GetInclusionPromise().GetSignedEntryTimestamp(),
},
}
// Verify tlog entry.
if _, err := verifyTlogEntry(ctx, *rekorEntry, false,
trustedRoot); err != nil {
return nil, err
}
return rekorEntry, nil
}
// getEnvelopeFromBundle extracts the DSSE envelope from the Sigstore bundle.
func getEnvelopeFromBundle(bundle *bundle_v1.Bundle) (*dsselib.Envelope, error) {
dsseEnvelope := bundle.GetDsseEnvelope()
if dsseEnvelope == nil {
return nil, ErrorUnexpectedBundleContent
}
env := &dsselib.Envelope{
PayloadType: dsseEnvelope.GetPayloadType(),
Payload: base64.StdEncoding.EncodeToString(dsseEnvelope.GetPayload()),
}
for _, sig := range dsseEnvelope.GetSignatures() {
env.Signatures = append(env.Signatures, dsselib.Signature{
KeyID: sig.GetKeyid(),
Sig: base64.StdEncoding.EncodeToString(sig.GetSig()),
})
}
return env, nil
}
func getEnvelopeFromBundleBytes(content []byte) (*dsselib.Envelope, error) {
var bundle bundle_v1.Bundle
if err := protojson.Unmarshal(content, &bundle); err != nil {
return nil, fmt.Errorf("unmarshaling bundle: %w", err)
}
env, err := getEnvelopeFromBundle(&bundle)
if err != nil {
return nil, err
}
return env, nil
}
// getLeafCertFromBundle extracts the signing cert from the Sigstore bundle.
func getLeafCertFromBundle(bundle *bundle_v1.Bundle) (*x509.Certificate, error) {
certChain := bundle.GetVerificationMaterial().GetX509CertificateChain().GetCertificates()
if len(certChain) == 0 {
return nil, ErrorMissingCertInBundle
}
// The first certificate is the leaf cert: see
// https://github.com/sigstore/protobuf-specs/blob/16541696de137c6281d66d075a4924d9bbd181ff/protos/sigstore_common.proto#L170
certBytes := certChain[0].GetRawBytes()
return x509.ParseCertificate(certBytes)
}
// matchRekorEntryWithEnvelope ensures that the log entry references the given
// DSSE envelope. It MUST verify that the signatures match to ensure that the
// tlog timestamp attests to the signature creation time.
func matchRekorEntryWithEnvelope(tlogEntry *v1.TransparencyLogEntry, env *dsselib.Envelope) error {
kindVersion := tlogEntry.GetKindVersion()
if kindVersion.Kind == "intoto" && kindVersion.Version == "0.0.2" {
return matchRekorEntryWithEnvelopeIntotov002(tlogEntry, env)
}
if kindVersion.Kind == "dsse" && kindVersion.Version == "0.0.1" {
return matchRekorEntryWithEnvelopeDSSEv001(tlogEntry, env)
}
return fmt.Errorf("%w: %s: %s", ErrorUnexpectedEntryType, kindVersion.Kind, kindVersion.Version)
}
// matchRekorEntryWithEnvelopeDSSEv001 handles matchRekorEntryWithEnvelope for the intoto v0.0.1 type version.
func matchRekorEntryWithEnvelopeIntotov002(tlogEntry *v1.TransparencyLogEntry, env *dsselib.Envelope) error {
canonicalBody := tlogEntry.GetCanonicalizedBody()
var toto models.Intoto
var intotoObj models.IntotoV002Schema
if err := json.Unmarshal(canonicalBody, &toto); err != nil {
return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err)
}
specMarshal, err := json.Marshal(toto.Spec)
if err != nil {
return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err)
}
if err := json.Unmarshal(specMarshal, &intotoObj); err != nil {
return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err)
}
if len(env.Signatures) != len(intotoObj.Content.Envelope.Signatures) {
return fmt.Errorf("expected %d sigs in canonical body, got %d",
len(env.Signatures),
len(intotoObj.Content.Envelope.Signatures))
}
// TODO(#487): verify the certs match.
for _, sig := range env.Signatures {
// The signature in the canonical body is double base64-encoded.
encodedEnvSig := base64.StdEncoding.EncodeToString(
[]byte(sig.Sig))
var matchCanonical bool
for _, canonicalSig := range intotoObj.Content.Envelope.Signatures {
if canonicalSig.Sig.String() == encodedEnvSig {
matchCanonical = true
}
}
if !matchCanonical {
return ErrorMismatchSignature
}
}
return nil
}
// matchRekorEntryWithEnvelopeDSSEv001 handles matchRekorEntryWithEnvelope for the dsse v0.0.1 type version.
func matchRekorEntryWithEnvelopeDSSEv001(tlogEntry *v1.TransparencyLogEntry, env *dsselib.Envelope) error {
canonicalBody := tlogEntry.GetCanonicalizedBody()
var dsseObj models.DSSE
if err := json.Unmarshal(canonicalBody, &dsseObj); err != nil {
return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err)
}
var dsseSchemaObj models.DSSEV001Schema
specMarshal, err := json.Marshal(dsseObj.Spec)
fmt.Println(fmt.Sprintf("%s", specMarshal))
if err != nil {
return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err)
}
if err := json.Unmarshal(specMarshal, &dsseSchemaObj); err != nil {
return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err)
}
if len(env.Signatures) != len(dsseSchemaObj.Signatures) {
return fmt.Errorf("expected %d sigs in canonical body, got %d",
len(env.Signatures),
len(dsseSchemaObj.Signatures))
}
// TODO(#487): verify the certs match.
for _, sig := range env.Signatures {
var matchCanonical bool
for _, canonicalSig := range dsseSchemaObj.Signatures {
if *canonicalSig.Signature == sig.Sig {
matchCanonical = true
}
}
if !matchCanonical {
return ErrorMismatchSignature
}
}
return nil
}
// VerifyProvenanceBundle verifies the DSSE envelope using the offline Rekor bundle and
// returns the verified DSSE envelope containing the provenance
// and the signing certificate given the provenance.
func VerifyProvenanceBundle(ctx context.Context, bundleBytes []byte,
trustedRoot *sigstoreRoot.LiveTrustedRoot) (
*SignedAttestation, error,
) {
proposedSignedAtt, err := verifyBundleAndEntryFromBytes(ctx, bundleBytes, trustedRoot, true)
if err != nil {
return nil, err
}
if err := verifySignedAttestation(proposedSignedAtt, trustedRoot); err != nil {
return nil, err
}
return proposedSignedAtt, nil
}
// verifyBundleAndEntry validates the rekor entry inn the bundle
// and that the entry (cert, signatures) matches the data in the bundle.
func verifyBundleAndEntry(ctx context.Context, bundle *bundle_v1.Bundle,
trustedRoot *sigstoreRoot.LiveTrustedRoot, requireCert bool,
) (*SignedAttestation, error) {
// We only expect one TLOG entry. If this changes in the future, we must iterate
// for a matching one.
if bundle.GetVerificationMaterial() == nil ||
len(bundle.GetVerificationMaterial().GetTlogEntries()) == 0 {
return nil, fmt.Errorf("bundle missing offline tlog verification material %d", len(bundle.GetVerificationMaterial().GetTlogEntries()))
}
// Verify tlog entry.
tlogEntry := bundle.GetVerificationMaterial().GetTlogEntries()[0]
rekorEntry, err := verifyRekorEntryFromBundle(ctx, tlogEntry, trustedRoot)
if err != nil {
return nil, err
}
// Extract the PublicKey
publicKey := bundle.GetVerificationMaterial().GetPublicKey()
// Extract DSSE envelope.
env, err := getEnvelopeFromBundle(bundle)
if err != nil {
return nil, err
}
// Match tlog entry signature with the envelope.
if err := matchRekorEntryWithEnvelope(tlogEntry, env); err != nil {
return nil, fmt.Errorf("matching bundle entry with content: %w", err)
}
// Get certificate from bundle.
var cert *x509.Certificate
if requireCert {
cert, err = getLeafCertFromBundle(bundle)
if err != nil {
return nil, err
}
}
return &SignedAttestation{
SigningCert: cert,
PublicKey: publicKey,
Envelope: env,
RekorEntry: rekorEntry,
}, nil
}
// verifyBundleAndEntryFromBytes validates the rekor entry inn the bundle
// and that the entry (cert, signatures) matches the data in the bundle.
func verifyBundleAndEntryFromBytes(ctx context.Context, bundleBytes []byte,
trustedRoot *sigstoreRoot.LiveTrustedRoot, requireCert bool,
) (*SignedAttestation, error) {
// Extract the SigningCert, Envelope, and RekorEntry from the bundle.
var bundle bundle_v1.Bundle
if err := protojson.Unmarshal(bundleBytes, &bundle); err != nil {
return nil, fmt.Errorf("unmarshaling bundle: %w", err)
}
return verifyBundleAndEntry(ctx, &bundle,
trustedRoot, requireCert)
}