diff --git a/cmd/notation/inspect.go b/cmd/notation/inspect.go new file mode 100644 index 000000000..15e6b167c --- /dev/null +++ b/cmd/notation/inspect.go @@ -0,0 +1,302 @@ +package main + +import ( + "crypto/sha1" + b64 "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "os" + "strings" + "strconv" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/plugin/proto" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/internal/envelope" + "github.com/notaryproject/notation/internal/ioutil" + "github.com/notaryproject/notation/internal/tree" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" +) + +type inspectOpts struct { + cmd.LoggingFlagOpts + SecureFlagOpts + reference string + outputFormat string +} + +type inspectOutput struct { + MediaType string `json:"mediaType"` + Signatures []signatureOutput +} + +type signatureOutput struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + SignatureAlgorithm string `json:"signatureAlgorithm"` + SignedAttributes map[string]string `json:"signedAttributes"` + UserDefinedAttributes map[string]string `json:"userDefinedAttributes"` + UnsignedAttributes map[string]string `json:"unsignedAttributes"` + Certificates []certificateOutput `json:"certificates"` + SignedArtifact ocispec.Descriptor `json:"signedArtifact"` +} + +type certificateOutput struct { + SHA1Fingerprint string `json:"SHA1Fingerprint"` + IssuedTo string `json:"issuedTo"` + IssuedBy string `json:"issuedBy"` + Expiry string `json:"expiry"` +} + +func inspectCommand(opts *inspectOpts) *cobra.Command { + if opts == nil { + opts = &inspectOpts{} + } + command := &cobra.Command{ + Use: "inspect [reference]", + Short: "Inspect all signatures associated with the signed artifact", + Long: `Inspect all signatures associated with the signed artifact. + +Example - Inspect signatures on an OCI artifact identified by a digest: + notation inspect /@ + +Example - Inspect signatures on an OCI artifact identified by a tag (Notation will resolve tag to digest): + notation inspect /: + +Example - Inspect signatures on an OCI artifact identified by a digest and output as json: + notation inspect --output json /@ +`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing reference") + } + opts.reference = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runInspect(cmd, opts) + }, + } + + opts.LoggingFlagOpts.ApplyFlags(command.Flags()) + opts.SecureFlagOpts.ApplyFlags(command.Flags()) + cmd.SetPflagOutput(command.Flags(), &opts.outputFormat, cmd.PflagOutputUsage) + return command +} + +func runInspect(command *cobra.Command, opts *inspectOpts) error { + // set log level + ctx := opts.LoggingFlagOpts.SetLoggerLevel(command.Context()) + + if opts.outputFormat != cmd.OutputJSON && opts.outputFormat != cmd.OutputPlaintext { + return fmt.Errorf("unrecognized output format %s", opts.outputFormat) + } + + // initialize + reference := opts.reference + sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference) + if err != nil { + return err + } + + manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo) + 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 { + for _, sigManifestDesc := range signatureManifests { + sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: unable to fetch signature %s due to error: %v\n", sigManifestDesc.Digest.String(), err) + skippedSignatures = true + continue + } + + sigEnvelope, err := signature.ParseEnvelope(sigDesc.MediaType, sigBlob) + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + envelopeContent, err := sigEnvelope.Content() + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload) + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm) + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + sig := signatureOutput{ + MediaType: sigDesc.MediaType, + Digest: sigManifestDesc.Digest.String(), + SignatureAlgorithm: string(signatureAlgorithm), + SignedAttributes: getSignedAttributes(opts.outputFormat, envelopeContent), + UserDefinedAttributes: signedArtifactDesc.Annotations, + UnsignedAttributes: getUnsignedAttributes(envelopeContent), + Certificates: getCertificates(opts.outputFormat, envelopeContent), + SignedArtifact: *signedArtifactDesc, + } + + // clearing annotations from the SignedArtifact field since they're already + // displayed as UserDefinedAttributes + sig.SignedArtifact.Annotations = nil + + output.Signatures = append(output.Signatures, sig) + } + return nil + }) + + if err != nil { + return err + } + + err = printOutput(opts.outputFormat, ref.String(), output) + if err != nil { + return err + } + + if skippedSignatures { + return errors.New("at least one signature was skipped and not displayed") + } + + return nil +} + +func logSkippedSignature(sigDesc ocispec.Descriptor, err error) { + fmt.Fprintf(os.Stderr, "Warning: Skipping signature %s because of error: %v\n", sigDesc.Digest.String(), err) +} + +func getSignedAttributes(outputFormat string, envContent *signature.EnvelopeContent) map[string]string { + signedAttributes := map[string]string{ + "signingScheme": string(envContent.SignerInfo.SignedAttributes.SigningScheme), + "signingTime": formatTimestamp(outputFormat, envContent.SignerInfo.SignedAttributes.SigningTime), + "expiry": formatTimestamp(outputFormat, envContent.SignerInfo.SignedAttributes.Expiry), + } + + for _, attribute := range envContent.SignerInfo.SignedAttributes.ExtendedAttributes { + signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value) + } + + return signedAttributes +} + +func getUnsignedAttributes(envContent *signature.EnvelopeContent) map[string]string { + unsignedAttributes := map[string]string{} + + if envContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil { + unsignedAttributes["timestampSignature"] = b64.StdEncoding.EncodeToString(envContent.SignerInfo.UnsignedAttributes.TimestampSignature) + } + + if envContent.SignerInfo.UnsignedAttributes.SigningAgent != "" { + unsignedAttributes["signingAgent"] = envContent.SignerInfo.UnsignedAttributes.SigningAgent + } + + return unsignedAttributes +} + +func formatTimestamp(outputFormat string, t time.Time) string { + switch outputFormat { + case cmd.OutputJSON: + return t.Format(time.RFC3339) + default: + return t.Format(time.ANSIC) + } +} + +func getCertificates(outputFormat string, envContent *signature.EnvelopeContent) []certificateOutput { + certificates := []certificateOutput{} + + for _, cert := range envContent.SignerInfo.CertificateChain { + h := sha1.Sum(cert.Raw) + fingerprint := strings.ToLower(hex.EncodeToString(h[:])) + + certificate := certificateOutput{ + SHA1Fingerprint: fingerprint, + IssuedTo: cert.Subject.String(), + IssuedBy: cert.Issuer.String(), + Expiry: formatTimestamp(outputFormat, cert.NotAfter), + } + + certificates = append(certificates, certificate) + } + + return certificates +} + +func printOutput(outputFormat string, ref string, output inspectOutput) error { + if outputFormat == cmd.OutputJSON { + return ioutil.PrintObjectAsJSON(output) + } + + fmt.Println("Inspecting all signatures for signed artifact") + root := tree.New(ref) + cncfSigNode := root.Add(registry.ArtifactTypeNotation) + + for _, signature := range output.Signatures { + sigNode := cncfSigNode.Add(signature.Digest) + sigNode.AddPair("media type", signature.MediaType) + sigNode.AddPair("signature algorithm", signature.SignatureAlgorithm) + + signedAttributesNode := sigNode.Add("signed attributes") + addMapToTree(signedAttributesNode, signature.SignedAttributes) + + userDefinedAttributesNode := sigNode.Add("user defined attributes") + addMapToTree(userDefinedAttributesNode, signature.UserDefinedAttributes) + + unsignedAttributesNode := sigNode.Add("unsigned attributes") + addMapToTree(unsignedAttributesNode, signature.UnsignedAttributes) + + certListNode := sigNode.Add("certificates") + for _, cert := range signature.Certificates { + certNode := certListNode.AddPair("SHA1 fingerprint", cert.SHA1Fingerprint) + certNode.AddPair("issued to", cert.IssuedTo) + certNode.AddPair("issued by", cert.IssuedBy) + certNode.AddPair("expiry", cert.Expiry) + } + + artifactNode := sigNode.Add("signed artifact") + artifactNode.AddPair("media type", signature.SignedArtifact.MediaType) + artifactNode.AddPair("digest", signature.SignedArtifact.Digest.String()) + artifactNode.AddPair("size", strconv.FormatInt(signature.SignedArtifact.Size, 10)) + } + + root.Print() + return nil +} + +func addMapToTree(node *tree.Node, m map[string]string) { + if len(m) > 0 { + for k, v := range m { + node.AddPair(k, v) + } + } else { + node.Add("(empty)") + } +} diff --git a/cmd/notation/inspect_test.go b/cmd/notation/inspect_test.go new file mode 100644 index 000000000..e143c3003 --- /dev/null +++ b/cmd/notation/inspect_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "testing" + + "github.com/notaryproject/notation/internal/cmd" +) + +func TestInspectCommand_SecretsFromArgs(t *testing.T) { + opts := &inspectOpts{} + command := inspectCommand(opts) + expected := &inspectOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Password: "password", + PlainHTTP: true, + Username: "user", + }, + outputFormat: cmd.OutputPlaintext, + } + if err := command.ParseFlags([]string{ + "--password", expected.Password, + expected.reference, + "-u", expected.Username, + "--plain-http", + "--output", "text"}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse Args failed: %v", err) + } + if *opts != *expected { + t.Fatalf("Expect inspect opts: %v, got: %v", expected, opts) + } +} + +func TestInspectCommand_SecretsFromEnv(t *testing.T) { + t.Setenv(defaultUsernameEnv, "user") + t.Setenv(defaultPasswordEnv, "password") + opts := &inspectOpts{} + expected := &inspectOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Password: "password", + Username: "user", + }, + outputFormat: cmd.OutputJSON, + } + command := inspectCommand(opts) + if err := command.ParseFlags([]string{ + expected.reference, + "--output", "json"}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse Args failed: %v", err) + } + if *opts != *expected { + t.Fatalf("Expect inspect opts: %v, got: %v", expected, opts) + } +} + +func TestInspectCommand_MissingArgs(t *testing.T) { + command := inspectCommand(nil) + if err := command.ParseFlags(nil); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err == nil { + t.Fatal("Parse Args expected error, but ok") + } +} diff --git a/cmd/notation/main.go b/cmd/notation/main.go index 5f0f3df39..32eb1bc2b 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -23,6 +23,7 @@ func main() { loginCommand(nil), logoutCommand(nil), versionCommand(), + inspectCommand(nil), ) if err := cmd.Execute(); err != nil { os.Exit(1) diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 5d8ec3f35..b8fbb15c6 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -84,7 +84,7 @@ Example - Sign an OCI artifact and use OCI image manifest to store the signature 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") + command.Flags().StringVar(&opts.signatureManifest, "signature-manifest", signatureManifestArtifact, "manifest type for signature. options: \"artifact\", \"image\"") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataSignUsage) return command } diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 45e086230..2bf0c6565 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -11,6 +11,11 @@ import ( "github.com/spf13/pflag" ) +const ( + OutputPlaintext = "text" + OutputJSON = "json" +) + var ( PflagKey = &pflag.Flag{ Name: "key", @@ -23,7 +28,7 @@ var ( PflagSignatureFormat = &pflag.Flag{ Name: "signature-format", - Usage: "signature envelope format, options: 'jws', 'cose'", + Usage: "signature envelope format, options: \"jws\", \"cose\"", } SetPflagSignatureFormat = func(fs *pflag.FlagSet, p *string) { defaultSignatureFormat := envelope.JWS @@ -75,11 +80,20 @@ var ( Name: "user-metadata", Shorthand: "m", } - PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" + PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" PflagUserMetadataVerifyUsage = "user defined {key}={value} pairs that must be present in the signature for successful verification if provided" - SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { + SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { fs.StringArrayVarP(p, PflagUserMetadata.Name, PflagUserMetadata.Shorthand, nil, usage) } + + PflagOutput = &pflag.Flag{ + Name: "output", + Shorthand: "o", + } + PflagOutputUsage = fmt.Sprintf("output format, options: '%s', '%s'", OutputJSON, OutputPlaintext) + SetPflagOutput = func(fs *pflag.FlagSet, p *string, usage string) { + fs.StringVarP(p, PflagOutput.Name, PflagOutput.Shorthand, OutputPlaintext, usage) + } ) // KeyValueSlice is a flag with type int diff --git a/internal/envelope/envelope.go b/internal/envelope/envelope.go index 868e3363a..aeb6f62dc 100644 --- a/internal/envelope/envelope.go +++ b/internal/envelope/envelope.go @@ -1,18 +1,30 @@ package envelope import ( + "encoding/json" + "errors" "fmt" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/cose" "github.com/notaryproject/notation-core-go/signature/jws" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -// Supported envelope format. const ( + // Supported envelope format. COSE = "cose" JWS = "jws" + + // MediaTypePayloadV1 is the supported content type for signature's payload. + MediaTypePayloadV1 = "application/vnd.cncf.notary.payload.v1+json" ) +// Payload describes the content that gets signed. +type Payload struct { + TargetArtifact ocispec.Descriptor `json:"targetArtifact"` +} + // GetEnvelopeMediaType converts the envelope type to mediaType name. func GetEnvelopeMediaType(sigFormat string) (string, error) { switch sigFormat { @@ -23,3 +35,34 @@ func GetEnvelopeMediaType(sigFormat string) (string, error) { } return "", fmt.Errorf("signature format %q not supported", sigFormat) } + +// ValidatePayloadContentType validates signature payload's content type. +func ValidatePayloadContentType(payload *signature.Payload) error { + switch payload.ContentType { + case MediaTypePayloadV1: + return nil + default: + return fmt.Errorf("payload content type %q not supported", payload.ContentType) + } +} + +// DescriptorFromPayload parses a signature payload and returns the descriptor +// that was signed. Note: the descriptor was signed but may not be trusted +func DescriptorFromSignaturePayload(payload *signature.Payload) (*ocispec.Descriptor, error) { + if payload == nil { + return nil, errors.New("empty payload") + } + + err := ValidatePayloadContentType(payload) + if err != nil { + return nil, err + } + + var parsedPayload Payload + err = json.Unmarshal(payload.Content, &parsedPayload) + if err != nil { + return nil, errors.New("failed to unmarshall the payload content to Payload") + } + + return &parsedPayload.TargetArtifact, nil +} diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index f7ae97b4d..25075ca83 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -1,6 +1,7 @@ package ioutil import ( + "encoding/json" "fmt" "io" "text/tabwriter" @@ -42,4 +43,16 @@ func PrintMetadataMap(w io.Writer, metadata map[string]string) error { } return tw.Flush() -} \ No newline at end of file +} + +// PrintObjectAsJSON takes an interface and prints it as an indented JSON string +func PrintObjectAsJSON(i interface{}) error { + jsonBytes, err := json.MarshalIndent(i, "", " ") + if err != nil { + return err + } + + fmt.Println(string(jsonBytes)) + + return nil +} diff --git a/internal/tree/tree.go b/internal/tree/tree.go new file mode 100644 index 000000000..4cf224057 --- /dev/null +++ b/internal/tree/tree.go @@ -0,0 +1,57 @@ +package tree + +import ( + "fmt" +) + +const ( + treeItemPrefix = "├── " + treeItemPrefixLast = "└── " + subTreePrefix = "│ " + subTreePrefixLast = " " +) + +// represents a Node in a tree +type Node struct { + Value string + Children []*Node +} + +// creates a new Node with the given value +func New(value string) *Node { + return &Node{Value: value} +} + +// adds a new child node with the given value +func (parent *Node) Add(value string) *Node { + node := New(value) + parent.Children = append(parent.Children, node) + return node +} + +// adds a new child node with the formatted pair as the value +func (parent *Node) AddPair(key string, value string) *Node { + return parent.Add(key + ": " + value) +} + +// prints the tree represented by the root node +func (root *Node) Print() { + print("", "", "", root) +} + +func print(prefix string, itemMarker string, nextPrefix string, n *Node) { + fmt.Println(prefix + itemMarker + n.Value) + + nextItemPrefix := treeItemPrefix + nextSubTreePrefix := subTreePrefix + + if len(n.Children) > 0 { + for i, child := range n.Children { + if i == len(n.Children)-1 { + nextItemPrefix = treeItemPrefixLast + nextSubTreePrefix = subTreePrefixLast + } + print(nextPrefix, nextItemPrefix, nextPrefix+nextSubTreePrefix, child) + } + } +} diff --git a/internal/tree/tree_test.go b/internal/tree/tree_test.go new file mode 100644 index 000000000..b0284a582 --- /dev/null +++ b/internal/tree/tree_test.go @@ -0,0 +1,83 @@ +package tree + +import ( + "reflect" + "testing" +) + +func TestNodeCreation(t *testing.T) { + node := New("root") + expected := Node{Value: "root"} + + if !reflect.DeepEqual(*node, expected) { + t.Fatalf("expected %+v, got %+v", expected, *node) + } +} + +func TestNodeAdd(t *testing.T) { + root := New("root") + root.Add("child") + + if !root.ContainsChild("child") { + t.Error("expected root to have child node with value 'child'") + t.Fatalf("actual root: %+v", root) + } +} + +func TestNodeAddPair(t *testing.T) { + root := New("root") + root.AddPair("key", "value") + + if !root.ContainsChild("key: value") { + t.Error("expected root to have child node with value 'key: value'") + t.Fatalf("actual root: %+v", root) + } +} + +func ExampleRootPrint() { + root := New("root") + root.Print() + + // Output: + // root +} + +func ExampleSingleLayerPrint() { + root := New("root") + root.Add("child1") + root.Add("child2") + root.Print() + + // Output: + // root + // ├── child1 + // └── child2 +} + +func ExampleMultiLayerPrint() { + root := New("root") + child1 := root.Add("child1") + child1.AddPair("key", "value") + child2 := root.Add("child2") + child2.Add("child2.1") + child2.Add("child2.2") + root.Print() + + // Output: + // root + // ├── child1 + // │ └── key: value + // └── child2 + // ├── child2.1 + // └── child2.2 +} + +func (n *Node) ContainsChild(value string) bool { + for _, child := range n.Children { + if child.Value == value { + return true + } + } + + return false +} diff --git a/specs/commandline/sign.md b/specs/commandline/sign.md index 95be17a21..70aa13e44 100644 --- a/specs/commandline/sign.md +++ b/specs/commandline/sign.md @@ -28,25 +28,27 @@ Usage: notation sign [flags] Flags: - -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 + -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 signature, options: "image", "artifact" (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 `--signature-manifest 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 registries that don't support `OCI artifact` or [Referrers API][oci-referers-api] is not enabled, users SHOULD use flag `--signature-manifest image` to force Notation to store the signatures using [OCI image manifest][oci-image-spec]. -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. +Note that there is no deterministic way to determine whether a registry supports `OCI artifact` or not. The following response status contained in error messages MAY indicate that the registry doesn't support `OCI artifact`. + +- Response status `400 BAD Request` with error code `MANIFEST_INVALID` or `UNSUPPORTED` ### Set config property for OCI image manifest @@ -62,14 +64,6 @@ 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 `--signature-manifest image` to sign artifacts stored in those registries. - -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` - ## Usage ### Sign an OCI artifact @@ -168,6 +162,4 @@ notation sign --signature-manifest image /@ [oci-artifact-manifest]: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md [oci-image-spec]: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/spec.md -[oci-backward-compatibility]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#backwards-compatibility -[registry-support]: https://notaryproject.dev/docs/registrysupport/ -[oras-land]: https://oras.land/ +[oci-referers-api]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers diff --git a/test/e2e/suite/scenario/quickstart.go b/test/e2e/suite/scenario/quickstart.go index d5c298f69..895d2696a 100644 --- a/test/e2e/suite/scenario/quickstart.go +++ b/test/e2e/suite/scenario/quickstart.go @@ -74,7 +74,7 @@ var _ = Describe("notation quickstart E2E test", Ordered, func() { It("Verify the container image with jws format", func() { notation.Exec("verify", artifact.ReferenceWithDigest()). - MatchContent(fmt.Sprintf("Successfully verified signature for %s\n", artifact.ReferenceWithDigest())) + MatchKeyWords(fmt.Sprintf("Successfully verified signature for %s\n", artifact.ReferenceWithDigest())) }) It("Verify the container image with cose format", func() {