diff --git a/docs/containers-policy.json.5.md b/docs/containers-policy.json.5.md index ad3a1f5db2..0291b13328 100644 --- a/docs/containers-policy.json.5.md +++ b/docs/containers-policy.json.5.md @@ -329,6 +329,14 @@ This requirement requires an image to be signed using a sigstore signature with "oidcIssuer": "https://expected.OIDC.issuer/", "subjectEmail", "expected-signing-user@example.com", }, + "pki": { + "caRootsPath": "/path/to/local/CARoots/file", + "caRootsData": "base64-encoded-CARoots-data", + "caIntermediatesPath": "/path/to/local/CAIntermediates/file", + "caIntermediatesData": "base64-encoded-CAIntermediates-data", + "subjectHostname": "expected-signing-hostname.example.com", + "subjectEmail": "expected-signing-user@example.com" + }, "rekorPublicKeyPath": "/path/to/local/public/key/file", "rekorPublicKeyPaths": ["/path/to/local/public/key/one","/path/to/local/public/key/two"], "rekorPublicKeyData": "base64-encoded-public-key-data", @@ -336,7 +344,7 @@ This requirement requires an image to be signed using a sigstore signature with "signedIdentity": identity_requirement } ``` -Exactly one of `keyPath`, `keyPaths`, `keyData`, `keyDatas` and `fulcio` must be present. +Exactly one of `keyPath`, `keyPaths`, `keyData`, `keyDatas`, `fulcio` and `pki` must be present. If `keyPath` or `keyData` is present, it contains a sigstore public key. Only signatures made by this key are accepted. @@ -350,6 +358,11 @@ Both `oidcIssuer` and `subjectEmail` are mandatory, exactly specifying the expected identity provider, and the identity of the user obtaining the Fulcio certificate. +If `pki` is present, the signature must be based on a non-Fulcio X.509 certificate. +One of `caRootsPath` and `caRootsData` must be specified, containing the public key of the CA. +Only one of `caIntermediatesPath` and `caIntermediatesData` can be present, containing the public key of the intermediate CA. +One of `subjectEmail` and `subjectHostname` must be specified, exactly specifying the expected identity provider, and the identity of the user obtaining the certificate. + At most one of `rekorPublicKeyPath`, `rekorPublicKeyPaths`, `rekorPublicKeyData` and `rekorPublicKeyDatas` can be present; it is mandatory if `fulcio` is specified. If a Rekor public key is specified, @@ -407,6 +420,19 @@ selectively allow individual transports and scopes as desired. "rekorPublicKeyPath": "/path/to/rekor.pub", } ], + /* A Sigstore-signed repository using a certificate generated by the Bring Your Own Public Key Infrastructure (BYOPKI).*/ + "hostname:5000/myns/sigstore-signed-byopki": [ + { + "type": "sigstoreSigned", + "pki": { + "caRootsPath": "/path/to/pki_roots_crt.pem", + "caIntermediatesPath": "/path/to/pki_intermediates_crt.pem", + "oidcIssuer": "https://github.com/login/oauth", + "subjectHostname": "test-user.example.com" + "subjectEmail": "test-user@example.com" + } + } + ], /* A sigstore-signed repository, accepts signatures by /usr/bin/cosign */ "hostname:5000/myns/sigstore-signed-allows-malicious-tag-substitution": [ { diff --git a/signature/fixtures/dir-img-cosign-pki-valid/manifest.json b/signature/fixtures/dir-img-cosign-pki-valid/manifest.json new file mode 100644 index 0000000000..351244104f --- /dev/null +++ b/signature/fixtures/dir-img-cosign-pki-valid/manifest.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:84e2abbb0b1347753fa15b585fb2181509ee296e29eed9f4bd3fd7778d027a98","size":348},"layers":[],"annotations":{"org.opencontainers.image.base.digest":"","org.opencontainers.image.base.name":""}} \ No newline at end of file diff --git a/signature/fixtures/dir-img-cosign-pki-valid/signature-1 b/signature/fixtures/dir-img-cosign-pki-valid/signature-1 new file mode 100644 index 0000000000..9cd8f6cb99 Binary files /dev/null and b/signature/fixtures/dir-img-cosign-pki-valid/signature-1 differ diff --git a/signature/fixtures/pki-cert b/signature/fixtures/pki-cert new file mode 100644 index 0000000000..177e83d9bc --- /dev/null +++ b/signature/fixtures/pki-cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/signature/fixtures/pki-chain b/signature/fixtures/pki-chain new file mode 100644 index 0000000000..986ee01e9f --- /dev/null +++ b/signature/fixtures/pki-chain @@ -0,0 +1,67 @@ +-----BEGIN CERTIFICATE----- +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 +a+uS+G24eP8F/EA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/signature/fixtures/pki_intermediates_crt.pem b/signature/fixtures/pki_intermediates_crt.pem new file mode 100644 index 0000000000..b8a90df8d0 --- /dev/null +++ b/signature/fixtures/pki_intermediates_crt.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +a+uS+G24eP8F/EA= +-----END CERTIFICATE----- diff --git a/signature/fixtures/pki_roots_crt.pem b/signature/fixtures/pki_roots_crt.pem new file mode 100644 index 0000000000..abaddcaba1 --- /dev/null +++ b/signature/fixtures/pki_roots_crt.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/signature/pki_cert.go b/signature/pki_cert.go new file mode 100644 index 0000000000..7189cd0b48 --- /dev/null +++ b/signature/pki_cert.go @@ -0,0 +1,100 @@ +package signature + +import ( + "crypto" + "crypto/x509" + "errors" + "fmt" + "slices" + + "github.com/containers/image/v5/signature/internal" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +type pkiTrustRoot struct { + caRootsCertificates *x509.CertPool + caIntermediateCertificates []byte + subjectEmail string + subjectHostname string +} + +func (p *pkiTrustRoot) validate() error { + if p.subjectEmail == "" && p.subjectHostname == "" { + return errors.New("Internal inconsistency: PKI use set up without subject email or subject hostname") + } + return nil +} + +func verifyPKI(pkiTrustRoot *pkiTrustRoot, untrustedCertificateBytes []byte, untrustedIntermediateChainBytes []byte) (crypto.PublicKey, error) { + + untrustedLeafCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(untrustedCertificateBytes) + if err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("parsing leaf certificate: %v", err)) + } + switch len(untrustedLeafCerts) { + case 0: + return nil, internal.NewInvalidSignatureError("no certificate found in signature certificate data") + case 1: + break // OK + default: + return nil, internal.NewInvalidSignatureError("unexpected multiple certificates present in signature certificate data") + } + untrustedCertificate := untrustedLeafCerts[0] + + if pkiTrustRoot.subjectEmail != "" { + if !slices.Contains(untrustedCertificate.EmailAddresses, pkiTrustRoot.subjectEmail) { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %q not found (got %q)", + pkiTrustRoot.subjectEmail, + untrustedCertificate.EmailAddresses)) + } + } + + if pkiTrustRoot.subjectHostname != "" { + if err = untrustedCertificate.VerifyHostname(pkiTrustRoot.subjectHostname); err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Unexpected subject hostname: %v", err)) + } + } + + untrustedIntermediatePool := x509.NewCertPool() + if len(untrustedIntermediateChainBytes) > 0 { + untrustedIntermediateChain, err := cryptoutils.UnmarshalCertificatesFromPEM(untrustedIntermediateChainBytes) + if err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading certificate chain: %v", err)) + } + if len(untrustedIntermediateChain) > 1 { + for _, untrustedIntermediateCert := range untrustedIntermediateChain { + if isIntermediateCA(untrustedIntermediateCert) { + untrustedIntermediatePool.AddCert(untrustedIntermediateCert) + } + } + } + } + + if len(pkiTrustRoot.caIntermediateCertificates) > 0 { + trustedIntermediates, err := cryptoutils.UnmarshalCertificatesFromPEM(pkiTrustRoot.caIntermediateCertificates) + if err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading trusted intermediate certificates: %v", err)) + } + for _, trustedIntermediateCert := range trustedIntermediates { + untrustedIntermediatePool.AddCert(trustedIntermediateCert) + } + } + + if _, err := untrustedCertificate.Verify(x509.VerifyOptions{ + Intermediates: untrustedIntermediatePool, + Roots: pkiTrustRoot.caRootsCertificates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }); err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("veryfing leaf certificate failed: %v", err)) + } + + return untrustedCertificate.PublicKey, nil +} + +// isIntermediateCA checks if the certificate is an intermediate CA +func isIntermediateCA(cert *x509.Certificate) bool { + // The certificate is an intermediate CA if: + // 1. It is a CA (cert.IsCA is true). + // 2. It is not self-signed (Issuer != Subject). + return cert.IsCA && cert.Issuer.String() != cert.Subject.String() +} diff --git a/signature/pki_cert_test.go b/signature/pki_cert_test.go new file mode 100644 index 0000000000..4382c30716 --- /dev/null +++ b/signature/pki_cert_test.go @@ -0,0 +1,99 @@ +package signature + +import ( + "crypto/x509" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPKITrustRootValidate(t *testing.T) { + certs := x509.NewCertPool() // Empty is valid enough for our purposes. + + for _, tr := range []pkiTrustRoot{ + { + caRootsCertificates: certs, + }, + { + caRootsCertificates: certs, + caIntermediateCertificates: []byte("intermediate"), + }, + } { + err := tr.validate() + assert.Error(t, err) + } + + for _, tr := range []pkiTrustRoot{ + { + caRootsCertificates: certs, + subjectEmail: "", + subjectHostname: "hostname", + }, + { + caRootsCertificates: certs, + subjectEmail: "email", + subjectHostname: "", + }, + { + caRootsCertificates: certs, + caIntermediateCertificates: []byte("intermediate"), + subjectEmail: "", + subjectHostname: "hostname", + }, + { + caRootsCertificates: certs, + caIntermediateCertificates: []byte("intermediate"), + subjectEmail: "email", + subjectHostname: "", + }, + } { + err := tr.validate() + assert.NoError(t, err) + } +} + +func TestPKIVerify(t *testing.T) { + caRootsCertificates := x509.NewCertPool() + pkiRootsCrtPEM, err := os.ReadFile("fixtures/pki_roots_crt.pem") + require.NoError(t, err) + ok := caRootsCertificates.AppendCertsFromPEM(pkiRootsCrtPEM) + require.True(t, ok) + pkiIntermediatesCrtPEM, err := os.ReadFile("fixtures/pki_intermediates_crt.pem") + require.NoError(t, err) + certBytes, err := os.ReadFile("fixtures/pki-cert") + require.NoError(t, err) + chainBytes, err := os.ReadFile("fixtures/pki-chain") + require.NoError(t, err) + + // Succcess + pk, err := verifyPKI(&pkiTrustRoot{ + caRootsCertificates: caRootsCertificates, + caIntermediateCertificates: pkiIntermediatesCrtPEM, + subjectEmail: "qiwan@redhat.com", + subjectHostname: "myhost.example.com", + }, certBytes, chainBytes) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, certBytes, pk) + + // Failure + pk, err = verifyPKI(&pkiTrustRoot{ + caRootsCertificates: caRootsCertificates, + caIntermediateCertificates: pkiIntermediatesCrtPEM, + subjectEmail: "qiwan@redhat.com", + subjectHostname: "not-mutch.example.com", + }, certBytes, chainBytes) + require.Error(t, err) + assert.Nil(t, pk) + + // Failure + pk, err = verifyPKI(&pkiTrustRoot{ + caRootsCertificates: caRootsCertificates, + caIntermediateCertificates: pkiIntermediatesCrtPEM, + subjectEmail: "this-does-not-match@redhat.com", + subjectHostname: "myhost.example.com", + }, certBytes, chainBytes) + require.Error(t, err) + assert.Nil(t, pk) +} diff --git a/signature/policy_config_sigstore.go b/signature/policy_config_sigstore.go index 965901e187..d273b65523 100644 --- a/signature/policy_config_sigstore.go +++ b/signature/policy_config_sigstore.go @@ -71,6 +71,17 @@ func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedO } } +// PRSigstoreSignedWithPKI specifies a value for the "pki" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithPKI(p PRSigstoreSignedPKI) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.PKI != nil { + return InvalidPolicyFormatError(`"pki" already specified`) + } + pr.PKI = p + return nil + } +} + // PRSigstoreSignedWithRekorPublicKeyPath specifies a value for the "rekorPublicKeyPath" field when calling NewPRSigstoreSigned. func PRSigstoreSignedWithRekorPublicKeyPath(rekorPublicKeyPath string) PRSigstoreSignedOption { return func(pr *prSigstoreSigned) error { @@ -159,8 +170,11 @@ func newPRSigstoreSigned(options ...PRSigstoreSignedOption) (*prSigstoreSigned, if res.Fulcio != nil { keySources++ } + if res.PKI != nil { + keySources++ + } if keySources != 1 { - return nil, InvalidPolicyFormatError("exactly one of keyPath, keyPaths, keyData, keyDatas and fulcio must be specified") + return nil, InvalidPolicyFormatError("exactly one of keyPath, keyPaths, keyData, keyDatas, fulcio, and PKI must be specified") } rekorSources := 0 @@ -218,9 +232,10 @@ var _ json.Unmarshaler = (*prSigstoreSigned)(nil) func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error { *pr = prSigstoreSigned{} var tmp prSigstoreSigned - var gotKeyPath, gotKeyPaths, gotKeyData, gotKeyDatas, gotFulcio bool + var gotKeyPath, gotKeyPaths, gotKeyData, gotKeyDatas, gotFulcio, gotPKI bool var gotRekorPublicKeyPath, gotRekorPublicKeyPaths, gotRekorPublicKeyData, gotRekorPublicKeyDatas bool var fulcio prSigstoreSignedFulcio + var pki prSigstoreSignedPKI var signedIdentity json.RawMessage if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { switch key { @@ -253,6 +268,9 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error { case "rekorPublicKeyDatas": gotRekorPublicKeyDatas = true return &tmp.RekorPublicKeyDatas + case "pki": + gotPKI = true + return &pki case "signedIdentity": return &signedIdentity default: @@ -303,6 +321,9 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error { if gotRekorPublicKeyDatas { opts = append(opts, PRSigstoreSignedWithRekorPublicKeyDatas(tmp.RekorPublicKeyDatas)) } + if gotPKI { + opts = append(opts, PRSigstoreSignedWithPKI(&pki)) + } opts = append(opts, PRSigstoreSignedWithSignedIdentity(tmp.SignedIdentity)) res, err := newPRSigstoreSigned(opts...) @@ -440,3 +461,167 @@ func (f *prSigstoreSignedFulcio) UnmarshalJSON(data []byte) error { *f = *res return nil } + +// PRSigstoreSignedPKIOption is a way to pass values to NewPRSigstoreSignedPKI +type PRSigstoreSignedPKIOption func(*prSigstoreSignedPKI) error + +// PRSigstoreSignedPKIWithCARootsPath specifies a value for the "caRootsPath" field when calling NewPRSigstoreSignedPKI +func PRSigstoreSignedPKIWithCARootsPath(caRootsPath string) PRSigstoreSignedPKIOption { + return func(p *prSigstoreSignedPKI) error { + if p.CARootsPath != "" { + return InvalidPolicyFormatError(`"caRootsPath" already specified`) + } + p.CARootsPath = caRootsPath + return nil + } +} + +// PRSigstoreSignedPKIWithCARootsData specifies a value for the "caRootsData" field when calling NewPRSigstoreSignedPKI +func PRSigstoreSignedPKIWithCARootsData(caRootsData []byte) PRSigstoreSignedPKIOption { + return func(p *prSigstoreSignedPKI) error { + if p.CARootsData != nil { + return InvalidPolicyFormatError(`"caRootsData" already specified`) + } + p.CARootsData = caRootsData + return nil + } +} + +// PRSigstoreSignedPKIWithCAIntermediatesPath specifies a value for the "caIntermediatesPath" field when calling NewPRSigstoreSignedPKI +func PRSigstoreSignedPKIWithCAIntermediatesPath(caIntermediatesPath string) PRSigstoreSignedPKIOption { + return func(p *prSigstoreSignedPKI) error { + if p.CAIntermediatesPath != "" { + return InvalidPolicyFormatError(`"caIntermediatesPath" already specified`) + } + p.CAIntermediatesPath = caIntermediatesPath + return nil + } +} + +// PRSigstoreSignedPKIWithCAIntermediatesData specifies a value for the "caIntermediatesData" field when calling NewPRSigstoreSignedPKI +func PRSigstoreSignedPKIWithCAIntermediatesData(caIntermediatesData []byte) PRSigstoreSignedPKIOption { + return func(p *prSigstoreSignedPKI) error { + if p.CAIntermediatesData != nil { + return InvalidPolicyFormatError(`"caIntermediatesData" already specified`) + } + p.CAIntermediatesData = caIntermediatesData + return nil + } +} + +// PRSigstoreSignedPKIWithSubjectEmail specifies a value for the "subjectEmail" field when calling NewPRSigstoreSignedPKI +func PRSigstoreSignedPKIWithSubjectEmail(subjectEmail string) PRSigstoreSignedPKIOption { + return func(p *prSigstoreSignedPKI) error { + if p.SubjectEmail != "" { + return InvalidPolicyFormatError(`"subjectEmail" already specified`) + } + p.SubjectEmail = subjectEmail + return nil + } +} + +// PRSigstoreSignedPKIWithSubjectHostname specifies a value for the "subjectHostname" field when calling NewPRSigstoreSignedPKI +func PRSigstoreSignedPKIWithSubjectHostname(subjectHostname string) PRSigstoreSignedPKIOption { + return func(p *prSigstoreSignedPKI) error { + if p.SubjectHostname != "" { + return InvalidPolicyFormatError(`"subjectHostname" already specified`) + } + p.SubjectHostname = subjectHostname + return nil + } +} + +// newPRSigstoreSignedPKI is NewPRSigstoreSignedPKI, except it returns the private type +func newPRSigstoreSignedPKI(options ...PRSigstoreSignedPKIOption) (*prSigstoreSignedPKI, error) { + res := prSigstoreSignedPKI{} + for _, o := range options { + if err := o(&res); err != nil { + return nil, err + } + } + + if res.CARootsPath != "" && res.CARootsData != nil { + return nil, InvalidPolicyFormatError("caRootsPath and caRootsData cannot be used simultaneously") + } + if res.CARootsPath == "" && res.CARootsData == nil { + return nil, InvalidPolicyFormatError("At least one of caRootsPath and caRootsData must be specified") + } + + if res.CAIntermediatesPath != "" && res.CAIntermediatesData != nil { + return nil, InvalidPolicyFormatError("caIntermediatesPath and caIntermediatesData cannot be used simultaneously") + } + + if res.SubjectEmail == "" && res.SubjectHostname == "" { + return nil, InvalidPolicyFormatError("At least one of subjectEmail, subjectHostname must be specified") + } + + return &res, nil +} + +// NewPRSigstoreSignedPKI returns a PRSigstoreSignedPKI based on options. +func NewPRSigstoreSignedPKI(options ...PRSigstoreSignedPKIOption) (PRSigstoreSignedPKI, error) { + return newPRSigstoreSignedPKI(options...) +} + +// Compile-time check that prSigstoreSignedPKI implements json.Unmarshaler. +var _ json.Unmarshaler = (*prSigstoreSignedPKI)(nil) + +func (p *prSigstoreSignedPKI) UnmarshalJSON(data []byte) error { + *p = prSigstoreSignedPKI{} + var tmp prSigstoreSignedPKI + var gotCARootsPath, gotCARootsData, gotCAIntermediatesPath, gotCAIntermediatesData, gotSubjectEmail, gotSubjectHostname bool + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { + switch key { + case "caRootsPath": + gotCARootsPath = true + return &tmp.CARootsPath + case "caRootsData": + gotCARootsData = true + return &tmp.CARootsData + case "caIntermediatesPath": + gotCAIntermediatesPath = true + return &tmp.CAIntermediatesPath + case "caIntermediatesData": + gotCAIntermediatesData = true + return &tmp.CAIntermediatesData + case "subjectEmail": + gotSubjectEmail = true + return &tmp.SubjectEmail + case "subjectHostname": + gotSubjectHostname = true + return &tmp.SubjectHostname + default: + return nil + } + }); err != nil { + return err + } + + var opts []PRSigstoreSignedPKIOption + if gotCARootsPath { + opts = append(opts, PRSigstoreSignedPKIWithCARootsPath(tmp.CARootsPath)) + } + if gotCARootsData { + opts = append(opts, PRSigstoreSignedPKIWithCARootsData(tmp.CARootsData)) + } + if gotCAIntermediatesPath { + opts = append(opts, PRSigstoreSignedPKIWithCAIntermediatesPath(tmp.CAIntermediatesPath)) + } + if gotCAIntermediatesData { + opts = append(opts, PRSigstoreSignedPKIWithCAIntermediatesData(tmp.CAIntermediatesData)) + } + if gotSubjectEmail { + opts = append(opts, PRSigstoreSignedPKIWithSubjectEmail(tmp.SubjectEmail)) + } + if gotSubjectHostname { + opts = append(opts, PRSigstoreSignedPKIWithSubjectHostname(tmp.SubjectHostname)) + } + + res, err := newPRSigstoreSignedPKI(opts...) + if err != nil { + return err + } + + *p = *res + return nil +} diff --git a/signature/policy_config_sigstore_test.go b/signature/policy_config_sigstore_test.go index c6ec3d8018..ec562b1dfb 100644 --- a/signature/policy_config_sigstore_test.go +++ b/signature/policy_config_sigstore_test.go @@ -698,3 +698,178 @@ func TestPRSigstoreSignedFulcioUnmarshalJSON(t *testing.T) { duplicateFields: []string{"caData", "oidcIssuer", "subjectEmail"}, }.run(t) } + +func TestNewPRSigstoreSignedPKI(t *testing.T) { + const testCARootsPath = "/foo/bar" + testCARootsData := []byte("abc") + const testCAIntermediatesPath = "/foo/baz" + testCAIntermediatesData := []byte("def") + const testSubjectHostname = "https://example.com" + const testSubjectEmail = "test@example.com" + + // Success: + for _, c := range []struct { + options []PRSigstoreSignedPKIOption + expected prSigstoreSignedPKI + }{ + { + options: []PRSigstoreSignedPKIOption{ + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedPKI{ + CARootsPath: testCARootsPath, + SubjectHostname: testSubjectHostname, + SubjectEmail: testSubjectEmail, + }, + }, + { + options: []PRSigstoreSignedPKIOption{ + PRSigstoreSignedPKIWithCARootsData(testCARootsData), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedPKI{ + CARootsData: testCARootsData, + SubjectHostname: testSubjectHostname, + SubjectEmail: testSubjectEmail, + }, + }, + { + options: []PRSigstoreSignedPKIOption{ + PRSigstoreSignedPKIWithCARootsData(testCARootsData), + PRSigstoreSignedPKIWithCAIntermediatesData(testCAIntermediatesData), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedPKI{ + CARootsData: testCARootsData, + CAIntermediatesData: testCAIntermediatesData, + SubjectHostname: testSubjectHostname, + SubjectEmail: testSubjectEmail, + }, + }, + { + options: []PRSigstoreSignedPKIOption{ + PRSigstoreSignedPKIWithCARootsData(testCARootsData), + PRSigstoreSignedPKIWithCAIntermediatesPath(testCAIntermediatesPath), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedPKI{ + CARootsData: testCARootsData, + CAIntermediatesPath: testCAIntermediatesPath, + SubjectHostname: testSubjectHostname, + SubjectEmail: testSubjectEmail, + }, + }, + } { + pr, err := newPRSigstoreSignedPKI(c.options...) + require.NoError(t, err) + assert.Equal(t, &c.expected, pr) + } + + for _, c := range [][]PRSigstoreSignedPKIOption{ + { // Neither caRootsPath nor caRootsData specified + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + }, + { // Both caRootsPath and caRootsData specified + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath), + PRSigstoreSignedPKIWithCARootsData(testCARootsData), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + }, + { // Both caIntermediatesPath and caIntermediatesData specified + PRSigstoreSignedPKIWithCAIntermediatesPath(testCAIntermediatesPath), + PRSigstoreSignedPKIWithCAIntermediatesData(testCAIntermediatesData), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + }, + { // Duplicate caRootsPath + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath), + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath + "1"), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate caRootsData + PRSigstoreSignedPKIWithCARootsData(testCARootsData), + PRSigstoreSignedPKIWithCARootsData([]byte("def")), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + }, + { // Missing subjectEmail and subjectHostname + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath), + }, + { // Duplicate subjectHostname + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname), + PRSigstoreSignedPKIWithSubjectHostname(testSubjectHostname + "1"), + }, + { // Duplicate subjectEmail + PRSigstoreSignedPKIWithCARootsPath(testCARootsPath), + PRSigstoreSignedPKIWithSubjectEmail(testSubjectEmail), + PRSigstoreSignedPKIWithSubjectEmail("1" + testSubjectEmail), + }, + } { + _, err := newPRSigstoreSignedPKI(c...) + logrus.Errorf("%#v", err) + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedPKIUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PRSigstoreSignedPKI]{ + newDest: func() json.Unmarshaler { return &prSigstoreSignedPKI{} }, + newValidObject: func() (PRSigstoreSignedPKI, error) { + return NewPRSigstoreSignedPKI( + PRSigstoreSignedPKIWithCARootsPath("fixtures/pki_roots_crt.pem"), + PRSigstoreSignedPKIWithCAIntermediatesPath("fixtures/pki_intermediates_crt.pem"), + PRSigstoreSignedPKIWithSubjectHostname("myhost.example.com"), + PRSigstoreSignedPKIWithSubjectEmail("qiwan@redhat.com"), + ) + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // Both of "caRootsPath" and "caRootsData" are missing + func(v mSA) { delete(v, "caRootsPath") }, + // Both "caPath" and "caData" is present + func(v mSA) { v["caRootsData"] = "" }, + // Invalid "caPath" field + func(v mSA) { v["caRootsPath"] = 1 }, + // Invalid "caIntermediatesPath" field + func(v mSA) { v["caIntermediatesPath"] = 1 }, + // Invalid "subjectHostname" field + func(v mSA) { v["subjectHostname"] = 1 }, + // Invalid "subjectEmail" field + func(v mSA) { v["subjectEmail"] = 1 }, + // Both "subjectHostname" and "subjectEmail" are missing + func(v mSA) { + delete(v, "subjectHostname") + delete(v, "subjectEmail") + }, + }, + duplicateFields: []string{"caRootsPath", "caIntermediatesPath", "subjectHostname", "subjectEmail"}, + }.run(t) + + // Test caRootsData specifics + policyJSONUmarshallerTests[PRSigstoreSignedPKI]{ + newDest: func() json.Unmarshaler { return &prSigstoreSignedPKI{} }, + newValidObject: func() (PRSigstoreSignedPKI, error) { + return NewPRSigstoreSignedPKI( + PRSigstoreSignedPKIWithCARootsData([]byte("abc")), + PRSigstoreSignedPKIWithCAIntermediatesData([]byte("def")), + PRSigstoreSignedPKIWithSubjectHostname("myhost.example.com"), + PRSigstoreSignedPKIWithSubjectEmail("qiwan@redhat.com"), + ) + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // Invalid "caRootsData" field + func(v mSA) { v["caRootsData"] = 1 }, + func(v mSA) { v["caRootsData"] = "this is invalid base64" }, + // Invalid "caIntermediatesData" field + func(v mSA) { v["caIntermediatesData"] = 1 }, + func(v mSA) { v["caIntermediatesData"] = "this is invalid base64" }, + }, + duplicateFields: []string{"caRootsData", "caIntermediatesData", "subjectHostname", "subjectEmail"}, + }.run(t) +} diff --git a/signature/policy_eval_sigstore.go b/signature/policy_eval_sigstore.go index 9c553771cb..3fa7b20002 100644 --- a/signature/policy_eval_sigstore.go +++ b/signature/policy_eval_sigstore.go @@ -102,6 +102,7 @@ type sigstoreSignedTrustRoot struct { publicKeys []crypto.PublicKey fulcio *fulcioTrustRoot rekorPublicKeys []*ecdsa.PublicKey + pki *pkiTrustRoot } func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error) { @@ -166,6 +167,14 @@ func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error) } } + if pr.PKI != nil { + p, err := pr.PKI.prepareTrustRoot() + if err != nil { + return nil, err + } + res.pki = p + } + return &res, nil } @@ -193,8 +202,12 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva switch { case trustRoot.publicKeys != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations. return sarRejected, errors.New("Internal inconsistency: Both a public key and Fulcio CA specified") - case trustRoot.publicKeys == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations. - return sarRejected, errors.New("Internal inconsistency: Neither a public key nor a Fulcio CA specified") + case trustRoot.publicKeys != nil && trustRoot.pki != nil: // newPRSigstoreSigned rejects such combinations. + return sarRejected, errors.New("Internal inconsistency: Both a public key and PKI specified") + case trustRoot.fulcio != nil && trustRoot.pki != nil: // newPRSigstoreSigned rejects such combinations. + return sarRejected, errors.New("Internal inconsistency: Both Fulcio CA and PKI specified") + case trustRoot.publicKeys == nil && trustRoot.fulcio == nil && trustRoot.pki == nil: // newPRSigstoreSigned rejects such combinations. + return sarRejected, errors.New("Internal inconsistency: A public key, Fulcio, or PKI must be specified.") case trustRoot.publicKeys != nil: if trustRoot.rekorPublicKeys != nil { @@ -254,6 +267,21 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva return sarRejected, err } publicKeys = []crypto.PublicKey{pk} + + case trustRoot.pki != nil: + untrustedCert, ok := untrustedAnnotations[signature.SigstoreCertificateAnnotationKey] + if !ok { + return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreCertificateAnnotationKey) + } + var untrustedIntermediateChainBytes []byte + if untrustedIntermediateChain, ok := untrustedAnnotations[signature.SigstoreIntermediateCertificateChainAnnotationKey]; ok { + untrustedIntermediateChainBytes = []byte(untrustedIntermediateChain) + } + pk, err := verifyPKI(trustRoot.pki, []byte(untrustedCert), untrustedIntermediateChainBytes) + if err != nil { + return sarRejected, err + } + publicKeys = []crypto.PublicKey{pk} } if len(publicKeys) == 0 { @@ -344,3 +372,43 @@ func (pr *prSigstoreSigned) isRunningImageAllowed(ctx context.Context, image pri } return false, summary } + +// prepareTrustRoot creates a pkiTrustRoot from the input data. +// (This also prevents external implementations of this interface, ensuring that prSigstoreSignedPKI is the only one.) +func (p *prSigstoreSignedPKI) prepareTrustRoot() (*pkiTrustRoot, error) { + caRootsCertPEMs, err := loadBytesFromConfigSources(configBytesSources{ + inconsistencyErrorMessage: `Internal inconsistency: both "caRootsPath" and "caRootsData" specified`, + path: p.CARootsPath, + data: p.CARootsData, + }) + if err != nil { + return nil, err + } + if len(caRootsCertPEMs) != 1 { + return nil, errors.New(`Internal inconsistency: PKI specified with not exactly one of "caRootsPath" nor "caRootsData"`) + } + rootsCerts := x509.NewCertPool() + if ok := rootsCerts.AppendCertsFromPEM(caRootsCertPEMs[0]); !ok { + return nil, errors.New("error loading PKI CA Roots certificates") + } + pki := pkiTrustRoot{ + caRootsCertificates: rootsCerts, + subjectEmail: p.SubjectEmail, + subjectHostname: p.SubjectHostname, + } + caIntermediatesCertPEMs, err := loadBytesFromConfigSources(configBytesSources{ + inconsistencyErrorMessage: `Internal inconsistency: both "caIntermediatesPath" and "caIntermediatesData" specified`, + path: p.CAIntermediatesPath, + data: p.CAIntermediatesData, + }) + if err != nil { + return nil, err + } + if caIntermediatesCertPEMs != nil { + pki.caIntermediateCertificates = caIntermediatesCertPEMs[0] + } + if err := pki.validate(); err != nil { + return nil, err + } + return &pki, nil +} diff --git a/signature/policy_eval_sigstore_test.go b/signature/policy_eval_sigstore_test.go index b5e74c4d15..c9542dac7d 100644 --- a/signature/policy_eval_sigstore_test.go +++ b/signature/policy_eval_sigstore_test.go @@ -103,6 +103,13 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) { const testRekorPublicKeyPath = "fixtures/rekor.pub" testRekorPublicKeyData, err := os.ReadFile(testRekorPublicKeyPath) require.NoError(t, err) + testPKI, err := NewPRSigstoreSignedPKI( + PRSigstoreSignedPKIWithCARootsPath("fixtures/pki_roots_crt.pem"), + PRSigstoreSignedPKIWithCAIntermediatesPath("fixtures/pki_intermediates_crt.pem"), + PRSigstoreSignedPKIWithSubjectEmail("qiwan@redhat.com"), + PRSigstoreSignedPKIWithSubjectHostname("myhost.example.com"), + ) + require.NoError(t, err) testIdentity := newPRMMatchRepoDigestOrExact() testIdentityOption := PRSigstoreSignedWithSignedIdentity(testIdentity) @@ -162,6 +169,16 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) { assert.Len(t, res.rekorPublicKeys, rekor.numKeys) } } + // Success with PKI + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithPKI(testPKI), + testIdentityOption, + ) + require.NoError(t, err) + res, err = pr.prepareTrustRoot() + require.NoError(t, err) + assert.Nil(t, res.publicKeys) + assert.NotNil(t, res.pki) // Failure for _, pr := range []prSigstoreSigned{ // Use a prSigstoreSigned because these configurations should be rejected by NewPRSigstoreSigned. @@ -292,6 +309,10 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) { RekorPublicKeyPath: "fixtures/some-rsa-key.pub", SignedIdentity: testIdentity, }, + { // Invalid PKI configuration + PKI: &prSigstoreSignedPKI{}, + SignedIdentity: testIdentity, + }, } { _, err = pr.prepareTrustRoot() assert.Error(t, err) @@ -361,6 +382,8 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { testKeyRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-key-rekor-valid/signature-1") testFulcioRekorImage := dirImageMock(t, "fixtures/dir-img-cosign-fulcio-rekor-valid", "192.168.64.2:5000/cosign-signed/fulcio-rekor-1") testFulcioRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1") + testPKIImage := dirImageMock(t, "fixtures/dir-img-cosign-pki-valid", "localhost:5000/test") + testPKIImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-pki-valid/signature-1") keyData, err := os.ReadFile("fixtures/cosign.pub") require.NoError(t, err) keyData2, err := os.ReadFile("fixtures/cosign2.pub") @@ -532,7 +555,7 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) assertRejected(sar, err) // … but a signature without the intermediate annotation is fine if the issuer is directly trusted - // (which we handle by trusing the intermediates) + // (which we handle by trusting the intermediates) fulcio2, err := NewPRSigstoreSignedFulcio( PRSigstoreSignedFulcioWithCAData([]byte(testFulcioRekorImageSig.UntrustedAnnotations()[signature.SigstoreIntermediateCertificateChainAnnotationKey])), PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), @@ -697,6 +720,46 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { require.NoError(t, err) sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) assertRejected(sar, err) + + // Successful PKI certificate use + pki, err := NewPRSigstoreSignedPKI( + PRSigstoreSignedPKIWithCARootsPath("fixtures/pki_roots_crt.pem"), + PRSigstoreSignedPKIWithCAIntermediatesPath("fixtures/pki_intermediates_crt.pem"), + PRSigstoreSignedPKIWithSubjectEmail("qiwan@redhat.com"), + PRSigstoreSignedPKIWithSubjectHostname("myhost.example.com"), + ) + require.NoError(t, err) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithPKI(pki), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testPKIImage, testPKIImageSig) + assertAccepted(sar, err) + // PKI, missing certificate annotation causes the Cosign-issued signature to be rejected + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testPKIImageSig, signature.SigstoreCertificateAnnotationKey)) + assertRejected(sar, err) + // PKI, missing certificate chain annotation is fine if the issuer is directly trusted + sar, err = pr.isSignatureAccepted(context.Background(), testPKIImage, + sigstoreSignatureWithoutAnnotation(t, testPKIImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) + assertAccepted(sar, err) + + // PKI, missing certificate chain annotation causes the Cosign-issued signature to be rejected + pki2, err := NewPRSigstoreSignedPKI( + PRSigstoreSignedPKIWithCARootsPath("fixtures/pki_roots_crt.pem"), + PRSigstoreSignedPKIWithSubjectEmail("qiwan@redhat.com"), + PRSigstoreSignedPKIWithSubjectHostname("myhost.example.com"), + ) + require.NoError(t, err) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithPKI(pki2), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr2.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testPKIImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) + assertRejected(sar, err) } func TestPRSigstoreSignedIsRunningImageAllowed(t *testing.T) { diff --git a/signature/policy_types.go b/signature/policy_types.go index 32aa1c0ad4..04b990a530 100644 --- a/signature/policy_types.go +++ b/signature/policy_types.go @@ -120,7 +120,7 @@ type prSigstoreSigned struct { // KeyDatas is a set of trusted keys, base64-encoded. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified. KeyDatas [][]byte `json:"keyDatas,omitempty"` - // Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified. + // Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas, Fulcio, and PKI must be specified. // If Fulcio is specified, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well. Fulcio PRSigstoreSignedFulcio `json:"fulcio,omitempty"` @@ -141,6 +141,9 @@ type prSigstoreSigned struct { // otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified). RekorPublicKeyDatas [][]byte `json:"rekorPublicKeyDatas,omitempty"` + // PKI specifies which PKI-generated certificates are accepted. Exactly one of KeyPath, KeyData, Fulcio, PKI must be specified. + PKI PRSigstoreSignedPKI `json:"pki,omitempty"` + // SignedIdentity specifies what image identity the signature must be claiming about the image. // Defaults to "matchRepoDigestOrExact" if not specified. // Note that /usr/bin/cosign interoperability might require using repo-only matching. @@ -167,6 +170,30 @@ type prSigstoreSignedFulcio struct { SubjectEmail string `json:"subjectEmail,omitempty"` } +// PRSigstoreSignedPKI contains PKI configuration options for a "sigstoreSigned" PolicyRequirement. +type PRSigstoreSignedPKI interface { + // prepareTrustRoot creates a pkiTrustRoot from the input data. + // (This also prevents external implementations of this interface, ensuring that prSigstoreSignedPKI is the only one.) + prepareTrustRoot() (*pkiTrustRoot, error) +} + +// prSigstoreSignedPKI contains non-fulcio certificate PKI configuration options for prSigstoreSigned +type prSigstoreSignedPKI struct { + // CARootsPath a path to a file containing accepted CA root certificates, in PEM format. Exactly one of CARootsPath and CARootsData must be specified. + CARootsPath string `json:"caRootsPath"` + // CARootsData contains accepted CA root certificates in PEM format, all of that base64-encoded. Exactly one of CARootsPath and CARootsData must be specified. + CARootsData []byte `json:"caRootsData"` + // CAIntermediatesPath a path to a file containing accepted CA intermediate certificates, in PEM format. Only one of CAIntermediatesPath or CAIntermediatesData can be specified, not both. + CAIntermediatesPath string `json:"caIntermediatesPath"` + // CAIntermediatesData contains accepted CA intermediate certificates in PEM format, all of that base64-encoded. Only one of CAIntermediatesPath or CAIntermediatesData can be specified, not both. + CAIntermediatesData []byte `json:"caIntermediatesData"` + + // SubjectEmail specifies the expected email address imposed on the subject to which the certificate was issued. Exactly one of SubjectEmail and SubjectHostname must be specified. + SubjectEmail string `json:"subjectEmail"` + // SubjectHostname specifies the expected hostname imposed on the subject to which the certificate was issued. Exactly one of SubjectEmail and SubjectHostname must be specified. + SubjectHostname string `json:"subjectHostname"` +} + // PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement. // The type is public, but its implementation is private.