diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 4754072c325..b3123b9950d 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -19,7 +19,7 @@ import ( "bytes" "context" "crypto" - _ "crypto/sha256" // for `crypto.SHA256` + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" @@ -28,6 +28,7 @@ import ( "fmt" "io" "os" + "strings" "time" "github.com/go-openapi/runtime" @@ -37,6 +38,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/pkg/blob" "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/cosign/pivkey" "github.com/sigstore/cosign/pkg/cosign/pkcs11key" sigs "github.com/sigstore/cosign/pkg/signature" @@ -45,11 +47,15 @@ import ( ctypes "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/pki" "github.com/sigstore/rekor/pkg/types" - hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" - rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" + "github.com/sigstore/rekor/pkg/types/hashedrekord" + hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" + "github.com/sigstore/rekor/pkg/types/intoto" + intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" + "github.com/sigstore/rekor/pkg/types/rekord" + rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) @@ -65,14 +71,13 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, certGithubWorkflowName, certGithubWorkflowRepository, certGithubWorkflowRef string, enforceSCT bool) error { - var verifier signature.Verifier var cert *x509.Certificate if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() && ko.BundlePath == "" { return &options.PubKeyParseError{} } - sig, b64sig, err := signatures(sigRef, ko.BundlePath) + sig, err := signatures(sigRef, ko.BundlePath) if err != nil { return err } @@ -82,14 +87,44 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, return err } + co := &cosign.CheckOpts{ + CertEmail: certEmail, + CertOidcIssuer: certOidcIssuer, + CertGithubWorkflowTrigger: certGithubWorkflowTrigger, + CertGithubWorkflowSha: certGithubWorkflowSha, + CertGithubWorkflowName: certGithubWorkflowName, + CertGithubWorkflowRepository: certGithubWorkflowRepository, + CertGithubWorkflowRef: certGithubWorkflowRef, + EnforceSCT: enforceSCT, + } + if options.EnableExperimental() { + if ko.RekorURL != "" { + rekorClient, err := rekor.NewClient(ko.RekorURL) + if err != nil { + return fmt.Errorf("creating Rekor client: %w", err) + } + co.RekorClient = rekorClient + } + } + if certRef == "" || options.EnableExperimental() { + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) + } + } + // Keys are optional! switch { case ko.KeyRef != "": - verifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef) + co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef) if err != nil { return fmt.Errorf("loading public key: %w", err) } - pkcs11Key, ok := verifier.(*pkcs11key.Key) + pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) if ok { defer pkcs11Key.Close() } @@ -99,7 +134,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, return fmt.Errorf("opening piv token: %w", err) } defer sk.Close() - verifier, err = sk.Verifier() + co.SigVerifier, err = sk.Verifier() if err != nil { return fmt.Errorf("loading public key from token: %w", err) } @@ -108,27 +143,8 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, if err != nil { return err } - co := &cosign.CheckOpts{ - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, - CertGithubWorkflowTrigger: certGithubWorkflowTrigger, - CertGithubWorkflowSha: certGithubWorkflowSha, - CertGithubWorkflowName: certGithubWorkflowName, - CertGithubWorkflowRepository: certGithubWorkflowRepository, - CertGithubWorkflowRef: certGithubWorkflowRef, - EnforceSCT: enforceSCT, - } if certChain == "" { - // If no certChain is passed, the Fulcio root certificate will be used - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) - } - verifier, err = cosign.ValidateAndUnpackCert(cert, co) + co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) if err != nil { return err } @@ -138,7 +154,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, if err != nil { return err } - verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { return err } @@ -151,7 +167,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, if b.Cert == "" { return fmt.Errorf("bundle does not contain cert for verification, please provide public key") } - // cert can either be a cert or public key + // b.Cert can either be a certificate or public key certBytes := []byte(b.Cert) if isb64(certBytes) { certBytes, _ = base64.StdEncoding.DecodeString(b.Cert) @@ -159,20 +175,19 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, cert, err = loadCertFromPEM(certBytes) if err != nil { // check if cert is actually a public key - verifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) + co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) } else { - verifier, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256) + co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) + if err != nil { + return err + } } if err != nil { return err } + // No certificate is provided: search by artifact sha in the TLOG. case options.EnableExperimental(): - rClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return err - } - - uuids, err := cosign.FindTLogEntriesByPayload(ctx, rClient, blobBytes) + uuids, err := cosign.FindTLogEntriesByPayload(ctx, co.RekorClient, blobBytes) if err != nil { return err } @@ -180,21 +195,49 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, if len(uuids) == 0 { return errors.New("could not find a tlog entry for provided blob") } - return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT) - } - // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. - if isIntotoDSSE(blobBytes) { - verifier = dsse.WrapVerifier(verifier) - } + // Iterate through and try to find a matching Rekor entry. + // This does not support intoto properly! c/f extractCerts and + // the verifier. + for _, u := range uuids { + tlogEntry, err := cosign.GetTlogEntry(ctx, co.RekorClient, u) + if err != nil { + continue + } + + // Note that this will error out if the TLOG entry was signed with a + // raw public key. Again, using search on artifact sha is unreliable. + certs, err := extractCerts(tlogEntry) + if err != nil { + continue + } + + cert := certs[0] + co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) + if err != nil { + continue + } + + if err := verifyBlob(ctx, co, blobBytes, sig, cert, + ko.BundlePath, tlogEntry); err == nil { + // We found a succesful Rekor entry! + fmt.Fprintln(os.Stderr, "Verified OK") + return nil + } + } + + // No successful Rekor entry found. + fmt.Fprintln(os.Stderr, `WARNING: No valid entries were found in rekor to verify this blob. + +Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists. + +We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature].`) + return fmt.Errorf("could not find a valid tlog entry for provided blob, found %d invalid entries", len(uuids)) - // verify the signature - if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil { - return err } - // verify the rekor entry - if err := verifyRekorEntry(ctx, ko, nil, verifier, cert, b64sig, blobBytes); err != nil { + // Performs all blob verification. + if err := verifyBlob(ctx, co, blobBytes, sig, cert, ko.BundlePath, nil); err != nil { return err } @@ -202,69 +245,145 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, return nil } -func verifySigByUUID(ctx context.Context, ko options.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, - uuids []string, blobBytes []byte, enforceSCT bool) error { - var validSigExists bool - for _, u := range uuids { - tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u) +/* Verify Blob main entry point. This will perform the following: + 1. Verifies the signature on the blob using the provided verifier. + 2. Checks for transparency log entry presence: + a. Verifies the Rekor entry in the bundle, if provided. OR + b. If we don't have a Rekor entry retrieved via cert, do an online lookup (assuming + we are in experimental mode). + c. Uses the provided Rekor entry (may have been retrieved through Rekor SearchIndex) OR + 3. If a certificate is provided, check it's expiration. +*/ +// TODO: Make a version of this public. This could be VerifyBlobCmd, but we need to +// clean up the args into CheckOpts or use KeyOpts here to resolve different KeyOpts. +func verifyBlob(ctx context.Context, co *cosign.CheckOpts, + blobBytes []byte, sig string, cert *x509.Certificate, + bundlePath string, e *models.LogEntryAnon) error { + if cert != nil { + // This would have already be done in the main entrypoint, but do this for robustness. + var err error + co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) if err != nil { - continue + return fmt.Errorf("validating cert: %w", err) } + } - certs, err := extractCerts(tlogEntry) - if err != nil { - continue - } + // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. + // TODO: This verifier only supports verification of a single signer/signature on + // the envelope. Either have the verifier validate that only one signature exists, + // or use a multi-signature verifier. + if isIntotoDSSE(blobBytes) { + co.SigVerifier = dsse.WrapVerifier(co.SigVerifier) + } - co := &cosign.CheckOpts{ - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, - EnforceSCT: enforceSCT, - } + // 1. Verify the signature. + if err := co.SigVerifier.VerifySignature(strings.NewReader(sig), bytes.NewReader(blobBytes)); err != nil { + return err + } - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) + // This is the signature creation time. Without a transparency log entry timestamp, + // we can only use the current time as a bound. + var validityTime time.Time + // 2. Checks for transparency log entry presence: + switch { + // a. We have a local bundle. + case bundlePath != "": + var svBytes []byte + var err error + if cert != nil { + svBytes, err = cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return fmt.Errorf("marshalling cert: %w", err) + } + } else { + svBytes, err = sigs.PublicKeyPem(co.SigVerifier, signatureoptions.WithContext(ctx)) + if err != nil { + return fmt.Errorf("marshalling pubkey: %w", err) + } } - co.IntermediateCerts, err = fulcio.GetIntermediates() + bundle, err := verifyRekorBundle(ctx, bundlePath, co.RekorClient, blobBytes, sig, svBytes) if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) + // Return when the provided bundle fails verification. (Do not fallback). + return err + } + validityTime = time.Unix(bundle.IntegratedTime, 0) + fmt.Fprintf(os.Stderr, "tlog entry verified offline\n") + // b. We can make an online lookup to the transparency log since we don't have an entry. + case co.RekorClient != nil && e == nil: + var tlogFindErr error + if cert == nil { + pub, err := co.SigVerifier.PublicKey(co.PKOpts...) + if err != nil { + return err + } + e, tlogFindErr = tlogFindPublicKey(ctx, co.RekorClient, blobBytes, sig, pub) + } else { + e, tlogFindErr = tlogFindCertificate(ctx, co.RekorClient, blobBytes, sig, cert) + } + if tlogFindErr != nil { + // TODO: Think about whether we should break here. + // This is COSIGN_EXPERIMENTAL mode, but in the case where someone + // provided a public key or still-valid cert, + /// they don't need TLOG lookup for the timestamp. + fmt.Fprintf(os.Stderr, "could not find entry in tlog: %s", tlogFindErr) + return tlogFindErr + } + // Fallthrough here to verify the TLOG entry and compute the integrated time. + fallthrough + // We are provided a log entry, possibly from above, or search. + case e != nil: + if err := cosign.VerifyTLogEntry(ctx, co.RekorClient, e); err != nil { + return err } - cert := certs[0] - verifier, err := cosign.ValidateAndUnpackCert(cert, co) + uuid, err := cosign.ComputeLeafHash(e) if err != nil { - continue - } - // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. - if isIntotoDSSE(blobBytes) { - verifier = dsse.WrapVerifier(verifier) - } - // verify the signature - if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil { - continue + return err } - // verify the rekor entry - if err := verifyRekorEntry(ctx, ko, tlogEntry, verifier, cert, b64sig, blobBytes); err != nil { - continue - } - validSigExists = true + validityTime = time.Unix(*e.IntegratedTime, 0) + fmt.Fprintf(os.Stderr, "tlog entry verified with uuid: %s index: %d\n", hex.EncodeToString(uuid), *e.LogIndex) + // If we do not have access to a bundle, a Rekor entry, or the access to lookup, + // then we can only use the current time as the signature creation time to verify + // the signature was created when the certificate was valid. + default: + validityTime = time.Now() } - if !validSigExists { - fmt.Fprintln(os.Stderr, `WARNING: No valid entries were found in rekor to verify this blob. -Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists. + // 3. If a certificate is provided, check it's expiration. + if cert == nil { + return nil + } -We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature].`) - return fmt.Errorf("could not find a valid tlog entry for provided blob, found %d invalid entries", len(uuids)) + return cosign.CheckExpiry(cert, validityTime) +} + +func tlogFindPublicKey(ctx context.Context, rekorClient *client.Rekor, + blobBytes []byte, sig string, pub crypto.PublicKey) (*models.LogEntryAnon, error) { + pemBytes, err := cryptoutils.MarshalPublicKeyToPEM(pub) + if err != nil { + return nil, err } - fmt.Fprintln(os.Stderr, "Verified OK") - return nil + return tlogFindEntry(ctx, rekorClient, blobBytes, sig, pemBytes) +} + +func tlogFindCertificate(ctx context.Context, rekorClient *client.Rekor, + blobBytes []byte, sig string, cert *x509.Certificate) (*models.LogEntryAnon, error) { + pemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return nil, err + } + return tlogFindEntry(ctx, rekorClient, blobBytes, sig, pemBytes) } -// signatures returns the raw signature and the base64 encoded signature -func signatures(sigRef string, bundlePath string) (string, string, error) { +func tlogFindEntry(ctx context.Context, client *client.Rekor, + blobBytes []byte, sig string, pem []byte) (*models.LogEntryAnon, error) { + b64sig := base64.StdEncoding.EncodeToString([]byte(sig)) + return cosign.FindTlogEntry(ctx, client, b64sig, blobBytes, pem) +} + +// signatures returns the raw signature +func signatures(sigRef string, bundlePath string) (string, error) { var targetSig []byte var err error switch { @@ -273,18 +392,18 @@ func signatures(sigRef string, bundlePath string) (string, string, error) { if err != nil { if !os.IsNotExist(err) { // ignore if file does not exist, it can be a base64 encoded string as well - return "", "", err + return "", err } targetSig = []byte(sigRef) } case bundlePath != "": b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) if err != nil { - return "", "", err + return "", err } targetSig = []byte(b.Base64Signature) default: - return "", "", fmt.Errorf("missing flag '--signature'") + return "", fmt.Errorf("missing flag '--signature'") } var sig, b64sig string @@ -296,7 +415,7 @@ func signatures(sigRef string, bundlePath string) (string, string, error) { sig = string(targetSig) b64sig = base64.StdEncoding.EncodeToString(targetSig) } - return sig, b64sig, nil + return sig, nil } func payloadBytes(blobRef string) ([]byte, error) { @@ -313,119 +432,176 @@ func payloadBytes(blobRef string) ([]byte, error) { return blobBytes, nil } -func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { - // TODO: This can be moved below offline bundle verification when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY - // is removed. - rekorClient, err := rekor.NewClient(ko.RekorURL) +// TODO: RekorClient can be removed when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY +// is removed. +func verifyRekorBundle(ctx context.Context, bundlePath string, rekorClient *client.Rekor, + blobBytes []byte, sig string, pubKeyBytes []byte) (*bundle.RekorPayload, error) { + b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) if err != nil { - return err - } - - // If we have a bundle with a rekor entry, let's first try to verify offline - if ko.BundlePath != "" { - if err := verifyRekorBundle(ctx, ko.BundlePath, cert, rekorClient); err == nil { - fmt.Fprintf(os.Stderr, "tlog entry verified offline\n") - return nil - } + return nil, err } - if !options.EnableExperimental() { - return nil + if b.Bundle == nil { + return nil, fmt.Errorf("rekor entry could not be extracted from local bundle") } - // Only fetch from rekor tlog if we don't already have the entry. - if e == nil { - var pubBytes []byte - if pubKey != nil { - pubBytes, err = sigs.PublicKeyPem(pubKey, signatureoptions.WithContext(ctx)) - if err != nil { - return err - } - } - if cert != nil { - pubBytes, err = cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return err - } - } - e, err = cosign.FindTlogEntry(ctx, rekorClient, b64sig, blobBytes, pubBytes) - if err != nil { - return err - } + if err := verifyBundleMatchesData(ctx, b.Bundle, blobBytes, pubKeyBytes, []byte(sig)); err != nil { + return nil, err } - if err := cosign.VerifyTLogEntry(ctx, rekorClient, e); err != nil { - return nil + publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient) + if err != nil { + return nil, fmt.Errorf("retrieving rekor public key: %w", err) } - uuid, err := cosign.ComputeLeafHash(e) + pubKey, ok := publicKeys[b.Bundle.Payload.LogID] + if !ok { + return nil, errors.New("rekor log public key not found for payload") + } + err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) if err != nil { - return err + return nil, err } - - fmt.Fprintf(os.Stderr, "tlog entry verified with uuid: %s index: %d\n", hex.EncodeToString(uuid), *e.LogIndex) - if cert == nil { - return nil + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } - // if we have a cert, we should check expiry - return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0)) + + return &b.Bundle.Payload, nil } -func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate, rekorClient *client.Rekor) error { - b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) +func verifyBundleMatchesData(ctx context.Context, bundle *bundle.RekorBundle, blobBytes, certBytes, sigBytes []byte) error { + eimpl, kind, apiVersion, err := unmarshalEntryImpl(bundle.Payload.Body.(string)) if err != nil { return err } - if b.Bundle == nil { - return fmt.Errorf("rekor entry is not available") - } - publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient) + + targetImpl, err := reconstructCanonicalizedEntry(ctx, kind, apiVersion, blobBytes, certBytes, sigBytes) if err != nil { - return fmt.Errorf("retrieving rekor public key: %w", err) + return fmt.Errorf("recontructing rekorEntry for bundle comparison: %w", err) } - pubKey, ok := publicKeys[b.Bundle.Payload.LogID] - if !ok { - return errors.New("rekor log public key not found for payload") + switch e := eimpl.(type) { + case *rekord_v001.V001Entry: + t := targetImpl.(*rekord_v001.V001Entry) + data, err := e.RekordObj.Data.Content.MarshalText() + if err != nil { + return fmt.Errorf("invalid rekord data: %w", err) + } + tData, err := t.RekordObj.Data.Content.MarshalText() + if err != nil { + return fmt.Errorf("invalid rekord data: %w", err) + } + if !bytes.Equal(data, tData) { + return fmt.Errorf("rekord data does not match blob") + } + if err := compareBase64Strings(e.RekordObj.Signature.Content.String(), + t.RekordObj.Signature.Content.String()); err != nil { + return fmt.Errorf("rekord signature does not match bundle %s", err) + } + if err := compareBase64Strings(e.RekordObj.Signature.PublicKey.Content.String(), + t.RekordObj.Signature.PublicKey.Content.String()); err != nil { + return fmt.Errorf("rekord public key does not match bundle") + } + case *hashedrekord_v001.V001Entry: + t := targetImpl.(*hashedrekord_v001.V001Entry) + if *e.HashedRekordObj.Data.Hash.Value != *t.HashedRekordObj.Data.Hash.Value { + return fmt.Errorf("hashedRekord data does not match blob") + } + if err := compareBase64Strings(e.HashedRekordObj.Signature.Content.String(), + t.HashedRekordObj.Signature.Content.String()); err != nil { + return fmt.Errorf("hashedRekord signature does not match bundle %s", err) + } + if err := compareBase64Strings(e.HashedRekordObj.Signature.PublicKey.Content.String(), + t.HashedRekordObj.Signature.PublicKey.Content.String()); err != nil { + return fmt.Errorf("hashedRekord public key does not match bundle") + } + case *intoto_v001.V001Entry: + t := targetImpl.(*intoto_v001.V001Entry) + if *e.IntotoObj.Content.Hash.Value != *t.IntotoObj.Content.Hash.Value { + return fmt.Errorf("intoto content hash does not match attestation") + } + if *e.IntotoObj.Content.PayloadHash.Value != *t.IntotoObj.Content.PayloadHash.Value { + return fmt.Errorf("intoto payload hash does not match attestation") + } + if err := compareBase64Strings(e.IntotoObj.PublicKey.String(), + t.IntotoObj.PublicKey.String()); err != nil { + return fmt.Errorf("intoto public key does not match bundle") + } + default: + return errors.New("unexpected tlog entry type") } - err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) + return nil +} + +func reconstructCanonicalizedEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) { + props := types.ArtifactProperties{ + PublicKeyBytes: certBytes, + PKIFormat: string(pki.X509), + } + switch kind { + case rekord.KIND: + props.ArtifactBytes = blobBytes + props.SignatureBytes = sigBytes + case hashedrekord.KIND: + blobHash := sha256.Sum256(blobBytes) + props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:])) + props.SignatureBytes = sigBytes + case intoto.KIND: + props.ArtifactBytes = blobBytes + default: + return nil, fmt.Errorf("unexpected entry kind: %s", kind) + } + proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props) if err != nil { - return err + return nil, err } - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") + entry, err := types.NewEntry(proposedEntry) + if err != nil { + return nil, err } - - if cert == nil { - return nil + can, err := entry.Canonicalize(ctx) + if err != nil { + return nil, err + } + proposedEntryCan, err := models.UnmarshalProposedEntry(bytes.NewReader(can), runtime.JSONConsumer()) + if err != nil { + return nil, err } - it := time.Unix(b.Bundle.Payload.IntegratedTime, 0) - return cosign.CheckExpiry(cert, it) + return types.NewEntry(proposedEntryCan) } -func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) { - b, err := base64.StdEncoding.DecodeString(e.Body.(string)) +// unmarshalEntryImpl decodes the base64-encoded entry to a specific entry type (types.EntryImpl). +func unmarshalEntryImpl(e string) (types.EntryImpl, string, string, error) { + b, err := base64.StdEncoding.DecodeString(e) if err != nil { - return nil, err + return nil, "", "", err } pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) if err != nil { - return nil, err + return nil, "", "", err + } + + entry, err := types.NewEntry(pe) + if err != nil { + return nil, "", "", err } + return entry, pe.Kind(), entry.APIVersion(), nil +} - eimpl, err := types.NewEntry(pe) +func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) { + eimpl, _, _, err := unmarshalEntryImpl(e.Body.(string)) if err != nil { return nil, err } var publicKeyB64 []byte switch e := eimpl.(type) { - case *rekord.V001Entry: + case *rekord_v001.V001Entry: publicKeyB64, err = e.RekordObj.Signature.PublicKey.Content.MarshalText() if err != nil { return nil, err } - case *hashedrekord.V001Entry: + case *hashedrekord_v001.V001Entry: publicKeyB64, err = e.HashedRekordObj.Signature.PublicKey.Content.MarshalText() if err != nil { return nil, err @@ -463,3 +639,19 @@ func isIntotoDSSE(blobBytes []byte) bool { return true } + +// TODO: Use this function to compare bundle signatures in OCI. +func compareBase64Strings(got string, expected string) error { + decodeFirst, err := base64.StdEncoding.DecodeString(got) + if err != nil { + return fmt.Errorf("decoding base64 string %s", got) + } + decodeSecond, err := base64.StdEncoding.DecodeString(expected) + if err != nil { + return fmt.Errorf("decoding base64 string %s", expected) + } + if !bytes.Equal(decodeFirst, decodeSecond) { + return fmt.Errorf("comparing base64 strings, expected %s, got %s", expected, got) + } + return nil +} diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index b6aa05f3e03..47872e60796 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -15,14 +15,46 @@ package verify import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/json" + "fmt" "os" "path/filepath" + "strings" "testing" + "time" - "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" + "github.com/go-openapi/swag" + ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/internal/pkg/cosign/rekor/mock" "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/bundle" + sigs "github.com/sigstore/cosign/pkg/signature" + ctypes "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/cosign/test" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/pki" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/rekor/pkg/types/hashedrekord" + hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" + "github.com/sigstore/rekor/pkg/types/intoto" + "github.com/sigstore/rekor/pkg/types/rekord" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/dsse" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) func TestSignaturesRef(t *testing.T) { @@ -48,7 +80,7 @@ func TestSignaturesRef(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - gotSig, gotb64Sig, err := signatures(test.sigRef, "") + gotSig, err := signatures(test.sigRef, "") if test.shouldErr && err != nil { return } @@ -58,9 +90,6 @@ func TestSignaturesRef(t *testing.T) { if gotSig != sig { t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig) } - if gotb64Sig != b64sig { - t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig) - } }) } } @@ -84,28 +113,25 @@ func TestSignaturesBundle(t *testing.T) { t.Fatal(err) } - gotSig, gotb64Sig, err := signatures("", fp) + gotSig, err := signatures("", fp) if err != nil { t.Fatal(err) } if gotSig != sig { t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig) } - if gotb64Sig != b64sig { - t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig) - } } func TestIsIntotoDSSEWithEnvelopes(t *testing.T) { tts := []struct { - envelope dsse.Envelope + envelope ssldsse.Envelope isIntotoDSSE bool }{ { - envelope: dsse.Envelope{ + envelope: ssldsse.Envelope{ PayloadType: "application/vnd.in-toto+json", Payload: base64.StdEncoding.EncodeToString([]byte("This is a test")), - Signatures: []dsse.Signature{}, + Signatures: []ssldsse.Signature{}, }, isIntotoDSSE: true, }, @@ -141,3 +167,950 @@ func TestIsIntotoDSSEWithBytes(t *testing.T) { } } } + +// Does not test identity options, only blob verification with different +// options. +func TestVerifyBlob(t *testing.T) { + ctx := context.Background() + td := t.TempDir() + + leafPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + signer, err := signature.LoadECDSASignerVerifier(leafPriv, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + pubKeyBytes, err := sigs.PublicKeyPem(signer, signatureoptions.WithContext(ctx)) + if err != nil { + t.Fatal(err) + } + + // Generate expired and unexpired certificates + identity := "hello@foo.com" + issuer := "issuer" + rootCert, rootPriv, _ := test.GenerateRootCa() + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + unexpiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer, + time.Now(), leafPriv, rootCert, rootPriv) + unexpiredCertPem, _ := cryptoutils.MarshalCertificateToPEM(unexpiredLeafCert) + + expiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer, + time.Now().Add(-time.Hour), leafPriv, rootCert, rootPriv) + expiredLeafPem, _ := cryptoutils.MarshalCertificateToPEM(expiredLeafCert) + + // Make rekor signer + rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + rekorSigner, err := signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorSigner.Public()) + if err != nil { + t.Fatal(err) + } + tmpRekorPubFile, err := os.CreateTemp(td, "cosign_rekor_pub_*.key") + if err != nil { + t.Fatalf("failed to create temp rekor pub file: %v", err) + } + defer tmpRekorPubFile.Close() + if _, err := tmpRekorPubFile.Write(pemRekor); err != nil { + t.Fatalf("failed to write rekor pub file: %v", err) + } + t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name()) + + var makeSignature = func(blob []byte) string { + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + return string(sig) + } + blobBytes := []byte("foo") + blobSignature := makeSignature(blobBytes) + + otherBytes := []byte("bar") + otherSignature := makeSignature(otherBytes) + + tts := []struct { + name string + blob []byte + signature string + sigVerifier signature.Verifier + cert *x509.Certificate + bundlePath string + // If online lookups to Rekor are enabled + experimental bool + // The rekor entry response when Rekor is enabled + rekorEntry *models.LogEntry + shouldErr bool + }{ + { + name: "valid signature with public key", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + shouldErr: false, + }, + { + name: "valid signature with public key - experimental no rekor fail", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: true, + rekorEntry: nil, + shouldErr: true, + }, + { + name: "valid signature with public key - experimental rekor entry success", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: true, + rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), + pubKeyBytes, true), + shouldErr: false, + }, + { + name: "valid signature with public key - good bundle provided", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + pubKeyBytes, true), + shouldErr: false, + }, + { + name: "valid signature with public key - bad bundle SET", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), + unexpiredCertPem, true), + shouldErr: true, + }, + { + name: "valid signature with public key - bad bundle cert mismatch", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + unexpiredCertPem, true), + shouldErr: true, + }, + { + name: "valid signature with public key - bad bundle signature mismatch", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)), + pubKeyBytes, true), + shouldErr: true, + }, + { + name: "valid signature with public key - bad bundle msg & signature mismatch", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature), + pubKeyBytes, true), + shouldErr: true, + }, + { + name: "invalid signature with public key", + blob: blobBytes, + signature: otherSignature, + sigVerifier: signer, + experimental: false, + shouldErr: true, + }, + { + name: "invalid signature with public key - experimental", + blob: blobBytes, + signature: otherSignature, + sigVerifier: signer, + experimental: true, + shouldErr: true, + }, + { + name: "valid signature with unexpired certificate", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + cert: unexpiredLeafCert, + experimental: false, + shouldErr: false, + }, + { + name: "valid signature with unexpired certificate - bad bundle cert mismatch", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + cert: unexpiredLeafCert, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + pubKeyBytes, true), + shouldErr: true, + }, + { + name: "valid signature with unexpired certificate - bad bundle signature mismatch", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + cert: unexpiredLeafCert, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)), + unexpiredCertPem, true), + shouldErr: true, + }, + { + name: "valid signature with unexpired certificate - bad bundle msg & signature mismatch", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + experimental: false, + cert: unexpiredLeafCert, + bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature), + unexpiredCertPem, true), + shouldErr: true, + }, + { + name: "invalid signature with unexpired certificate", + blob: blobBytes, + signature: otherSignature, + sigVerifier: signer, + cert: unexpiredLeafCert, + experimental: false, + shouldErr: true, + }, + { + name: "valid signature with unexpired certificate - experimental", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, + sigVerifier: signer, + experimental: true, + rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), + unexpiredCertPem, true), + shouldErr: false, + }, + + { + name: "valid signature with unexpired certificate - experimental & rekor entry found", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, + experimental: true, + rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), + unexpiredCertPem, true), + shouldErr: false, + }, + { + name: "valid signature with expired certificate", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, + sigVerifier: signer, + experimental: false, + shouldErr: true, + }, + + { + name: "valid signature with expired certificate - experimental good rekor lookup", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + cert: expiredLeafCert, + experimental: true, + rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), + expiredLeafPem, true), + shouldErr: false, + }, + + { + name: "valid signature with expired certificate - experimental bad rekor integrated time", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, + sigVerifier: signer, + experimental: true, + rekorEntry: makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), + expiredLeafPem, false), + shouldErr: true, + }, + + { + name: "valid signature with unexpired certificate - good bundle, nonexperimental", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + unexpiredCertPem, true), + shouldErr: false, + }, + { + name: "valid signature with expired certificate - good bundle, nonexperimental", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, + sigVerifier: signer, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + expiredLeafPem, true), + shouldErr: false, + }, + { + name: "valid signature with expired certificate - bundle with bad expiration", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + cert: expiredLeafCert, + experimental: false, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + expiredLeafPem, false), + shouldErr: true, + }, + { + name: "valid signature with expired certificate - bundle with bad SET", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + cert: expiredLeafCert, + experimental: false, + bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), + expiredLeafPem, true), + shouldErr: true, + }, + { + name: "valid signature with expired certificate - experimental good bundle", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + cert: expiredLeafCert, + experimental: true, + bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), + expiredLeafPem, true), + shouldErr: false, + }, + { + name: "valid signature with expired certificate - experimental bad rekor entry", + blob: blobBytes, + signature: blobSignature, + sigVerifier: signer, + cert: expiredLeafCert, + experimental: true, + // This is the wrong signer for the SET! + rekorEntry: makeRekorEntry(t, *signer, blobBytes, []byte(blobSignature), + expiredLeafPem, true), + shouldErr: true, + }, + } + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + tt := tt + var mClient client.Rekor + mClient.Entries = &mock.EntriesClient{Entries: tt.rekorEntry} + co := &cosign.CheckOpts{ + SigVerifier: tt.sigVerifier, + RootCerts: rootPool, + } + // if expermental is enabled, add RekorClient to co. + if tt.experimental { + co.RekorClient = &mClient + } + + err := verifyBlob(ctx, co, tt.blob, tt.signature, tt.cert, tt.bundlePath, nil) + if (err != nil) != tt.shouldErr { + t.Fatalf("verifyBlob()= %s, expected shouldErr=%t ", err, tt.shouldErr) + } + }) + } +} + +func makeRekorEntry(t *testing.T, rekorSigner signature.ECDSASignerVerifier, + pyld, sig, svBytes []byte, expiryValid bool) *models.LogEntry { + ctx := context.Background() + // Calculate log ID, the digest of the Rekor public key + logID, err := getLogID(rekorSigner.Public()) + if err != nil { + t.Fatal(err) + } + + hashedrekord := &hashedrekord_v001.V001Entry{} + h := sha256.Sum256(pyld) + pe, err := hashedrekord.CreateFromArtifactProperties(ctx, types.ArtifactProperties{ + ArtifactHash: hex.EncodeToString(h[:]), + SignatureBytes: sig, + PublicKeyBytes: svBytes, + PKIFormat: "x509", + }) + if err != nil { + t.Fatal(err) + } + entry, err := types.NewEntry(pe) + if err != nil { + t.Fatal(err) + } + leaf, err := entry.Canonicalize(ctx) + if err != nil { + t.Fatal(err) + } + + integratedTime := time.Now() + certs, _ := cryptoutils.UnmarshalCertificatesFromPEM(svBytes) + if certs != nil && len(certs) > 0 { + if expiryValid { + integratedTime = certs[0].NotAfter.Add(-time.Second) + } else { + integratedTime = certs[0].NotAfter.Add(time.Second) + } + } + e := models.LogEntryAnon{ + Body: base64.StdEncoding.EncodeToString(leaf), + IntegratedTime: swag.Int64(integratedTime.Unix()), + LogIndex: swag.Int64(0), + LogID: swag.String(logID), + } + // Marshal payload, sign, and set SET in Bundle + jsonPayload, err := json.Marshal(e) + if err != nil { + t.Fatal(err) + } + canonicalized, err := jsoncanonicalizer.Transform(jsonPayload) + if err != nil { + t.Fatal(err) + } + bundleSig, err := rekorSigner.SignMessage(bytes.NewReader(canonicalized)) + if err != nil { + t.Fatal(err) + } + uuid, _ := cosign.ComputeLeafHash(&e) + + e.Verification = &models.LogEntryAnonVerification{ + SignedEntryTimestamp: bundleSig, + InclusionProof: &models.InclusionProof{ + LogIndex: swag.Int64(0), + TreeSize: swag.Int64(1), + RootHash: swag.String(hex.EncodeToString(uuid)), + Hashes: []string{}, + }, + } + return &models.LogEntry{hex.EncodeToString(uuid): e} +} + +func makeLocalBundle(t *testing.T, rekorSigner signature.ECDSASignerVerifier, + pyld []byte, sig []byte, svBytes []byte, expiryValid bool) string { + td := t.TempDir() + + // Create bundle. + entry := makeRekorEntry(t, rekorSigner, pyld, sig, svBytes, expiryValid) + var e models.LogEntryAnon + for _, v := range *entry { + e = v + } + b := cosign.LocalSignedPayload{ + Base64Signature: base64.StdEncoding.EncodeToString(sig), + Cert: string(svBytes), + Bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: e.Body, + IntegratedTime: *e.IntegratedTime, + LogIndex: *e.LogIndex, + LogID: *e.LogID, + }, + SignedEntryTimestamp: e.Verification.SignedEntryTimestamp, + }, + } + + // Write bundle to disk + jsonBundle, err := json.Marshal(b) + if err != nil { + t.Fatal(err) + } + bundlePath := filepath.Join(td, "bundle.sig") + if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { + t.Fatal(err) + } + return bundlePath +} + +func TestVerifyBlobCmdWithBundle(t *testing.T) { + keyless := newKeylessStack(t) + + t.Run("Normal verification", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + // Create blob + blob := "someblob" + + // Sign blob with private key + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + // Create bundle + entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + identity, /*certEmail*/ + issuer, /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err != nil { + t.Fatal(err) + } + }) + t.Run("Mismatched cert/sig", func(t *testing.T) { + // This test ensures that the signature and cert at the top level in the LocalSignedPayload must be identical to the ones in the RekorBundle. + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + _, _, leafPemCert2, signer2 := keyless.genLeafCert(t, identity, issuer) + + // Create blob + blob := "someblob" + + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + sig2, err := signer2.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + // Create bundle + entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert2, sig2) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + "", /*certEmail*/ + "", /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err == nil { + t.Fatal("expecting err due to mismatched signatures, got nil") + } + }) + t.Run("Expired cert", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + // Create blob + blob := "someblob" + + // Sign blob with private key + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + // Create bundle + entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()-1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + "", /*certEmail*/ + "", /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err == nil { + t.Fatal("expected error due to expired cert, received nil") + } + }) + t.Run("Attestation", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}` + wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType) + signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background())) + if err != nil { + t.Fatal(err) + } + // intoto sig = json-serialized dsse envelope + sig := signedPayload + + // Create bundle + entry := genRekorEntry(t, intoto.KIND, intoto.New().DefaultVersion(), signedPayload, leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + "", /*certEmail*/ + "", /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err != nil { + t.Fatal(err) + } + }) + t.Run("Invalid blob signature", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + // Create blob + blob := "someblob" + + // Sign blob with private key + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + // Create bundle + entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = []byte{'i', 'n', 'v', 'a', 'l', 'i', 'd'} + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + "", /*certEmail*/ + "", /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err == nil || !strings.Contains(err.Error(), "unable to verify SET") { + t.Fatalf("expected error verifying SET, got %v", err) + } + }) + t.Run("Mismatched certificate email", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + // Create blob + blob := "someblob" + + // Sign blob with private key + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + // Create bundle + entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + "invalid@example.com", /*certEmail*/ + issuer, /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err == nil || !strings.Contains(err.Error(), "expected email not found in certificate") { + t.Fatalf("expected error with mismatched identity, got %v", err) + } + }) + t.Run("Mismatched certificate issuer", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + // Create blob + blob := "someblob" + + // Sign blob with private key + sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) + if err != nil { + t.Fatal(err) + } + + // Create bundle + entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") + + // Verify command + err = VerifyBlobCmd(context.Background(), + options.KeyOpts{BundlePath: bundlePath}, + "", /*certRef*/ // Cert is fetched from bundle + identity, /*certEmail*/ + "invalid", /*certOidcIssuer*/ + "", /*certChain*/ // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + "", /*sigRef*/ // Sig is fetched from bundle + blobPath, /*blobRef*/ + // GitHub identity flags start + "", "", "", "", "", + // GitHub identity flags end + false /*enforceSCT*/) + if err == nil || !strings.Contains(err.Error(), "expected oidc issuer not found in certificate") { + t.Fatalf("expected error with mismatched issuer, got %v", err) + } + }) +} + +type keylessStack struct { + rootCert *x509.Certificate + rootPriv *ecdsa.PrivateKey + rootPemCert []byte + subCert *x509.Certificate + subPriv *ecdsa.PrivateKey + subPemCert []byte + rekorSigner *signature.ECDSASignerVerifier + rekorLogID string + td string // temporary directory +} + +func newKeylessStack(t *testing.T) *keylessStack { + stack := &keylessStack{td: t.TempDir()} + stack.rootCert, stack.rootPriv, _ = test.GenerateRootCa() + stack.rootPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.rootCert) + stack.subCert, stack.subPriv, _ = test.GenerateSubordinateCa(stack.rootCert, stack.rootPriv) + stack.subPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.subCert) + + stack.genChainFile(t) + stack.genRekor(t) + return stack +} + +func (s *keylessStack) genLeafCert(t *testing.T, subject string, issuer string) (*x509.Certificate, *ecdsa.PrivateKey, []byte, *signature.ECDSASignerVerifier) { + cert, priv, _ := test.GenerateLeafCert(subject, issuer, s.subCert, s.subPriv) + pemCert, _ := cryptoutils.MarshalCertificateToPEM(cert) + signer, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + return cert, priv, pemCert, signer +} + +func (s *keylessStack) genChainFile(t *testing.T) { + var chain []byte + chain = append(chain, s.subPemCert...) + chain = append(chain, s.rootPemCert...) + tmpChainFile, err := os.CreateTemp(s.td, "cosign_fulcio_chain_*.cert") + if err != nil { + t.Fatalf("failed to create temp chain file: %v", err) + } + defer tmpChainFile.Close() + if _, err := tmpChainFile.Write(chain); err != nil { + t.Fatalf("failed to write chain file: %v", err) + } + // Override for Fulcio root so it doesn't use TUF + t.Setenv("SIGSTORE_ROOT_FILE", tmpChainFile.Name()) +} + +func (s *keylessStack) genRekor(t *testing.T) { + // Create Rekor private key and write to disk + rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + s.rekorSigner, err = signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + rekorPub := s.rekorSigner.Public() + pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorPub) + if err != nil { + t.Fatal(err) + } + tmpRekorPubFile, err := os.CreateTemp(s.td, "cosign_rekor_pub_*.key") + if err != nil { + t.Fatalf("failed to create temp rekor pub file: %v", err) + } + defer tmpRekorPubFile.Close() + if _, err := tmpRekorPubFile.Write(pemRekor); err != nil { + t.Fatalf("failed to write rekor pub file: %v", err) + } + + // Calculate log ID, the digest of the Rekor public key + s.rekorLogID, err = getLogID(rekorPub) + if err != nil { + t.Fatal(err) + } + // Override for Rekor public key so it doesn't use TUF + t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name()) +} +func (s *keylessStack) rekorSignPayload(t *testing.T, payload bundle.RekorPayload) []byte { + // Marshal payload, sign, and return SET + jsonPayload, err := json.Marshal(payload) + if err != nil { + t.Fatal(err) + } + canonicalized, err := jsoncanonicalizer.Transform(jsonPayload) + if err != nil { + t.Fatal(err) + } + bundleSig, err := s.rekorSigner.SignMessage(bytes.NewReader(canonicalized)) + if err != nil { + t.Fatal(err) + } + return bundleSig +} + +// getLogID calculates the digest of a PKIX-encoded public key +func getLogID(pub crypto.PublicKey) (string, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return "", err + } + digest := sha256.Sum256(pubBytes) + return hex.EncodeToString(digest[:]), nil +} + +func genRekorEntry(t *testing.T, kind, version string, artifact []byte, cert []byte, sig []byte) string { + // Generate the Rekor Entry + entryImpl, err := createEntry(context.Background(), kind, version, artifact, cert, sig) + if err != nil { + t.Fatal(err) + } + entryBytes, err := entryImpl.Canonicalize(context.Background()) + if err != nil { + t.Fatal(err) + } + return base64.StdEncoding.EncodeToString(entryBytes) +} + +func createBundle(t *testing.T, sig []byte, certPem []byte, logID string, integratedTime int64, rekorEntry string) *cosign.LocalSignedPayload { + // Create bundle with: + // * Blob signature + // * Signing certificate + // * Bundle with a payload and signature over the payload + b := &cosign.LocalSignedPayload{ + Base64Signature: base64.StdEncoding.EncodeToString(sig), + Cert: string(certPem), + Bundle: &bundle.RekorBundle{ + SignedEntryTimestamp: []byte{}, + Payload: bundle.RekorPayload{ + LogID: logID, + IntegratedTime: integratedTime, + LogIndex: 1, + Body: rekorEntry, + }, + }, + } + + return b +} + +func createEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) { + props := types.ArtifactProperties{ + PublicKeyBytes: certBytes, + PKIFormat: string(pki.X509), + } + switch kind { + case rekord.KIND: + props.ArtifactBytes = blobBytes + props.SignatureBytes = sigBytes + case hashedrekord.KIND: + blobHash := sha256.Sum256(blobBytes) + props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:])) + props.SignatureBytes = sigBytes + case intoto.KIND: + props.ArtifactBytes = blobBytes + default: + return nil, fmt.Errorf("unexpected entry kind: %s", kind) + } + proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props) + if err != nil { + return nil, err + } + return types.NewEntry(proposedEntry) +} + +func writeBundleFile(t *testing.T, td string, b *cosign.LocalSignedPayload, name string) string { + // Write bundle to disk + jsonBundle, err := json.Marshal(b) + if err != nil { + t.Fatal(err) + } + bundlePath := filepath.Join(td, name) + if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { + t.Fatal(err) + } + return bundlePath +} + +func writeBlobFile(t *testing.T, td string, blob string, name string) string { + // Write blob to disk + blobPath := filepath.Join(td, name) + if err := os.WriteFile(blobPath, []byte(blob), 0644); err != nil { + t.Fatal(err) + } + return blobPath +} diff --git a/internal/pkg/cosign/rekor/mock/mock_rekor_client.go b/internal/pkg/cosign/rekor/mock/mock_rekor_client.go index 2574dfb03d9..af229242321 100644 --- a/internal/pkg/cosign/rekor/mock/mock_rekor_client.go +++ b/internal/pkg/cosign/rekor/mock/mock_rekor_client.go @@ -15,81 +15,58 @@ package mock import ( - "encoding/base64" - "encoding/hex" + "errors" "github.com/go-openapi/runtime" - "github.com/transparency-dev/merkle/rfc6962" "github.com/sigstore/rekor/pkg/generated/client/entries" "github.com/sigstore/rekor/pkg/generated/models" ) -var ( - lea = models.LogEntryAnon{ - Attestation: &models.LogEntryAnonAttestation{}, - Body: base64.StdEncoding.EncodeToString([]byte("asdf")), - IntegratedTime: new(int64), - LogID: new(string), - LogIndex: new(int64), - Verification: &models.LogEntryAnonVerification{ - InclusionProof: &models.InclusionProof{ - RootHash: new(string), - TreeSize: new(int64), - LogIndex: new(int64), - }, - }, - } - data = models.LogEntry{ - uuid(lea): lea, - } -) - -// uuid generates the UUID for the given LogEntry. -// This is effectively a reimplementation of -// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated -// to avoid a circular dependency. -// TODO?: Perhaps we should refactor the tlog libraries into a separate -// package? -func uuid(e models.LogEntryAnon) string { - entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string)) - if err != nil { - panic(err) - } - return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes)) -} - // EntriesClient is a client that implements entries.ClientService for Rekor // To use: // var mClient client.Rekor -// mClient.entries = &EntriesClient{} +// mClient.Entries = &logEntry type EntriesClient struct { - Entries models.LogEntry + Entries *models.LogEntry } func (m *EntriesClient) CreateLogEntry(params *entries.CreateLogEntryParams, opts ...entries.ClientOption) (*entries.CreateLogEntryCreated, error) { - return &entries.CreateLogEntryCreated{ - ETag: "", - Location: "", - Payload: data, - }, nil + if m.Entries != nil { + return &entries.CreateLogEntryCreated{ + ETag: "", + Location: "", + Payload: *m.Entries, + }, nil + } + return nil, errors.New("entry not provided") } func (m *EntriesClient) GetLogEntryByIndex(params *entries.GetLogEntryByIndexParams, opts ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) { - return &entries.GetLogEntryByIndexOK{ - Payload: data, - }, nil + if m.Entries != nil { + return &entries.GetLogEntryByIndexOK{ + Payload: *m.Entries, + }, nil + } + return nil, errors.New("entry not provided") } func (m *EntriesClient) GetLogEntryByUUID(params *entries.GetLogEntryByUUIDParams, opts ...entries.ClientOption) (*entries.GetLogEntryByUUIDOK, error) { - return &entries.GetLogEntryByUUIDOK{ - Payload: data, - }, nil + if m.Entries != nil { + return &entries.GetLogEntryByUUIDOK{ + Payload: *m.Entries, + }, nil + } + return nil, errors.New("entry not provided") } func (m *EntriesClient) SearchLogQuery(params *entries.SearchLogQueryParams, opts ...entries.ClientOption) (*entries.SearchLogQueryOK, error) { + resp := []models.LogEntry{} + if m.Entries != nil { + resp = append(resp, *m.Entries) + } return &entries.SearchLogQueryOK{ - Payload: []models.LogEntry{data}, + Payload: resp, }, nil } diff --git a/internal/pkg/cosign/rekor/signer_test.go b/internal/pkg/cosign/rekor/signer_test.go index cb705718605..95bdc57d5f3 100644 --- a/internal/pkg/cosign/rekor/signer_test.go +++ b/internal/pkg/cosign/rekor/signer_test.go @@ -22,10 +22,12 @@ import ( "strings" "testing" + "github.com/go-openapi/swag" "github.com/sigstore/cosign/internal/pkg/cosign/payload" "github.com/sigstore/cosign/internal/pkg/cosign/rekor/mock" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/signature" ) @@ -48,7 +50,12 @@ func TestSigner(t *testing.T) { // Mock out Rekor client var mClient client.Rekor - mClient.Entries = &mock.EntriesClient{} + + mClient.Entries = &mock.EntriesClient{ + Entries: &models.LogEntry{"123": models.LogEntryAnon{ + LogIndex: swag.Int64(123), + }}, + } testSigner := NewSigner(payloadSigner, &mClient) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 7efe8a1100e..ddba36dbdf8 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -992,7 +992,7 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa. // verify the SET against the public key hash := sha256.Sum256(canonicalized) if !ecdsa.VerifyASN1(pub, hash[:], signature) { - return &VerificationError{"unable to verify"} + return &VerificationError{"unable to verify SET"} } return nil } diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index bc6fdc3f905..fbf1692cf86 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -23,6 +23,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/json" "encoding/pem" "errors" @@ -46,11 +47,13 @@ import ( "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/cosign/test" "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" rtypes "github.com/sigstore/rekor/pkg/types" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" "github.com/stretchr/testify/require" + "github.com/transparency-dev/merkle/rfc6962" ) type mockVerifier struct { @@ -343,6 +346,40 @@ func TestVerifyImageSignatureWithExistingSub(t *testing.T) { } } +var ( + lea = models.LogEntryAnon{ + Attestation: &models.LogEntryAnonAttestation{}, + Body: base64.StdEncoding.EncodeToString([]byte("asdf")), + IntegratedTime: new(int64), + LogID: new(string), + LogIndex: new(int64), + Verification: &models.LogEntryAnonVerification{ + InclusionProof: &models.InclusionProof{ + RootHash: new(string), + TreeSize: new(int64), + LogIndex: new(int64), + }, + }, + } + data = models.LogEntry{ + uuid(lea): lea, + } +) + +// uuid generates the UUID for the given LogEntry. +// This is effectively a reimplementation of +// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated +// to avoid a circular dependency. +// TODO?: Perhaps we should refactor the tlog libraries into a separate +// package? +func uuid(e models.LogEntryAnon) string { + entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string)) + if err != nil { + panic(err) + } + return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes)) +} + // This test ensures that image signature validation fails properly if we are // using a SigVerifier with Rekor. // See https://github.com/sigstore/cosign/issues/1816 for more details. @@ -361,7 +398,9 @@ func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) { // tlog entry for the signature during validation (even though it does not // match the underlying data / key) mClient := new(client.Rekor) - mClient.Entries = &mock.EntriesClient{} + mClient.Entries = &mock.EntriesClient{ + Entries: &data, + } if _, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ SigVerifier: sv, diff --git a/test/cert_utils.go b/test/cert_utils.go index 6908a5f67e7..03fd5fa690a 100644 --- a/test/cert_utils.go +++ b/test/cert_utils.go @@ -69,7 +69,7 @@ func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) { CommonName: "sigstore", Organization: []string{"sigstore.dev"}, }, - NotBefore: time.Now().Add(-5 * time.Minute), + NotBefore: time.Now().Add(-5 * time.Hour), NotAfter: time.Now().Add(5 * time.Hour), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, @@ -117,6 +117,34 @@ func GenerateSubordinateCa(rootTemplate *x509.Certificate, rootPriv crypto.Signe return cert, priv, nil } +func GenerateLeafCertWithExpiration(subject string, oidcIssuer string, expiration time.Time, + priv *ecdsa.PrivateKey, + parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, error) { + certTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + EmailAddresses: []string{subject}, + NotBefore: expiration, + NotAfter: expiration.Add(10 * time.Minute), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + IsCA: false, + ExtraExtensions: []pkix.Extension{{ + // OID for OIDC Issuer extension + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Critical: false, + Value: []byte(oidcIssuer), + }, + }, + } + + cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv) + if err != nil { + return nil, err + } + + return cert, nil +} + func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) { certTemplate := &x509.Certificate{ SerialNumber: big.NewInt(1),