diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index e51c1216eb1..effa27120e8 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -574,6 +574,30 @@ func validateCertExtensions(ce CertExtensions, co *CheckOpts) error { return nil } +// intermediatePoolFromChain builds the intermediate pool from a certificate chain that +// was attached alongside a signature. The chain ends with a root only when the signer +// attached the whole chain; when the root is supplied separately (co.RootCerts), every +// certificate in the chain is an intermediate and must be kept, including the last one. +// Returns nil when the chain contributes no intermediates. +func intermediatePoolFromChain(chain []*x509.Certificate) *x509.CertPool { + intermediates := chain + if len(chain) > 0 { + last := chain[len(chain)-1] + if bytes.Equal(last.RawIssuer, last.RawSubject) { + // Self-signed: this is a root, not an intermediate. + intermediates = chain[:len(chain)-1] + } + } + if len(intermediates) == 0 { + return nil + } + pool := x509.NewCertPool() + for _, cert := range intermediates { + pool.AddCert(cert) + } + return pool +} + // ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate // chains up to the provided root. Chain should start with the parent of the certificate and end with the root. // Optionally verifies the subject and issuer of the certificate. @@ -902,14 +926,9 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, } // If there is no chain annotation present, we preserve the pools set in the CheckOpts. var pool *x509.CertPool - if len(chain) > 1 { - 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 co.IntermediateCerts == nil { + // If the intermediate certs have not been loaded in by TUF + pool = intermediatePoolFromChain(chain) } // In case pool is not set than set it from co.IntermediateCerts if pool == nil { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index f08234acba8..c27815e1774 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -1364,6 +1364,60 @@ func TestValidateAndUnpackCertInvalidGithubWorkflowRef(t *testing.T) { require.Contains(t, err.Error(), "expected GitHub Workflow Ref not found in certificate") } +func TestIntermediatePoolFromChain(t *testing.T) { + // A chain attached next to a signature ends with a root only when the signer attached + // the whole chain. When the root is supplied separately, the chain is intermediates + // only and the last entry must not be dropped. + // https://github.com/sigstore/cosign/issues/3976 + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + sub2Cert, _, _ := test.GenerateSubordinateCa(subCert, subKey) + + tests := []struct { + name string + chain []*x509.Certificate + want []*x509.Certificate + }{{ + name: "empty chain contributes nothing", + chain: nil, + want: nil, + }, { + name: "intermediate only, root supplied elsewhere", + chain: []*x509.Certificate{subCert}, + want: []*x509.Certificate{subCert}, + }, { + name: "intermediate and root attached", + chain: []*x509.Certificate{subCert, rootCert}, + want: []*x509.Certificate{subCert}, + }, { + name: "two intermediates, root supplied elsewhere", + chain: []*x509.Certificate{sub2Cert, subCert}, + want: []*x509.Certificate{sub2Cert, subCert}, + }, { + name: "root only", + chain: []*x509.Certificate{rootCert}, + want: nil, + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pool := intermediatePoolFromChain(tc.chain) + if len(tc.want) == 0 { + if pool != nil { + t.Fatalf("expected no pool, got one") + } + return + } + if pool == nil { + t.Fatalf("expected a pool with %d cert(s), got nil", len(tc.want)) + } + if got := len(pool.Subjects()); got != len(tc.want) { //nolint:staticcheck // pool built here, not from the system + t.Errorf("expected %d cert(s) in pool, got %d", len(tc.want), got) + } + }) + } +} + func TestValidateAndUnpackCertWithChainSuccess(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com"