diff --git a/cmd/notation/inspect.go b/cmd/notation/inspect.go index 15e6b167c..3b7b3f4e1 100644 --- a/cmd/notation/inspect.go +++ b/cmd/notation/inspect.go @@ -7,8 +7,8 @@ import ( "errors" "fmt" "os" - "strings" "strconv" + "strings" "time" "github.com/notaryproject/notation-core-go/signature" @@ -98,22 +98,16 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { // initialize reference := opts.reference - sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference) + sigRepo, err := getRemoteRepository(ctx, &opts.SecureFlagOpts, reference) if err != nil { return err } - - manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo) + manifestDesc, resolvedRef, err := resolveReference(ctx, inputTypeRegistry, reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) { + fmt.Fprintf(os.Stderr, "Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref) + }) if err != nil { return err } - - // reference is a digest reference - if err := ref.ValidateReferenceAsDigest(); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref.Reference) - ref.Reference = manifestDesc.Digest.String() - } - output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}} skippedSignatures := false err = sigRepo.ListSignatures(ctx, manifestDesc, func(signatureManifests []ocispec.Descriptor) error { @@ -177,7 +171,7 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { return err } - err = printOutput(opts.outputFormat, ref.String(), output) + err = printOutput(opts.outputFormat, resolvedRef, output) if err != nil { return err } diff --git a/cmd/notation/internal/errors/errors.go b/cmd/notation/internal/errors/errors.go index 18d39e234..3445d0aef 100644 --- a/cmd/notation/internal/errors/errors.go +++ b/cmd/notation/internal/errors/errors.go @@ -12,3 +12,16 @@ func (e ErrorReferrersAPINotSupported) Error() string { } return "referrers API not supported" } + +// ErrorOCILayoutMissingReference is used when signing local content in oci +// layout folder but missing input tag or digest. +type ErrorOCILayoutMissingReference struct { + Msg string +} + +func (e ErrorOCILayoutMissingReference) Error() string { + if e.Msg != "" { + return e.Msg + } + return "reference is missing either digest or tag" +} diff --git a/cmd/notation/list.go b/cmd/notation/list.go index 92c5c8880..e06a087fe 100644 --- a/cmd/notation/list.go +++ b/cmd/notation/list.go @@ -7,21 +7,25 @@ import ( notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/internal/experimental" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" - "oras.land/oras-go/v2/registry" ) type listOpts struct { cmd.LoggingFlagOpts SecureFlagOpts reference string + ociLayout bool + inputType inputType } func listCommand(opts *listOpts) *cobra.Command { if opts == nil { - opts = &listOpts{} + opts = &listOpts{ + inputType: inputTypeRegistry, // remote registry by default + } } cmd := &cobra.Command{ Use: "list [flags] ", @@ -35,12 +39,20 @@ func listCommand(opts *listOpts) *cobra.Command { opts.reference = args[0] return nil }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.ociLayout { + opts.inputType = inputTypeOCILayout + } + return experimental.CheckFlagsAndWarn(cmd, "oci-layout") + }, RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), opts) }, } opts.LoggingFlagOpts.ApplyFlags(cmd.Flags()) opts.SecureFlagOpts.ApplyFlags(cmd.Flags()) + cmd.Flags().BoolVar(&opts.ociLayout, "oci-layout", false, "[Experimental] list signatures stored in OCI image layout") + experimental.HideFlags(cmd, "oci-layout") return cmd } @@ -50,23 +62,21 @@ func runList(ctx context.Context, opts *listOpts) error { // initialize reference := opts.reference - sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference) + sigRepo, err := getRepository(ctx, opts.inputType, reference, &opts.SecureFlagOpts) if err != nil { return err } - manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo) + targetDesc, resolvedRef, err := resolveReference(ctx, opts.inputType, reference, sigRepo, nil) if err != nil { return err } - // print all signature manifest digests - return printSignatureManifestDigests(ctx, manifestDesc, sigRepo, ref) + return printSignatureManifestDigests(ctx, targetDesc, sigRepo, resolvedRef) } // printSignatureManifestDigests returns the signature manifest digests of // the subject manifest. -func printSignatureManifestDigests(ctx context.Context, manifestDesc ocispec.Descriptor, sigRepo notationregistry.Repository, ref registry.Reference) error { - ref.Reference = manifestDesc.Digest.String() +func printSignatureManifestDigests(ctx context.Context, targetDesc ocispec.Descriptor, sigRepo notationregistry.Repository, ref string) error { titlePrinted := false printTitle := func() { if !titlePrinted { @@ -77,7 +87,7 @@ func printSignatureManifestDigests(ctx context.Context, manifestDesc ocispec.Des } var prevDigest digest.Digest - err := sigRepo.ListSignatures(ctx, manifestDesc, func(signatureManifests []ocispec.Descriptor) error { + err := sigRepo.ListSignatures(ctx, targetDesc, func(signatureManifests []ocispec.Descriptor) error { for _, sigManifestDesc := range signatureManifests { if prevDigest != "" { // check and print title diff --git a/cmd/notation/manifest.go b/cmd/notation/manifest.go index 84ac72742..2c171640e 100644 --- a/cmd/notation/manifest.go +++ b/cmd/notation/manifest.go @@ -3,34 +3,119 @@ package main import ( "context" "errors" + "fmt" + "os" + "strings" + "unicode" "github.com/notaryproject/notation-go/log" notationregistry "github.com/notaryproject/notation-go/registry" + notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/registry" ) -// getManifestDescriptor returns target artifact manifest descriptor and -// registry.Reference given user input reference. -func getManifestDescriptor(ctx context.Context, opts *SecureFlagOpts, reference string, sigRepo notationregistry.Repository) (ocispec.Descriptor, registry.Reference, error) { - logger := log.GetLogger(ctx) - +// resolveReference resolves user input reference based on user input type. +// Returns the resolved manifest descriptor and resolvedRef in digest +func resolveReference(ctx context.Context, inputType inputType, reference string, sigRepo notationregistry.Repository, fn func(string, ocispec.Descriptor)) (ocispec.Descriptor, string, error) { + // sanity check if reference == "" { - return ocispec.Descriptor{}, registry.Reference{}, errors.New("missing reference") + return ocispec.Descriptor{}, "", errors.New("missing user input reference") + } + var tagOrDigestRef string + var resolvedRef string + switch inputType { + case inputTypeRegistry: + ref, err := registry.ParseReference(reference) + if err != nil { + return ocispec.Descriptor{}, "", fmt.Errorf("failed to resolve user input reference: %w", err) + } + tagOrDigestRef = ref.Reference + resolvedRef = ref.Registry + "/" + ref.Repository + case inputTypeOCILayout: + layoutPath, layoutReference, err := parseOCILayoutReference(reference) + if err != nil { + return ocispec.Descriptor{}, "", fmt.Errorf("failed to resolve user input reference: %w", err) + } + layoutPathInfo, err := os.Stat(layoutPath) + if err != nil { + return ocispec.Descriptor{}, "", fmt.Errorf("failed to resolve user input reference: %w", err) + } + if !layoutPathInfo.IsDir() { + return ocispec.Descriptor{}, "", errors.New("failed to resolve user input reference: input path is not a dir") + } + tagOrDigestRef = layoutReference + resolvedRef = layoutPath + default: + return ocispec.Descriptor{}, "", fmt.Errorf("unsupported user inputType: %d", inputType) } - ref, err := registry.ParseReference(reference) + + manifestDesc, err := getManifestDescriptor(ctx, tagOrDigestRef, sigRepo) if err != nil { - return ocispec.Descriptor{}, registry.Reference{}, err + return ocispec.Descriptor{}, "", fmt.Errorf("failed to get manifest descriptor: %w", err) + } + resolvedRef = resolvedRef + "@" + manifestDesc.Digest.String() + if _, err := digest.Parse(tagOrDigestRef); err == nil { + // tagOrDigestRef is a digest reference + return manifestDesc, resolvedRef, nil } - if ref.Reference == "" { - return ocispec.Descriptor{}, registry.Reference{}, errors.New("reference is missing digest or tag") + // tagOrDigestRef is a tag reference + if fn != nil { + fn(tagOrDigestRef, manifestDesc) } + return manifestDesc, resolvedRef, nil +} - manifestDesc, err := sigRepo.Resolve(ctx, ref.Reference) - if err != nil { - return ocispec.Descriptor{}, registry.Reference{}, err +// resolveArtifactDigestReference creates reference in Verification given user input +// trust policy scope +func resolveArtifactDigestReference(reference, policyScope string) string { + if policyScope != "" { + if _, digest, ok := strings.Cut(reference, "@"); ok { + return policyScope + "@" + digest + } } + return reference +} - logger.Infof("Reference %s resolved to manifest descriptor: %+v", ref.Reference, manifestDesc) - return manifestDesc, ref, nil +// parseOCILayoutReference parses the raw in format of [:|@]. +// Returns the path to the OCI layout and the reference (tag or digest). +func parseOCILayoutReference(raw string) (string, string, error) { + var path string + var ref string + if idx := strings.LastIndex(raw, "@"); idx != -1 { + // `digest` found + path, ref = raw[:idx], raw[idx+1:] + } else { + // find `tag` + idx := strings.LastIndex(raw, ":") + if idx == -1 || (idx == 1 && len(raw) > 2 && unicode.IsLetter(rune(raw[0])) && raw[2] == '\\') { + return "", "", notationerrors.ErrorOCILayoutMissingReference{} + } else { + path, ref = raw[:idx], raw[idx+1:] + } + } + if path == "" { + return "", "", fmt.Errorf("found empty file path in %q", raw) + } + if ref == "" { + return "", "", fmt.Errorf("found empty reference in %q", raw) + } + return path, ref, nil +} + +// getManifestDescriptor returns target artifact manifest descriptor given +// reference (digest or tag) and Repository. +func getManifestDescriptor(ctx context.Context, reference string, sigRepo notationregistry.Repository) (ocispec.Descriptor, error) { + logger := log.GetLogger(ctx) + + if reference == "" { + return ocispec.Descriptor{}, errors.New("reference cannot be empty") + } + manifestDesc, err := sigRepo.Resolve(ctx, reference) + if err != nil { + return ocispec.Descriptor{}, err + } + logger.Infof("Reference %s resolved to manifest descriptor: %+v", reference, manifestDesc) + return manifestDesc, nil } diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index ea25eba4b..889dae540 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -21,9 +21,53 @@ import ( "oras.land/oras-go/v2/registry/remote/errcode" ) -const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" +// inputType denotes the user input type +type inputType int -func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference string) (notationregistry.Repository, error) { +const ( + inputTypeRegistry inputType = 1 + iota // inputType remote registry + inputTypeOCILayout // inputType oci-layout +) + +const ( + zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" +) + +// getRepository returns a notationregistry.Repository given user input type and +// user input reference +func getRepository(ctx context.Context, inputType inputType, reference string, opts *SecureFlagOpts) (notationregistry.Repository, error) { + switch inputType { + case inputTypeRegistry: + return getRemoteRepository(ctx, opts, reference) + case inputTypeOCILayout: + layoutPath, _, err := parseOCILayoutReference(reference) + if err != nil { + return nil, err + } + return notationregistry.NewOCIRepository(layoutPath, notationregistry.RepositoryOptions{}) + default: + return nil, errors.New("unsupported input type") + } +} + +// getRepositoryForSign returns a notationregistry.Repository given user input +// type and user input reference during Sign process +func getRepositoryForSign(ctx context.Context, inputType inputType, reference string, opts *SecureFlagOpts, ociImageManifest bool) (notationregistry.Repository, error) { + switch inputType { + case inputTypeRegistry: + return getRemoteRepositoryForSign(ctx, opts, reference, ociImageManifest) + case inputTypeOCILayout: + layoutPath, _, err := parseOCILayoutReference(reference) + if err != nil { + return nil, err + } + return notationregistry.NewOCIRepository(layoutPath, notationregistry.RepositoryOptions{OCIImageManifest: ociImageManifest}) + default: + return nil, errors.New("unsupported input type") + } +} + +func getRemoteRepository(ctx context.Context, opts *SecureFlagOpts, reference string) (notationregistry.Repository, error) { ref, err := registry.ParseReference(reference) if err != nil { return nil, err @@ -37,13 +81,13 @@ func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference return notationregistry.NewRepository(remoteRepo), nil } -// getSignatureRepositoryForSign returns a registry.Repository for Sign. +// getRemoteRepositoryForSign returns a registry.Repository for Sign. // ociImageManifest denotes the type of manifest used to store signatures during // Sign process. // Setting ociImageManifest to true means using OCI image manifest and the // Referrers tag schema. // Otherwise, use OCI artifact manifest and requires the Referrers API. -func getSignatureRepositoryForSign(ctx context.Context, opts *SecureFlagOpts, reference string, ociImageManifest bool) (notationregistry.Repository, error) { +func getRemoteRepositoryForSign(ctx context.Context, opts *SecureFlagOpts, reference string, ociImageManifest bool) (notationregistry.Repository, error) { logger := log.GetLogger(ctx) ref, err := registry.ParseReference(reference) if err != nil { diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 96aa1356e..cff93e07e 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -12,10 +12,10 @@ import ( notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/envelope" + "github.com/notaryproject/notation/internal/experimental" "github.com/notaryproject/notation/internal/slices" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" - "oras.land/oras-go/v2/registry" ) const ( @@ -36,11 +36,15 @@ type signOpts struct { userMetadata []string reference string signatureManifest string + ociLayout bool + inputType inputType } func signCommand(opts *signOpts) *cobra.Command { if opts == nil { - opts = &signOpts{} + opts = &signOpts{ + inputType: inputTypeRegistry, // remote registry by default + } } command := &cobra.Command{ Use: "sign [flags] ", @@ -64,6 +68,12 @@ Example - Sign an OCI artifact identified by a tag (Notation will resolve tag to Example - Sign an OCI artifact stored in a registry and specify the signature expiry duration, for example 24 hours notation sign --expiry 24h /@ +Example - [Experimental] Sign an OCI artifact referenced in an OCI layout + notation sign --oci-layout "@" + +Example - [Experimental] Sign an OCI artifact identified by a tag and referenced in an OCI layout + notation sign --oci-layout ":" + Example - [Experimental] Sign an OCI artifact and use OCI artifact manifest to store the signature: notation sign --signature-manifest artifact /@ `, @@ -74,6 +84,12 @@ Example - [Experimental] Sign an OCI artifact and use OCI artifact manifest to s opts.reference = args[0] return nil }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.ociLayout { + opts.inputType = inputTypeOCILayout + } + return experimental.CheckFlagsAndWarn(cmd, "signature-manifest", "oci-layout") + }, RunE: func(cmd *cobra.Command, args []string) error { // sanity check if !validateSignatureManifest(opts.signatureManifest) { @@ -89,6 +105,8 @@ Example - [Experimental] Sign an OCI artifact and use OCI artifact manifest to s cmd.SetPflagPluginConfig(command.Flags(), &opts.pluginConfig) command.Flags().StringVar(&opts.signatureManifest, "signature-manifest", signatureManifestImage, "[Experimental] manifest type for signature. options: \"image\", \"artifact\"") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataSignUsage) + command.Flags().BoolVar(&opts.ociLayout, "oci-layout", false, "[Experimental] sign the artifact stored as OCI image layout") + experimental.HideFlags(command, "signature-manifest", "oci-layout") return command } @@ -102,69 +120,65 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error { return err } ociImageManifest := cmdOpts.signatureManifest == signatureManifestImage - sigRepo, err := getSignatureRepositoryForSign(ctx, &cmdOpts.SecureFlagOpts, cmdOpts.reference, ociImageManifest) + sigRepo, err := getRepositoryForSign(ctx, cmdOpts.inputType, cmdOpts.reference, &cmdOpts.SecureFlagOpts, ociImageManifest) if err != nil { return err } - opts, ref, err := prepareSigningContent(ctx, cmdOpts, sigRepo) + signOpts, err := prepareSigningOpts(ctx, cmdOpts, sigRepo) if err != nil { return err } + manifestDesc, resolvedRef, err := resolveReference(ctx, cmdOpts.inputType, cmdOpts.reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) { + fmt.Fprintf(os.Stderr, "Warning: Always sign the artifact using digest(@sha256:...) rather than a tag(:%s) because tags are mutable and a tag reference can point to a different artifact than the one signed.\n", ref) + }) + if err != nil { + return err + } + signOpts.ArtifactReference = manifestDesc.Digest.String() // core process - _, err = notation.Sign(ctx, signer, sigRepo, opts) + _, err = notation.Sign(ctx, signer, sigRepo, signOpts) if err != nil { var errorPushSignatureFailed notation.ErrorPushSignatureFailed if errors.As(err, &errorPushSignatureFailed) { if !ociImageManifest { - return fmt.Errorf("%v. Possible reason: target registry does not support OCI artifact manifest. Try removing the flag `--signature-manifest artifact` to store signatures using OCI image manifest", err) + return fmt.Errorf("%v. Possible reason: OCI artifact manifest is not supported. Try removing the flag `--signature-manifest artifact` to store signatures using OCI image manifest", err) } if strings.Contains(err.Error(), referrersTagSchemaDeleteError) { - fmt.Fprintln(os.Stderr, "Warning: Removal of outdated referrers index is not supported by the remote registry. Garbage collection may be required.") + fmt.Fprintln(os.Stderr, "Warning: Removal of outdated referrers index from remote registry failed. Garbage collection may be required.") // write out - fmt.Println("Successfully signed", ref) + fmt.Println("Successfully signed", resolvedRef) return nil } } return err } - - // write out - fmt.Println("Successfully signed", ref) + fmt.Println("Successfully signed", resolvedRef) return nil } -func prepareSigningContent(ctx context.Context, opts *signOpts, sigRepo notationregistry.Repository) (notation.RemoteSignOptions, registry.Reference, error) { - ref, err := resolveReference(ctx, &opts.SecureFlagOpts, opts.reference, sigRepo, func(ref registry.Reference, manifestDesc ocispec.Descriptor) { - fmt.Fprintf(os.Stderr, "Warning: Always sign the artifact using digest(@sha256:...) rather than a tag(:%s) because tags are mutable and a tag reference can point to a different artifact than the one signed.\n", ref.Reference) - }) - if err != nil { - return notation.RemoteSignOptions{}, registry.Reference{}, err - } - +func prepareSigningOpts(ctx context.Context, opts *signOpts, sigRepo notationregistry.Repository) (notation.SignOptions, error) { mediaType, err := envelope.GetEnvelopeMediaType(opts.SignerFlagOpts.SignatureFormat) if err != nil { - return notation.RemoteSignOptions{}, registry.Reference{}, err + return notation.SignOptions{}, err } pluginConfig, err := cmd.ParseFlagMap(opts.pluginConfig, cmd.PflagPluginConfig.Name) if err != nil { - return notation.RemoteSignOptions{}, registry.Reference{}, err + return notation.SignOptions{}, err } userMetadata, err := cmd.ParseFlagMap(opts.userMetadata, cmd.PflagUserMetadata.Name) if err != nil { - return notation.RemoteSignOptions{}, registry.Reference{}, err + return notation.SignOptions{}, err } - - signOpts := notation.RemoteSignOptions{ - SignOptions: notation.SignOptions{ - ArtifactReference: ref.String(), + signOpts := notation.SignOptions{ + SignerSignOptions: notation.SignerSignOptions{ SignatureMediaType: mediaType, ExpiryDuration: opts.expiry, PluginConfig: pluginConfig, }, UserMetadata: userMetadata, } - return signOpts, ref, nil + return signOpts, nil } func validateSignatureManifest(signatureManifest string) bool { diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index 182bc1cca..bc1061b8d 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -1,7 +1,6 @@ package main import ( - "context" "errors" "fmt" "math" @@ -9,28 +8,34 @@ import ( "reflect" "github.com/notaryproject/notation-go" - notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation-go/verifier" "github.com/notaryproject/notation-go/verifier/trustpolicy" "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/internal/experimental" "github.com/notaryproject/notation/internal/ioutil" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" - "oras.land/oras-go/v2/registry" ) +const maxSignatureAttempts = math.MaxInt64 + type verifyOpts struct { cmd.LoggingFlagOpts SecureFlagOpts - reference string - pluginConfig []string - userMetadata []string + reference string + pluginConfig []string + userMetadata []string + ociLayout bool + trustPolicyScope string + inputType inputType } func verifyCommand(opts *verifyOpts) *cobra.Command { if opts == nil { - opts = &verifyOpts{} + opts = &verifyOpts{ + inputType: inputTypeRegistry, // remote registry by default + } } command := &cobra.Command{ Use: "verify [reference]", @@ -44,6 +49,12 @@ Example - Verify a signature on an OCI artifact identified by a digest: Example - Verify a signature on an OCI artifact identified by a tag (Notation will resolve tag to digest): notation verify /: + +Example - [Experimental] Verify a signature on an OCI artifact referenced in an OCI layout using trust policy statement specified by scope. + notation verify --oci-layout /@ --scope + +Example - [Experimental] Verify a signature on an OCI artifact identified by a tag and referenced in an OCI layout using trust policy statement specified by scope. + notation verify --oci-layout /: --scope `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -52,6 +63,12 @@ Example - Verify a signature on an OCI artifact identified by a tag (Notation w opts.reference = args[0] return nil }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.ociLayout { + opts.inputType = inputTypeOCILayout + } + return experimental.CheckFlagsAndWarn(cmd, "oci-layout", "scope") + }, RunE: func(cmd *cobra.Command, args []string) error { return runVerify(cmd, opts) }, @@ -60,6 +77,10 @@ Example - Verify a signature on an OCI artifact identified by a tag (Notation w opts.SecureFlagOpts.ApplyFlags(command.Flags()) command.Flags().StringArrayVar(&opts.pluginConfig, "plugin-config", nil, "{key}={value} pairs that are passed as it is to a plugin, if the verification is associated with a verification plugin, refer plugin documentation to set appropriate values") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataVerifyUsage) + command.Flags().BoolVar(&opts.ociLayout, "oci-layout", false, "[Experimental] verify the artifact stored as OCI image layout") + command.Flags().StringVar(&opts.trustPolicyScope, "scope", "", "[Experimental] set trust policy scope for artifact verification, required and can only be used when flag \"--oci-layout\" is set") + command.MarkFlagsRequiredTogether("oci-layout", "scope") + experimental.HideFlags(command, "oci-layout", "scope") return command } @@ -68,49 +89,55 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error { ctx := opts.LoggingFlagOpts.SetLoggerLevel(command.Context()) // initialize - reference := opts.reference - sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference) + verifier, err := verifier.NewFromConfig() if err != nil { return err } - // resolve the given reference and set the digest - ref, err := resolveReference(command.Context(), &opts.SecureFlagOpts, reference, sigRepo, func(ref registry.Reference, manifestDesc ocispec.Descriptor) { - fmt.Fprintf(os.Stderr, "Warning: Always verify the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref.Reference) - }) + // set up verification plugin config. + configs, err := cmd.ParseFlagMap(opts.pluginConfig, cmd.PflagPluginConfig.Name) if err != nil { return err } - // initialize verifier - verifier, err := verifier.NewFromConfig() + // set up user metadata + userMetadata, err := cmd.ParseFlagMap(opts.userMetadata, cmd.PflagUserMetadata.Name) if err != nil { return err } - // set up verification plugin config. - configs, err := cmd.ParseFlagMap(opts.pluginConfig, cmd.PflagPluginConfig.Name) + // core verify process + reference := opts.reference + sigRepo, err := getRepository(ctx, opts.inputType, reference, &opts.SecureFlagOpts) if err != nil { return err } - - // set up user metadata - userMetadata, err := cmd.ParseFlagMap(opts.userMetadata, cmd.PflagUserMetadata.Name) + // resolve the given reference and set the digest + _, resolvedRef, err := resolveReference(ctx, opts.inputType, reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) { + fmt.Fprintf(os.Stderr, "Warning: Always verify the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref) + }) if err != nil { return err } - - verifyOpts := notation.RemoteVerifyOptions{ - ArtifactReference: ref.String(), + intendedRef := resolveArtifactDigestReference(resolvedRef, opts.trustPolicyScope) + verifyOpts := notation.VerifyOptions{ + ArtifactReference: intendedRef, PluginConfig: configs, // TODO: need to change MaxSignatureAttempts as a user input flag or // a field in config.json - MaxSignatureAttempts: math.MaxInt64, + MaxSignatureAttempts: maxSignatureAttempts, UserMetadata: userMetadata, } - - // core verify process _, outcomes, err := notation.Verify(ctx, verifier, sigRepo, verifyOpts) + err = checkVerificationFailure(outcomes, resolvedRef, err) + if err != nil { + return err + } + reportVerificationSuccess(outcomes, resolvedRef) + return nil +} + +func checkVerificationFailure(outcomes []*notation.VerificationOutcome, printOut string, err error) error { // write out on failure if err != nil || len(outcomes) == 0 { if err != nil { @@ -119,9 +146,12 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error { return fmt.Errorf("signature verification failed: %w", err) } } - return fmt.Errorf("signature verification failed for all the signatures associated with %s", ref.String()) + return fmt.Errorf("signature verification failed for all the signatures associated with %s", printOut) } + return nil +} +func reportVerificationSuccess(outcomes []*notation.VerificationOutcome, printout string) { // write out on success outcome := outcomes[0] // print out warning for any failed result with logged verification action @@ -133,31 +163,11 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error { } } if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) { - fmt.Println("Trust policy is configured to skip signature verification for", ref.String()) + fmt.Println("Trust policy is configured to skip signature verification for", printout) } else { - fmt.Println("Successfully verified signature for", ref.String()) + fmt.Println("Successfully verified signature for", printout) printMetadataIfPresent(outcome) } - return nil -} - -func resolveReference(ctx context.Context, opts *SecureFlagOpts, reference string, sigRepo notationregistry.Repository, fn func(registry.Reference, ocispec.Descriptor)) (registry.Reference, error) { - manifestDesc, ref, err := getManifestDescriptor(ctx, opts, reference, sigRepo) - if err != nil { - return registry.Reference{}, err - } - - // reference is a digest reference - if err := ref.ValidateReferenceAsDigest(); err == nil { - return ref, nil - } - - // reference is a tag reference - fn(ref, manifestDesc) - // resolve tag to digest reference - ref.Reference = manifestDesc.Digest.String() - - return ref, nil } func printMetadataIfPresent(outcome *notation.VerificationOutcome) { diff --git a/go.mod b/go.mod index 1d52bb6a1..d542ce995 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/docker/docker-credential-helpers v0.7.0 github.com/notaryproject/notation-core-go v1.0.0-rc.2 - github.com/notaryproject/notation-go v1.0.0-rc.3 + github.com/notaryproject/notation-go v1.0.0-rc.3.0.20230419050135-cd1a135381c3 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc2 github.com/sirupsen/logrus v1.9.0 @@ -25,7 +25,7 @@ require ( github.com/veraison/go-cose v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.6.0 // indirect - golang.org/x/mod v0.8.0 // indirect + golang.org/x/mod v0.10.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index d728b8495..fc09c4315 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/notaryproject/notation-core-go v1.0.0-rc.2 h1:nNJuXa12jVNSSETjGNJEcZgv1NwY5ToYPo+c0P9syCI= github.com/notaryproject/notation-core-go v1.0.0-rc.2/go.mod h1:ASoc9KbJkSHLbKhO96lb0pIEWJRMZq9oprwBSZ0EAx0= -github.com/notaryproject/notation-go v1.0.0-rc.3 h1:J93pnI42xw6UzeeCn8a5r3j1n8n5nHjnM3GwrsHzjkQ= -github.com/notaryproject/notation-go v1.0.0-rc.3/go.mod h1:IlP9GVzPUavxljgJIWoHY0GY1unlqfee7tIiCbSem1w= +github.com/notaryproject/notation-go v1.0.0-rc.3.0.20230419050135-cd1a135381c3 h1:/cjZprMXiX0X7eChRB8BwTlq4CrYX0KJZkmOuls6hIQ= +github.com/notaryproject/notation-go v1.0.0-rc.3.0.20230419050135-cd1a135381c3/go.mod h1:ouYjFEbBRP4pXaebg9H7mfCLiiH+zOKshEu0FUNdkR4= 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.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= @@ -45,8 +45,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/experimental/experimental.go b/internal/experimental/experimental.go index 3bf0d30fa..1c31c6159 100644 --- a/internal/experimental/experimental.go +++ b/internal/experimental/experimental.go @@ -18,28 +18,19 @@ func IsDisabled() bool { return os.Getenv(envName) != enabled } -// Error returns an error for a disabled experimental feature. -func Error(description string) error { - return fmt.Errorf("%s is experimental and not enabled by default. To use, please set %s=%s environment variable", description, envName, enabled) -} - // CheckCommandAndWarn checks whether an experimental command can be run. func CheckCommandAndWarn(cmd *cobra.Command, _ []string) error { - if err := Check(func() (string, bool) { + return CheckAndWarn(func() (string, bool) { return fmt.Sprintf("%q", cmd.CommandPath()), true - }); err != nil { - return err - } - return Warn() + }) } // CheckFlagsAndWarn checks whether experimental flags can be run. func CheckFlagsAndWarn(cmd *cobra.Command, flags ...string) error { - if err := Check(func() (string, bool) { + return CheckAndWarn(func() (string, bool) { var changedFlags []string flagSet := cmd.Flags() for _, flag := range flags { - flagSet.MarkHidden(flag) if flagSet.Changed(flag) { changedFlags = append(changedFlags, "--"+flag) } @@ -49,27 +40,34 @@ func CheckFlagsAndWarn(cmd *cobra.Command, flags ...string) error { return "", false } return fmt.Sprintf("flag(s) %s in %q", strings.Join(changedFlags, ","), cmd.CommandPath()), true - }); err != nil { - return err - } - return Warn() + }) } -// Check checks whether a feature can be used. -func Check(doCheck func() (feature string, isExperimental bool)) error { - if IsDisabled() { - feature, isExperimental := doCheck() - if isExperimental { +// CheckAndWarn checks whether a feature can be used. +func CheckAndWarn(doCheck func() (feature string, isExperimental bool)) error { + feature, isExperimental := doCheck() + if isExperimental { + if IsDisabled() { // feature is experimental and disabled - return Error(feature) + return fmt.Errorf("%s is experimental and not enabled by default. To use, please set %s=%s environment variable", feature, envName, enabled) } - return nil + return warn() } return nil } -// Warn prints a warning message for using the experimental feature. -func Warn() error { - _, err := fmt.Fprintf(os.Stderr, "Caution: This feature is experimental and may not be fully tested or completed and may be deprecated. Report any issues to \"https://github/notaryproject/notation\"\n") +// warn prints a warning message for using the experimental feature. +func warn() error { + _, err := fmt.Fprintf(os.Stderr, "Warning: This feature is experimental and may not be fully tested or completed and may be deprecated. Report any issues to \"https://github/notaryproject/notation\"\n") return err } + +// HideFlags hide experimental flags when NOTATION_EXPERIMENTAL is disabled. +func HideFlags(cmd *cobra.Command, flags ...string) { + if IsDisabled() { + flagsSet := cmd.Flags() + for _, flag := range flags { + flagsSet.MarkHidden(flag) + } + } +} diff --git a/internal/osutil/file.go b/internal/osutil/file.go index b69f88721..139cffd3f 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -71,3 +71,13 @@ func CopyToDir(src, dst string) (int64, error) { } return io.Copy(destination, source) } + +// IsRegularFile checks if path is a regular file +func IsRegularFile(path string) (bool, error) { + fileStat, err := os.Stat(path) + if err != nil { + return false, err + } + + return fileStat.Mode().IsRegular(), nil +} diff --git a/specs/commandline/verify.md b/specs/commandline/verify.md index 7cb4e9526..065039230 100644 --- a/specs/commandline/verify.md +++ b/specs/commandline/verify.md @@ -41,7 +41,7 @@ Flags: -p, --password string password for registry operations (default to $NOTATION_PASSWORD if not specified) --plain-http registry access via plain HTTP --plugin-config stringArray {key}={value} pairs that are passed as it is to a plugin, if the verification is associated with a verification plugin, refer plugin documentation to set appropriate values - --scope string [Experimental] set trust policy scope for artifact verification, only required if flag "--oci-layout" is set + --scope string [Experimental] set trust policy scope for artifact verification, required and can only be used when flag "--oci-layout" is set -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) -m, --user-metadata stringArray user defined {key}={value} pairs that must be present in the signature for successful verification if provided -v, --verbose verbose mode diff --git a/test/e2e/internal/notation/host.go b/test/e2e/internal/notation/host.go index 88d2e6ae2..af7657585 100644 --- a/test/e2e/internal/notation/host.go +++ b/test/e2e/internal/notation/host.go @@ -14,6 +14,13 @@ import ( // vhost is the VirtualHost instance. type CoreTestFunc func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) +// GeneralTestFunc is the test function running in a VirtualHost agnostic to +// the Repository of the artifact. +// +// notation is an Executor isolated by $XDG_CONFIG_HOME. +// vhost is the VirtualHost instance. +type GeneralTestFunc func(notation *utils.ExecOpts, vhost *utils.VirtualHost) + // Host creates a virtualized notation testing host by modify // the "XDG_CONFIG_HOME" environment variable of the Executor. // @@ -33,6 +40,23 @@ func Host(options []utils.HostOption, fn CoreTestFunc) { fn(vhost.Executor, artifact, vhost) } +// GeneralHost creates a virtualized notation testing host by modify +// the "XDG_CONFIG_HOME" environment variable of the Executor. It's agnostic to +// the Repository of the artifact. +// +// options is the required testing environment options +// fn is the callback function containing the testing logic. +func GeneralHost(options []utils.HostOption, fn GeneralTestFunc) { + // create a notation vhost + vhost, err := createNotationHost(NotationBinPath, options...) + if err != nil { + panic(err) + } + + // run the main logic + fn(vhost.Executor, vhost) +} + // OldNotation create an old version notation ExecOpts in a VirtualHost // for testing forward compatibility. func OldNotation(options ...utils.HostOption) *utils.ExecOpts { @@ -75,6 +99,16 @@ func BaseOptions() []utils.HostOption { ) } +func BaseOptionsWithExperimental() []utils.HostOption { + return Opts( + AuthOption("", ""), + AddKeyOption("e2e.key", "e2e.crt"), + AddTrustStoreOption("e2e", filepath.Join(NotationE2ELocalKeysDir, "e2e.crt")), + AddTrustPolicyOption("trustpolicy.json"), + EnableExperimental(), + ) +} + // TestLoginOptions returns the BaseOptions with removing AuthOption and adding ConfigOption. // testing environment. func TestLoginOptions() []utils.HostOption { @@ -174,3 +208,11 @@ func authEnv(username, password string) map[string]string { "NOTATION_PASSWORD": password, } } + +// EnableExperimental enables experimental features. +func EnableExperimental() utils.HostOption { + return func(vhost *utils.VirtualHost) error { + vhost.UpdateEnv(map[string]string{"NOTATION_EXPERIMENTAL": "1"}) + return nil + } +} diff --git a/test/e2e/internal/notation/init.go b/test/e2e/internal/notation/init.go index 6e4fc6dea..0615cb806 100644 --- a/test/e2e/internal/notation/init.go +++ b/test/e2e/internal/notation/init.go @@ -28,6 +28,7 @@ const ( envKeyNotationPluginPath = "NOTATION_E2E_PLUGIN_PATH" envKeyNotationConfigPath = "NOTATION_E2E_CONFIG_PATH" envKeyOCILayoutPath = "NOTATION_E2E_OCI_LAYOUT_PATH" + envKeyOCILayoutTestPath = "NOTATION_E2E_OCI_LAYOUT_TEST_PATH" envKeyTestRepo = "NOTATION_E2E_TEST_REPO" envKeyTestTag = "NOTATION_E2E_TEST_TAG" ) @@ -47,6 +48,7 @@ var ( var ( OCILayoutPath string + OCILayoutTestPath string TestRepoUri string TestTag string RegistryStoragePath string @@ -55,6 +57,7 @@ var ( func init() { RegisterFailHandler(Fail) setUpRegistry() + setUpOCILayout() setUpNotationValues() } @@ -68,6 +71,10 @@ func setUpRegistry() { setValue(envKeyTestTag, &TestTag) } +func setUpOCILayout() { + setPathValue(envKeyOCILayoutTestPath, &OCILayoutTestPath) +} + func setUpNotationValues() { // set Notation binary path setPathValue(envKeyNotationBinPath, &NotationBinPath) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index b4e9b7ba9..4d9eb861e 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -92,6 +92,7 @@ trap cleanup EXIT # set environment variable for E2E testing export NOTATION_E2E_CONFIG_PATH=$CWD/testdata/config export NOTATION_E2E_OCI_LAYOUT_PATH=$CWD/testdata/registry/oci_layout +export NOTATION_E2E_OCI_LAYOUT_TEST_PATH=$CWD/testdata/oci-layout/e2e export NOTATION_E2E_TEST_REPO=e2e export NOTATION_E2E_TEST_TAG=v1 export NOTATION_E2E_PLUGIN_PATH=$CWD/plugin/bin/$PLUGIN_NAME diff --git a/test/e2e/suite/command/sign.go b/test/e2e/suite/command/sign.go index 72d79b924..67a75df6c 100644 --- a/test/e2e/suite/command/sign.go +++ b/test/e2e/suite/command/sign.go @@ -106,4 +106,48 @@ var _ = Describe("notation sign", func() { MatchErrKeyWords("signature verification failed for all the signatures") }) }) + + It("by digest with oci layout", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + const digest = "sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f" + ociLayoutReference := OCILayoutTestPath + "@" + digest + notation.Exec("sign", "--oci-layout", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + }) + }) + + It("by digest with oci layout and COSE format", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + const digest = "sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f" + ociLayoutReference := OCILayoutTestPath + "@" + digest + notation.Exec("sign", "--oci-layout", "--signature-format", "cose", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + }) + }) + + It("by tag with oci layout", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + ociLayoutReference := OCILayoutTestPath + ":" + TestTag + notation.Exec("sign", "--oci-layout", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + }) + }) + + It("by tag with oci layout and COSE format", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + ociLayoutReference := OCILayoutTestPath + ":" + TestTag + notation.Exec("sign", "--oci-layout", "--signature-format", "cose", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + }) + }) + + It("by digest with oci layout but without experimental", func() { + GeneralHost(BaseOptions(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + const digest = "sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f" + expectedErrMsg := "Error: flag(s) --oci-layout in \"notation sign\" is experimental and not enabled by default. To use, please set NOTATION_EXPERIMENTAL=1 environment variable\n" + ociLayoutReference := OCILayoutTestPath + "@" + digest + notation.ExpectFailure().Exec("sign", "--oci-layout", ociLayoutReference). + MatchErrContent(expectedErrMsg) + }) + }) }) diff --git a/test/e2e/suite/command/verify.go b/test/e2e/suite/command/verify.go index a2d7236ff..a9c6f0b65 100644 --- a/test/e2e/suite/command/verify.go +++ b/test/e2e/suite/command/verify.go @@ -49,4 +49,56 @@ var _ = Describe("notation verify", func() { MatchKeyWords(VerifySuccessfully) }) }) + + It("by digest with oci layout", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + const digest = "sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f" + ociLayoutReference := OCILayoutTestPath + "@" + digest + notation.Exec("sign", "--oci-layout", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + + experimentalMsg := "Warning: This feature is experimental and may not be fully tested or completed and may be deprecated. Report any issues to \"https://github/notaryproject/notation\"\n" + notation.Exec("verify", "--oci-layout", "--scope", "local/e2e", ociLayoutReference). + MatchKeyWords(VerifySuccessfully). + MatchErrKeyWords(experimentalMsg) + }) + }) + + It("by tag with oci layout and COSE format", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + ociLayoutReference := OCILayoutTestPath + ":" + TestTag + notation.Exec("sign", "--oci-layout", "--signature-format", "cose", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + + experimentalMsg := "Warning: This feature is experimental and may not be fully tested or completed and may be deprecated. Report any issues to \"https://github/notaryproject/notation\"\n" + notation.Exec("verify", "--oci-layout", "--scope", "local/e2e", ociLayoutReference). + MatchKeyWords(VerifySuccessfully). + MatchErrKeyWords(experimentalMsg) + }) + }) + + It("by digest with oci layout but without experimental", func() { + GeneralHost(BaseOptions(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + const digest = "sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f" + expectedErrMsg := "Error: flag(s) --oci-layout,--scope in \"notation verify\" is experimental and not enabled by default. To use, please set NOTATION_EXPERIMENTAL=1 environment variable\n" + ociLayoutReference := OCILayoutTestPath + "@" + digest + notation.ExpectFailure().Exec("verify", "--oci-layout", "--scope", "local/e2e", ociLayoutReference). + MatchErrContent(expectedErrMsg) + }) + }) + + It("by digest with oci layout but missing scope", func() { + GeneralHost(BaseOptionsWithExperimental(), func(notation *utils.ExecOpts, vhost *utils.VirtualHost) { + const digest = "sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f" + ociLayoutReference := OCILayoutTestPath + "@" + digest + notation.Exec("sign", "--oci-layout", ociLayoutReference). + MatchKeyWords(SignSuccessfully) + + experimentalMsg := "Warning: This feature is experimental and may not be fully tested or completed and may be deprecated. Report any issues to \"https://github/notaryproject/notation\"\n" + expectedErrMsg := "Error: if any flags in the group [oci-layout scope] are set they must all be set; missing [scope]" + notation.ExpectFailure().Exec("verify", "--oci-layout", ociLayoutReference). + MatchErrKeyWords(experimentalMsg). + MatchErrKeyWords(expectedErrMsg) + }) + }) }) diff --git a/test/e2e/suite/trustpolicy/registry_scope.go b/test/e2e/suite/trustpolicy/registry_scope.go index 24a0b7c1c..59c5ae3e4 100644 --- a/test/e2e/suite/trustpolicy/registry_scope.go +++ b/test/e2e/suite/trustpolicy/registry_scope.go @@ -29,7 +29,7 @@ var _ = Describe("notation trust policy registryScope test", func() { OldNotation().Exec("sign", artifact.ReferenceWithDigest()).MatchKeyWords(SignSuccessfully) notation.ExpectFailure().Exec("verify", artifact.ReferenceWithDigest()). - MatchErrKeyWords(`registry scope "localhost:5000\\test-repo" is not valid, make sure it is the fully qualified registry URL without the scheme/protocol. e.g domain.com/my/repository`) + MatchErrKeyWords(`registry scope "localhost:5000\\test-repo" is not valid, make sure it is a fully qualified registry URL without the scheme/protocol, e.g domain.com/my/repository OR a local trust policy scope, e.g local/myOCILayout`) }) }) diff --git a/test/e2e/testdata/oci-layout/e2e/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 b/test/e2e/testdata/oci-layout/e2e/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 new file mode 100644 index 000000000..765bc38c9 --- /dev/null +++ b/test/e2e/testdata/oci-layout/e2e/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 @@ -0,0 +1 @@ +Awesome Notation diff --git a/test/e2e/testdata/oci-layout/e2e/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f b/test/e2e/testdata/oci-layout/e2e/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f new file mode 100644 index 000000000..45028368d --- /dev/null +++ b/test/e2e/testdata/oci-layout/e2e/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f @@ -0,0 +1 @@ +{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"application/vnd.notation.config","blobs":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578","size":17,"annotations":{"org.opencontainers.image.title":"awesome-notation.txt"}}],"annotations":{"org.opencontainers.artifact.created":"2022-12-22T01:11:12Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/oci-layout/e2e/index.json b/test/e2e/testdata/oci-layout/e2e/index.json new file mode 100644 index 000000000..430948531 --- /dev/null +++ b/test/e2e/testdata/oci-layout/e2e/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f","size":417,"annotations":{"org.opencontainers.image.ref.name":"v1"}}]} \ No newline at end of file diff --git a/test/e2e/testdata/oci-layout/e2e/oci-layout b/test/e2e/testdata/oci-layout/e2e/oci-layout new file mode 100644 index 000000000..1343d370f --- /dev/null +++ b/test/e2e/testdata/oci-layout/e2e/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion":"1.0.0"} \ No newline at end of file