From 4535d707a8272a5238f0ce473a0ed7aa77093e04 Mon Sep 17 00:00:00 2001 From: Slavek Kabrda Date: Thu, 16 Jan 2025 10:44:38 +0100 Subject: [PATCH] Enable signing and verifying image without registry access Signed-off-by: Slavek Kabrda --- cmd/cosign/cli/sign/sign.go | 2 +- cmd/cosign/cli/verify/verify.go | 1 + cmd/cosign/cli/verify/verify_blob_test.go | 11 ++++- pkg/cosign/verify.go | 35 ++++++++++++++-- pkg/cosign/verify_test.go | 50 +++++++++++++++++++---- 5 files changed, 86 insertions(+), 13 deletions(-) diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1289e7b1bb4..d8a1900dd6c 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -181,7 +181,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO if digest, ok := ref.(name.Digest); ok && !signOpts.Recursive { se, err := ociremote.SignedEntity(ref, opts...) - if _, isEntityNotFoundErr := err.(*ociremote.EntityNotFoundError); isEntityNotFoundErr { + if _, isEntityNotFoundErr := err.(*ociremote.EntityNotFoundError); isEntityNotFoundErr || !signOpts.Upload { se = ociremote.SignedUnknown(digest) } else if err != nil { return fmt.Errorf("accessing image: %w", err) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index f5fc86bfddf..ebe709b5248 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -243,6 +243,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { default: return errors.New("no certificate chain provided to verify certificate") } + co.CodeSigningCert = cert if c.SCTRef != "" { sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index 6b1b127052c..1aefd29a4e9 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -263,10 +263,19 @@ func TestVerifyBlob(t *testing.T) { shouldErr: true, }, { - name: "valid signature with public key - experimental rekor entry success", + name: "valid signature with public key - experimental rekor entry fail no certificate", blob: blobBytes, signature: blobSignature, key: pubKeyBytes, + rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), + pubKeyBytes, true)}, + shouldErr: true, + }, + { + name: "valid signature with public key - experimental rekor entry success", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), pubKeyBytes, true)}, shouldErr: false, diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3a6ee79b461..8cffcba1204 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -112,6 +112,8 @@ type CheckOpts struct { RootCerts *x509.CertPool // IntermediateCerts are the optional intermediate CA certs used to verify a certificate chain. IntermediateCerts *x509.CertPool + // CodeSigningCert is the code signing certificate, if provided + CodeSigningCert *x509.Certificate // CertGithubWorkflowTrigger is the GitHub Workflow Trigger name expected for a certificate to be valid. The empty string means any certificate can be valid. CertGithubWorkflowTrigger string @@ -704,7 +706,13 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, return false, fmt.Errorf("rekor client not provided for online verification") } - pemBytes, err := keyBytes(sig, co) + certBytes, err := sig.Cert() + if err != nil { + return false, err + } else if certBytes == nil || len(certBytes.Raw) == 0 { + return false, fmt.Errorf("code signing certificate not provided for rekor lookup") + } + pemBytes, err := cryptoutils.MarshalCertificateToPEM(certBytes) if err != nil { return false, err } @@ -715,7 +723,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, } t := time.Unix(*e.IntegratedTime, 0) acceptableRekorBundleTime = &t - bundleVerified = true + bundleVerified = false } } @@ -872,7 +880,28 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name } } - sig, err := static.NewSignature(payload, b64sig) + // Gather the cert for the signature and add the cert along with the + // cert chain into the signature object. + opts := []static.Option{} + var certPEM []byte + if co.CodeSigningCert != nil { + certPEM, err = cryptoutils.MarshalCertificateToPEM(co.CodeSigningCert) + if err != nil { + return nil, err + } + // TODO: what if there are no chains? + chains, err := TrustedCert(co.CodeSigningCert, co.RootCerts, co.IntermediateCerts) + if err != nil { + return nil, err + } + chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chains[0]) + if err != nil { + return nil, err + } + opts = append(opts, static.WithCertChain(certPEM, chainPEM)) + } + + sig, err := static.NewSignature(payload, b64sig, opts...) if err != nil { return nil, err } diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 30586d4a759..c9ba5d216a2 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -537,8 +537,14 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { signer, publicKey := generateSigner(t) blob, blobSignature, blobSignatureBase64 := generateBlobSignature(t, signer) - // Create an OCI signature which will be verified. - ociSignature, err := static.NewSignature(blob, blobSignatureBase64) + // Create an OCI signature with a mock certificate which will be verified. + opts := []static.Option{} + opts = append(opts, static.WithCertChain([]byte(testLeafCert), []byte{})) + ociSignature, err := static.NewSignature(blob, blobSignatureBase64, opts...) + require.NoError(t, err, "error creating OCI signature with certificate") + + // Create an OCI signature without certificate to test error + ociSignatureNoCert, err := static.NewSignature(blob, blobSignatureBase64) require.NoError(t, err, "error creating OCI signature") // Set up mock Rekor signer and log ID. @@ -582,13 +588,27 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { tests := []struct { name string + signature oci.Signature checkOpts CheckOpts rekorClient *client.Rekor expectError bool errorMsg string }{ { - name: "Verification succeeds with valid Rekor public keys", + name: "Verification fails without certificate", + signature: ociSignatureNoCert, + checkOpts: CheckOpts{ + SigVerifier: signer, + RekorClient: mockClient, + RekorPubKeys: trustedRekorPubKeys, + Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, + }, + rekorClient: mockClient, + expectError: true, + }, + { + name: "Verification succeeds with valid Rekor public keys", + signature: ociSignature, checkOpts: CheckOpts{ SigVerifier: signer, RekorClient: mockClient, @@ -599,7 +619,8 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { expectError: false, }, { - name: "Verification fails with no Rekor public keys", + name: "Verification fails with no Rekor public keys", + signature: ociSignature, checkOpts: CheckOpts{ SigVerifier: signer, RekorClient: mockClient, @@ -610,7 +631,8 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { errorMsg: "no valid tlog entries found no trusted rekor public keys provided", }, { - name: "Verification fails with non-matching Rekor public keys", + name: "Verification fails with non-matching Rekor public keys", + signature: ociSignature, checkOpts: CheckOpts{ SigVerifier: signer, RekorClient: mockClient, @@ -625,13 +647,14 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bundleVerified, err := VerifyImageSignature(ctx, ociSignature, v1.Hash{}, &tt.checkOpts) + bundleVerified, err := VerifyImageSignature(ctx, tt.signature, v1.Hash{}, &tt.checkOpts) if tt.expectError { assert.Error(t, err) assert.Contains(t, err.Error(), tt.errorMsg) } else { assert.NoError(t, err) - assert.True(t, bundleVerified, "bundle verification failed") + // when verifying against Rekor, we expect bundleVerified to be false + assert.False(t, bundleVerified, "bundle verification failed") } }) } @@ -711,7 +734,18 @@ func TestVerifyImageSignatureWithSigVerifierAndRekorTSA(t *testing.T) { if err != nil { t.Fatalf("error signing the payload with the rekor and tsa clients: %v", err) } - if _, err := VerifyImageSignature(context.TODO(), sig, v1.Hash{}, &CheckOpts{ + rawSig, err := sig.Signature() + if err != nil { + t.Fatalf("error getting raw signature: %v", err) + } + + // Create an OCI signature with a mock certificate which will be verified. + opts := []static.Option{} + opts = append(opts, static.WithCertChain([]byte(testLeafCert), []byte{})) + ociSig, err := static.NewSignature(payload, base64.StdEncoding.EncodeToString(rawSig), opts...) + require.NoError(t, err, "error creating OCI signature with certificate") + + if _, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ SigVerifier: sv, TSACertificate: leaves[0], TSAIntermediateCertificates: intermediates,