From f020a87e3073890a6a19ad914068aac84352d643 Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Fri, 20 Sep 2024 15:52:56 -0400 Subject: [PATCH] policy.json BYOPKI signature verification API Signed-off-by: Qi Wang --- docs/containers-policy.json.5.md | 27 ++- .../dir-img-cosign-pki-valid/manifest.json | 1 + .../dir-img-cosign-pki-valid/signature-1 | Bin 0 -> 7587 bytes signature/fixtures/pki-cert | 34 ++++ signature/fixtures/pki-chain | 67 +++++++ signature/fixtures/pki_intermediates_crt.pem | 34 ++++ signature/fixtures/pki_roots_crt.pem | 33 +++ signature/pki_cert.go | 85 ++++++++ signature/pki_cert_test.go | 100 +++++++++ signature/policy_config_sigstore.go | 189 +++++++++++++++++- signature/policy_config_sigstore_test.go | 175 ++++++++++++++++ signature/policy_eval_sigstore.go | 96 ++++++++- signature/policy_eval_sigstore_test.go | 65 +++++- signature/policy_types.go | 29 ++- 14 files changed, 925 insertions(+), 10 deletions(-) create mode 100644 signature/fixtures/dir-img-cosign-pki-valid/manifest.json create mode 100644 signature/fixtures/dir-img-cosign-pki-valid/signature-1 create mode 100644 signature/fixtures/pki-cert create mode 100644 signature/fixtures/pki-chain create mode 100644 signature/fixtures/pki_intermediates_crt.pem create mode 100644 signature/fixtures/pki_roots_crt.pem create mode 100644 signature/pki_cert.go create mode 100644 signature/pki_cert_test.go diff --git a/docs/containers-policy.json.5.md b/docs/containers-policy.json.5.md index ad3a1f5db..0301aff31 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,18 @@ 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", + "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 000000000..351244104 --- /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 0000000000000000000000000000000000000000..9cd8f6cb99757a9f59eaf0d6267bd5419dd077c5 GIT binary patch literal 7587 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 IfBn~g1CT@8!~g&Q literal 0 HcmV?d00001 diff --git a/signature/fixtures/pki-cert b/signature/fixtures/pki-cert new file mode 100644 index 000000000..177e83d9b --- /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 000000000..986ee01e9 --- /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 000000000..b8a90df8d --- /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 000000000..abaddcaba --- /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 000000000..fc07df652 --- /dev/null +++ b/signature/pki_cert.go @@ -0,0 +1,85 @@ +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 + caIntermediatesCertificates *x509.CertPool + 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)) + } + } + + var trustedAndUntrustedIntermediatePool *x509.CertPool + if pkiTrustRoot.caIntermediatesCertificates != nil { + trustedAndUntrustedIntermediatePool = pkiTrustRoot.caIntermediatesCertificates.Clone() + } else { + trustedAndUntrustedIntermediatePool = 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 { + trustedAndUntrustedIntermediatePool.AddCert(untrustedIntermediateCert) + } + } + } + + if _, err := untrustedCertificate.Verify(x509.VerifyOptions{ + Intermediates: trustedAndUntrustedIntermediatePool, + 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 +} diff --git a/signature/pki_cert_test.go b/signature/pki_cert_test.go new file mode 100644 index 000000000..55a47ad66 --- /dev/null +++ b/signature/pki_cert_test.go @@ -0,0 +1,100 @@ +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. + intermediatesCerts := x509.NewCertPool() // Empty is valid enough for our purposes. + + for _, tr := range []pkiTrustRoot{ + { + caRootsCertificates: certs, + caIntermediatesCertificates: intermediatesCerts, + }, + } { + err := tr.validate() + assert.Error(t, err) + } + + for _, tr := range []pkiTrustRoot{ + { + caRootsCertificates: certs, + subjectEmail: "", + subjectHostname: "hostname", + }, + { + caRootsCertificates: certs, + subjectEmail: "email", + subjectHostname: "", + }, + { + caRootsCertificates: certs, + caIntermediatesCertificates: intermediatesCerts, + subjectEmail: "", + subjectHostname: "hostname", + }, + { + caRootsCertificates: certs, + caIntermediatesCertificates: intermediatesCerts, + 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) + caIntermediatesCertificates := x509.NewCertPool() + pkiIntermediatesCrtPEM, err := os.ReadFile("fixtures/pki_intermediates_crt.pem") + require.NoError(t, err) + ok = caIntermediatesCertificates.AppendCertsFromPEM(pkiIntermediatesCrtPEM) + require.True(t, ok) + certBytes, err := os.ReadFile("fixtures/pki-cert") + require.NoError(t, err) + chainBytes, err := os.ReadFile("fixtures/pki-chain") + require.NoError(t, err) + + // Success + pk, err := verifyPKI(&pkiTrustRoot{ + caRootsCertificates: caRootsCertificates, + caIntermediatesCertificates: caIntermediatesCertificates, + 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, + caIntermediatesCertificates: caIntermediatesCertificates, + 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, + caIntermediatesCertificates: caIntermediatesCertificates, + 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 965901e18..d273b6552 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 c6ec3d801..ec562b1df 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 9c553771c..f8f479295 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 } @@ -189,13 +198,23 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva } untrustedPayload := sig.UntrustedPayload() + keySources := 0 + if trustRoot.publicKeys != nil { + keySources++ + } + if trustRoot.fulcio != nil { + keySources++ + } + if trustRoot.pki != nil { + keySources++ + } + var publicKeys []crypto.PublicKey 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 keySources > 1: // newPRSigstoreSigned rejects more than one key sources. + return sarRejected, errors.New("Internal inconsistency: More than one of public key, Fulcio, or PKI specified") + case keySources == 0: // newPRSigstoreSigned rejects empty key sources. + return sarRejected, errors.New("Internal inconsistency: A public key, Fulcio, or PKI must be specified.") case trustRoot.publicKeys != nil: if trustRoot.rekorPublicKeys != nil { untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey] @@ -254,6 +273,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 +378,55 @@ 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 { + if len(caIntermediatesCertPEMs) != 1 { + return nil, errors.New(`Internal inconsistency: PKI specified with invalid value from "caIntermediatesPath" or "caIntermediatesData"`) + } + trustedIntermediatePool := x509.NewCertPool() + trustedIntermediates, err := cryptoutils.UnmarshalCertificatesFromPEM(caIntermediatesCertPEMs[0]) + if err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading trusted intermediate certificates: %v", err)) + } + for _, trustedIntermediateCert := range trustedIntermediates { + trustedIntermediatePool.AddCert(trustedIntermediateCert) + } + pki.caIntermediatesCertificates = trustedIntermediatePool + } + + 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 b5e74c4d1..c9542dac7 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 32aa1c0ad..04b990a53 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.