diff --git a/verifiers/internal/gha/provenance.go b/verifiers/internal/gha/provenance.go index ab26ec885..009851049 100644 --- a/verifiers/internal/gha/provenance.go +++ b/verifiers/internal/gha/provenance.go @@ -15,6 +15,7 @@ import ( dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/rekor/pkg/generated/client" + "github.com/slsa-framework/slsa-github-generator/signing/envelope" serrors "github.com/slsa-framework/slsa-verifier/errors" "github.com/slsa-framework/slsa-verifier/options" ) @@ -150,27 +151,33 @@ func verifySha256Digest(prov *intoto.ProvenanceStatement, expectedHash string) e // VerifyProvenanceSignature returns the verified DSSE envelope containing the provenance // and the signing certificate given the provenance and artifact hash. -func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor, provenance []byte, artifactHash string) (*dsselib.Envelope, *x509.Certificate, error) { - // Get Rekor entries corresponding to provenance - env, cert, err := GetRekorEntriesWithCert(rClient, provenance) - if err == nil { - return env, cert, nil +func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor, + provenance []byte, artifactHash string) ( + *dsselib.Envelope, *x509.Certificate, error) { + // There are two cases, either we have an embedded certificate, or we need + // to use the Redis index for searching by artifact SHA. + var err error + if hasCertInEnvelope(provenance) { + // Get Rekor entries corresponding to provenance + return GetRekorEntriesWithCert(rClient, provenance) } // Fallback on using the redis search index to get matching UUIDs. - fmt.Fprintf(os.Stderr, "Getting rekor entry error %s, trying Redis search index to find entries by subject digest\n", err) + fmt.Fprintf(os.Stderr, "No certificate provided, trying Redis search index to find entries by subject digest\n") + + // Search redis index for matching UUIDs. uuids, err := GetRekorEntries(rClient, artifactHash) if err != nil { return nil, nil, err } - env, err = EnvelopeFromBytes(provenance) + env, err := EnvelopeFromBytes(provenance) if err != nil { return nil, nil, err } // Verify the provenance and return the signing certificate. - cert, err = FindSigningCertificate(ctx, uuids, *env, rClient) + cert, err := FindSigningCertificate(ctx, uuids, *env, rClient) if err != nil { return nil, nil, err } @@ -556,3 +563,10 @@ func getBranch(prov *intoto.ProvenanceStatement) (string, error) { "unknown ref type", refType) } } + +// hasCertInEnvelope checks if a valid x509 certificate is present in the +// envelope. +func hasCertInEnvelope(provenance []byte) bool { + _, err := envelope.GetCertFromEnvelope(provenance) + return err == nil +} diff --git a/verifiers/internal/gha/rekor.go b/verifiers/internal/gha/rekor.go index 0a9514a96..9d4de1840 100644 --- a/verifiers/internal/gha/rekor.go +++ b/verifiers/internal/gha/rekor.go @@ -4,10 +4,8 @@ import ( "bytes" "context" "crypto" - "crypto/ecdsa" "crypto/x509" "encoding/base64" - "encoding/hex" "errors" "fmt" "os" @@ -21,22 +19,18 @@ import ( dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/entries" "github.com/sigstore/rekor/pkg/generated/client/index" - "github.com/sigstore/rekor/pkg/generated/client/tlog" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/sharding" "github.com/sigstore/rekor/pkg/types" intotod "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" - "github.com/sigstore/rekor/pkg/util" + rverify "github.com/sigstore/rekor/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" "github.com/slsa-framework/slsa-github-generator/signing/envelope" - "github.com/transparency-dev/merkle/proof" - "github.com/transparency-dev/merkle/rfc6962" serrors "github.com/slsa-framework/slsa-verifier/errors" ) @@ -45,75 +39,8 @@ const ( defaultRekorAddr = "https://rekor.sigstore.dev" ) -func verifyRootHash(ctx context.Context, rekorClient *client.Rekor, - treeID int64, eproof *models.InclusionProof, pub *ecdsa.PublicKey) error { - treeIDString := fmt.Sprintf("%d", treeID) - infoParams := tlog.NewGetLogInfoParamsWithContext(ctx) - result, err := rekorClient.Tlog.GetLogInfo(infoParams) - if err != nil { - return err - } - - logInfo := result.GetPayload() - - sth := util.SignedCheckpoint{} - if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { - return err - } - for _, inactiveShard := range logInfo.InactiveShards { - if *inactiveShard.TreeID == treeIDString { - if err := sth.UnmarshalText([]byte(*inactiveShard.SignedTreeHead)); err != nil { - return err - } - } - } - - verifier, err := signature.LoadVerifier(pub, crypto.SHA256) - if err != nil { - return err - } - - if !sth.Verify(verifier) { - return errors.New("signature on tree head did not verify") - } - - rootHash, err := hex.DecodeString(*eproof.RootHash) - if err != nil { - return errors.New("error decoding root hash in inclusion proof") - } - - if *eproof.TreeSize == int64(sth.Size) { - if !bytes.Equal(rootHash, sth.Hash) { - return errors.New("root hash returned from server does not match inclusion proof hash") - } - } else if *eproof.TreeSize < int64(sth.Size) { - consistencyParams := tlog.NewGetLogProofParamsWithContext(ctx) - consistencyParams.FirstSize = eproof.TreeSize // Root hash at the time the proof was returned - consistencyParams.LastSize = int64(sth.Size) // Root hash verified with rekor pubkey - - consistencyProof, err := rekorClient.Tlog.GetLogProof(consistencyParams) - if err != nil { - return err - } - var hashes [][]byte - for _, h := range consistencyProof.Payload.Hashes { - b, err := hex.DecodeString(h) - if err != nil { - return errors.New("error decoding consistency proof hashes") - } - hashes = append(hashes, b) - } - if err := proof.VerifyConsistency(rfc6962.DefaultHasher, - uint64(*eproof.TreeSize), sth.Size, hashes, rootHash, sth.Hash); err != nil { - return err - } - } else if *eproof.TreeSize > int64(sth.Size) { - return errors.New("inclusion proof returned a tree size larger than the verified tree size") - } - return nil -} - -func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, entryUUID string) (*models.LogEntryAnon, error) { +func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, entryUUID string) ( + *models.LogEntryAnon, error) { params := entries.NewGetLogEntryByUUIDParamsWithContext(ctx) params.EntryUUID = entryUUID @@ -140,89 +67,33 @@ func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, entry if returnUUID != uuid { return nil, errors.New("expected matching UUID") } - return verifyTlogEntry(ctx, rekorClient, k, entry) + // Validate the entry response. + return verifyTlogEntry(ctx, rekorClient, entry) } return nil, serrors.ErrorRekorSearch } -func verifyTlogEntry(ctx context.Context, rekorClient *client.Rekor, - entryUUID string, e models.LogEntryAnon) (*models.LogEntryAnon, error) { - if e.Verification == nil || e.Verification.InclusionProof == nil { - return nil, errors.New("inclusion proof not provided") - } - - uuid, err := sharding.GetUUIDFromIDString(entryUUID) - if err != nil { - return nil, fmt.Errorf("%w: retrieving uuid from entry uuid", err) - } - treeID, err := sharding.TreeID(entryUUID) - if err != nil { - return nil, fmt.Errorf("%w: retrieving tree ID", err) - } - - var hashes [][]byte - for _, h := range e.Verification.InclusionProof.Hashes { - hb, err := hex.DecodeString(h) - if err != nil { - return nil, errors.New("error decoding inclusion proof hashes") - } - hashes = append(hashes, hb) - } - - rootHash, err := hex.DecodeString(*e.Verification.InclusionProof.RootHash) - if err != nil { - return nil, errors.New("error decoding hex encoded root hash") - } - leafHash, err := hex.DecodeString(uuid) - if err != nil { - return nil, errors.New("error decoding hex encoded leaf hash") - } - +func verifyTlogEntry(ctx context.Context, rekorClient *client.Rekor, e models.LogEntryAnon) ( + *models.LogEntryAnon, error) { // Verify the root hash against the current Signed Entry Tree Head pubs, err := cosign.GetRekorPubs(ctx, nil) if err != nil { return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository") } - var entryVerError error - for _, pubKey := range pubs { - // Verify inclusion against the signed tree head - entryVerError = verifyRootHash(ctx, rekorClient, treeID, - e.Verification.InclusionProof, pubKey.PubKey) - if entryVerError == nil { - break - } - } - if entryVerError != nil { - return nil, fmt.Errorf("%w: %s", entryVerError, "error verifying root hash") - } - - // Verify the entry's inclusion - if err := proof.VerifyInclusion(rfc6962.DefaultHasher, - uint64(*e.Verification.InclusionProof.LogIndex), - uint64(*e.Verification.InclusionProof.TreeSize), leafHash, hashes, rootHash); err != nil { - return nil, fmt.Errorf("%w: %s", err, "verifying inclusion proof") - } - - // Verify rekor's signature over the SET. - payload := bundle.RekorPayload{ - Body: e.Body, - IntegratedTime: *e.IntegratedTime, - LogIndex: *e.LogIndex, - LogID: *e.LogID, + verifier, err := signature.LoadECDSAVerifier(pubs[*e.LogID].PubKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository") } - var setVerError error - for _, pubKey := range pubs { - setVerError = cosign.VerifySET(payload, e.Verification.SignedEntryTimestamp, pubKey.PubKey) - // Return once the SET is verified successfully. - if setVerError == nil { - break - } + // This function verifies the inclusion proof, the signature on the root hash of the + // inclusion proof, and the SignedEntryTimestamp. + if err := rverify.VerifyLogEntry(ctx, &e, verifier); err != nil { + return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository") } - return &e, setVerError + return &e, nil } func extractCert(e *models.LogEntryAnon) (*x509.Certificate, error) { @@ -334,25 +205,36 @@ func GetRekorEntriesWithCert(rClient *client.Rekor, provenance []byte) (*dsselib } logEntry := resp.Payload[0] + var ts time.Time for uuid, e := range logEntry { - if _, err := verifyTlogEntry(context.Background(), rClient, uuid, e); err != nil { + if _, err := verifyTlogEntry(context.Background(), rClient, e); err != nil { return nil, nil, fmt.Errorf("error verifying tlog entry: %w", err) } + ts = time.Unix(*e.IntegratedTime, 0) url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", uuid) fmt.Fprintf(os.Stderr, "Verified signature against tlog entry index %d at URL: %s\n", *e.LogIndex, url) } + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPem) + if err != nil { + return nil, nil, err + } + if len(certs) != 1 { + return nil, nil, fmt.Errorf("error unmarshaling certificate from pem") + } + env, err := EnvelopeFromBytes(provenance) if err != nil { return nil, nil, err } - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPem) + attBytes, err := cjson.MarshalCanonical(env) if err != nil { return nil, nil, err } - if len(certs) != 1 { - return nil, nil, fmt.Errorf("error unmarshaling certificate from pem") + + if err := verifyAttestationSignature(certs[0], ts, attBytes); err != nil { + return nil, nil, err } return env, certs[0], nil @@ -379,46 +261,59 @@ func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dss errs = append(errs, fmt.Sprintf("%s: verifying tlog entry %s", err, uuid)) continue } + + // Check the signature validity using the certificate in the entry. + it := time.Unix(*entry.IntegratedTime, 0) cert, err := extractCert(entry) if err != nil { // this is unexpected, hold on to this error. errs = append(errs, fmt.Sprintf("%s: extracting certificate from %s", err, uuid)) continue } - - roots, err := fulcio.GetRoots() - if err != nil { - // this is unexpected, hold on to this error. - errs = append(errs, fmt.Sprintf("%s: retrieving fulcio root", err)) - continue - } - co := &cosign.CheckOpts{ - RootCerts: roots, - CertOidcIssuer: certOidcIssuer, - } - verifier, err := cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - continue - } - verifier = dsse.WrapVerifier(verifier) - if err := verifier.VerifySignature(bytes.NewReader(attBytes), bytes.NewReader(attBytes)); err != nil { - continue - } - it := time.Unix(*entry.IntegratedTime, 0) - if err := cosign.CheckExpiry(cert, it); err != nil { - continue - } - uuid, err := cosign.ComputeLeafHash(entry) - if err != nil { - fmt.Fprintf(os.Stderr, "Error computing leaf hash for tlog entry at index: %d\n", *entry.LogIndex) + err = verifyAttestationSignature(cert, it, attBytes) + if errors.Is(err, serrors.ErrorInternal) { + // Return on an internal error + return nil, err + } else if err != nil { + errs = append(errs, err.Error()) continue } // success! - url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", hex.EncodeToString(uuid)) + url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", uuid) fmt.Fprintf(os.Stderr, "Verified signature against tlog entry index %d at URL: %s\n", *entry.LogIndex, url) return cert, nil } return nil, fmt.Errorf("%w: got unexpected errors %s", serrors.ErrorNoValidRekorEntries, strings.Join(errs, ", ")) } + +func verifyAttestationSignature(cert *x509.Certificate, ts time.Time, attBytes []byte) error { + roots, err := fulcio.GetRoots() + if err != nil { + // this is unexpected, hold on to this error. + return fmt.Errorf("%w: %s", serrors.ErrorInternal, err) + } + intermediates, err := fulcio.GetIntermediates() + if err != nil { + // this is unexpected, hold on to this error. + return fmt.Errorf("%w: %s", serrors.ErrorInternal, err) + } + co := &cosign.CheckOpts{ + RootCerts: roots, + IntermediateCerts: intermediates, + CertOidcIssuer: certOidcIssuer, + } + verifier, err := cosign.ValidateAndUnpackCert(cert, co) + if err != nil { + return fmt.Errorf("%w: %s", serrors.ErrorInvalidSignature, err) + } + verifier = dsse.WrapVerifier(verifier) + if err := verifier.VerifySignature(bytes.NewReader(attBytes), bytes.NewReader(attBytes)); err != nil { + return fmt.Errorf("%w: %s", serrors.ErrorInvalidSignature, err) + } + if err := cosign.CheckExpiry(cert, ts); err != nil { + return fmt.Errorf("%w: %s", serrors.ErrorInvalidSignature, err) + } + return nil +} diff --git a/verifiers/internal/gha/verifier.go b/verifiers/internal/gha/verifier.go index 49f7274be..a2286294a 100644 --- a/verifiers/internal/gha/verifier.go +++ b/verifiers/internal/gha/verifier.go @@ -92,6 +92,7 @@ func (v *GHAVerifier) VerifyArtifact(ctx context.Context, } /* Verify signature on the intoto attestation. */ + // TODO: We will also need to support bundles when those are complete. env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash) if err != nil { return nil, nil, err