From 219e11b447f316444735d1f99f158cd4b6f50737 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 8 Nov 2022 13:59:05 +0800 Subject: [PATCH 01/28] update Signed-off-by: Patrick Zheng --- internal/mock/mocks.go | 4 +- notation.go => internal/notation.go | 0 {registry => internal/registry}/interface.go | 2 +- internal/registry/mediatype.go | 4 + internal/registry/repository.go | 198 +++++++ .../registry}/repository_test.go | 2 +- notation/errors.go | 49 ++ notation/notation.go | 299 +++++++++++ notation/notation_test.go | 139 +++++ registry/repository.go | 208 +------- registry/repositoryClient.go | 163 ++++++ registry/repositoryClient_test.go | 499 ++++++++++++++++++ signature/envelope.go | 2 +- signature/envelope_test.go | 2 +- signature/plugin.go | 2 +- signature/plugin_test.go | 2 +- signature/signer.go | 2 +- signature/signer_test.go | 2 +- verification/verifier.go | 4 +- verification/verifier_helpers.go | 2 +- verification/verifier_test.go | 4 +- 21 files changed, 1387 insertions(+), 202 deletions(-) rename notation.go => internal/notation.go (100%) rename {registry => internal/registry}/interface.go (94%) create mode 100644 internal/registry/mediatype.go create mode 100644 internal/registry/repository.go rename {registry => internal/registry}/repository_test.go (99%) create mode 100644 notation/errors.go create mode 100644 notation/notation.go create mode 100644 notation/notation_test.go create mode 100644 registry/repositoryClient.go create mode 100644 registry/repositoryClient_test.go diff --git a/internal/mock/mocks.go b/internal/mock/mocks.go index 7903b9f1..7982b916 100644 --- a/internal/mock/mocks.go +++ b/internal/mock/mocks.go @@ -5,10 +5,10 @@ import ( _ "embed" "github.com/notaryproject/notation-core-go/signature" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" + "github.com/notaryproject/notation-go/internal/registry" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" - "github.com/notaryproject/notation-go/registry" "github.com/opencontainers/go-digest" ) diff --git a/notation.go b/internal/notation.go similarity index 100% rename from notation.go rename to internal/notation.go diff --git a/registry/interface.go b/internal/registry/interface.go similarity index 94% rename from registry/interface.go rename to internal/registry/interface.go index e8e621bc..6d683146 100644 --- a/registry/interface.go +++ b/internal/registry/interface.go @@ -3,7 +3,7 @@ package registry import ( "context" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" "github.com/opencontainers/go-digest" ) diff --git a/internal/registry/mediatype.go b/internal/registry/mediatype.go new file mode 100644 index 00000000..a7350d19 --- /dev/null +++ b/internal/registry/mediatype.go @@ -0,0 +1,4 @@ +package registry + +// ArtifactTypeNotation specifies the artifact type for a notation object. +const ArtifactTypeNotation = "application/vnd.cncf.notary.v2.signature" diff --git a/internal/registry/repository.go b/internal/registry/repository.go new file mode 100644 index 00000000..f59f9d37 --- /dev/null +++ b/internal/registry/repository.go @@ -0,0 +1,198 @@ +package registry + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + notation "github.com/notaryproject/notation-go/internal" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" +) + +const ( + maxBlobSizeLimit = 32 * 1024 * 1024 // 32 MiB + maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB +) + +type RepositoryClient struct { + remote.Repository +} + +type SignatureManifest struct { + Blob notation.Descriptor + Annotations map[string]string +} + +// NewRepositoryClient creates a new registry client. +func NewRepositoryClient(client remote.Client, ref registry.Reference, plainHTTP bool) *RepositoryClient { + return &RepositoryClient{ + Repository: remote.Repository{ + Client: client, + Reference: ref, + PlainHTTP: plainHTTP, + }, + } +} + +// Resolve resolves a reference(tag or digest) to a manifest descriptor +func (c *RepositoryClient) Resolve(ctx context.Context, reference string) (notation.Descriptor, error) { + desc, err := c.Repository.Resolve(ctx, reference) + if err != nil { + return notation.Descriptor{}, err + } + return notationDescriptorFromOCI(desc), nil +} + +// ListSignatureManifests returns all signature manifests given the manifest digest +func (c *RepositoryClient) ListSignatureManifests(ctx context.Context, manifestDigest digest.Digest) ([]SignatureManifest, error) { + var signatureManifests []SignatureManifest + if err := c.Repository.Referrers(ctx, ocispec.Descriptor{ + Digest: manifestDigest, + }, ArtifactTypeNotation, func(referrers []artifactspec.Descriptor) error { + for _, desc := range referrers { + if desc.MediaType != artifactspec.MediaTypeArtifactManifest { + continue + } + artifact, err := c.getArtifactManifest(ctx, desc.Digest) + if err != nil { + return fmt.Errorf("failed to fetch manifest: %v: %v", desc.Digest, err) + } + if len(artifact.Blobs) == 0 { + continue + } + signatureManifests = append(signatureManifests, SignatureManifest{ + Blob: notationDescriptorFromArtifact(artifact.Blobs[0]), + Annotations: artifact.Annotations, + }) + } + return nil + }); err != nil { + return nil, err + } + return signatureManifests, nil +} + +// GetBlob downloads the content of the specified digest's Blob +func (c *RepositoryClient) GetBlob(ctx context.Context, digest digest.Digest) ([]byte, error) { + desc, err := c.Repository.Blobs().Resolve(ctx, digest.String()) + if err != nil { + return nil, err + } + if desc.Size > maxBlobSizeLimit { + return nil, fmt.Errorf("signature blob too large: %d", desc.Size) + } + return content.FetchAll(ctx, c.Repository.Blobs(), desc) +} + +// PutSignatureManifest creates and uploads an signature artifact linking the manifest and the signature +func (c *RepositoryClient) PutSignatureManifest(ctx context.Context, signature []byte, signatureMediaType string, subjectManifest notation.Descriptor, annotations map[string]string) (notation.Descriptor, SignatureManifest, error) { + signatureDesc, err := c.uploadSignature(ctx, signature, signatureMediaType) + if err != nil { + return notation.Descriptor{}, SignatureManifest{}, err + } + + manifestDesc, err := c.uploadSignatureManifest(ctx, artifactDescriptorFromNotation(subjectManifest), signatureDesc, annotations) + if err != nil { + return notation.Descriptor{}, SignatureManifest{}, err + } + + signatureManifest := SignatureManifest{ + Blob: notationDescriptorFromArtifact(signatureDesc), + Annotations: annotations, + } + return notationDescriptorFromOCI(manifestDesc), signatureManifest, nil +} + +func (c *RepositoryClient) getArtifactManifest(ctx context.Context, manifestDigest digest.Digest) (artifactspec.Manifest, error) { + repo := c.Repository + repo.ManifestMediaTypes = []string{ + artifactspec.MediaTypeArtifactManifest, + } + store := repo.Manifests() + desc, err := store.Resolve(ctx, manifestDigest.String()) + if err != nil { + return artifactspec.Manifest{}, err + } + if desc.Size > maxManifestSizeLimit { + return artifactspec.Manifest{}, fmt.Errorf("manifest too large: %d", desc.Size) + } + manifestJSON, err := content.FetchAll(ctx, store, desc) + if err != nil { + return artifactspec.Manifest{}, err + } + + var manifest artifactspec.Manifest + err = json.Unmarshal(manifestJSON, &manifest) + if err != nil { + return artifactspec.Manifest{}, err + } + return manifest, nil +} + +// uploadSignature uploads the signature to the registry +func (c *RepositoryClient) uploadSignature(ctx context.Context, signature []byte, signatureMediaType string) (artifactspec.Descriptor, error) { + desc := ocispec.Descriptor{ + MediaType: signatureMediaType, + Digest: digest.FromBytes(signature), + Size: int64(len(signature)), + } + if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(signature)); err != nil { + return artifactspec.Descriptor{}, err + } + return artifactDescriptorFromOCI(desc), nil +} + +// uploadSignatureManifest uploads the signature manifest to the registry +func (c *RepositoryClient) uploadSignatureManifest(ctx context.Context, subjectManifest, signatureDesc artifactspec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { + opts := oras.PackArtifactOptions{ + Subject: &subjectManifest, + ManifestAnnotations: annotations, + } + + return oras.PackArtifact( + ctx, + c.Repository.Manifests(), + ArtifactTypeNotation, + []artifactspec.Descriptor{signatureDesc}, + opts, + ) +} + +func artifactDescriptorFromNotation(desc notation.Descriptor) artifactspec.Descriptor { + return artifactspec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} + +func notationDescriptorFromArtifact(desc artifactspec.Descriptor) notation.Descriptor { + return notation.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} + +func artifactDescriptorFromOCI(desc ocispec.Descriptor) artifactspec.Descriptor { + return artifactspec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} + +func notationDescriptorFromOCI(desc ocispec.Descriptor) notation.Descriptor { + return notation.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} diff --git a/registry/repository_test.go b/internal/registry/repository_test.go similarity index 99% rename from registry/repository_test.go rename to internal/registry/repository_test.go index 97917b2b..0b69877b 100644 --- a/registry/repository_test.go +++ b/internal/registry/repository_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" "github.com/opencontainers/go-digest" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" "oras.land/oras-go/v2/registry" diff --git a/notation/errors.go b/notation/errors.go new file mode 100644 index 00000000..733b0cae --- /dev/null +++ b/notation/errors.go @@ -0,0 +1,49 @@ +package notation + +// ErrorVerificationInconclusive is used when signature verification fails due to a runtime error (e.g. a network error) +type ErrorVerificationInconclusive struct { + Msg string +} + +func (e ErrorVerificationInconclusive) Error() string { + if e.Msg != "" { + return e.Msg + } + return "signature verification was inclusive due to an unexpected error" +} + +// ErrorNoApplicableTrustPolicy is used when there is no trust policy that applies to the given artifact +type ErrorNoApplicableTrustPolicy struct { + Msg string +} + +func (e ErrorNoApplicableTrustPolicy) Error() string { + if e.Msg != "" { + return e.Msg + } + return "there is no applicable trust policy for the given artifact" +} + +// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the digital signature/s for the given artifact +type ErrorSignatureRetrievalFailed struct { + Msg string +} + +func (e ErrorSignatureRetrievalFailed) Error() string { + if e.Msg != "" { + return e.Msg + } + return "unable to retrieve the digital signature from the registry" +} + +// ErrorVerificationFailed is used when it is determined that the digital signature/s is not valid for the given artifact +type ErrorVerificationFailed struct { + Msg string +} + +func (e ErrorVerificationFailed) Error() string { + if e.Msg != "" { + return e.Msg + } + return "signature verification failed" +} diff --git a/notation/notation.go b/notation/notation.go new file mode 100644 index 00000000..57eeab3a --- /dev/null +++ b/notation/notation.go @@ -0,0 +1,299 @@ +package notation + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/timestamp" + "github.com/notaryproject/notation-go/internal/policy" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +const AnnotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" + +// Descriptor describes the artifact that needs to be signed. +type Descriptor struct { + // The media type of the targeted content. + MediaType string `json:"mediaType"` + + // The digest of the targeted content. + Digest digest.Digest `json:"digest"` + + // Specifies the size in bytes of the blob. + Size int64 `json:"size"` + + // Contains optional user defined attributes. + Annotations map[string]string `json:"annotations,omitempty"` +} + +// Equal reports whether d and t points to the same content. +func (d Descriptor) Equal(t Descriptor) bool { + return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size +} + +// SignOptions contains parameters for Signer.Sign. +type SignOptions struct { + // Expiry identifies the expiration time of the resulted signature. + Expiry time.Time + + // TSA is the TimeStamp Authority to timestamp the resulted signature if + // present. + TSA timestamp.Timestamper + + // TSAVerifyOptions is the verify option to verify the fetched timestamp + // signature. + // The `Intermediates` in the verify options will be ignored and + // re-contrusted using the certificates in the fetched timestamp signature. + // An empty list of `KeyUsages` in the verify options implies + // ExtKeyUsageTimeStamping. + TSAVerifyOptions x509.VerifyOptions + + // Sets or overrides the plugin configuration. + PluginConfig map[string]string +} + +// Payload describes the content that gets signed. +type Payload struct { + TargetArtifact Descriptor `json:"targetArtifact"` +} + +// Signer is a generic interface for signing an artifact. +// The interface allows signing with local or remote keys, +// and packing in various signature formats. +type Signer interface { + // Sign signs the artifact described by its descriptor, + // and returns the signature, SignerInfo, and envelopeMediaType. + Sign(ctx context.Context, desc Descriptor, envelopeMediaType string, opts SignOptions) ([]byte, *signature.SignerInfo, error) +} + +// Sign signs the artifact in the remote registry and push the signature to the +// remote. +// The descriptor of the sign content is returned upon sucessful signing. +func Sign(ctx context.Context, signer Signer, repo registry.Repository, reference string, envelopeMediaType string, opts SignOptions) (Descriptor, error) { + ociDesc, err := repo.Resolve(ctx, reference) + if err != nil { + return Descriptor{}, err + } + desc := notationDescriptorFromOCI(ociDesc) + sig, signerInfo, err := signer.Sign(ctx, desc, envelopeMediaType, opts) + if err != nil { + return Descriptor{}, err + } + annotations, err := generateAnnotations(signerInfo) + if err != nil { + return Descriptor{}, err + } + _, _, err = repo.PushSignature(ctx, sig, envelopeMediaType, ociDesc, annotations) + if err != nil { + return Descriptor{}, err + } + + return desc, nil +} + +// VerifyOptions contains parameters for Verifier.Verify. +type VerifyOptions struct { + ArtifactReference string + // SignatureMediaType is the envelope type of the signature. + // Currently both `application/jose+json` and `application/cose` are + // supported. + SignatureMediaType string + PluginConfig map[string]string +} + +// VerificationResult encapsulates the verification result (passed or failed) +// for a verification type, including the desired verification action as +// +// specified in the trust policy +type VerificationResult struct { + // Success is set to true if the verification was successful + Success bool + // Type of verification that is performed + Type trustpolicy.ValidationType + // Action is the intended action for the given verification type as defined + // in the trust policy + Action trustpolicy.ValidationAction + // Err is set if there are any errors during the verification process + Error error +} + +// VerificationOutcome encapsulates the SignerInfo (that includes the details of +// the digital signature) +// and results for each verification type that was performed +type VerificationOutcome struct { + SignatureBlobDescriptor *ocispec.Descriptor + // EnvelopeContent contains the details of the digital signature and + // associated metadata + EnvelopeContent *signature.EnvelopeContent + // VerificationLevel describes what verification level was used for + // performing signature verification + VerificationLevel *trustpolicy.VerificationLevel + // VerificationResults contains the verifications performed on the signature + // and their results + VerificationResults []*VerificationResult + // SignedAnnotations contains arbitrary metadata relating to the target + // artifact that was signed + SignedAnnotations map[string]string + // Error that caused the verification to fail (if it fails) + Error error +} + +// Verifier is a generic interface for verifying an artifact. +type Verifier interface { + // Verify verifies the signature blob and returns the verified descriptor + // upon successful verification. + Verify(ctx context.Context, signature []byte, opts VerifyOptions, outcome *VerificationOutcome) (Descriptor, error) + + // TrustPolicyDocument gets the validated trust policy document. + TrustPolicyDocument() (*trustpolicy.Document, error) +} + +/* +Verify performs signature verification on each of the notation supported +verification types (like integrity, authenticity, etc.) and return the +verification outcomes. + +Given an artifact reference, Verify will retrieve all the signatures associated +with the reference and perform signature verification. +A signature is considered not valid if verification fails due to any one of the +following reasons + + 1. Artifact Reference is not associated with a signature i.e. unsigned + 2. Registry is unavailable to retrieve the signature + 3. Signature does not satisfy the verification rules configured in the trust + policy + 4. Signature specifies a plugin for extended verification and that throws an + error + 5. Digest in the signature does not match the digest present in the reference + +If each and every signature associated with the reference fail the verification, +then Verify will return `ErrorVerificationFailed` error along with an array +of `VerificationOutcome`. + +# Callers can pass the verification plugin config in VerifyOptions.PluginConfig + +For more details on signature verification, see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#signature-verification +*/ +func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (Descriptor, []*VerificationOutcome, error) { + var verificationOutcomes []*VerificationOutcome + artifactRef := opts.ArtifactReference + artifactDescriptor, err := repo.Resolve(ctx, artifactRef) + if err != nil { + return Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} + } + + trustpolicyDoc, err := verifier.TrustPolicyDocument() + if err != nil { + return Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} + } + trustPolicy, err := policy.GetApplicableTrustPolicy(trustpolicyDoc, artifactRef) + if err != nil { + return Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} + } + // ignore the error since we already validated the policy document + verificationLevel, _ := trustpolicy.GetVerificationLevel(trustPolicy.SignatureVerification) + if verificationLevel.Name == trustpolicy.LevelSkip.Name { + verificationOutcomes = append(verificationOutcomes, &VerificationOutcome{VerificationLevel: verificationLevel}) + return Descriptor{}, verificationOutcomes, nil + } + + // get signature manifests + var success bool + var verifiedSigBlobDesc ocispec.Descriptor + err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { + if len(signatureManifests) < 1 { + return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", artifactRef)} + } + // if already verified successfully, no need to continue + if success { + return nil + } + // process signatures + for _, sigManifest := range signatureManifests { + // get signature envelope + sigBlob, sigBlobDesc, err := repo.FetchSignatureBlob(ctx, sigManifest) + if err != nil { + return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %s", sigBlobDesc.Digest, artifactRef, err.Error())} + } + outcome := &VerificationOutcome{ + SignatureBlobDescriptor: &sigBlobDesc, + VerificationResults: []*VerificationResult{}, + VerificationLevel: verificationLevel, + } + _, err = verifier.Verify(ctx, sigBlob, opts, outcome) + if err != nil { + if outcome != nil && outcome.Error != nil { + verificationOutcomes = append(verificationOutcomes, outcome) + } + continue + } + verificationOutcomes = append(verificationOutcomes, outcome) + + // artifact digest must match the digest from the signature payload + payload := &Payload{} + err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) + if err != nil || !notationDescriptorFromOCI(artifactDescriptor).Equal(payload.TargetArtifact) { + outcome.Error = fmt.Errorf("given digest %q does not match the digest %q present in the digital signature", artifactDescriptor.Digest.String(), payload.TargetArtifact.Digest.String()) + continue + } + outcome.SignedAnnotations = payload.TargetArtifact.Annotations + + // At this point, we've found a signature verified successfully + success = true + // Descriptor of the signature blob that get verified successfully + verifiedSigBlobDesc = sigBlobDesc + + return nil + } + return nil + }) + + if err != nil { + return Descriptor{}, nil, err + } + + // check whether verification was successful or not + if success { + // signature verification succeeds if there is at least one good + // signature + return notationDescriptorFromOCI(verifiedSigBlobDesc), verificationOutcomes, nil + } + + return Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} +} + +func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, error) { + annotations := make(map[string]string) + var thumbprints []string + certChain := signerInfo.CertificateChain + for _, cert := range certChain { + checkSum := sha256.Sum256(cert.Raw) + thumbprints = append(thumbprints, strings.ToLower(hex.EncodeToString(checkSum[:]))) + } + val, err := json.Marshal(thumbprints) + if err != nil { + return nil, err + } + annotations[AnnotationX509ChainThumbprint] = string(val) + + return annotations, nil +} + +func notationDescriptorFromOCI(desc ocispec.Descriptor) Descriptor { + return Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + Annotations: desc.Annotations, + } +} diff --git a/notation/notation_test.go b/notation/notation_test.go new file mode 100644 index 00000000..e1c782cd --- /dev/null +++ b/notation/notation_test.go @@ -0,0 +1,139 @@ +package notation + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/notaryproject/notation-go/internal/mock" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestRegistryResolveError(t *testing.T) { + policyDocument := dummyPolicyDocument() + repo := mock.NewRepository() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + + errorMessage := "network error" + expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} + + // mock the repository + repo.ResolveError = errors.New(errorMessage) + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} + _, _, err := Verify(context.Background(), &verifier, repo, opts) + + if err == nil || !errors.Is(err, expectedErr) { + t.Fatalf("RegistryResolve expected: %v got: %v", expectedErr, err) + } +} + +func TestSkippedSignatureVerification(t *testing.T) { + policyDocument := dummyPolicyDocument() + policyDocument.TrustPolicies[0].SignatureVerification.VerificationLevel = "skip" + repo := mock.NewRepository() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} + _, outcomes, err := Verify(context.Background(), &verifier, repo, opts) + + if err != nil || outcomes[0].VerificationLevel != trustpolicy.LevelSkip { + t.Fatalf("\"skip\" verification level must pass overall signature verification") + } +} + +func TestRegistryListSignaturesError(t *testing.T) { + policyDocument := dummyPolicyDocument() + repo := mock.NewRepository() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true} + errorMessage := "network error" + expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} + + // mock the repository + repo.ListSignatureManifestsError = ErrorSignatureRetrievalFailed{Msg: "network error"} + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} + _, _, err := Verify(context.Background(), &verifier, repo, opts) + + if err == nil || !errors.Is(err, expectedErr) { + t.Fatalf("RegistryListSignatureManifests expected: %v got: %v", expectedErr, err) + } +} + +func TestRegistryNoSignatureManifests(t *testing.T) { + policyDocument := dummyPolicyDocument() + repo := mock.NewRepository() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + errorMessage := fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", mock.SampleArtifactUri) + expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} + + // mock the repository + repo.ListSignatureManifestsResponse = []ocispec.Descriptor{} + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} + _, _, err := Verify(context.Background(), &verifier, repo, opts) + + if err == nil || !errors.Is(err, expectedErr) { + t.Fatalf("RegistryNoSignatureManifests expected: %v got: %v", expectedErr, err) + } +} + +func TestRegistryFetchSignatureBlobError(t *testing.T) { + policyDocument := dummyPolicyDocument() + repo := mock.NewRepository() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : network error", mock.SampleDigest, mock.SampleArtifactUri) + expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} + + // mock the repository + repo.GetError = errors.New("network error") + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} + _, _, err := Verify(context.Background(), &verifier, repo, opts) + + if err == nil || !errors.Is(err, expectedErr) { + t.Fatalf("RegistryGetBlob expected: %v got: %v", expectedErr, err) + } +} + +func dummyPolicyDocument() (policyDoc trustpolicy.Document) { + policyDoc = trustpolicy.Document{ + Version: "1.0", + TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()}, + } + return +} + +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 +} + +type dummyVerifier struct { + TrustPolicyDoc *trustpolicy.Document + PluginManager pluginManager + FailVerify bool +} + +func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts VerifyOptions, outcome *VerificationOutcome) (Descriptor, error) { + if v.FailVerify { + return Descriptor{}, errors.New("failed verify") + } + return Descriptor{}, nil +} + +func (v *dummyVerifier) TrustPolicyDocument() (*trustpolicy.Document, error) { + return v.TrustPolicyDoc, nil +} + +// pluginManager is for mocking in unit tests +type pluginManager interface { + Get(ctx context.Context, name string) (*manager.Plugin, error) + Runner(name string) (plugin.Runner, error) +} diff --git a/registry/repository.go b/registry/repository.go index 37f38e14..7406e2eb 100644 --- a/registry/repository.go +++ b/registry/repository.go @@ -1,198 +1,32 @@ +// Package registry provides Repository for remote signing and verification package registry import ( - "bytes" "context" - "encoding/json" - "fmt" - "github.com/notaryproject/notation-go" - "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" - "oras.land/oras-go/v2" - "oras.land/oras-go/v2/content" - "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" ) -const ( - maxBlobSizeLimit = 32 * 1024 * 1024 // 32 MiB - maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB -) - -type RepositoryClient struct { - remote.Repository -} - -type SignatureManifest struct { - Blob notation.Descriptor - Annotations map[string]string -} - -// NewRepositoryClient creates a new registry client. -func NewRepositoryClient(client remote.Client, ref registry.Reference, plainHTTP bool) *RepositoryClient { - return &RepositoryClient{ - Repository: remote.Repository{ - Client: client, - Reference: ref, - PlainHTTP: plainHTTP, - }, - } -} - -// Resolve resolves a reference(tag or digest) to a manifest descriptor -func (c *RepositoryClient) Resolve(ctx context.Context, reference string) (notation.Descriptor, error) { - desc, err := c.Repository.Resolve(ctx, reference) - if err != nil { - return notation.Descriptor{}, err - } - return notationDescriptorFromOCI(desc), nil -} - -// ListSignatureManifests returns all signature manifests given the manifest digest -func (c *RepositoryClient) ListSignatureManifests(ctx context.Context, manifestDigest digest.Digest) ([]SignatureManifest, error) { - var signatureManifests []SignatureManifest - if err := c.Repository.Referrers(ctx, ocispec.Descriptor{ - Digest: manifestDigest, - }, ArtifactTypeNotation, func(referrers []artifactspec.Descriptor) error { - for _, desc := range referrers { - if desc.MediaType != artifactspec.MediaTypeArtifactManifest { - continue - } - artifact, err := c.getArtifactManifest(ctx, desc.Digest) - if err != nil { - return fmt.Errorf("failed to fetch manifest: %v: %v", desc.Digest, err) - } - if len(artifact.Blobs) == 0 { - continue - } - signatureManifests = append(signatureManifests, SignatureManifest{ - Blob: notationDescriptorFromArtifact(artifact.Blobs[0]), - Annotations: artifact.Annotations, - }) - } - return nil - }); err != nil { - return nil, err - } - return signatureManifests, nil -} - -// GetBlob downloads the content of the specified digest's Blob -func (c *RepositoryClient) GetBlob(ctx context.Context, digest digest.Digest) ([]byte, error) { - desc, err := c.Repository.Blobs().Resolve(ctx, digest.String()) - if err != nil { - return nil, err - } - if desc.Size > maxBlobSizeLimit { - return nil, fmt.Errorf("signature blob too large: %d", desc.Size) - } - return content.FetchAll(ctx, c.Repository.Blobs(), desc) -} - -// PutSignatureManifest creates and uploads an signature artifact linking the manifest and the signature -func (c *RepositoryClient) PutSignatureManifest(ctx context.Context, signature []byte, signatureMediaType string, subjectManifest notation.Descriptor, annotations map[string]string) (notation.Descriptor, SignatureManifest, error) { - signatureDesc, err := c.uploadSignature(ctx, signature, signatureMediaType) - if err != nil { - return notation.Descriptor{}, SignatureManifest{}, err - } - - manifestDesc, err := c.uploadSignatureManifest(ctx, artifactDescriptorFromNotation(subjectManifest), signatureDesc, annotations) - if err != nil { - return notation.Descriptor{}, SignatureManifest{}, err - } - - signatureManifest := SignatureManifest{ - Blob: notationDescriptorFromArtifact(signatureDesc), - Annotations: annotations, - } - return notationDescriptorFromOCI(manifestDesc), signatureManifest, nil -} - -func (c *RepositoryClient) getArtifactManifest(ctx context.Context, manifestDigest digest.Digest) (artifactspec.Manifest, error) { - repo := c.Repository - repo.ManifestMediaTypes = []string{ - artifactspec.MediaTypeArtifactManifest, - } - store := repo.Manifests() - desc, err := store.Resolve(ctx, manifestDigest.String()) - if err != nil { - return artifactspec.Manifest{}, err - } - if desc.Size > maxManifestSizeLimit { - return artifactspec.Manifest{}, fmt.Errorf("manifest too large: %d", desc.Size) - } - manifestJSON, err := content.FetchAll(ctx, store, desc) - if err != nil { - return artifactspec.Manifest{}, err - } - - var manifest artifactspec.Manifest - err = json.Unmarshal(manifestJSON, &manifest) - if err != nil { - return artifactspec.Manifest{}, err - } - return manifest, nil -} - -// uploadSignature uploads the signature to the registry -func (c *RepositoryClient) uploadSignature(ctx context.Context, signature []byte, signatureMediaType string) (artifactspec.Descriptor, error) { - desc := ocispec.Descriptor{ - MediaType: signatureMediaType, - Digest: digest.FromBytes(signature), - Size: int64(len(signature)), - } - if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(signature)); err != nil { - return artifactspec.Descriptor{}, err - } - return artifactDescriptorFromOCI(desc), nil -} - -// uploadSignatureManifest uploads the signature manifest to the registry -func (c *RepositoryClient) uploadSignatureManifest(ctx context.Context, subjectManifest, signatureDesc artifactspec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { - opts := oras.PackArtifactOptions{ - Subject: &subjectManifest, - ManifestAnnotations: annotations, - } - - return oras.PackArtifact( - ctx, - c.Repository.Manifests(), - ArtifactTypeNotation, - []artifactspec.Descriptor{signatureDesc}, - opts, - ) -} - -func artifactDescriptorFromNotation(desc notation.Descriptor) artifactspec.Descriptor { - return artifactspec.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, - } -} - -func notationDescriptorFromArtifact(desc artifactspec.Descriptor) notation.Descriptor { - return notation.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, - } -} - -func artifactDescriptorFromOCI(desc ocispec.Descriptor) artifactspec.Descriptor { - return artifactspec.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, - } -} - -func notationDescriptorFromOCI(desc ocispec.Descriptor) notation.Descriptor { - return notation.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, +// Repository provides registry functionalities for remote signing and +// verification. +type Repository interface { + // Resolve resolves a reference(tag or digest) to a manifest descriptor + Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) + // ListSignatures returns signature manifests filtered by fn given the + // artifact manifest descriptor + ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error + // FetchSignatureBlob returns signature envelope blob and descriptor given + // signature manifest descriptor + FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) + // PushSignature creates and uploads an signature manifest along with its + // linked signature envelope blob. + PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) +} + +// NewRepository returns a new Repository +func NewRepository(repo remote.Repository) Repository { + return &repositoryClient{ + Repository: repo, } } diff --git a/registry/repositoryClient.go b/registry/repositoryClient.go new file mode 100644 index 00000000..806cd68d --- /dev/null +++ b/registry/repositoryClient.go @@ -0,0 +1,163 @@ +package registry + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/registry/remote" +) + +const ( + maxBlobSizeLimit = 32 * 1024 * 1024 // 32 MiB + maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB +) + +// repositoryClient implements Repository +type repositoryClient struct { + remote.Repository +} + +// Resolve resolves a reference(tag or digest) to a manifest descriptor +func (c *repositoryClient) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + return c.Repository.Resolve(ctx, reference) +} + +// ListSignatures returns signature manifests filtered by fn given the +// artifact manifest descriptor +func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error { + if err := c.Repository.Referrers(ctx, ocispec.Descriptor{ + Digest: desc.Digest, + }, ArtifactTypeNotation, func(referrers []artifactspec.Descriptor) error { + var sigManifestDesc []ocispec.Descriptor + for _, referrer := range referrers { + sigManifestDesc = append(sigManifestDesc, ocispecDescriptorFromArtifact(referrer)) + } + return fn(sigManifestDesc) + }); err != nil { + return err + } + return nil +} + +// FetchSignatureBlob returns signature envelope blob and descriptor given +// signature manifest descriptor +func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) { + sigManifest, err := c.getSignatureManifest(ctx, desc) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + if len(sigManifest.Blobs) == 0 { + return nil, ocispec.Descriptor{}, errors.New("signature manifest missing signature envelope blob") + } + sigDesc, err := c.Repository.Blobs().Resolve(ctx, sigManifest.Blobs[0].Digest.String()) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + if sigDesc.Size > maxBlobSizeLimit { + return nil, ocispec.Descriptor{}, fmt.Errorf("signature blob too large: %d", sigDesc.Size) + } + sigBlob, err := content.FetchAll(ctx, c.Repository.Blobs(), sigDesc) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + return sigBlob, sigDesc, nil +} + +// PushSignature creates and uploads an signature manifest along with its +// linked signature envelope blob. Upon successful, PushSignature returns +// signature envelope blob and manifest descriptors. +func (c *repositoryClient) PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { + blobDesc, err = c.uploadSignature(ctx, blob, mediaType) + if err != nil { + return ocispec.Descriptor{}, ocispec.Descriptor{}, err + } + + manifestDesc, err = c.uploadSignatureManifest(ctx, artifactspecDescriptorFromOCI(subject), artifactspecDescriptorFromOCI(blobDesc), annotations) + if err != nil { + return ocispec.Descriptor{}, ocispec.Descriptor{}, err + } + + return blobDesc, manifestDesc, nil +} + +// getSignatureManifest returns signature manifest given signature manifest +// descriptor +func (c *repositoryClient) getSignatureManifest(ctx context.Context, sigManifestDesc ocispec.Descriptor) (artifactspec.Manifest, error) { + + repo := c.Repository + repo.ManifestMediaTypes = []string{ + artifactspec.MediaTypeArtifactManifest, + } + store := repo.Manifests() + ociDesc, err := store.Resolve(ctx, sigManifestDesc.Digest.String()) + if err != nil { + return artifactspec.Manifest{}, err + } + if ociDesc.Size > maxManifestSizeLimit { + return artifactspec.Manifest{}, fmt.Errorf("manifest too large: %d", ociDesc.Size) + } + manifestJSON, err := content.FetchAll(ctx, store, ociDesc) + if err != nil { + return artifactspec.Manifest{}, err + } + + var sigManifest artifactspec.Manifest + err = json.Unmarshal(manifestJSON, &sigManifest) + if err != nil { + return artifactspec.Manifest{}, err + } + return sigManifest, nil +} + +// uploadSignature uploads the signature envelope blob to the registry +func (c *repositoryClient) uploadSignature(ctx context.Context, blob []byte, mediaType string) (ocispec.Descriptor, error) { + desc := ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(blob)); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil +} + +// uploadSignatureManifest uploads the signature manifest to the registry +func (c *repositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc artifactspec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { + opts := oras.PackArtifactOptions{ + Subject: &subject, + ManifestAnnotations: annotations, + } + + manifestDesc, err := oras.PackArtifact(ctx, c.Repository.Manifests(), ArtifactTypeNotation, []artifactspec.Descriptor{blobDesc}, opts) + if err != nil { + return ocispec.Descriptor{}, err + } + return manifestDesc, nil +} + +func ocispecDescriptorFromArtifact(desc artifactspec.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + Annotations: desc.Annotations, + } +} + +func artifactspecDescriptorFromOCI(desc ocispec.Descriptor) artifactspec.Descriptor { + return artifactspec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + Annotations: desc.Annotations, + } +} diff --git a/registry/repositoryClient_test.go b/registry/repositoryClient_test.go new file mode 100644 index 00000000..993619a2 --- /dev/null +++ b/registry/repositoryClient_test.go @@ -0,0 +1,499 @@ +package registry + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" +) + +const ( + validDigest = "6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b" + validDigest2 = "9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" + validDigest3 = "1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2" + validDigest4 = "277000f8d32d2b2a7d65f4533339f7d4c064e0540facf1d54c69d9916f05d28c" + validDigest5 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + validDigest6 = "daffbe5f71beaf7b05c080e8ae4f9739cdf21e24c89561e35792f1251d38148d" + validDigest7 = "13b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + validDigest8 = "57f2c47061dae97063dc46598168a80a9f89302c1f24fe2a422a1ec0aba3017a" + validDigest9 = "023c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b" + validDigest10 = "1761e09cad8aa44e48ffb41c78371a6c139bd0df555c90b5d99739b9551c7828" + invalidDigest = "invaliddigest" + algo = "sha256" + validDigestWithAlgo = algo + ":" + validDigest + validDigestWithAlgo2 = algo + ":" + validDigest2 + validDigestWithAlgo3 = algo + ":" + validDigest3 + validDigestWithAlgo4 = algo + ":" + validDigest4 + validDigestWithAlgo5 = algo + ":" + validDigest5 + validDigestWithAlgo6 = algo + ":" + validDigest6 + validDigestWithAlgo7 = algo + ":" + validDigest7 + validDigestWithAlgo8 = algo + ":" + validDigest8 + validDigestWithAlgo9 = algo + ":" + validDigest9 + validDigestWithAlgo10 = algo + ":" + validDigest10 + validHost = "localhost" + validRegistry = validHost + ":5000" + invalidHost = "badhost" + invalidRegistry = invalidHost + ":5000" + validRepo = "test" + msg = "message" + errMsg = "error message" + mediaType = "application/json" + validReference = validRegistry + "/" + validRepo + "@" + validDigest + referenceWithInvalidHost = invalidRegistry + "/" + validRepo + "@" + validDigest + validReference6 = validRegistry + "/" + validRepo + "@" + validDigest6 + invalidReference = "invalid reference" + joseTag = "application/jose+json" + coseTag = "application/cose" + validTimestamp = "2022-07-29T02:23:10Z" + size = 104 + size2 = 135 + validPage = ` + { + "references": [{}], + "referrers": [ + { + "artifactType": "application/vnd.cncf.notary.v2.signature", + "mediaType": "application/vnd.cncf.oras.artifact.manifest.v1+json", + "digest": "localhost:5000/test@57f2c47061dae97063dc46598168a80a9f89302c1f24fe2a422a1ec0aba3017a" + } + ] + }` + pageWithWrongMediaType = ` + { + "references": [{}], + "referrers": [ + { + "artifactType": "application/vnd.cncf.notary.v2.signature", + "mediaType": "application/vnd.cncf.oras.artifact.manifest.invalid", + "digest": "localhost:5000/test@1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2" + } + ] + }` + pageWithBadDigest = ` + { + "references": [{}], + "referrers": [ + { + "artifactType": "application/vnd.cncf.notary.v2.signature", + "mediaType": "application/vnd.cncf.oras.artifact.manifest.v1+json", + "digest": "localhost:5000/test@9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" + } + ] + }` + validBlob = `{ + "digest": "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b", + "size": 90 + }` + validManifest = `{ + "blobs": [ + { + "digest": "sha256:023c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b", + "size": 90 + } + ] + }` +) + +type args struct { + ctx context.Context + reference string + remoteClient remote.Client + plainHttp bool + digest digest.Digest + annotations map[string]string + subjectManifest ocispec.Descriptor + signature []byte + signatureMediaType string + signatureManifestDesc ocispec.Descriptor + artifactManifestDesc ocispec.Descriptor +} + +type mockRemoteClient struct { +} + +func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/v2/test/manifests/" + validDigest: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(msg))), + Header: map[string][]string{ + "Content-Type": {mediaType}, + "Docker-Content-Digest": {validDigestWithAlgo}, + }, + }, nil + case "/v2/test/blobs/" + validDigestWithAlgo6: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validBlob))), + ContentLength: size, + Header: map[string][]string{ + "Content-Type": {mediaType}, + "Docker-Content-Digest": {validDigestWithAlgo6}, + }, + }, nil + case "/v2/test/blobs/" + validDigestWithAlgo3: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validBlob))), + ContentLength: maxBlobSizeLimit + 1, + Header: map[string][]string{ + "Content-Type": {mediaType}, + "Docker-Content-Digest": {validDigestWithAlgo3}, + }, + }, nil + case "/v2/test/manifests/" + invalidDigest: + return &http.Response{}, fmt.Errorf(errMsg) + case "/v2/test/_oras/artifacts/referrers": + if strings.HasSuffix(req.URL.RawQuery, invalidDigest) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(pageWithBadDigest))), + Header: map[string][]string{ + "Oras-Api-Version": {"oras/1.1"}, + }, + Request: &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/v2/test/_oras/artifacts/referrers"}, + }, + }, nil + } else if strings.HasSuffix(req.URL.RawQuery, validDigest7) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(pageWithWrongMediaType))), + Header: map[string][]string{ + "Oras-Api-Version": {"oras/1.1"}, + }, + Request: &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/v2/test/_oras/artifacts/referrers"}, + }, + }, nil + } else if strings.HasSuffix(req.URL.RawQuery, validDigest8) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validPage))), + Header: map[string][]string{ + "Oras-Api-Version": {"oras/1.1"}, + }, + Request: &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/v2/test/_oras/artifacts/referrers"}, + }, + }, nil + } + return &http.Response{}, fmt.Errorf(msg) + case "/v2/test/manifests/" + validDigest2: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validDigest2))), + ContentLength: size, + Header: map[string][]string{ + "Content-Type": {mediaType}, + "Docker-Content-Digest": {validDigestWithAlgo4}, + }, + }, nil + case "v2/test/manifest/" + validDigest3: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validDigest3))), + Header: map[string][]string{ + "Content-Type": {mediaType}, + "Docker-Content-Digest": {validDigestWithAlgo3}, + }, + }, nil + case "/v2/test/manifests/" + validDigest8: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validDigest8))), + ContentLength: size2, + Header: map[string][]string{ + "Content-Type": {mediaType}, + "Docker-Content-Digest": {validDigestWithAlgo8}, + }, + }, nil + case "/v2/test/manifests/" + validDigestWithAlgo4: + if req.Method == "GET" { + return &http.Response{}, fmt.Errorf(msg) + } + return &http.Response{ + StatusCode: http.StatusCreated, + Body: io.NopCloser(bytes.NewReader([]byte(msg))), + Header: map[string][]string{ + "Docker-Content-Digest": {validDigestWithAlgo4}, + }, + }, nil + case "/v2/test/manifests/" + validDigestWithAlgo7: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(msg))), + Header: map[string][]string{ + "Docker-Content-Digest": {validDigestWithAlgo4}, + }, + }, nil + case "/v2/test/manifests/" + validDigestWithAlgo8: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validManifest))), + ContentLength: size2, + Header: map[string][]string{ + "Docker-Content-Digest": {validDigestWithAlgo8}, + "Content-Type": {mediaType}, + }, + }, nil + case "/v2/test/manifests/" + validDigestWithAlgo2: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(validBlob))), + Header: map[string][]string{ + "Docker-Content-Digest": {validDigestWithAlgo2}, + "Content-Type": {mediaType}, + }, + }, nil + case "/v2/test/manifests/" + validDigestWithAlgo10: + if req.Method == "GET" { + return &http.Response{}, fmt.Errorf(msg) + } + return &http.Response{ + StatusCode: http.StatusCreated, + Body: io.NopCloser(bytes.NewReader([]byte(msg))), + Header: map[string][]string{ + "Docker-Content-Digest": {validDigestWithAlgo10}, + }, + }, nil + case "/v2/test/blobs/uploads/": + switch req.Host { + case validRegistry: + return &http.Response{ + StatusCode: http.StatusAccepted, + Body: io.NopCloser(bytes.NewReader([]byte(msg))), + Request: &http.Request{ + Header: map[string][]string{}, + }, + Header: map[string][]string{ + "Location": {"test"}, + }, + }, nil + default: + return &http.Response{}, fmt.Errorf(msg) + } + case validRepo: + return &http.Response{ + StatusCode: http.StatusCreated, + Body: io.NopCloser(bytes.NewReader([]byte(msg))), + }, nil + default: + return &http.Response{}, fmt.Errorf(errMsg) + } +} + +func TestResolve(t *testing.T) { + tests := []struct { + name string + args args + expect ocispec.Descriptor + expectErr bool + }{ + { + name: "failed to resolve", + args: args{ + ctx: context.Background(), + reference: invalidReference, + remoteClient: mockRemoteClient{}, + plainHttp: false, + }, + expect: ocispec.Descriptor{}, + expectErr: true, + }, + { + name: "succeed to resolve", + args: args{ + ctx: context.Background(), + reference: validReference, + remoteClient: mockRemoteClient{}, + plainHttp: false, + }, + expect: ocispec.Descriptor{ + MediaType: mediaType, + Digest: validDigestWithAlgo, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := tt.args + ref, _ := registry.ParseReference(args.reference) + client := newRepositoryClient(args.remoteClient, ref, args.plainHttp) + res, err := client.Resolve(args.ctx, args.reference) + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(res, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, res) + } + }) + } +} + +func TestFetchSignatureBlob(t *testing.T) { + tests := []struct { + name string + args args + expect []byte + expectErr bool + }{ + { + name: "failed to resolve", + expect: nil, + expectErr: true, + args: args{ + ctx: context.Background(), + reference: validReference, + remoteClient: mockRemoteClient{}, + plainHttp: false, + signatureManifestDesc: ocispec.Descriptor{ + MediaType: artifactspec.MediaTypeArtifactManifest, + Digest: digest.Digest(invalidDigest), + }, + }, + }, + { + name: "exceed max blob size", + expect: nil, + expectErr: true, + args: args{ + ctx: context.Background(), + reference: validReference, + remoteClient: mockRemoteClient{}, + plainHttp: false, + signatureManifestDesc: ocispec.Descriptor{ + MediaType: artifactspec.MediaTypeArtifactManifest, + Digest: digest.Digest(validDigestWithAlgo3), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := tt.args + ref, _ := registry.ParseReference(args.reference) + client := newRepositoryClient(args.remoteClient, ref, args.plainHttp) + res, _, err := client.FetchSignatureBlob(args.ctx, args.signatureManifestDesc) + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(res, tt.expect) { + t.Errorf("expect %+v, got %+v", tt.expect, res) + } + }) + } +} + +func TestListSignatures(t *testing.T) { + tests := []struct { + name string + args args + expect []interface{} + expectErr bool + }{ + { + name: "failed to fetch content", + expectErr: true, + expect: nil, + args: args{ + ctx: context.Background(), + reference: validReference, + remoteClient: mockRemoteClient{}, + plainHttp: false, + digest: digest.Digest(invalidDigest), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := tt.args + ref, _ := registry.ParseReference(args.reference) + client := newRepositoryClient(args.remoteClient, ref, args.plainHttp) + + err := client.ListSignatures(args.ctx, args.artifactManifestDesc, nil) + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + }) + } +} + +func TestPushSignature(t *testing.T) { + tests := []struct { + name string + args args + expectDes ocispec.Descriptor + expectManifest ocispec.Descriptor + expectErr bool + }{ + { + name: "failed to upload signature", + expectErr: true, + expectDes: ocispec.Descriptor{}, + expectManifest: ocispec.Descriptor{}, + args: args{ + reference: referenceWithInvalidHost, + signature: make([]byte, 0), + ctx: context.Background(), + remoteClient: mockRemoteClient{}, + }, + }, + { + name: "failed to upload signature manifest", + expectErr: true, + expectDes: ocispec.Descriptor{}, + expectManifest: ocispec.Descriptor{}, + args: args{ + reference: validReference, + signature: make([]byte, 0), + ctx: context.Background(), + remoteClient: mockRemoteClient{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := tt.args + ref, _ := registry.ParseReference(args.reference) + client := newRepositoryClient(args.remoteClient, ref, args.plainHttp) + + des, _, err := client.PushSignature(args.ctx, args.signature, args.signatureMediaType, args.subjectManifest, args.annotations) + if (err != nil) != tt.expectErr { + t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) + } + if !reflect.DeepEqual(des, tt.expectDes) { + t.Errorf("expect descriptor: %+v, got %+v", tt.expectDes, des) + } + }) + } +} + +// newRepositoryClient creates a new repository client. +func newRepositoryClient(client remote.Client, ref registry.Reference, plainHTTP bool) *repositoryClient { + return &repositoryClient{ + Repository: remote.Repository{ + Client: client, + Reference: ref, + PlainHTTP: plainHTTP, + }, + } +} diff --git a/signature/envelope.go b/signature/envelope.go index 88fa9914..ba3e9dfe 100644 --- a/signature/envelope.go +++ b/signature/envelope.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/notaryproject/notation-core-go/signature" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" ) // ValidateEnvelopeMediaType validetes envelope media type is supported by notation-core-go. diff --git a/signature/envelope_test.go b/signature/envelope_test.go index 2bf672b1..488f4ab2 100644 --- a/signature/envelope_test.go +++ b/signature/envelope_test.go @@ -7,7 +7,7 @@ import ( "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-go" + notation "github.com/notaryproject/notation-go/internal" gcose "github.com/veraison/go-cose" ) diff --git a/signature/plugin.go b/signature/plugin.go index fd6ba335..da8d9dba 100644 --- a/signature/plugin.go +++ b/signature/plugin.go @@ -9,7 +9,7 @@ import ( "time" "github.com/notaryproject/notation-core-go/signature" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" "github.com/notaryproject/notation-go/plugin" ) diff --git a/signature/plugin_test.go b/signature/plugin_test.go index 51182981..7bc9ef86 100644 --- a/signature/plugin_test.go +++ b/signature/plugin_test.go @@ -15,7 +15,7 @@ import ( "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-go" + notation "github.com/notaryproject/notation-go/internal" "github.com/notaryproject/notation-go/plugin" gcose "github.com/veraison/go-cose" ) diff --git a/signature/signer.go b/signature/signer.go index 9fa2ce7e..40537c2d 100644 --- a/signature/signer.go +++ b/signature/signer.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" ) // NewSignerFromFiles creates a signer from key, certificate files diff --git a/signature/signer_test.go b/signature/signer_test.go index f1e092cb..28c0aa35 100644 --- a/signature/signer_test.go +++ b/signature/signer_test.go @@ -19,7 +19,7 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" "github.com/notaryproject/notation-core-go/timestamp/timestamptest" - "github.com/notaryproject/notation-go" + notation "github.com/notaryproject/notation-go/internal" "github.com/notaryproject/notation-go/plugin" "github.com/opencontainers/go-digest" ) diff --git a/verification/verifier.go b/verification/verifier.go index 860b4b98..1939bf25 100644 --- a/verification/verifier.go +++ b/verification/verifier.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" - "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/dir" + notation "github.com/notaryproject/notation-go/internal" + "github.com/notaryproject/notation-go/internal/registry" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" - "github.com/notaryproject/notation-go/registry" ) type Verifier struct { diff --git a/verification/verifier_helpers.go b/verification/verifier_helpers.go index 90b8e2fd..67557c1d 100644 --- a/verification/verifier_helpers.go +++ b/verification/verifier_helpers.go @@ -10,8 +10,8 @@ import ( "time" "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/internal/registry" "github.com/notaryproject/notation-go/plugin" - "github.com/notaryproject/notation-go/registry" sig "github.com/notaryproject/notation-go/signature" ) diff --git a/verification/verifier_test.go b/verification/verifier_test.go index b2b31879..145cda71 100644 --- a/verification/verifier_test.go +++ b/verification/verifier_test.go @@ -8,12 +8,12 @@ import ( "testing" "github.com/notaryproject/notation-core-go/signature" - "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/dir" + notation "github.com/notaryproject/notation-go/internal" "github.com/notaryproject/notation-go/internal/mock" + "github.com/notaryproject/notation-go/internal/registry" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" - "github.com/notaryproject/notation-go/registry" _ "github.com/notaryproject/notation-core-go/signature/cose" _ "github.com/notaryproject/notation-core-go/signature/jws" From 1862c7eb43292cf06912349f82bfb62eb2a32969 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 8 Nov 2022 14:17:51 +0800 Subject: [PATCH 02/28] notation refactoring Signed-off-by: Patrick Zheng --- internal/common/common.go | 97 ++++ internal/mock/mocks.go | 52 +- internal/mock_origin/mockfs/fs.go | 25 + internal/mock_origin/mocks.go | 134 ++++++ .../testdata/ca_expired_sig_env.json | 12 + .../testdata/ca_invalid_sig_env.json | 12 + .../testdata/ca_plugin_sig_env.json | 12 + .../testdata/ca_valid_sig_env.json | 12 + .../testdata/sa_expired_sig_env.json | 12 + .../testdata/sa_invalid_sig_env.json | 12 + .../testdata/sa_plugin_sig_env.json | 12 + .../testdata/sa_valid_sig_env.json | 12 + internal/policy/policy.go | 64 +++ internal/policy/policy_test.go | 71 +++ notation/notation.go | 2 +- notation/notation_test.go | 8 +- verification/trustpolicy/trustpolicy.go | 378 +++++++++++++++ verification/trustpolicy/trustpolicy_test.go | 446 ++++++++++++++++++ verification/truststore/truststore.go | 126 +++++ verification/truststore/truststore_test.go | 58 +++ verification/verifier_test.go | 2 +- 21 files changed, 1530 insertions(+), 29 deletions(-) create mode 100644 internal/common/common.go create mode 100644 internal/mock_origin/mockfs/fs.go create mode 100644 internal/mock_origin/mocks.go create mode 100644 internal/mock_origin/testdata/ca_expired_sig_env.json create mode 100644 internal/mock_origin/testdata/ca_invalid_sig_env.json create mode 100644 internal/mock_origin/testdata/ca_plugin_sig_env.json create mode 100644 internal/mock_origin/testdata/ca_valid_sig_env.json create mode 100644 internal/mock_origin/testdata/sa_expired_sig_env.json create mode 100644 internal/mock_origin/testdata/sa_invalid_sig_env.json create mode 100644 internal/mock_origin/testdata/sa_plugin_sig_env.json create mode 100644 internal/mock_origin/testdata/sa_valid_sig_env.json create mode 100644 internal/policy/policy.go create mode 100644 internal/policy/policy_test.go create mode 100644 verification/trustpolicy/trustpolicy.go create mode 100644 verification/trustpolicy/trustpolicy_test.go create mode 100644 verification/truststore/truststore.go create mode 100644 verification/truststore/truststore_test.go diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 00000000..92a09f04 --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,97 @@ +package common + +import ( + "fmt" + "regexp" + "strings" + + ldapv3 "github.com/go-ldap/ldap/v3" +) + +const ( + Wildcard = "*" + X509Subject = "x509.subject" +) + +// isPresent is a utility function to check if a string exists in an array +func IsPresent(val string, values []string) bool { + for _, v := range values { + if v == val { + return true + } + } + return false +} + +// Internal type to hold raw and parsed Distinguished Names +type ParsedDN struct { + RawString string + ParsedMap map[string]string +} + +// ParseDistinguishedName parses a DN name and validates Notary V2 rules +func ParseDistinguishedName(name string) (map[string]string, error) { + mandatoryFields := []string{"C", "ST", "O"} + attrKeyValue := make(map[string]string) + dn, err := ldapv3.ParseDN(name) + + if err != nil { + return nil, fmt.Errorf("distinguished name (DN) %q is not valid, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name) + } + + for _, rdn := range dn.RDNs { + + // multi-valued RDNs are not supported (TODO: add spec reference here) + if len(rdn.Attributes) > 1 { + return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name) + } + for _, attribute := range rdn.Attributes { + if attrKeyValue[attribute.Type] == "" { + attrKeyValue[attribute.Type] = attribute.Value + } else { + return nil, fmt.Errorf("distinguished name (DN) %q has duplicate RDN attribute for %q, DN can only have unique RDN attributes", name, attribute.Type) + } + } + } + + // Verify mandatory fields are present + for _, field := range mandatoryFields { + if attrKeyValue[field] == "" { + return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field) + } + } + // No errors + return attrKeyValue, nil +} + +// IsSubsetDN returns true if dn1 is a subset of dn2 i.e. every key/value pair of dn1 has a matching key/value pair in dn2, otherwise returns false +func IsSubsetDN(dn1 map[string]string, dn2 map[string]string) bool { + for key := range dn1 { + if dn1[key] != dn2[key] { + return false + } + } + return true +} + +// 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]+)+)?)+)?$`) + errorMessage := "registry scope %q is not valid, make sure it is the fully qualified registry URL without the scheme/protocol. e.g domain.com/my/repository" + firstSlash := strings.Index(scope, "/") + if firstSlash < 0 { + return fmt.Errorf(errorMessage, scope) + } + domain := scope[:firstSlash] + repository := scope[firstSlash+1:] + + if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) { + return fmt.Errorf(errorMessage, scope) + } + + // No errors + return nil +} diff --git a/internal/mock/mocks.go b/internal/mock/mocks.go index 7982b916..a94ab09a 100644 --- a/internal/mock/mocks.go +++ b/internal/mock/mocks.go @@ -5,11 +5,10 @@ import ( _ "embed" "github.com/notaryproject/notation-core-go/signature" - notation "github.com/notaryproject/notation-go/internal" - "github.com/notaryproject/notation-go/internal/registry" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) //go:embed testdata/ca_valid_sig_env.json @@ -40,13 +39,19 @@ var ( SampleArtifactUri = "registry.acme-rockets.io/software/net-monitor@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e" SampleDigest = digest.Digest("sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e") Annotations = map[string]string{"key": "value"} - ImageDescriptor = notation.Descriptor{ + ImageDescriptor = ocispec.Descriptor{ MediaType: "application/vnd.docker.distribution.manifest.v2+json", Digest: SampleDigest, Size: 528, Annotations: nil, } - JwsSigEnvDescriptor = notation.Descriptor{ + SigManfiestDescriptor = ocispec.Descriptor{ + MediaType: "application/vnd.cncf.oras.artifact.manifest.v1+json", + Digest: SampleDigest, + Size: 300, + Annotations: Annotations, + } + JwsSigEnvDescriptor = ocispec.Descriptor{ MediaType: "application/jose+json", Digest: SampleDigest, Size: 100, @@ -60,39 +65,40 @@ var ( ) type Repository struct { - ResolveResponse notation.Descriptor - ResolveError error - ListSignatureManifestsResponse []registry.SignatureManifest - ListSignatureManifestsError error - GetResponse []byte - GetError error + ResolveResponse ocispec.Descriptor + ResolveError error + ListSignaturesResponse []ocispec.Descriptor + ListSignaturesError error + FetchSignatureBlobResponse []byte + FetchSignatureBlobError error } func NewRepository() Repository { return Repository{ - ResolveResponse: ImageDescriptor, - ListSignatureManifestsResponse: []registry.SignatureManifest{{ - Blob: JwsSigEnvDescriptor, - Annotations: Annotations, - }}, - GetResponse: MockCaValidSigEnv, + ResolveResponse: ImageDescriptor, + ListSignaturesResponse: []ocispec.Descriptor{SigManfiestDescriptor}, + FetchSignatureBlobResponse: MockCaValidSigEnv, } } -func (t Repository) Resolve(ctx context.Context, reference string) (notation.Descriptor, error) { +func (t Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { return t.ResolveResponse, t.ResolveError } -func (t Repository) ListSignatureManifests(ctx context.Context, manifestDigest digest.Digest) ([]registry.SignatureManifest, error) { - return t.ListSignatureManifestsResponse, t.ListSignatureManifestsError +func (t Repository) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error { + err := fn(t.ListSignaturesResponse) + if err != nil { + return err + } + return t.ListSignaturesError } -func (t Repository) GetBlob(ctx context.Context, digest digest.Digest) ([]byte, error) { - return t.GetResponse, t.GetError +func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) { + return t.FetchSignatureBlobResponse, JwsSigEnvDescriptor, t.FetchSignatureBlobError } -func (t Repository) PutSignatureManifest(ctx context.Context, signature []byte, signatureMediaType string, manifest notation.Descriptor, annotaions map[string]string) (notation.Descriptor, registry.SignatureManifest, error) { - return notation.Descriptor{}, registry.SignatureManifest{}, nil +func (t Repository) PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { + return ocispec.Descriptor{}, ocispec.Descriptor{}, nil } type PluginManager struct { diff --git a/internal/mock_origin/mockfs/fs.go b/internal/mock_origin/mockfs/fs.go new file mode 100644 index 00000000..7757c9bb --- /dev/null +++ b/internal/mock_origin/mockfs/fs.go @@ -0,0 +1,25 @@ +package mockfs + +import ( + "io/fs" + "path/filepath" + + "github.com/notaryproject/notation-go/dir" +) + +type sysFSMock struct { + fs.FS + root string +} + +func (s sysFSMock) SysPath(items ...string) (string, error) { + pathItems := []string{s.root} + pathItems = append(pathItems, items...) + return filepath.Join(pathItems...), nil +} + +func NewSysFSMock(fsys fs.FS, root string) dir.SysFS { + return sysFSMock{ + FS: fsys, + root: root} +} diff --git a/internal/mock_origin/mocks.go b/internal/mock_origin/mocks.go new file mode 100644 index 00000000..7982b916 --- /dev/null +++ b/internal/mock_origin/mocks.go @@ -0,0 +1,134 @@ +package mock + +import ( + "context" + _ "embed" + + "github.com/notaryproject/notation-core-go/signature" + notation "github.com/notaryproject/notation-go/internal" + "github.com/notaryproject/notation-go/internal/registry" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/plugin/manager" + "github.com/opencontainers/go-digest" +) + +//go:embed testdata/ca_valid_sig_env.json +var MockCaValidSigEnv []byte + +//go:embed testdata/ca_invalid_sig_env.json +var MockCaInvalidSigEnv []byte + +//go:embed testdata/sa_valid_sig_env.json +var MockSaValidSigEnv []byte + +//go:embed testdata/ca_plugin_sig_env.json +var MockCaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name" + +//go:embed testdata/sa_invalid_sig_env.json +var MockSaInvalidSigEnv []byte + +//go:embed testdata/ca_expired_sig_env.json +var MockCaExpiredSigEnv []byte + +//go:embed testdata/sa_expired_sig_env.json +var MockSaExpiredSigEnv []byte + +//go:embed testdata/sa_plugin_sig_env.json +var MockSaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name" + +var ( + SampleArtifactUri = "registry.acme-rockets.io/software/net-monitor@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e" + SampleDigest = digest.Digest("sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e") + Annotations = map[string]string{"key": "value"} + ImageDescriptor = notation.Descriptor{ + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Digest: SampleDigest, + Size: 528, + Annotations: nil, + } + JwsSigEnvDescriptor = notation.Descriptor{ + MediaType: "application/jose+json", + Digest: SampleDigest, + Size: 100, + Annotations: Annotations, + } + PluginExtendedCriticalAttribute = signature.Attribute{ + Key: "SomeKey", + Critical: true, + Value: "SomeValue", + } +) + +type Repository struct { + ResolveResponse notation.Descriptor + ResolveError error + ListSignatureManifestsResponse []registry.SignatureManifest + ListSignatureManifestsError error + GetResponse []byte + GetError error +} + +func NewRepository() Repository { + return Repository{ + ResolveResponse: ImageDescriptor, + ListSignatureManifestsResponse: []registry.SignatureManifest{{ + Blob: JwsSigEnvDescriptor, + Annotations: Annotations, + }}, + GetResponse: MockCaValidSigEnv, + } +} + +func (t Repository) Resolve(ctx context.Context, reference string) (notation.Descriptor, error) { + return t.ResolveResponse, t.ResolveError +} + +func (t Repository) ListSignatureManifests(ctx context.Context, manifestDigest digest.Digest) ([]registry.SignatureManifest, error) { + return t.ListSignatureManifestsResponse, t.ListSignatureManifestsError +} + +func (t Repository) GetBlob(ctx context.Context, digest digest.Digest) ([]byte, error) { + return t.GetResponse, t.GetError +} + +func (t Repository) PutSignatureManifest(ctx context.Context, signature []byte, signatureMediaType string, manifest notation.Descriptor, annotaions map[string]string) (notation.Descriptor, registry.SignatureManifest, error) { + return notation.Descriptor{}, registry.SignatureManifest{}, nil +} + +type PluginManager struct { + PluginCapabilities []plugin.Capability + GetPluginError error + PluginRunnerLoadError error + PluginRunnerExecuteResponse interface{} + PluginRunnerExecuteError error +} + +type PluginRunner struct { + Response interface{} + Error error +} + +func (pr PluginRunner) Run(ctx context.Context, req plugin.Request) (interface{}, error) { + return pr.Response, pr.Error +} + +func (pm PluginManager) Get(ctx context.Context, name string) (*manager.Plugin, error) { + return &manager.Plugin{ + Metadata: plugin.Metadata{ + Name: "plugin-name", + Description: "for mocking in unit tests", + Version: "1.0.0", + URL: ".", + SupportedContractVersions: []string{"1.0"}, + Capabilities: pm.PluginCapabilities, + }, + Path: ".", + Err: nil, + }, pm.GetPluginError +} +func (pm PluginManager) Runner(name string) (plugin.Runner, error) { + return PluginRunner{ + Response: pm.PluginRunnerExecuteResponse, + Error: pm.PluginRunnerExecuteError, + }, pm.PluginRunnerLoadError +} diff --git a/internal/mock_origin/testdata/ca_expired_sig_env.json b/internal/mock_origin/testdata/ca_expired_sig_env.json new file mode 100644 index 00000000..9d629269 --- /dev/null +++ b/internal/mock_origin/testdata/ca_expired_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ", + "protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjAyMi0wNy0yOVQyMzo1OTowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjItMDctMjlUMDA6MDA6MDBaIn0", + "header": { + "x5c": [ + "MIIEWDCCAsCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMTAwOTA3MDAwMFoYDzIxMjIwODA2MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwE8YkFUAA0R7aUkRYxHKYoVbFPx9xhuNovLKDy72/7X0+j4XdGP4C0aAX2KLfgy9OR1RIUwtpMyI7k7ZFRd+ljcMW/FgbirfhkY/8axjamOYMBO0Qg+w93oaI6HA1gvZ/WZem4PHu68LlZhLQ2BrQwCz/F/3Ft0IZ2S1aF6N6vajx2le8xTI5hQS+UZFPQGrBUqrjcYc6GkL8XqL+rLGZaKGfh3c7bF9cEbA1H2Tm6MDFnfoFemerbP3v19JoUH+EtOnvYmNZWEU51RaLsNGkC3E/unXAnIfXrNxHDcbehyfa5y3AT10Shiron6O4Bc9S0MvwtXyLT6qein3Nh0VKBFUMSdthu5ZrSR28T9wDWHMXngpa115VjHOQDY3gDPwfzZ0xitN3NpMnivxculGUCkEQpst957tqQNJpS/zipI5Mtej0YOAhVKGQMjDIJekZ2DXDNd1X3xfahrR5VEQF0gnRFhA3vhycDqFj4E6Hoc5y3SxnFqrhX3w2wyFt/xRAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAYEAAdONCAJxdB7H0uFDw6H+8Z5MtoRdJe6ZhlM2O5WMzkC1DLSyrF7arPnUMTeSyNS2Fx1BU38n5R1wvdgSfWtjm7o2ZyR8JQ+AngPklUCTNeL18kxNNXpmjDuMvsRlfHcr5hherjiQ49jWlpFqGRrNtZQWiVEI0r9Qz8DtZTw3GYF4MSuotA6wuUjolI1V2oMn/gdt8FFo0XUTDyiA12qpZzkUHY1rg3zJxKq3pIk04E7k6rFakHyZL91ipV2UeSbNq9vwLL7cglfPJ8+J+9AKvIPDstDF5k0ivUCYH5fIFZBGoceLiNfHSMcqA/qWfErqLBWAkACRUNyCWpAEv3DfDRbTHId0n6QQwOXj5d9YnDrmOLvQcn/sa+ZBfFMK7RdG9uVwMRyo+sRUnxo+v2lcvYwWymL7ONQqVWZbTJCxuG90Unxa3cQHZiKB5mgKweMft+vp6C3IQFhFfP8j1kvRTJq8ZqSEBADppUuBZJ1KWalwauK0AE4jpHlE0KsYDXiP", + "MIIEizCCAvOgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMDkwOTA3MDAwMFoYDzIxMjIwOTA1MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxxAZ8VZegqBUctz3BkwhObZKnW+KsN5/N1/u2vPLmEzHDj6xgd8Hn0JoughDaxeQCV66NC2obqPnPp4+68G/qZnxkXVXdFyqVodu4FgPUjiqcJjft7bh45BVgLFpOqSqDQ3ko30B7gdGfIIkoBj/8gz3tHnmIvl3MywtOhDeGnlLNzBY52wVmhPIdKOaW/7WkMrXKFCkLkNICGnIpWuyBtC+7RfM8hG6eRW1KCm5xrkRmn5ptonjxix/JTGj4me/NMkwdVkz6wcCSAJnqTgHi2oqk73qqNu0LHsEMFBF8IGqmVkn2MOHkFamPBokzQ6HXXfvR4nbcWQZCUgRinPTVg9CF0B6XSCEMCSH5kveZxTQtAFRB6NosbzuU5jDmJgpbDfauev7Eg/6bZzphcugRkVuwulymzsake5Jbvs9Kyw3CNPYH2G3Kli1FNhfc46ugXHbIfXgNQcou3xabcu+r6cFRqqK6NmV9ouMQRj8Ri95Gp2BUlpTEFhcvMb9d4nXAgMBAAGjWjBYMA4GA1UdDwEB/wQEAwICBDATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBS5FZjt9UsEPkcKrStrnjSpTq4kDTANBgkqhkiG9w0BAQsFAAOCAYEAKtxfv12LzM85bxOMp5++pIDa6eMcBaurYbAM2yC9B6LuHf0JGeFdNqt4Fw38Ajooj2vWMWBrARVEZRVqTC5+ZSN2meGBXBXlT4n8FdEdmv+05iwVYdmDFp8FKeoOZZZF23u+r2OrazJo1ufWmoSI2P0lEfZQQFQElltWu3QH+OLOWXJmB7KbLKyheelGK5XhtAYYapRdW4sKJ398ybpv5C1oALCcTwoSmvH8wW5J4/gjmhKICYh2goMauf0lesdxj+0His7E8blOWrUmfOB5dp73XawLKcd/UxHN8zAPC08LDL9NMcihn3ZHKi7/dtkiV2iSaDPD1ChSGdqfXIysYqOhYoktgAfBZ43CWnqQhgB8NezRKdOStYC3P2AGJW18irxxTRp2CO+gnXEcyhyr+cvyf0j8MkRSaHLXzjIrECu8BUitB6sKughdN13fs5t5SIiO6foeFdvIpZFFKO8s+4oTOSDCos2WFoC+8TZS6r583OtFLmywl1HRgQkobGgw" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "RZtqCD4KGh5_CD8wjG69TJIzzB4Cr-cxQhKTvZJYsRVIJyl3s5D0215GhBrggomVk9-LGD2FdWd2VfuaLd4bmhW3rSV3ltmAext7DNQFg2xtMeYSeCL2U_ygN2j4bc80RDaX8w_zOTVOmuhW6i2jgwRjWXdDaJeYTbZA2syA5R38tYYewVcZJ6U057Wsflt5yPWJCdxZLuTago5CkbLASL8HHnmlUkDvKKB1Y9SNDOQ3AmGP4-XJykcX_MfPo5RGRvZE-zHUJOEKj3ryfC0UTUT7V1ISTagqOt7zOa1BEzgQ-1GQk1MbaPPZWkiOZX4RqMXMV3hVqtDuZxlpT25KzZPm1USwWwJkycv7YB69fc2aoHJAPo-39uEV9fdAz_03whnrQSpfJbmHHTXMJkWKrZ5ozU-8zlEttWyL5D85zAouSMVXWm22zMrDW-XxST9QoeV4b1_BedW1PwJDbeU6P1hhobnQh3jHmSueVl_WZ5_g8_iVepSmSBcR1e4WpoPi" +} \ No newline at end of file diff --git a/internal/mock_origin/testdata/ca_invalid_sig_env.json b/internal/mock_origin/testdata/ca_invalid_sig_env.json new file mode 100644 index 00000000..255eb32e --- /dev/null +++ b/internal/mock_origin/testdata/ca_invalid_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ=", + "protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjEyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjAtMTEtMDlUMDc6MDA6MDBaIn0", + "header": { + "x5c": [ + "MIIEWDCCAsCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMTAwOTA3MDAwMFoYDzIxMjIwODA2MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwE8YkFUAA0R7aUkRYxHKYoVbFPx9xhuNovLKDy72/7X0+j4XdGP4C0aAX2KLfgy9OR1RIUwtpMyI7k7ZFRd+ljcMW/FgbirfhkY/8axjamOYMBO0Qg+w93oaI6HA1gvZ/WZem4PHu68LlZhLQ2BrQwCz/F/3Ft0IZ2S1aF6N6vajx2le8xTI5hQS+UZFPQGrBUqrjcYc6GkL8XqL+rLGZaKGfh3c7bF9cEbA1H2Tm6MDFnfoFemerbP3v19JoUH+EtOnvYmNZWEU51RaLsNGkC3E/unXAnIfXrNxHDcbehyfa5y3AT10Shiron6O4Bc9S0MvwtXyLT6qein3Nh0VKBFUMSdthu5ZrSR28T9wDWHMXngpa115VjHOQDY3gDPwfzZ0xitN3NpMnivxculGUCkEQpst957tqQNJpS/zipI5Mtej0YOAhVKGQMjDIJekZ2DXDNd1X3xfahrR5VEQF0gnRFhA3vhycDqFj4E6Hoc5y3SxnFqrhX3w2wyFt/xRAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAYEAAdONCAJxdB7H0uFDw6H+8Z5MtoRdJe6ZhlM2O5WMzkC1DLSyrF7arPnUMTeSyNS2Fx1BU38n5R1wvdgSfWtjm7o2ZyR8JQ+AngPklUCTNeL18kxNNXpmjDuMvsRlfHcr5hherjiQ49jWlpFqGRrNtZQWiVEI0r9Qz8DtZTw3GYF4MSuotA6wuUjolI1V2oMn/gdt8FFo0XUTDyiA12qpZzkUHY1rg3zJxKq3pIk04E7k6rFakHyZL91ipV2UeSbNq9vwLL7cglfPJ8+J+9AKvIPDstDF5k0ivUCYH5fIFZBGoceLiNfHSMcqA/qWfErqLBWAkACRUNyCWpAEv3DfDRbTHId0n6QQwOXj5d9YnDrmOLvQcn/sa+ZBfFMK7RdG9uVwMRyo+sRUnxo+v2lcvYwWymL7ONQqVWZbTJCxuG90Unxa3cQHZiKB5mgKweMft+vp6C3IQFhFfP8j1kvRTJq8ZqSEBADppUuBZJ1KWalwauK0AE4jpHlE0KsYDXiP", + "MIIEizCCAvOgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMDkwOTA3MDAwMFoYDzIxMjIwOTA1MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxxAZ8VZegqBUctz3BkwhObZKnW+KsN5/N1/u2vPLmEzHDj6xgd8Hn0JoughDaxeQCV66NC2obqPnPp4+68G/qZnxkXVXdFyqVodu4FgPUjiqcJjft7bh45BVgLFpOqSqDQ3ko30B7gdGfIIkoBj/8gz3tHnmIvl3MywtOhDeGnlLNzBY52wVmhPIdKOaW/7WkMrXKFCkLkNICGnIpWuyBtC+7RfM8hG6eRW1KCm5xrkRmn5ptonjxix/JTGj4me/NMkwdVkz6wcCSAJnqTgHi2oqk73qqNu0LHsEMFBF8IGqmVkn2MOHkFamPBokzQ6HXXfvR4nbcWQZCUgRinPTVg9CF0B6XSCEMCSH5kveZxTQtAFRB6NosbzuU5jDmJgpbDfauev7Eg/6bZzphcugRkVuwulymzsake5Jbvs9Kyw3CNPYH2G3Kli1FNhfc46ugXHbIfXgNQcou3xabcu+r6cFRqqK6NmV9ouMQRj8Ri95Gp2BUlpTEFhcvMb9d4nXAgMBAAGjWjBYMA4GA1UdDwEB/wQEAwICBDATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBS5FZjt9UsEPkcKrStrnjSpTq4kDTANBgkqhkiG9w0BAQsFAAOCAYEAKtxfv12LzM85bxOMp5++pIDa6eMcBaurYbAM2yC9B6LuHf0JGeFdNqt4Fw38Ajooj2vWMWBrARVEZRVqTC5+ZSN2meGBXBXlT4n8FdEdmv+05iwVYdmDFp8FKeoOZZZF23u+r2OrazJo1ufWmoSI2P0lEfZQQFQElltWu3QH+OLOWXJmB7KbLKyheelGK5XhtAYYapRdW4sKJ398ybpv5C1oALCcTwoSmvH8wW5J4/gjmhKICYh2goMauf0lesdxj+0His7E8blOWrUmfOB5dp73XawLKcd/UxHN8zAPC08LDL9NMcihn3ZHKi7/dtkiV2iSaDPD1ChSGdqfXIysYqOhYoktgAfBZ43CWnqQhgB8NezRKdOStYC3P2AGJW18irxxTRp2CO+gnXEcyhyr+cvyf0j8MkRSaHLXzjIrECu8BUitB6sKughdN13fs5t5SIiO6foeFdvIpZFFKO8s+4oTOSDCos2WFoC+8TZS6r583OtFLmywl1HRgQkobGgw" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "ZvsxyaSqDzS7mY_jKpnq2XtBcmyWmSE461BHL6q2pAx_-Rxr8Fvs2oIfZdSG2o3qugPDjzZDMhKdYdnrW1AIEkVIG_QUmeyGj28PVXxsC5NKpXwrPUMOzrXSFLHIvBNZ2q87wRYInsgCPtv5ZPv0IgA2sAW6y7NlVM2D0vJax55ITsJO5aEaEUlAdi_H7-TCD48DHuFpnJdNkVB_hZkwYfxuqIKU2C__Z2hLLHxaS2LzuzhqOnYlbqn4e225uZt9odXq3qmZ_44Vx3DYL_-ZuV0S9jEk7NW8-dO0T0MeQn6VXDyfT1rjc6IVPnLxAnELFyLn121GYulYC8V2D1_MLcv8sDHY23rHb3-R-WCLMDSfaIvReY89vQfxcfpdCRC0F3N2CcnrgsrUC6Fplm5Uy45Gn9--b7x5cdSzOzQsefCH1GpixW7YyNs1xZQ17WqdYyWD2EBrB5vqVFzkzDYnQ4H-p9G3AzM4HTrjWqHX-0cYHlpmTS4AjVxn0UV80Jn9" +} \ No newline at end of file diff --git a/internal/mock_origin/testdata/ca_plugin_sig_env.json b/internal/mock_origin/testdata/ca_plugin_sig_env.json new file mode 100644 index 00000000..8b14bef8 --- /dev/null +++ b/internal/mock_origin/testdata/ca_plugin_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ", + "protected": "eyJTb21lS2V5IjoiU29tZVZhbHVlIiwiYWxnIjoiUFMzODQiLCJjcml0IjpbImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiLCJTb21lS2V5IiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luIl0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiOiIyMTIwLTExLTA5VDA3OjAwOjAwWiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iOiJwbHVnaW4tbmFtZSJ9", + "header": { + "x5c": [ + "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" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "cyB34qtMss9N1E_2XAQ_71c6j1fOcamenm7YrYsXn562XOhFgJKUjmDYWkz9mmdLN-GqQNKA8MhAfKt2ipXxsWldrb3a-6AZ-y4jIkY5XIY_s7Sndz58DPtez0X4kAehvKiyUtDVPbqIJQ5Hwgj8tC_f0Yva6pdrSD7xwenxwiCZmxM6N_LV9d1oYSDQi9890XRrFK4M1YRlOZquJ19HrhADLVJXS-ZfqcTE_tceoU2Hq82pqd2MnazAtJiWZm0cxwt-OsGlgGrkvHoNcMYS8K6BSBvL-vVtOuSpca89QrLsTCnKnmvUlw3wrWTDf83qhPyfw-2ASrE2V57vunpxSNyoA_70fNgOuhWUZZUTi9eXxutp0GCcGTem7MzZRBJVOVdw9OgR3pClGiRxP3BE2Atn3EUXs2HgQHEiE1KZvVHFeObB6asMqfbAMMNDgZCsZi7Yah7NaYg1NH9YwrJgAtNFW0p2trxiQ6uqICD2m54yGtRmvw_O9kt5HnUaBQJX" +} \ No newline at end of file diff --git a/internal/mock_origin/testdata/ca_valid_sig_env.json b/internal/mock_origin/testdata/ca_valid_sig_env.json new file mode 100644 index 00000000..e547a13f --- /dev/null +++ b/internal/mock_origin/testdata/ca_valid_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ", + "protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjEyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjAtMTEtMDlUMDc6MDA6MDBaIn0", + "header": { + "x5c": [ + "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" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "ZvsxyaSqDzS7mY_jKpnq2XtBcmyWmSE461BHL6q2pAx_-Rxr8Fvs2oIfZdSG2o3qugPDjzZDMhKdYdnrW1AIEkVIG_QUmeyGj28PVXxsC5NKpXwrPUMOzrXSFLHIvBNZ2q87wRYInsgCPtv5ZPv0IgA2sAW6y7NlVM2D0vJax55ITsJO5aEaEUlAdi_H7-TCD48DHuFpnJdNkVB_hZkwYfxuqIKU2C__Z2hLLHxaS2LzuzhqOnYlbqn4e225uZt9odXq3qmZ_44Vx3DYL_-ZuV0S9jEk7NW8-dO0T0MeQn6VXDyfT1rjc6IVPnLxAnELFyLn121GYulYC8V2D1_MLcv8sDHY23rHb3-R-WCLMDSfaIvReY89vQfxcfpdCRC0F3N2CcnrgsrUC6Fplm5Uy45Gn9--b7x5cdSzOzQsefCH1GpixW7YyNs1xZQ17WqdYyWD2EBrB5vqVFzkzDYnQ4H-p9G3AzM4HTrjWqHX-0cYHlpmTS4AjVxn0UV80Jn9" +} \ No newline at end of file diff --git a/internal/mock_origin/testdata/sa_expired_sig_env.json b/internal/mock_origin/testdata/sa_expired_sig_env.json new file mode 100644 index 00000000..19a316c0 --- /dev/null +++ b/internal/mock_origin/testdata/sa_expired_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ", + "protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmF1dGhlbnRpY1NpZ25pbmdUaW1lIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5hdXRoZW50aWNTaWduaW5nVGltZSI6IjIwMjItMDctMjlUMDA6MDA6MDBaIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjAyMi0wNy0yOVQyMzo1OTowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkuc2lnbmluZ0F1dGhvcml0eSJ9", + "header": { + "x5c": [ + "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" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "nDpYiwd536V2krjmxH2FCk6QgUTRyA6AFL9D5sDBJ3JwS9q9znsefSIg9rz6PMskVO9GUzUSG0ZIna5izrVR9pctLw4yQrWIZz3fp-lc3orK4w1nmHG_pCdpasH4FxpvXa0-4dllJmX2Yc3GrdeFaxJhcgtr2iiArabKnOFh5DbfOpeyMGDEa2XVRnrcS4VRgc5UdewFkq2NslMw1Y9loQwrNr3JGTQQpvZHOR4yBtnfCWFJ7G8AYDUb4H1Us8iaIlyp-jSIVSOT9HQzizDzZgn-Gtv90pq9xqAEtrW4thkPUOOJP_P0-_huAH3475UEPi-Yc7ekyt7PH6PazyI9yuTsJlkM_eWDsNLDARRfgygzr9DJHPkYQG3S8MRfNGqskob6Lcfl8nPaXnTfAhLNl-JiWvzMpwq1af2sWek-NVcGf5-81hRF9GTCE1IAtjQ0ITR86zq_G8pEj4JfI-H0c0yXTDUilUHzwzXV_7zE0gEB8UFHHg9VHGflYRdbWuS9" +} \ No newline at end of file diff --git a/internal/mock_origin/testdata/sa_invalid_sig_env.json b/internal/mock_origin/testdata/sa_invalid_sig_env.json new file mode 100644 index 00000000..ec86ab8a --- /dev/null +++ b/internal/mock_origin/testdata/sa_invalid_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ=", + "protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmF1dGhlbnRpY1NpZ25pbmdUaW1lIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5hdXRoZW50aWNTaWduaW5nVGltZSI6IjIwMjAtMTEtMDlUMDc6MDA6MDBaIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjEyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkuc2lnbmluZ0F1dGhvcml0eSJ9", + "header": { + "x5c": [ + "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" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "kqt4plYZgCdPkoVmC-1_JfH7dPUjIQOMaONP6pEucnKC1QiTa7peN83Ka8_0kAvAT3BIZ8CFjVuazioZpjHw-ydRlL3-pgagnENS8Fz2Vfwj9nKJF7mmFGi3R0t6fFFyx_Tw9rtxi4Nsv8y4k-2XLFLeSm1_EEDThHPVMbWE6XJpOIdvr2w3Iq1PsEOVo9QqVOd3FYcGLQAbiAAi_jREYpEKImFqQeY8noUCDOtULPwxbslrglOOBtKouI4OUT0ZtG3tDCBdoZUOAfNgKSlHQutlA0-G6GdBuytCz0ku45DTnGAPS11WwsuPBJfouYlusJuZHmqJTodwEnu2B2AZpLu5wxRUwWOpSyc8ftnSBkiHJWIT3bwatPjlaHoIgwcEsGPRwvFCq7V7yH2yW2uHI1FsiMUHYuWx-hDpLf4Nzag5oc-PyaV3lzsvZZHwy43ilFO-WJOZeDQCWjIZ_U1f4hGsoDkqvoRn-aFZ-pE7Nn99buVRHDjQ6-8-jfJncJaB" +} diff --git a/internal/mock_origin/testdata/sa_plugin_sig_env.json b/internal/mock_origin/testdata/sa_plugin_sig_env.json new file mode 100644 index 00000000..966082a8 --- /dev/null +++ b/internal/mock_origin/testdata/sa_plugin_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ", + "protected": "eyJTb21lS2V5IjoiU29tZVZhbHVlIiwiYWxnIjoiUFMzODQiLCJjcml0IjpbImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiLCJTb21lS2V5IiwiaW8uY25jZi5ub3RhcnkuYXV0aGVudGljU2lnbmluZ1RpbWUiLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LmF1dGhlbnRpY1NpZ25pbmdUaW1lIjoiMjAyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiOiIyMTIwLTExLTA5VDA3OjAwOjAwWiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOS5zaWduaW5nQXV0aG9yaXR5IiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luIjoicGx1Z2luLW5hbWUifQ", + "header": { + "x5c": [ + "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" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "DwrGzND2JgpkeFASatpp-kBKpgrlt1Io3fbetSB3VUnRb0zWkj3vreKzAFpNBI6MN0lTuIWA3_igTqkYcFq8VFW2VSvWGidARJnzd4WDrCFp7n-Qp9TQPqbkLknZUxT2pFsTw1EF_plyAdJmRwbJikwvc2RkxW1Bz6fAcagJEul4lm6j2Yq4iTE8xThjn1ih7_9XMQ9I1f79CK3CTdu9jCrlQbyC1wEI9btyx-91OJ2V1oeGVtasNvRhA1ttVS3h7EQvzcJ9eKdEHPCVK6j5X7xvbjz40Z2kouZAb3ve9jsYZquMx6krrwAh4JPwUDJGT2x6ujdIIU6QioJgbOqRLdyYYERHqhO3P3FAsIJqIwtupMkcSJZJrMlzdi_nuHPHvy9ToQTW5z98LSQHqHtmWf4JdfVGq5iOWwrwLO4QINi716wcqiVp8srd2VdpoxvA5nnT2zzukzSXXVFj3V7XcqWutQoM3ihfw-aWDLU_OBo7aaSLaZUXhYkLsB3pHX1G" +} diff --git a/internal/mock_origin/testdata/sa_valid_sig_env.json b/internal/mock_origin/testdata/sa_valid_sig_env.json new file mode 100644 index 00000000..e1d49a6c --- /dev/null +++ b/internal/mock_origin/testdata/sa_valid_sig_env.json @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ", + "protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmF1dGhlbnRpY1NpZ25pbmdUaW1lIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5hdXRoZW50aWNTaWduaW5nVGltZSI6IjIwMjAtMTEtMDlUMDc6MDA6MDBaIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjEyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkuc2lnbmluZ0F1dGhvcml0eSJ9", + "header": { + "x5c": [ + "MIIEWDCCAsCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMTAwOTA3MDAwMFoYDzIxMjIwODA2MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwE8YkFUAA0R7aUkRYxHKYoVbFPx9xhuNovLKDy72/7X0+j4XdGP4C0aAX2KLfgy9OR1RIUwtpMyI7k7ZFRd+ljcMW/FgbirfhkY/8axjamOYMBO0Qg+w93oaI6HA1gvZ/WZem4PHu68LlZhLQ2BrQwCz/F/3Ft0IZ2S1aF6N6vajx2le8xTI5hQS+UZFPQGrBUqrjcYc6GkL8XqL+rLGZaKGfh3c7bF9cEbA1H2Tm6MDFnfoFemerbP3v19JoUH+EtOnvYmNZWEU51RaLsNGkC3E/unXAnIfXrNxHDcbehyfa5y3AT10Shiron6O4Bc9S0MvwtXyLT6qein3Nh0VKBFUMSdthu5ZrSR28T9wDWHMXngpa115VjHOQDY3gDPwfzZ0xitN3NpMnivxculGUCkEQpst957tqQNJpS/zipI5Mtej0YOAhVKGQMjDIJekZ2DXDNd1X3xfahrR5VEQF0gnRFhA3vhycDqFj4E6Hoc5y3SxnFqrhX3w2wyFt/xRAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAYEAAdONCAJxdB7H0uFDw6H+8Z5MtoRdJe6ZhlM2O5WMzkC1DLSyrF7arPnUMTeSyNS2Fx1BU38n5R1wvdgSfWtjm7o2ZyR8JQ+AngPklUCTNeL18kxNNXpmjDuMvsRlfHcr5hherjiQ49jWlpFqGRrNtZQWiVEI0r9Qz8DtZTw3GYF4MSuotA6wuUjolI1V2oMn/gdt8FFo0XUTDyiA12qpZzkUHY1rg3zJxKq3pIk04E7k6rFakHyZL91ipV2UeSbNq9vwLL7cglfPJ8+J+9AKvIPDstDF5k0ivUCYH5fIFZBGoceLiNfHSMcqA/qWfErqLBWAkACRUNyCWpAEv3DfDRbTHId0n6QQwOXj5d9YnDrmOLvQcn/sa+ZBfFMK7RdG9uVwMRyo+sRUnxo+v2lcvYwWymL7ONQqVWZbTJCxuG90Unxa3cQHZiKB5mgKweMft+vp6C3IQFhFfP8j1kvRTJq8ZqSEBADppUuBZJ1KWalwauK0AE4jpHlE0KsYDXiP", + "MIIEizCCAvOgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMDkwOTA3MDAwMFoYDzIxMjIwOTA1MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxxAZ8VZegqBUctz3BkwhObZKnW+KsN5/N1/u2vPLmEzHDj6xgd8Hn0JoughDaxeQCV66NC2obqPnPp4+68G/qZnxkXVXdFyqVodu4FgPUjiqcJjft7bh45BVgLFpOqSqDQ3ko30B7gdGfIIkoBj/8gz3tHnmIvl3MywtOhDeGnlLNzBY52wVmhPIdKOaW/7WkMrXKFCkLkNICGnIpWuyBtC+7RfM8hG6eRW1KCm5xrkRmn5ptonjxix/JTGj4me/NMkwdVkz6wcCSAJnqTgHi2oqk73qqNu0LHsEMFBF8IGqmVkn2MOHkFamPBokzQ6HXXfvR4nbcWQZCUgRinPTVg9CF0B6XSCEMCSH5kveZxTQtAFRB6NosbzuU5jDmJgpbDfauev7Eg/6bZzphcugRkVuwulymzsake5Jbvs9Kyw3CNPYH2G3Kli1FNhfc46ugXHbIfXgNQcou3xabcu+r6cFRqqK6NmV9ouMQRj8Ri95Gp2BUlpTEFhcvMb9d4nXAgMBAAGjWjBYMA4GA1UdDwEB/wQEAwICBDATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBS5FZjt9UsEPkcKrStrnjSpTq4kDTANBgkqhkiG9w0BAQsFAAOCAYEAKtxfv12LzM85bxOMp5++pIDa6eMcBaurYbAM2yC9B6LuHf0JGeFdNqt4Fw38Ajooj2vWMWBrARVEZRVqTC5+ZSN2meGBXBXlT4n8FdEdmv+05iwVYdmDFp8FKeoOZZZF23u+r2OrazJo1ufWmoSI2P0lEfZQQFQElltWu3QH+OLOWXJmB7KbLKyheelGK5XhtAYYapRdW4sKJ398ybpv5C1oALCcTwoSmvH8wW5J4/gjmhKICYh2goMauf0lesdxj+0His7E8blOWrUmfOB5dp73XawLKcd/UxHN8zAPC08LDL9NMcihn3ZHKi7/dtkiV2iSaDPD1ChSGdqfXIysYqOhYoktgAfBZ43CWnqQhgB8NezRKdOStYC3P2AGJW18irxxTRp2CO+gnXEcyhyr+cvyf0j8MkRSaHLXzjIrECu8BUitB6sKughdN13fs5t5SIiO6foeFdvIpZFFKO8s+4oTOSDCos2WFoC+8TZS6r583OtFLmywl1HRgQkobGgw" + ], + "io.cncf.notary.SigningAgent": "Notation/1.0.0" + }, + "signature": "kqt4plYZgCdPkoVmC-1_JfH7dPUjIQOMaONP6pEucnKC1QiTa7peN83Ka8_0kAvAT3BIZ8CFjVuazioZpjHw-ydRlL3-pgagnENS8Fz2Vfwj9nKJF7mmFGi3R0t6fFFyx_Tw9rtxi4Nsv8y4k-2XLFLeSm1_EEDThHPVMbWE6XJpOIdvr2w3Iq1PsEOVo9QqVOd3FYcGLQAbiAAi_jREYpEKImFqQeY8noUCDOtULPwxbslrglOOBtKouI4OUT0ZtG3tDCBdoZUOAfNgKSlHQutlA0-G6GdBuytCz0ku45DTnGAPS11WwsuPBJfouYlusJuZHmqJTodwEnu2B2AZpLu5wxRUwWOpSyc8ftnSBkiHJWIT3bwatPjlaHoIgwcEsGPRwvFCq7V7yH2yW2uHI1FsiMUHYuWx-hDpLf4Nzag5oc-PyaV3lzsvZZHwy43ilFO-WJOZeDQCWjIZ_U1f4hGsoDkqvoRn-aFZ-pE7Nn99buVRHDjQ6-8-jfJncJaB" +} diff --git a/internal/policy/policy.go b/internal/policy/policy.go new file mode 100644 index 00000000..33b80461 --- /dev/null +++ b/internal/policy/policy.go @@ -0,0 +1,64 @@ +package policy + +import ( + "fmt" + "strings" + + "github.com/notaryproject/notation-go/internal/common" + "github.com/notaryproject/notation-go/verification/trustpolicy" +) + +// getApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy statement that applies to the given +// registry URI. If no applicable trust policy is found, returns an error +// see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#selecting-a-trust-policy-based-on-artifact-uri +func GetApplicableTrustPolicy(trustPolicyDoc *trustpolicy.Document, artifactReference string) (*trustpolicy.TrustPolicy, error) { + + artifactPath, err := getArtifactPathFromReference(artifactReference) + if err != nil { + return nil, err + } + + var wildcardPolicy *trustpolicy.TrustPolicy + var applicablePolicy *trustpolicy.TrustPolicy + for _, policyStatement := range trustPolicyDoc.TrustPolicies { + if common.IsPresent(common.Wildcard, policyStatement.RegistryScopes) { + wildcardPolicy = deepCopy(&policyStatement) // we need to deep copy because we can't use the loop variable address. see https://stackoverflow.com/a/45967429 + } else if common.IsPresent(artifactPath, policyStatement.RegistryScopes) { + applicablePolicy = deepCopy(&policyStatement) + } + } + + if applicablePolicy != nil { + // a policy with exact match for registry URI 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", artifactReference) + } +} + +// deepCopy returns a pointer to the deeply copied TrustPolicy +func deepCopy(t *trustpolicy.TrustPolicy) *trustpolicy.TrustPolicy { + return &trustpolicy.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 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 := common.ValidateRegistryScopeFormat(artifactPath); err != nil { + return "", err + } + return artifactPath, nil +} diff --git a/internal/policy/policy_test.go b/internal/policy/policy_test.go new file mode 100644 index 00000000..bea6d7ae --- /dev/null +++ b/internal/policy/policy_test.go @@ -0,0 +1,71 @@ +package policy + +import ( + "fmt" + "testing" + + "github.com/notaryproject/notation-go/verification/trustpolicy" +) + +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 +} + +// 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 = trustpolicy.SignatureVerification{VerificationLevel: "strict"} + + policyDoc.TrustPolicies = []trustpolicy.TrustPolicy{ + policyStatement, + } + // existing Registry Scope + policy, err := GetApplicableTrustPolicy(&policyDoc, 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 = GetApplicableTrustPolicy(&policyDoc, "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" { + 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 = trustpolicy.SignatureVerification{VerificationLevel: "skip"} + + policyDoc.TrustPolicies = []trustpolicy.TrustPolicy{ + policyStatement, + wildcardStatement, + } + policy, err = GetApplicableTrustPolicy(&policyDoc, "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\"") + } +} diff --git a/notation/notation.go b/notation/notation.go index 57eeab3a..677a1f2a 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -14,7 +14,7 @@ import ( "github.com/notaryproject/notation-core-go/timestamp" "github.com/notaryproject/notation-go/internal/policy" "github.com/notaryproject/notation-go/registry" - "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/notaryproject/notation-go/verification/trustpolicy" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) diff --git a/notation/notation_test.go b/notation/notation_test.go index e1c782cd..be6fc04e 100644 --- a/notation/notation_test.go +++ b/notation/notation_test.go @@ -9,7 +9,7 @@ import ( "github.com/notaryproject/notation-go/internal/mock" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" - "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/notaryproject/notation-go/verification/trustpolicy" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -53,7 +53,7 @@ func TestRegistryListSignaturesError(t *testing.T) { expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} // mock the repository - repo.ListSignatureManifestsError = ErrorSignatureRetrievalFailed{Msg: "network error"} + repo.ListSignaturesError = ErrorSignatureRetrievalFailed{Msg: "network error"} opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} _, _, err := Verify(context.Background(), &verifier, repo, opts) @@ -70,7 +70,7 @@ func TestRegistryNoSignatureManifests(t *testing.T) { expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} // mock the repository - repo.ListSignatureManifestsResponse = []ocispec.Descriptor{} + repo.ListSignaturesResponse = []ocispec.Descriptor{} opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} _, _, err := Verify(context.Background(), &verifier, repo, opts) @@ -87,7 +87,7 @@ func TestRegistryFetchSignatureBlobError(t *testing.T) { expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} // mock the repository - repo.GetError = errors.New("network error") + repo.FetchSignatureBlobError = errors.New("network error") opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} _, _, err := Verify(context.Background(), &verifier, repo, opts) diff --git a/verification/trustpolicy/trustpolicy.go b/verification/trustpolicy/trustpolicy.go new file mode 100644 index 00000000..245e6c98 --- /dev/null +++ b/verification/trustpolicy/trustpolicy.go @@ -0,0 +1,378 @@ +package trustpolicy + +import ( + "errors" + "fmt" + "strings" + + "github.com/notaryproject/notation-go/internal/common" + "github.com/notaryproject/notation-go/verification/truststore" +) + +// ValidationType is an enum for signature verification types such as Integrity, Authenticity, etc. +type ValidationType string + +// ValidationAction is an enum for signature verification actions such as Enforced, Logged, Skipped. +type ValidationAction string + +// VerificationLevel encapsulates the signature verification preset and it's actions for each verification type +type VerificationLevel struct { + Name string + Enforcement map[ValidationType]ValidationAction +} + +const ( + TypeIntegrity ValidationType = "integrity" + TypeAuthenticity ValidationType = "authenticity" + TypeAuthenticTimestamp ValidationType = "authenticTimestamp" + TypeExpiry ValidationType = "expiry" + TypeRevocation ValidationType = "revocation" + + ActionEnforce ValidationAction = "enforce" + ActionLog ValidationAction = "log" + ActionSkip ValidationAction = "skip" +) + +var ( + LevelStrict = &VerificationLevel{ + Name: "strict", + Enforcement: map[ValidationType]ValidationAction{ + TypeIntegrity: ActionEnforce, + TypeAuthenticity: ActionEnforce, + TypeAuthenticTimestamp: ActionEnforce, + TypeExpiry: ActionEnforce, + TypeRevocation: ActionEnforce, + }, + } + + LevelPermissive = &VerificationLevel{ + Name: "permissive", + Enforcement: map[ValidationType]ValidationAction{ + TypeIntegrity: ActionEnforce, + TypeAuthenticity: ActionEnforce, + TypeAuthenticTimestamp: ActionLog, + TypeExpiry: ActionLog, + TypeRevocation: ActionLog, + }, + } + + LevelAudit = &VerificationLevel{ + Name: "audit", + Enforcement: map[ValidationType]ValidationAction{ + TypeIntegrity: ActionEnforce, + TypeAuthenticity: ActionLog, + TypeAuthenticTimestamp: ActionLog, + TypeExpiry: ActionLog, + TypeRevocation: ActionLog, + }, + } + + LevelSkip = &VerificationLevel{ + Name: "skip", + Enforcement: map[ValidationType]ValidationAction{ + TypeIntegrity: ActionSkip, + TypeAuthenticity: ActionSkip, + TypeAuthenticTimestamp: ActionSkip, + TypeExpiry: ActionSkip, + TypeRevocation: ActionSkip, + }, + } + + ValidationTypes = []ValidationType{ + TypeIntegrity, + TypeAuthenticity, + TypeAuthenticTimestamp, + TypeExpiry, + TypeRevocation, + } + + ValidationActions = []ValidationAction{ + ActionEnforce, + ActionLog, + ActionSkip, + } + + VerificationLevels = []*VerificationLevel{ + LevelStrict, + LevelPermissive, + LevelAudit, + LevelSkip, + } +) + +// 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 it's version's rule set. +// if any rule is violated, returns an error +func (policyDoc *Document) Validate() error { + // Constants + supportedPolicyVersions := []string{"1.0"} + + // Validate Version + if !common.IsPresent(policyDoc.Version, supportedPolicyVersions) { + 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 level is valid + verificationLevel, err := GetVerificationLevel(statement.SignatureVerification) + if err != nil { + return fmt.Errorf("trust policy statement %q uses invalid signatureVerification value %q", statement.Name, statement.SignatureVerification.VerificationLevel) + } + + // 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 +} + +// GetVerificationLevel returns VerificationLevel struct for the given SignatureVerification struct +// throws error if SignatureVerification is invalid +func GetVerificationLevel(signatureVerification SignatureVerification) (*VerificationLevel, error) { + var baseLevel *VerificationLevel + for _, l := range VerificationLevels { + if l.Name == signatureVerification.VerificationLevel { + baseLevel = l + } + } + if baseLevel == nil { + return nil, fmt.Errorf("invalid signature verification %q", signatureVerification.VerificationLevel) + } + + if len(signatureVerification.Override) == 0 { + // nothing to override, return the base verification level + return baseLevel, nil + } + + if baseLevel == LevelSkip { + return nil, fmt.Errorf("signature verification %q can't be used to customize signature verification", baseLevel.Name) + } + + customVerificationLevel := &VerificationLevel{ + Name: "custom", + Enforcement: make(map[ValidationType]ValidationAction), + } + + // populate the custom verification level with the base verification settings + for k, v := range baseLevel.Enforcement { + customVerificationLevel.Enforcement[k] = v + } + + // override the verification actions with the user configured settings + for key, value := range signatureVerification.Override { + var validationType ValidationType + for _, t := range ValidationTypes { + if t == key { + validationType = t + break + } + } + if validationType == "" { + return nil, fmt.Errorf("verification type %q in custom signature verification is not supported, supported values are %q", key, ValidationTypes) + } + + var validationAction ValidationAction + for _, action := range ValidationActions { + if action == value { + validationAction = action + break + } + } + if validationAction == "" { + return nil, fmt.Errorf("verification action %q in custom signature verification is not supported, supported values are %q", value, ValidationActions) + } + + if validationType == TypeIntegrity { + return nil, fmt.Errorf("%q verification can not be overridden in custom signature verification", key) + } else if validationType != TypeRevocation && validationAction == ActionSkip { + return nil, fmt.Errorf("%q verification can not be skipped in custom signature verification", key) + } + + customVerificationLevel.Enforcement[validationType] = validationAction + } + return customVerificationLevel, nil +} + +// validateTrustStore validates if the policy statement is following the Notary V2 spec rules for truststores +func validateTrustStore(statement TrustPolicy) error { + for _, trustStore := range statement.TrustStores { + i := strings.Index(trustStore, ":") + if i < 0 || !isValidTrustStoreType(trustStore[:i]) { + return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, trustStore[:i], trustStore) + } + } + + return nil +} + +// validateTrustedIdentities validates if the policy statement is following the Notary V2 spec rules for trusted identities +func validateTrustedIdentities(statement TrustPolicy) error { + + // If there is a wildcard in trusted identies, there shouldn't be any other identities + if len(statement.TrustedIdentities) > 1 && common.IsPresent(common.Wildcard, statement.TrustedIdentities) { + 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) + } + + var parsedDNs []common.ParsedDN + // If there are trusted identities, verify they are valid + for _, identity := range statement.TrustedIdentities { + if identity == "" { + return fmt.Errorf("trust policy statement %q has an empty trusted identity", statement.Name) + } + + if identity != common.Wildcard { + i := strings.Index(identity, ":") + if i < 0 { + return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity prefix", statement.Name, identity) + } + + identityPrefix := identity[:i] + identityValue := identity[i+1:] + + // notation natively supports x509.subject identities only + if identityPrefix == common.X509Subject { + dn, err := common.ParseDistinguishedName(identityValue) + if err != nil { + return err + } + parsedDNs = append(parsedDNs, common.ParsedDN{RawString: identity, ParsedMap: dn}) + } + } + } + + // Verify there are no overlapping DNs + if err := validateOverlappingDNs(statement.Name, parsedDNs); err != nil { + return err + } + + // No error + return nil +} + +// validateRegistryScopes validates if the policy document is following the Notary V2 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 && common.IsPresent(common.Wildcard, statement.RegistryScopes) { + 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 != common.Wildcard { + if err := common.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 []common.ParsedDN) error { + for i, dn1 := range parsedDNs { + for j, dn2 := range parsedDNs { + if i != j && common.IsSubsetDN(dn1.ParsedMap, dn2.ParsedMap) { + return fmt.Errorf("trust policy statement %q has overlapping x509 trustedIdentities, %q overlaps with %q", policyName, dn1.RawString, dn2.RawString) + } + } + } + + return nil +} + +// isValidTrustStoreType returns true if the given string is a valid truststore.Type, otherwise false. +func isValidTrustStoreType(s string) bool { + for _, p := range truststore.Types { + if s == string(p) { + return true + } + } + return false +} diff --git a/verification/trustpolicy/trustpolicy_test.go b/verification/trustpolicy/trustpolicy_test.go new file mode 100644 index 00000000..b8d4ee54 --- /dev/null +++ b/verification/trustpolicy/trustpolicy_test.go @@ -0,0 +1,446 @@ +package trustpolicy + +import ( + "strconv" + "testing" +) + +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 dummyPolicyDocument() (policyDoc Document) { + policyDoc = Document{ + Version: "1.0", + TrustPolicies: []TrustPolicy{dummyPolicyStatement()}, + } + 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) + } +} + +// TestValidateTrustedIdentities tests only valid x509.subjects are accepted +func TestValidateTrustedIdentities(t *testing.T) { + + // 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() + if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"C=US, ST=WA, O=wabbit-network.io, OU=org1\" without an identity prefix" { + t.Fatalf("trusted identity without a prefix 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() + 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() + if err == nil || err.Error() != "distinguished name (DN) \",,,\" is not valid, it 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 duplicate RDNs + policyDoc = dummyPolicyDocument() + policyStatement = dummyPolicyStatement() + invalidDN = "x509.subject:C=US,C=IN" + policyStatement.TrustedIdentities = []string{invalidDN} + policyDoc.TrustPolicies = []TrustPolicy{policyStatement} + err = policyDoc.Validate() + if err == nil || err.Error() != "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() + if err == nil || err.Error() != "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() + 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" + validDN4 := "x509.subject:C=US,ST=WA,O=My Org,1.3.6.1.4.1.1466.0=#04024869" + policyStatement.TrustedIdentities = []string{validDN1, validDN2, validDN3, validDN4} + policyDoc.TrustPolicies = []TrustPolicy{policyStatement} + err = policyDoc.Validate() + 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() + 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() + if err == nil || err.Error() != "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 the fully qualified registry URL without the scheme/protocol. e.g domain.com/my/repository" { + 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) { + + // 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\" uses invalid signatureVerification value \"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") + } + + // 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") + } + + // 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") + } + + // 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 + wantErr bool + verificationActions []ValidationAction + }{ + {SignatureVerification{VerificationLevel: "strict"}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionEnforce, ActionEnforce, ActionEnforce}}, + {SignatureVerification{VerificationLevel: "permissive"}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "audit"}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "skip"}, false, []ValidationAction{ActionSkip, ActionSkip, ActionSkip, ActionSkip, ActionSkip}}, + {SignatureVerification{VerificationLevel: "invalid"}, true, []ValidationAction{}}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + + level, err := GetVerificationLevel(tt.verificationLevel) + + if tt.wantErr != (err != nil) { + t.Fatalf("TestFindVerificationLevel Error: %q WantErr: %v", err, tt.wantErr) + } else { + for index, action := range tt.verificationActions { + if action != level.Enforcement[ValidationTypes[index]] { + t.Errorf("%q verification action should be %q for Verification Level %q", ValidationTypes[index], action, tt.verificationLevel) + } + } + } + }) + } +} + +func TestCustomVerificationLevel(t *testing.T) { + tests := []struct { + customVerification SignatureVerification + wantErr bool + verificationActions []ValidationAction + }{ + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"integrity": "log"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"authenticity": "skip"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"authenticTimestamp": "skip"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"expiry": "skip"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "skip", Override: map[ValidationType]ValidationAction{"authenticity": "log"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "invalid", Override: map[ValidationType]ValidationAction{"authenticity": "log"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"invalid": "log"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"authenticity": "invalid"}}, true, []ValidationAction{}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"authenticity": "log"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionEnforce, ActionEnforce, ActionEnforce}}, + {SignatureVerification{VerificationLevel: "permissive", Override: map[ValidationType]ValidationAction{"authenticity": "log"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "audit", Override: map[ValidationType]ValidationAction{"authenticity": "log"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"expiry": "log"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionEnforce, ActionLog, ActionEnforce}}, + {SignatureVerification{VerificationLevel: "permissive", Override: map[ValidationType]ValidationAction{"expiry": "log"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "audit", Override: map[ValidationType]ValidationAction{"expiry": "log"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"revocation": "log"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionEnforce, ActionEnforce, ActionLog}}, + {SignatureVerification{VerificationLevel: "permissive", Override: map[ValidationType]ValidationAction{"revocation": "log"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "audit", Override: map[ValidationType]ValidationAction{"revocation": "log"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"revocation": "skip"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionEnforce, ActionEnforce, ActionSkip}}, + {SignatureVerification{VerificationLevel: "permissive", Override: map[ValidationType]ValidationAction{"revocation": "skip"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionLog, ActionLog, ActionSkip}}, + {SignatureVerification{VerificationLevel: "audit", Override: map[ValidationType]ValidationAction{"revocation": "skip"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionSkip}}, + {SignatureVerification{VerificationLevel: "permissive", Override: map[ValidationType]ValidationAction{"authenticTimestamp": "log"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "audit", Override: map[ValidationType]ValidationAction{"authenticTimestamp": "log"}}, false, []ValidationAction{ActionEnforce, ActionLog, ActionLog, ActionLog, ActionLog}}, + {SignatureVerification{VerificationLevel: "strict", Override: map[ValidationType]ValidationAction{"authenticTimestamp": "log"}}, false, []ValidationAction{ActionEnforce, ActionEnforce, ActionLog, ActionEnforce, ActionEnforce}}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + level, err := GetVerificationLevel(tt.customVerification) + + if tt.wantErr != (err != nil) { + t.Fatalf("TestCustomVerificationLevel Error: %q WantErr: %v", err, tt.wantErr) + } else { + if !tt.wantErr && len(tt.verificationActions) == 0 { + t.Errorf("test case isn't configured with VerificationActions") + } + for index, action := range tt.verificationActions { + if action != level.Enforcement[ValidationTypes[index]] { + t.Errorf("%q verification action should be %q for custom verification %q", ValidationTypes[index], action, tt.customVerification) + } + } + } + }) + } +} diff --git a/verification/truststore/truststore.go b/verification/truststore/truststore.go new file mode 100644 index 00000000..10cef927 --- /dev/null +++ b/verification/truststore/truststore.go @@ -0,0 +1,126 @@ +// Package truststore reads certificates in a trust store +package truststore + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + corex509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/notation-go/dir" +) + +// Type is an enum for trust store types supported such as +// "ca" and "signingAuthority" +type Type string + +const ( + TypeCA Type = "ca" + TypeSigningAuthority Type = "signingAuthority" +) + +var ( + Types = []Type{ + TypeCA, + TypeSigningAuthority, + } +) + +// X509TrustStore provide list and get behaviors for the trust store +type X509TrustStore interface { + // List named stores under storeType + // List(ctx context.Context, storeType Type) ([]string, error) + + // GetCertificates returns certificates under storeType/namedStore + GetCertificates(ctx context.Context, storeType Type, namedStore string) ([]*x509.Certificate, error) +} + +// NewX509TrustStore generates a new X509TrustStore +func NewX509TrustStore(trustStorefs dir.SysFS) X509TrustStore { + return x509TrustStore{trustStorefs} +} + +// x509TrustStore implements X509TrustStore +type x509TrustStore struct { + trustStorefs dir.SysFS +} + +// GetCertificates returns certificates under storeType/namedStore +func (trustStore x509TrustStore) GetCertificates(ctx context.Context, storeType Type, namedStore string) ([]*x509.Certificate, error) { + if storeType == "" || namedStore == "" { + return nil, errors.New("storeType and namedStore cannot be empty") + } + path, err := trustStore.trustStorefs.SysPath(dir.X509TrustStoreDir(string(storeType), namedStore)) + if err != nil { + return nil, err + } + // check path is valid + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("%q does not exist", path) + } + + // throw error if path is not a directory or is a symlink + fileInfo, err := os.Lstat(path) + if err != nil { + return nil, err + } + mode := fileInfo.Mode() + if !mode.IsDir() || mode&fs.ModeSymlink != 0 { + return nil, fmt.Errorf("%q is not a regular directory (symlinks are not supported)", path) + } + + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + var certificates []*x509.Certificate + for _, file := range files { + joinedPath := filepath.Join(path, file.Name()) + if file.IsDir() || file.Type()&fs.ModeSymlink != 0 { + return nil, fmt.Errorf("%q is not a regular file (directories or symlinks are not supported)", joinedPath) + } + certs, err := corex509.ReadCertificateFile(joinedPath) + if err != nil { + return nil, fmt.Errorf("error while reading certificates from %q: %w", joinedPath, err) + } + + if err := validateCerts(certs, joinedPath); err != nil { + return nil, err + } + + certificates = append(certificates, certs...) + } + + if len(certificates) < 1 { + return nil, fmt.Errorf("trust store %q has no x509 certificates", path) + } + + return certificates, nil +} + +func validateCerts(certs []*x509.Certificate, path string) error { + // to prevent any trust store misconfigurations, ensure there is at least + // one certificate from each file. + if len(certs) < 1 { + return fmt.Errorf("could not parse a certificate from %q, every file in a trust store must have a PEM or DER certificate in it", path) + } + + for _, cert := range certs { + if !cert.IsCA { + if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { + return fmt.Errorf( + "certificate with subject %q from file %q is not a CA certificate or self-signed signing certificate", + cert.Subject, + path, + ) + } + } + } + + return nil +} diff --git a/verification/truststore/truststore_test.go b/verification/truststore/truststore_test.go new file mode 100644 index 00000000..c4f2bb95 --- /dev/null +++ b/verification/truststore/truststore_test.go @@ -0,0 +1,58 @@ +package truststore + +import ( + "context" + "fmt" + "path/filepath" + "testing" + + "github.com/notaryproject/notation-go/dir" +) + +var trustStore = NewX509TrustStore(dir.NewSysFS(filepath.FromSlash("../testdata/"))) + +// TestLoadTrustStore tests a valid trust store +func TestLoadValidTrustStore(t *testing.T) { + certs, err := trustStore.GetCertificates(context.Background(), "ca", "valid-trust-store") + if err != nil { + t.Fatalf("could not get certificates from trust store. %q", err) + } + if len(certs) != 3 { + t.Fatalf("unexpected number of certificates in the trust store, expected: %d, got: %d", 3, len(certs)) + } +} + +// TestLoadValidTrustStoreWithSelfSignedSigningCertificate tests a valid trust store with self-signed signing certificate +func TestLoadValidTrustStoreWithSelfSignedSigningCertificate(t *testing.T) { + certs, err := trustStore.GetCertificates(context.Background(), "ca", "valid-trust-store-self-signed") + if err != nil { + t.Fatalf("could not get certificates from trust store. %q", err) + } + if len(certs) != 1 { + t.Fatalf("unexpected number of certificates in the trust store, expected: %d, got: %d", 1, len(certs)) + } +} + +func TestLoadTrustStoreWithInvalidCerts(t *testing.T) { + failurePath := filepath.FromSlash("../testdata/truststore/x509/ca/trust-store-with-invalid-certs/invalid") + _, err := trustStore.GetCertificates(context.Background(), "ca", "trust-store-with-invalid-certs") + if err == nil || err.Error() != fmt.Sprintf("error while reading certificates from %q: x509: malformed certificate", failurePath) { + t.Fatalf("invalid certs should return error : %q", err) + } +} + +func TestLoadTrustStoreWithLeafCerts(t *testing.T) { + failurePath := filepath.FromSlash("../testdata/truststore/x509/ca/trust-store-with-leaf-certs/non-ca.crt") + _, err := trustStore.GetCertificates(context.Background(), "ca", "trust-store-with-leaf-certs") + if err == nil || err.Error() != fmt.Sprintf("certificate with subject \"CN=wabbit-networks.io,O=Notary,L=Seattle,ST=WA,C=US\" from file %q is not a CA certificate or self-signed signing certificate", failurePath) { + t.Fatalf("leaf cert in a trust store should return error : %q", err) + } +} + +func TestLoadTrustStoreWithLeafCertsInSingleFile(t *testing.T) { + failurePath := filepath.FromSlash("../testdata/truststore/x509/ca/trust-store-with-leaf-certs-in-single-file/RootAndLeafCerts.crt") + _, err := trustStore.GetCertificates(context.Background(), "ca", "trust-store-with-leaf-certs-in-single-file") + if err == nil || err.Error() != fmt.Sprintf("certificate with subject \"CN=wabbit-networks.io,O=Notary,L=Seattle,ST=WA,C=US\" from file %q is not a CA certificate or self-signed signing certificate", failurePath) { + t.Fatalf("leaf cert in a trust store should return error : %q", err) + } +} diff --git a/verification/verifier_test.go b/verification/verifier_test.go index 145cda71..76cf43e9 100644 --- a/verification/verifier_test.go +++ b/verification/verifier_test.go @@ -10,7 +10,7 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-go/dir" notation "github.com/notaryproject/notation-go/internal" - "github.com/notaryproject/notation-go/internal/mock" + mock "github.com/notaryproject/notation-go/internal/mock_origin" "github.com/notaryproject/notation-go/internal/registry" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" From f8f1b59f88c868834db912a34b10f0834c335ea5 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 8 Nov 2022 14:37:08 +0800 Subject: [PATCH 03/28] update Signed-off-by: Patrick Zheng --- .../ca/trust-store-with-invalid-certs/invalid | 1 + .../RootAndLeafCerts.crt | 43 ++++++++++++++++++ .../GlobalSignRootCA.crt | 22 +++++++++ .../ca/trust-store-with-leaf-certs/non-ca.crt | 21 +++++++++ .../valid-trust-store_SYMLINK | 1 + .../valid-trust-store-2/GlobalSign.der | Bin 0 -> 867 bytes .../valid-trust-store-2/GlobalSignRootCA.crt | 21 +++++++++ 7 files changed, 109 insertions(+) create mode 100644 verification/testdata/truststore/x509/ca/trust-store-with-invalid-certs/invalid create mode 100644 verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs-in-single-file/RootAndLeafCerts.crt create mode 100644 verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/GlobalSignRootCA.crt create mode 100644 verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/non-ca.crt create mode 100644 verification/testdata/truststore/x509/ca/valid-trust-store_SYMLINK/valid-trust-store_SYMLINK create mode 100644 verification/testdata/truststore/x509/signingAuthority/valid-trust-store-2/GlobalSign.der create mode 100644 verification/testdata/truststore/x509/signingAuthority/valid-trust-store-2/GlobalSignRootCA.crt diff --git a/verification/testdata/truststore/x509/ca/trust-store-with-invalid-certs/invalid b/verification/testdata/truststore/x509/ca/trust-store-with-invalid-certs/invalid new file mode 100644 index 00000000..9977a283 --- /dev/null +++ b/verification/testdata/truststore/x509/ca/trust-store-with-invalid-certs/invalid @@ -0,0 +1 @@ +invalid diff --git a/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs-in-single-file/RootAndLeafCerts.crt b/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs-in-single-file/RootAndLeafCerts.crt new file mode 100644 index 00000000..e6c38cfb --- /dev/null +++ b/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs-in-single-file/RootAndLeafCerts.crt @@ -0,0 +1,43 @@ +-----BEGIN CERTIFICATE----- +MIIDejCCAmKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEe +MBwGA1UEAxMVd2FiYml0LW5ldHdvcmtzLmlvIENBMB4XDTIyMDkyMDA2MzExM1oX +DTIyMDkyMTA2MzExM1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD +VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1u +ZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiZp5O+ +6YtaNO5GbWaZUxvJPXktJ7k7LBX5G/Kn6eh9JkJln1agqbax9MRDB/5YCdQBKMBq +NE2wYIwmCs7ArFU5DxvRhoBnCGLjcsIZ9pfaZ6lBppEvxMmUAYDmgjze0J13PwRp +WAZMfBlisZnJAWokgE5sWtggUXURyFk67H0R+4sWlm8SSZOiJCA/e0bYPCHTfFA/ +2zg6koNRSwvI6zvftGnnJ9ny0BTuGOjZ6lDfIX5awFrgRdO8wmwejo4oJ45tUotF +/Rt/yHkmjdGhONbJjcMLf9AIyVwMHg6t6mj2SYbHqzIyTcpjk90HgeiU5eS5JMqj +Jkug5U9XrGGCqIcCAwEAAaNIMEYwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG +CCsGAQUFBwMDMB8GA1UdIwQYMBaAFLAy4Il5S9zOd/AMWF8hATmldAjYMA0GCSqG +SIb3DQEBCwUAA4IBAQBLYBnSuMNCzzLmeqH/wBr6kKUtF10AN9VF8/3iZW8iCj4B +Bx7VDq7iZR/G9UTLsWdZqkkxnOGu4QffBHz2Lc1v9D923EEPDAP5mJYvUchvdXYT +lmyQr9QEjRC6IFhlBB27Bi207QJ8UxYgmbseQ3FQFE16Usdmlg9iWDn5tx/DZn9/ +yUd81yKKYp2uLx0x2sQDJh61QSZB6jtzjN7w4Xax2NViabLaH7raMrDbIqigkXJh +iXG9fWx1Ax7S3dJVIglbZGPgYDW14Ass40gs8vcOBg8CwszrKiEuwp20d12Ky87/ +0pLsOWJmcNyXbd3gztX01N1frSEbvTBJNI9E/jmI +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDjzCCAnegAwIBAgIBATANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEe +MBwGA1UEAxMVd2FiYml0LW5ldHdvcmtzLmlvIENBMB4XDTIyMDkyMDA2MzExM1oX +DTIyMTAyMDA2MzExM1owXTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD +VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxHjAcBgNVBAMTFXdhYmJpdC1u +ZXR3b3Jrcy5pbyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNM +3dUToC4TyegGMw47ax9aZt13pQgTeV7xZbVsOmZiv/8gZ9tEZWgQbvBJrWUH8y4o +eQLCVQOTESNP2TSyTqizNtG1ex6YfSpWKSqUkfGX2II9xCX8hNXZqTphAjrGGf2Z +EOLRIIkbhjkuiAR+7q4TF/KJhdfYD1HQBJ2PF92egV5JEZTrxIjVIi+WK19VKSwx +m7oFiijve4VPaQYQnWgj0dk+Tn9cMB/OMX6cszoJbn98ogQIvWaY3dd1qba4uGJ9 +vmkNKDJcUd1PbkaVlikXC4UM+PxXy7/ZvSihOXurAPIChS6JgWC8Ru2vxm9SC+BN +5J/hr92W2TdsrvLkrc8CAwEAAaNaMFgwDgYDVR0PAQH/BAQDAgIEMBMGA1UdJQQM +MAoGCCsGAQUFBwMDMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFLAy4Il5 +S9zOd/AMWF8hATmldAjYMA0GCSqGSIb3DQEBCwUAA4IBAQCTf6GbT5Z0x5ciNr9i +8i+QsIAg7ZHzv5RLLJuocGcKwbdi+btU6BPl/X4U5ZB6OArv4oiyPSbECoxkgGRq +cj+mfzXdm/3jEyRskHDfoxcJFYmcBsEykS7DoLYEy5HxgKSaGOLl4dMWbbj/E8mR +e9XC5ruvPNZX52pQMqSqUUTYlbR4YQojsp7ShcLLD/Iea90wXk44+wHAKNFpwkN1 +h5JMlYm+jKkol6u/Nmd3vNqhzrL91ZLPVtSWpfsBxh7l4BsDns2uPl+/fgCav9MJ +jUkWJbEaDPY5bSbHDhCbxMO37VbvkkFUvz7lfKAkXj6DnkPzMj3++KTFNdw3fJ4+ +WzLe +-----END CERTIFICATE----- diff --git a/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/GlobalSignRootCA.crt b/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/GlobalSignRootCA.crt new file mode 100644 index 00000000..c26fd442 --- /dev/null +++ b/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/GlobalSignRootCA.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjzCCAnegAwIBAgIBATANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEe +MBwGA1UEAxMVd2FiYml0LW5ldHdvcmtzLmlvIENBMB4XDTIyMDkyMDA2MzExM1oX +DTIyMTAyMDA2MzExM1owXTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD +VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxHjAcBgNVBAMTFXdhYmJpdC1u +ZXR3b3Jrcy5pbyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNM +3dUToC4TyegGMw47ax9aZt13pQgTeV7xZbVsOmZiv/8gZ9tEZWgQbvBJrWUH8y4o +eQLCVQOTESNP2TSyTqizNtG1ex6YfSpWKSqUkfGX2II9xCX8hNXZqTphAjrGGf2Z +EOLRIIkbhjkuiAR+7q4TF/KJhdfYD1HQBJ2PF92egV5JEZTrxIjVIi+WK19VKSwx +m7oFiijve4VPaQYQnWgj0dk+Tn9cMB/OMX6cszoJbn98ogQIvWaY3dd1qba4uGJ9 +vmkNKDJcUd1PbkaVlikXC4UM+PxXy7/ZvSihOXurAPIChS6JgWC8Ru2vxm9SC+BN +5J/hr92W2TdsrvLkrc8CAwEAAaNaMFgwDgYDVR0PAQH/BAQDAgIEMBMGA1UdJQQM +MAoGCCsGAQUFBwMDMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFLAy4Il5 +S9zOd/AMWF8hATmldAjYMA0GCSqGSIb3DQEBCwUAA4IBAQCTf6GbT5Z0x5ciNr9i +8i+QsIAg7ZHzv5RLLJuocGcKwbdi+btU6BPl/X4U5ZB6OArv4oiyPSbECoxkgGRq +cj+mfzXdm/3jEyRskHDfoxcJFYmcBsEykS7DoLYEy5HxgKSaGOLl4dMWbbj/E8mR +e9XC5ruvPNZX52pQMqSqUUTYlbR4YQojsp7ShcLLD/Iea90wXk44+wHAKNFpwkN1 +h5JMlYm+jKkol6u/Nmd3vNqhzrL91ZLPVtSWpfsBxh7l4BsDns2uPl+/fgCav9MJ +jUkWJbEaDPY5bSbHDhCbxMO37VbvkkFUvz7lfKAkXj6DnkPzMj3++KTFNdw3fJ4+ +WzLe +-----END CERTIFICATE----- diff --git a/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/non-ca.crt b/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/non-ca.crt new file mode 100644 index 00000000..74bc7b00 --- /dev/null +++ b/verification/testdata/truststore/x509/ca/trust-store-with-leaf-certs/non-ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDejCCAmKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEe +MBwGA1UEAxMVd2FiYml0LW5ldHdvcmtzLmlvIENBMB4XDTIyMDkyMDA2MzExM1oX +DTIyMDkyMTA2MzExM1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD +VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1u +ZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiZp5O+ +6YtaNO5GbWaZUxvJPXktJ7k7LBX5G/Kn6eh9JkJln1agqbax9MRDB/5YCdQBKMBq +NE2wYIwmCs7ArFU5DxvRhoBnCGLjcsIZ9pfaZ6lBppEvxMmUAYDmgjze0J13PwRp +WAZMfBlisZnJAWokgE5sWtggUXURyFk67H0R+4sWlm8SSZOiJCA/e0bYPCHTfFA/ +2zg6koNRSwvI6zvftGnnJ9ny0BTuGOjZ6lDfIX5awFrgRdO8wmwejo4oJ45tUotF +/Rt/yHkmjdGhONbJjcMLf9AIyVwMHg6t6mj2SYbHqzIyTcpjk90HgeiU5eS5JMqj +Jkug5U9XrGGCqIcCAwEAAaNIMEYwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG +CCsGAQUFBwMDMB8GA1UdIwQYMBaAFLAy4Il5S9zOd/AMWF8hATmldAjYMA0GCSqG +SIb3DQEBCwUAA4IBAQBLYBnSuMNCzzLmeqH/wBr6kKUtF10AN9VF8/3iZW8iCj4B +Bx7VDq7iZR/G9UTLsWdZqkkxnOGu4QffBHz2Lc1v9D923EEPDAP5mJYvUchvdXYT +lmyQr9QEjRC6IFhlBB27Bi207QJ8UxYgmbseQ3FQFE16Usdmlg9iWDn5tx/DZn9/ +yUd81yKKYp2uLx0x2sQDJh61QSZB6jtzjN7w4Xax2NViabLaH7raMrDbIqigkXJh +iXG9fWx1Ax7S3dJVIglbZGPgYDW14Ass40gs8vcOBg8CwszrKiEuwp20d12Ky87/ +0pLsOWJmcNyXbd3gztX01N1frSEbvTBJNI9E/jmI +-----END CERTIFICATE----- diff --git a/verification/testdata/truststore/x509/ca/valid-trust-store_SYMLINK/valid-trust-store_SYMLINK b/verification/testdata/truststore/x509/ca/valid-trust-store_SYMLINK/valid-trust-store_SYMLINK new file mode 100644 index 00000000..ca626091 --- /dev/null +++ b/verification/testdata/truststore/x509/ca/valid-trust-store_SYMLINK/valid-trust-store_SYMLINK @@ -0,0 +1 @@ +ca/valid-trust-store \ No newline at end of file diff --git a/verification/testdata/truststore/x509/signingAuthority/valid-trust-store-2/GlobalSign.der b/verification/testdata/truststore/x509/signingAuthority/valid-trust-store-2/GlobalSign.der new file mode 100644 index 0000000000000000000000000000000000000000..232c4b6121f7236eb429572c591127e8aa60166f GIT binary patch literal 867 zcmXqLVvaXxVsc-=%*4pV#LdD01dNIi!5oVWc-c6$+C196^D;7WvoaX?7%CXZu`!3T za0`pO=j10P<^*S^=P3l`=a(orJ1XcZ1Q{C&8wi3_a0zoERKNt8kp&Ip#CZ)Y4U7#f z3=KdaN}SgSnM-Bcni!Rky~D`Lz}&>h&tTBR$i>ve$jER;wQNEqTZNL?*8|PlT25)q z`^#D;cyw(?(H}P^=i{5Y=CZ`AoYwPxn9$_*FlaSTqkBQl-IR;3zv?XJZ?fglUN`;v zHjy@g%H7t&4dp!?4?QnsCF#q@{hF3>zf*mx#eBBwb|+7(Me-Kk+i>Eg8eg;MvG>v4 zmsk=`c`noVmTCR%^a+iLPv>?ehMTV`5xG?_`;jk@3_*EunPN{PO=3Mc%Jd@cgsZLDb_S zlVPG{+>Yt**OqTjnN_tv{-E&t*-5{7a~_0bimhb6mG`oFa$(uA%+@AxCT2zk#>Gws z4hDR{_><*lWc<&<0!$HX2K*qtFo@4;zzn1eWI+OaEMhDo{U5yRSLnH_tn&@{l{~Ba z-lMzHdyqpJm}r2Z%*f#FaQi^Os(&YV-hZDK;_A738Uv@}n$5y(Z5r&xr?Q`w?A*nm zyKV{B<*y$<@^|eoPWNg)?owUxV0~UrKC@<@v8C(bz9&1wb5{Kkn)W96nC=smoSjpW zf8PntNDs4X-f`Yk@$kuvf9Gx;3SM<)Lf6X=v2Hz6?^Z=gm=bbRTvM6jq*l?z>U;6vh=ZpX(UQTUo1KYI8Vet Date: Tue, 8 Nov 2022 15:03:51 +0800 Subject: [PATCH 04/28] refactored notation Signed-off-by: Patrick Zheng --- internal/policy/policy.go | 64 ------------- internal/policy/policy_test.go | 71 --------------- notation/notation.go | 3 +- verification/trustpolicy/trustpolicy.go | 94 +++++++++++++++++--- verification/trustpolicy/trustpolicy_test.go | 45 ++++++++++ 5 files changed, 128 insertions(+), 149 deletions(-) delete mode 100644 internal/policy/policy.go delete mode 100644 internal/policy/policy_test.go diff --git a/internal/policy/policy.go b/internal/policy/policy.go deleted file mode 100644 index 33b80461..00000000 --- a/internal/policy/policy.go +++ /dev/null @@ -1,64 +0,0 @@ -package policy - -import ( - "fmt" - "strings" - - "github.com/notaryproject/notation-go/internal/common" - "github.com/notaryproject/notation-go/verification/trustpolicy" -) - -// getApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy statement that applies to the given -// registry URI. If no applicable trust policy is found, returns an error -// see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#selecting-a-trust-policy-based-on-artifact-uri -func GetApplicableTrustPolicy(trustPolicyDoc *trustpolicy.Document, artifactReference string) (*trustpolicy.TrustPolicy, error) { - - artifactPath, err := getArtifactPathFromReference(artifactReference) - if err != nil { - return nil, err - } - - var wildcardPolicy *trustpolicy.TrustPolicy - var applicablePolicy *trustpolicy.TrustPolicy - for _, policyStatement := range trustPolicyDoc.TrustPolicies { - if common.IsPresent(common.Wildcard, policyStatement.RegistryScopes) { - wildcardPolicy = deepCopy(&policyStatement) // we need to deep copy because we can't use the loop variable address. see https://stackoverflow.com/a/45967429 - } else if common.IsPresent(artifactPath, policyStatement.RegistryScopes) { - applicablePolicy = deepCopy(&policyStatement) - } - } - - if applicablePolicy != nil { - // a policy with exact match for registry URI 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", artifactReference) - } -} - -// deepCopy returns a pointer to the deeply copied TrustPolicy -func deepCopy(t *trustpolicy.TrustPolicy) *trustpolicy.TrustPolicy { - return &trustpolicy.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 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 := common.ValidateRegistryScopeFormat(artifactPath); err != nil { - return "", err - } - return artifactPath, nil -} diff --git a/internal/policy/policy_test.go b/internal/policy/policy_test.go deleted file mode 100644 index bea6d7ae..00000000 --- a/internal/policy/policy_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package policy - -import ( - "fmt" - "testing" - - "github.com/notaryproject/notation-go/verification/trustpolicy" -) - -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 -} - -// 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 = trustpolicy.SignatureVerification{VerificationLevel: "strict"} - - policyDoc.TrustPolicies = []trustpolicy.TrustPolicy{ - policyStatement, - } - // existing Registry Scope - policy, err := GetApplicableTrustPolicy(&policyDoc, 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 = GetApplicableTrustPolicy(&policyDoc, "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" { - 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 = trustpolicy.SignatureVerification{VerificationLevel: "skip"} - - policyDoc.TrustPolicies = []trustpolicy.TrustPolicy{ - policyStatement, - wildcardStatement, - } - policy, err = GetApplicableTrustPolicy(&policyDoc, "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\"") - } -} diff --git a/notation/notation.go b/notation/notation.go index 677a1f2a..57414c35 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -12,7 +12,6 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/timestamp" - "github.com/notaryproject/notation-go/internal/policy" "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation-go/verification/trustpolicy" "github.com/opencontainers/go-digest" @@ -196,7 +195,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if err != nil { return Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} } - trustPolicy, err := policy.GetApplicableTrustPolicy(trustpolicyDoc, artifactRef) + trustPolicy, err := trustpolicy.GetApplicableTrustPolicy(trustpolicyDoc, artifactRef) if err != nil { return Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} } diff --git a/verification/trustpolicy/trustpolicy.go b/verification/trustpolicy/trustpolicy.go index 245e6c98..dbba67f7 100644 --- a/verification/trustpolicy/trustpolicy.go +++ b/verification/trustpolicy/trustpolicy.go @@ -9,13 +9,16 @@ import ( "github.com/notaryproject/notation-go/verification/truststore" ) -// ValidationType is an enum for signature verification types such as Integrity, Authenticity, etc. +// ValidationType is an enum for signature verification types such as Integrity, +// Authenticity, etc. type ValidationType string -// ValidationAction is an enum for signature verification actions such as Enforced, Logged, Skipped. +// ValidationAction is an enum for signature verification actions such as +// Enforced, Logged, Skipped. type ValidationAction string -// VerificationLevel encapsulates the signature verification preset and it's actions for each verification type +// VerificationLevel encapsulates the signature verification preset and its +// actions for each verification type type VerificationLevel struct { Name string Enforcement map[ValidationType]ValidationAction @@ -160,7 +163,8 @@ func (policyDoc *Document) Validate() error { return fmt.Errorf("trust policy statement %q uses invalid signatureVerification value %q", statement.Name, statement.SignatureVerification.VerificationLevel) } - // Any signature verification other than "skip" needs a trust store and trusted identities + // 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) @@ -199,8 +203,8 @@ func (policyDoc *Document) Validate() error { return nil } -// GetVerificationLevel returns VerificationLevel struct for the given SignatureVerification struct -// throws error if SignatureVerification is invalid +// GetVerificationLevel returns VerificationLevel struct for the given +// SignatureVerification struct throws error if SignatureVerification is invalid func GetVerificationLevel(signatureVerification SignatureVerification) (*VerificationLevel, error) { var baseLevel *VerificationLevel for _, l := range VerificationLevels { @@ -226,7 +230,8 @@ func GetVerificationLevel(signatureVerification SignatureVerification) (*Verific Enforcement: make(map[ValidationType]ValidationAction), } - // populate the custom verification level with the base verification settings + // populate the custom verification level with the base verification + // settings for k, v := range baseLevel.Enforcement { customVerificationLevel.Enforcement[k] = v } @@ -266,7 +271,8 @@ func GetVerificationLevel(signatureVerification SignatureVerification) (*Verific return customVerificationLevel, nil } -// validateTrustStore validates if the policy statement is following the Notary V2 spec rules for truststores +// validateTrustStore validates if the policy statement is following the +// Notary V2 spec rules for truststores func validateTrustStore(statement TrustPolicy) error { for _, trustStore := range statement.TrustStores { i := strings.Index(trustStore, ":") @@ -278,10 +284,12 @@ func validateTrustStore(statement TrustPolicy) error { return nil } -// validateTrustedIdentities validates if the policy statement is following the Notary V2 spec rules for trusted identities +// validateTrustedIdentities validates if the policy statement is following the +// Notary V2 spec rules for trusted identities func validateTrustedIdentities(statement TrustPolicy) error { - // If there is a wildcard in trusted identies, there shouldn't be any other identities + // If there is a wildcard in trusted identies, there shouldn't be any other + //identities if len(statement.TrustedIdentities) > 1 && common.IsPresent(common.Wildcard, statement.TrustedIdentities) { 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) } @@ -322,7 +330,8 @@ func validateTrustedIdentities(statement TrustPolicy) error { return nil } -// validateRegistryScopes validates if the policy document is following the Notary V2 spec rules for registry scopes +// validateRegistryScopes validates if the policy document is following the +// Notary V2 spec rules for registry scopes func validateRegistryScopes(policyDoc *Document) error { registryScopeCount := make(map[string]int) @@ -367,7 +376,8 @@ func validateOverlappingDNs(policyName string, parsedDNs []common.ParsedDN) erro return nil } -// isValidTrustStoreType returns true if the given string is a valid truststore.Type, otherwise false. +// isValidTrustStoreType returns true if the given string is a valid +// truststore.Type, otherwise false. func isValidTrustStoreType(s string) bool { for _, p := range truststore.Types { if s == string(p) { @@ -376,3 +386,63 @@ func isValidTrustStoreType(s string) bool { } return false } + +// GetApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy +// statement that applies to the given registry URI. If no applicable trust +// policy is found, returns an error +// see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#selecting-a-trust-policy-based-on-artifact-uri +func GetApplicableTrustPolicy(trustPolicyDoc *Document, 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 common.IsPresent(common.Wildcard, policyStatement.RegistryScopes) { + // we need to deep copy because we can't use the loop variable + // address. see https://stackoverflow.com/a/45967429 + wildcardPolicy = deepCopy(&policyStatement) + } else if common.IsPresent(artifactPath, policyStatement.RegistryScopes) { + applicablePolicy = deepCopy(&policyStatement) + } + } + + if applicablePolicy != nil { + // a policy with exact match for registry URI 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", artifactReference) + } +} + +// deepCopy returns a pointer to the deeply copied TrustPolicy +func deepCopy(t *TrustPolicy) *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 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 := common.ValidateRegistryScopeFormat(artifactPath); err != nil { + return "", err + } + return artifactPath, nil +} diff --git a/verification/trustpolicy/trustpolicy_test.go b/verification/trustpolicy/trustpolicy_test.go index b8d4ee54..b08b106c 100644 --- a/verification/trustpolicy/trustpolicy_test.go +++ b/verification/trustpolicy/trustpolicy_test.go @@ -1,6 +1,7 @@ package trustpolicy import ( + "fmt" "strconv" "testing" ) @@ -444,3 +445,47 @@ 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 := GetApplicableTrustPolicy(&policyDoc, 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 = GetApplicableTrustPolicy(&policyDoc, "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" { + 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"} + + policyDoc.TrustPolicies = []TrustPolicy{ + policyStatement, + wildcardStatement, + } + policy, err = GetApplicableTrustPolicy(&policyDoc, "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\"") + } +} From 1f5a10cc506bbbcd1fc8090c6a023becc8d9a702 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 8 Nov 2022 19:12:48 +0800 Subject: [PATCH 05/28] updates Signed-off-by: Patrick Zheng --- go.mod | 5 +- go.sum | 12 +- internal/registry/repository.go | 54 +++--- notation/notation.go | 62 ++++--- notation/notation_test.go | 10 +- registry/interface.go | 24 +++ registry/repository.go | 146 ++++++++++++++-- registry/repositoryClient.go | 163 ------------------ ...itoryClient_test.go => repository_test.go} | 14 -- 9 files changed, 231 insertions(+), 259 deletions(-) create mode 100644 registry/interface.go delete mode 100644 registry/repositoryClient.go rename registry/{repositoryClient_test.go => repository_test.go} (97%) diff --git a/go.mod b/go.mod index 3b61b04a..9f2db601 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/go-ldap/ldap/v3 v3.4.4 github.com/notaryproject/notation-core-go v0.2.0-beta.1 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.0.2 + github.com/opencontainers/image-spec v1.1.0-rc2 github.com/oras-project/artifacts-spec v1.0.0-rc.2 github.com/veraison/go-cose v1.0.0-rc.1.0.20220824135457-9d2fab636b83 - oras.land/oras-go/v2 v2.0.0-rc.3 + oras.land/oras-go/v2 v2.0.0-rc.4 ) require ( @@ -17,7 +17,6 @@ require ( github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect - github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect diff --git a/go.sum b/go.sum index 685070c7..38b7b6f6 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= @@ -12,12 +12,10 @@ github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQA github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/notaryproject/notation-core-go v0.2.0-beta.1 h1:8tFxNycWCcPLti9ZYST5kjkX2wMXtX9YPvMjiBAQ1tA= github.com/notaryproject/notation-core-go v0.2.0-beta.1/go.mod h1:s8DZptmN1rZS0tBLTPt/w+d4o6eAcGWTYYJlXaJhQ4U= -github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 h1:Oumw+lPnO8qNLTY2mrqPJZMoGExLi/0h/DdikoLTXVU= -github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86/go.mod h1:aA4vdXRS8E1TG7pLZOz85InHi3BiPdErh8IpJN6E0x4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/oras-project/artifacts-spec v1.0.0-rc.2 h1:9SMCNSxkJEHqWGDiMCuy6TXHgvjgwXGdXZZGXLKQvVE= github.com/oras-project/artifacts-spec v1.0.0-rc.2/go.mod h1:Xch2aLzSwtkhbFFN6LUzTfLtukYvMMdXJ4oZ8O7BOdc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -43,5 +41,5 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.0.0-rc.3 h1:O4GeIwJ9Ge7rbCkqa/M7DLrL55ww+ZEc+Rhc63OYitU= -oras.land/oras-go/v2 v2.0.0-rc.3/go.mod h1:PrY+cCglzK/DrQoJUtxbYVbL94ZHecVS3eJR01RglpE= +oras.land/oras-go/v2 v2.0.0-rc.4 h1:hg/R2znUQ1+qd43gRmL16VeX1GIZ8hQlLalBjYhhKSk= +oras.land/oras-go/v2 v2.0.0-rc.4/go.mod h1:YGHvWBGuqRlZgUyXUIoKsR3lcuCOb3DAtG0SEsEw1iY= diff --git a/internal/registry/repository.go b/internal/registry/repository.go index f59f9d37..f1f19c75 100644 --- a/internal/registry/repository.go +++ b/internal/registry/repository.go @@ -55,7 +55,7 @@ func (c *RepositoryClient) ListSignatureManifests(ctx context.Context, manifestD var signatureManifests []SignatureManifest if err := c.Repository.Referrers(ctx, ocispec.Descriptor{ Digest: manifestDigest, - }, ArtifactTypeNotation, func(referrers []artifactspec.Descriptor) error { + }, ArtifactTypeNotation, func(referrers []ocispec.Descriptor) error { for _, desc := range referrers { if desc.MediaType != artifactspec.MediaTypeArtifactManifest { continue @@ -98,13 +98,13 @@ func (c *RepositoryClient) PutSignatureManifest(ctx context.Context, signature [ return notation.Descriptor{}, SignatureManifest{}, err } - manifestDesc, err := c.uploadSignatureManifest(ctx, artifactDescriptorFromNotation(subjectManifest), signatureDesc, annotations) + manifestDesc, err := c.uploadSignatureManifest(ctx, ociDescriptorFromNotation(subjectManifest), signatureDesc, annotations) if err != nil { return notation.Descriptor{}, SignatureManifest{}, err } signatureManifest := SignatureManifest{ - Blob: notationDescriptorFromArtifact(signatureDesc), + Blob: notationDescriptorFromOCI(signatureDesc), Annotations: annotations, } return notationDescriptorFromOCI(manifestDesc), signatureManifest, nil @@ -137,40 +137,32 @@ func (c *RepositoryClient) getArtifactManifest(ctx context.Context, manifestDige } // uploadSignature uploads the signature to the registry -func (c *RepositoryClient) uploadSignature(ctx context.Context, signature []byte, signatureMediaType string) (artifactspec.Descriptor, error) { +// uploadSignature uploads the signature envelope blob to the registry +func (c *RepositoryClient) uploadSignature(ctx context.Context, blob []byte, mediaType string) (ocispec.Descriptor, error) { desc := ocispec.Descriptor{ - MediaType: signatureMediaType, - Digest: digest.FromBytes(signature), - Size: int64(len(signature)), + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), } - if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(signature)); err != nil { - return artifactspec.Descriptor{}, err + if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(blob)); err != nil { + return ocispec.Descriptor{}, err } - return artifactDescriptorFromOCI(desc), nil + return desc, nil } // uploadSignatureManifest uploads the signature manifest to the registry -func (c *RepositoryClient) uploadSignatureManifest(ctx context.Context, subjectManifest, signatureDesc artifactspec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { - opts := oras.PackArtifactOptions{ - Subject: &subjectManifest, +// uploadSignatureManifest uploads the signature manifest to the registry +func (c *RepositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc ocispec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { + opts := oras.PackOptions{ + Subject: &subject, ManifestAnnotations: annotations, } - return oras.PackArtifact( - ctx, - c.Repository.Manifests(), - ArtifactTypeNotation, - []artifactspec.Descriptor{signatureDesc}, - opts, - ) -} - -func artifactDescriptorFromNotation(desc notation.Descriptor) artifactspec.Descriptor { - return artifactspec.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, + manifestDesc, err := oras.Pack(ctx, c.Repository.Manifests(), ArtifactTypeNotation, []ocispec.Descriptor{blobDesc}, opts) + if err != nil { + return ocispec.Descriptor{}, err } + return manifestDesc, nil } func notationDescriptorFromArtifact(desc artifactspec.Descriptor) notation.Descriptor { @@ -181,16 +173,16 @@ func notationDescriptorFromArtifact(desc artifactspec.Descriptor) notation.Descr } } -func artifactDescriptorFromOCI(desc ocispec.Descriptor) artifactspec.Descriptor { - return artifactspec.Descriptor{ +func notationDescriptorFromOCI(desc ocispec.Descriptor) notation.Descriptor { + return notation.Descriptor{ MediaType: desc.MediaType, Digest: desc.Digest, Size: desc.Size, } } -func notationDescriptorFromOCI(desc ocispec.Descriptor) notation.Descriptor { - return notation.Descriptor{ +func ociDescriptorFromNotation(desc notation.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ MediaType: desc.MediaType, Digest: desc.Digest, Size: desc.Size, diff --git a/notation/notation.go b/notation/notation.go index 57414c35..bd022ba3 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "reflect" "strings" "time" @@ -18,7 +19,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -const AnnotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" +const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" // Descriptor describes the artifact that needs to be signed. type Descriptor struct { @@ -37,11 +38,19 @@ type Descriptor struct { // Equal reports whether d and t points to the same content. func (d Descriptor) Equal(t Descriptor) bool { - return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size + return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size && reflect.DeepEqual(d.Annotations, t.Annotations) } // SignOptions contains parameters for Signer.Sign. type SignOptions struct { + // Reference of the artifact that needs to be signed. + ArtifactReference string + + // SignatureMediaType is the envelope type of the signature. + // Currently both `application/jose+json` and `application/cose` are + // supported. + SignatureMediaType string + // Expiry identifies the expiration time of the resulted signature. Expiry time.Time @@ -62,7 +71,7 @@ type SignOptions struct { } // Payload describes the content that gets signed. -type Payload struct { +type payload struct { TargetArtifact Descriptor `json:"targetArtifact"` } @@ -78,13 +87,13 @@ type Signer interface { // Sign signs the artifact in the remote registry and push the signature to the // remote. // The descriptor of the sign content is returned upon sucessful signing. -func Sign(ctx context.Context, signer Signer, repo registry.Repository, reference string, envelopeMediaType string, opts SignOptions) (Descriptor, error) { - ociDesc, err := repo.Resolve(ctx, reference) +func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts SignOptions) (Descriptor, error) { + ociDesc, err := repo.Resolve(ctx, opts.ArtifactReference) if err != nil { return Descriptor{}, err } desc := notationDescriptorFromOCI(ociDesc) - sig, signerInfo, err := signer.Sign(ctx, desc, envelopeMediaType, opts) + sig, signerInfo, err := signer.Sign(ctx, desc, opts.SignatureMediaType, opts) if err != nil { return Descriptor{}, err } @@ -92,7 +101,7 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, referenc if err != nil { return Descriptor{}, err } - _, _, err = repo.PushSignature(ctx, sig, envelopeMediaType, ociDesc, annotations) + _, _, err = repo.PushSignature(ctx, sig, opts.SignatureMediaType, ociDesc, annotations) if err != nil { return Descriptor{}, err } @@ -102,19 +111,28 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, referenc // VerifyOptions contains parameters for Verifier.Verify. type VerifyOptions struct { + // ArtifactReference is the reference of the artifact that is been + // verified against to. ArtifactReference string + // SignatureMediaType is the envelope type of the signature. // Currently both `application/jose+json` and `application/cose` are // supported. SignatureMediaType string - PluginConfig map[string]string + + // PluginConfig is a map of plugin configs. + PluginConfig map[string]string + + // VerificationLevel encapsulates the signature verification preset and its + // actions for each verification type + VerificationLevel *trustpolicy.VerificationLevel } -// VerificationResult encapsulates the verification result (passed or failed) +// ValidationResult encapsulates the verification result (passed or failed) // for a verification type, including the desired verification action as // // specified in the trust policy -type VerificationResult struct { +type ValidationResult struct { // Success is set to true if the verification was successful Success bool // Type of verification that is performed @@ -126,9 +144,9 @@ type VerificationResult struct { Error error } -// VerificationOutcome encapsulates the SignerInfo (that includes the details of -// the digital signature) -// and results for each verification type that was performed +// VerificationOutcome encapsulates a signature blob's descriptor, its content, +// the verification level and results for each verification type that was +// performed. type VerificationOutcome struct { SignatureBlobDescriptor *ocispec.Descriptor // EnvelopeContent contains the details of the digital signature and @@ -139,7 +157,7 @@ type VerificationOutcome struct { VerificationLevel *trustpolicy.VerificationLevel // VerificationResults contains the verifications performed on the signature // and their results - VerificationResults []*VerificationResult + VerificationResults []*ValidationResult // SignedAnnotations contains arbitrary metadata relating to the target // artifact that was signed SignedAnnotations map[string]string @@ -151,7 +169,7 @@ type VerificationOutcome struct { type Verifier interface { // Verify verifies the signature blob and returns the verified descriptor // upon successful verification. - Verify(ctx context.Context, signature []byte, opts VerifyOptions, outcome *VerificationOutcome) (Descriptor, error) + Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, *VerificationOutcome, error) // TrustPolicyDocument gets the validated trust policy document. TrustPolicyDocument() (*trustpolicy.Document, error) @@ -205,6 +223,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op verificationOutcomes = append(verificationOutcomes, &VerificationOutcome{VerificationLevel: verificationLevel}) return Descriptor{}, verificationOutcomes, nil } + opts.VerificationLevel = verificationLevel // get signature manifests var success bool @@ -224,22 +243,19 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if err != nil { return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %s", sigBlobDesc.Digest, artifactRef, err.Error())} } - outcome := &VerificationOutcome{ - SignatureBlobDescriptor: &sigBlobDesc, - VerificationResults: []*VerificationResult{}, - VerificationLevel: verificationLevel, - } - _, err = verifier.Verify(ctx, sigBlob, opts, outcome) + _, outcome, err := verifier.Verify(ctx, sigBlob, opts) if err != nil { if outcome != nil && outcome.Error != nil { + outcome.SignatureBlobDescriptor = &sigBlobDesc verificationOutcomes = append(verificationOutcomes, outcome) } continue } + outcome.SignatureBlobDescriptor = &sigBlobDesc verificationOutcomes = append(verificationOutcomes, outcome) // artifact digest must match the digest from the signature payload - payload := &Payload{} + payload := &payload{} err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) if err != nil || !notationDescriptorFromOCI(artifactDescriptor).Equal(payload.TargetArtifact) { outcome.Error = fmt.Errorf("given digest %q does not match the digest %q present in the digital signature", artifactDescriptor.Digest.String(), payload.TargetArtifact.Digest.String()) @@ -283,7 +299,7 @@ func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, e if err != nil { return nil, err } - annotations[AnnotationX509ChainThumbprint] = string(val) + annotations[annotationX509ChainThumbprint] = string(val) return annotations, nil } diff --git a/notation/notation_test.go b/notation/notation_test.go index be6fc04e..c73b218d 100644 --- a/notation/notation_test.go +++ b/notation/notation_test.go @@ -121,11 +121,15 @@ type dummyVerifier struct { FailVerify bool } -func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts VerifyOptions, outcome *VerificationOutcome) (Descriptor, error) { +func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, *VerificationOutcome, error) { if v.FailVerify { - return Descriptor{}, errors.New("failed verify") + return Descriptor{}, nil, errors.New("failed verify") } - return Descriptor{}, nil + outcome := &VerificationOutcome{ + VerificationResults: []*ValidationResult{}, + VerificationLevel: opts.VerificationLevel, + } + return Descriptor{}, outcome, nil } func (v *dummyVerifier) TrustPolicyDocument() (*trustpolicy.Document, error) { diff --git a/registry/interface.go b/registry/interface.go new file mode 100644 index 00000000..27de5965 --- /dev/null +++ b/registry/interface.go @@ -0,0 +1,24 @@ +// Package registry provides Repository for remote signing and verification +package registry + +import ( + "context" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Repository provides registry functionalities for remote signing and +// verification. +type Repository interface { + // Resolve resolves a reference(tag or digest) to a manifest descriptor + Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) + // ListSignatures returns signature manifests filtered by fn given the + // artifact manifest descriptor + ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error + // FetchSignatureBlob returns signature envelope blob and descriptor given + // signature manifest descriptor + FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) + // PushSignature creates and uploads an signature manifest along with its + // linked signature envelope blob. + PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) +} diff --git a/registry/repository.go b/registry/repository.go index 7406e2eb..b6b91b0a 100644 --- a/registry/repository.go +++ b/registry/repository.go @@ -1,27 +1,28 @@ -// Package registry provides Repository for remote signing and verification package registry import ( + "bytes" "context" + "encoding/json" + "errors" + "fmt" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry/remote" ) -// Repository provides registry functionalities for remote signing and -// verification. -type Repository interface { - // Resolve resolves a reference(tag or digest) to a manifest descriptor - Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) - // ListSignatures returns signature manifests filtered by fn given the - // artifact manifest descriptor - ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error - // FetchSignatureBlob returns signature envelope blob and descriptor given - // signature manifest descriptor - FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) - // PushSignature creates and uploads an signature manifest along with its - // linked signature envelope blob. - PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) +const ( + maxBlobSizeLimit = 32 * 1024 * 1024 // 32 MiB + maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB +) + +// repositoryClient implements Repository +type repositoryClient struct { + remote.Repository } // NewRepository returns a new Repository @@ -30,3 +31,118 @@ func NewRepository(repo remote.Repository) Repository { Repository: repo, } } + +// Resolve resolves a reference(tag or digest) to a manifest descriptor +func (c *repositoryClient) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + return c.Repository.Resolve(ctx, reference) +} + +// ListSignatures returns signature manifests filtered by fn given the +// artifact manifest descriptor +func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error { + return c.Repository.Referrers(ctx, ocispec.Descriptor{ + Digest: desc.Digest, + }, ArtifactTypeNotation, func(referrers []ocispec.Descriptor) error { + var sigManifestDesc []ocispec.Descriptor + sigManifestDesc = append(sigManifestDesc, referrers...) + return fn(sigManifestDesc) + }) +} + +// FetchSignatureBlob returns signature envelope blob and descriptor given +// signature manifest descriptor +func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) { + sigManifest, err := c.getSignatureManifest(ctx, desc) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + if len(sigManifest.Blobs) == 0 { + return nil, ocispec.Descriptor{}, errors.New("signature manifest missing signature envelope blob") + } + sigDesc := ociDescriptorFromArtifact(sigManifest.Blobs[0]) + if sigDesc.Size > maxBlobSizeLimit { + return nil, ocispec.Descriptor{}, fmt.Errorf("signature blob too large: %d", sigDesc.Size) + } + sigBlob, err := content.FetchAll(ctx, c.Repository.Blobs(), sigDesc) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + return sigBlob, sigDesc, nil +} + +// PushSignature creates and uploads an signature manifest along with its +// linked signature envelope blob. Upon successful, PushSignature returns +// signature envelope blob and manifest descriptors. +func (c *repositoryClient) PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { + blobDesc, err = c.uploadSignature(ctx, blob, mediaType) + if err != nil { + return ocispec.Descriptor{}, ocispec.Descriptor{}, err + } + + manifestDesc, err = c.uploadSignatureManifest(ctx, subject, blobDesc, annotations) + if err != nil { + return ocispec.Descriptor{}, ocispec.Descriptor{}, err + } + + return blobDesc, manifestDesc, nil +} + +// getSignatureManifest returns signature manifest given signature manifest +// descriptor +func (c *repositoryClient) getSignatureManifest(ctx context.Context, sigManifestDesc ocispec.Descriptor) (*artifactspec.Manifest, error) { + + repo := c.Repository + repo.ManifestMediaTypes = []string{ + artifactspec.MediaTypeArtifactManifest, + } + store := repo.Manifests() + if sigManifestDesc.Size > maxManifestSizeLimit { + return &artifactspec.Manifest{}, fmt.Errorf("manifest too large: %d", sigManifestDesc.Size) + } + manifestJSON, err := content.FetchAll(ctx, store, sigManifestDesc) + if err != nil { + return &artifactspec.Manifest{}, err + } + + var sigManifest artifactspec.Manifest + err = json.Unmarshal(manifestJSON, &sigManifest) + if err != nil { + return &artifactspec.Manifest{}, err + } + return &sigManifest, nil +} + +// uploadSignature uploads the signature envelope blob to the registry +func (c *repositoryClient) uploadSignature(ctx context.Context, blob []byte, mediaType string) (ocispec.Descriptor, error) { + desc := ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(blob)); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil +} + +// uploadSignatureManifest uploads the signature manifest to the registry +func (c *repositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc ocispec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { + opts := oras.PackOptions{ + Subject: &subject, + ManifestAnnotations: annotations, + } + + manifestDesc, err := oras.Pack(ctx, c.Repository.Manifests(), ArtifactTypeNotation, []ocispec.Descriptor{blobDesc}, opts) + if err != nil { + return ocispec.Descriptor{}, err + } + return manifestDesc, nil +} + +func ociDescriptorFromArtifact(desc artifactspec.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} diff --git a/registry/repositoryClient.go b/registry/repositoryClient.go deleted file mode 100644 index 806cd68d..00000000 --- a/registry/repositoryClient.go +++ /dev/null @@ -1,163 +0,0 @@ -package registry - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - - "github.com/opencontainers/go-digest" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" - "oras.land/oras-go/v2" - "oras.land/oras-go/v2/content" - "oras.land/oras-go/v2/registry/remote" -) - -const ( - maxBlobSizeLimit = 32 * 1024 * 1024 // 32 MiB - maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB -) - -// repositoryClient implements Repository -type repositoryClient struct { - remote.Repository -} - -// Resolve resolves a reference(tag or digest) to a manifest descriptor -func (c *repositoryClient) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { - return c.Repository.Resolve(ctx, reference) -} - -// ListSignatures returns signature manifests filtered by fn given the -// artifact manifest descriptor -func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error { - if err := c.Repository.Referrers(ctx, ocispec.Descriptor{ - Digest: desc.Digest, - }, ArtifactTypeNotation, func(referrers []artifactspec.Descriptor) error { - var sigManifestDesc []ocispec.Descriptor - for _, referrer := range referrers { - sigManifestDesc = append(sigManifestDesc, ocispecDescriptorFromArtifact(referrer)) - } - return fn(sigManifestDesc) - }); err != nil { - return err - } - return nil -} - -// FetchSignatureBlob returns signature envelope blob and descriptor given -// signature manifest descriptor -func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) { - sigManifest, err := c.getSignatureManifest(ctx, desc) - if err != nil { - return nil, ocispec.Descriptor{}, err - } - if len(sigManifest.Blobs) == 0 { - return nil, ocispec.Descriptor{}, errors.New("signature manifest missing signature envelope blob") - } - sigDesc, err := c.Repository.Blobs().Resolve(ctx, sigManifest.Blobs[0].Digest.String()) - if err != nil { - return nil, ocispec.Descriptor{}, err - } - if sigDesc.Size > maxBlobSizeLimit { - return nil, ocispec.Descriptor{}, fmt.Errorf("signature blob too large: %d", sigDesc.Size) - } - sigBlob, err := content.FetchAll(ctx, c.Repository.Blobs(), sigDesc) - if err != nil { - return nil, ocispec.Descriptor{}, err - } - return sigBlob, sigDesc, nil -} - -// PushSignature creates and uploads an signature manifest along with its -// linked signature envelope blob. Upon successful, PushSignature returns -// signature envelope blob and manifest descriptors. -func (c *repositoryClient) PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { - blobDesc, err = c.uploadSignature(ctx, blob, mediaType) - if err != nil { - return ocispec.Descriptor{}, ocispec.Descriptor{}, err - } - - manifestDesc, err = c.uploadSignatureManifest(ctx, artifactspecDescriptorFromOCI(subject), artifactspecDescriptorFromOCI(blobDesc), annotations) - if err != nil { - return ocispec.Descriptor{}, ocispec.Descriptor{}, err - } - - return blobDesc, manifestDesc, nil -} - -// getSignatureManifest returns signature manifest given signature manifest -// descriptor -func (c *repositoryClient) getSignatureManifest(ctx context.Context, sigManifestDesc ocispec.Descriptor) (artifactspec.Manifest, error) { - - repo := c.Repository - repo.ManifestMediaTypes = []string{ - artifactspec.MediaTypeArtifactManifest, - } - store := repo.Manifests() - ociDesc, err := store.Resolve(ctx, sigManifestDesc.Digest.String()) - if err != nil { - return artifactspec.Manifest{}, err - } - if ociDesc.Size > maxManifestSizeLimit { - return artifactspec.Manifest{}, fmt.Errorf("manifest too large: %d", ociDesc.Size) - } - manifestJSON, err := content.FetchAll(ctx, store, ociDesc) - if err != nil { - return artifactspec.Manifest{}, err - } - - var sigManifest artifactspec.Manifest - err = json.Unmarshal(manifestJSON, &sigManifest) - if err != nil { - return artifactspec.Manifest{}, err - } - return sigManifest, nil -} - -// uploadSignature uploads the signature envelope blob to the registry -func (c *repositoryClient) uploadSignature(ctx context.Context, blob []byte, mediaType string) (ocispec.Descriptor, error) { - desc := ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), - } - if err := c.Repository.Blobs().Push(ctx, desc, bytes.NewReader(blob)); err != nil { - return ocispec.Descriptor{}, err - } - return desc, nil -} - -// uploadSignatureManifest uploads the signature manifest to the registry -func (c *repositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc artifactspec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) { - opts := oras.PackArtifactOptions{ - Subject: &subject, - ManifestAnnotations: annotations, - } - - manifestDesc, err := oras.PackArtifact(ctx, c.Repository.Manifests(), ArtifactTypeNotation, []artifactspec.Descriptor{blobDesc}, opts) - if err != nil { - return ocispec.Descriptor{}, err - } - return manifestDesc, nil -} - -func ocispecDescriptorFromArtifact(desc artifactspec.Descriptor) ocispec.Descriptor { - return ocispec.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, - Annotations: desc.Annotations, - } -} - -func artifactspecDescriptorFromOCI(desc ocispec.Descriptor) artifactspec.Descriptor { - return artifactspec.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, - Annotations: desc.Annotations, - } -} diff --git a/registry/repositoryClient_test.go b/registry/repository_test.go similarity index 97% rename from registry/repositoryClient_test.go rename to registry/repository_test.go index 993619a2..9ab60c64 100644 --- a/registry/repositoryClient_test.go +++ b/registry/repository_test.go @@ -316,20 +316,6 @@ func TestResolve(t *testing.T) { expect: ocispec.Descriptor{}, expectErr: true, }, - { - name: "succeed to resolve", - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - }, - expect: ocispec.Descriptor{ - MediaType: mediaType, - Digest: validDigestWithAlgo, - }, - expectErr: false, - }, } for _, tt := range tests { From 777578205fe0229845718388011dbdba58a53fc1 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 8 Nov 2022 20:38:10 +0800 Subject: [PATCH 06/28] update Signed-off-by: Patrick Zheng --- internal/registry/repository_test.go | 599 --------------------------- 1 file changed, 599 deletions(-) delete mode 100644 internal/registry/repository_test.go diff --git a/internal/registry/repository_test.go b/internal/registry/repository_test.go deleted file mode 100644 index 0b69877b..00000000 --- a/internal/registry/repository_test.go +++ /dev/null @@ -1,599 +0,0 @@ -package registry - -import ( - "bytes" - "context" - "fmt" - "io" - "net/http" - "net/url" - "reflect" - "strings" - "testing" - - notation "github.com/notaryproject/notation-go/internal" - "github.com/opencontainers/go-digest" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" - "oras.land/oras-go/v2/registry" - "oras.land/oras-go/v2/registry/remote" -) - -const ( - validDigest = "6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b" - validDigest2 = "9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" - validDigest3 = "1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2" - validDigest4 = "277000f8d32d2b2a7d65f4533339f7d4c064e0540facf1d54c69d9916f05d28c" - validDigest5 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - validDigest6 = "daffbe5f71beaf7b05c080e8ae4f9739cdf21e24c89561e35792f1251d38148d" - validDigest7 = "13b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - validDigest8 = "57f2c47061dae97063dc46598168a80a9f89302c1f24fe2a422a1ec0aba3017a" - validDigest9 = "023c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b" - validDigest10 = "1761e09cad8aa44e48ffb41c78371a6c139bd0df555c90b5d99739b9551c7828" - invalidDigest = "invaliddigest" - algo = "sha256" - validDigestWithAlgo = algo + ":" + validDigest - validDigestWithAlgo2 = algo + ":" + validDigest2 - validDigestWithAlgo3 = algo + ":" + validDigest3 - validDigestWithAlgo4 = algo + ":" + validDigest4 - validDigestWithAlgo5 = algo + ":" + validDigest5 - validDigestWithAlgo6 = algo + ":" + validDigest6 - validDigestWithAlgo7 = algo + ":" + validDigest7 - validDigestWithAlgo8 = algo + ":" + validDigest8 - validDigestWithAlgo9 = algo + ":" + validDigest9 - validDigestWithAlgo10 = algo + ":" + validDigest10 - validHost = "localhost" - validRegistry = validHost + ":5000" - invalidHost = "badhost" - invalidRegistry = invalidHost + ":5000" - validRepo = "test" - msg = "message" - errMsg = "error message" - mediaType = "application/json" - validReference = validRegistry + "/" + validRepo + "@" + validDigest - referenceWithInvalidHost = invalidRegistry + "/" + validRepo + "@" + validDigest - validReference6 = validRegistry + "/" + validRepo + "@" + validDigest6 - invalidReference = "invalid reference" - joseTag = "application/jose+json" - coseTag = "application/cose" - validTimestamp = "2022-07-29T02:23:10Z" - size = 104 - size2 = 135 - validPage = ` - { - "references": [{}], - "referrers": [ - { - "artifactType": "application/vnd.cncf.notary.v2.signature", - "mediaType": "application/vnd.cncf.oras.artifact.manifest.v1+json", - "digest": "localhost:5000/test@57f2c47061dae97063dc46598168a80a9f89302c1f24fe2a422a1ec0aba3017a" - } - ] - }` - pageWithWrongMediaType = ` - { - "references": [{}], - "referrers": [ - { - "artifactType": "application/vnd.cncf.notary.v2.signature", - "mediaType": "application/vnd.cncf.oras.artifact.manifest.invalid", - "digest": "localhost:5000/test@1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2" - } - ] - }` - pageWithBadDigest = ` - { - "references": [{}], - "referrers": [ - { - "artifactType": "application/vnd.cncf.notary.v2.signature", - "mediaType": "application/vnd.cncf.oras.artifact.manifest.v1+json", - "digest": "localhost:5000/test@9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" - } - ] - }` - validBlob = `{ - "digest": "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b", - "size": 90 - }` - validManifest = `{ - "blobs": [ - { - "digest": "sha256:023c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b", - "size": 90 - } - ] - }` -) - -type args struct { - ctx context.Context - reference string - remoteClient remote.Client - plainHttp bool - digest digest.Digest - annotations map[string]string - subjectManifest notation.Descriptor - signature []byte - signatureMediaType string -} - -type mockRemoteClient struct { -} - -func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) { - switch req.URL.Path { - case "/v2/test/manifests/" + validDigest: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(msg))), - Header: map[string][]string{ - "Content-Type": {mediaType}, - "Docker-Content-Digest": {validDigestWithAlgo}, - }, - }, nil - case "/v2/test/blobs/" + validDigestWithAlgo6: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validBlob))), - ContentLength: size, - Header: map[string][]string{ - "Content-Type": {mediaType}, - "Docker-Content-Digest": {validDigestWithAlgo6}, - }, - }, nil - case "/v2/test/blobs/" + validDigestWithAlgo3: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validBlob))), - ContentLength: maxBlobSizeLimit + 1, - Header: map[string][]string{ - "Content-Type": {mediaType}, - "Docker-Content-Digest": {validDigestWithAlgo3}, - }, - }, nil - case "/v2/test/manifests/" + invalidDigest: - return &http.Response{}, fmt.Errorf(errMsg) - case "/v2/test/_oras/artifacts/referrers": - if strings.HasSuffix(req.URL.RawQuery, invalidDigest) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(pageWithBadDigest))), - Header: map[string][]string{ - "Oras-Api-Version": {"oras/1.1"}, - }, - Request: &http.Request{ - Method: "GET", - URL: &url.URL{Path: "/v2/test/_oras/artifacts/referrers"}, - }, - }, nil - } else if strings.HasSuffix(req.URL.RawQuery, validDigest7) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(pageWithWrongMediaType))), - Header: map[string][]string{ - "Oras-Api-Version": {"oras/1.1"}, - }, - Request: &http.Request{ - Method: "GET", - URL: &url.URL{Path: "/v2/test/_oras/artifacts/referrers"}, - }, - }, nil - } else if strings.HasSuffix(req.URL.RawQuery, validDigest8) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validPage))), - Header: map[string][]string{ - "Oras-Api-Version": {"oras/1.1"}, - }, - Request: &http.Request{ - Method: "GET", - URL: &url.URL{Path: "/v2/test/_oras/artifacts/referrers"}, - }, - }, nil - } - return &http.Response{}, fmt.Errorf(msg) - case "/v2/test/manifests/" + validDigest2: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validDigest2))), - ContentLength: size, - Header: map[string][]string{ - "Content-Type": {mediaType}, - "Docker-Content-Digest": {validDigestWithAlgo4}, - }, - }, nil - case "v2/test/manifest/" + validDigest3: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validDigest3))), - Header: map[string][]string{ - "Content-Type": {mediaType}, - "Docker-Content-Digest": {validDigestWithAlgo3}, - }, - }, nil - case "/v2/test/manifests/" + validDigest8: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validDigest8))), - ContentLength: size2, - Header: map[string][]string{ - "Content-Type": {mediaType}, - "Docker-Content-Digest": {validDigestWithAlgo8}, - }, - }, nil - case "/v2/test/manifests/" + validDigestWithAlgo4: - if req.Method == "GET" { - return &http.Response{}, fmt.Errorf(msg) - } - return &http.Response{ - StatusCode: http.StatusCreated, - Body: io.NopCloser(bytes.NewReader([]byte(msg))), - Header: map[string][]string{ - "Docker-Content-Digest": {validDigestWithAlgo4}, - }, - }, nil - case "/v2/test/manifests/" + validDigestWithAlgo7: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(msg))), - Header: map[string][]string{ - "Docker-Content-Digest": {validDigestWithAlgo4}, - }, - }, nil - case "/v2/test/manifests/" + validDigestWithAlgo8: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validManifest))), - ContentLength: size2, - Header: map[string][]string{ - "Docker-Content-Digest": {validDigestWithAlgo8}, - "Content-Type": {mediaType}, - }, - }, nil - case "/v2/test/manifests/" + validDigestWithAlgo2: - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte(validBlob))), - Header: map[string][]string{ - "Docker-Content-Digest": {validDigestWithAlgo2}, - "Content-Type": {mediaType}, - }, - }, nil - case "/v2/test/manifests/" + validDigestWithAlgo10: - if req.Method == "GET" { - return &http.Response{}, fmt.Errorf(msg) - } - return &http.Response{ - StatusCode: http.StatusCreated, - Body: io.NopCloser(bytes.NewReader([]byte(msg))), - Header: map[string][]string{ - "Docker-Content-Digest": {validDigestWithAlgo10}, - }, - }, nil - case "/v2/test/blobs/uploads/": - switch req.Host { - case validRegistry: - return &http.Response{ - StatusCode: http.StatusAccepted, - Body: io.NopCloser(bytes.NewReader([]byte(msg))), - Request: &http.Request{ - Header: map[string][]string{}, - }, - Header: map[string][]string{ - "Location": {"test"}, - }, - }, nil - default: - return &http.Response{}, fmt.Errorf(msg) - } - case validRepo: - return &http.Response{ - StatusCode: http.StatusCreated, - Body: io.NopCloser(bytes.NewReader([]byte(msg))), - }, nil - default: - return &http.Response{}, fmt.Errorf(errMsg) - } -} - -func TestNewRepositoryClient_constructCorrectly(t *testing.T) { - remoteClient := mockRemoteClient{} - ref := registry.Reference{} - plainHttp := false - client := NewRepositoryClient(remoteClient, ref, plainHttp) - - if client.Client != remoteClient || - client.PlainHTTP != plainHttp || - client.Reference != ref { - t.Fatalf("Expect client with remoteClient: %v, plainHTTP: %v, ref: %v, got: %+v", - remoteClient, plainHttp, ref, client.Client) - } -} - -func TestResolve(t *testing.T) { - tests := []struct { - name string - args args - expect notation.Descriptor - expectErr bool - }{ - { - name: "failed to resolve", - args: args{ - ctx: context.Background(), - reference: invalidReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - }, - expect: notation.Descriptor{}, - expectErr: true, - }, - { - name: "succeed to resolve", - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - }, - expect: notation.Descriptor{ - MediaType: mediaType, - Digest: validDigestWithAlgo, - }, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args := tt.args - ref, _ := registry.ParseReference(args.reference) - client := NewRepositoryClient(args.remoteClient, ref, args.plainHttp) - res, err := client.Resolve(args.ctx, args.reference) - if (err != nil) != tt.expectErr { - t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) - } - if !reflect.DeepEqual(res, tt.expect) { - t.Errorf("expect %+v, got %+v", tt.expect, res) - } - }) - } -} - -func TestGetBlob(t *testing.T) { - tests := []struct { - name string - args args - expect []byte - expectErr bool - }{ - { - name: "failed to resolve", - expect: nil, - expectErr: true, - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - digest: digest.Digest(invalidDigest), - }, - }, - { - name: "exceed max blob size", - expect: nil, - expectErr: true, - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - digest: digest.Digest(validDigestWithAlgo3), - }, - }, - { - name: "succeed to get", - expect: []byte(validBlob), - expectErr: false, - args: args{ - ctx: context.Background(), - reference: validReference6, - remoteClient: mockRemoteClient{}, - plainHttp: false, - digest: digest.Digest(validDigestWithAlgo6), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args := tt.args - ref, _ := registry.ParseReference(args.reference) - client := NewRepositoryClient(args.remoteClient, ref, args.plainHttp) - res, err := client.GetBlob(args.ctx, args.digest) - if (err != nil) != tt.expectErr { - t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) - } - if !reflect.DeepEqual(res, tt.expect) { - t.Errorf("expect %+v, got %+v", tt.expect, res) - } - }) - } -} - -func TestListSignatureManifests(t *testing.T) { - tests := []struct { - name string - args args - expect []SignatureManifest - expectErr bool - }{ - { - name: "wrong mediaType", - expectErr: false, - expect: nil, - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - digest: digest.Digest(validDigest7), - }, - }, - { - name: "failed to fetch content", - expectErr: true, - expect: nil, - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - digest: digest.Digest(invalidDigest), - }, - }, - { - name: "succeed to list", - expectErr: false, - expect: []SignatureManifest{ - { - Blob: notation.Descriptor{ - Digest: validDigestWithAlgo9, - Size: 90, - }, - }, - }, - args: args{ - ctx: context.Background(), - reference: validReference, - remoteClient: mockRemoteClient{}, - plainHttp: false, - digest: digest.Digest(validDigest8), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args := tt.args - ref, _ := registry.ParseReference(args.reference) - client := NewRepositoryClient(args.remoteClient, ref, args.plainHttp) - - res, err := client.ListSignatureManifests(args.ctx, args.digest) - if (err != nil) != tt.expectErr { - t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) - } - if !reflect.DeepEqual(res, tt.expect) { - t.Errorf("expect %+v, got %+v", tt.expect, res) - } - }) - } -} - -func TestPutSignatureManifest(t *testing.T) { - tests := []struct { - name string - args args - expectDes notation.Descriptor - expectManifest SignatureManifest - expectErr bool - }{ - { - name: "failed to upload signature", - expectErr: true, - expectDes: notation.Descriptor{}, - expectManifest: SignatureManifest{}, - args: args{ - reference: referenceWithInvalidHost, - signature: make([]byte, 0), - ctx: context.Background(), - remoteClient: mockRemoteClient{}, - }, - }, - { - name: "failed to upload signature manifest", - expectErr: true, - expectDes: notation.Descriptor{}, - expectManifest: SignatureManifest{}, - args: args{ - reference: validReference, - signature: make([]byte, 0), - ctx: context.Background(), - remoteClient: mockRemoteClient{}, - }, - }, - { - name: "succeed to put signature manifest with jws media type", - expectErr: false, - expectDes: notation.Descriptor{ - MediaType: artifactspec.MediaTypeArtifactManifest, - Digest: digest.Digest(validDigestWithAlgo4), - Size: 369, - }, - expectManifest: SignatureManifest{ - Annotations: map[string]string{ - artifactspec.AnnotationArtifactCreated: validTimestamp, - }, - Blob: notation.Descriptor{ - MediaType: joseTag, - Digest: validDigestWithAlgo5, - }, - }, - args: args{ - reference: validReference, - signature: make([]byte, 0), - ctx: context.Background(), - remoteClient: mockRemoteClient{}, - annotations: map[string]string{ - artifactspec.AnnotationArtifactCreated: validTimestamp, - }, - signatureMediaType: joseTag, - }, - }, - { - name: "succeed to put signature manifest with cose media type", - expectErr: false, - expectDes: notation.Descriptor{ - MediaType: artifactspec.MediaTypeArtifactManifest, - Digest: digest.Digest(validDigestWithAlgo10), - Size: 364, - }, - expectManifest: SignatureManifest{ - Annotations: map[string]string{ - artifactspec.AnnotationArtifactCreated: validTimestamp, - }, - Blob: notation.Descriptor{ - MediaType: coseTag, - Digest: validDigestWithAlgo5, - }, - }, - args: args{ - reference: validReference, - signature: make([]byte, 0), - ctx: context.Background(), - remoteClient: mockRemoteClient{}, - annotations: map[string]string{ - artifactspec.AnnotationArtifactCreated: validTimestamp, - }, - signatureMediaType: coseTag, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args := tt.args - ref, _ := registry.ParseReference(args.reference) - client := NewRepositoryClient(args.remoteClient, ref, args.plainHttp) - - des, manifest, err := client.PutSignatureManifest(args.ctx, args.signature, args.signatureMediaType, args.subjectManifest, args.annotations) - if (err != nil) != tt.expectErr { - t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) - } - if !reflect.DeepEqual(des, tt.expectDes) { - t.Errorf("expect descriptor: %+v, got %+v", tt.expectDes, des) - } - if !reflect.DeepEqual(manifest, tt.expectManifest) { - t.Errorf("expect manifest: %+v, got %+v", tt.expectManifest, manifest) - } - }) - } -} From 293d7a7d5c64fd9586d19635660f728b18e5d6a3 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 15:09:34 +0800 Subject: [PATCH 07/28] update Signed-off-by: Patrick Zheng --- notation/notation.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index bd022ba3..e5565752 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -80,7 +80,7 @@ type payload struct { // and packing in various signature formats. type Signer interface { // Sign signs the artifact described by its descriptor, - // and returns the signature, SignerInfo, and envelopeMediaType. + // and returns the signature and SignerInfo. Sign(ctx context.Context, desc Descriptor, envelopeMediaType string, opts SignOptions) ([]byte, *signature.SignerInfo, error) } @@ -167,8 +167,8 @@ type VerificationOutcome struct { // Verifier is a generic interface for verifying an artifact. type Verifier interface { - // Verify verifies the signature blob and returns the verified descriptor - // upon successful verification. + // Verify verifies the signature blob and returns the signature blob + // descriptor upon successful verification. Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, *VerificationOutcome, error) // TrustPolicyDocument gets the validated trust policy document. From f01fa86ec09aad583a73b87fe373007e8b369dd2 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 19:58:40 +0800 Subject: [PATCH 08/28] update Signed-off-by: Patrick Zheng --- internal/common/common.go | 97 ------------------------- verification/trustpolicy/trustpolicy.go | 1 + 2 files changed, 1 insertion(+), 97 deletions(-) delete mode 100644 internal/common/common.go diff --git a/internal/common/common.go b/internal/common/common.go deleted file mode 100644 index 92a09f04..00000000 --- a/internal/common/common.go +++ /dev/null @@ -1,97 +0,0 @@ -package common - -import ( - "fmt" - "regexp" - "strings" - - ldapv3 "github.com/go-ldap/ldap/v3" -) - -const ( - Wildcard = "*" - X509Subject = "x509.subject" -) - -// isPresent is a utility function to check if a string exists in an array -func IsPresent(val string, values []string) bool { - for _, v := range values { - if v == val { - return true - } - } - return false -} - -// Internal type to hold raw and parsed Distinguished Names -type ParsedDN struct { - RawString string - ParsedMap map[string]string -} - -// ParseDistinguishedName parses a DN name and validates Notary V2 rules -func ParseDistinguishedName(name string) (map[string]string, error) { - mandatoryFields := []string{"C", "ST", "O"} - attrKeyValue := make(map[string]string) - dn, err := ldapv3.ParseDN(name) - - if err != nil { - return nil, fmt.Errorf("distinguished name (DN) %q is not valid, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name) - } - - for _, rdn := range dn.RDNs { - - // multi-valued RDNs are not supported (TODO: add spec reference here) - if len(rdn.Attributes) > 1 { - return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name) - } - for _, attribute := range rdn.Attributes { - if attrKeyValue[attribute.Type] == "" { - attrKeyValue[attribute.Type] = attribute.Value - } else { - return nil, fmt.Errorf("distinguished name (DN) %q has duplicate RDN attribute for %q, DN can only have unique RDN attributes", name, attribute.Type) - } - } - } - - // Verify mandatory fields are present - for _, field := range mandatoryFields { - if attrKeyValue[field] == "" { - return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field) - } - } - // No errors - return attrKeyValue, nil -} - -// IsSubsetDN returns true if dn1 is a subset of dn2 i.e. every key/value pair of dn1 has a matching key/value pair in dn2, otherwise returns false -func IsSubsetDN(dn1 map[string]string, dn2 map[string]string) bool { - for key := range dn1 { - if dn1[key] != dn2[key] { - return false - } - } - return true -} - -// 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]+)+)?)+)?$`) - errorMessage := "registry scope %q is not valid, make sure it is the fully qualified registry URL without the scheme/protocol. e.g domain.com/my/repository" - firstSlash := strings.Index(scope, "/") - if firstSlash < 0 { - return fmt.Errorf(errorMessage, scope) - } - domain := scope[:firstSlash] - repository := scope[firstSlash+1:] - - if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) { - return fmt.Errorf(errorMessage, scope) - } - - // No errors - return nil -} diff --git a/verification/trustpolicy/trustpolicy.go b/verification/trustpolicy/trustpolicy.go index 73950630..5daad9f3 100644 --- a/verification/trustpolicy/trustpolicy.go +++ b/verification/trustpolicy/trustpolicy.go @@ -116,6 +116,7 @@ var supportedPolicyVersions = []string{"1.0"} type Document struct { // Version of the policy document Version string `json:"version"` + // TrustPolicies include each policy statement TrustPolicies []TrustPolicy `json:"trustPolicies"` } From 9b8ed37907ce353af6fe99cd169d2bbf47ca928c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 20:03:45 +0800 Subject: [PATCH 09/28] update Signed-off-by: Patrick Zheng --- notation/notation.go | 54 ++++++++++++++------------------------- notation/notation_test.go | 6 ++--- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index cf2d55d4..5462b74c 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -15,29 +15,13 @@ import ( "github.com/notaryproject/notation-core-go/timestamp" "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation-go/verification/trustpolicy" - "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" -// Descriptor describes the artifact that needs to be signed. -type Descriptor struct { - // The media type of the targeted content. - MediaType string `json:"mediaType"` - - // The digest of the targeted content. - Digest digest.Digest `json:"digest"` - - // Specifies the size in bytes of the blob. - Size int64 `json:"size"` - - // Contains optional user defined attributes. - Annotations map[string]string `json:"annotations,omitempty"` -} - // Equal reports whether d and t points to the same content. -func (d Descriptor) Equal(t Descriptor) bool { +func Equal(d ocispec.Descriptor, t ocispec.Descriptor) bool { return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size && reflect.DeepEqual(d.Annotations, t.Annotations) } @@ -72,7 +56,7 @@ type SignOptions struct { // Payload describes the content that gets signed. type payload struct { - TargetArtifact Descriptor `json:"targetArtifact"` + TargetArtifact ocispec.Descriptor `json:"targetArtifact"` } // Signer is a generic interface for signing an artifact. @@ -81,29 +65,29 @@ type payload struct { type Signer interface { // Sign signs the artifact described by its descriptor, // and returns the signature and SignerInfo. - Sign(ctx context.Context, desc Descriptor, envelopeMediaType string, opts SignOptions) ([]byte, *signature.SignerInfo, error) + Sign(ctx context.Context, desc ocispec.Descriptor, envelopeMediaType string, opts SignOptions) ([]byte, *signature.SignerInfo, error) } // Sign signs the artifact in the remote registry and push the signature to the // remote. // The descriptor of the sign content is returned upon sucessful signing. -func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts SignOptions) (Descriptor, error) { +func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts SignOptions) (ocispec.Descriptor, error) { ociDesc, err := repo.Resolve(ctx, opts.ArtifactReference) if err != nil { - return Descriptor{}, err + return ocispec.Descriptor{}, err } desc := notationDescriptorFromOCI(ociDesc) sig, signerInfo, err := signer.Sign(ctx, desc, opts.SignatureMediaType, opts) if err != nil { - return Descriptor{}, err + return ocispec.Descriptor{}, err } annotations, err := generateAnnotations(signerInfo) if err != nil { - return Descriptor{}, err + return ocispec.Descriptor{}, err } _, _, err = repo.PushSignature(ctx, sig, opts.SignatureMediaType, ociDesc, annotations) if err != nil { - return Descriptor{}, err + return ocispec.Descriptor{}, err } return desc, nil @@ -169,7 +153,7 @@ type VerificationOutcome struct { type Verifier interface { // Verify verifies the signature blob and returns the signature blob // descriptor upon successful verification. - Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, *VerificationOutcome, error) + Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) // TrustPolicyDocument gets the validated trust policy document. TrustPolicyDocument() (*trustpolicy.Document, error) @@ -201,27 +185,27 @@ of `VerificationOutcome`. For more details on signature verification, see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#signature-verification */ -func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (Descriptor, []*VerificationOutcome, error) { +func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) { var verificationOutcomes []*VerificationOutcome artifactRef := opts.ArtifactReference artifactDescriptor, err := repo.Resolve(ctx, artifactRef) if err != nil { - return Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} + return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} } trustpolicyDoc, err := verifier.TrustPolicyDocument() if err != nil { - return Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} + return ocispec.Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} } trustPolicy, err := trustpolicyDoc.GetApplicableTrustPolicy(artifactRef) if err != nil { - return Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} + return ocispec.Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} } // ignore the error since we already validated the policy document verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel() if verificationLevel.Name == trustpolicy.LevelSkip.Name { verificationOutcomes = append(verificationOutcomes, &VerificationOutcome{VerificationLevel: verificationLevel}) - return Descriptor{}, verificationOutcomes, nil + return ocispec.Descriptor{}, verificationOutcomes, nil } opts.VerificationLevel = verificationLevel @@ -257,7 +241,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op // artifact digest must match the digest from the signature payload payload := &payload{} err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) - if err != nil || !notationDescriptorFromOCI(artifactDescriptor).Equal(payload.TargetArtifact) { + if err != nil || Equal(artifactDescriptor, payload.TargetArtifact) { outcome.Error = fmt.Errorf("given digest %q does not match the digest %q present in the digital signature", artifactDescriptor.Digest.String(), payload.TargetArtifact.Digest.String()) continue } @@ -274,7 +258,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op }) if err != nil { - return Descriptor{}, nil, err + return ocispec.Descriptor{}, nil, err } // check whether verification was successful or not @@ -284,7 +268,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op return notationDescriptorFromOCI(verifiedSigBlobDesc), verificationOutcomes, nil } - return Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} + return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} } func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, error) { @@ -304,8 +288,8 @@ func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, e return annotations, nil } -func notationDescriptorFromOCI(desc ocispec.Descriptor) Descriptor { - return Descriptor{ +func notationDescriptorFromOCI(desc ocispec.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ MediaType: desc.MediaType, Digest: desc.Digest, Size: desc.Size, diff --git a/notation/notation_test.go b/notation/notation_test.go index c73b218d..e899ce0f 100644 --- a/notation/notation_test.go +++ b/notation/notation_test.go @@ -121,15 +121,15 @@ type dummyVerifier struct { FailVerify bool } -func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, *VerificationOutcome, error) { +func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) { if v.FailVerify { - return Descriptor{}, nil, errors.New("failed verify") + return ocispec.Descriptor{}, nil, errors.New("failed verify") } outcome := &VerificationOutcome{ VerificationResults: []*ValidationResult{}, VerificationLevel: opts.VerificationLevel, } - return Descriptor{}, outcome, nil + return ocispec.Descriptor{}, outcome, nil } func (v *dummyVerifier) TrustPolicyDocument() (*trustpolicy.Document, error) { From 53fa4a40eb4eb81dd5a67c980a813435738edcd9 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 20:17:54 +0800 Subject: [PATCH 10/28] update Signed-off-by: Patrick Zheng --- notation/notation.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index 5462b74c..40282e32 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -3,11 +3,9 @@ package notation import ( "context" "crypto/sha256" - "crypto/x509" "encoding/hex" "encoding/json" "fmt" - "reflect" "strings" "time" @@ -20,11 +18,6 @@ import ( const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" -// Equal reports whether d and t points to the same content. -func Equal(d ocispec.Descriptor, t ocispec.Descriptor) bool { - return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size && reflect.DeepEqual(d.Annotations, t.Annotations) -} - // SignOptions contains parameters for Signer.Sign. type SignOptions struct { // Reference of the artifact that needs to be signed. @@ -42,14 +35,6 @@ type SignOptions struct { // present. TSA timestamp.Timestamper - // TSAVerifyOptions is the verify option to verify the fetched timestamp - // signature. - // The `Intermediates` in the verify options will be ignored and - // re-contrusted using the certificates in the fetched timestamp signature. - // An empty list of `KeyUsages` in the verify options implies - // ExtKeyUsageTimeStamping. - TSAVerifyOptions x509.VerifyOptions - // Sets or overrides the plugin configuration. PluginConfig map[string]string } @@ -241,8 +226,8 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op // artifact digest must match the digest from the signature payload payload := &payload{} err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) - if err != nil || Equal(artifactDescriptor, payload.TargetArtifact) { - outcome.Error = fmt.Errorf("given digest %q does not match the digest %q present in the digital signature", artifactDescriptor.Digest.String(), payload.TargetArtifact.Digest.String()) + if err != nil { + outcome.Error = err continue } outcome.SignedAnnotations = payload.TargetArtifact.Annotations From e1a4497a628660f6c3a5dd1845672ce90fa1fb1d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 20:36:08 +0800 Subject: [PATCH 11/28] update Signed-off-by: Patrick Zheng --- notation/notation.go | 38 +++++++++++++++++++------------------- notation/notation_test.go | 1 - 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index 40282e32..43b0a533 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -91,10 +91,6 @@ type VerifyOptions struct { // PluginConfig is a map of plugin configs. PluginConfig map[string]string - - // VerificationLevel encapsulates the signature verification preset and its - // actions for each verification type - VerificationLevel *trustpolicy.VerificationLevel } // ValidationResult encapsulates the verification result (passed or failed) @@ -102,13 +98,13 @@ type VerifyOptions struct { // // specified in the trust policy type ValidationResult struct { - // Success is set to true if the verification was successful - Success bool // Type of verification that is performed Type trustpolicy.ValidationType + // Action is the intended action for the given verification type as defined // in the trust policy Action trustpolicy.ValidationAction + // Err is set if there are any errors during the verification process Error error } @@ -117,19 +113,25 @@ type ValidationResult struct { // the verification level and results for each verification type that was // performed. type VerificationOutcome struct { + // SignatureBlobDescriptor is descriptor of the signature envelope blob SignatureBlobDescriptor *ocispec.Descriptor + // EnvelopeContent contains the details of the digital signature and // associated metadata EnvelopeContent *signature.EnvelopeContent + // VerificationLevel describes what verification level was used for // performing signature verification VerificationLevel *trustpolicy.VerificationLevel + // VerificationResults contains the verifications performed on the signature // and their results VerificationResults []*ValidationResult + // SignedAnnotations contains arbitrary metadata relating to the target // artifact that was signed SignedAnnotations map[string]string + // Error that caused the verification to fail (if it fails) Error error } @@ -171,13 +173,7 @@ of `VerificationOutcome`. For more details on signature verification, see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#signature-verification */ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) { - var verificationOutcomes []*VerificationOutcome artifactRef := opts.ArtifactReference - artifactDescriptor, err := repo.Resolve(ctx, artifactRef) - if err != nil { - return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} - } - trustpolicyDoc, err := verifier.TrustPolicyDocument() if err != nil { return ocispec.Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} @@ -186,17 +182,22 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if err != nil { return ocispec.Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} } + + var verificationOutcomes []*VerificationOutcome // ignore the error since we already validated the policy document verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel() if verificationLevel.Name == trustpolicy.LevelSkip.Name { verificationOutcomes = append(verificationOutcomes, &VerificationOutcome{VerificationLevel: verificationLevel}) return ocispec.Descriptor{}, verificationOutcomes, nil } - opts.VerificationLevel = verificationLevel // get signature manifests var success bool var verifiedSigBlobDesc ocispec.Descriptor + artifactDescriptor, err := repo.Resolve(ctx, artifactRef) + if err != nil { + return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} + } err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { if len(signatureManifests) < 1 { return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", artifactRef)} @@ -214,16 +215,17 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op } _, outcome, err := verifier.Verify(ctx, sigBlob, opts) if err != nil { - if outcome != nil && outcome.Error != nil { + if outcome != nil { outcome.SignatureBlobDescriptor = &sigBlobDesc + outcome.Error = err verificationOutcomes = append(verificationOutcomes, outcome) } continue } + + // At this point, we've found a signature verified successfully outcome.SignatureBlobDescriptor = &sigBlobDesc verificationOutcomes = append(verificationOutcomes, outcome) - - // artifact digest must match the digest from the signature payload payload := &payload{} err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) if err != nil { @@ -231,10 +233,8 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op continue } outcome.SignedAnnotations = payload.TargetArtifact.Annotations - - // At this point, we've found a signature verified successfully success = true - // Descriptor of the signature blob that get verified successfully + // Descriptor of the signature blob that gets verified successfully verifiedSigBlobDesc = sigBlobDesc return nil diff --git a/notation/notation_test.go b/notation/notation_test.go index e899ce0f..12b71815 100644 --- a/notation/notation_test.go +++ b/notation/notation_test.go @@ -127,7 +127,6 @@ func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts Verif } outcome := &VerificationOutcome{ VerificationResults: []*ValidationResult{}, - VerificationLevel: opts.VerificationLevel, } return ocispec.Descriptor{}, outcome, nil } From 5fc3cc0f09400d7edd7d91549b6a26c5ed3394fe Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 20:46:11 +0800 Subject: [PATCH 12/28] updated per code review Signed-off-by: Patrick Zheng --- notation/notation.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index 43b0a533..1344d251 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -199,13 +199,15 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} } err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { - if len(signatureManifests) < 1 { - return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", artifactRef)} - } // if already verified successfully, no need to continue if success { return nil } + + if len(signatureManifests) < 1 { + return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", artifactRef)} + } + // process signatures for _, sigManifest := range signatureManifests { // get signature envelope From 6cd2b892e5de7c3389e6c3a5c5494ce979531bee Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 21:09:56 +0800 Subject: [PATCH 13/28] updated per code review Signed-off-by: Patrick Zheng --- notation/notation.go | 53 +++++++++----------------------------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index 1344d251..fb29f395 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -95,8 +95,7 @@ type VerifyOptions struct { // ValidationResult encapsulates the verification result (passed or failed) // for a verification type, including the desired verification action as -// -// specified in the trust policy +// specified in the trust policy type ValidationResult struct { // Type of verification that is performed Type trustpolicy.ValidationType @@ -141,51 +140,21 @@ type Verifier interface { // Verify verifies the signature blob and returns the signature blob // descriptor upon successful verification. Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) - - // TrustPolicyDocument gets the validated trust policy document. - TrustPolicyDocument() (*trustpolicy.Document, error) } -/* -Verify performs signature verification on each of the notation supported -verification types (like integrity, authenticity, etc.) and return the -verification outcomes. - -Given an artifact reference, Verify will retrieve all the signatures associated -with the reference and perform signature verification. -A signature is considered not valid if verification fails due to any one of the -following reasons - - 1. Artifact Reference is not associated with a signature i.e. unsigned - 2. Registry is unavailable to retrieve the signature - 3. Signature does not satisfy the verification rules configured in the trust - policy - 4. Signature specifies a plugin for extended verification and that throws an - error - 5. Digest in the signature does not match the digest present in the reference - -If each and every signature associated with the reference fail the verification, -then Verify will return `ErrorVerificationFailed` error along with an array -of `VerificationOutcome`. - -# Callers can pass the verification plugin config in VerifyOptions.PluginConfig - -For more details on signature verification, see https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#signature-verification -*/ +// Verify performs signature verification on each of the notation supported +// verification types (like integrity, authenticity, etc.) and return the +// verification outcomes. +// For more details on signature verification, see +// https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#signature-verification func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) { + var verificationOutcomes []*VerificationOutcome artifactRef := opts.ArtifactReference - trustpolicyDoc, err := verifier.TrustPolicyDocument() - if err != nil { - return ocispec.Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} - } - trustPolicy, err := trustpolicyDoc.GetApplicableTrustPolicy(artifactRef) - if err != nil { - return ocispec.Descriptor{}, nil, ErrorNoApplicableTrustPolicy{Msg: err.Error()} + _, outcome, err := verifier.Verify(ctx, nil, opts) // passing nil signature to + if outcome == nil { + return ocispec.Descriptor{}, nil, err } - - var verificationOutcomes []*VerificationOutcome - // ignore the error since we already validated the policy document - verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel() + verificationLevel := outcome.VerificationLevel if verificationLevel.Name == trustpolicy.LevelSkip.Name { verificationOutcomes = append(verificationOutcomes, &VerificationOutcome{VerificationLevel: verificationLevel}) return ocispec.Descriptor{}, verificationOutcomes, nil From e496e5cf3f7dfc38c95912eeb51b99c8bc904cfe Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 21:33:52 +0800 Subject: [PATCH 14/28] update Signed-off-by: Patrick Zheng --- notation/notation_test.go | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/notation/notation_test.go b/notation/notation_test.go index 12b71815..cd770410 100644 --- a/notation/notation_test.go +++ b/notation/notation_test.go @@ -16,7 +16,7 @@ import ( func TestRegistryResolveError(t *testing.T) { policyDocument := dummyPolicyDocument() repo := mock.NewRepository() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := "network error" expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -33,39 +33,21 @@ func TestRegistryResolveError(t *testing.T) { func TestSkippedSignatureVerification(t *testing.T) { policyDocument := dummyPolicyDocument() - policyDocument.TrustPolicies[0].SignatureVerification.VerificationLevel = "skip" repo := mock.NewRepository() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip} opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} _, outcomes, err := Verify(context.Background(), &verifier, repo, opts) - if err != nil || outcomes[0].VerificationLevel != trustpolicy.LevelSkip { + if err != nil || outcomes[0].VerificationLevel.Name != trustpolicy.LevelSkip.Name { t.Fatalf("\"skip\" verification level must pass overall signature verification") } } -func TestRegistryListSignaturesError(t *testing.T) { - policyDocument := dummyPolicyDocument() - repo := mock.NewRepository() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true} - errorMessage := "network error" - expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} - - // mock the repository - repo.ListSignaturesError = ErrorSignatureRetrievalFailed{Msg: "network error"} - opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri} - _, _, err := Verify(context.Background(), &verifier, repo, opts) - - if err == nil || !errors.Is(err, expectedErr) { - t.Fatalf("RegistryListSignatureManifests expected: %v got: %v", expectedErr, err) - } -} - func TestRegistryNoSignatureManifests(t *testing.T) { policyDocument := dummyPolicyDocument() repo := mock.NewRepository() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", mock.SampleArtifactUri) expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -82,7 +64,7 @@ func TestRegistryNoSignatureManifests(t *testing.T) { func TestRegistryFetchSignatureBlobError(t *testing.T) { policyDocument := dummyPolicyDocument() repo := mock.NewRepository() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false} + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : network error", mock.SampleDigest, mock.SampleArtifactUri) expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} @@ -116,9 +98,10 @@ func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) { } type dummyVerifier struct { - TrustPolicyDoc *trustpolicy.Document - PluginManager pluginManager - FailVerify bool + TrustPolicyDoc *trustpolicy.Document + PluginManager pluginManager + FailVerify bool + VerificationLevel trustpolicy.VerificationLevel } func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) { @@ -127,6 +110,7 @@ func (v *dummyVerifier) Verify(ctx context.Context, signature []byte, opts Verif } outcome := &VerificationOutcome{ VerificationResults: []*ValidationResult{}, + VerificationLevel: &v.VerificationLevel, } return ocispec.Descriptor{}, outcome, nil } From 5a77b81ab3373481f4cf8dafb7b82fd3a1816a38 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 9 Nov 2022 22:01:49 +0800 Subject: [PATCH 15/28] updated per code review Signed-off-by: Patrick Zheng --- notation/notation.go | 47 ++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index fb29f395..a51d37b9 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "reflect" "strings" "time" @@ -39,11 +40,6 @@ type SignOptions struct { PluginConfig map[string]string } -// Payload describes the content that gets signed. -type payload struct { - TargetArtifact ocispec.Descriptor `json:"targetArtifact"` -} - // Signer is a generic interface for signing an artifact. // The interface allows signing with local or remote keys, // and packing in various signature formats. @@ -61,8 +57,7 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig if err != nil { return ocispec.Descriptor{}, err } - desc := notationDescriptorFromOCI(ociDesc) - sig, signerInfo, err := signer.Sign(ctx, desc, opts.SignatureMediaType, opts) + sig, signerInfo, err := signer.Sign(ctx, ociDesc, opts.SignatureMediaType, opts) if err != nil { return ocispec.Descriptor{}, err } @@ -75,7 +70,7 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig return ocispec.Descriptor{}, err } - return desc, nil + return ociDesc, nil } // VerifyOptions contains parameters for Verifier.Verify. @@ -104,7 +99,7 @@ type ValidationResult struct { // in the trust policy Action trustpolicy.ValidationAction - // Err is set if there are any errors during the verification process + // Error is set if there are any errors during the verification process Error error } @@ -137,7 +132,7 @@ type VerificationOutcome struct { // Verifier is a generic interface for verifying an artifact. type Verifier interface { - // Verify verifies the signature blob and returns the signature blob + // Verify verifies the signature blob and returns the artifact // descriptor upon successful verification. Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) } @@ -184,7 +179,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if err != nil { return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %s", sigBlobDesc.Digest, artifactRef, err.Error())} } - _, outcome, err := verifier.Verify(ctx, sigBlob, opts) + targetArtifactDescriptor, outcome, err := verifier.Verify(ctx, sigBlob, opts) if err != nil { if outcome != nil { outcome.SignatureBlobDescriptor = &sigBlobDesc @@ -194,16 +189,17 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op continue } - // At this point, we've found a signature verified successfully - outcome.SignatureBlobDescriptor = &sigBlobDesc - verificationOutcomes = append(verificationOutcomes, outcome) - payload := &payload{} - err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) - if err != nil { + // + if !equal(&targetArtifactDescriptor, &artifactDescriptor) { + outcome.SignatureBlobDescriptor = &sigBlobDesc outcome.Error = err + verificationOutcomes = append(verificationOutcomes, outcome) continue } - outcome.SignedAnnotations = payload.TargetArtifact.Annotations + + // At this point, we've found a signature verified successfully + outcome.SignatureBlobDescriptor = &sigBlobDesc + verificationOutcomes = append(verificationOutcomes, outcome) success = true // Descriptor of the signature blob that gets verified successfully verifiedSigBlobDesc = sigBlobDesc @@ -221,7 +217,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if success { // signature verification succeeds if there is at least one good // signature - return notationDescriptorFromOCI(verifiedSigBlobDesc), verificationOutcomes, nil + return verifiedSigBlobDesc, verificationOutcomes, nil } return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} @@ -244,11 +240,10 @@ func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, e return annotations, nil } -func notationDescriptorFromOCI(desc ocispec.Descriptor) ocispec.Descriptor { - return ocispec.Descriptor{ - MediaType: desc.MediaType, - Digest: desc.Digest, - Size: desc.Size, - Annotations: desc.Annotations, - } +// Equal reports whether d and t points to the same content. +func equal(d *ocispec.Descriptor, t *ocispec.Descriptor) bool { + return d.MediaType == t.MediaType && + d.Digest == t.Digest && + d.Size == t.Size && + reflect.DeepEqual(d.Annotations, t.Annotations) } From aeea620739e223aa28f8d8add74c3efe7d9cf3ef Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 07:24:39 +0800 Subject: [PATCH 16/28] updated per code review Signed-off-by: Patrick Zheng --- notation/notation.go | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/notation/notation.go b/notation/notation.go index a51d37b9..4045dfa3 100644 --- a/notation/notation.go +++ b/notation/notation.go @@ -5,13 +5,13 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "reflect" "strings" "time" "github.com/notaryproject/notation-core-go/signature" - "github.com/notaryproject/notation-core-go/timestamp" "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation-go/verification/trustpolicy" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -32,10 +32,6 @@ type SignOptions struct { // Expiry identifies the expiration time of the resulted signature. Expiry time.Time - // TSA is the TimeStamp Authority to timestamp the resulted signature if - // present. - TSA timestamp.Timestamper - // Sets or overrides the plugin configuration. PluginConfig map[string]string } @@ -145,7 +141,8 @@ type Verifier interface { func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) { var verificationOutcomes []*VerificationOutcome artifactRef := opts.ArtifactReference - _, outcome, err := verifier.Verify(ctx, nil, opts) // passing nil signature to + // passing nil signature to check 'skip' + _, outcome, err := verifier.Verify(ctx, nil, opts) if outcome == nil { return ocispec.Descriptor{}, nil, err } @@ -157,6 +154,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op // get signature manifests var success bool + doneVerification := errors.New("done verification") var verifiedSigBlobDesc ocispec.Descriptor artifactDescriptor, err := repo.Resolve(ctx, artifactRef) if err != nil { @@ -179,7 +177,10 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if err != nil { return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %s", sigBlobDesc.Digest, artifactRef, err.Error())} } - targetArtifactDescriptor, outcome, err := verifier.Verify(ctx, sigBlob, opts) + payloadArtifactDescriptor, outcome, err := verifier.Verify(ctx, sigBlob, opts) + if err == nil && !equal(&payloadArtifactDescriptor, &artifactDescriptor) { + err = errors.New("payloadArtifactDescriptor does not match artifactDescriptor") + } if err != nil { if outcome != nil { outcome.SignatureBlobDescriptor = &sigBlobDesc @@ -189,14 +190,6 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op continue } - // - if !equal(&targetArtifactDescriptor, &artifactDescriptor) { - outcome.SignatureBlobDescriptor = &sigBlobDesc - outcome.Error = err - verificationOutcomes = append(verificationOutcomes, outcome) - continue - } - // At this point, we've found a signature verified successfully outcome.SignatureBlobDescriptor = &sigBlobDesc verificationOutcomes = append(verificationOutcomes, outcome) @@ -204,12 +197,12 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op // Descriptor of the signature blob that gets verified successfully verifiedSigBlobDesc = sigBlobDesc - return nil + return doneVerification } return nil }) - if err != nil { + if err != nil && !errors.Is(err, doneVerification) { return ocispec.Descriptor{}, nil, err } From f192a8ca8c9311fd62448bec37b28c4b1ba24d74 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 10:31:41 +0800 Subject: [PATCH 17/28] update Signed-off-by: Patrick Zheng --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f2db601..56822548 100644 --- a/go.mod +++ b/go.mod @@ -19,5 +19,5 @@ require ( github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect ) diff --git a/go.sum b/go.sum index 38b7b6f6..57355e89 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 81eeafc78df11d8feb20d7c75d42f11859b6aec3 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 10:56:38 +0800 Subject: [PATCH 18/28] update Signed-off-by: Patrick Zheng --- notation/errors.go => errors.go | 0 internal/mock_origin/mockfs/fs.go | 25 ------------------- notation/notation.go => notation.go | 0 notation/notation_test.go => notation_test.go | 0 4 files changed, 25 deletions(-) rename notation/errors.go => errors.go (100%) delete mode 100644 internal/mock_origin/mockfs/fs.go rename notation/notation.go => notation.go (100%) rename notation/notation_test.go => notation_test.go (100%) diff --git a/notation/errors.go b/errors.go similarity index 100% rename from notation/errors.go rename to errors.go diff --git a/internal/mock_origin/mockfs/fs.go b/internal/mock_origin/mockfs/fs.go deleted file mode 100644 index 7757c9bb..00000000 --- a/internal/mock_origin/mockfs/fs.go +++ /dev/null @@ -1,25 +0,0 @@ -package mockfs - -import ( - "io/fs" - "path/filepath" - - "github.com/notaryproject/notation-go/dir" -) - -type sysFSMock struct { - fs.FS - root string -} - -func (s sysFSMock) SysPath(items ...string) (string, error) { - pathItems := []string{s.root} - pathItems = append(pathItems, items...) - return filepath.Join(pathItems...), nil -} - -func NewSysFSMock(fsys fs.FS, root string) dir.SysFS { - return sysFSMock{ - FS: fsys, - root: root} -} diff --git a/notation/notation.go b/notation.go similarity index 100% rename from notation/notation.go rename to notation.go diff --git a/notation/notation_test.go b/notation_test.go similarity index 100% rename from notation/notation_test.go rename to notation_test.go From af27f20064cbc623058cd201102dfbbc8b74837a Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 15:26:35 +0800 Subject: [PATCH 19/28] updated per code review Signed-off-by: Patrick Zheng --- internal/mock/mocks.go | 2 +- notation.go | 85 +++++++++++++++---------------------- notation_test.go | 2 +- registry/interface.go | 2 +- registry/repository.go | 2 +- registry/repository_test.go | 2 +- 6 files changed, 40 insertions(+), 55 deletions(-) diff --git a/internal/mock/mocks.go b/internal/mock/mocks.go index 5c0c5f6a..a6a146d5 100644 --- a/internal/mock/mocks.go +++ b/internal/mock/mocks.go @@ -97,7 +97,7 @@ func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descrip return t.FetchSignatureBlobResponse, JwsSigEnvDescriptor, t.FetchSignatureBlobError } -func (t Repository) PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { +func (t Repository) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { return ocispec.Descriptor{}, ocispec.Descriptor{}, nil } diff --git a/notation.go b/notation.go index 4045dfa3..9d2c16f9 100644 --- a/notation.go +++ b/notation.go @@ -7,18 +7,19 @@ import ( "encoding/json" "errors" "fmt" - "reflect" - "strings" "time" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation-go/verification/trustpolicy" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" ) const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" +var errDoneVerification = errors.New("done verification") + // SignOptions contains parameters for Signer.Sign. type SignOptions struct { // Reference of the artifact that needs to be signed. @@ -49,11 +50,11 @@ type Signer interface { // remote. // The descriptor of the sign content is returned upon sucessful signing. func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts SignOptions) (ocispec.Descriptor, error) { - ociDesc, err := repo.Resolve(ctx, opts.ArtifactReference) + targetDesc, err := repo.Resolve(ctx, opts.ArtifactReference) if err != nil { return ocispec.Descriptor{}, err } - sig, signerInfo, err := signer.Sign(ctx, ociDesc, opts.SignatureMediaType, opts) + sig, signerInfo, err := signer.Sign(ctx, targetDesc, opts.SignatureMediaType, opts) if err != nil { return ocispec.Descriptor{}, err } @@ -61,12 +62,12 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig if err != nil { return ocispec.Descriptor{}, err } - _, _, err = repo.PushSignature(ctx, sig, opts.SignatureMediaType, ociDesc, annotations) + _, _, err = repo.PushSignature(ctx, opts.SignatureMediaType, sig, targetDesc, annotations) if err != nil { return ocispec.Descriptor{}, err } - return ociDesc, nil + return targetDesc, nil } // VerifyOptions contains parameters for Verifier.Verify. @@ -118,10 +119,6 @@ type VerificationOutcome struct { // and their results VerificationResults []*ValidationResult - // SignedAnnotations contains arbitrary metadata relating to the target - // artifact that was signed - SignedAnnotations map[string]string - // Error that caused the verification to fail (if it fails) Error error } @@ -130,6 +127,8 @@ type VerificationOutcome struct { type Verifier interface { // Verify verifies the signature blob and returns the artifact // descriptor upon successful verification. + // If signature == nil and the verification level is not 'skip', an error + // will be returned. Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) } @@ -137,48 +136,38 @@ type Verifier interface { // verification types (like integrity, authenticity, etc.) and return the // verification outcomes. // For more details on signature verification, see -// https://github.com/notaryproject/notaryproject/blob/main/trust-store-trust-policy-specification.md#signature-verification +// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) { var verificationOutcomes []*VerificationOutcome artifactRef := opts.ArtifactReference // passing nil signature to check 'skip' _, outcome, err := verifier.Verify(ctx, nil, opts) - if outcome == nil { - return ocispec.Descriptor{}, nil, err - } - verificationLevel := outcome.VerificationLevel - if verificationLevel.Name == trustpolicy.LevelSkip.Name { - verificationOutcomes = append(verificationOutcomes, &VerificationOutcome{VerificationLevel: verificationLevel}) - return ocispec.Descriptor{}, verificationOutcomes, nil + if err != nil { + if outcome == nil { + return ocispec.Descriptor{}, nil, err + } + } else if outcome.VerificationLevel.Name == trustpolicy.LevelSkip.Name { + return ocispec.Descriptor{}, []*VerificationOutcome{outcome}, nil } // get signature manifests var success bool - doneVerification := errors.New("done verification") - var verifiedSigBlobDesc ocispec.Descriptor + + var targetArtifactDesc ocispec.Descriptor artifactDescriptor, err := repo.Resolve(ctx, artifactRef) if err != nil { return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} } err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { - // if already verified successfully, no need to continue - if success { - return nil - } - - if len(signatureManifests) < 1 { - return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", artifactRef)} - } - // process signatures - for _, sigManifest := range signatureManifests { + for _, sigManifestDesc := range signatureManifests { // get signature envelope - sigBlob, sigBlobDesc, err := repo.FetchSignatureBlob(ctx, sigManifest) + sigBlob, sigBlobDesc, err := repo.FetchSignatureBlob(ctx, sigManifestDesc) if err != nil { return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %s", sigBlobDesc.Digest, artifactRef, err.Error())} } payloadArtifactDescriptor, outcome, err := verifier.Verify(ctx, sigBlob, opts) - if err == nil && !equal(&payloadArtifactDescriptor, &artifactDescriptor) { + if err == nil && !content.Equal(payloadArtifactDescriptor, artifactDescriptor) { err = errors.New("payloadArtifactDescriptor does not match artifactDescriptor") } if err != nil { @@ -195,14 +184,14 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op verificationOutcomes = append(verificationOutcomes, outcome) success = true // Descriptor of the signature blob that gets verified successfully - verifiedSigBlobDesc = sigBlobDesc + targetArtifactDesc = payloadArtifactDescriptor - return doneVerification + return errDoneVerification } return nil }) - if err != nil && !errors.Is(err, doneVerification) { + if err != nil && !errors.Is(err, errDoneVerification) { return ocispec.Descriptor{}, nil, err } @@ -210,33 +199,29 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if success { // signature verification succeeds if there is at least one good // signature - return verifiedSigBlobDesc, verificationOutcomes, nil + return targetArtifactDesc, verificationOutcomes, nil + } + + // At this point, it means no signature is associated with the reference + if len(verificationOutcomes) == 0 { + return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", artifactRef)} } return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} } func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, error) { - annotations := make(map[string]string) var thumbprints []string - certChain := signerInfo.CertificateChain - for _, cert := range certChain { + for _, cert := range signerInfo.CertificateChain { checkSum := sha256.Sum256(cert.Raw) - thumbprints = append(thumbprints, strings.ToLower(hex.EncodeToString(checkSum[:]))) + thumbprints = append(thumbprints, hex.EncodeToString(checkSum[:])) } val, err := json.Marshal(thumbprints) if err != nil { return nil, err } - annotations[annotationX509ChainThumbprint] = string(val) - - return annotations, nil -} -// Equal reports whether d and t points to the same content. -func equal(d *ocispec.Descriptor, t *ocispec.Descriptor) bool { - return d.MediaType == t.MediaType && - d.Digest == t.Digest && - d.Size == t.Size && - reflect.DeepEqual(d.Annotations, t.Annotations) + return map[string]string{ + annotationX509ChainThumbprint: string(val), + }, nil } diff --git a/notation_test.go b/notation_test.go index f0d29490..ddedfed5 100644 --- a/notation_test.go +++ b/notation_test.go @@ -48,7 +48,7 @@ func TestRegistryNoSignatureManifests(t *testing.T) { policyDocument := dummyPolicyDocument() repo := mock.NewRepository() verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} - errorMessage := fmt.Sprintf("no signatures are associated with %q, make sure the image was signed successfully", mock.SampleArtifactUri) + errorMessage := fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", mock.SampleArtifactUri) expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage} // mock the repository diff --git a/registry/interface.go b/registry/interface.go index 9fb4452d..b185d5df 100644 --- a/registry/interface.go +++ b/registry/interface.go @@ -23,5 +23,5 @@ type Repository interface { // PushSignature creates and uploads an signature manifest along with its // linked signature envelope blob. - PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) + PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) } diff --git a/registry/repository.go b/registry/repository.go index a153060b..b39e8c7d 100644 --- a/registry/repository.go +++ b/registry/repository.go @@ -70,7 +70,7 @@ func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec. // PushSignature creates and uploads an signature manifest along with its // linked signature envelope blob. Upon successful, PushSignature returns // signature envelope blob and manifest descriptors. -func (c *repositoryClient) PushSignature(ctx context.Context, blob []byte, mediaType string, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { +func (c *repositoryClient) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { blobDesc, err = oras.PushBytes(ctx, c.Repository.Blobs(), mediaType, blob) if err != nil { return ocispec.Descriptor{}, ocispec.Descriptor{}, err diff --git a/registry/repository_test.go b/registry/repository_test.go index 71100909..71062bb7 100644 --- a/registry/repository_test.go +++ b/registry/repository_test.go @@ -461,7 +461,7 @@ func TestPushSignature(t *testing.T) { ref, _ := registry.ParseReference(args.reference) client := newRepositoryClient(args.remoteClient, ref, args.plainHttp) - des, _, err := client.PushSignature(args.ctx, args.signature, args.signatureMediaType, args.subjectManifest, args.annotations) + des, _, err := client.PushSignature(args.ctx, args.signatureMediaType, args.signature, args.subjectManifest, args.annotations) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } From c9e4409f0810b4cd0bbbae19cd31d42da5a41141 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 16:27:46 +0800 Subject: [PATCH 20/28] update Signed-off-by: Patrick Zheng --- notation.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/notation.go b/notation.go index 9d2c16f9..6280ff8d 100644 --- a/notation.go +++ b/notation.go @@ -176,6 +176,8 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op outcome.Error = err verificationOutcomes = append(verificationOutcomes, outcome) } + // TODO: add log here to track when err != nil, + // but outcome == nil continue } From 537851b0f078a21051f100e7e928658d99d073f6 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 20:00:38 +0800 Subject: [PATCH 21/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/notation.go b/notation.go index 6280ff8d..4e779bfa 100644 --- a/notation.go +++ b/notation.go @@ -18,6 +18,8 @@ import ( const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" +const verificationSignatureNumLimit = 50 + var errDoneVerification = errors.New("done verification") // SignOptions contains parameters for Signer.Sign. @@ -104,8 +106,8 @@ type ValidationResult struct { // the verification level and results for each verification type that was // performed. type VerificationOutcome struct { - // SignatureBlobDescriptor is descriptor of the signature envelope blob - SignatureBlobDescriptor *ocispec.Descriptor + // RawSignature is the signature envelope blob + RawSignature []byte // EnvelopeContent contains the details of the digital signature and // associated metadata @@ -127,8 +129,8 @@ type VerificationOutcome struct { type Verifier interface { // Verify verifies the signature blob and returns the artifact // descriptor upon successful verification. - // If signature == nil and the verification level is not 'skip', an error - // will be returned. + // If nil signature is present and the verification level is not 'skip', + // an error will be returned. Verify(ctx context.Context, signature []byte, opts VerifyOptions) (ocispec.Descriptor, *VerificationOutcome, error) } @@ -151,40 +153,41 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op } // get signature manifests - var success bool - var targetArtifactDesc ocispec.Descriptor artifactDescriptor, err := repo.Resolve(ctx, artifactRef) if err != nil { return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} } + count := 0 err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { + if count >= verificationSignatureNumLimit { + return fmt.Errorf("number of signatures attempted to be verified should be less than: %d", verificationSignatureNumLimit) + } // process signatures for _, sigManifestDesc := range signatureManifests { // get signature envelope - sigBlob, sigBlobDesc, err := repo.FetchSignatureBlob(ctx, sigManifestDesc) + sigBlob, _, err := repo.FetchSignatureBlob(ctx, sigManifestDesc) if err != nil { - return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %s", sigBlobDesc.Digest, artifactRef, err.Error())} + return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %v", sigManifestDesc.Digest, artifactRef, err.Error())} } payloadArtifactDescriptor, outcome, err := verifier.Verify(ctx, sigBlob, opts) - if err == nil && !content.Equal(payloadArtifactDescriptor, artifactDescriptor) { - err = errors.New("payloadArtifactDescriptor does not match artifactDescriptor") - } if err != nil { - if outcome != nil { - outcome.SignatureBlobDescriptor = &sigBlobDesc - outcome.Error = err - verificationOutcomes = append(verificationOutcomes, outcome) + if outcome == nil { + // TODO: log fatal error + return err } - // TODO: add log here to track when err != nil, - // but outcome == nil + verificationOutcomes = append(verificationOutcomes, outcome) + continue + } + if !content.Equal(payloadArtifactDescriptor, artifactDescriptor) { + outcome.Error = errors.New("content descriptor mismatch") + verificationOutcomes = append(verificationOutcomes, outcome) continue } // At this point, we've found a signature verified successfully - outcome.SignatureBlobDescriptor = &sigBlobDesc + outcome.RawSignature = sigBlob verificationOutcomes = append(verificationOutcomes, outcome) - success = true // Descriptor of the signature blob that gets verified successfully targetArtifactDesc = payloadArtifactDescriptor @@ -198,13 +201,13 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op } // check whether verification was successful or not - if success { + if verificationOutcomes[len(verificationOutcomes)-1].Error == nil { // signature verification succeeds if there is at least one good // signature return targetArtifactDesc, verificationOutcomes, nil } - // At this point, it means no signature is associated with the reference + // If there's no signature associated with the reference if len(verificationOutcomes) == 0 { return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", artifactRef)} } From 8b7066e2eda2359efeac6d178bd3229cb171f77c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 10 Nov 2022 20:03:08 +0800 Subject: [PATCH 22/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notation.go b/notation.go index 4e779bfa..fb24d124 100644 --- a/notation.go +++ b/notation.go @@ -200,6 +200,11 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op return ocispec.Descriptor{}, nil, err } + // If there's no signature associated with the reference + if len(verificationOutcomes) == 0 { + return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", artifactRef)} + } + // check whether verification was successful or not if verificationOutcomes[len(verificationOutcomes)-1].Error == nil { // signature verification succeeds if there is at least one good @@ -207,11 +212,6 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op return targetArtifactDesc, verificationOutcomes, nil } - // If there's no signature associated with the reference - if len(verificationOutcomes) == 0 { - return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", artifactRef)} - } - return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} } From 55aee1a61c2e24753ca2c42d2acf239c2457aae9 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 11 Nov 2022 00:31:48 +0800 Subject: [PATCH 23/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/notation.go b/notation.go index fb24d124..0da16701 100644 --- a/notation.go +++ b/notation.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "time" "github.com/notaryproject/notation-core-go/signature" @@ -140,31 +141,32 @@ type Verifier interface { // For more details on signature verification, see // https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) { - var verificationOutcomes []*VerificationOutcome - artifactRef := opts.ArtifactReference // passing nil signature to check 'skip' _, outcome, err := verifier.Verify(ctx, nil, opts) if err != nil { if outcome == nil { return ocispec.Descriptor{}, nil, err } - } else if outcome.VerificationLevel.Name == trustpolicy.LevelSkip.Name { + } else if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) { return ocispec.Descriptor{}, []*VerificationOutcome{outcome}, nil } - // get signature manifests - var targetArtifactDesc ocispec.Descriptor + artifactRef := opts.ArtifactReference artifactDescriptor, err := repo.Resolve(ctx, artifactRef) if err != nil { return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()} } + + var verificationOutcomes []*VerificationOutcome + var targetArtifactDesc ocispec.Descriptor count := 0 err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { - if count >= verificationSignatureNumLimit { - return fmt.Errorf("number of signatures attempted to be verified should be less than: %d", verificationSignatureNumLimit) - } // process signatures for _, sigManifestDesc := range signatureManifests { + if count >= verificationSignatureNumLimit { + return ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", verificationSignatureNumLimit)} + } + count++ // get signature envelope sigBlob, _, err := repo.FetchSignatureBlob(ctx, sigManifestDesc) if err != nil { @@ -186,7 +188,6 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op } // At this point, we've found a signature verified successfully - outcome.RawSignature = sigBlob verificationOutcomes = append(verificationOutcomes, outcome) // Descriptor of the signature blob that gets verified successfully targetArtifactDesc = payloadArtifactDescriptor @@ -206,13 +207,10 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op } // check whether verification was successful or not - if verificationOutcomes[len(verificationOutcomes)-1].Error == nil { - // signature verification succeeds if there is at least one good - // signature - return targetArtifactDesc, verificationOutcomes, nil + if verificationOutcomes[len(verificationOutcomes)-1].Error != nil { + return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} } - - return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} + return targetArtifactDesc, verificationOutcomes, nil } func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, error) { From 01df7de9b0d0a4f6fc10a96e388c2f95571663ff Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 11 Nov 2022 08:51:49 +0800 Subject: [PATCH 24/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/notation.go b/notation.go index 0da16701..d89eaf52 100644 --- a/notation.go +++ b/notation.go @@ -19,8 +19,6 @@ import ( const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" -const verificationSignatureNumLimit = 50 - var errDoneVerification = errors.New("done verification") // SignOptions contains parameters for Signer.Sign. @@ -86,6 +84,11 @@ type VerifyOptions struct { // PluginConfig is a map of plugin configs. PluginConfig map[string]string + + // MaxVerificationLimit is the maximum number of signature envelopes that + // can be associated with the target artifact. If not set by user, it will + // be set to 50 by default. + MaxVerificationLimit int } // ValidationResult encapsulates the verification result (passed or failed) @@ -160,11 +163,15 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op var verificationOutcomes []*VerificationOutcome var targetArtifactDesc ocispec.Descriptor count := 0 + if opts.MaxVerificationLimit == 0 { + // Set MaxVerificationLimit to 50 as default + opts.MaxVerificationLimit = 50 + } err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { // process signatures for _, sigManifestDesc := range signatureManifests { - if count >= verificationSignatureNumLimit { - return ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", verificationSignatureNumLimit)} + if count >= opts.MaxVerificationLimit { + return ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", opts.MaxVerificationLimit)} } count++ // get signature envelope From 631731b36765f8ffc23627284c7af65034f11d3b Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 11 Nov 2022 11:22:37 +0800 Subject: [PATCH 25/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/notation.go b/notation.go index d89eaf52..dc5f22d9 100644 --- a/notation.go +++ b/notation.go @@ -19,6 +19,8 @@ import ( const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256" +const maxVerificationLimitDefault = 50 + var errDoneVerification = errors.New("done verification") // SignOptions contains parameters for Signer.Sign. @@ -88,6 +90,8 @@ type VerifyOptions struct { // MaxVerificationLimit is the maximum number of signature envelopes that // can be associated with the target artifact. If not set by user, it will // be set to 50 by default. + // Note: this option is scoped to notation.Verify(). verifier.Verify() is + // for signle signature verification, and therefore, does not use it. MaxVerificationLimit int } @@ -162,16 +166,16 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op var verificationOutcomes []*VerificationOutcome var targetArtifactDesc ocispec.Descriptor - count := 0 - if opts.MaxVerificationLimit == 0 { + if opts.MaxVerificationLimit <= 0 { // Set MaxVerificationLimit to 50 as default - opts.MaxVerificationLimit = 50 + opts.MaxVerificationLimit = maxVerificationLimitDefault } + count := 0 err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { // process signatures for _, sigManifestDesc := range signatureManifests { if count >= opts.MaxVerificationLimit { - return ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", opts.MaxVerificationLimit)} + break } count++ // get signature envelope @@ -201,6 +205,11 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op return errDoneVerification } + + if count >= opts.MaxVerificationLimit { + return ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", opts.MaxVerificationLimit)} + } + return nil }) From 73e2c5fde814f10e93dfb54feadcc10becf6189d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 11 Nov 2022 12:29:51 +0800 Subject: [PATCH 26/28] update Signed-off-by: Patrick Zheng --- notation.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/notation.go b/notation.go index dc5f22d9..cb6d4e75 100644 --- a/notation.go +++ b/notation.go @@ -93,6 +93,12 @@ type VerifyOptions struct { // Note: this option is scoped to notation.Verify(). verifier.Verify() is // for signle signature verification, and therefore, does not use it. MaxVerificationLimit int + + // ReturnOnFirstSuccess determines whether to end the verification on the + // first succeeded signature verification. It is set to true by default. + // Note: this option is scoped to notation.Verify(). verifier.Verify() is + // for signle signature verification, and therefore, does not use it. + ReturnOnFirstSuccess bool } // ValidationResult encapsulates the verification result (passed or failed) From 70a1cf9c51f953674a7f89a13b9729a4c4a9ab5b Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 11 Nov 2022 13:12:09 +0800 Subject: [PATCH 27/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/notation.go b/notation.go index cb6d4e75..f7b59836 100644 --- a/notation.go +++ b/notation.go @@ -87,18 +87,12 @@ type VerifyOptions struct { // PluginConfig is a map of plugin configs. PluginConfig map[string]string - // MaxVerificationLimit is the maximum number of signature envelopes that + // MaxSignatureAttempts is the maximum number of signature envelopes that // can be associated with the target artifact. If not set by user, it will // be set to 50 by default. // Note: this option is scoped to notation.Verify(). verifier.Verify() is // for signle signature verification, and therefore, does not use it. - MaxVerificationLimit int - - // ReturnOnFirstSuccess determines whether to end the verification on the - // first succeeded signature verification. It is set to true by default. - // Note: this option is scoped to notation.Verify(). verifier.Verify() is - // for signle signature verification, and therefore, does not use it. - ReturnOnFirstSuccess bool + MaxSignatureAttempts int } // ValidationResult encapsulates the verification result (passed or failed) @@ -172,15 +166,16 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op var verificationOutcomes []*VerificationOutcome var targetArtifactDesc ocispec.Descriptor - if opts.MaxVerificationLimit <= 0 { + if opts.MaxSignatureAttempts <= 0 { // Set MaxVerificationLimit to 50 as default - opts.MaxVerificationLimit = maxVerificationLimitDefault + opts.MaxSignatureAttempts = maxVerificationLimitDefault } + errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", opts.MaxSignatureAttempts)} count := 0 err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { // process signatures for _, sigManifestDesc := range signatureManifests { - if count >= opts.MaxVerificationLimit { + if count >= opts.MaxSignatureAttempts { break } count++ @@ -212,14 +207,17 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op return errDoneVerification } - if count >= opts.MaxVerificationLimit { - return ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", opts.MaxVerificationLimit)} + if count >= opts.MaxSignatureAttempts { + return errExceededMaxVerificationLimit } return nil }) if err != nil && !errors.Is(err, errDoneVerification) { + if errors.Is(err, errExceededMaxVerificationLimit) { + return ocispec.Descriptor{}, verificationOutcomes, err + } return ocispec.Descriptor{}, nil, err } @@ -232,6 +230,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op if verificationOutcomes[len(verificationOutcomes)-1].Error != nil { return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{} } + return targetArtifactDesc, verificationOutcomes, nil } From 23a6feb9e89cc131d1d4bd18eb60de0d3e85d24d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 11 Nov 2022 14:45:31 +0800 Subject: [PATCH 28/28] updated per code review Signed-off-by: Patrick Zheng --- notation.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notation.go b/notation.go index f7b59836..43637179 100644 --- a/notation.go +++ b/notation.go @@ -88,8 +88,8 @@ type VerifyOptions struct { PluginConfig map[string]string // MaxSignatureAttempts is the maximum number of signature envelopes that - // can be associated with the target artifact. If not set by user, it will - // be set to 50 by default. + // can be associated with the target artifact. If set to less than or equals + // to zero, value defaults to 50. // Note: this option is scoped to notation.Verify(). verifier.Verify() is // for signle signature verification, and therefore, does not use it. MaxSignatureAttempts int @@ -170,7 +170,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op // Set MaxVerificationLimit to 50 as default opts.MaxSignatureAttempts = maxVerificationLimitDefault } - errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("number of signatures associated with an artifact should be less than: %d", opts.MaxSignatureAttempts)} + errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("total number of signatures associated with an artifact should be less than: %d", opts.MaxSignatureAttempts)} count := 0 err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error { // process signatures