diff --git a/cmd/notation/internal/errors/errors.go b/cmd/notation/internal/errors/errors.go new file mode 100644 index 000000000..18d39e234 --- /dev/null +++ b/cmd/notation/internal/errors/errors.go @@ -0,0 +1,14 @@ +package errors + +// ErrorReferrersAPINotSupported is used when the target registry does not +// support the Referrers API +type ErrorReferrersAPINotSupported struct { + Msg string +} + +func (e ErrorReferrersAPINotSupported) Error() string { + if e.Msg != "" { + return e.Msg + } + return "referrers API not supported" +} diff --git a/cmd/notation/key_test.go b/cmd/notation/key_test.go index 45313905c..fe5785157 100644 --- a/cmd/notation/key_test.go +++ b/cmd/notation/key_test.go @@ -17,7 +17,7 @@ func TestKeyAddCommand_BasicArgs(t *testing.T) { if err := cmd.ParseFlags([]string{ "--plugin", expected.plugin, "--id", expected.id, - "-c", "pluginconfig", + "--plugin-config", "pluginconfig", expected.name}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } diff --git a/cmd/notation/notation b/cmd/notation/notation deleted file mode 100755 index 6cbf6e855..000000000 Binary files a/cmd/notation/notation and /dev/null differ diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 64f3d17e3..40788d003 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -8,16 +8,21 @@ import ( "github.com/notaryproject/notation-go/log" notationregistry "github.com/notaryproject/notation-go/registry" + notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" "github.com/notaryproject/notation/internal/trace" "github.com/notaryproject/notation/internal/version" loginauth "github.com/notaryproject/notation/pkg/auth" "github.com/notaryproject/notation/pkg/configutil" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/errcode" ) +const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference string) (notationregistry.Repository, error) { ref, err := registry.ParseReference(reference) if err != nil { @@ -25,34 +30,80 @@ func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference } // generate notation repository - return getRepositoryClient(ctx, opts, ref) + remoteRepo, err := getRepositoryClient(ctx, opts, ref) + if err != nil { + return nil, err + } + return notationregistry.NewRepository(remoteRepo), nil } -func getRegistryClient(ctx context.Context, opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) { - reg, err := remote.NewRegistry(serverAddress) +// getSignatureRepositoryForSign 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) { + logger := log.GetLogger(ctx) + ref, err := registry.ParseReference(reference) if err != nil { return nil, err } - reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, opts, reg.Reference) + // generate notation repository + remoteRepo, err := getRepositoryClient(ctx, opts, ref) if err != nil { return nil, err } - return reg, nil + + // Notation enforces the following two paths during Sign process: + // 1. OCI artifact manifest uses the Referrers API + // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers + // 2. OCI image manifest uses the Referrers Tag Schema + // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema + if !ociImageManifest { + logger.Info("Use OCI artifact manifest and Referrers API to store signature") + // ping Referrers API + if err := pingReferrersAPI(ctx, remoteRepo); err != nil { + return nil, err + } + logger.Info("Successfully pinged Referrers API on target registry") + } else { + logger.Info("Use OCI image manifest and Referrers Tag Schema to store signature") + if err := remoteRepo.SetReferrersCapability(false); err != nil { + return nil, err + } + } + repositoryOpts := notationregistry.RepositoryOptions{ + OCIImageManifest: ociImageManifest, + } + return notationregistry.NewRepositoryWithOptions(remoteRepo, repositoryOpts), nil } -func getRepositoryClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference) (notationregistry.Repository, error) { +func getRepositoryClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference) (*remote.Repository, error) { authClient, plainHTTP, err := getAuthClient(ctx, opts, ref) if err != nil { return nil, err } - remoteRepo := &remote.Repository{ + return &remote.Repository{ Client: authClient, Reference: ref, PlainHTTP: plainHTTP, + }, nil +} + +func getRegistryClient(ctx context.Context, opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) { + reg, err := remote.NewRegistry(serverAddress) + if err != nil { + return nil, err } - return notationregistry.NewRepository(remoteRepo), nil + + reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, opts, reg.Reference) + if err != nil { + return nil, err + } + return reg, nil } func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { @@ -127,3 +178,40 @@ func getSavedCreds(ctx context.Context, serverAddress string) (auth.Credential, return nativeStore.Get(serverAddress) } + +func pingReferrersAPI(ctx context.Context, remoteRepo *remote.Repository) error { + logger := log.GetLogger(ctx) + if err := remoteRepo.SetReferrersCapability(true); err != nil { + return err + } + var checkReferrerDesc ocispec.Descriptor + checkReferrerDesc.Digest = zeroDigest + // core process + err := remoteRepo.Referrers(ctx, checkReferrerDesc, "", func(referrers []ocispec.Descriptor) error { + return nil + }) + if err != nil { + var errResp *errcode.ErrorResponse + if !errors.As(err, &errResp) || errResp.StatusCode != http.StatusNotFound { + return err + } + if isErrorCode(errResp, errcode.ErrorCodeNameUnknown) { + // The repository is not found in the target registry. + // This is triggered when putting signatures to an empty repository. + // For notation, this path should never be triggered. + return err + } + // A 404 returned by Referrers API indicates that Referrers API is + // not supported. + logger.Infof("failed to ping Referrers API with error: %v", err) + errMsg := "Target registry does not support the Referrers API. Try the flag `--signature-manifest image` to store signatures using OCI image manifest for backwards compatibility" + return notationerrors.ErrorReferrersAPINotSupported{Msg: errMsg} + } + return nil +} + +// isErrorCode returns true if err is an Error and its Code equals to code. +func isErrorCode(err error, code string) bool { + var ec errcode.Error + return errors.As(err, &ec) && ec.Code == code +} diff --git a/cmd/notation/registry_test.go b/cmd/notation/registry_test.go new file mode 100644 index 000000000..41699d272 --- /dev/null +++ b/cmd/notation/registry_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + + notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/errcode" +) + +func TestRegistry_pingReferrersAPI_Success(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "test": "TEST" }`)) + return + } + t.Errorf("unexpected access: %s %q", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + repo, err := remote.NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + ctx := context.Background() + err = pingReferrersAPI(ctx, repo) + if err != nil { + t.Errorf("pingReferrersAPI() expected nil error, but got error: %v", err) + } +} + +func TestRegistry_pingReferrersAPI_ReferrersAPINotSupported(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{ "errorresponse": { "method": "GET", "statuscode": 404 } }`)) + return + } + t.Errorf("unexpected access: %s %q", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + ctx := context.Background() + repo, err := remote.NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + err = pingReferrersAPI(ctx, repo) + var errorReferrersAPINotSupported notationerrors.ErrorReferrersAPINotSupported + if err == nil || !errors.As(err, &errorReferrersAPINotSupported) { + t.Errorf("pingReferrersAPI() expected ErrorReferrersAPINotSupported, but got: %v", err) + } +} + +func TestRegistry_pingReferrersAPI_Failed(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { + w.WriteHeader(http.StatusOK) + return + } + t.Errorf("unexpected access: %s %q", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + ctx := context.Background() + repo, err := remote.NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + err = pingReferrersAPI(ctx, repo) + if err == nil { + t.Errorf("pingReferrersAPI expected to get error but got nil") + } +} + +func TestRegistry_pingReferrersAPI_RepositoryNotFound(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) + return + } + t.Errorf("unexpected access: %s %q", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + ctx := context.Background() + expectedErr := errcode.Error{ + Code: errcode.ErrorCodeNameUnknown, + Message: "repository name not known to registry", + } + + repo, err := remote.NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + err = pingReferrersAPI(ctx, repo) + if err == nil { + t.Fatalf("pingReferrersAPI() expected error but got nil") + } + var ec errcode.Error + if !errors.As(err, &ec) { + t.Errorf("pingReferrersAPI() expected errcode.Error") + } + if !reflect.DeepEqual(ec, expectedErr) { + t.Errorf("pingReferrersAPI() expected error: %v, but got: %v", expectedErr, err) + } +} diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 9db9ea362..5d8ec3f35 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -11,19 +11,28 @@ 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/slices" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" "oras.land/oras-go/v2/registry" ) +const ( + signatureManifestArtifact = "artifact" + signatureManifestImage = "image" +) + +var supportedSignatureManifest = []string{signatureManifestArtifact, signatureManifestImage} + type signOpts struct { cmd.LoggingFlagOpts cmd.SignerFlagOpts SecureFlagOpts - expiry time.Duration - pluginConfig []string - userMetadata []string - reference string + expiry time.Duration + pluginConfig []string + userMetadata []string + reference string + signatureManifest string } func signCommand(opts *signOpts) *cobra.Command { @@ -51,6 +60,9 @@ 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 - Sign an OCI artifact and use OCI image manifest to store the signature, with the default JWS envelope: + notation sign --signature-manifest image /@ `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -60,6 +72,10 @@ Example - Sign an OCI artifact stored in a registry and specify the signature ex return nil }, RunE: func(cmd *cobra.Command, args []string) error { + // sanity check + if !validateSignatureManifest(opts.signatureManifest) { + return fmt.Errorf("signature manifest must be one of the following %v but got %s", supportedSignatureManifest, opts.signatureManifest) + } return runSign(cmd, opts) }, } @@ -68,6 +84,7 @@ Example - Sign an OCI artifact stored in a registry and specify the signature ex opts.SecureFlagOpts.ApplyFlags(command.Flags()) cmd.SetPflagExpiry(command.Flags(), &opts.expiry) cmd.SetPflagPluginConfig(command.Flags(), &opts.pluginConfig) + command.Flags().StringVar(&opts.signatureManifest, "signature-manifest", signatureManifestArtifact, "manifest type for signatures. options: artifact, image") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataSignUsage) return command } @@ -81,7 +98,8 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error { if err != nil { return err } - sigRepo, err := getSignatureRepository(ctx, &cmdOpts.SecureFlagOpts, cmdOpts.reference) + ociImageManifest := cmdOpts.signatureManifest == signatureManifestImage + sigRepo, err := getSignatureRepositoryForSign(ctx, &cmdOpts.SecureFlagOpts, cmdOpts.reference, ociImageManifest) if err != nil { return err } @@ -93,6 +111,10 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error { // core process _, err = notation.Sign(ctx, signer, sigRepo, opts) if err != nil { + var errorPushSignatureFailed notation.ErrorPushSignatureFailed + if errors.As(err, &errorPushSignatureFailed) { + return fmt.Errorf("%v. Target registry does not seem to support OCI artifact manifest. Try the flag `--signature-manifest image` to store signatures using OCI image manifest for backwards compatibility", err) + } return err } @@ -133,3 +155,7 @@ func prepareSigningContent(ctx context.Context, opts *signOpts, sigRepo notation } return signOpts, ref, nil } + +func validateSignatureManifest(signatureManifest string) bool { + return slices.Contains(supportedSignatureManifest, signatureManifest) +} diff --git a/cmd/notation/sign_test.go b/cmd/notation/sign_test.go index 39b936b0a..e6e69e8d2 100644 --- a/cmd/notation/sign_test.go +++ b/cmd/notation/sign_test.go @@ -23,6 +23,7 @@ func TestSignCommand_BasicArgs(t *testing.T) { Key: "key", SignatureFormat: envelope.JWS, }, + signatureManifest: "artifact", } if err := command.ParseFlags([]string{ expected.reference, @@ -53,7 +54,8 @@ func TestSignCommand_MoreArgs(t *testing.T) { Key: "key", SignatureFormat: envelope.COSE, }, - expiry: 24 * time.Hour, + expiry: 24 * time.Hour, + signatureManifest: signatureManifestImage, } if err := command.ParseFlags([]string{ expected.reference, @@ -62,7 +64,8 @@ func TestSignCommand_MoreArgs(t *testing.T) { "--key", expected.Key, "--plain-http", "--signature-format", expected.SignerFlagOpts.SignatureFormat, - "--expiry", expected.expiry.String()}); err != nil { + "--expiry", expected.expiry.String(), + "--signature-manifest", signatureManifestImage}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } if err := command.Args(command, command.Flags().Args()); err != nil { @@ -80,10 +83,11 @@ func TestSignCommand_CorrectConfig(t *testing.T) { reference: "ref", SignerFlagOpts: cmd.SignerFlagOpts{ Key: "key", - SignatureFormat: envelope.JWS, + SignatureFormat: envelope.COSE, }, - expiry: 365 * 24 * time.Hour, - pluginConfig: []string{"key0=val0", "key1=val1"}, + expiry: 365 * 24 * time.Hour, + pluginConfig: []string{"key0=val0", "key1=val1"}, + signatureManifest: "artifact", } if err := command.ParseFlags([]string{ expected.reference, diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index d49b94157..182bc1cca 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -58,7 +58,7 @@ Example - Verify a signature on an OCI artifact identified by a tag (Notation w } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) opts.SecureFlagOpts.ApplyFlags(command.Flags()) - command.Flags().StringArrayVarP(&opts.pluginConfig, "plugin-config", "c", 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") + 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) return command } diff --git a/go.sum b/go.sum index 376669868..07a799fa8 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ 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.1 h1:ACi0gr6mD1bzp9+gu3P0meJ/N6iWHlyM9zgtdnooNAA= github.com/notaryproject/notation-core-go v1.0.0-rc.1/go.mod h1:n8Gbvl9sKa00KptkKEL5XKUyMTIALe74QipKauE2rj4= -github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230203031935-510def1a3f48 h1:MHjaRqAn+uCBYkDuIGaVo91CnJY9MlTcZdYFfoE4yek= -github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230203031935-510def1a3f48/go.mod h1:B/26FcjJ9GVXm1j7z+/pWKck80LdFi3KiX4Zu7gixB8= github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230208032042-6ef3544efa06 h1:0AuNQ3303yvINJSEzHUrLHSsJOyAEJvCGUit44GhERk= github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230208032042-6ef3544efa06/go.mod h1:B/26FcjJ9GVXm1j7z+/pWKck80LdFi3KiX4Zu7gixB8= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 561d206e9..45e086230 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -64,12 +64,11 @@ var ( } PflagPluginConfig = &pflag.Flag{ - Name: "plugin-config", - Shorthand: "c", - Usage: "{key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values", + Name: "plugin-config", + Usage: "{key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values", } SetPflagPluginConfig = func(fs *pflag.FlagSet, p *[]string) { - fs.StringArrayVarP(p, PflagPluginConfig.Name, PflagPluginConfig.Shorthand, nil, PflagPluginConfig.Usage) + fs.StringArrayVar(p, PflagPluginConfig.Name, nil, PflagPluginConfig.Usage) } PflagUserMetadata = &pflag.Flag{ diff --git a/internal/slices/slices.go b/internal/slices/slices.go new file mode 100644 index 000000000..0d348cdf1 --- /dev/null +++ b/internal/slices/slices.go @@ -0,0 +1,11 @@ +package slices + +// Contains reports whether v is present in s. +func Contains[E comparable](s []E, v E) bool { + for _, vs := range s { + if v == vs { + return true + } + } + return false +} diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go new file mode 100644 index 000000000..2c19ff559 --- /dev/null +++ b/internal/slices/slices_test.go @@ -0,0 +1,25 @@ +package slices + +import ( + "testing" +) + +func TestContainerElement(t *testing.T) { + tests := []struct { + c []string + v string + want bool + }{ + {nil, "", false}, + {[]string{}, "", false}, + {[]string{"1", "2", "3"}, "4", false}, + {[]string{"1", "2", "3"}, "2", true}, + {[]string{"1", "2", "2", "3"}, "2", true}, + {[]string{"1", "2", "3", "2"}, "2", true}, + } + for _, tt := range tests { + if got := Contains(tt.c, tt.v); got != tt.want { + t.Errorf("ContainerElement() = %v, want %v", got, tt.want) + } + } +} diff --git a/specs/commandline/list.md b/specs/commandline/list.md index e62c60c74..4b1b39f23 100644 --- a/specs/commandline/list.md +++ b/specs/commandline/list.md @@ -27,10 +27,12 @@ Aliases: list, ls Flags: + -d, --debug debug mode -h, --help help for list -p, --password string password for registry operations (default to $NOTATION_PASSWORD if not specified) --plain-http registry access via plain HTTP -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) + -v, --verbose verbose mode ``` ## Usage diff --git a/specs/commandline/sign.md b/specs/commandline/sign.md index 8abf0de85..95be17a21 100644 --- a/specs/commandline/sign.md +++ b/specs/commandline/sign.md @@ -28,27 +28,29 @@ Usage: notation sign [flags] Flags: - -e, --expiry duration optional expiry that provides a "best by use" time for the artifact. The duration is specified in minutes(m) and/or hours(h). For example: 12h, 30m, 3h20m - -h, --help help for sign - --image-spec string manifest type for signatures. options: v1.1-artifact, v1.1-image (default: v1.1-artifact) - -k, --key string signing key name, for a key previously added to notation's key list. - -p, --password string password for registry operations (default to $NOTATION_PASSWORD if not specified) - --plain-http registry access via plain HTTP - --plugin-config strings {key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values - --signature-format string signature envelope format, options: 'jws', 'cose' (default "jws") - -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) - -m, --user-metadata strings {key}={value} pairs that are added to the signature payload + -d, --debug debug mode + -e, --expiry duration optional expiry that provides a "best by use" time for the artifact. The duration is specified in minutes(m) and/or hours(h). For example: 12h, 30m, 3h20m + -h, --help help for sign + -k, --key string signing key name, for a key previously added to notation's key list. + -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, refer plugin's documentation to set appropriate values + --signature-format string signature envelope format, options: 'jws', 'cose' (default "jws") + --signature-manifest string manifest type for signatures. options: artifact, image (default "artifact") + -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) + -m, --user-metadata stringArray {key}={value} pairs that are added to the signature payload + -v, --verbose verbose mode ``` ## Use OCI image manifest to store signatures -By default, Notation uses [OCI artifact manifest][oci-artifact-manifest] to store signatures in registries. For backward compatibility, Notation supports using `OCI image manifest` to store signatures in registries that partially implement the [OCI Image specification v1.1][oci-image-spec]. Use flag `--image-spec v1.1-image` to force Notation to store the signatures using OCI image manifest. +By default, Notation uses [OCI artifact manifest][oci-artifact-manifest] to store signatures in registries. For backward compatibility, Notation supports using `OCI image manifest` to store signatures in registries that partially implement the [OCI Image specification v1.1][oci-image-spec]. Use flag `--signature-manifest image` to force Notation to store the signatures using OCI image manifest. Registries MAY not implement or enable the `Referrers API`, which is used by clients to fetch referrers. In the context of Notation, the referrers are signatures. Notation follows the fallback procedure defined in [OCI distribution spec][oci-backward-compatibility] if `Referrers API` is unavailable. ### Set config property for OCI image manifest -OCI image manifest requires additional property `config` of type `descriptor`, which is not required by OCI artifact manifest. Notation creates a default config descriptor for the user if flag `--image-spec v1.1-image` is used. +OCI image manifest requires additional property `config` of type `descriptor`, which is not required by OCI artifact manifest. Notation creates a default config descriptor for the user if flag `--signature-manifest image` is used. Notation uses empty JSON object `{}` as the default configuration content, and thus the default `config` property is fixed, as following: @@ -62,9 +64,9 @@ Notation uses empty JSON object `{}` as the default configuration content, and t ### When to use OCI image manifest -[Registry support][registry-support] lists registries with different compatibilities. For registries not supporting `OCI artifact manifest`, users can use flag `--image-spec v1.1-image` to sign artifacts stored in those registries. +[Registry support][registry-support] lists registries with different compatibilities. For registries not supporting `OCI artifact manifest`, users can use flag `--signature-manifest image` to sign artifacts stored in those registries. -For registries not listed in the page, users can consider using flag `--image-spec v1.1-image` by checking the error message. Note that there is no deterministic way to determine whether a registry supports `OCI artifact manifest` or not. The error message is just for reference. The following response status contained in error messages MAY indicate that the registry doesn't support `OCI artifact manifest`: +For registries not listed in the page, users can consider using flag `--signature-manifest image` by checking the error message. Note that there is no deterministic way to determine whether a registry supports `OCI artifact manifest` or not. The error message is just for reference. The following response status contained in error messages MAY indicate that the registry doesn't support `OCI artifact manifest`: - Response status `400 BAD Request` with error code `MANIFEST_INVALID` or `UNSUPPORTED` @@ -161,7 +163,7 @@ Successfully signed localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da ### Sign an artifact and store the signature using OCI image manifest ```shell -notation sign --image-spec v1.1-image /@ +notation sign --signature-manifest image /@ ``` [oci-artifact-manifest]: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md diff --git a/specs/commandline/verify.md b/specs/commandline/verify.md index db5eb2d76..3741cb966 100644 --- a/specs/commandline/verify.md +++ b/specs/commandline/verify.md @@ -35,12 +35,14 @@ Usage: notation verify [flags] Flags: - -h, --help help for verify - -p, --password string password for registry operations (default to $NOTATION_PASSWORD if not specified) - --plain-http registry access via plain HTTP - --plugin-config strings {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 - -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) - -m, --user-metadata strings user defined {key}={value} pairs that must be present in the signature for successful verification if provided + -d, --debug debug mode + -h, --help help for verify + -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 + -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 ``` ## Usage