diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 4284aeaf983..954e7dcdd42 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -16,49 +16,30 @@ package verify import ( - "bytes" "context" "crypto" - "crypto/sha256" "crypto/x509" "encoding/base64" - "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" - "strings" - "time" - "github.com/go-openapi/runtime" ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/options" "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" + "github.com/sigstore/cosign/pkg/oci/static" sigs "github.com/sigstore/cosign/pkg/signature" - "github.com/sigstore/sigstore/pkg/tuf" 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" - "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/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) func isb64(data []byte) bool { @@ -88,14 +69,19 @@ type VerifyBlobCmd struct { // nolint func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { var cert *x509.Certificate - var bundle *bundle.RekorBundle + opts := make([]static.Option, 0) // Require a certificate/key OR a local bundle file that has the cert. - if options.NOf(c.KeyRef, c.Sk) > 1 { + if options.NOf(c.KeyRef, c.CertRef, c.Sk, c.BundlePath) == 0 { + return fmt.Errorf("please provide a cert to verify against via --certificate or a bundle via --bundle") + } + + // Key, sk, and cert are mutually exclusive. + if options.NOf(c.KeyRef, c.Sk, c.CertRef) > 1 { return &options.KeyParseError{} } - sig, err := signatures(c.SigRef, c.BundlePath) + sig, err := base64signature(c.SigRef, c.BundlePath) if err != nil { return err } @@ -125,13 +111,16 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } co.RekorClient = rekorClient } - 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) + // Use default TUF roots if a cert chain is not provided. + if c.CertChain == "" { + 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) + } } } @@ -161,118 +150,77 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { if err != nil { return err } - if c.CertChain == "" { - 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) - } - co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return fmt.Errorf("validating certRef: %w", err) - } - } else { - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return fmt.Errorf("verifying certRef with certChain: %w", err) - } - } - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - case c.BundlePath != "": + } + if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { return err } - if b.Cert == "" { + // A certificate is required in the bundle unless we specified with + // --key, --sk, or --certificate. + if b.Cert == "" && co.SigVerifier == nil && cert == nil { return fmt.Errorf("bundle does not contain cert for verification, please provide 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) - } - cert, err = loadCertFromPEM(certBytes) - if err != nil { - // check if cert is actually a public key - co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) - } else { - if c.CertChain == "" { - 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) - } - co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return fmt.Errorf("verifying certificate from bundle: %w", err) - } - } else { - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + // We have to condition on this because sign-blob may not output the signing + // key to the bundle when there is no tlog upload. + if b.Cert != "" { + // b.Cert can either be a certificate or public key + certBytes := []byte(b.Cert) + if isb64(certBytes) { + certBytes, _ = base64.StdEncoding.DecodeString(b.Cert) + } + cert, err = loadCertFromPEM(certBytes) + if err != nil { + // check if cert is actually a public key + co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) if err != nil { - return fmt.Errorf("verifying certificate from bundle with chain: %w", err) + return fmt.Errorf("loading verifier from bundle: %w", err) } } } + opts = append(opts, static.WithBundle(b.Bundle)) + } + // Set an SCT if provided via the CLI. + if c.SCTRef != "" { + sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) if err != nil { - return fmt.Errorf("loading verifier from bundle: %w", err) + return fmt.Errorf("reading sct from file: %w", err) } - bundle = b.Bundle - default: - return fmt.Errorf("please provide a cert to verify against via --certificate or a bundle via --bundle") + co.SCT = sct } - - // Performs all blob verification. - if err := verifyBlob(ctx, co, blobBytes, sig, cert, bundle); err != nil { - return err + // Set a cert chain if provided. + var chainPEM []byte + if c.CertChain != "" { + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + if chain == nil { + return errors.New("expected certificate chain in --certificate-chain") + } + // Set the last one in the co.RootCerts. This is trusted, as its passed in + // via the CLI. + if co.RootCerts == nil { + co.RootCerts = x509.NewCertPool() + } + co.RootCerts.AddCert(chain[len(chain)-1]) + // Use the whole as the cert chain in the signature object. + // The last one is omitted because it is considered the "root". + chainPEM, err = cryptoutils.MarshalCertificatesToPEM(chain) + if err != nil { + return err + } } - fmt.Fprintln(os.Stderr, "Verified OK") - return nil -} - -/* 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. This works offline OR - b. If we don't have a Rekor entry retrieved via cert, do an online lookup (assuming - we are in experimental mode). - 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, - bundle *bundle.RekorBundle) error { + // Gather the cert for the signature and add the cert along with the + // cert chain into the signature object. + var certPEM []byte 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) + certPEM, err = cryptoutils.MarshalCertificateToPEM(cert) if err != nil { - return fmt.Errorf("validating cert: %w", err) + return err } + opts = append(opts, static.WithCertChain(certPEM, chainPEM)) } // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. @@ -280,133 +228,33 @@ func verifyBlob(ctx context.Context, co *cosign.CheckOpts, // 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) - } - - // 1. Verify the signature. - if err := co.SigVerifier.VerifySignature(strings.NewReader(sig), bytes.NewReader(blobBytes)); err != nil { - return 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 bundle != nil: - 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) - } - } - bundle, err := verifyRekorBundle(ctx, bundle, blobBytes, sig, svBytes) + // co.SigVerifier = dsse.WrapVerifier(co.SigVerifier) + signature, err := static.NewAttestation(blobBytes, opts...) if err != nil { - // 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: - var tlogFindErr error - var e *models.LogEntryAnon - 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 - } - if err := cosign.VerifyTLogEntry(ctx, nil, e); err != nil { + // We have no artifact the attestation is tied to, so we can't do any claim + // verification. + // TODO: Add an option to support this to populate the v1.Hash for a claim. + if _, err = cosign.VerifyBlobAttestation(ctx, signature, co); err != nil { return err } - - uuid, err := cosign.ComputeLeafHash(e) + } else { + signature, err := static.NewSignature(blobBytes, sig, opts...) if err != nil { return err } - - 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() - } - - // 3. If a certificate is provided, check it's expiration. - if cert == nil { - return nil - } - - 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 - } - 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) -} - -func tlogFindEntry(ctx context.Context, client *client.Rekor, - blobBytes []byte, sig string, pem []byte) (*models.LogEntryAnon, error) { - b64sig := base64.StdEncoding.EncodeToString([]byte(sig)) - tlogEntries, err := cosign.FindTlogEntry(ctx, client, b64sig, blobBytes, pem) - if err != nil { - return nil, err - } - if len(tlogEntries) == 0 { - return nil, fmt.Errorf("no valid tlog entries found with proposed entry") - } - // Always return the earliest integrated entry. That - // always suffices for verification of signature time. - var earliestLogEntry models.LogEntryAnon - var earliestLogEntryTime *time.Time - // We'll always return a tlog entry because there's at least one entry in the log. - for _, entry := range tlogEntries { - entryTime := time.Unix(*entry.IntegratedTime, 0) - if earliestLogEntryTime == nil || entryTime.Before(*earliestLogEntryTime) { - earliestLogEntryTime = &entryTime - earliestLogEntry = entry + if _, err = cosign.VerifyBlobSignature(ctx, signature, co); err != nil { + return err } } - return &earliestLogEntry, nil + + fmt.Fprintln(os.Stderr, "Verified OK") + return nil } -// signatures returns the raw signature -func signatures(sigRef string, bundlePath string) (string, error) { +// base64signature returns the base64 encoded signature +func base64signature(sigRef string, bundlePath string) (string, error) { var targetSig []byte var err error switch { @@ -429,15 +277,10 @@ func signatures(sigRef string, bundlePath string) (string, error) { return "", fmt.Errorf("missing flag '--signature'") } - var sig, b64sig string if isb64(targetSig) { - b64sig = string(targetSig) - sigBytes, _ := base64.StdEncoding.DecodeString(b64sig) - sig = string(sigBytes) - } else { - sig = string(targetSig) + return string(targetSig), nil } - return sig, nil + return base64.StdEncoding.EncodeToString(targetSig), nil } func payloadBytes(blobRef string) ([]byte, error) { @@ -454,160 +297,6 @@ func payloadBytes(blobRef string) ([]byte, error) { return blobBytes, nil } -func verifyRekorBundle(ctx context.Context, bundle *bundle.RekorBundle, - blobBytes []byte, sig string, pubKeyBytes []byte) (*bundle.RekorPayload, error) { - if err := verifyBundleMatchesData(ctx, bundle, blobBytes, pubKeyBytes, []byte(sig)); err != nil { - return nil, err - } - - publicKeys, err := cosign.GetRekorPubs(ctx, nil) - if err != nil { - return nil, fmt.Errorf("retrieving rekor public key: %w", err) - } - - pubKey, ok := publicKeys[bundle.Payload.LogID] - if !ok { - return nil, errors.New("rekor log public key not found for payload") - } - err = cosign.VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, pubKey.PubKey) - if err != nil { - return nil, err - } - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - - return &bundle.Payload, nil -} - -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 - } - - targetImpl, err := reconstructCanonicalizedEntry(ctx, kind, apiVersion, blobBytes, certBytes, sigBytes) - if err != nil { - return fmt.Errorf("recontructing rekorEntry for bundle comparison: %w", err) - } - - 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 %w", 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 %w", 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") - } - return nil -} - -func reconstructCanonicalizedEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) { - props := types.ArtifactProperties{ - PublicKeyBytes: [][]byte{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 - } - - eimpl, err := types.CreateVersionedEntry(proposedEntry) - if err != nil { - return nil, err - } - - can, err := types.CanonicalizeEntry(ctx, eimpl) - if err != nil { - return nil, err - } - proposedEntryCan, err := models.UnmarshalProposedEntry(bytes.NewReader(can), runtime.JSONConsumer()) - if err != nil { - return nil, err - } - - eimpl, err = types.UnmarshalEntry(proposedEntryCan) - if err != nil { - return nil, err - } - - return eimpl, nil -} - -// 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 - } - - pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) - if err != nil { - return nil, "", "", err - } - - entry, err := types.UnmarshalEntry(pe) - if err != nil { - return nil, "", "", err - } - return entry, pe.Kind(), entry.APIVersion(), nil -} - // isIntotoDSSE checks whether a payload is a Dead Simple Signing Envelope with the In-Toto format. func isIntotoDSSE(blobBytes []byte) bool { DSSEenvelope := ssldsse.Envelope{} @@ -620,19 +309,3 @@ 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 5a0ac7cfc39..b2c1b7f5d6a 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -27,6 +27,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -38,13 +40,11 @@ import ( "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" @@ -81,14 +81,14 @@ func TestSignaturesRef(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - gotSig, err := signatures(test.sigRef, "") + gotSig, err := base64signature(test.sigRef, "") if test.shouldErr && err != nil { return } if test.shouldErr { t.Fatal("should have received an error") } - if gotSig != sig { + if gotSig != b64sig { t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig) } }) @@ -99,7 +99,6 @@ func TestSignaturesBundle(t *testing.T) { td := t.TempDir() fp := filepath.Join(td, "file") - sig := "a==" b64sig := "YT09" // save as a LocalSignedPayload to the file @@ -114,12 +113,12 @@ func TestSignaturesBundle(t *testing.T) { t.Fatal(err) } - gotSig, err := signatures("", fp) + gotSig, err := base64signature("", fp) if err != nil { t.Fatal(err) } - if gotSig != sig { - t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig) + if gotSig != b64sig { + t.Fatalf("unexpected signature, expected: %s got: %s", b64sig, gotSig) } } @@ -194,6 +193,9 @@ func TestVerifyBlob(t *testing.T) { rootCert, rootPriv, _ := test.GenerateRootCa() rootPool := x509.NewCertPool() rootPool.AddCert(rootCert) + chain, _ := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{rootCert}) + chainPath := writeBlobFile(t, td, string(chain), "chain.pem") + unexpiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer, time.Now(), leafPriv, rootCert, rootPriv) unexpiredCertPem, _ := cryptoutils.MarshalCertificateToPEM(unexpiredLeafCert) @@ -215,15 +217,8 @@ func TestVerifyBlob(t *testing.T) { 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()) + tmpRekorPubFile := writeBlobFile(t, td, string(pemRekor), "rekor_pub.key") + t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile) var makeSignature = func(blob []byte) string { sig, err := signer.SignMessage(bytes.NewReader(blob)) @@ -239,12 +234,12 @@ func TestVerifyBlob(t *testing.T) { otherSignature := makeSignature(otherBytes) tts := []struct { - name string - blob []byte - signature string - sigVerifier signature.Verifier - cert *x509.Certificate - bundlePath string + name string + blob []byte + signature string + key []byte + cert *x509.Certificate + bundlePath string // If online lookups to Rekor are enabled experimental bool // The rekor entry response when Rekor is enabled @@ -255,24 +250,28 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, shouldErr: false, }, + /* TODO: + This currently passes without error because we don't require an experimental + lookup for public keys. Add this test back in when we have a --verify-tlog-online { name: "valid signature with public key - experimental no rekor fail", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: true, rekorEntry: nil, shouldErr: true, }, + */ { name: "valid signature with public key - experimental rekor entry success", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: true, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), pubKeyBytes, true)}, @@ -282,7 +281,7 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key - good bundle provided", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), pubKeyBytes, true), @@ -292,7 +291,7 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key - bundle without rekor bundle fails", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, bundlePath: makeLocalBundleWithoutRekorBundle(t, []byte(blobSignature), pubKeyBytes), shouldErr: false, @@ -301,7 +300,7 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key - bad bundle SET", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), unexpiredCertPem, true), @@ -311,7 +310,7 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key - bad bundle cert mismatch", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true), @@ -321,7 +320,7 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key - bad bundle signature mismatch", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)), pubKeyBytes, true), @@ -331,7 +330,7 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with public key - bad bundle msg & signature mismatch", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature), pubKeyBytes, true), @@ -341,7 +340,7 @@ func TestVerifyBlob(t *testing.T) { name: "invalid signature with public key", blob: blobBytes, signature: otherSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: false, shouldErr: true, }, @@ -349,35 +348,32 @@ func TestVerifyBlob(t *testing.T) { name: "invalid signature with public key - experimental", blob: blobBytes, signature: otherSignature, - sigVerifier: signer, + key: pubKeyBytes, experimental: true, shouldErr: true, }, { - name: "valid signature with unexpired certificate", + name: "valid signature with unexpired certificate - no rekor entry", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, cert: unexpiredLeafCert, experimental: false, - shouldErr: false, + shouldErr: true, }, { name: "valid signature with unexpired certificate - bad bundle cert mismatch", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, experimental: false, - cert: unexpiredLeafCert, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - pubKeyBytes, true), + unexpiredCertPem, 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)), @@ -388,7 +384,6 @@ func TestVerifyBlob(t *testing.T) { 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), @@ -399,7 +394,6 @@ func TestVerifyBlob(t *testing.T) { name: "invalid signature with unexpired certificate", blob: blobBytes, signature: otherSignature, - sigVerifier: signer, cert: unexpiredLeafCert, experimental: false, shouldErr: true, @@ -409,7 +403,6 @@ func TestVerifyBlob(t *testing.T) { blob: blobBytes, signature: blobSignature, cert: unexpiredLeafCert, - sigVerifier: signer, experimental: true, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true)}, @@ -430,7 +423,6 @@ func TestVerifyBlob(t *testing.T) { blob: blobBytes, signature: blobSignature, cert: expiredLeafCert, - sigVerifier: signer, experimental: false, shouldErr: true, }, @@ -438,7 +430,6 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with expired certificate - experimental good rekor lookup", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, cert: expiredLeafCert, experimental: true, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), @@ -449,7 +440,6 @@ func TestVerifyBlob(t *testing.T) { name: "valid signature with expired certificate - experimental multiple rekor entries", blob: blobBytes, signature: blobSignature, - sigVerifier: signer, cert: expiredLeafCert, experimental: true, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), @@ -462,7 +452,6 @@ func TestVerifyBlob(t *testing.T) { blob: blobBytes, signature: blobSignature, cert: expiredLeafCert, - sigVerifier: signer, experimental: true, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, false)}, @@ -474,7 +463,6 @@ func TestVerifyBlob(t *testing.T) { blob: blobBytes, signature: blobSignature, cert: unexpiredLeafCert, - sigVerifier: signer, experimental: false, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true), @@ -485,7 +473,6 @@ func TestVerifyBlob(t *testing.T) { blob: blobBytes, signature: blobSignature, cert: expiredLeafCert, - sigVerifier: signer, experimental: false, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, true), @@ -495,7 +482,6 @@ func TestVerifyBlob(t *testing.T) { 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), @@ -506,7 +492,6 @@ func TestVerifyBlob(t *testing.T) { 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), @@ -517,7 +502,6 @@ func TestVerifyBlob(t *testing.T) { 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), @@ -528,7 +512,6 @@ func TestVerifyBlob(t *testing.T) { 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! @@ -540,25 +523,46 @@ func TestVerifyBlob(t *testing.T) { 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, - IgnoreSCT: true, + entries := make([]models.LogEntry, 0) + for _, entry := range tt.rekorEntry { + entries = append(entries, *entry) } - // if expermental is enabled, add RekorClient to co. - if tt.experimental { - co.RekorClient = &mClient + testServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(entries) + })) + defer testServer.Close() + + // Verify command + cmd := VerifyBlobCmd{ + KeyOpts: options.KeyOpts{ + BundlePath: tt.bundlePath, + RekorURL: testServer.URL}, + CertEmail: identity, + CertOIDCIssuer: issuer, + IgnoreSCT: true, + CertChain: chainPath, } - - var bundle *bundle.RekorBundle - b, err := cosign.FetchLocalSignedPayloadFromPath(tt.bundlePath) - if err == nil && b.Bundle != nil { - bundle = b.Bundle + blobPath := writeBlobFile(t, td, string(blobBytes), "blob.txt") + if tt.signature != "" { + sigPath := writeBlobFile(t, td, tt.signature, "signature.txt") + cmd.SigRef = sigPath + } + if tt.cert != nil { + certPEM, err := cryptoutils.MarshalCertificateToPEM(tt.cert) + if err != nil { + t.Fatal("MarshalCertificateToPEM: %w", err) + } + certPath := writeBlobFile(t, td, string(certPEM), "cert.pem") + cmd.CertRef = certPath + } + if tt.key != nil { + keyPath := writeBlobFile(t, td, string(tt.key), "key.pem") + cmd.KeyRef = keyPath } - err = verifyBlob(ctx, co, tt.blob, tt.signature, tt.cert, bundle) + err := cmd.Exec(context.Background(), blobPath) if (err != nil) != tt.shouldErr { t.Fatalf("verifyBlob()= %s, expected shouldErr=%t ", err, tt.shouldErr) } @@ -1048,7 +1052,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "verifying certificate from bundle with chain: x509: certificate signed by unknown authority") { + if err == nil || !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { t.Fatalf("expected error with mismatched root, got %v", err) } }) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index d07fa283b46..5743bb36deb 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -25,6 +25,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "encoding/pem" "errors" "fmt" "os" @@ -449,33 +450,8 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif return ValidateAndUnpackCert(cert, co) } -func tlogValidatePublicKey(ctx context.Context, rekorClient *client.Rekor, pub crypto.PublicKey, sig oci.Signature) error { - pemBytes, err := cryptoutils.MarshalPublicKeyToPEM(pub) - if err != nil { - return err - } - _, err = tlogValidateEntry(ctx, rekorClient, sig, pemBytes) - return err -} - -func tlogValidateCertificate(ctx context.Context, rekorClient *client.Rekor, sig oci.Signature) error { - cert, err := sig.Cert() - if err != nil { - return err - } - pemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return err - } - e, err := tlogValidateEntry(ctx, rekorClient, sig, pemBytes) - if err != nil { - return err - } - // if we have a cert, we should check expiry - return CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0)) -} - -func tlogValidateEntry(ctx context.Context, client *client.Rekor, sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { +func tlogValidateEntry(ctx context.Context, client *client.Rekor, sig oci.Signature, pem []byte) ( + *models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() if err != nil { return nil, err @@ -639,9 +615,13 @@ func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *C return checkedSignatures, bundleVerified, nil } -// verifyInternal holds the main verification flow for blobs and attestations. -// It verifies the certificate chain, the signature, claims, a bundle or the online -// Rekor client. +// verifyInternal holds the main verification flow for signatures and attestations. +// 1. Verifies the signature using the provided verifier. +// 2. Checks for transparency log entry presence: +// a. Verifies the Rekor entry in the bundle, if provided. This works offline OR +// b. If we don't have a Rekor entry retrieved via cert, do an online lookup (assuming +// we are in experimental mode). +// 3. If a certificate is provided, check it's expiration using the transparency log timestamp. func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, verifyFn signatureVerificationFn, co *CheckOpts) ( bundleVerified bool, err error) { @@ -660,16 +640,18 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, if err != nil { return bundleVerified, err } - // If the chain annotation is not present or there is only a root - if chain == nil || len(chain) <= 1 { - co.IntermediateCerts = nil - } else if co.IntermediateCerts == nil { - // If the intermediate certs have not been loaded in by TUF - pool := x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - pool.AddCert(cert) + // If there is no chain annotation present, we preserve the pools set in the CheckOpts. + if len(chain) > 0 { + if len(chain) == 1 { + co.IntermediateCerts = nil + } else if co.IntermediateCerts == nil { + // If the intermediate certs have not been loaded in by TUF + pool := x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + pool.AddCert(cert) + } + co.IntermediateCerts = pool } - co.IntermediateCerts = pool } verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { @@ -677,6 +659,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, } } + // 1. Perform cryptographic verification of the signature using the certificate's public key. if err := verifyFn(ctx, verifier, sig); err != nil { return bundleVerified, err } @@ -697,33 +680,83 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, return bundleVerified, nil } - bundleVerified, err = VerifyBundle(ctx, sig, co.RekorClient) + // 2. Check the validity time of the signature. + // This is the signature creation time. As a default upper bound, use the current + // time. + validityTime := time.Now() + + bundleVerified, err = VerifyBundle(ctx, sig, co, co.RekorClient) if err != nil { return false, fmt.Errorf("error verifying bundle: %w", err) } - // If the --offline flag was specified, fail here + if bundleVerified { + // Update with the verified bundle's integrated time. + validityTime, err = getBundleIntegratedTime(sig) + if err != nil { + return false, fmt.Errorf("error getting bundle integrated time: %w", err) + } + } + + // If the --offline flag was specified, fail here. bundleVerified returns false with + // no error when there was no bundle provided. + // TODO: You can be offline when you are verifying with a public key. This shouldn't + // error out, but maybe should be a log message. if !bundleVerified && co.Offline { return false, fmt.Errorf("offline verification failed") } - if !bundleVerified && co.RekorClient != nil { - if co.SigVerifier != nil { - pub, err := co.SigVerifier.PublicKey(co.PKOpts...) - if err != nil { - return bundleVerified, err - } - return bundleVerified, tlogValidatePublicKey(ctx, co.RekorClient, pub, sig) + cert, err := sig.Cert() + if err != nil { + return false, err + } + if !bundleVerified && co.RekorClient != nil && !co.Offline { + pemBytes, err := keyBytes(sig, co) + if err != nil { + return bundleVerified, err + } + + e, err := tlogValidateEntry(ctx, co.RekorClient, sig, pemBytes) + if err != nil { + return bundleVerified, err } + validityTime = time.Unix(*e.IntegratedTime, 0) + } - return bundleVerified, tlogValidateCertificate(ctx, co.RekorClient, sig) + // 3. if a certificate was used, verify the cert against the integrated time. + if cert != nil { + if err := CheckExpiry(cert, validityTime); err != nil { + return false, fmt.Errorf("checking expiry on cert: %w", err) + } } + return bundleVerified, nil } +func keyBytes(sig oci.Signature, co *CheckOpts) ([]byte, error) { + cert, err := sig.Cert() + if err != nil { + return nil, err + } + // We have a public key. + if co.SigVerifier != nil { + pub, err := co.SigVerifier.PublicKey(co.PKOpts...) + if err != nil { + return nil, err + } + return cryptoutils.MarshalPublicKeyToPEM(pub) + } + return cryptoutils.MarshalCertificateToPEM(cert) +} + +// VerifyBlobSignature verifies a blob signature. +func VerifyBlobSignature(ctx context.Context, sig oci.Signature, co *CheckOpts) (bundleVerified bool, err error) { + // The hash of the artifact is unused. + return verifyInternal(ctx, sig, v1.Hash{}, verifyOCISignature, co) +} + // VerifyImageSignature verifies a signature -func VerifyImageSignature(ctx context.Context, sig oci.Signature, - h v1.Hash, co *CheckOpts) (bundleVerified bool, err error) { +func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co *CheckOpts) (bundleVerified bool, err error) { return verifyInternal(ctx, sig, h, verifyOCISignature, co) } @@ -842,6 +875,13 @@ func VerifyLocalImageAttestations(ctx context.Context, path string, co *CheckOpt return verifyImageAttestations(ctx, atts, h, co) } +func VerifyBlobAttestation(ctx context.Context, att oci.Signature, co *CheckOpts) ( + bool, error) { + // A blob attestation does not have an associated artifact (currently) to check the claims against. + // So we can safely add a nil hash. + return verifyInternal(ctx, att, v1.Hash{}, verifyOCIAttestation, co) +} + func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { sl, err := atts.Get() if err != nil { @@ -889,7 +929,17 @@ func CheckExpiry(cert *x509.Certificate, it time.Time) error { return nil } -func VerifyBundle(ctx context.Context, sig oci.Signature, rekorClient *client.Rekor) (bool, error) { +func getBundleIntegratedTime(sig oci.Signature) (time.Time, error) { + bundle, err := sig.Bundle() + if err != nil { + return time.Now(), err + } else if bundle == nil { + return time.Now(), nil + } + return time.Unix(bundle.Payload.IntegratedTime, 0), nil +} + +func VerifyBundle(ctx context.Context, sig oci.Signature, co *CheckOpts, rekorClient *client.Rekor) (bool, error) { bundle, err := sig.Bundle() if err != nil { return false, err @@ -901,6 +951,10 @@ func VerifyBundle(ctx context.Context, sig oci.Signature, rekorClient *client.Re return false, err } + if err := comparePublicKey(bundle.Payload.Body.(string), sig, co); err != nil { + return false, err + } + publicKeys, err := GetRekorPubs(ctx, nil) if err != nil { return false, fmt.Errorf("retrieving rekor public key: %w", err) @@ -918,19 +972,6 @@ func VerifyBundle(ctx context.Context, sig oci.Signature, rekorClient *client.Re fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } - cert, err := sig.Cert() - if err != nil { - return false, err - } - - if cert != nil { - // Verify the cert against the integrated time. - // Note that if the caller requires the certificate to be present, it has to ensure that itself. - if err := CheckExpiry(cert, time.Unix(bundle.Payload.IntegratedTime, 0)); err != nil { - return false, fmt.Errorf("checking expiry on cert: %w", err) - } - } - payload, err := sig.Payload() if err != nil { return false, fmt.Errorf("reading payload: %w", err) @@ -1030,6 +1071,40 @@ func compareSigs(bundleBody string, sig oci.Signature) error { return nil } +func comparePublicKey(bundleBody string, sig oci.Signature, co *CheckOpts) error { + pemBytes, err := keyBytes(sig, co) + if err != nil { + return err + } + + bundleKey, err := bundleKey(bundleBody) + if err != nil { + return fmt.Errorf("failed to extract key from bundle: %w", err) + } + + decodeSecond, err := base64.StdEncoding.DecodeString(bundleKey) + if err != nil { + return fmt.Errorf("decoding base64 string %s", bundleKey) + } + + // Compare the PEM bytes, to ignore spurious newlines in the public key bytes. + pemFirst, rest := pem.Decode(pemBytes) + if len(rest) > 0 { + return fmt.Errorf("unexpected PEM block: %s", rest) + } + pemSecond, rest := pem.Decode(decodeSecond) + if len(rest) > 0 { + return fmt.Errorf("unexpected PEM block: %s", rest) + } + + if !bytes.Equal(pemFirst.Bytes, pemSecond.Bytes) { + return fmt.Errorf("comparing public key PEMs, expected %s, got %s", + pemBytes, decodeSecond) + } + + return nil +} + func bundleHash(bundleBody, signature string) (string, string, error) { var toto models.Intoto var rekord models.Rekord @@ -1129,6 +1204,58 @@ func bundleSig(bundleBody string) (string, error) { return hrekordObj.Signature.Content.String(), nil } +// bundleKey extracts the key from the rekor bundle body +func bundleKey(bundleBody string) (string, error) { + var rekord models.Rekord + var hrekord models.Hashedrekord + var intotod models.Intoto + var rekordObj models.RekordV001Schema + var hrekordObj models.HashedrekordV001Schema + var intotodObj models.IntotoV001Schema + + bodyDecoded, err := base64.StdEncoding.DecodeString(bundleBody) + if err != nil { + return "", fmt.Errorf("decoding bundleBody: %w", err) + } + + // Try Rekord + if err := json.Unmarshal(bodyDecoded, &rekord); err == nil { + specMarshal, err := json.Marshal(rekord.Spec) + if err != nil { + return "", err + } + if err := json.Unmarshal(specMarshal, &rekordObj); err != nil { + return "", err + } + return rekordObj.Signature.PublicKey.Content.String(), nil + } + + // Try hashedRekordObj + if err := json.Unmarshal(bodyDecoded, &hrekord); err == nil { + specMarshal, err := json.Marshal(hrekord.Spec) + if err != nil { + return "", err + } + if err := json.Unmarshal(specMarshal, &hrekordObj); err != nil { + return "", err + } + return hrekordObj.Signature.PublicKey.Content.String(), nil + } + + // Try Intoto + if err := json.Unmarshal(bodyDecoded, &intotod); err != nil { + return "", err + } + specMarshal, err := json.Marshal(intotod.Spec) + if err != nil { + return "", err + } + if err := json.Unmarshal(specMarshal, &intotodObj); err != nil { + return "", err + } + return intotodObj.PublicKey.String(), nil +} + func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa.PublicKey) error { contents, err := json.Marshal(bundlePayload) if err != nil {