From a1825db13da0d96a7c82670866cb8bd26c8e8753 Mon Sep 17 00:00:00 2001 From: Pritesh Bandi Date: Thu, 27 Jun 2024 00:10:28 -0700 Subject: [PATCH] feat: Implements VerifyBlob functionality (#394) This PR implements VerifyBlob functionality as per https://github.com/notaryproject/specifications/blob/main/specs/trust-store-trust-policy.md --------- Signed-off-by: Pritesh Bandi --- dir/path.go | 10 +- example_signBlob_test.go | 85 ++++ example_verifyBlob_test.go | 151 ++++++ notation.go | 117 ++++- notation_test.go | 112 ++++- verifier/helpers.go | 6 +- verifier/helpers_test.go | 54 +- verifier/trustpolicy/blob.go | 151 ++++++ verifier/trustpolicy/blob_test.go | 168 +++++++ verifier/trustpolicy/oci.go | 257 ++++++++++ verifier/trustpolicy/oci_test.go | 366 ++++++++++++++ verifier/trustpolicy/trustpolicy.go | 368 ++++---------- verifier/trustpolicy/trustpolicy_test.go | 616 +++++++---------------- verifier/verifier.go | 266 +++++++--- verifier/verifier_test.go | 520 ++++++++++++------- 15 files changed, 2244 insertions(+), 1003 deletions(-) create mode 100644 example_signBlob_test.go create mode 100644 example_verifyBlob_test.go create mode 100644 verifier/trustpolicy/blob.go create mode 100644 verifier/trustpolicy/blob_test.go create mode 100644 verifier/trustpolicy/oci.go create mode 100644 verifier/trustpolicy/oci_test.go diff --git a/dir/path.go b/dir/path.go index 08dbd178..28c1db4a 100644 --- a/dir/path.go +++ b/dir/path.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// package dir implements Notation directory structure. +// Package dir implements Notation directory structure. // [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md // // Example: @@ -57,8 +57,14 @@ const ( PathConfigFile = "config.json" // PathSigningKeys is the signingkeys file relative path. PathSigningKeys = "signingkeys.json" - // PathTrustPolicy is the trust policy file relative path. + // PathTrustPolicy is the OCI trust policy file relative path. + // Deprecated: PathTrustPolicy exists for historical compatibility and should not be used. + // To get OCI trust policy path, use PathOCITrustPolicy. PathTrustPolicy = "trustpolicy.json" + // PathOCITrustPolicy is the OCI trust policy file relative path. + PathOCITrustPolicy = "trustpolicy.oci.json" + // PathBlobTrustPolicy is the Blob trust policy file relative path. + PathBlobTrustPolicy = "trustpolicy.blob.json" // PathPlugins is the plugins directory relative path. PathPlugins = "plugins" // LocalKeysDir is the directory name for local key relative path. diff --git a/example_signBlob_test.go b/example_signBlob_test.go new file mode 100644 index 00000000..cab78ca5 --- /dev/null +++ b/example_signBlob_test.go @@ -0,0 +1,85 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notation_test + +import ( + "context" + "fmt" + "strings" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/signer" +) + +// ExampleSignBlob demonstrates how to use signer.BlobSign to sign arbitrary data. +func Example_signBlob() { + //exampleSigner implements notation.Signer and notation.BlobSigner. Given key and X509 certificate chain, + // it provides method to sign OCI artifacts or blobs. + // Users should replace `exampleCertTuple.PrivateKey` with their own private + // key and replace `exampleCerts` with the corresponding certificate chain, + //following the Notary certificate requirements: + // https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements + exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts) + if err != nil { + panic(err) // Handle error + } + + // Both COSE ("application/cose") and JWS ("application/jose+json") + // signature mediaTypes are supported. + exampleSignatureMediaType := jws.MediaTypeEnvelope + exampleContentMediaType := "video/mp4" + + // exampleSignOptions is an example of notation.SignBlobOptions. + exampleSignOptions := notation.SignBlobOptions{ + SignerSignOptions: notation.SignerSignOptions{ + SignatureMediaType: exampleSignatureMediaType, + SigningAgent: "example signing agent", + }, + ContentMediaType: exampleContentMediaType, + UserMetadata: map[string]string{"buildId": "101"}, + } + + // exampleReader reads the data that needs to be signed. This data can be in a file or in memory. + exampleReader := strings.NewReader("example blob") + + // Upon successful signing, signature envelope and signerInfo are returned. + // signatureEnvelope can be used in a verification process later on. + signatureEnvelope, signerInfo, err := notation.SignBlob(context.Background(), exampleSigner, exampleReader, exampleSignOptions) + if err != nil { + panic(err) // Handle error + } + + fmt.Println("Successfully signed") + + // a peek of the signature envelope generated + sigBlob, err := signature.ParseEnvelope(exampleSignatureMediaType, signatureEnvelope) + if err != nil { + panic(err) // Handle error + } + sigContent, err := sigBlob.Content() + if err != nil { + panic(err) // Handle error + } + fmt.Println("signature Payload ContentType:", sigContent.Payload.ContentType) + fmt.Println("signature Payload Content:", string(sigContent.Payload.Content)) + fmt.Println("signerInfo SigningAgent:", signerInfo.UnsignedAttributes.SigningAgent) + + // Output: + // Successfully signed + // signature Payload ContentType: application/vnd.cncf.notary.payload.v1+json + // signature Payload Content: {"targetArtifact":{"annotations":{"buildId":"101"},"digest":"sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61","mediaType":"video/mp4","size":12}} + // signerInfo SigningAgent: example signing agent +} diff --git a/example_verifyBlob_test.go b/example_verifyBlob_test.go new file mode 100644 index 00000000..ce9d94de --- /dev/null +++ b/example_verifyBlob_test.go @@ -0,0 +1,151 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notation_test + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/verifier" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/notaryproject/notation-go/verifier/truststore" +) + +// examplePolicyDocument is an example of a valid trust policy document. +// trust policy document should follow this spec: +// https://github.com/notaryproject/notaryproject/blob/v1.1.0/specs/trust-store-trust-policy.md#trust-policy +var exampleBlobPolicyDocument = trustpolicy.BlobDocument{ + Version: "1.0", + TrustPolicies: []trustpolicy.BlobTrustPolicy{ + { + Name: "test-statement-name", + SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name, Override: map[trustpolicy.ValidationType]trustpolicy.ValidationAction{trustpolicy.TypeRevocation: trustpolicy.ActionSkip}}, + TrustStores: []string{"ca:valid-trust-store"}, + TrustedIdentities: []string{"*"}, + }, + }, +} + +// ExampleVerifyBlob demonstrates how to use verifier.Verify to verify a +// signature of the blob. +func Example_verifyBlob() { + // Both COSE ("application/cose") and JWS ("application/jose+json") + // signature mediaTypes are supported. + exampleSignatureMediaType := jws.MediaTypeEnvelope + + // exampleSignatureEnvelope is a valid signature envelope. + exampleSignatureEnvelope := getSignatureEnvelope() + + // createTrustStoreForBlobVerify creates a trust store directory for demo purpose. + // Users could use the default trust store from Notary and add trusted + // certificates into it following the trust store spec: + // https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store + if err := createTrustStoreForBlobVerify(); err != nil { + panic(err) // Handle error + } + + // exampleVerifier implements notation.Verify and notation.VerifyBlob. + exampleVerifier, err := verifier.NewVerifier(nil, &exampleBlobPolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil) + if err != nil { + panic(err) // Handle error + } + + // exampleReader reads the data that needs to be verified. This data can be in a file or in memory. + exampleReader := strings.NewReader("example blob") + + // exampleVerifyOptions is an example of notation.VerifierVerifyOptions + exampleVerifyOptions := notation.VerifyBlobOptions{ + BlobVerifierVerifyOptions: notation.BlobVerifierVerifyOptions{ + SignatureMediaType: exampleSignatureMediaType, + TrustPolicyName: "test-statement-name", + }, + } + + // upon successful verification, the signature verification outcome is + // returned. + _, outcome, err := notation.VerifyBlob(context.Background(), exampleVerifier, exampleReader, []byte(exampleSignatureEnvelope), exampleVerifyOptions) + if err != nil { + panic(err) // Handle error + } + + fmt.Println("Successfully verified") + + // a peek of the payload inside the signature envelope + fmt.Println("payload ContentType:", outcome.EnvelopeContent.Payload.ContentType) + + // Note, upon successful verification, payload.TargetArtifact from the + // signature envelope matches exactly with our exampleTargetDescriptor. + // (This check has been done for the user inside verifier.Verify.) + fmt.Println("payload Content:", string(outcome.EnvelopeContent.Payload.Content)) + + // Output: + // Successfully verified + // payload ContentType: application/vnd.cncf.notary.payload.v1+json + // payload Content: {"targetArtifact":{"digest":"sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61","mediaType":"video/mp4","size":12}} +} + +func createTrustStoreForBlobVerify() error { + // changing the path of the trust store for demo purpose. + // Users could keep the default value, i.e. os.UserConfigDir. + dir.UserConfigDir = "tmp" + + // an example of a valid X509 self-signed certificate for demo purpose ONLY. + // (This self-signed cert is paired with the private key used to + // generate the `exampleSignatureEnvelopePem` above.) + // Users should replace `exampleX509Certificate` with their own trusted + // certificate and add to the trust store, following the + // Notary certificate requirements: + // https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements + exampleX509Certificate := `-----BEGIN CERTIFICATE----- +MIIEbDCCAtSgAwIBAgIBUzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEl +MCMGA1UEAxMcTm90YXRpb24gRXhhbXBsZSBzZWxmLXNpZ25lZDAgFw0yNDA0MDQy +MTIwMjBaGA8yMTI0MDQwNDIxMjAyMFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT +AldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMT +HE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwggGiMA0GCSqGSIb3DQEBAQUA +A4IBjwAwggGKAoIBgQDGIiN4yCjSVqFELZwxK/BMb8BokP587L8oPrZ1g8H7LudB +moLNDT7vF9xccbCfU3yNuOd0WaOgnENiCs81VHidyJsj1Oz3u+0Zn3ng7V+uZr6m +AIO74efA9ClMiY4i4HIt8IAZF57AL2mzDnCITgSWxikf030Il85MI42STvA+qYuz +ZEOp3XvKo8bDgQFvbtgK0HYYMfrka7VDmIWVo0rBMGm5btI8HOYQ0r9aqsrCxLAv +1AQeOQm+wbRcp4R5PIUJr+REGn7JCbOyXg/7qqHXKKmvV5yrGaraw8gZ5pqP/RHK +XUJIfvD0Vf2epJmsvC+6vXkSWtz+cA8J4GQx4J4SXL57hoYkC5qv39SOLzlWls3I +6fgeO+SZ0sceMd8NKlom/L5eOJBfB3bTQB83hq/3bRtjT7/qCMsL3VcndKkS+vGF +JPw5uTH+pmBgHrLr6tRoRRjwRFuZ0dO05AbdjCaxgVDtFI3wNbaXn/1VlRGySQIS +UNWxCrUsSzndeqwmjqsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM +MAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBgQBdi0SaJAaeKBB0I+Fjcbmc +4zRvHE4GDSMSDnAK97nrZCZ9iwKuY4x6mv9lwQe2P3VXROoL9JmONNf0yaObOwQj +ILGnbe2rzYtUardz2gzh+6KNzJHspRvk1f06mp4496XQ3STMRSr8kno1svKQMy0Y +FRsGMKs4fWHavIAqNXg9ymrZvvXiatN2UiVtAA/jBFScZAWskeb2WHNzORi7H5Z1 +mp5+IlNYQpzdIu/dvLVxzhh2UvkRdsQqsMgt/MOU84RncwUNZM4yI5EGPoaSJdsj +AGNd+UV6ur7QmVI2Q9EZNRlaDJtaoZmKns5j1SlmDXWKbdRmw42ORDudODj/pHA9 ++u+ca9t3uLsbqO9yPm8m+6fyxffWS11QAH6O7EjydJWcEe5tYkPpL6kcaEyQKESm +5CDlsk+W3ElpaUu6tsnGKODvgdAN3m0noC+qxzCMqoCM4+M5V6OptR98MDl2FK0B +5+WF6YHBxf/uqDvFktUczjrIWuyfECywp05bpGAErGE= +-----END CERTIFICATE-----` + + // Adding the certificate into the trust store. + if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil { + return err + } + return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationBlobExample.pem", []byte(exampleX509Certificate), 0600) +} + +func getSignatureEnvelope() string { + return `{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEzODQ6YjhhYjI0ZGFmYmE1Y2Y3ZTRjODljNTYyZjgxMWNmMTA0OTNkNDIwM2RhOTgyZDNiMTM0NWYzNjZjYTg2M2Q5YzJlZDMyM2RiZDBmYjdmZjgzYTgwMzAyY2VmZmE1YTYxIiwibWVkaWFUeXBlIjoidmlkZW8vbXA0Iiwic2l6ZSI6MTJ9fQ","protected":"eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTA0LTA0VDE0OjIwOjIxLTA3OjAwIn0","header":{"x5c":["MIIEbDCCAtSgAwIBAgIBUzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTElMCMGA1UEAxMcTm90YXRpb24gRXhhbXBsZSBzZWxmLXNpZ25lZDAgFw0yNDA0MDQyMTIwMjBaGA8yMTI0MDQwNDIxMjAyMFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMTHE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDGIiN4yCjSVqFELZwxK/BMb8BokP587L8oPrZ1g8H7LudBmoLNDT7vF9xccbCfU3yNuOd0WaOgnENiCs81VHidyJsj1Oz3u+0Zn3ng7V+uZr6mAIO74efA9ClMiY4i4HIt8IAZF57AL2mzDnCITgSWxikf030Il85MI42STvA+qYuzZEOp3XvKo8bDgQFvbtgK0HYYMfrka7VDmIWVo0rBMGm5btI8HOYQ0r9aqsrCxLAv1AQeOQm+wbRcp4R5PIUJr+REGn7JCbOyXg/7qqHXKKmvV5yrGaraw8gZ5pqP/RHKXUJIfvD0Vf2epJmsvC+6vXkSWtz+cA8J4GQx4J4SXL57hoYkC5qv39SOLzlWls3I6fgeO+SZ0sceMd8NKlom/L5eOJBfB3bTQB83hq/3bRtjT7/qCMsL3VcndKkS+vGFJPw5uTH+pmBgHrLr6tRoRRjwRFuZ0dO05AbdjCaxgVDtFI3wNbaXn/1VlRGySQISUNWxCrUsSzndeqwmjqsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBgQBdi0SaJAaeKBB0I+Fjcbmc4zRvHE4GDSMSDnAK97nrZCZ9iwKuY4x6mv9lwQe2P3VXROoL9JmONNf0yaObOwQjILGnbe2rzYtUardz2gzh+6KNzJHspRvk1f06mp4496XQ3STMRSr8kno1svKQMy0YFRsGMKs4fWHavIAqNXg9ymrZvvXiatN2UiVtAA/jBFScZAWskeb2WHNzORi7H5Z1mp5+IlNYQpzdIu/dvLVxzhh2UvkRdsQqsMgt/MOU84RncwUNZM4yI5EGPoaSJdsjAGNd+UV6ur7QmVI2Q9EZNRlaDJtaoZmKns5j1SlmDXWKbdRmw42ORDudODj/pHA9+u+ca9t3uLsbqO9yPm8m+6fyxffWS11QAH6O7EjydJWcEe5tYkPpL6kcaEyQKESm5CDlsk+W3ElpaUu6tsnGKODvgdAN3m0noC+qxzCMqoCM4+M5V6OptR98MDl2FK0B5+WF6YHBxf/uqDvFktUczjrIWuyfECywp05bpGAErGE="],"io.cncf.notary.signingAgent":"example signing agent"},"signature":"liOjdgQ9BKuQTZGXRh3o6P8AMUIq_MKQReEcqA5h8M4RYs3DV_wXfaLCr2x_NRcwjTZsoO1_J77hmzkkk4L0IuFP8Qw0KKtmc83G0yFi4yYV5fwzrIbnhC2GRLuqLPnK-C4qYmv52ld3ebvo7XWwRHu30-VXePmTRFp6iG-eSAgkNgwhxSZ0ZmTFLG3ceNiX2bxpLHlXdPwA3aFKbd6nKrzo4CZ1ZyLNmAIaoA5-kmc0Hyt45trpxaaiWusI_pcTLw71YCqEAs32tEq3q6hRAgAZZN-Qvm9GyNp9EuaPiKjMbJFqtjome5ITxyNd-5t09dDCUgSe3t-iqv2Blm4E080AP1TYwUKLYklGniUP1dAtOau5G2juZLpl7tr4LQ99mycflnAmV7e79eEWXffvy5EAl77dW4_vM7lEemm08m2wddGuDOWXYb1j1r2_a5Xb92umHq6ZMhAp200A0pUkm9640x8z5jdudi_7KeezdqUK7ZMmSxHohiylyKD_20Cy"}` +} diff --git a/notation.go b/notation.go index e5738062..1a6290e3 100644 --- a/notation.go +++ b/notation.go @@ -83,6 +83,12 @@ type SignBlobOptions struct { } // BlobDescriptorGenerator creates descriptor using the digest Algorithm. +// Below is the example of minimal descriptor, it must contain mediatype, digest and size of the artifact +// { +// "mediaType": "application/octet-stream", +// "digest": "sha256:2f3a23b6373afb134ddcd864be8e037e34a662d090d33ee849471ff73c873345", +// "size": 1024 +// } type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error) // BlobSigner is a generic interface for signing arbitrary data. @@ -194,8 +200,8 @@ func SignBlob(ctx context.Context, signer BlobSigner, blobReader io.Reader, sign return nil, nil, errors.New("content media-type cannot be empty") } - if _, _, err := mime.ParseMediaType(signBlobOpts.ContentMediaType); err != nil { - return nil, nil, fmt.Errorf("invalid content media-type '%s': %v", signBlobOpts.ContentMediaType, err) + if err := validateContentMediaType(signBlobOpts.ContentMediaType); err != nil { + return nil, nil, err } getDescFunc := getDescriptorFunc(ctx, blobReader, signBlobOpts.ContentMediaType, signBlobOpts.UserMetadata) @@ -215,9 +221,8 @@ func validateSignArguments(signer any, signOpts SignerSignOptions) error { if signOpts.SignatureMediaType == "" { return errors.New("signature media-type cannot be empty") } - - if !(signOpts.SignatureMediaType == jws.MediaTypeEnvelope || signOpts.SignatureMediaType == cose.MediaTypeEnvelope) { - return fmt.Errorf("invalid signature media-type '%s'", signOpts.SignatureMediaType) + if err := validateSigMediaType(signOpts.SignatureMediaType); err != nil { + return err } return nil @@ -305,14 +310,14 @@ func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) { return payload.TargetArtifact.Annotations, nil } -// VerifierVerifyOptions contains parameters for Verifier.Verify. +// VerifierVerifyOptions contains parameters for Verifier.Verify used for verifying OCI artifact. type VerifierVerifyOptions struct { // ArtifactReference is the reference of the artifact that is being // verified against to. It must be a full reference. ArtifactReference string // SignatureMediaType is the envelope type of the signature. - // Currently both `application/jose+json` and `application/cose` are + // Currently only `application/jose+json` and `application/cose` are // supported. SignatureMediaType string @@ -324,16 +329,43 @@ type VerifierVerifyOptions struct { UserMetadata map[string]string } -// Verifier is a generic interface for verifying an artifact. +// Verifier is a interface for verifying an OCI artifact. type Verifier interface { - // Verify verifies the signature blob `signature` against the target OCI - // artifact with manifest descriptor `desc`, and returns the outcome upon + // Verify verifies the `signature` associated with the target OCI artifact + //with manifest descriptor `desc`, and returns the outcome upon // successful verification. // If nil signature is present and the verification level is not 'skip', // an error will be returned. Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error) } +// BlobVerifierVerifyOptions contains parameters for BlobVerifier.Verify. +type BlobVerifierVerifyOptions struct { + // SignatureMediaType is the envelope type of the signature. + // Currently only `application/jose+json` and `application/cose` are + // supported. + SignatureMediaType string + + // PluginConfig is a map of plugin configs. + PluginConfig map[string]string + + // UserMetadata contains key-value pairs that must be present in the + // signature. + UserMetadata map[string]string + + // TrustPolicyName is the name of trust policy picked by caller. + // If empty, the global trust policy will be applied. + TrustPolicyName string +} + +// BlobVerifier is a generic interface for verifying a blob. +type BlobVerifier interface { + // VerifyBlob verifies the `signature` against the target artifact using the + // descriptor returned by descGenFunc parameter and + // returns the outcome upon successful verification. + VerifyBlob(ctx context.Context, descGenFunc BlobDescriptorGenerator, signature []byte, opts BlobVerifierVerifyOptions) (*VerificationOutcome, error) +} + type verifySkipper interface { // SkipVerify validates whether the verification level is skip. SkipVerify(ctx context.Context, opts VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error) @@ -358,6 +390,55 @@ type VerifyOptions struct { UserMetadata map[string]string } +// VerifyBlobOptions contains parameters for notation.VerifyBlob. +type VerifyBlobOptions struct { + BlobVerifierVerifyOptions + + // ContentMediaType is the media-type type of the content being verified. + ContentMediaType string +} + +// VerifyBlob performs signature verification for a blob using notation supported +// verification types (like integrity, authenticity, etc.) and return the +// successful signature verification outcome. The blob is read using blobReader and +// upon successful verification, it returns the descriptor of the blob. +// For more details on signature verification, see +// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification +func VerifyBlob(ctx context.Context, blobVerifier BlobVerifier, blobReader io.Reader, signature []byte, verifyBlobOpts VerifyBlobOptions) (ocispec.Descriptor, *VerificationOutcome, error) { + if blobVerifier == nil { + return ocispec.Descriptor{}, nil, errors.New("blobVerifier cannot be nil") + } + + if blobReader == nil { + return ocispec.Descriptor{}, nil, errors.New("blobReader cannot be nil") + } + + if len(signature) == 0 { + return ocispec.Descriptor{}, nil, errors.New("signature cannot be nil or empty") + } + + if err := validateContentMediaType(verifyBlobOpts.ContentMediaType); err != nil { + return ocispec.Descriptor{}, nil, err + } + + if err := validateSigMediaType(verifyBlobOpts.SignatureMediaType); err != nil { + return ocispec.Descriptor{}, nil, err + } + + getDescFunc := getDescriptorFunc(ctx, blobReader, verifyBlobOpts.ContentMediaType, verifyBlobOpts.UserMetadata) + vo, err := blobVerifier.VerifyBlob(ctx, getDescFunc, signature, verifyBlobOpts.BlobVerifierVerifyOptions) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + var desc ocispec.Descriptor + if err = json.Unmarshal(vo.EnvelopeContent.Payload.Content, &desc); err != nil { + return ocispec.Descriptor{}, nil, err + } + + return desc, vo, nil +} + // Verify performs signature verification on each of the notation supported // verification types (like integrity, authenticity, etc.) and return the // successful signature verification outcome. @@ -536,3 +617,19 @@ func getDescriptorFunc(ctx context.Context, reader io.Reader, contentMediaType s return addUserMetadataToDescriptor(ctx, targetDesc, userMetadata) } } + +func validateContentMediaType(contentMediaType string) error { + if contentMediaType != "" { + if _, _, err := mime.ParseMediaType(contentMediaType); err != nil { + return fmt.Errorf("invalid content media-type %q: %v", contentMediaType, err) + } + } + return nil +} + +func validateSigMediaType(sigMediaType string) error { + if !(sigMediaType == jws.MediaTypeEnvelope || sigMediaType == cose.MediaTypeEnvelope) { + return fmt.Errorf("invalid signature media-type %q", sigMediaType) + } + return nil +} diff --git a/notation_test.go b/notation_test.go index 03ef24a5..51840959 100644 --- a/notation_test.go +++ b/notation_test.go @@ -26,6 +26,8 @@ import ( "testing" "time" + "oras.land/oras-go/v2/registry/remote" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/cose" "github.com/notaryproject/notation-core-go/signature/jws" @@ -37,7 +39,6 @@ import ( "github.com/notaryproject/notation-go/verifier/trustpolicy" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "oras.land/oras-go/v2/registry/remote" ) var expectedMetadata = map[string]string{"foo": "bar", "bar": "foo"} @@ -117,7 +118,7 @@ func TestSignBlobError(t *testing.T) { }{ {"negativeExpiry", &dummySigner{}, -1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration cannot be a negative value"}, {"milliSecExpiry", &dummySigner{}, 1 * time.Millisecond, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration supports minimum granularity of seconds"}, - {"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type 'video/mp4/zoping': mime: unexpected content after media subtype"}, + {"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"}, {"emptyContentMediaType", &dummySigner{}, 1 * time.Second, reader, "", jws.MediaTypeEnvelope, "content media-type cannot be empty"}, {"invalidSignatureMediaType", &dummySigner{}, 1 * time.Second, reader, "", "", "content media-type cannot be empty"}, {"nilReader", &dummySigner{}, 1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"}, @@ -304,10 +305,11 @@ func TestSignOptsUnknownMediaType(t *testing.T) { } func TestRegistryResolveError(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} + errorMessage := "network error" expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -322,10 +324,11 @@ func TestRegistryResolveError(t *testing.T) { } func TestVerifyEmptyReference(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} + errorMessage := "reference is missing digest or tag" expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -338,8 +341,8 @@ func TestVerifyEmptyReference(t *testing.T) { } func TestVerifyTagReferenceFailed(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := "invalid reference: invalid repository \"UPPERCASE/test\"" @@ -354,9 +357,9 @@ func TestVerifyTagReferenceFailed(t *testing.T) { } func TestVerifyDigestNotMatchResolve(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() repo.MissMatchDigest = true + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest) @@ -390,8 +393,8 @@ func TestSignDigestNotMatchResolve(t *testing.T) { } func TestSkippedSignatureVerification(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip} opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50} @@ -403,8 +406,8 @@ func TestSkippedSignatureVerification(t *testing.T) { } func TestRegistryNoSignatureManifests(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := fmt.Sprintf("no signature is associated with %q, make sure the artifact was signed successfully", mock.SampleArtifactUri) expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -420,8 +423,8 @@ func TestRegistryNoSignatureManifests(t *testing.T) { } func TestRegistryFetchSignatureBlobError(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the Repository, error : network error", mock.SampleDigest, mock.SampleArtifactUri) expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -437,8 +440,8 @@ func TestRegistryFetchSignatureBlobError(t *testing.T) { } func TestVerifyValid(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} // mock the repository @@ -451,8 +454,8 @@ func TestVerifyValid(t *testing.T) { } func TestMaxSignatureAttemptsMissing(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} expectedErr := ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", 0)} @@ -466,10 +469,11 @@ func TestMaxSignatureAttemptsMissing(t *testing.T) { } func TestExceededMaxSignatureAttempts(t *testing.T) { - policyDocument := dummyPolicyDocument() repo := mock.NewRepository() repo.ExceededNumOfSignatures = true + policyDocument := dummyPolicyDocument() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict} + expectedErr := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", 1)} // mock the repository @@ -525,6 +529,63 @@ func TestVerifyFailed(t *testing.T) { }) } +func TestVerifyBlobError(t *testing.T) { + reader := strings.NewReader("some content") + sig := []byte("signature") + testCases := []struct { + name string + verifier BlobVerifier + sig []byte + rdr io.Reader + ctMType string + sigMType string + errMsg string + }{ + {"nilVerifier", nil, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "blobVerifier cannot be nil"}, + {"verifierError", &dummyVerifier{FailVerify: true}, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "failed verify"}, + {"nilSignature", &dummyVerifier{}, nil, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"}, + {"emptySignature", &dummyVerifier{}, []byte{}, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"}, + {"nilReader", &dummyVerifier{}, sig, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"}, + {"invalidContentType", &dummyVerifier{}, sig, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"}, + {"invalidSigType", &dummyVerifier{}, sig, reader, "video/mp4", "hola!", "invalid signature media-type \"hola!\""}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := VerifyBlobOptions{ + BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{ + SignatureMediaType: tc.sigMType, + UserMetadata: nil, + TrustPolicyName: "", + }, + ContentMediaType: tc.ctMType, + } + + _, _, err := VerifyBlob(context.Background(), tc.verifier, tc.rdr, tc.sig, opts) + if err == nil { + t.Fatalf("expected error but didnt found") + } + if err.Error() != tc.errMsg { + t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error()) + } + }) + } +} + +func TestVerifyBlobValid(t *testing.T) { + opts := VerifyBlobOptions{ + BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{ + SignatureMediaType: jws.MediaTypeEnvelope, + UserMetadata: nil, + TrustPolicyName: "", + }, + } + + _, _, err := VerifyBlob(context.Background(), &dummyVerifier{}, strings.NewReader("some content"), []byte("signature"), opts) + if err != nil { + t.Fatalf("SignaureMediaTypeMismatch expected: %v got: %v", nil, err) + } +} + func dummyPolicyDocument() (policyDoc trustpolicy.Document) { policyDoc = trustpolicy.Document{ Version: "1.0", @@ -544,11 +605,12 @@ func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) { return } + type dummySigner struct { fail bool } -func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) { +func (s *dummySigner) Sign(_ context.Context, _ ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) { return []byte("ABC"), &signature.SignerInfo{ SignedAttributes: signature.SignedAttributes{ SigningTime: time.Now(), @@ -575,7 +637,7 @@ func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGene type verifyMetadataSigner struct{} -func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) { +func (s *verifyMetadataSigner) Sign(_ context.Context, desc ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) { for k, v := range expectedMetadata { if desc.Annotations[k] != v { return nil, nil, errors.New("expected metadata not present in descriptor") @@ -589,13 +651,13 @@ func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor } type dummyVerifier struct { - TrustPolicyDoc *trustpolicy.Document + TrustPolicyDoc *trustpolicy.OCIDocument PluginManager plugin.Manager FailVerify bool VerificationLevel trustpolicy.VerificationLevel } -func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error) { +func (v *dummyVerifier) Verify(_ context.Context, _ ocispec.Descriptor, _ []byte, _ VerifierVerifyOptions) (*VerificationOutcome, error) { outcome := &VerificationOutcome{ VerificationResults: []*ValidationResult{}, VerificationLevel: &v.VerificationLevel, @@ -606,6 +668,22 @@ func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, sig return outcome, nil } +func (v *dummyVerifier) VerifyBlob(_ context.Context, _ BlobDescriptorGenerator, _ []byte, _ BlobVerifierVerifyOptions) (*VerificationOutcome, error) { + if v.FailVerify { + return nil, errors.New("failed verify") + } + + return &VerificationOutcome{ + VerificationResults: []*ValidationResult{}, + VerificationLevel: &v.VerificationLevel, + EnvelopeContent: &signature.EnvelopeContent{ + Payload: signature.Payload{ + Content: []byte("{}"), + }, + }, + }, nil +} + var ( reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0" artifactReference = "local/oci-layout@sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0" @@ -614,7 +692,7 @@ var ( type ociDummySigner struct{} -func (s *ociDummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) { +func (s *ociDummySigner) Sign(_ context.Context, _ ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) { sigBlob, err := os.ReadFile(signaturePath) if err != nil { return nil, nil, err diff --git a/verifier/helpers.go b/verifier/helpers.go index d432f0c1..03753fa2 100644 --- a/verifier/helpers.go +++ b/verifier/helpers.go @@ -47,7 +47,7 @@ var VerificationPluginHeaders = []string{ var errExtendedAttributeNotExist = errors.New("extended attribute not exist") -func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policy *trustpolicy.TrustPolicy, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) { +func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) { var typeToLoad truststore.Type switch scheme { case signature.SigningSchemeX509: @@ -60,7 +60,7 @@ func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, po processedStoreSet := set.New[string]() var certificates []*x509.Certificate - for _, trustStore := range policy.TrustStores { + for _, trustStore := range trustStores { if processedStoreSet.Contains(trustStore) { // we loaded this trust store already continue @@ -68,7 +68,7 @@ func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, po storeType, name, found := strings.Cut(trustStore, ":") if !found { - return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, trust policy statement %q is missing separator in trust store value %q. The required format is :", policy.Name, trustStore)} + return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, trust policy statement %q is missing separator in trust store value %q. The required format is :", policyName, trustStore)} } if typeToLoad != truststore.Type(storeType) { continue diff --git a/verifier/helpers_test.go b/verifier/helpers_test.go index 730bb988..ee785258 100644 --- a/verifier/helpers_test.go +++ b/verifier/helpers_test.go @@ -28,25 +28,6 @@ import ( "github.com/notaryproject/notation-go/verifier/truststore" ) -func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) { - policyStatement = trustpolicy.TrustPolicy{ - Name: "test-statement-name", - RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"}, - SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"}, - TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"}, - TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"}, - } - return -} - -func dummyPolicyDocument() (policyDoc trustpolicy.Document) { - policyDoc = trustpolicy.Document{ - Version: "1.0", - TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()}, - } - return -} - func TestGetArtifactDigestFromUri(t *testing.T) { tests := []struct { @@ -79,15 +60,15 @@ func TestLoadX509TrustStore(t *testing.T) { // load "ca" and "signingAuthority" trust store caStore := "ca:valid-trust-store" signingAuthorityStore := "signingAuthority:valid-trust-store" - dummyPolicy := dummyPolicyStatement() + dummyPolicy := dummyOCIPolicyDocument().TrustPolicies[0] dummyPolicy.TrustStores = []string{caStore, signingAuthorityStore} dir.UserConfigDir = "testdata" x509truststore := truststore.NewX509TrustStore(dir.ConfigFS()) - caCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509, &dummyPolicy, x509truststore) + caCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509, dummyPolicy.Name, dummyPolicy.TrustStores, x509truststore) if err != nil { t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err) } - saCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, &dummyPolicy, x509truststore) + saCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, dummyPolicy.Name, dummyPolicy.TrustStores, x509truststore) if err != nil { t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err) } @@ -131,3 +112,32 @@ func getArtifactDigestFromReference(artifactReference string) (string, error) { return artifactReference[i+1:], nil } + +func dummyOCIPolicyDocument() (policyDoc trustpolicy.OCIDocument) { + return trustpolicy.OCIDocument{ + Version: "1.0", + TrustPolicies: []trustpolicy.OCITrustPolicy{ + { + Name: "test-statement-name", + RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"}, + SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"}, + TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"}, + TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"}, + }, + }, + } +} + +func dummyBlobPolicyDocument() (policyDoc trustpolicy.BlobDocument) { + return trustpolicy.BlobDocument{ + Version: "1.0", + TrustPolicies: []trustpolicy.BlobTrustPolicy{ + { + Name: "blob-test-statement-name", + SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"}, + TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"}, + TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"}, + }, + }, + } +} diff --git a/verifier/trustpolicy/blob.go b/verifier/trustpolicy/blob.go new file mode 100644 index 00000000..b016c0ec --- /dev/null +++ b/verifier/trustpolicy/blob.go @@ -0,0 +1,151 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trustpolicy + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/notaryproject/notation-go/dir" + set "github.com/notaryproject/notation-go/internal/container" + "github.com/notaryproject/notation-go/internal/slices" +) + +// BlobDocument represents a trustpolicy.blob.json document +type BlobDocument struct { + // Version of the policy document + Version string `json:"version"` + + // TrustPolicies include each policy statement + TrustPolicies []BlobTrustPolicy `json:"trustPolicies"` +} + +// BlobTrustPolicy represents a policy statement in the blob policy document +type BlobTrustPolicy struct { + // Name of the policy statement + Name string `json:"name"` + + // SignatureVerification setting for this policy statement + SignatureVerification SignatureVerification `json:"signatureVerification"` + + // TrustStores this policy statement uses + TrustStores []string `json:"trustStores"` + + // TrustedIdentities this policy statement pins + TrustedIdentities []string `json:"trustedIdentities"` + + // GlobalPolicy defines if policy statement is global or not + GlobalPolicy bool `json:"globalPolicy,omitempty"` +} + +var supportedBlobPolicyVersions = []string{"1.0"} + +// LoadBlobDocument loads a trust policy document from a local file system +func LoadBlobDocument() (*BlobDocument, error) { + var doc BlobDocument + err := getDocument(dir.PathBlobTrustPolicy, &doc) + return &doc, err +} + +// Validate validates a policy document according to its version's rule set. +// if any rule is violated, returns an error +func (policyDoc *BlobDocument) Validate() error { + // sanity check + if policyDoc == nil { + return errors.New("blob trust policy document cannot be nil") + } + + // Validate Version + if policyDoc.Version == "" { + return errors.New("blob trust policy has empty version, version must be specified") + } + if !slices.Contains(supportedBlobPolicyVersions, policyDoc.Version) { + return fmt.Errorf("blob trust policy document uses unsupported version %q", policyDoc.Version) + } + + // Validate the policy according to 1.0 rules + if len(policyDoc.TrustPolicies) == 0 { + return errors.New("blob trust policy document can not have zero trust policy statements") + } + + policyNames := set.New[string]() + var foundGlobalPolicy bool + for _, statement := range policyDoc.TrustPolicies { + // Verify unique policy statement names across the policy document + if policyNames.Contains(statement.Name) { + return fmt.Errorf("multiple blob trust policy statements use the same name %q, statement names must be unique", statement.Name) + } + + if err := validatePolicyCore(statement.Name, statement.SignatureVerification, statement.TrustStores, statement.TrustedIdentities); err != nil { + return fmt.Errorf("blob trust policy: %w", err) + } + + if statement.GlobalPolicy { + if foundGlobalPolicy { + return errors.New("multiple blob trust policy statements have globalPolicy set to true. Only one trust policy statement can be marked as global policy") + } + + // verificationLevel is skip + if reflect.DeepEqual(statement.SignatureVerification.VerificationLevel, LevelSkip) { + return errors.New("global blob trust policy statement cannot have verification level set to skip") + } + + foundGlobalPolicy = true + } + policyNames.Add(statement.Name) + } + + return nil +} + +// GetApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy for given policy name +// see https://github.com/notaryproject/notaryproject/blob/v1.1.0/specs/trust-store-trust-policy.md#blob-trust-policy +func (policyDoc *BlobDocument) GetApplicableTrustPolicy(policyName string) (*BlobTrustPolicy, error) { + if strings.TrimSpace(policyName) == "" { + return nil, errors.New("policy name cannot be empty") + } + for _, policyStatement := range policyDoc.TrustPolicies { + // exact match + if policyStatement.Name == policyName { + return (&policyStatement).clone(), nil + } + } + + return nil, fmt.Errorf("no applicable blob trust policy with name %q", policyName) +} + +// GetGlobalTrustPolicy returns a pointer to the deep copy of the TrustPolicy that is marked as global policy +// see https://github.com/notaryproject/notaryproject/blob/v1.1.0/specs/trust-store-trust-policy.md#blob-trust-policy +func (policyDoc *BlobDocument) GetGlobalTrustPolicy() (*BlobTrustPolicy, error) { + for _, policyStatement := range policyDoc.TrustPolicies { + if policyStatement.GlobalPolicy { + return (&policyStatement).clone(), nil + } + } + + return nil, fmt.Errorf("no global blob trust policy") +} + +// clone returns a pointer to the deeply copied TrustPolicy +func (t *BlobTrustPolicy) clone() *BlobTrustPolicy { + return &BlobTrustPolicy{ + Name: t.Name, + SignatureVerification: t.SignatureVerification, + TrustedIdentities: append([]string(nil), t.TrustedIdentities...), + TrustStores: append([]string(nil), t.TrustStores...), + GlobalPolicy: t.GlobalPolicy, + } +} diff --git a/verifier/trustpolicy/blob_test.go b/verifier/trustpolicy/blob_test.go new file mode 100644 index 00000000..06ada681 --- /dev/null +++ b/verifier/trustpolicy/blob_test.go @@ -0,0 +1,168 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trustpolicy + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/notaryproject/notation-go/dir" +) + +func TestLoadBlobDocument(t *testing.T) { + tempRoot := t.TempDir() + dir.UserConfigDir = tempRoot + path := filepath.Join(tempRoot, "trustpolicy.blob.json") + policyJson, _ := json.Marshal(dummyBlobPolicyDocument()) + if err := os.WriteFile(path, policyJson, 0600); err != nil { + t.Fatalf("TestLoadBlobDocument write policy file failed. Error: %v", err) + } + t.Cleanup(func() { os.RemoveAll(tempRoot) }) + + if _, err := LoadBlobDocument(); err != nil { + t.Fatalf("LoadBlobDocument() should not throw error for an existing policy file. Error: %v", err) + } +} + +func TestValidate_BlobDocument(t *testing.T) { + policyDoc := dummyBlobPolicyDocument() + if err := policyDoc.Validate(); err != nil { + t.Fatalf("Validate() returned error: %v", err) + } +} + +func TestValidate_BlobDocument_Error(t *testing.T) { + // Sanity check + var nilPolicyDoc *BlobDocument + err := nilPolicyDoc.Validate() + if err == nil || err.Error() != "blob trust policy document cannot be nil" { + t.Fatalf("nil policyDoc should return error") + } + + // empty Version + policyDoc := dummyBlobPolicyDocument() + policyDoc.Version = "" + err = policyDoc.Validate() + if err == nil || err.Error() != "blob trust policy has empty version, version must be specified" { + t.Fatalf("empty version should return error") + } + + // Invalid Version + policyDoc = dummyBlobPolicyDocument() + policyDoc.Version = "invalid" + err = policyDoc.Validate() + if err == nil || err.Error() != "blob trust policy document uses unsupported version \"invalid\"" { + t.Fatalf("invalid version should return error") + } + + // No Policy Statements + policyDoc = dummyBlobPolicyDocument() + policyDoc.TrustPolicies = nil + err = policyDoc.Validate() + if err == nil || err.Error() != "blob trust policy document can not have zero trust policy statements" { + t.Fatalf("zero policy statements should return error") + } + + // No Policy Statement Name + policyDoc = dummyBlobPolicyDocument() + policyDoc.TrustPolicies[0].Name = "" + err = policyDoc.Validate() + if err == nil || err.Error() != "blob trust policy: a trust policy statement is missing a name, every statement requires a name" { + t.Fatalf("policy statement with no name should return an error") + } + + // multiple global rust policy + policyDoc = dummyBlobPolicyDocument() + policyStatement1 := policyDoc.TrustPolicies[0].clone() + policyStatement1.GlobalPolicy = true + policyStatement2 := policyDoc.TrustPolicies[0].clone() + policyStatement2.Name = "test-statement-name-2" + policyStatement2.GlobalPolicy = true + policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement1, *policyStatement2} + err = policyDoc.Validate() + if err == nil || err.Error() != "multiple blob trust policy statements have globalPolicy set to true. Only one trust policy statement can be marked as global policy" { + t.Error(err) + t.Fatalf("multiple global blob policy should return error") + } + + // Policy Document with duplicate policy statement names + policyDoc = dummyBlobPolicyDocument() + policyStatement1 = policyDoc.TrustPolicies[0].clone() + policyStatement2 = policyDoc.TrustPolicies[0].clone() + policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement1, *policyStatement2} + err = policyDoc.Validate() + if err == nil || err.Error() != "multiple blob trust policy statements use the same name \"test-statement-name\", statement names must be unique" { + t.Fatalf("policy statements with same name should return error") + } +} + +func TestGetApplicableTrustPolicy(t *testing.T) { + policyDoc := dummyBlobPolicyDocument() + + policyStatement := policyDoc.TrustPolicies[0].clone() + policyStatement1 := policyStatement.clone() + policyStatement1.Name = "test-statement-name-1" + policyStatement1.GlobalPolicy = true + policyStatement2 := policyStatement.clone() + policyStatement2.Name = "test-statement-name-2" + policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement, *policyStatement1, *policyStatement2} + + validateGetApplicableTrustPolicy(t, policyDoc, "test-statement-name-2", policyStatement2) + validateGetApplicableTrustPolicy(t, policyDoc, "test-statement-name", policyStatement) +} + +func TestGetApplicableTrustPolicy_Error(t *testing.T) { + policyDoc := dummyBlobPolicyDocument() + t.Run("empty policy name", func(t *testing.T) { + _, err := policyDoc.GetApplicableTrustPolicy("") + if err == nil || err.Error() != "policy name cannot be empty" { + t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err) + } + }) + + t.Run("non existent policy name", func(t *testing.T) { + _, err := policyDoc.GetApplicableTrustPolicy("blaah") + if err == nil || err.Error() != "no applicable blob trust policy with name \"blaah\"" { + t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err) + } + }) +} + +func TestGetGlobalTrustPolicy(t *testing.T) { + policyDoc := dummyBlobPolicyDocument() + policyDoc.TrustPolicies[0].GlobalPolicy = true + + policy, err := policyDoc.GetGlobalTrustPolicy() + if err != nil { + t.Fatalf("GetGlobalTrustPolicy() returned error: %v", err) + } + + if !reflect.DeepEqual(*policy, policyDoc.TrustPolicies[0]) { + t.Fatalf("GetGlobalTrustPolicy() returned unexpected policy") + } +} + +func validateGetApplicableTrustPolicy(t *testing.T, policyDoc BlobDocument, policyName string, expectedPolicy *BlobTrustPolicy) { + policy, err := policyDoc.GetApplicableTrustPolicy(policyName) + if err != nil { + t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err) + } + + if reflect.DeepEqual(policy, *expectedPolicy) { + t.Fatalf("GetApplicableTrustPolicy() returned unexpected policy for %s", policyName) + } +} diff --git a/verifier/trustpolicy/oci.go b/verifier/trustpolicy/oci.go new file mode 100644 index 00000000..1203d2b8 --- /dev/null +++ b/verifier/trustpolicy/oci.go @@ -0,0 +1,257 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trustpolicy + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/notaryproject/notation-go/dir" + set "github.com/notaryproject/notation-go/internal/container" + "github.com/notaryproject/notation-go/internal/slices" + "github.com/notaryproject/notation-go/internal/trustpolicy" +) + +// OCIDocument represents a trustPolicy.json document for OCI artifacts +type OCIDocument struct { + // Version of the policy document + Version string `json:"version"` + + // TrustPolicies include each policy statement + TrustPolicies []OCITrustPolicy `json:"trustPolicies"` +} + +// OCITrustPolicy represents a policy statement in the policy document for OCI artifacts +type OCITrustPolicy struct { + // Name of the policy statement + Name string `json:"name"` + + // SignatureVerification setting for this policy statement + SignatureVerification SignatureVerification `json:"signatureVerification"` + + // TrustStores this policy statement uses + TrustStores []string `json:"trustStores"` + + // TrustedIdentities this policy statement pins + TrustedIdentities []string `json:"trustedIdentities"` + + // RegistryScopes that this policy statement affects + RegistryScopes []string `json:"registryScopes"` +} + +// Document represents a trustPolicy.json document +// Deprecated: Document exists for historical compatibility and should not be used. +// To create OCI Document, use OCIDocument. +type Document = OCIDocument + +// TrustPolicy represents a policy statement in the policy document +// Deprecated: TrustPolicy exists for historical compatibility and should not be used. +// To create OCI TrustPolicy, use OCITrustPolicy. +type TrustPolicy = OCITrustPolicy + +// LoadDocument loads a trust policy document from a local file system +// Deprecated: LoadDocument function exists for historical compatibility and should not be used. +// To load OCI Document, use LoadOCIDocument function. +var LoadDocument = LoadOCIDocument + +var supportedOCIPolicyVersions = []string{"1.0"} + +// LoadOCIDocument retrieves a trust policy document from the local file system. +// It attempts to read from dir.PathOCITrustPolicy first; if not found, it tries dir.PathTrustPolicy. +// If both dir.PathOCITrustPolicy and dir.PathTrustPolicy exist, dir.PathOCITrustPolicy will be read. +func LoadOCIDocument() (*OCIDocument, error) { + + var doc OCIDocument + // attempt to load the document from dir.PathOCITrustPolicy + if err := getDocument(dir.PathOCITrustPolicy, &doc); err != nil { + // if the document is not found at the first path, try the second path + if errors.As(err, &errPolicyNotExist{}) { + if err := getDocument(dir.PathTrustPolicy, &doc); err != nil { + return nil, err + } + return &doc, nil + } + // if an error occurred other than the document not found, return it + return nil, err + } + + return &doc, nil +} + +// Validate validates a policy document according to its version's rule set. +// if any rule is violated, returns an error +func (policyDoc *OCIDocument) Validate() error { + // sanity check + if policyDoc == nil { + return errors.New("oci trust policy document cannot be nil") + } + + // Validate Version + if policyDoc.Version == "" { + return errors.New("oci trust policy document has empty version, version must be specified") + } + if !slices.Contains(supportedOCIPolicyVersions, policyDoc.Version) { + return fmt.Errorf("oci trust policy document uses unsupported version %q", policyDoc.Version) + } + + // Validate the policy according to 1.0 rules + if len(policyDoc.TrustPolicies) == 0 { + return errors.New("oci trust policy document can not have zero trust policy statements") + } + + policyNames := set.New[string]() + for _, statement := range policyDoc.TrustPolicies { + // Verify unique policy statement names across the policy document + if policyNames.Contains(statement.Name) { + return fmt.Errorf("multiple oci trust policy statements use the same name %q, statement names must be unique", statement.Name) + } + + if err := validatePolicyCore(statement.Name, statement.SignatureVerification, statement.TrustStores, statement.TrustedIdentities); err != nil { + return fmt.Errorf("oci trust policy: %w", err) + } + + policyNames.Add(statement.Name) + } + + // Verify registry scopes are valid + if err := validateRegistryScopes(policyDoc); err != nil { + return err + } + + return nil +} + +// GetApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy +// statement that applies to the given registry scope. If no applicable trust +// policy is found, returns an error +// see https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#selecting-a-trust-policy-based-on-artifact-uri +func (policyDoc *OCIDocument) GetApplicableTrustPolicy(artifactReference string) (*OCITrustPolicy, error) { + artifactPath, err := getArtifactPathFromReference(artifactReference) + if err != nil { + return nil, err + } + + var wildcardPolicy *OCITrustPolicy + var applicablePolicy *OCITrustPolicy + for _, policyStatement := range policyDoc.TrustPolicies { + if slices.Contains(policyStatement.RegistryScopes, trustpolicy.Wildcard) { + // we need to deep copy because we can't use the loop variable + // address. see https://stackoverflow.com/a/45967429 + wildcardPolicy = (&policyStatement).clone() + } else if slices.Contains(policyStatement.RegistryScopes, artifactPath) { + applicablePolicy = (&policyStatement).clone() + } + } + + if applicablePolicy != nil { + // a policy with exact match for registry scope takes precedence over + // a wildcard (*) policy. + return applicablePolicy, nil + } else if wildcardPolicy != nil { + return wildcardPolicy, nil + } else { + return nil, fmt.Errorf("artifact %q has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: %s", artifactReference, trustPolicyLink) + } +} + +// clone returns a pointer to the deeply copied TrustPolicy +func (t *OCITrustPolicy) clone() *OCITrustPolicy { + return &OCITrustPolicy{ + Name: t.Name, + SignatureVerification: t.SignatureVerification, + TrustedIdentities: append([]string(nil), t.TrustedIdentities...), + TrustStores: append([]string(nil), t.TrustStores...), + RegistryScopes: append([]string(nil), t.RegistryScopes...), + } +} + +// validateRegistryScopes validates if the policy document is following the +// Notary Project spec rules for registry scopes +func validateRegistryScopes(policyDoc *OCIDocument) error { + registryScopeCount := make(map[string]int) + for _, statement := range policyDoc.TrustPolicies { + // Verify registry scopes are valid + if len(statement.RegistryScopes) == 0 { + return fmt.Errorf("oci trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name) + } + if len(statement.RegistryScopes) > 1 && slices.Contains(statement.RegistryScopes, trustpolicy.Wildcard) { + return fmt.Errorf("oci trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name) + } + for _, scope := range statement.RegistryScopes { + if scope != trustpolicy.Wildcard { + if err := validateRegistryScopeFormat(scope); err != nil { + return err + } + } + registryScopeCount[scope]++ + } + } + + // Verify one policy statement per registry scope + for key := range registryScopeCount { + if registryScopeCount[key] > 1 { + return fmt.Errorf("registry scope %q is present in multiple oci trust policy statements, one registry scope value can only be associated with one statement", key) + } + } + + // No error + return nil +} + +func getArtifactPathFromReference(artifactReference string) (string, error) { + // TODO support more types of URI like "domain.com/repository", + // "domain.com/repository:tag" + i := strings.LastIndex(artifactReference, "@") + if i < 0 { + return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified oci artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference) + } + + artifactPath := artifactReference[:i] + if err := validateRegistryScopeFormat(artifactPath); err != nil { + return "", err + } + return artifactPath, nil +} + +// validateRegistryScopeFormat validates if a scope is following the format +// defined in distribution spec +func validateRegistryScopeFormat(scope string) error { + // Domain and Repository regexes are adapted from distribution + // implementation + // https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31 + domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`) + repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + ensureMessage := "make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" + errorMessage := "registry scope %q is not valid, " + ensureMessage + errorWildCardMessage := "registry scope %q with wild card(s) is not valid, " + ensureMessage + + // Check for presence of * in scope + if len(scope) > 1 && strings.Contains(scope, "*") { + return fmt.Errorf(errorWildCardMessage, scope) + } + + domain, repository, found := strings.Cut(scope, "/") + if !found { + return fmt.Errorf(errorMessage, scope) + } + + if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) { + return fmt.Errorf(errorMessage, scope) + } + + // No errors + return nil +} diff --git a/verifier/trustpolicy/oci_test.go b/verifier/trustpolicy/oci_test.go new file mode 100644 index 00000000..2eff90ec --- /dev/null +++ b/verifier/trustpolicy/oci_test.go @@ -0,0 +1,366 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trustpolicy + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/notaryproject/notation-go/dir" +) + +func TestLoadOCIDocumentFromOldFileLocation(t *testing.T) { + tempRoot := t.TempDir() + dir.UserConfigDir = tempRoot + path := filepath.Join(tempRoot, "trustpolicy.json") + policyJson, _ := json.Marshal(dummyOCIPolicyDocument()) + if err := os.WriteFile(path, policyJson, 0600); err != nil { + t.Fatalf("TestLoadOCIDocument write policy file failed. Error: %v", err) + } + t.Cleanup(func() { os.RemoveAll(tempRoot) }) + + if _, err := LoadOCIDocument(); err != nil { + t.Fatalf("LoadOCIDocument() should not throw error for an existing policy file. Error: %v", err) + } +} + +func TestLoadOCIDocumentFromNewFileLocation(t *testing.T) { + tempRoot := t.TempDir() + dir.UserConfigDir = tempRoot + path := filepath.Join(tempRoot, "trustpolicy.oci.json") + policyJson, _ := json.Marshal(dummyOCIPolicyDocument()) + if err := os.WriteFile(path, policyJson, 0600); err != nil { + t.Fatalf("TestLoadOCIDocument write policy file failed. Error: %v", err) + } + t.Cleanup(func() { os.RemoveAll(tempRoot) }) + + if _, err := LoadOCIDocument(); err != nil { + t.Fatalf("LoadOCIDocument() should not throw error for an existing policy file. Error: %v", err) + } +} + +func TestLoadOCIDocumentError(t *testing.T) { + tempRoot := t.TempDir() + dir.UserConfigDir = tempRoot + if _, err := LoadOCIDocument(); err == nil { + t.Fatalf("LoadOCIDocument() should throw error if OCI trust policy is not found") + } +} + +// TestApplicableTrustPolicy tests filtering policies against registry scopes +func TestApplicableTrustPolicy(t *testing.T) { + policyDoc := dummyOCIPolicyDocument() + + policyStatement := policyDoc.TrustPolicies[0] + policyStatement.Name = "test-statement-name-1" + registryScope := "registry.wabbit-networks.io/software/unsigned/net-utils" + registryUri := fmt.Sprintf("%s@sha256:hash", registryScope) + policyStatement.RegistryScopes = []string{registryScope} + policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "strict"} + + policyDoc.TrustPolicies = []OCITrustPolicy{ + policyStatement, + } + // existing Registry Scope + policy, err := (&policyDoc).GetApplicableTrustPolicy(registryUri) + if policy.Name != policyStatement.Name || err != nil { + t.Fatalf("GetApplicableTrustPolicy() should return %q for registry scope %q", policyStatement.Name, registryScope) + } + + // non-existing Registry Scope + policy, err = (&policyDoc).GetApplicableTrustPolicy("non.existing.scope/repo@sha256:hash") + if policy != nil || err == nil || err.Error() != "artifact \"non.existing.scope/repo@sha256:hash\" has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy" { + t.Fatalf("GetApplicableTrustPolicy() should return nil for non existing registry scope") + } + + // wildcard registry scope + wildcardStatement := OCITrustPolicy{ + Name: "test-statement-name-2", + SignatureVerification: SignatureVerification{VerificationLevel: "skip"}, + TrustStores: []string{}, + TrustedIdentities: []string{}, + RegistryScopes: []string{"*"}, + } + + policyDoc.TrustPolicies = []OCITrustPolicy{ + policyStatement, + wildcardStatement, + } + policy, err = (&policyDoc).GetApplicableTrustPolicy("some.registry.that/has.no.policy@sha256:hash") + if policy.Name != wildcardStatement.Name || err != nil { + t.Fatalf("GetApplicableTrustPolicy() should return wildcard policy for registry scope \"some.registry.that/has.no.policy\"") + } +} + +// TestValidatePolicyDocument calls policyDoc.Validate() +// and tests various validations on policy elements +func TestValidateInvalidPolicyDocument(t *testing.T) { + // Sanity check + var nilPolicyDoc *OCIDocument + err := nilPolicyDoc.Validate() + if err == nil || err.Error() != "oci trust policy document cannot be nil" { + t.Fatalf("nil policyDoc should return error") + } + + // Invalid Version + policyDoc := dummyOCIPolicyDocument() + policyDoc.Version = "invalid" + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy document uses unsupported version \"invalid\"" { + t.Fatalf("invalid version should return error") + } + + // No Policy Statements + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies = nil + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy document can not have zero trust policy statements" { + t.Fatalf("zero policy statements should return error") + } + + // No Policy Statement Name + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].Name = "" + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: a trust policy statement is missing a name, every statement requires a name" { + t.Fatalf("policy statement with no name should return an error") + } + + // No Registry Scopes + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].RegistryScopes = nil + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy statement \"test-statement-name\" has zero registry scopes, it must specify registry scopes with at least one value" { + t.Fatalf("policy statement with registry scopes should return error") + } + + // Multiple policy statements with same registry scope + policyDoc = dummyOCIPolicyDocument() + policyStatement1 := policyDoc.TrustPolicies[0].clone() + policyStatement2 := policyDoc.TrustPolicies[0].clone() + policyStatement2.Name = "test-statement-name-2" + policyDoc.TrustPolicies = []OCITrustPolicy{*policyStatement1, *policyStatement2} + err = policyDoc.Validate() + if err == nil || err.Error() != "registry scope \"registry.acme-rockets.io/software/net-monitor\" is present in multiple oci trust policy statements, one registry scope value can only be associated with one statement" { + t.Fatalf("Policy statements with same registry scope should return error %q", err) + } + + // Registry scopes with a wildcard + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].RegistryScopes = []string{"*", "registry.acme-rockets.io/software/net-monitor"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy statement \"test-statement-name\" uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values" { + t.Fatalf("policy statement with more than a wildcard registry scope should return error") + } + + // Invalid SignatureVerification + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "invalid"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has invalid signatureVerification: invalid signature verification level \"invalid\"" { + t.Fatalf("policy statement with invalid SignatureVerification should return error") + } + + // strict SignatureVerification should have a trust store + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustStores = []string{} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" { + t.Fatalf("strict SignatureVerification should have a trust store") + } + + // strict SignatureVerification should have trusted identities + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustedIdentities = []string{} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" { + t.Fatalf("strict SignatureVerification should have trusted identities") + } + + // skip SignatureVerification should not have trust store or trusted identities + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "skip"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped" { + t.Fatalf("strict SignatureVerification should have trusted identities") + } + + // Empty Trusted Identity should throw error + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustedIdentities = []string{""} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has an empty trusted identity" { + t.Fatalf("policy statement with empty trusted identity should return error") + } + + // Trusted Identity without separator should throw error + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustedIdentities = []string{"x509.subject"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator" { + t.Fatalf("policy statement with trusted identity missing separator should return error") + } + + // Empty Trusted Identity value should throw error + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustedIdentities = []string{"x509.subject:"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" { + t.Fatalf("policy statement with trusted identity missing identity value should return error") + } + + // trust store/trusted identities are optional for skip SignatureVerification + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "skip"} + policyDoc.TrustPolicies[0].TrustStores = []string{} + policyDoc.TrustPolicies[0].TrustedIdentities = []string{} + err = policyDoc.Validate() + if err != nil { + t.Fatalf("skip SignatureVerification should not require a trust store or trusted identities") + } + + // Trust Store missing separator + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustStores = []string{"ca"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has malformed trust store value \"ca\". The required format is :" { + t.Fatalf("policy statement with trust store missing separator should return error") + } + + // Invalid Trust Store type + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustStores = []string{"invalid:test-trust-store"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses an unsupported trust store type \"invalid\" in trust store value \"invalid:test-trust-store\"" { + t.Fatalf("policy statement with invalid trust store type should return error") + } + + // Empty Named Store + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustStores = []string{"ca:"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses an unsupported trust store name \"\" in trust store value \"ca:\". Named store name needs to follow [a-zA-Z0-9_.-]+ format" { + t.Fatalf("policy statement with trust store missing named store should return error") + } + + // trusted identities with a wildcard + policyDoc = dummyOCIPolicyDocument() + policyDoc.TrustPolicies[0].TrustedIdentities = []string{"*", "test-identity"} + err = policyDoc.Validate() + if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" { + t.Fatalf("policy statement with more than a wildcard trusted identity should return error") + } + + // Policy Document with duplicate policy statement names + policyDoc = dummyOCIPolicyDocument() + policyStatement1 = policyDoc.TrustPolicies[0].clone() + policyStatement2 = policyDoc.TrustPolicies[0].clone() + policyStatement2.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"} + policyDoc.TrustPolicies = []OCITrustPolicy{*policyStatement1, *policyStatement2} + err = policyDoc.Validate() + if err == nil || err.Error() != "multiple oci trust policy statements use the same name \"test-statement-name\", statement names must be unique" { + t.Fatalf("policy statements with same name should return error") + } +} + +// TestValidRegistryScopes tests valid scopes are accepted +func TestValidRegistryScopes(t *testing.T) { + policyDoc := dummyOCIPolicyDocument() + validScopes := []string{ + "*", "example.com/rep", "example.com:8080/rep/rep2", "example.com/rep/subrep/subsub", + "10.10.10.10:8080/rep/rep2", "domain/rep", "domain:1234/rep", + } + + for _, scope := range validScopes { + policyDoc.TrustPolicies[0].RegistryScopes = []string{scope} + err := policyDoc.Validate() + if err != nil { + t.Fatalf("valid registry scope should not return error. Error : %q", err) + } + } +} + +// TestInvalidRegistryScopes tests invalid scopes are rejected +func TestInvalidRegistryScopes(t *testing.T) { + policyDoc := dummyOCIPolicyDocument() + invalidScopes := []string{ + "", "1:1", "a,b", "abcd", "1111", "1,2", "example.com/rep:tag", + "example.com/rep/subrep/sub:latest", "example.com", "rep/rep2:latest", + "repository", "10.10.10.10", "10.10.10.10:8080/rep/rep2:latest", + } + + for _, scope := range invalidScopes { + policyDoc.TrustPolicies[0].RegistryScopes = []string{scope} + err := policyDoc.Validate() + if err == nil || err.Error() != "registry scope \""+scope+"\" is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" { + t.Fatalf("invalid registry scope should return error. Error : %q", err) + } + } + + // Test invalid scope with wild card suffix + invalidWildCardScopes := []string{"example.com/*", "*/", "example*/", "ex*test"} + for _, scope := range invalidWildCardScopes { + policyDoc.TrustPolicies[0].RegistryScopes = []string{scope} + err := policyDoc.Validate() + if err == nil || err.Error() != "registry scope \""+scope+"\" with wild card(s) is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" { + t.Fatalf("invalid registry scope should return error. Error : %q", err) + } + } +} + +// TestValidateValidPolicyDocument tests a happy policy document +func TestValidateValidPolicyDocument(t *testing.T) { + policyDoc := dummyOCIPolicyDocument() + + policyStatement1 := policyDoc.TrustPolicies[0].clone() + + policyStatement2 := policyStatement1.clone() + policyStatement2.Name = "test-statement-name-2" + policyStatement2.RegistryScopes = []string{"registry.wabbit-networks.io/software/unsigned/net-utils"} + policyStatement2.SignatureVerification = SignatureVerification{VerificationLevel: "permissive"} + + policyStatement3 := policyStatement1.clone() + policyStatement3.Name = "test-statement-name-3" + policyStatement3.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"} + policyStatement3.TrustStores = []string{} + policyStatement3.TrustedIdentities = []string{} + policyStatement3.SignatureVerification = SignatureVerification{VerificationLevel: "skip"} + + policyStatement4 := policyStatement1.clone() + policyStatement4.Name = "test-statement-name-4" + policyStatement4.RegistryScopes = []string{"*"} + policyStatement4.TrustStores = []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store-2"} + policyStatement4.SignatureVerification = SignatureVerification{VerificationLevel: "audit"} + + policyStatement5 := policyStatement1.clone() + policyStatement5.Name = "test-statement-name-5" + policyStatement5.RegistryScopes = []string{"registry.acme-rockets2.io/software"} + policyStatement5.TrustedIdentities = []string{"*"} + policyStatement5.SignatureVerification = SignatureVerification{VerificationLevel: "strict"} + + policyDoc.TrustPolicies = []OCITrustPolicy{ + *policyStatement1, + *policyStatement2, + *policyStatement3, + *policyStatement4, + *policyStatement5, + } + err := policyDoc.Validate() + if err != nil { + t.Fatalf("validation failed on a good policy document. Error : %q", err) + } +} diff --git a/verifier/trustpolicy/trustpolicy.go b/verifier/trustpolicy/trustpolicy.go index b0aaede5..e4f86c85 100644 --- a/verifier/trustpolicy/trustpolicy.go +++ b/verifier/trustpolicy/trustpolicy.go @@ -21,8 +21,6 @@ import ( "fmt" "io/fs" "os" - "path/filepath" - "regexp" "strings" "github.com/notaryproject/notation-go/dir" @@ -134,187 +132,16 @@ var ( } ) -var supportedPolicyVersions = []string{"1.0"} - -// Document represents a trustPolicy.json document -type Document struct { - // Version of the policy document - Version string `json:"version"` - - // TrustPolicies include each policy statement - TrustPolicies []TrustPolicy `json:"trustPolicies"` -} - -// TrustPolicy represents a policy statement in the policy document -type TrustPolicy struct { - // Name of the policy statement - Name string `json:"name"` - - // RegistryScopes that this policy statement affects - RegistryScopes []string `json:"registryScopes"` - - // SignatureVerification setting for this policy statement - SignatureVerification SignatureVerification `json:"signatureVerification"` - - // TrustStores this policy statement uses - TrustStores []string `json:"trustStores,omitempty"` - - // TrustedIdentities this policy statement pins - TrustedIdentities []string `json:"trustedIdentities,omitempty"` -} - // SignatureVerification represents verification configuration in a trust policy type SignatureVerification struct { VerificationLevel string `json:"level"` Override map[ValidationType]ValidationAction `json:"override,omitempty"` } -// Validate validates a policy document according to its version's rule set. -// if any rule is violated, returns an error -func (policyDoc *Document) Validate() error { - // sanity check - if policyDoc == nil { - return errors.New("trust policy document cannot be nil") - } - - // Validate Version - if policyDoc.Version == "" { - return errors.New("trust policy document is missing or has empty version, it must be specified") - } - if !slices.Contains(supportedPolicyVersions, policyDoc.Version) { - return fmt.Errorf("trust policy document uses unsupported version %q", policyDoc.Version) - } - - // Validate the policy according to 1.0 rules - if len(policyDoc.TrustPolicies) == 0 { - return errors.New("trust policy document can not have zero trust policy statements") - } - - policyStatementNameCount := make(map[string]int) - - for _, statement := range policyDoc.TrustPolicies { - - // Verify statement name is valid - if statement.Name == "" { - return errors.New("a trust policy statement is missing a name, every statement requires a name") - } - policyStatementNameCount[statement.Name]++ - - // Verify signature verification is valid - verificationLevel, err := statement.SignatureVerification.GetVerificationLevel() - if err != nil { - return fmt.Errorf("trust policy statement %q has invalid signatureVerification: %w", statement.Name, err) - } - - // Any signature verification other than "skip" needs a trust store and - // trusted identities - if verificationLevel.Name == "skip" { - if len(statement.TrustStores) > 0 || len(statement.TrustedIdentities) > 0 { - return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped", statement.Name) - } - } else { - if len(statement.TrustStores) == 0 || len(statement.TrustedIdentities) == 0 { - return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", statement.Name) - } - - // Verify Trust Store is valid - if err := validateTrustStore(statement); err != nil { - return err - } - - // Verify Trusted Identities are valid - if err := validateTrustedIdentities(statement); err != nil { - return err - } - } - - } - - // Verify registry scopes are valid - if err := validateRegistryScopes(policyDoc); err != nil { - return err - } - - // Verify unique policy statement names across the policy document - for key := range policyStatementNameCount { - if policyStatementNameCount[key] > 1 { - return fmt.Errorf("multiple trust policy statements use the same name %q, statement names must be unique", key) - } - } - - // No errors - return nil -} - -// GetApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy -// statement that applies to the given registry scope. If no applicable trust -// policy is found, returns an error -// see https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.2/specs/trust-store-trust-policy.md#selecting-a-trust-policy-based-on-artifact-uri -func (trustPolicyDoc *Document) GetApplicableTrustPolicy(artifactReference string) (*TrustPolicy, error) { - artifactPath, err := getArtifactPathFromReference(artifactReference) - if err != nil { - return nil, err - } - - var wildcardPolicy *TrustPolicy - var applicablePolicy *TrustPolicy - for _, policyStatement := range trustPolicyDoc.TrustPolicies { - if slices.Contains(policyStatement.RegistryScopes, trustpolicy.Wildcard) { - // we need to deep copy because we can't use the loop variable - // address. see https://stackoverflow.com/a/45967429 - wildcardPolicy = (&policyStatement).clone() - } else if slices.Contains(policyStatement.RegistryScopes, artifactPath) { - applicablePolicy = (&policyStatement).clone() - } - } - - if applicablePolicy != nil { - // a policy with exact match for registry scope takes precedence over - // a wildcard (*) policy. - return applicablePolicy, nil - } else if wildcardPolicy != nil { - return wildcardPolicy, nil - } else { - return nil, fmt.Errorf("artifact %q has no applicable trust policy. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: %s", artifactReference, trustPolicyLink) - } -} - -// LoadDocument loads a trust policy document from a local file system -func LoadDocument() (*Document, error) { - path, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy) - if err != nil { - return nil, err - } - - // throw error if path is a directory or a symlink or does not exist. - fileInfo, err := os.Lstat(path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink) - } - return nil, err - } - - mode := fileInfo.Mode() - if mode.IsDir() || mode&fs.ModeSymlink != 0 { - return nil, fmt.Errorf("trust policy is not a regular file (symlinks are not supported). To create a trust policy, see: %s", trustPolicyLink) - } - - jsonFile, err := os.Open(path) - if err != nil { - if errors.Is(err, os.ErrPermission) { - return nil, fmt.Errorf("unable to read trust policy due to file permissions, please verify the permissions of %s", filepath.Join(dir.UserConfigDir, dir.PathTrustPolicy)) - } - return nil, err - } - defer jsonFile.Close() +type errPolicyNotExist struct{} - policyDocument := &Document{} - err = json.NewDecoder(jsonFile).Decode(policyDocument) - if err != nil { - return nil, fmt.Errorf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink) - } - return policyDocument, nil +func (e errPolicyNotExist) Error() string { + return fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink) } // GetVerificationLevel returns VerificationLevel struct for the given @@ -389,30 +216,91 @@ func (signatureVerification *SignatureVerification) GetVerificationLevel() (*Ver return customVerificationLevel, nil } -// clone returns a pointer to the deeply copied TrustPolicy -func (t *TrustPolicy) clone() *TrustPolicy { - return &TrustPolicy{ - Name: t.Name, - SignatureVerification: t.SignatureVerification, - RegistryScopes: append([]string(nil), t.RegistryScopes...), - TrustedIdentities: append([]string(nil), t.TrustedIdentities...), - TrustStores: append([]string(nil), t.TrustStores...), +func getDocument(path string, v any) error { + path, err := dir.ConfigFS().SysPath(path) + if err != nil { + return err + } + + // throw error if path is a directory or a symlink or does not exist. + fileInfo, err := os.Lstat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return errPolicyNotExist{} + } + return err + } + + mode := fileInfo.Mode() + if mode.IsDir() || mode&fs.ModeSymlink != 0 { + return fmt.Errorf("trust policy is not a regular file (symlinks are not supported). To create a trust policy, see: %s", trustPolicyLink) + } + + jsonFile, err := os.Open(path) + if err != nil { + if errors.Is(err, os.ErrPermission) { + return fmt.Errorf("unable to read trust policy due to file permissions, please verify the permissions of %s", path) + } + return err + } + defer jsonFile.Close() + + err = json.NewDecoder(jsonFile).Decode(v) + if err != nil { + return fmt.Errorf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink) + } + return nil +} + +func validatePolicyCore(name string, signatureVerification SignatureVerification, trustStores, trustedIdentities []string) error { + // Verify statement name is valid + if name == "" { + return errors.New("a trust policy statement is missing a name, every statement requires a name") + } + + // Verify signature verification is valid + verificationLevel, err := signatureVerification.GetVerificationLevel() + if err != nil { + return fmt.Errorf("trust policy statement %q has invalid signatureVerification: %w", name, err) + } + + // Any signature verification other than "skip" needs a trust store and + // trusted identities + if verificationLevel.Name == "skip" { + if len(trustStores) > 0 || len(trustedIdentities) > 0 { + return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped", name) + } + } else { + if len(trustStores) == 0 || len(trustedIdentities) == 0 { + return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", name) + } + + // Verify Trust Store is valid + if err := validateTrustStore(name, trustStores); err != nil { + return err + } + + // Verify Trusted Identities are valid + if err := validateTrustedIdentities(name, trustedIdentities); err != nil { + return err + } } + return nil } // validateTrustStore validates if the policy statement is following the -// Notary Project spec rules for truststores -func validateTrustStore(statement TrustPolicy) error { - for _, trustStore := range statement.TrustStores { +// Notary Project spec rules for truststore +func validateTrustStore(policyName string, trustStores []string) error { + for _, trustStore := range trustStores { storeType, namedStore, found := strings.Cut(trustStore, ":") if !found { - return fmt.Errorf("trust policy statement %q has malformed trust store value %q. The required format is :", statement.Name, trustStore) + return fmt.Errorf("trust policy statement %q has malformed trust store value %q. The required format is :", policyName, trustStore) } if !isValidTrustStoreType(storeType) { - return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, storeType, trustStore) + return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", policyName, storeType, trustStore) } if !file.IsValidFileName(namedStore) { - return fmt.Errorf("trust policy statement %q uses an unsupported trust store name %q in trust store value %q. Named store name needs to follow [a-zA-Z0-9_.-]+ format", statement.Name, namedStore, trustStore) + return fmt.Errorf("trust policy statement %q uses an unsupported trust store name %q in trust store value %q. Named store name needs to follow [a-zA-Z0-9_.-]+ format", policyName, namedStore, trustStore) } } @@ -421,35 +309,35 @@ func validateTrustStore(statement TrustPolicy) error { // validateTrustedIdentities validates if the policy statement is following the // Notary Project spec rules for trusted identities -func validateTrustedIdentities(statement TrustPolicy) error { - // If there is a wildcard in trusted identies, there shouldn't be any other +func validateTrustedIdentities(policyName string, tis []string) error { + // If there is a wildcard in trusted identities, there shouldn't be any other //identities - if len(statement.TrustedIdentities) > 1 && slices.Contains(statement.TrustedIdentities, trustpolicy.Wildcard) { - return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", statement.Name) + if len(tis) > 1 && slices.Contains(tis, trustpolicy.Wildcard) { + return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", policyName) } var parsedDNs []parsedDN // If there are trusted identities, verify they are valid - for _, identity := range statement.TrustedIdentities { + for _, identity := range tis { if identity == "" { - return fmt.Errorf("trust policy statement %q has an empty trusted identity", statement.Name) + return fmt.Errorf("trust policy statement %q has an empty trusted identity", policyName) } if identity != trustpolicy.Wildcard { identityPrefix, identityValue, found := strings.Cut(identity, ":") if !found { - return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", statement.Name, identity) + return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", policyName, identity) } // notation natively supports x509.subject identities only if identityPrefix == trustpolicy.X509Subject { // identityValue cannot be empty if identityValue == "" { - return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", statement.Name, identity) + return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", policyName, identity) } dn, err := pkix.ParseDistinguishedName(identityValue) if err != nil { - return fmt.Errorf("trust policy statement %q has trusted identity %q with invalid identity value: %w", statement.Name, identity, err) + return fmt.Errorf("trust policy statement %q has trusted identity %q with invalid identity value: %w", policyName, identity, err) } parsedDNs = append(parsedDNs, parsedDN{RawString: identity, ParsedMap: dn}) } @@ -457,7 +345,7 @@ func validateTrustedIdentities(statement TrustPolicy) error { } // Verify there are no overlapping DNs - if err := validateOverlappingDNs(statement.Name, parsedDNs); err != nil { + if err := validateOverlappingDNs(policyName, parsedDNs); err != nil { return err } @@ -465,39 +353,6 @@ func validateTrustedIdentities(statement TrustPolicy) error { return nil } -// validateRegistryScopes validates if the policy document is following the -// Notary Project spec rules for registry scopes -func validateRegistryScopes(policyDoc *Document) error { - registryScopeCount := make(map[string]int) - for _, statement := range policyDoc.TrustPolicies { - // Verify registry scopes are valid - if len(statement.RegistryScopes) == 0 { - return fmt.Errorf("trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name) - } - if len(statement.RegistryScopes) > 1 && slices.Contains(statement.RegistryScopes, trustpolicy.Wildcard) { - return fmt.Errorf("trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name) - } - for _, scope := range statement.RegistryScopes { - if scope != trustpolicy.Wildcard { - if err := validateRegistryScopeFormat(scope); err != nil { - return err - } - } - registryScopeCount[scope]++ - } - } - - // Verify one policy statement per registry scope - for key := range registryScopeCount { - if registryScopeCount[key] > 1 { - return fmt.Errorf("registry scope %q is present in multiple trust policy statements, one registry scope value can only be associated with one statement", key) - } - } - - // No error - return nil -} - func validateOverlappingDNs(policyName string, parsedDNs []parsedDN) error { for i, dn1 := range parsedDNs { for j, dn2 := range parsedDNs { @@ -521,53 +376,8 @@ func isValidTrustStoreType(s string) bool { return false } -func getArtifactPathFromReference(artifactReference string) (string, error) { - // TODO support more types of URI like "domain.com/repository", - // "domain.com/repository:tag" - i := strings.LastIndex(artifactReference, "@") - if i < 0 { - return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified OCI artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference) - } - - artifactPath := artifactReference[:i] - if err := validateRegistryScopeFormat(artifactPath); err != nil { - return "", err - } - return artifactPath, nil -} - -// Internal type to hold raw and parsed Distinguished Names +// parsedDN holds raw and parsed Distinguished Names type parsedDN struct { RawString string ParsedMap map[string]string } - -// validateRegistryScopeFormat validates if a scope is following the format -// defined in distribution spec -func validateRegistryScopeFormat(scope string) error { - // Domain and Repository regexes are adapted from distribution - // implementation - // https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31 - domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`) - repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) - ensureMessage := "make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" - errorMessage := "registry scope %q is not valid, " + ensureMessage - errorWildCardMessage := "registry scope %q with wild card(s) is not valid, " + ensureMessage - - // Check for presence of * in scope - if len(scope) > 1 && strings.Contains(scope, "*") { - return fmt.Errorf(errorWildCardMessage, scope) - } - - domain, repository, found := strings.Cut(scope, "/") - if !found { - return fmt.Errorf(errorMessage, scope) - } - - if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) { - return fmt.Errorf(errorMessage, scope) - } - - // No errors - return nil -} diff --git a/verifier/trustpolicy/trustpolicy_test.go b/verifier/trustpolicy/trustpolicy_test.go index ead28cae..b8cb9dff 100644 --- a/verifier/trustpolicy/trustpolicy_test.go +++ b/verifier/trustpolicy/trustpolicy_test.go @@ -26,425 +26,201 @@ import ( "github.com/notaryproject/notation-go/dir" ) -func dummyPolicyStatement() (policyStatement TrustPolicy) { - policyStatement = TrustPolicy{ - Name: "test-statement-name", - RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"}, - SignatureVerification: SignatureVerification{VerificationLevel: "strict"}, - TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"}, - TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"}, - } - return +func dummyOCIPolicyDocument() OCIDocument { + return OCIDocument{ + Version: "1.0", + TrustPolicies: []OCITrustPolicy{ + { + Name: "test-statement-name", + RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"}, + SignatureVerification: SignatureVerification{VerificationLevel: "strict"}, + TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"}, + TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"}, + }, + }, + } } -func dummyPolicyDocument() (policyDoc Document) { - policyDoc = Document{ - Version: "1.0", - TrustPolicies: []TrustPolicy{dummyPolicyStatement()}, +func dummyBlobPolicyDocument() BlobDocument { + return BlobDocument{ + Version: "1.0", + TrustPolicies: []BlobTrustPolicy{ + { + Name: "test-statement-name", + SignatureVerification: SignatureVerification{VerificationLevel: "strict"}, + TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"}, + TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"}, + }, + }, } - return } -// TestValidateValidPolicyDocument tests a happy policy document -func TestValidateValidPolicyDocument(t *testing.T) { - policyDoc := dummyPolicyDocument() - - policyStatement1 := dummyPolicyStatement() - - policyStatement2 := dummyPolicyStatement() - policyStatement2.Name = "test-statement-name-2" - policyStatement2.RegistryScopes = []string{"registry.wabbit-networks.io/software/unsigned/net-utils"} - policyStatement2.SignatureVerification = SignatureVerification{VerificationLevel: "permissive"} - - policyStatement3 := dummyPolicyStatement() - policyStatement3.Name = "test-statement-name-3" - policyStatement3.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"} - policyStatement3.TrustStores = []string{} - policyStatement3.TrustedIdentities = []string{} - policyStatement3.SignatureVerification = SignatureVerification{VerificationLevel: "skip"} - - policyStatement4 := dummyPolicyStatement() - policyStatement4.Name = "test-statement-name-4" - policyStatement4.TrustStores = []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store-2"} - policyStatement4.RegistryScopes = []string{"*"} - policyStatement4.SignatureVerification = SignatureVerification{VerificationLevel: "audit"} - - policyStatement5 := dummyPolicyStatement() - policyStatement5.Name = "test-statement-name-5" - policyStatement5.RegistryScopes = []string{"registry.acme-rockets2.io/software"} - policyStatement5.TrustedIdentities = []string{"*"} - policyStatement5.SignatureVerification = SignatureVerification{VerificationLevel: "strict"} - - policyDoc.TrustPolicies = []TrustPolicy{ - policyStatement1, - policyStatement2, - policyStatement3, - policyStatement4, - policyStatement5, - } - err := policyDoc.Validate() - if err != nil { - t.Fatalf("validation failed on a good policy document. Error : %q", err) +// create testcase for validatePolicyCore method +func TestValidatePolicyCore(t *testing.T) { + policyName := "test-statement-name" + sigVerification := SignatureVerification{VerificationLevel: "strict"} + // valid policy + if err := validatePolicyCore(policyName, sigVerification, []string{"ca:valid-ts"}, []string{"*"}); err != nil { + t.Errorf("validatePolicyCore returned error: '%v'", err) + } + + // check valid skip SignatureVerification + if err := validatePolicyCore(policyName, SignatureVerification{VerificationLevel: "skip"}, []string{}, []string{}); err != nil { + t.Errorf("validatePolicyCore returned error: '%v'", err) + } + + // check skip SignatureVerification doesn't has trust store and trusted identity + expectedErr := "trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped" + if err := validatePolicyCore(policyName, SignatureVerification{VerificationLevel: "skip"}, []string{"ca:valid-ts"}, []string{}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + if err := validatePolicyCore(policyName, SignatureVerification{VerificationLevel: "skip"}, []string{}, []string{"x509:zoop"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + // empty policy name + expectedErr = "a trust policy statement is missing a name, every statement requires a name" + if err := validatePolicyCore("", sigVerification, []string{"ca:valid-ts"}, []string{"*"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + // invalid SignatureVerification + expectedErr = "trust policy statement \"test-statement-name\" has invalid signatureVerification: signature verification level is empty or missing in the trust policy statement" + if err := validatePolicyCore(policyName, SignatureVerification{}, []string{"ca:valid-ts"}, []string{"*"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + // invalid trust-store or trust-policy + expectedErr = "trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" + if err := validatePolicyCore(policyName, sigVerification, []string{}, []string{}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + if err := validatePolicyCore(policyName, sigVerification, []string{"ca:valid-ts"}, []string{}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + expectedErr = "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"hola\" in trust store value \"hola:valid-ts\"" + if err := validatePolicyCore(policyName, sigVerification, []string{"hola:valid-ts"}, []string{"hola"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + expectedErr = "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator" + if err := validatePolicyCore(policyName, sigVerification, []string{"ca:valid-ts"}, []string{"x509.subject"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } +} + +// TestValidateTrustedIdentities tests only valid x509.subjects are accepted +func TestValidateTrustStore(t *testing.T) { + // valid trust-store + if err := validateTrustStore("test-statement-name", []string{"ca:my-ts"}); err != nil { + t.Errorf("validateTrustStore returned error: '%v", err) + } + + // empty trust-store + expectedErr := "trust policy statement \"test-statement-name\" has malformed trust store value \"\". The required format is :" + if err := validateTrustStore("test-statement-name", []string{""}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + // invalid trust-store type + expectedErr = "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"unknown\" in trust store value \"unknown:my-ts\"" + if err := validateTrustStore("test-statement-name", []string{"unknown:my-ts"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) + } + + // invalid trust-store directory name + expectedErr = "trust policy statement \"test-statement-name\" uses an unsupported trust store name \"#@$@$\" in trust store value \"ca:#@$@$\". Named store name needs to follow [a-zA-Z0-9_.-]+ format" + if err := validateTrustStore("test-statement-name", []string{"ca:#@$@$"}); err == nil || err.Error() != expectedErr { + t.Errorf("expected error '%s' but not found", expectedErr) } } // TestValidateTrustedIdentities tests only valid x509.subjects are accepted func TestValidateTrustedIdentities(t *testing.T) { + // wildcard present with specific trusted identity throws error. + err := validateTrustedIdentities("test-statement-name", []string{"*", "C=US, ST=WA, O=wabbit-network.io"}) + if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" { + t.Fatalf("trusted identities with wildcard and specific identityshould return error") + } + + // If empty trust policy throws error. + err = validateTrustedIdentities("test-statement-name", []string{""}) + if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has an empty trusted identity" { + t.Fatalf("empty trusted identity should return error") + } // No trusted identity prefix throws error - policyDoc := dummyPolicyDocument() - policyStatement := dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{"C=US, ST=WA, O=wabbit-network.io, OU=org1"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err := policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{"C=US, ST=WA, O=wabbit-network.io, OU=org1"}) if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"C=US, ST=WA, O=wabbit-network.io, OU=org1\" missing separator" { t.Fatalf("trusted identity without separator should return error") } // Accept unknown identity prefixes - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{"unknown:my-trusted-idenity"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{"unknown:my-trusted-identity"}) if err != nil { t.Fatalf("unknown identity prefix should not return an error. Error: %q", err) } // Validate x509.subject identities - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() invalidDN := "x509.subject:,,," - policyStatement.TrustedIdentities = []string{invalidDN} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{invalidDN}) if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:,,,\" with invalid identity value: parsing distinguished name (DN) \",,,\" failed with err: incomplete type, value pair. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard" { t.Fatalf("invalid x509.subject identity should return error. Error : %q", err) } + // Validate x509.subject with no value + err = validateTrustedIdentities("test-statement-name", []string{"x509.subject:"}) + if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" { + t.Fatalf("x509.subject identity without value should return error. Error : %q", err) + } + // Validate duplicate RDNs - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() invalidDN = "x509.subject:C=US,C=IN" - policyStatement.TrustedIdentities = []string{invalidDN} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{invalidDN}) if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,C=IN\" with invalid identity value: distinguished name (DN) \"C=US,C=IN\" has duplicate RDN attribute for \"C\", DN can only have unique RDN attributes" { t.Fatalf("invalid x509.subject identity should return error. Error : %q", err) } // Validate mandatory RDNs - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() invalidDN = "x509.subject:C=US,ST=WA" - policyStatement.TrustedIdentities = []string{invalidDN} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{invalidDN}) if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,ST=WA\" with invalid identity value: distinguished name (DN) \"C=US,ST=WA\" has no mandatory RDN attribute for \"O\", it must contain 'C', 'ST', and 'O' RDN attributes at a minimum" { t.Fatalf("invalid x509.subject identity should return error. Error : %q", err) } // DN may have optional RDNs - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() validDN := "x509.subject:C=US,ST=WA,O=MyOrg,CustomRDN=CustomValue" - policyStatement.TrustedIdentities = []string{validDN} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{validDN}) if err != nil { t.Fatalf("valid x509.subject identity should not return error. Error : %q", err) } // Validate rfc4514 DNs - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() validDN1 := "x509.subject:C=US,ST=WA,O=MyOrg" validDN2 := "x509.subject:C=US,ST=WA,O= My. Org" validDN3 := "x509.subject:C=US,ST=WA,O=My \"special\" Org \\, \\; \\\\ others" - policyStatement.TrustedIdentities = []string{validDN1, validDN2, validDN3} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{validDN1, validDN2, validDN3}) if err != nil { t.Fatalf("valid x509.subject identity should not return error. Error : %q", err) } // Validate overlapping DNs - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() validDN1 = "x509.subject:C=US,ST=WA,O=MyOrg" validDN2 = "x509.subject:C=US,ST=WA,O=MyOrg,X=Y" - policyStatement.TrustedIdentities = []string{validDN1, validDN2} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + err = validateTrustedIdentities("test-statement-name", []string{validDN1, validDN2}) if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has overlapping x509 trustedIdentities, \"x509.subject:C=US,ST=WA,O=MyOrg\" overlaps with \"x509.subject:C=US,ST=WA,O=MyOrg,X=Y\"" { t.Fatalf("overlapping DNs should return error") } // Validate multi-valued RDNs - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - multiValduedRDN := "x509.subject:C=US+ST=WA,O=MyOrg" - policyStatement.TrustedIdentities = []string{multiValduedRDN} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() + multiValuedRUN := "x509.subject:C=US+ST=WA,O=MyOrg" + err = validateTrustedIdentities("test-statement-name", []string{multiValuedRUN}) if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US+ST=WA,O=MyOrg\" with invalid identity value: distinguished name (DN) \"C=US+ST=WA,O=MyOrg\" has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported" { t.Fatalf("multi-valued RDN should return error. Error : %q", err) } } -// TestInvalidRegistryScopes tests invalid scopes are rejected -func TestInvalidRegistryScopes(t *testing.T) { - invalidScopes := []string{ - "", "1:1", "a,b", "abcd", "1111", "1,2", "example.com/rep:tag", - "example.com/rep/subrep/sub:latest", "example.com", "rep/rep2:latest", - "repository", "10.10.10.10", "10.10.10.10:8080/rep/rep2:latest", - } - - for _, scope := range invalidScopes { - policyDoc := dummyPolicyDocument() - policyStatement := dummyPolicyStatement() - policyStatement.RegistryScopes = []string{scope} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err := policyDoc.Validate() - if err == nil || err.Error() != "registry scope \""+scope+"\" is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" { - t.Fatalf("invalid registry scope should return error. Error : %q", err) - } - } - - // Test invalid scope with wild card suffix - - invalidWildCardScopes := []string{"example.com/*", "*/", "example*/", "ex*test"} - for _, scope := range invalidWildCardScopes { - policyDoc := dummyPolicyDocument() - policyStatement := dummyPolicyStatement() - policyStatement.RegistryScopes = []string{scope} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err := policyDoc.Validate() - if err == nil || err.Error() != "registry scope \""+scope+"\" with wild card(s) is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" { - t.Fatalf("invalid registry scope should return error. Error : %q", err) - } - } -} - -// TestValidRegistryScopes tests valid scopes are accepted -func TestValidRegistryScopes(t *testing.T) { - validScopes := []string{ - "*", "example.com/rep", "example.com:8080/rep/rep2", "example.com/rep/subrep/subsub", - "10.10.10.10:8080/rep/rep2", "domain/rep", "domain:1234/rep", - } - - for _, scope := range validScopes { - policyDoc := dummyPolicyDocument() - policyStatement := dummyPolicyStatement() - policyStatement.RegistryScopes = []string{scope} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err := policyDoc.Validate() - if err != nil { - t.Fatalf("valid registry scope should not return error. Error : %q", err) - } - } -} - -// TestValidatePolicyDocument calls policyDoc.Validate() -// and tests various validations on policy eliments -func TestValidateInvalidPolicyDocument(t *testing.T) { - // Sanity check - var nilPolicyDoc *Document - err := nilPolicyDoc.Validate() - if err == nil || err.Error() != "trust policy document cannot be nil" { - t.Fatalf("nil policyDoc should return error") - } - - // Invalid Version - policyDoc := dummyPolicyDocument() - policyDoc.Version = "invalid" - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy document uses unsupported version \"invalid\"" { - t.Fatalf("invalid version should return error") - } - - // No Policy Satements - policyDoc = dummyPolicyDocument() - policyDoc.TrustPolicies = nil - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy document can not have zero trust policy statements" { - t.Fatalf("zero policy statements should return error") - } - - // No Policy Satement Name - policyDoc = dummyPolicyDocument() - policyStatement := dummyPolicyStatement() - policyStatement.Name = "" - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "a trust policy statement is missing a name, every statement requires a name" { - t.Fatalf("policy statement with no name should return an error") - } - - // No Registry Scopes - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.RegistryScopes = nil - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has zero registry scopes, it must specify registry scopes with at least one value" { - t.Fatalf("policy statement with registry scopes should return error") - } - - // Multiple policy statements with same registry scope - policyDoc = dummyPolicyDocument() - policyStatement1 := dummyPolicyStatement() - policyStatement2 := dummyPolicyStatement() - policyStatement2.Name = "test-statement-name-2" - policyDoc.TrustPolicies = []TrustPolicy{policyStatement1, policyStatement2} - err = policyDoc.Validate() - if err == nil || err.Error() != "registry scope \"registry.acme-rockets.io/software/net-monitor\" is present in multiple trust policy statements, one registry scope value can only be associated with one statement" { - t.Fatalf("Policy statements with same registry scope should return error %q", err) - } - - // Registry scopes with a wildcard - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.RegistryScopes = []string{"*", "registry.acme-rockets.io/software/net-monitor"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values" { - t.Fatalf("policy statement with more than a wildcard registry scope should return error") - } - - // Invlaid SignatureVerification - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "invalid"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has invalid signatureVerification: invalid signature verification level \"invalid\"" { - t.Fatalf("policy statement with invalid SignatureVerification should return error") - } - - // strict SignatureVerification should have a trust store - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustStores = []string{} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" { - t.Fatalf("strict SignatureVerification should have a trust store") - } - - // strict SignatureVerification should have trusted identities - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" { - t.Fatalf("strict SignatureVerification should have trusted identities") - } - - // skip SignatureVerification should not have trust store or trusted identities - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "skip"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped" { - t.Fatalf("strict SignatureVerification should have trusted identities") - } - - // Empty Trusted Identity should throw error - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{""} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has an empty trusted identity" { - t.Fatalf("policy statement with empty trusted identity should return error") - } - - // Trusted Identity without spearator should throw error - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{"x509.subject"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator" { - t.Fatalf("policy statement with trusted identity missing separator should return error") - } - - // Empty Trusted Identity value should throw error - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{"x509.subject:"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" { - t.Fatalf("policy statement with trusted identity missing identity value should return error") - } - - // trust store/trusted identites are optional for skip SignatureVerification - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "skip"} - policyStatement.TrustStores = []string{} - policyStatement.TrustedIdentities = []string{} - err = policyDoc.Validate() - if err != nil { - t.Fatalf("skip SignatureVerification should not require a trust store or trusted identities") - } - - // Trust Store missing separator - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustStores = []string{"ca"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has malformed trust store value \"ca\". The required format is :" { - t.Fatalf("policy statement with trust store missing separator should return error") - } - - // Invalid Trust Store type - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustStores = []string{"invalid:test-trust-store"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"invalid\" in trust store value \"invalid:test-trust-store\"" { - t.Fatalf("policy statement with invalid trust store type should return error") - } - - // Empty Named Store - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustStores = []string{"ca:"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses an unsupported trust store name \"\" in trust store value \"ca:\". Named store name needs to follow [a-zA-Z0-9_.-]+ format" { - t.Fatalf("policy statement with trust store missing named store should return error") - } - - // trusted identities with a wildcard - policyDoc = dummyPolicyDocument() - policyStatement = dummyPolicyStatement() - policyStatement.TrustedIdentities = []string{"*", "test-identity"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement} - err = policyDoc.Validate() - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" { - t.Fatalf("policy statement with more than a wildcard trusted identity should return error") - } - - // Policy Document with duplicate policy statement names - policyDoc = dummyPolicyDocument() - policyStatement1 = dummyPolicyStatement() - policyStatement2 = dummyPolicyStatement() - policyStatement2.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"} - policyDoc.TrustPolicies = []TrustPolicy{policyStatement1, policyStatement2} - err = policyDoc.Validate() - if err == nil || err.Error() != "multiple trust policy statements use the same name \"test-statement-name\", statement names must be unique" { - t.Fatalf("policy statements with same name should return error") - } -} - func TestGetVerificationLevel(t *testing.T) { tests := []struct { verificationLevel SignatureVerification @@ -525,83 +301,64 @@ func TestCustomVerificationLevel(t *testing.T) { } } -// TestApplicableTrustPolicy tests filtering policies against registry scopes -func TestApplicableTrustPolicy(t *testing.T) { - policyDoc := dummyPolicyDocument() - - policyStatement := dummyPolicyStatement() - policyStatement.Name = "test-statement-name-1" - registryScope := "registry.wabbit-networks.io/software/unsigned/net-utils" - registryUri := fmt.Sprintf("%s@sha256:hash", registryScope) - policyStatement.RegistryScopes = []string{registryScope} - policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "strict"} - - policyDoc.TrustPolicies = []TrustPolicy{ - policyStatement, - } - // existing Registry Scope - policy, err := (&policyDoc).GetApplicableTrustPolicy(registryUri) - if policy.Name != policyStatement.Name || err != nil { - t.Fatalf("getApplicableTrustPolicy should return %q for registry scope %q", policyStatement.Name, registryScope) - } - - // non-existing Registry Scope - policy, err = (&policyDoc).GetApplicableTrustPolicy("non.existing.scope/repo@sha256:hash") - if policy != nil || err == nil || err.Error() != "artifact \"non.existing.scope/repo@sha256:hash\" has no applicable trust policy. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy" { - t.Fatalf("getApplicableTrustPolicy should return nil for non existing registry scope") - } - - // wildcard registry scope - wildcardStatement := dummyPolicyStatement() - wildcardStatement.Name = "test-statement-name-2" - wildcardStatement.RegistryScopes = []string{"*"} - wildcardStatement.TrustStores = []string{} - wildcardStatement.TrustedIdentities = []string{} - wildcardStatement.SignatureVerification = SignatureVerification{VerificationLevel: "skip"} +func TestGetDocument(t *testing.T) { + dir.UserConfigDir = "/" + var ociDoc OCIDocument + var blobDoc BlobDocument + tests := []struct { + name string + expectedDocument any + actualDocument any + }{ + { + name: "valid OCI policy file", + expectedDocument: dummyOCIPolicyDocument(), + actualDocument: &ociDoc, + }, + { + name: "valid Blob policy file", + expectedDocument: dummyBlobPolicyDocument(), + actualDocument: &blobDoc, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempRoot := t.TempDir() + path := filepath.Join(tempRoot, "trustpolicy.json") + policyJson, _ := json.Marshal(tt.expectedDocument) + if err := os.WriteFile(path, policyJson, 0600); err != nil { + t.Fatalf("TestGetDocument write policy file failed. Error: %v", err) + } + t.Cleanup(func() { os.RemoveAll(tempRoot) }) - policyDoc.TrustPolicies = []TrustPolicy{ - policyStatement, - wildcardStatement, - } - policy, err = (&policyDoc).GetApplicableTrustPolicy("some.registry.that/has.no.policy@sha256:hash") - if policy.Name != wildcardStatement.Name || err != nil { - t.Fatalf("getApplicableTrustPolicy should return wildcard policy for registry scope \"some.registry.that/has.no.policy\"") + if err := getDocument(path, tt.actualDocument); err != nil { + t.Fatalf("getDocument() should not throw error for an existing policy file. Error: %v", err) + } + }) } } -func TestLoadDocument(t *testing.T) { - +func TestGetDocumentErrors(t *testing.T) { + dir.UserConfigDir = "/" t.Run("non-existing policy file", func(t *testing.T) { - tempRoot := t.TempDir() - dir.UserConfigDir = tempRoot - if _, err := LoadDocument(); err == nil || err.Error() != fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink) { - t.Fatalf("TestLoadPolicyDocument should throw error for non existent policy") + var doc OCIDocument + if err := getDocument("blaah", &doc); err == nil || err.Error() != fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink) { + t.Fatalf("getDocument() should throw error for non existent policy") } }) t.Run("invalid json file", func(t *testing.T) { tempRoot := t.TempDir() - dir.UserConfigDir = tempRoot path := filepath.Join(tempRoot, "invalid.json") if err := os.WriteFile(path, []byte(`{"invalid`), 0600); err != nil { - t.Fatalf("TestLoadPolicyDocument create invalid policy file failed. Error: %v", err) - } - if _, err := LoadDocument(); err == nil { - t.Fatalf("TestLoadPolicyDocument should throw error for invalid policy file. Error: %v", err) + t.Fatalf("creation of invalid policy file failed. Error: %v", err) } - }) + t.Cleanup(func() { os.RemoveAll(tempRoot) }) - t.Run("valid policy file", func(t *testing.T) { - tempRoot := t.TempDir() - dir.UserConfigDir = tempRoot - path := filepath.Join(tempRoot, "trustpolicy.json") - policyDoc1 := dummyPolicyDocument() - policyJson, _ := json.Marshal(policyDoc1) - if err := os.WriteFile(path, policyJson, 0600); err != nil { - t.Fatalf("TestLoadPolicyDocument create valid policy file failed. Error: %v", err) - } - if _, err := LoadDocument(); err != nil { - t.Fatalf("TestLoadPolicyDocument should not throw error for an existing policy file. Error: %v", err) + var doc OCIDocument + if err := getDocument(path, &doc); err == nil || err.Error() != fmt.Sprintf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink) { + t.Fatalf("getDocument() should throw error for invalid policy file. Error: %v", err) } }) @@ -610,15 +367,16 @@ func TestLoadDocument(t *testing.T) { t.Skip("skipping test on Windows") } tempRoot := t.TempDir() - dir.UserConfigDir = tempRoot policyJson, _ := json.Marshal([]byte("Some String")) path := filepath.Join(tempRoot, "trustpolicy.json") if err := os.WriteFile(path, policyJson, 0000); err != nil { - t.Fatalf("TestLoadPolicyDocument write policy file failed. Error: %v", err) + t.Fatalf("creation of invalid permission policy file failed. Error: %v", err) } - _, err := LoadDocument() - if err == nil || err.Error() != fmt.Sprintf("unable to read trust policy due to file permissions, please verify the permissions of %s/trustpolicy.json", tempRoot) { - t.Fatalf("TestLoadPolicyDocument should throw error for a policy file with bad permissions. Error: %v", err) + expectedErrMsg := fmt.Sprintf("unable to read trust policy due to file permissions, please verify the permissions of %s", path) + var doc OCIDocument + if err := getDocument(path, &doc); err == nil || err.Error() != expectedErrMsg { + t.Errorf("getDocument() should throw error for a policy file with bad permissions. "+ + "Expected error: '%v'qq but found '%v'", expectedErrMsg, err.Error()) } }) @@ -627,12 +385,18 @@ func TestLoadDocument(t *testing.T) { t.Skip("skipping test on Windows") } tempRoot := t.TempDir() - dir.UserConfigDir = tempRoot + path := filepath.Join(tempRoot, "trustpolicy.json") + if err := os.WriteFile(path, []byte(`{"invalid`), 0600); err != nil { + t.Fatalf("creation of policy file failed. Error: %v", err) + } - os.Symlink("some/filepath", filepath.Join(tempRoot, "trustpolicy.json")) - _, err := LoadDocument() - if err == nil || !strings.HasPrefix(err.Error(), "trust policy is not a regular file (symlinks are not supported)") { - t.Fatalf("TestLoadPolicyDocument should throw error for a symlink policy file. Error: %v", err) + symlinkPath := filepath.Join(tempRoot, "invalid.json") + if err := os.Symlink(path, symlinkPath); err != nil { + t.Fatalf("creation of symlink for policy file failed. Error: %v", err) + } + var doc OCIDocument + if err := getDocument(symlinkPath, &doc); err == nil || !strings.HasPrefix(err.Error(), "trust policy is not a regular file (symlinks are not supported)") { + t.Fatalf("getDocument() should throw error for a symlink policy file. Error: %v", err) } }) } diff --git a/verifier/verifier.go b/verifier/verifier.go index e6436050..4ba87fec 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -16,6 +16,7 @@ package verifier import ( "context" + "crypto" "crypto/x509" "encoding/json" "errors" @@ -43,47 +44,76 @@ import ( "github.com/notaryproject/notation-go/verifier/trustpolicy" "github.com/notaryproject/notation-go/verifier/truststore" pluginframework "github.com/notaryproject/notation-plugin-framework-go/plugin" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -// verifier implements notation.Verifier and notation.verifySkipper +var algorithms = map[crypto.Hash]digest.Algorithm{ + crypto.SHA256: digest.SHA256, + crypto.SHA384: digest.SHA384, + crypto.SHA512: digest.SHA512, +} + +// verifier implements notation.Verifier, notation.BlobVerifier and notation.verifySkipper type verifier struct { - trustPolicyDoc *trustpolicy.Document - trustStore truststore.X509TrustStore - pluginManager plugin.Manager - revocationClient revocation.Revocation + ociTrustPolicyDoc *trustpolicy.OCIDocument + blobTrustPolicyDoc *trustpolicy.BlobDocument + trustStore truststore.X509TrustStore + pluginManager plugin.Manager + revocationClient revocation.Revocation } // VerifierOptions specifies additional parameters that can be set when using -// the NewWithOptions constructor +// the NewVerifierWithOptions constructor type VerifierOptions struct { // RevocationClient is an implementation of revocation.Revocation to use for // verifying revocation RevocationClient revocation.Revocation } -// NewFromConfig returns a verifier based on local file system -func NewFromConfig() (notation.Verifier, error) { +// NewOCIVerifierFromConfig returns a OCI verifier based on local file system +func NewOCIVerifierFromConfig() (*verifier, error) { // load trust policy - policyDocument, err := trustpolicy.LoadDocument() + policyDocument, err := trustpolicy.LoadOCIDocument() if err != nil { return nil, err } // load trust store x509TrustStore := truststore.NewX509TrustStore(dir.ConfigFS()) - return New(policyDocument, x509TrustStore, plugin.NewCLIManager(dir.PluginFS())) + return NewVerifier(policyDocument, nil, x509TrustStore, plugin.NewCLIManager(dir.PluginFS())) } -// New creates a new verifier given trustPolicy, trustStore and pluginManager -func New(trustPolicy *trustpolicy.Document, trustStore truststore.X509TrustStore, pluginManager plugin.Manager) (notation.Verifier, error) { - return NewWithOptions(trustPolicy, trustStore, pluginManager, VerifierOptions{}) +// NewBlobVerifierFromConfig returns a Blob verifier based on local file system +func NewBlobVerifierFromConfig() (*verifier, error) { + // load trust policy + policyDocument, err := trustpolicy.LoadBlobDocument() + if err != nil { + return nil, err + } + // load trust store + x509TrustStore := truststore.NewX509TrustStore(dir.ConfigFS()) + + return NewVerifier(nil, policyDocument, x509TrustStore, plugin.NewCLIManager(dir.PluginFS())) } -// NewWithOptions creates a new verifier given trustPolicy, trustStore, +// NewWithOptions creates a new verifier given ociTrustPolicy, trustStore, // pluginManager, and VerifierOptions -func NewWithOptions(trustPolicy *trustpolicy.Document, trustStore truststore.X509TrustStore, pluginManager plugin.Manager, opts VerifierOptions) (notation.Verifier, error) { - revocationClient := opts.RevocationClient +// Deprecated: NewWithOptions function exists for historical compatibility and should not be used. +// To create verifier, use NewVerifierWithOptions function. +func NewWithOptions(ociTrustPolicy *trustpolicy.OCIDocument, trustStore truststore.X509TrustStore, pluginManager plugin.Manager, opts VerifierOptions) (notation.Verifier, error) { + return NewVerifierWithOptions(ociTrustPolicy, nil, trustStore, pluginManager, opts) +} + +// NewVerifier creates a new verifier given ociTrustPolicy, trustStore and pluginManager +func NewVerifier(ociTrustPolicy *trustpolicy.OCIDocument, blobTrustPolicy *trustpolicy.BlobDocument, trustStore truststore.X509TrustStore, pluginManager plugin.Manager) (*verifier, error) { + return NewVerifierWithOptions(ociTrustPolicy, blobTrustPolicy, trustStore, pluginManager, VerifierOptions{}) +} + +// NewVerifierWithOptions creates a new verifier given ociTrustPolicy, blobTrustPolicy, +// trustStore, pluginManager, and verifierOptions +func NewVerifierWithOptions(ociTrustPolicy *trustpolicy.OCIDocument, blobTrustPolicy *trustpolicy.BlobDocument, trustStore truststore.X509TrustStore, pluginManager plugin.Manager, verifierOptions VerifierOptions) (*verifier, error) { + revocationClient := verifierOptions.RevocationClient if revocationClient == nil { var err error revocationClient, err = revocation.New(&http.Client{Timeout: 2 * time.Second}) @@ -91,18 +121,49 @@ func NewWithOptions(trustPolicy *trustpolicy.Document, trustStore truststore.X50 return nil, err } } - if trustPolicy == nil || trustStore == nil { - return nil, errors.New("trustPolicy or trustStore cannot be nil") + + if trustStore == nil { + return nil, errors.New("trustStore cannot be nil") } - if err := trustPolicy.Validate(); err != nil { - return nil, err + + if ociTrustPolicy == nil && blobTrustPolicy == nil { + return nil, errors.New("ociTrustPolicy and blobTrustPolicy both cannot be nil") + } + + if ociTrustPolicy != nil { + if err := ociTrustPolicy.Validate(); err != nil { + return nil, err + } } + + if blobTrustPolicy != nil { + if err := blobTrustPolicy.Validate(); err != nil { + return nil, err + } + } + return &verifier{ - trustPolicyDoc: trustPolicy, - trustStore: trustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: ociTrustPolicy, + blobTrustPolicyDoc: blobTrustPolicy, + trustStore: trustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, }, nil + +} + +// NewFromConfig returns a OCI verifier based on local file system +// Deprecated: NewFromConfig function exists for historical compatibility and should not be used. +// To create an OCI verifier, use NewOCIVerifierFromConfig function. +func NewFromConfig() (notation.Verifier, error) { + return NewOCIVerifierFromConfig() +} + +// New creates a new verifier given ociTrustPolicy, trustStore and pluginManager +// Deprecated: New function exists for historical compatibility and should not be used. +// To create verifier, use NewVerifier function. +func New(ociTrustPolicy *trustpolicy.OCIDocument, trustStore truststore.X509TrustStore, pluginManager plugin.Manager) (notation.Verifier, error) { + return NewVerifier(ociTrustPolicy, nil, trustStore, pluginManager) } // SkipVerify validates whether the verification level is skip. @@ -110,23 +171,105 @@ func (v *verifier) SkipVerify(ctx context.Context, opts notation.VerifierVerifyO logger := log.GetLogger(ctx) logger.Debugf("Check verification level against artifact %v", opts.ArtifactReference) - trustPolicy, err := v.trustPolicyDoc.GetApplicableTrustPolicy(opts.ArtifactReference) + trustPolicy, err := v.ociTrustPolicyDoc.GetApplicableTrustPolicy(opts.ArtifactReference) if err != nil { return false, nil, notation.ErrorNoApplicableTrustPolicy{Msg: err.Error()} } logger.Infof("Trust policy configuration: %+v", trustPolicy) + // ignore the error since we already validated the policy document verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel() - // verificationLevel is skip if reflect.DeepEqual(verificationLevel, trustpolicy.LevelSkip) { logger.Debug("Skipping signature verification") return true, trustpolicy.LevelSkip, nil } + return false, verificationLevel, nil } -// Verify verifies the signature blob `signature` against the target OCI +// VerifyBlob verifies the signature of given blob , and returns the outcome upon +// successful verification. +func (v *verifier) VerifyBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, signature []byte, opts notation.BlobVerifierVerifyOptions) (*notation.VerificationOutcome, error) { + logger := log.GetLogger(ctx) + logger.Debugf("Verify signature of media type %v", opts.SignatureMediaType) + if v.blobTrustPolicyDoc == nil { + return nil, errors.New("blobTrustPolicyDoc is nil") + } + + var trustPolicy *trustpolicy.BlobTrustPolicy + var err error + if opts.TrustPolicyName == "" { + trustPolicy, err = v.blobTrustPolicyDoc.GetGlobalTrustPolicy(); + } else { + trustPolicy, err = v.blobTrustPolicyDoc.GetApplicableTrustPolicy(opts.TrustPolicyName) + } + if err != nil { + return nil, notation.ErrorNoApplicableTrustPolicy{Msg: err.Error()} + } + logger.Infof("Trust policy configuration: %+v", trustPolicy) + + // ignore the error since we already validated the policy document + verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel() + outcome := ¬ation.VerificationOutcome{ + RawSignature: signature, + VerificationLevel: verificationLevel, + } + // verificationLevel is skip + if reflect.DeepEqual(verificationLevel, trustpolicy.LevelSkip) { + logger.Debug("Skipping signature verification") + return outcome, nil + } + err = v.processSignature(ctx, signature, opts.SignatureMediaType, trustPolicy.Name, trustPolicy.TrustedIdentities, trustPolicy.TrustStores, opts.PluginConfig, outcome) + if err != nil { + outcome.Error = err + return outcome, err + } + + payload := &envelope.Payload{} + err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) + if err != nil { + logger.Error("Failed to unmarshal the payload content in the signature blob to envelope.Payload") + outcome.Error = err + return outcome, err + } + + cryptoHash := outcome.EnvelopeContent.SignerInfo.SignatureAlgorithm.Hash() + digestAlgo, ok := algorithms[cryptoHash] + if !ok { + logger.Error("Unsupported hashing algorithm: %v", cryptoHash) + err := fmt.Errorf("unsupported hashing algorithm: %v", cryptoHash) + outcome.Error = err + return outcome, err + } + + desc, err := descGenFunc(digestAlgo) + if err != nil { + errMsg := fmt.Sprintf("failed to generate descriptor for given artifact. Error: %s", err) + logger.Error(errMsg) + descErr := errors.New(errMsg) + outcome.Error = descErr + return outcome, descErr + } + + if desc.Digest != payload.TargetArtifact.Digest || desc.Size != payload.TargetArtifact.Size || + (desc.MediaType != "" && desc.MediaType != payload.TargetArtifact.MediaType) { + logger.Infof("payload present in the signature: %+v", payload.TargetArtifact) + logger.Infof("payload derived from the blob: %+v", desc) + outcome.Error = errors.New("integrity check failed. signature does not match the given blob") + } + + if len(opts.UserMetadata) > 0 { + err := verifyUserMetadata(logger, payload, opts.UserMetadata) + if err != nil { + outcome.Error = err + } + } + + return outcome, outcome.Error +} + +// Verify verifies the signature associated the target OCI // artifact with manifest descriptor `desc`, and returns the outcome upon // successful verification. // If nil signature is present and the verification level is not 'skip', @@ -138,10 +281,15 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur logger := log.GetLogger(ctx) logger.Debugf("Verify signature against artifact %v referenced as %s in signature media type %v", desc.Digest, artifactRef, envelopeMediaType) - trustPolicy, err := v.trustPolicyDoc.GetApplicableTrustPolicy(artifactRef) + if v.ociTrustPolicyDoc == nil { + return nil, errors.New("ociTrustPolicyDoc is nil") + } + + trustPolicy, err := v.ociTrustPolicyDoc.GetApplicableTrustPolicy(artifactRef) if err != nil { return nil, notation.ErrorNoApplicableTrustPolicy{Msg: err.Error()} } + logger.Infof("Trust policy configuration: %+v", trustPolicy) // ignore the error since we already validated the policy document verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel() @@ -155,7 +303,7 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur logger.Debug("Skipping signature verification") return outcome, nil } - err = v.processSignature(ctx, signature, envelopeMediaType, trustPolicy, pluginConfig, outcome) + err = v.processSignature(ctx, signature, envelopeMediaType, trustPolicy.Name, trustPolicy.TrustedIdentities, trustPolicy.TrustStores, pluginConfig, outcome) if err != nil { outcome.Error = err @@ -186,7 +334,7 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur return outcome, outcome.Error } -func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelopeMediaType string, trustPolicy *trustpolicy.TrustPolicy, pluginConfig map[string]string, outcome *notation.VerificationOutcome) error { +func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelopeMediaType, policyName string, trustedIdentities, trustStores []string, pluginConfig map[string]string, outcome *notation.VerificationOutcome) error { logger := log.GetLogger(ctx) // verify integrity first. notation will always verify integrity no matter @@ -254,7 +402,18 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop // verify x509 trust store based authenticity logger.Debug("Validating cert chain") - authenticityResult := verifyAuthenticity(ctx, trustPolicy, v.trustStore, outcome) + trustCerts, err := loadX509TrustStores(ctx, outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme, policyName, trustStores, v.trustStore) + var authenticityResult *notation.ValidationResult + if err != nil { + authenticityResult = ¬ation.ValidationResult{ + Error: err, + Type: trustpolicy.TypeAuthenticity, + Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticity], + } + } else { + // verify authenticity + authenticityResult = verifyAuthenticity(trustCerts, outcome) + } outcome.VerificationResults = append(outcome.VerificationResults, authenticityResult) logVerificationResult(logger, authenticityResult) if isCriticalFailure(authenticityResult) { @@ -265,7 +424,7 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop // to perform this verification rather than a plugin) if !slices.Contains(pluginCapabilities, pluginframework.CapabilityTrustedIdentityVerifier) { logger.Debug("Validating trust identity") - err = verifyX509TrustedIdentities(outcome.EnvelopeContent.SignerInfo.CertificateChain, trustPolicy) + err = verifyX509TrustedIdentities(policyName, trustedIdentities, outcome.EnvelopeContent.SignerInfo.CertificateChain) if err != nil { authenticityResult.Error = err logVerificationResult(logger, authenticityResult) @@ -323,18 +482,18 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop if len(capabilitiesToVerify) > 0 { logger.Debugf("Executing verification plugin %q with capabilities %v", verificationPluginName, capabilitiesToVerify) - response, err := executePlugin(ctx, installedPlugin, trustPolicy, capabilitiesToVerify, outcome.EnvelopeContent, pluginConfig) + response, err := executePlugin(ctx, installedPlugin, capabilitiesToVerify, outcome.EnvelopeContent, trustedIdentities, pluginConfig) if err != nil { return err } - return processPluginResponse(logger, capabilitiesToVerify, response, outcome) + return processPluginResponse(capabilitiesToVerify, response, outcome) } } return nil } -func processPluginResponse(logger log.Logger, capabilitiesToVerify []pluginframework.Capability, response *pluginframework.VerifySignatureResponse, outcome *notation.VerificationOutcome) error { +func processPluginResponse(capabilitiesToVerify []pluginframework.Capability, response *pluginframework.VerifySignatureResponse, outcome *notation.VerificationOutcome) error { verificationPluginName, err := getVerificationPlugin(&outcome.EnvelopeContent.SignerInfo) if err != nil { return err @@ -442,18 +601,7 @@ func verifyIntegrity(sigBlob []byte, envelopeMediaType string, outcome *notation } } -func verifyAuthenticity(ctx context.Context, trustPolicy *trustpolicy.TrustPolicy, x509TrustStore truststore.X509TrustStore, outcome *notation.VerificationOutcome) *notation.ValidationResult { - // verify authenticity - trustCerts, err := loadX509TrustStores(ctx, outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme, trustPolicy, x509TrustStore) - - if err != nil { - return ¬ation.ValidationResult{ - Error: err, - Type: trustpolicy.TypeAuthenticity, - Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticity], - } - } - +func verifyAuthenticity(trustCerts []*x509.Certificate, outcome *notation.VerificationOutcome) *notation.ValidationResult { if len(trustCerts) < 1 { return ¬ation.ValidationResult{ Error: notation.ErrorVerificationInconclusive{Msg: "no trusted certificates are found to verify authenticity"}, @@ -461,7 +609,7 @@ func verifyAuthenticity(ctx context.Context, trustPolicy *trustpolicy.TrustPolic Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticity], } } - _, err = signature.VerifyAuthenticity(&outcome.EnvelopeContent.SignerInfo, trustCerts) + _, err := signature.VerifyAuthenticity(&outcome.EnvelopeContent.SignerInfo, trustCerts) if err != nil { switch err.(type) { case *signature.SignatureAuthenticityError: @@ -635,7 +783,7 @@ func verifyRevocation(outcome *notation.VerificationOutcome, r revocation.Revoca return result } -func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPlugin, trustPolicy *trustpolicy.TrustPolicy, capabilitiesToVerify []pluginframework.Capability, envelopeContent *signature.EnvelopeContent, pluginConfig map[string]string) (*pluginframework.VerifySignatureResponse, error) { +func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPlugin, capabilitiesToVerify []pluginframework.Capability, envelopeContent *signature.EnvelopeContent, trustedIdentities []string, pluginConfig map[string]string) (*pluginframework.VerifySignatureResponse, error) { logger := log.GetLogger(ctx) // sanity check if installedPlugin == nil { @@ -663,7 +811,7 @@ func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPl // https://github.com/notaryproject/notation-core-go/issues/38 } - signature := pluginframework.Signature{ + sig := pluginframework.Signature{ CriticalAttributes: pluginframework.CriticalAttributes{ ContentType: payloadInfo.ContentType, SigningScheme: string(signerInfo.SignedAttributes.SigningScheme), @@ -676,36 +824,36 @@ func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPl } policy := pluginframework.TrustPolicy{ - TrustedIdentities: trustPolicy.TrustedIdentities, + TrustedIdentities: trustedIdentities, SignatureVerification: capabilitiesToVerify, } req := &pluginframework.VerifySignatureRequest{ ContractVersion: pluginframework.ContractVersion, - Signature: signature, + Signature: sig, TrustPolicy: policy, PluginConfig: pluginConfig, } return installedPlugin.VerifySignature(ctx, req) } -func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *trustpolicy.TrustPolicy) error { - if slices.Contains(trustPolicy.TrustedIdentities, trustpolicyInternal.Wildcard) { +func verifyX509TrustedIdentities(policyName string, trustedIdentities []string, certs []*x509.Certificate) error { + if slices.Contains(trustedIdentities, trustpolicyInternal.Wildcard) { return nil } var trustedX509Identities []map[string]string - for _, identity := range trustPolicy.TrustedIdentities { + for _, identity := range trustedIdentities { identityPrefix, identityValue, found := strings.Cut(identity, ":") if !found { - return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", trustPolicy.Name, identity) + return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", policyName, identity) } // notation natively supports x509.subject identities only if identityPrefix == trustpolicyInternal.X509Subject { // identityValue cannot be empty if identityValue == "" { - return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", trustPolicy.Name, identity) + return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", policyName, identity) } parsedSubject, err := pkix.ParseDistinguishedName(identityValue) if err != nil { @@ -716,7 +864,7 @@ func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *trustpo } if len(trustedX509Identities) == 0 { - return fmt.Errorf("no x509 trusted identities are configured in the trust policy %q", trustPolicy.Name) + return fmt.Errorf("no x509 trusted identities are configured in the trust policy %q", policyName) } leafCert := certs[0] // trusted identities only supported on the leaf cert @@ -732,7 +880,7 @@ func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *trustpo } } - return fmt.Errorf("signing certificate from the digital signature does not match the X.509 trusted identities %q defined in the trust policy %q", trustedX509Identities, trustPolicy.Name) + return fmt.Errorf("signing certificate from the digital signature does not match the X.509 trusted identities %q defined in the trust policy %q", trustedX509Identities, policyName) } func logVerificationResult(logger log.Logger, result *notation.ValidationResult) { diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index ae9943d2..571c7499 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -16,6 +16,7 @@ package verifier import ( "context" "crypto/x509" + "encoding/pem" "errors" "fmt" "net/http" @@ -25,8 +26,13 @@ import ( "testing" "time" + "golang.org/x/crypto/ocsp" + "github.com/notaryproject/notation-core-go/revocation" "github.com/notaryproject/notation-core-go/signature" + _ "github.com/notaryproject/notation-core-go/signature/cose" + "github.com/notaryproject/notation-core-go/signature/jws" + _ "github.com/notaryproject/notation-core-go/signature/jws" "github.com/notaryproject/notation-core-go/testhelper" corex509 "github.com/notaryproject/notation-core-go/x509" "github.com/notaryproject/notation-go" @@ -38,50 +44,56 @@ import ( "github.com/notaryproject/notation-go/signer" "github.com/notaryproject/notation-go/verifier/trustpolicy" "github.com/notaryproject/notation-go/verifier/truststore" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "golang.org/x/crypto/ocsp" - - _ "github.com/notaryproject/notation-core-go/signature/cose" - _ "github.com/notaryproject/notation-core-go/signature/jws" ) -func verifyResult(outcome *notation.VerificationOutcome, expectedResult notation.ValidationResult, expectedErr error, t *testing.T) { - var actualResult *notation.ValidationResult - for _, r := range outcome.VerificationResults { - if r.Type == expectedResult.Type { - if actualResult == nil { - actualResult = r - } else { - t.Fatalf("expected only one VerificatiionResult for %q but found one more. first: %+v second: %+v", r.Type, actualResult, r) - } - } - } - - if actualResult == nil || - (expectedResult.Error != nil && expectedResult.Error.Error() != actualResult.Error.Error()) || - expectedResult.Action != actualResult.Action { - t.Fatalf("assertion failed. expected : %+v got : %+v", expectedResult, actualResult) - } - - if expectedResult.Action == trustpolicy.ActionEnforce && expectedErr != nil && outcome.Error.Error() != expectedErr.Error() { - t.Fatalf("assertion failed. expected : %v got : %v", expectedErr, outcome.Error) - } -} +var testSig = `{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJhbm5vdGF0aW9ucyI6eyJidWlsZElkIjoiMTAxIn0sImRpZ2VzdCI6InNoYTM4NDpiOGFiMjRkYWZiYTVjZjdlNGM4OWM1NjJmODExY2YxMDQ5M2Q0MjAzZGE5ODJkM2IxMzQ1ZjM2NmNhODYzZDljMmVkMzIzZGJkMGZiN2ZmODNhODAzMDJjZWZmYTVhNjEiLCJtZWRpYVR5cGUiOiJ2aWRlby9tcDQiLCJzaXplIjoxMn19","protected":"eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTA0LTA0VDE1OjAzOjA2LTA3OjAwIn0","header":{"x5c":["MIIEbTCCAtWgAwIBAgICAK0wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMTHE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwIBcNMjQwNDA0MjIwMzA1WhgPMjEyNDA0MDQyMjAzMDVaMGQxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHU2VhdHRsZTEPMA0GA1UEChMGTm90YXJ5MSUwIwYDVQQDExxOb3RhdGlvbiBFeGFtcGxlIHNlbGYtc2lnbmVkMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0dXD9UqzZcGlBlvPHO2uf+Sel/xwf/eOMS6Q30GV6JPeu9czLmyR0YMfC6P0N4zDzVYYZtQLkS5lalTMGX9A3yj9aXtXvtoYtLx2mF1CfdQJMcrT63wVVTWiPPe2JT8KHkkiACzVY6LTwc4s+DIAw9Gv21Uu6bFy4WWlGMp8UwTucR0JqaFoXzB6vxVRTkK8RRLM9Pj0hM5NwobpuZ+pc+ZS/7PhdvQHVzHeLLV9S7fHxw3n1c0ti8VUjSPSqCIEqOL3Eu/0pWMXB2A1xzn3RBfnzZMD3Tw3ksFgLMVzblhv41c6gr4cgjaS4wWwUvq9Xndd7Io8QNvxyiRDX5cHwQSEOmDfmegTIaLR0dKfvjY4ZJq8Y1DnaXU4RD6XeihtZykMlx7nTUyZZXpQ1akjh3VMzPykJ4mIknHh02zGRT9ZE8E1kYzRWhU/0MAzVrTTFHpric6jO459ouTnQXFjKwAcoD5+bNY6TuhC18iar7+l4BPPI1mFuqETnMfkkJQZAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAYEAe5wyQPo+h1Yk2PkaA5aJKuU8azF2pTLfhQwAn/1XqPcmhNQuomOP0waoBsh+6sexfIDZaNuJ+zZUxqYHke/23+768SMiJCpuJfion3ak3Ka/IVNz48G0V+V+Vog+elkZzpdUQd30njLVcoQsihp0I/Gs3pnG2SeHmsdvYVuzycdYWTt5BFu4N8VWg4x4pfRMgDG7HGxRAacz2vTdqAx6rpWjO4xc0ZO8iUKjAeKHc7RuSx2dhUaRP9P8G8NBNtG6xNnbXIEjH6kP05srFRZ2jxm1an7sjsOpbBdIDztc0J+cb5yjBx7zo1OzWcmDUqMEXDR/WoygPzwhhHvWWvTqwVSEUvYnSaI6wxyHGxPFuX3+vCEZxU8NEGIuJtfYXWeo9cev5+PqjDgVu0uCWF53ZFsXNWbpff1qpG/CgrpFh3vN6uquMK9H5zaJBKr0GZFUsNRB1S8cUBgcjIZlWv3wrJQaOIFzF4RFO9dsYcG/b7ubdqSNGe4qfbsyuWf+1xsx"],"io.cncf.notary.signingAgent":"example signing agent"},"signature":"WMtF0u9GnQxJCpgrcxKZtNKNf3fvu2vnvOjd_2vQvjB4I9YKRYDQdr1q0AC0rU9b5aAGqP6Uh3jTbPkHHmOzGhXhRtidunfzOAeC6dPinR_RlnVMnVUY4cimZZG6Tg2tlgqGazgdzphnuZQpxUnK5mSInnWztXz_1-l_UJdPII49loJVE23hvWKDp8xOvMLftFXFlCYF9wE1ecTsYEAdrgB_XurFqbhhfeNcYie02aSMXfN0-ip9MHlIPhGrrOKLVm0w_S3nNBnuHHZ5lARgTm7tHtiNC0XxGCCk8qqteRZ4Vm2VM_UFMVOpdfh5KE_iTzmPCiHfNOJfgmvg5nysL1XUwGJ_KzCkPfY1Hq_4k73lia6RS6NSl1bSQ_s3uMBm3nx74WCmjK89RAihMIQ6s0PmUKQoWsIZ_5lWZ6uFW6LreoYyBFwvVVsSGSUx54-Gh76bwrt75va2VHpolSEXdhjcTK0KgscKLjU-LYDA_JD6AUaCi3WzMnpMSnO-9u_G"}` +var trustedCert = `-----BEGIN CERTIFICATE----- +MIIEbTCCAtWgAwIBAgICAK0wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3Rhcnkx +JTAjBgNVBAMTHE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwIBcNMjQwNDA0 +MjIwMzA1WhgPMjEyNDA0MDQyMjAzMDVaMGQxCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJXQTEQMA4GA1UEBxMHU2VhdHRsZTEPMA0GA1UEChMGTm90YXJ5MSUwIwYDVQQD +ExxOb3RhdGlvbiBFeGFtcGxlIHNlbGYtc2lnbmVkMIIBojANBgkqhkiG9w0BAQEF +AAOCAY8AMIIBigKCAYEA0dXD9UqzZcGlBlvPHO2uf+Sel/xwf/eOMS6Q30GV6JPe +u9czLmyR0YMfC6P0N4zDzVYYZtQLkS5lalTMGX9A3yj9aXtXvtoYtLx2mF1CfdQJ +McrT63wVVTWiPPe2JT8KHkkiACzVY6LTwc4s+DIAw9Gv21Uu6bFy4WWlGMp8UwTu +cR0JqaFoXzB6vxVRTkK8RRLM9Pj0hM5NwobpuZ+pc+ZS/7PhdvQHVzHeLLV9S7fH +xw3n1c0ti8VUjSPSqCIEqOL3Eu/0pWMXB2A1xzn3RBfnzZMD3Tw3ksFgLMVzblhv +41c6gr4cgjaS4wWwUvq9Xndd7Io8QNvxyiRDX5cHwQSEOmDfmegTIaLR0dKfvjY4 +ZJq8Y1DnaXU4RD6XeihtZykMlx7nTUyZZXpQ1akjh3VMzPykJ4mIknHh02zGRT9Z +E8E1kYzRWhU/0MAzVrTTFHpric6jO459ouTnQXFjKwAcoD5+bNY6TuhC18iar7+l +4BPPI1mFuqETnMfkkJQZAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE +DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAYEAe5wyQPo+h1Yk2PkaA5aJ +KuU8azF2pTLfhQwAn/1XqPcmhNQuomOP0waoBsh+6sexfIDZaNuJ+zZUxqYHke/2 +3+768SMiJCpuJfion3ak3Ka/IVNz48G0V+V+Vog+elkZzpdUQd30njLVcoQsihp0 +I/Gs3pnG2SeHmsdvYVuzycdYWTt5BFu4N8VWg4x4pfRMgDG7HGxRAacz2vTdqAx6 +rpWjO4xc0ZO8iUKjAeKHc7RuSx2dhUaRP9P8G8NBNtG6xNnbXIEjH6kP05srFRZ2 +jxm1an7sjsOpbBdIDztc0J+cb5yjBx7zo1OzWcmDUqMEXDR/WoygPzwhhHvWWvTq +wVSEUvYnSaI6wxyHGxPFuX3+vCEZxU8NEGIuJtfYXWeo9cev5+PqjDgVu0uCWF53 +ZFsXNWbpff1qpG/CgrpFh3vN6uquMK9H5zaJBKr0GZFUsNRB1S8cUBgcjIZlWv3w +rJQaOIFzF4RFO9dsYcG/b7ubdqSNGe4qfbsyuWf+1xsx +-----END CERTIFICATE-----` + +var ociPolicy = dummyOCIPolicyDocument() +var blobPolicy = dummyBlobPolicyDocument() +var store = truststore.NewX509TrustStore(dir.ConfigFS()) +var pm = mock.PluginManager{} func TestNewVerifier_Error(t *testing.T) { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() _, err := New(&policyDocument, nil, nil) - expectedErr := errors.New("trustPolicy or trustStore cannot be nil") + expectedErr := errors.New("trustStore cannot be nil") if err == nil || err.Error() != expectedErr.Error() { t.Fatalf("TestNewVerifier_Error expected error %v, got %v", expectedErr, err) } } func TestInvalidArtifactUriValidations(t *testing.T) { - policyDocument := dummyPolicyDocument() verifier := verifier{ - trustPolicyDoc: &policyDocument, - pluginManager: mock.PluginManager{}, + ociTrustPolicyDoc: &ociPolicy, + pluginManager: mock.PluginManager{}, } tests := []struct { @@ -110,14 +122,13 @@ func TestInvalidArtifactUriValidations(t *testing.T) { } func TestErrorNoApplicableTrustPolicy_Error(t *testing.T) { - policyDocument := dummyPolicyDocument() verifier := verifier{ - trustPolicyDoc: &policyDocument, - pluginManager: mock.PluginManager{}, + ociTrustPolicyDoc: &ociPolicy, + pluginManager: mock.PluginManager{}, } opts := notation.VerifierVerifyOptions{ArtifactReference: "non-existent-domain.com/repo@sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333"} _, err := verifier.Verify(context.Background(), ocispec.Descriptor{}, []byte{}, opts) - if !errors.Is(err, notation.ErrorNoApplicableTrustPolicy{Msg: "artifact \"non-existent-domain.com/repo@sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\" has no applicable trust policy. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy"}) { + if !errors.Is(err, notation.ErrorNoApplicableTrustPolicy{Msg: "artifact \"non-existent-domain.com/repo@sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\" has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy"}) { t.Fatalf("no applicable trust policy must throw error") } } @@ -156,7 +167,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Unsupported Signature Envelope for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() expectedErr := fmt.Errorf("unable to parse the digital signature, error : signature envelope format with media type \"application/unsupported+json\" is not supported") testCases = append(testCases, testCase{ verificationType: trustpolicy.TypeIntegrity, @@ -169,7 +180,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Integrity Success for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() testCases = append(testCases, testCase{ signatureBlob: validSigEnv, verificationType: trustpolicy.TypeIntegrity, @@ -181,7 +192,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Integrity Failure for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() expectedErr := fmt.Errorf("signature is invalid. Error: illegal base64 data at input byte 242") testCases = append(testCases, testCase{ signatureBlob: invalidSigEnv, @@ -195,7 +206,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Authenticity Success for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() // trust store is configured with the root certificate of the signature by default + policyDocument := dummyOCIPolicyDocument() // trust store is configured with the root certificate of the signature by default testCases = append(testCases, testCase{ signatureBlob: validSigEnv, verificationType: trustpolicy.TypeAuthenticity, @@ -207,7 +218,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Authenticity Failure for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() policyDocument.TrustPolicies[0].TrustStores = []string{"ca:valid-trust-store-2", "signingAuthority:valid-trust-store-2"} // trust store is not configured with the root certificate of the signature expectedErr := fmt.Errorf("signature is not produced by a trusted signer") testCases = append(testCases, testCase{ @@ -222,7 +233,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Authenticity Failure with trust store missing separator for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() policyDocument.TrustPolicies[0].TrustStores = []string{"ca:valid-trust-store-2", "signingAuthority"} expectedErr := fmt.Errorf("error while loading the trust store, trust policy statement \"test-statement-name\" is missing separator in trust store value \"signingAuthority\". The required format is :") testCases = append(testCases, testCase{ @@ -237,7 +248,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // TrustedIdentity Failure for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() policyDocument.TrustPolicies[0].TrustedIdentities = []string{"x509.subject:CN=LOL,O=DummyOrg,L=Hyderabad,ST=TG,C=IN"} // configure policy to not trust "CN=Notation Test Leaf Cert,O=Notary,L=Seattle,ST=WA,C=US" which is the subject of the signature's signing certificate expectedErr := fmt.Errorf("signing certificate from the digital signature does not match the X.509 trusted identities [map[\"C\":\"IN\" \"CN\":\"LOL\" \"L\":\"Hyderabad\" \"O\":\"DummyOrg\" \"ST\":\"TG\"]] defined in the trust policy \"test-statement-name\"") testCases = append(testCases, testCase{ @@ -252,7 +263,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // TrustedIdentity Failure without separator for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() policyDocument.TrustPolicies[0].TrustedIdentities = []string{"x509.subject"} expectedErr := fmt.Errorf("trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator") testCases = append(testCases, testCase{ @@ -267,7 +278,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // TrustedIdentity Failure with empty value for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() policyDocument.TrustPolicies[0].TrustedIdentities = []string{"x509.subject:"} expectedErr := fmt.Errorf("trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value") testCases = append(testCases, testCase{ @@ -282,7 +293,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Expiry Success for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() testCases = append(testCases, testCase{ signatureBlob: validSigEnv, verificationType: trustpolicy.TypeExpiry, @@ -294,7 +305,7 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { // Expiry Failure for _, level := range verificationLevels { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() expectedErr := fmt.Errorf("digital signature has expired on \"Fri, 29 Jul 2022 23:59:00 +0000\"") testCases = append(testCases, testCase{ signatureBlob: expiredSigEnv, @@ -326,10 +337,10 @@ func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { t.Fatalf("unexpected error while creating revocation object: %v", err) } verifier := verifier{ - trustPolicyDoc: &tt.policyDocument, - trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &tt.policyDocument, + trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), + pluginManager: pluginManager, + revocationClient: revocationClient, } outcome, _ := verifier.Verify(context.Background(), ocispec.Descriptor{}, tt.signatureBlob, tt.opts) verifyResult(outcome, expectedResult, tt.expectedErr, t) @@ -377,7 +388,7 @@ func TestVerifyRevocationEnvelope(t *testing.T) { t.Run("enforced revoked cert", func(t *testing.T) { testedLevel := trustpolicy.LevelStrict - policyDoc := dummyPolicyDocument() + policyDoc := dummyOCIPolicyDocument() policyDoc.TrustPolicies[0].SignatureVerification.VerificationLevel = testedLevel.Name policyDoc.TrustPolicies[0].SignatureVerification.Override = map[trustpolicy.ValidationType]trustpolicy.ValidationAction{ trustpolicy.TypeAuthenticity: trustpolicy.ActionLog, @@ -393,10 +404,10 @@ func TestVerifyRevocationEnvelope(t *testing.T) { dir.UserConfigDir = "testdata" verifier := verifier{ - trustPolicyDoc: &policyDoc, - trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDoc, + trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), + pluginManager: pluginManager, + revocationClient: revocationClient, } outcome, err := verifier.Verify(context.Background(), desc, envelopeBlob, opts) if err == nil || err.Error() != expectedErr.Error() { @@ -406,7 +417,7 @@ func TestVerifyRevocationEnvelope(t *testing.T) { }) t.Run("log revoked cert", func(t *testing.T) { testedLevel := trustpolicy.LevelStrict - policyDoc := dummyPolicyDocument() + policyDoc := dummyOCIPolicyDocument() policyDoc.TrustPolicies[0].SignatureVerification.VerificationLevel = testedLevel.Name policyDoc.TrustPolicies[0].SignatureVerification.Override = map[trustpolicy.ValidationType]trustpolicy.ValidationAction{ trustpolicy.TypeAuthenticity: trustpolicy.ActionLog, @@ -422,10 +433,10 @@ func TestVerifyRevocationEnvelope(t *testing.T) { dir.UserConfigDir = "testdata" verifier := verifier{ - trustPolicyDoc: &policyDoc, - trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDoc, + trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), + pluginManager: pluginManager, + revocationClient: revocationClient, } ctx := context.Background() outcome, err := verifier.Verify(ctx, desc, envelopeBlob, opts) @@ -436,7 +447,7 @@ func TestVerifyRevocationEnvelope(t *testing.T) { }) t.Run("skip revoked cert", func(t *testing.T) { testedLevel := trustpolicy.LevelStrict - policyDoc := dummyPolicyDocument() + policyDoc := dummyOCIPolicyDocument() policyDoc.TrustPolicies[0].SignatureVerification.VerificationLevel = testedLevel.Name policyDoc.TrustPolicies[0].SignatureVerification.Override = map[trustpolicy.ValidationType]trustpolicy.ValidationAction{ trustpolicy.TypeAuthenticity: trustpolicy.ActionLog, @@ -446,10 +457,10 @@ func TestVerifyRevocationEnvelope(t *testing.T) { dir.UserConfigDir = "testdata" verifier := verifier{ - trustPolicyDoc: &policyDoc, - trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDoc, + trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), + pluginManager: pluginManager, + revocationClient: revocationClient, } outcome, err := verifier.Verify(context.Background(), desc, envelopeBlob, opts) if err != nil { @@ -672,88 +683,175 @@ func TestVerifyRevocation(t *testing.T) { }) } +func TestNew(t *testing.T) { + if _, err := New(&ociPolicy, store, pm); err != nil { + t.Fatalf("expected New constructor to succeed, but got %v", err) + } +} + func TestNewWithOptions(t *testing.T) { - policy := dummyPolicyDocument() - store := truststore.NewX509TrustStore(dir.ConfigFS()) - pm := mock.PluginManager{} - client := &http.Client{Timeout: 2 * time.Second} - r, err := revocation.New(client) + if _, err := NewWithOptions(&ociPolicy, store, pm, VerifierOptions{}); err != nil { + t.Fatalf("expected NewWithOptions constructor to succeed, but got %v", err) + } +} + +func TestNewVerifierWithOptions(t *testing.T) { + r, err := revocation.New(&http.Client{}) if err != nil { t.Fatalf("unexpected error while creating revocation object: %v", err) } opts := VerifierOptions{RevocationClient: r} - t.Run("successful call from New (default value)", func(t *testing.T) { - v, err := New(&policy, store, pm) - if err != nil { - t.Fatalf("expected New constructor to succeed with default client, but got %v", err) - } - verifierV, ok := v.(*verifier) - if !ok { - t.Fatal("expected constructor to return a verifier object") - } - if !(verifierV.trustPolicyDoc == &policy) { - t.Fatalf("expected trustPolicyDoc %v, but got %v", &policy, verifierV.trustPolicyDoc) - } - if !(verifierV.trustStore == store) { - t.Fatalf("expected trustStore %v, but got %v", store, verifierV.trustStore) - } - if !reflect.DeepEqual(verifierV.pluginManager, pm) { - t.Fatalf("expected pluginManager %v, but got %v", pm, verifierV.pluginManager) - } - if verifierV.revocationClient == nil { - t.Fatal("expected nonnil revocationClient") + v, err := NewVerifierWithOptions(&ociPolicy, &blobPolicy, store, pm, opts) + if err != nil { + t.Fatalf("expected NewVerifierWithOptions constructor to succeed, but got %v", err) + } + if !(v.ociTrustPolicyDoc == &ociPolicy) { + t.Fatalf("expected ociTrustPolicyDoc %v, but got %v", v, v.ociTrustPolicyDoc) + } + if !(v.trustStore == store) { + t.Fatalf("expected trustStore %v, but got %v", store, v.trustStore) + } + if !reflect.DeepEqual(v.pluginManager, pm) { + t.Fatalf("expected pluginManager %v, but got %v", pm, v.pluginManager) + } + if v.revocationClient == nil { + t.Fatal("expected nonnil revocationClient") + } + + _, err = NewVerifierWithOptions(nil, &blobPolicy, store, pm, opts) + if err != nil { + t.Fatalf("expected NewVerifierWithOptions constructor to succeed, but got %v", err) + } + + _, err = NewVerifierWithOptions(&ociPolicy, nil, store, pm, opts) + if err != nil { + t.Fatalf("expected NewVerifierWithOptions constructor to succeed, but got %v", err) + } + + opts.RevocationClient = nil + _, err = NewVerifierWithOptions(&ociPolicy, nil, store, pm, opts) + if err != nil { + t.Fatalf("expected NewVerifierWithOptions constructor to succeed, but got %v", err) + } +} + +func TestNewVerifierWithOptionsError(t *testing.T) { + r, err := revocation.New(&http.Client{}) + if err != nil { + t.Fatalf("unexpected error while creating revocation object: %v", err) + } + opts := VerifierOptions{RevocationClient: r} + + _, err = NewVerifierWithOptions(nil, nil, store, pm, opts) + if err == nil || err.Error() != "ociTrustPolicy and blobTrustPolicy both cannot be nil" { + t.Errorf("expected err but not found.") + } + + _, err = NewVerifierWithOptions(&ociPolicy, &blobPolicy, nil, pm, opts) + if err == nil || err.Error() != "trustStore cannot be nil" { + t.Errorf("expected err but not found.") + } +} + +func TestVerifyBlob(t *testing.T) { + policy := &trustpolicy.BlobDocument{ + Version: "1.0", + TrustPolicies: []trustpolicy.BlobTrustPolicy{ + { + Name: "blob-test-policy", + SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"}, + TrustStores: []string{"ca:dummy-ts"}, + TrustedIdentities: []string{"*"}, + }, + }, + } + v, err := NewVerifier(nil, policy, &testTrustStore{}, pm) + if err != nil { + t.Fatalf("unexpected error while creating verifier: %v", err) + } + + opts := notation.BlobVerifierVerifyOptions{ + SignatureMediaType: jws.MediaTypeEnvelope, + TrustPolicyName: "blob-test-policy", + } + descGenFunc := getTestDescGenFunc(false, "") + + t.Run("without user defined metadata", func(t *testing.T) { + // verify with + if _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(testSig), opts); err != nil { + t.Fatalf("VerifyBlob() returned unexpected error: %v", err) } }) - t.Run("successful with empty options", func(t *testing.T) { - v, err := NewWithOptions(&policy, store, pm, VerifierOptions{}) - if err != nil { - t.Fatalf("expected NewWithOptions constructor to succeed with empty options, but got %v", err) - } - verifierV, ok := v.(*verifier) - if !ok { - t.Fatal("expected constructor to return a verifier object") - } - if !(verifierV.trustPolicyDoc == &policy) { - t.Fatalf("expected trustPolicyDoc %v, but got %v", &policy, verifierV.trustPolicyDoc) - } - if !(verifierV.trustStore == store) { - t.Fatalf("expected trustStore %v, but got %v", store, verifierV.trustStore) - } - if !reflect.DeepEqual(verifierV.pluginManager, pm) { - t.Fatalf("expected pluginManager %v, but got %v", pm, verifierV.pluginManager) - } - if verifierV.revocationClient == nil { - t.Fatal("expected nonnil revocationClient") + t.Run("with user defined metadata", func(t *testing.T) { + opts.UserMetadata = map[string]string{"buildId": "101"} + if _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(testSig), opts); err != nil { + t.Fatalf("VerifyBlob() with user metadata returned unexpected error: %v", err) } }) - t.Run("successful with client", func(t *testing.T) { - v, err := NewWithOptions(&policy, store, pm, opts) - if err != nil { - t.Fatalf("expected NewWithOptions constructor to succeed, but got %v", err) + t.Run("trust policy set to skip", func(t *testing.T) { + policy.TrustPolicies[0].SignatureVerification = trustpolicy.SignatureVerification{VerificationLevel: "skip"} + opts.UserMetadata = map[string]string{"buildId": "101"} + if _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(testSig), opts); err != nil { + t.Fatalf("VerifyBlob() with user metadata returned unexpected error: %v", err) } + }) +} - expectedV := &verifier{ - trustPolicyDoc: &policy, - trustStore: store, - pluginManager: pm, - revocationClient: r, +func TestVerifyBlob_Error(t *testing.T) { + policy := &trustpolicy.BlobDocument{ + Version: "1.0", + TrustPolicies: []trustpolicy.BlobTrustPolicy{ + { + Name: "blob-test-policy", + SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"}, + TrustStores: []string{"ca:dummy-ts"}, + TrustedIdentities: []string{"*"}, + }, + }, + } + v, err := NewVerifier(nil, policy, &testTrustStore{}, pm) + if err != nil { + t.Fatalf("unexpected error while creating verifier: %v", err) + } + + opts := notation.BlobVerifierVerifyOptions{ + SignatureMediaType: jws.MediaTypeEnvelope, + TrustPolicyName: "blob-test-policy", + } + + t.Run("BlobDescriptorGenerator returns error", func(t *testing.T) { + descGenFunc := getTestDescGenFunc(true, "") + _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(testSig), opts) + if err == nil || err.Error() != "failed to generate descriptor for given artifact. Error: intentional test desc generation error" { + t.Errorf("VerifyBlob() didn't return error or didnt returned expected error: %v", err) } - if !reflect.DeepEqual(expectedV, v) { - t.Fatalf("expected %v to be created, but got %v", expectedV, v) + }) + + t.Run("descriptor mismatch returns error", func(t *testing.T) { + descGenFunc := getTestDescGenFunc(false, "sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a62") + _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(testSig), opts) + if err == nil || err.Error() != "integrity check failed. signature does not match the given blob" { + t.Errorf("VerifyBlob() didn't return error or didnt returned expected error: %v", err) } }) - t.Run("fail with nil trust policy", func(t *testing.T) { - v, err := NewWithOptions(nil, store, pm, opts) - expectedErrMsg := "trustPolicy or trustStore cannot be nil" - if err == nil || err.Error() != expectedErrMsg { - t.Fatalf("expected NewWithOptions constructor to fail with %v, but got %v", expectedErrMsg, err) + t.Run("signature malformed returns error", func(t *testing.T) { + descGenFunc := getTestDescGenFunc(false, "") + _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(""), opts) + if err == nil || err.Error() != "unable to parse the digital signature, error : unexpected end of JSON input" { + t.Errorf("VerifyBlob() didn't return error or didnt returned expected error: %v", err) } - if v != nil { - t.Fatal("expected constructor to return nil") + }) + + t.Run("user defined metadata mismatch returns error", func(t *testing.T) { + descGenFunc := getTestDescGenFunc(false, "") + opts.UserMetadata = map[string]string{"buildId": "zzz"} + _, err = v.VerifyBlob(context.Background(), descGenFunc, []byte(testSig), opts) + if err == nil || err.Error() != "unable to find specified metadata in the signature" { + t.Fatalf("VerifyBlob() with user metadata returned unexpected error: %v", err) } }) } @@ -771,7 +869,7 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { pluginSigEnv = mock.MockSaPluginSigEnv } - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() dir.UserConfigDir = "testdata" x509TrustStore := truststore.NewX509TrustStore(dir.ConfigFS()) @@ -784,10 +882,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { t.Fatalf("unexpected error while creating revocation object: %v", err) } v := verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts := notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err := v.Verify(context.Background(), ocispec.Descriptor{}, pluginSigEnv, opts) @@ -800,10 +898,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { pluginManager.PluginCapabilities = []proto.Capability{proto.CapabilitySignatureGenerator} v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), ocispec.Descriptor{}, pluginSigEnv, opts) @@ -824,10 +922,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), mock.ImageDescriptor, pluginSigEnv, opts) @@ -849,10 +947,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), ocispec.Descriptor{}, pluginSigEnv, opts) @@ -873,10 +971,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), mock.ImageDescriptor, pluginSigEnv, opts) @@ -898,10 +996,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), ocispec.Descriptor{}, pluginSigEnv, opts) @@ -925,10 +1023,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), mock.ImageDescriptor, pluginSigEnv, opts) @@ -943,10 +1041,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { pluginManager.PluginRunnerExecuteError = errors.New("revocation plugin should not be invoked when the trust policy skips revocation check") v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} trustPolicy, err := (&policyDocument).GetApplicableTrustPolicy(opts.ArtifactReference) @@ -970,10 +1068,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { pluginManager.PluginRunnerExecuteError = errors.New("invalid plugin response") v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} trustPolicy, err = (&policyDocument).GetApplicableTrustPolicy(opts.ArtifactReference) @@ -1003,10 +1101,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), mock.ImageDescriptor, pluginSigEnv, opts) @@ -1023,10 +1121,10 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } v = verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts = notation.VerifierVerifyOptions{ArtifactReference: mock.SampleArtifactUri, SignatureMediaType: "application/jose+json"} outcome, err = v.Verify(context.Background(), mock.ImageDescriptor, pluginSigEnv, opts) @@ -1036,7 +1134,6 @@ func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { } func TestVerifyX509TrustedIdentities(t *testing.T) { - certs, _ := corex509.ReadCertificateFile(filepath.FromSlash("testdata/verifier/signing-cert.pem")) // cert's subject is "CN=SomeCN,OU=SomeOU,O=SomeOrg,L=Seattle,ST=WA,C=US" unsupportedCerts, _ := corex509.ReadCertificateFile(filepath.FromSlash("testdata/verifier/bad-cert.pem")) // cert's subject is "CN=bad=#CN,OU=SomeOU,O=SomeOrg,L=Seattle,ST=WA,C=US" @@ -1065,7 +1162,7 @@ func TestVerifyX509TrustedIdentities(t *testing.T) { TrustStores: []string{"ca:test-store"}, TrustedIdentities: tt.x509Identities, } - err := verifyX509TrustedIdentities(tt.certs, &trustPolicy) + err := verifyX509TrustedIdentities(trustPolicy.Name, trustPolicy.TrustedIdentities, tt.certs) if tt.wantErr != (err != nil) { t.Fatalf("TestVerifyX509TrustedIdentities Error: %q WantErr: %v", err, tt.wantErr) @@ -1075,7 +1172,7 @@ func TestVerifyX509TrustedIdentities(t *testing.T) { } func TestVerifyUserMetadata(t *testing.T) { - policyDocument := dummyPolicyDocument() + policyDocument := dummyOCIPolicyDocument() policyDocument.TrustPolicies[0].SignatureVerification.VerificationLevel = trustpolicy.LevelAudit.Name pluginManager := mock.PluginManager{} @@ -1086,10 +1183,10 @@ func TestVerifyUserMetadata(t *testing.T) { t.Fatalf("unexpected error while creating revocation object: %v", err) } verifier := verifier{ - trustPolicyDoc: &policyDocument, - trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: truststore.NewX509TrustStore(dir.ConfigFS()), + pluginManager: pluginManager, + revocationClient: revocationClient, } tests := []struct { @@ -1155,10 +1252,10 @@ func TestPluginVersionCompatibility(t *testing.T) { t.Fatalf("unexpected error while creating revocation object: %v", err) } v := verifier{ - trustPolicyDoc: &policyDocument, - trustStore: x509TrustStore, - pluginManager: pluginManager, - revocationClient: revocationClient, + ociTrustPolicyDoc: &policyDocument, + trustStore: x509TrustStore, + pluginManager: pluginManager, + revocationClient: revocationClient, } opts := notation.VerifierVerifyOptions{ArtifactReference: "localhost:5000/net-monitor@sha256:fe7e9333395060c2f5e63cf36a38fba10176f183b4163a5794e081a480abba5f", SignatureMediaType: "application/jose+json"} @@ -1187,7 +1284,6 @@ func TestPluginVersionCompatibility(t *testing.T) { } func TestIsRequiredVerificationPluginVer(t *testing.T) { - testPlugVer := "1.0.0" tests := []struct { @@ -1210,3 +1306,57 @@ func TestIsRequiredVerificationPluginVer(t *testing.T) { } } } + +func verifyResult(outcome *notation.VerificationOutcome, expectedResult notation.ValidationResult, expectedErr error, t *testing.T) { + var actualResult *notation.ValidationResult + for _, r := range outcome.VerificationResults { + if r.Type == expectedResult.Type { + if actualResult == nil { + actualResult = r + } else { + t.Fatalf("expected only one VerificatiionResult for %q but found one more. first: %+v second: %+v", r.Type, actualResult, r) + } + } + } + + if actualResult == nil || + (expectedResult.Error != nil && expectedResult.Error.Error() != actualResult.Error.Error()) || + expectedResult.Action != actualResult.Action { + t.Fatalf("assertion failed. expected : %+v got : %+v", expectedResult, actualResult) + } + + if expectedResult.Action == trustpolicy.ActionEnforce && expectedErr != nil && outcome.Error.Error() != expectedErr.Error() { + t.Fatalf("assertion failed. expected : %v got : %v", expectedErr, outcome.Error) + } +} + +// testTrustStore implements truststore.X509TrustStore and returns the trusted certificates for a given trust-store. +type testTrustStore struct { + certs []*x509.Certificate +} + +func (ts *testTrustStore) GetCertificates(_ context.Context, _ truststore.Type, _ string) ([]*x509.Certificate, error) { + block, _ := pem.Decode([]byte(trustedCert)) + cert, _ := x509.ParseCertificate(block.Bytes) + return []*x509.Certificate{cert}, nil +} + +func getTestDescGenFunc(returnErr bool, customDigest digest.Digest) notation.BlobDescriptorGenerator { + return func(digest.Algorithm) (ocispec.Descriptor, error) { + var err error = nil + if returnErr { + err = errors.New("intentional test desc generation error") + } + + var expDigest digest.Digest = "sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61" + if customDigest != "" { + expDigest = customDigest + } + + return ocispec.Descriptor{ + MediaType: "video/mp4", + Digest: expDigest, + Size: 12, + }, err + } +}