diff --git a/cmd/notation/inspect.go b/cmd/notation/inspect.go index a55595c51..3ad485684 100644 --- a/cmd/notation/inspect.go +++ b/cmd/notation/inspect.go @@ -14,26 +14,16 @@ package main import ( - "crypto/sha256" - "crypto/x509" - "encoding/hex" "errors" "fmt" "os" - "strconv" - "strings" - "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/cmd/notation/internal/display" cmderr "github.com/notaryproject/notation/cmd/notation/internal/errors" "github.com/notaryproject/notation/cmd/notation/internal/experimental" + "github.com/notaryproject/notation/cmd/notation/internal/option" "github.com/notaryproject/notation/internal/cmd" - "github.com/notaryproject/notation/internal/envelope" - "github.com/notaryproject/notation/internal/ioutil" - "github.com/notaryproject/notation/internal/tree" - "github.com/notaryproject/tspclient-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) @@ -41,41 +31,13 @@ import ( type inspectOpts struct { cmd.LoggingFlagOpts SecureFlagOpts + option.Common + option.Format reference string - outputFormat string allowReferrersAPI bool maxSignatures int } -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]any `json:"unsignedAttributes"` - Certificates []certificateOutput `json:"certificates"` - SignedArtifact ocispec.Descriptor `json:"signedArtifact"` -} - -type certificateOutput struct { - SHA256Fingerprint string `json:"SHA256Fingerprint"` - IssuedTo string `json:"issuedTo"` - IssuedBy string `json:"issuedBy"` - Expiry string `json:"expiry"` -} - -type timestampOutput struct { - Timestamp string `json:"timestamp,omitempty"` - Certificates []certificateOutput `json:"certificates,omitempty"` - Error string `json:"error,omitempty"` -} - func inspectCommand(opts *inspectOpts) *cobra.Command { if opts == nil { opts = &inspectOpts{} @@ -103,6 +65,10 @@ Example - Inspect signatures on an OCI artifact identified by a digest and outpu return nil }, PreRunE: func(cmd *cobra.Command, args []string) error { + if err := opts.Format.Parse(cmd); err != nil { + return err + } + opts.Common.Parse(cmd) return experimental.CheckFlagsAndWarn(cmd, "allow-referrers-api") }, RunE: func(cmd *cobra.Command, args []string) error { @@ -118,9 +84,11 @@ Example - Inspect signatures on an OCI artifact identified by a digest and outpu opts.LoggingFlagOpts.ApplyFlags(command.Flags()) opts.SecureFlagOpts.ApplyFlags(command.Flags()) - cmd.SetPflagOutput(command.Flags(), &opts.outputFormat, cmd.PflagOutputUsage) command.Flags().IntVar(&opts.maxSignatures, "max-signatures", 100, "maximum number of signatures to evaluate or examine") cmd.SetPflagReferrersAPI(command.Flags(), &opts.allowReferrersAPI, fmt.Sprintf(cmd.PflagReferrersUsageFormat, "inspect")) + + // set output format + opts.Format.ApplyFlags(command.Flags(), option.FormatTypeText, option.FormatTypeJSON) return command } @@ -128,8 +96,9 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { // set log level ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) - if opts.outputFormat != cmd.OutputJSON && opts.outputFormat != cmd.OutputPlaintext { - return fmt.Errorf("unrecognized output format %s", opts.outputFormat) + displayHandler, err := display.NewInpsectHandler(opts.Printer, opts.Format) + if err != nil { + return err } // initialize @@ -144,7 +113,8 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { if err != nil { return err } - output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}} + displayHandler.OnReferenceResolved(resolvedRef, manifestDesc.MediaType) + skippedSignatures := false err = listSignatures(ctx, sigRepo, manifestDesc, opts.maxSignatures, func(sigManifestDesc ocispec.Descriptor) error { sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc) @@ -161,44 +131,11 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { return nil } - envelopeContent, err := sigEnvelope.Content() - if err != nil { - logSkippedSignature(sigManifestDesc, err) - skippedSignatures = true - return nil - } - - signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload) - if err != nil { + if err := displayHandler.InspectSignature(sigManifestDesc, sigEnvelope); err != nil { logSkippedSignature(sigManifestDesc, err) skippedSignatures = true return nil } - - signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm) - if err != nil { - logSkippedSignature(sigManifestDesc, err) - skippedSignatures = true - return nil - } - - sig := signatureOutput{ - MediaType: sigDesc.MediaType, - Digest: sigManifestDesc.Digest.String(), - SignatureAlgorithm: string(signatureAlgorithm), - SignedAttributes: getSignedAttributes(opts.outputFormat, envelopeContent), - UserDefinedAttributes: signedArtifactDesc.Annotations, - UnsignedAttributes: getUnsignedAttributes(opts.outputFormat, envelopeContent), - Certificates: getCertificates(opts.outputFormat, envelopeContent.SignerInfo.CertificateChain), - 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 }) var errorExceedMaxSignatures cmderr.ErrorExceedMaxSignatures @@ -206,7 +143,7 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { return err } - if err := printOutput(opts.outputFormat, resolvedRef, output); err != nil { + if err := displayHandler.Render(); err != nil { return err } @@ -224,169 +161,3 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { 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 := envContent.SignerInfo.SignedAttributes.Expiry - if !expiry.IsZero() { - signedAttributes["expiry"] = formatTimestamp(outputFormat, expiry) - } - - for _, attribute := range envContent.SignerInfo.SignedAttributes.ExtendedAttributes { - signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value) - } - - return signedAttributes -} - -func getUnsignedAttributes(outputFormat string, envContent *signature.EnvelopeContent) map[string]any { - unsignedAttributes := make(map[string]any) - - if envContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil { - unsignedAttributes["timestampSignature"] = parseTimestamp(outputFormat, envContent.SignerInfo) - } - - 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, certChain []*x509.Certificate) []certificateOutput { - certificates := []certificateOutput{} - - for _, cert := range certChain { - h := sha256.Sum256(cert.Raw) - fingerprint := strings.ToLower(hex.EncodeToString(h[:])) - - certificate := certificateOutput{ - SHA256Fingerprint: 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) - } - - if len(output.Signatures) == 0 { - fmt.Printf("%s has no associated signature\n", ref) - return nil - } - - 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") - for k, v := range signature.UnsignedAttributes { - switch value := v.(type) { - case string: - unsignedAttributesNode.AddPair(k, value) - case timestampOutput: - timestampNode := unsignedAttributesNode.Add("timestamp signature") - if value.Error != "" { - timestampNode.AddPair("error", value.Error) - break - } - timestampNode.AddPair("timestamp", value.Timestamp) - addCertificatesToTree(timestampNode, "certificates", value.Certificates) - } - } - - addCertificatesToTree(sigNode, "certificates", signature.Certificates) - - 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)") - } -} - -func addCertificatesToTree(node *tree.Node, name string, certs []certificateOutput) { - certListNode := node.Add(name) - for _, cert := range certs { - certNode := certListNode.AddPair("SHA256 fingerprint", cert.SHA256Fingerprint) - certNode.AddPair("issued to", cert.IssuedTo) - certNode.AddPair("issued by", cert.IssuedBy) - certNode.AddPair("expiry", cert.Expiry) - } -} - -func parseTimestamp(outputFormat string, signerInfo signature.SignerInfo) timestampOutput { - signedToken, err := tspclient.ParseSignedToken(signerInfo.UnsignedAttributes.TimestampSignature) - if err != nil { - return timestampOutput{ - Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()), - } - } - info, err := signedToken.Info() - if err != nil { - return timestampOutput{ - Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()), - } - } - timestamp, err := info.Validate(signerInfo.Signature) - if err != nil { - return timestampOutput{ - Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()), - } - } - certificates := getCertificates(outputFormat, signedToken.Certificates) - var formatTimestamp string - switch outputFormat { - case cmd.OutputJSON: - formatTimestamp = timestamp.Format(time.RFC3339) - default: - formatTimestamp = timestamp.Format(time.ANSIC) - } - return timestampOutput{ - Timestamp: formatTimestamp, - Certificates: certificates, - } -} diff --git a/cmd/notation/inspect_test.go b/cmd/notation/inspect_test.go index a6c20f596..f5401d17f 100644 --- a/cmd/notation/inspect_test.go +++ b/cmd/notation/inspect_test.go @@ -14,15 +14,19 @@ package main import ( + "reflect" "testing" - "github.com/notaryproject/notation-core-go/signature" - "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/cmd/notation/internal/option" + "github.com/spf13/pflag" ) func TestInspectCommand_SecretsFromArgs(t *testing.T) { opts := &inspectOpts{} command := inspectCommand(opts) + format := option.Format{} + format.ApplyFlags(&pflag.FlagSet{}, option.FormatTypeText, option.FormatTypeJSON) + format.CurrentType = string(option.FormatTypeText) expected := &inspectOpts{ reference: "ref", SecureFlagOpts: SecureFlagOpts{ @@ -30,7 +34,7 @@ func TestInspectCommand_SecretsFromArgs(t *testing.T) { InsecureRegistry: true, Username: "user", }, - outputFormat: cmd.OutputPlaintext, + Format: format, maxSignatures: 100, } if err := command.ParseFlags([]string{ @@ -44,24 +48,29 @@ func TestInspectCommand_SecretsFromArgs(t *testing.T) { 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) + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %v, got: %v", expected, opts) } } func TestInspectCommand_SecretsFromEnv(t *testing.T) { t.Setenv(defaultUsernameEnv, "user") t.Setenv(defaultPasswordEnv, "password") - opts := &inspectOpts{} + + format := option.Format{} + format.ApplyFlags(&pflag.FlagSet{}, option.FormatTypeText, option.FormatTypeJSON) + format.CurrentType = string(option.FormatTypeJSON) expected := &inspectOpts{ reference: "ref", SecureFlagOpts: SecureFlagOpts{ Password: "password", Username: "user", }, - outputFormat: cmd.OutputJSON, + Format: format, maxSignatures: 100, } + + opts := &inspectOpts{} command := inspectCommand(opts) if err := command.ParseFlags([]string{ expected.reference, @@ -71,8 +80,8 @@ func TestInspectCommand_SecretsFromEnv(t *testing.T) { 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) + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %v, got: %v", expected, opts) } } @@ -86,21 +95,21 @@ func TestInspectCommand_MissingArgs(t *testing.T) { } } -func TestGetUnsignedAttributes(t *testing.T) { - envContent := &signature.EnvelopeContent{ - SignerInfo: signature.SignerInfo{ - UnsignedAttributes: signature.UnsignedAttributes{ - TimestampSignature: []byte("invalid"), - }, - }, +func TestInspectCommand_Invalid_Output(t *testing.T) { + opts := &inspectOpts{} + command := inspectCommand(opts) + if err := command.ParseFlags([]string{ + "ref", + "--output", "invalidFormat"}); 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) } - expectedErrMsg := "failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length" - unsignedAttr := getUnsignedAttributes(cmd.OutputPlaintext, envContent) - val, ok := unsignedAttr["timestampSignature"].(timestampOutput) - if !ok { - t.Fatal("expected to have timestampSignature") + if err := command.PreRunE(command, command.Flags().Args()); err == nil || err.Error() != "invalid format type: \"invalidFormat\"" { + t.Fatalf("PreRunE expected error 'invalid format type: \"invalidFormat\"', got: %v", err) } - if val.Error != expectedErrMsg { - t.Fatalf("expected %s, but got %s", expectedErrMsg, val.Error) + if err := command.RunE(command, command.Flags().Args()); err == nil || err.Error() != "unrecognized output format invalidFormat" { + t.Fatalf("RunE expected error 'unrecognized output format invalidFormat', got: %v", err) } } diff --git a/cmd/notation/internal/display/handler.go b/cmd/notation/internal/display/handler.go new file mode 100644 index 000000000..1f127d896 --- /dev/null +++ b/cmd/notation/internal/display/handler.go @@ -0,0 +1,42 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package display provides the display handlers to render information for +// commands. +// +// - It includes the metadata, content and status packages for handling +// different types of information. +// - It includes the output package for writing information to the output. +package display + +import ( + "fmt" + + "github.com/notaryproject/notation/cmd/notation/internal/display/metadata" + "github.com/notaryproject/notation/cmd/notation/internal/display/metadata/json" + "github.com/notaryproject/notation/cmd/notation/internal/display/metadata/tree" + "github.com/notaryproject/notation/cmd/notation/internal/display/output" + "github.com/notaryproject/notation/cmd/notation/internal/option" +) + +// NewInpsectHandler creates a new metadata InspectHandler based on the output +// format. +func NewInpsectHandler(printer *output.Printer, format option.Format) (metadata.InspectHandler, error) { + switch option.FormatType(format.CurrentType) { + case option.FormatTypeJSON: + return json.NewInspectHandler(printer), nil + case option.FormatTypeText: + return tree.NewInspectHandler(printer), nil + } + return nil, fmt.Errorf("unrecognized output format %s", format.CurrentType) +} diff --git a/cmd/notation/internal/display/metadata/interface.go b/cmd/notation/internal/display/metadata/interface.go new file mode 100644 index 000000000..01c717703 --- /dev/null +++ b/cmd/notation/internal/display/metadata/interface.go @@ -0,0 +1,39 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package metadata defines interfaces for handlers that render metadata +// information for each command. The metadata provides information about the +// original data with formatted output in JSON, tree, or text. +package metadata + +import ( + "github.com/notaryproject/notation-core-go/signature" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Renderer renders metadata information when an operation is complete. +type Renderer interface { + Render() error +} + +// InspectHandler is a handler for inspecting metadata information and rendering +// it in a specific format. +type InspectHandler interface { + Renderer + + // OnReferenceResolved sets the artifact reference and media type for the handler. + OnReferenceResolved(reference, mediaType string) + + // InspectSignature inspects a signature to get it ready to be rendered. + InspectSignature(manifestDesc ocispec.Descriptor, envelope signature.Envelope) error +} diff --git a/cmd/notation/internal/display/metadata/json/inspect.go b/cmd/notation/internal/display/metadata/json/inspect.go new file mode 100644 index 000000000..67c8c4957 --- /dev/null +++ b/cmd/notation/internal/display/metadata/json/inspect.go @@ -0,0 +1,202 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package json + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "fmt" + "strings" + "time" + + coresignature "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/plugin/proto" + "github.com/notaryproject/notation/cmd/notation/internal/display/output" + "github.com/notaryproject/notation/internal/envelope" + "github.com/notaryproject/tspclient-go" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type inspectOutput struct { + MediaType string `json:"mediaType"` + Signatures []*signature `json:"signatures"` +} + +// signature is the signature envelope for printing in JSON format. +type signature struct { + Digest string `json:"digest,omitempty"` + SignatureAlgorithm string `json:"signatureAlgorithm"` + SignedAttributes map[string]any `json:"signedAttributes"` + UserDefinedAttributes map[string]string `json:"userDefinedAttributes"` + UnsignedAttributes map[string]any `json:"unsignedAttributes"` + Certificates []*certificate `json:"certificates"` + SignedArtifact ocispec.Descriptor `json:"signedArtifact"` +} + +// certificate is the certificate information for printing in JSON format. +type certificate struct { + SHA256Fingerprint string `json:"SHA256Fingerprint"` + IssuedTo string `json:"issuedTo"` + IssuedBy string `json:"issuedBy"` + Expiry time.Time `json:"expiry"` +} + +// timestamp is the timestamp information for printing in JSON format. +type timestamp struct { + Timestamp string `json:"timestamp,omitempty"` + Certificates []*certificate `json:"certificates,omitempty"` + Error string `json:"error,omitempty"` +} + +// InspectHandler is the handler for inspecting metadata information and +// rendering it in JSON format. It implements the metadata.InspectHandler +// interface. +type InspectHandler struct { + printer *output.Printer + + output inspectOutput +} + +// NewInspectHandler creates a new JsonHandler to inspect signatures and print in +// JSON format. +func NewInspectHandler(printer *output.Printer) *InspectHandler { + return &InspectHandler{ + printer: printer, + output: inspectOutput{ + Signatures: []*signature{}, + }, + } +} + +// OnReferenceResolved sets the artifact reference and media type for the +// handler. +// +// The reference is no-op for this handler. +func (h *InspectHandler) OnReferenceResolved(_, mediaType string) { + h.output.MediaType = mediaType +} + +// InspectSignature inspects a signature to get it ready to be rendered. +func (h *InspectHandler) InspectSignature(manifestDesc ocispec.Descriptor, envelope coresignature.Envelope) error { + sig, err := newSignature(manifestDesc.Digest.String(), envelope) + if err != nil { + return err + } + h.output.Signatures = append(h.output.Signatures, sig) + return nil +} + +func (h *InspectHandler) Render() error { + return output.PrintPrettyJSON(h.printer, h.output) +} + +// newSignature parses the signature blob and returns a Signature object. +func newSignature(digest string, sigEnvelope coresignature.Envelope) (*signature, error) { + envelopeContent, err := sigEnvelope.Content() + if err != nil { + return nil, err + } + + signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload) + if err != nil { + return nil, err + } + + signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm) + if err != nil { + return nil, err + } + sig := &signature{ + Digest: digest, + SignatureAlgorithm: string(signatureAlgorithm), + SignedAttributes: getSignedAttributes(envelopeContent), + UserDefinedAttributes: signedArtifactDesc.Annotations, + UnsignedAttributes: getUnsignedAttributes(envelopeContent), + Certificates: getCertificates(envelopeContent.SignerInfo.CertificateChain), + SignedArtifact: signedArtifactDesc, + } + + // clearing annotations from the SignedArtifact field since they're already + // displayed as UserDefinedAttributes + sig.SignedArtifact.Annotations = nil + + return sig, nil +} + +func getSignedAttributes(envelopeContent *coresignature.EnvelopeContent) map[string]any { + signedAttributes := map[string]any{ + "contentType": envelopeContent.Payload.ContentType, + "signingScheme": string(envelopeContent.SignerInfo.SignedAttributes.SigningScheme), + "signingTime": envelopeContent.SignerInfo.SignedAttributes.SigningTime, + } + if expiry := envelopeContent.SignerInfo.SignedAttributes.Expiry; !expiry.IsZero() { + signedAttributes["expiry"] = expiry + } + for _, attribute := range envelopeContent.SignerInfo.SignedAttributes.ExtendedAttributes { + signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value) + } + return signedAttributes +} + +func getUnsignedAttributes(envelopeContent *coresignature.EnvelopeContent) map[string]any { + unsignedAttributes := make(map[string]any) + if envelopeContent.SignerInfo.UnsignedAttributes.SigningAgent != "" { + unsignedAttributes["signingAgent"] = envelopeContent.SignerInfo.UnsignedAttributes.SigningAgent + } + if envelopeContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil { + unsignedAttributes["timestampSignature"] = parseTimestamp(envelopeContent.SignerInfo) + } + return unsignedAttributes +} + +func getCertificates(certChain []*x509.Certificate) []*certificate { + var certificates []*certificate + for _, cert := range certChain { + hash := sha256.Sum256(cert.Raw) + certificates = append(certificates, &certificate{ + SHA256Fingerprint: strings.ToLower(hex.EncodeToString(hash[:])), + IssuedTo: cert.Subject.String(), + IssuedBy: cert.Issuer.String(), + Expiry: cert.NotAfter, + }) + } + return certificates +} + +func parseTimestamp(signerInfo coresignature.SignerInfo) *timestamp { + signedToken, err := tspclient.ParseSignedToken(signerInfo.UnsignedAttributes.TimestampSignature) + if err != nil { + return ×tamp{ + Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err), + } + } + info, err := signedToken.Info() + if err != nil { + return ×tamp{ + Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err), + } + } + ts, err := info.Validate(signerInfo.Signature) + if err != nil { + return ×tamp{ + Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err), + } + } + return ×tamp{ + Timestamp: ts.Format(time.RFC3339Nano), + Certificates: getCertificates(signedToken.Certificates), + } +} diff --git a/cmd/notation/internal/display/metadata/json/inspect_test.go b/cmd/notation/internal/display/metadata/json/inspect_test.go new file mode 100644 index 000000000..783c65fac --- /dev/null +++ b/cmd/notation/internal/display/metadata/json/inspect_test.go @@ -0,0 +1,129 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package json + +import ( + "errors" + "os" + "strings" + "testing" + "time" + + coresignature "github.com/notaryproject/notation-core-go/signature" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type errorEnvelope struct{} + +func (e errorEnvelope) Sign(req *coresignature.SignRequest) ([]byte, error) { + return nil, errors.New("mock sign error") +} + +func (e errorEnvelope) Verify() (*coresignature.EnvelopeContent, error) { + return nil, errors.New("mock verify error") +} + +func (e errorEnvelope) Content() (*coresignature.EnvelopeContent, error) { + return nil, errors.New("mock content error") +} + +func TestGetUnsignedAttributes(t *testing.T) { + envContent := &coresignature.EnvelopeContent{ + SignerInfo: coresignature.SignerInfo{ + UnsignedAttributes: coresignature.UnsignedAttributes{ + TimestampSignature: []byte("invalid"), + }, + }, + } + expectedErrMsg := "failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length" + unsignedAttr := getUnsignedAttributes(envContent) + val, ok := unsignedAttr["timestampSignature"].(*timestamp) + if !ok { + t.Fatal("expected to have timestampSignature") + } + if val.Error != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, val.Error) + } +} + +func TestGetSignedAttributes(t *testing.T) { + expiry := time.Now() + envContent := &coresignature.EnvelopeContent{ + SignerInfo: coresignature.SignerInfo{ + SignedAttributes: coresignature.SignedAttributes{ + Expiry: expiry, + ExtendedAttributes: []coresignature.Attribute{ + { + Key: "keyName", + Value: "value", + }, + }, + }, + }, + } + signedAttr := getSignedAttributes(envContent) + if signedAttr["expiry"] != expiry { + t.Fatalf("expected %s, but got %s", expiry, signedAttr["expiry"]) + } + + if signedAttr["keyName"] != "value" { + t.Fatalf("expected value, but got %s", signedAttr["keyName"]) + } +} + +func TestParseTimestamp(t *testing.T) { + t.Run("invalid timestamp signature", func(t *testing.T) { + signerInfo := coresignature.SignerInfo{ + UnsignedAttributes: coresignature.UnsignedAttributes{ + TimestampSignature: []byte("invalid"), + }, + } + val := parseTimestamp(signerInfo) + expectedErrMsg := "failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length" + if val.Error != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, val.Error) + } + }) + + t.Run("timestamp validation error", func(t *testing.T) { + tsaToken, err := os.ReadFile("../testdata/TimeStampTokenWithInvalidSignature.p7s") + if err != nil { + t.Fatal(err) + } + + signerInfo := coresignature.SignerInfo{ + UnsignedAttributes: coresignature.UnsignedAttributes{ + TimestampSignature: tsaToken, + }, + } + val := parseTimestamp(signerInfo) + expectedErrMsg := "failed to parse timestamp countersignature: invalid TSTInfo: mismatched message" + + if val.Error != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, val.Error) + } + }) +} + +func TestInspectSignature_NewSignatureError(t *testing.T) { + h := NewInspectHandler(nil) + // ...existing code to ensure h.output.MediaType is set... + h.OnReferenceResolved("test-ref", "test-media-type") + manifestDesc := ocispec.Descriptor{Digest: "fake-digest"} + + err := h.InspectSignature(manifestDesc, errorEnvelope{}) + if err == nil || !strings.Contains(err.Error(), "mock content error") { + t.Fatalf("expected error 'mock content error', got %v", err) + } +} diff --git a/cmd/notation/internal/display/metadata/testdata/TimeStampTokenWithInvalidSignature.p7s b/cmd/notation/internal/display/metadata/testdata/TimeStampTokenWithInvalidSignature.p7s new file mode 100644 index 000000000..5da09e0b8 Binary files /dev/null and b/cmd/notation/internal/display/metadata/testdata/TimeStampTokenWithInvalidSignature.p7s differ diff --git a/cmd/notation/internal/display/metadata/tree/inspect.go b/cmd/notation/internal/display/metadata/tree/inspect.go new file mode 100644 index 000000000..6804e0a5d --- /dev/null +++ b/cmd/notation/internal/display/metadata/tree/inspect.go @@ -0,0 +1,182 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tree + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "fmt" + "maps" + "slices" + "strconv" + "strings" + "time" + + coresignature "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/plugin/proto" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation/cmd/notation/internal/display/output" + "github.com/notaryproject/notation/internal/envelope" + "github.com/notaryproject/tspclient-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// InspectHandler is a handler for inspecting metadata information and rendering +// it in a tree format. It implements the metadata.InspectHandler interface. +type InspectHandler struct { + printer *output.Printer + + // rootReferenceNode is the root node with the artifact reference as the + // value. + rootReferenceNode *node + // notationSignaturesNode is the node for all signatures associated with the + // artifact. + notationSignaturesNode *node +} + +// NewInspectHandler creates a TreeHandler to inspect signatures and print in tree +// format. +func NewInspectHandler(printer *output.Printer) *InspectHandler { + return &InspectHandler{ + printer: printer, + } +} + +// OnReferenceResolved sets the artifact reference and media type for the +// handler. +// +// mediaType is a no-op for this handler. +func (h *InspectHandler) OnReferenceResolved(reference, _ string) { + h.rootReferenceNode = newNode(reference) + h.notationSignaturesNode = h.rootReferenceNode.Add(registry.ArtifactTypeNotation) +} + +// InspectSignature inspects a signature to get it ready to be rendered. +func (h *InspectHandler) InspectSignature(manifestDesc ocispec.Descriptor, envelope coresignature.Envelope) error { + return addSignature(h.notationSignaturesNode, manifestDesc.Digest.String(), envelope) +} + +// Render renders the metadata information when an operation is complete. +func (h *InspectHandler) Render() error { + if len(h.notationSignaturesNode.Children) == 0 { + return h.printer.Printf("%s has no associated signature\n", h.rootReferenceNode.Value) + } + h.printer.Println("Inspecting all signatures for signed artifact") + return h.rootReferenceNode.Print(h.printer) +} + +func addSignature(node *node, digest string, sigEnvelope coresignature.Envelope) error { + envelopeContent, err := sigEnvelope.Content() + if err != nil { + return err + } + signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload) + if err != nil { + return err + } + signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm) + if err != nil { + return err + } + + // create signature node + sigNode := node.Add(digest) + sigNode.AddPair("signature algorithm", string(signatureAlgorithm)) + + addSignedAttributes(sigNode, envelopeContent) + addUserDefinedAttributes(sigNode, signedArtifactDesc.Annotations) + addUnsignedAttributes(sigNode, envelopeContent) + addCertificates(sigNode, envelopeContent.SignerInfo.CertificateChain) + addSignedArtifact(sigNode, signedArtifactDesc) + return nil +} + +func addSignedAttributes(node *node, envelopeContent *coresignature.EnvelopeContent) { + signedAttributesNode := node.Add("signed attributes") + signedAttributesNode.AddPair("content type", string(envelopeContent.Payload.ContentType)) + signedAttributesNode.AddPair("signing scheme", string(envelopeContent.SignerInfo.SignedAttributes.SigningScheme)) + signedAttributesNode.AddPair("signing time", formatTime(envelopeContent.SignerInfo.SignedAttributes.SigningTime)) + if expiry := envelopeContent.SignerInfo.SignedAttributes.Expiry; !expiry.IsZero() { + signedAttributesNode.AddPair("expiry", formatTime(expiry)) + } + for _, attribute := range envelopeContent.SignerInfo.SignedAttributes.ExtendedAttributes { + signedAttributesNode.AddPair(fmt.Sprint(attribute.Key), fmt.Sprint(attribute.Value)) + } +} + +func addUserDefinedAttributes(node *node, annotations map[string]string) { + userDefinedAttributesNode := node.Add("user defined attributes") + if len(annotations) == 0 { + userDefinedAttributesNode.Add("(empty)") + return + } + for _, k := range slices.Sorted(maps.Keys(annotations)) { + v := annotations[k] + userDefinedAttributesNode.AddPair(k, v) + } +} + +func addUnsignedAttributes(node *node, envelopeContent *coresignature.EnvelopeContent) { + unsignedAttributesNode := node.Add("unsigned attributes") + if signingAgent := envelopeContent.SignerInfo.UnsignedAttributes.SigningAgent; signingAgent != "" { + unsignedAttributesNode.AddPair("signing agent", signingAgent) + } + if timestamp := envelopeContent.SignerInfo.UnsignedAttributes.TimestampSignature; timestamp != nil { + addTimestamp(unsignedAttributesNode, envelopeContent.SignerInfo) + } +} + +func addSignedArtifact(node *node, signedArtifactDesc ocispec.Descriptor) { + artifactNode := node.Add("signed artifact") + artifactNode.AddPair("media type", signedArtifactDesc.MediaType) + artifactNode.AddPair("digest", signedArtifactDesc.Digest.String()) + artifactNode.AddPair("size", strconv.FormatInt(signedArtifactDesc.Size, 10)) +} + +func addTimestamp(node *node, signerInfo coresignature.SignerInfo) { + timestampNode := node.Add("timestamp signature") + signedToken, err := tspclient.ParseSignedToken(signerInfo.UnsignedAttributes.TimestampSignature) + if err != nil { + timestampNode.AddPair("error", fmt.Sprintf("failed to parse timestamp countersignature: %s", err)) + return + } + info, err := signedToken.Info() + if err != nil { + timestampNode.AddPair("error", fmt.Sprintf("failed to parse timestamp countersignature: %s", err)) + return + } + timestamp, err := info.Validate(signerInfo.Signature) + if err != nil { + timestampNode.AddPair("error", fmt.Sprintf("failed to parse timestamp countersignature: %s", err)) + return + } + timestampNode.AddPair("timestamp", timestamp.Format(time.ANSIC)) + addCertificates(timestampNode, signedToken.Certificates) +} + +func addCertificates(node *node, certChain []*x509.Certificate) { + certListNode := node.Add("certificates") + for _, cert := range certChain { + hash := sha256.Sum256(cert.Raw) + certNode := certListNode.AddPair("SHA256 fingerprint", strings.ToLower(hex.EncodeToString(hash[:]))) + certNode.AddPair("issued to", cert.Subject.String()) + certNode.AddPair("issued by", cert.Issuer.String()) + certNode.AddPair("expiry", formatTime(cert.NotAfter)) + } +} + +func formatTime(t time.Time) string { + return t.Format(time.ANSIC) +} diff --git a/cmd/notation/internal/display/metadata/tree/inspect_test.go b/cmd/notation/internal/display/metadata/tree/inspect_test.go new file mode 100644 index 000000000..9f8b86033 --- /dev/null +++ b/cmd/notation/internal/display/metadata/tree/inspect_test.go @@ -0,0 +1,161 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tree + +import ( + "fmt" + "os" + "testing" + "time" + + coresignature "github.com/notaryproject/notation-core-go/signature" +) + +func TestAddSignedAttributes(t *testing.T) { + t.Run("empty envelopeContent", func(t *testing.T) { + node := newNode("root") + ec := &coresignature.EnvelopeContent{} + addSignedAttributes(node, ec) + // No error or panic expected; minimal check or just ensure it doesn't crash. + }) + + t.Run("with expiry and extented node", func(t *testing.T) { + node := newNode("root") + expiryTime := time.Now().Add(time.Hour) + ec := &coresignature.EnvelopeContent{ + Payload: coresignature.Payload{ + ContentType: "application/vnd.cncf.notary.payload.v1+json", + }, + SignerInfo: coresignature.SignerInfo{ + SignedAttributes: coresignature.SignedAttributes{ + Expiry: expiryTime, + ExtendedAttributes: []coresignature.Attribute{ + { + Key: "key", + Value: "value", + }, + }, + }, + }, + } + addSignedAttributes(node, ec) + // Verify node was added; for brevity, just check no panic + if len(node.Children) == 0 { + t.Fatal("expected children to be added") + } + signedAttrNode := node.Children[0] + if signedAttrNode.Value != "signed attributes" { + t.Fatalf("expected name 'signed attributes', got: %v", signedAttrNode.Value) + } + if len(signedAttrNode.Children) != 5 { + t.Fatalf("expected 5 children, got: %v", len(signedAttrNode.Children)) + } + // verify expiry node + expiryNode := signedAttrNode.Children[3] + if expiryNode.Value != fmt.Sprintf("expiry: %s", expiryTime.Format(time.ANSIC)) { + t.Fatalf("expected expiry node, got: %v", expiryNode.Value) + } + // verify extended attribute node + extendedAttrNode := signedAttrNode.Children[4] + if extendedAttrNode.Value != "key: value" { + t.Fatalf("expected extended attribute node, got: %v", extendedAttrNode.Value) + } + }) +} + +func TestAddUserDefinedAttributes(t *testing.T) { + t.Run("empty map", func(t *testing.T) { + node := newNode("root") + addUserDefinedAttributes(node, nil) + if len(node.Children) == 0 { + t.Fatal("expected node to have children") + } + udaNode := node.Children[0] + if udaNode.Value != "user defined attributes" { + t.Fatalf("expected 'user defined attributes' node, got %s", udaNode.Value) + } + if len(udaNode.Children) == 0 || udaNode.Children[0].Value != "(empty)" { + t.Fatalf("expected '(empty)' node, got %v", udaNode.Children) + } + }) + + t.Run("non-empty map", func(t *testing.T) { + node := newNode("root") + annotations := map[string]string{"key1": "val1", "key2": "val2"} + addUserDefinedAttributes(node, annotations) + udaNode := node.Children[0] + if udaNode.Value != "user defined attributes" { + t.Fatalf("expected 'user defined attributes' node, got %s", udaNode.Value) + } + if len(udaNode.Children) != len(annotations) { + t.Fatalf("expected %d children, got %d", len(annotations), len(udaNode.Children)) + } + }) +} + +func TestAddTimestamp(t *testing.T) { + t.Run("invalid timestamp signature", func(t *testing.T) { + node := newNode("root") + signerInfo := coresignature.SignerInfo{ + UnsignedAttributes: coresignature.UnsignedAttributes{ + TimestampSignature: []byte("invalid"), + }, + } + addTimestamp(node, signerInfo) + if len(node.Children) == 0 { + t.Fatal("expected node to have children") + } + timestampNode := node.Children[0] + if timestampNode.Value != "timestamp signature" { + t.Fatalf("expected 'timestamp signature' node, got %s", timestampNode.Value) + } + if len(timestampNode.Children) == 0 { + t.Fatal("expected node to have children") + } + errNode := timestampNode.Children[0] + expectedErrMsg := "error: failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length" + if errNode.Value != expectedErrMsg { + t.Fatalf("expected error node, got %s", errNode.Value) + } + }) + + t.Run("timestamp validation error", func(t *testing.T) { + tsaToken, err := os.ReadFile("../testdata/TimeStampTokenWithInvalidSignature.p7s") + if err != nil { + t.Fatal(err) + } + signerInfo := coresignature.SignerInfo{ + UnsignedAttributes: coresignature.UnsignedAttributes{ + TimestampSignature: tsaToken, + }, + } + node := newNode("root") + addTimestamp(node, signerInfo) + if len(node.Children) == 0 { + t.Fatal("expected node to have children") + } + timestampNode := node.Children[0] + if timestampNode.Value != "timestamp signature" { + t.Fatalf("expected 'timestamp signature' node, got %s", timestampNode.Value) + } + if len(timestampNode.Children) == 0 { + t.Fatal("expected node to have children") + } + errNode := timestampNode.Children[0] + expectedErrMsg := "error: failed to parse timestamp countersignature: invalid TSTInfo: mismatched message" + if errNode.Value != expectedErrMsg { + t.Fatalf("expected error node, got %s", errNode.Value) + } + }) +} diff --git a/internal/tree/tree.go b/cmd/notation/internal/display/metadata/tree/tree.go similarity index 64% rename from internal/tree/tree.go rename to cmd/notation/internal/display/metadata/tree/tree.go index 616cfee1d..b12766140 100644 --- a/internal/tree/tree.go +++ b/cmd/notation/internal/display/metadata/tree/tree.go @@ -15,6 +15,7 @@ package tree import ( "fmt" + "io" ) const ( @@ -24,36 +25,38 @@ const ( subTreePrefixLast = " " ) -// represents a Node in a tree -type Node struct { +// represents a node in a tree +type node struct { Value string - Children []*Node + Children []*node } -// creates a new Node with the given value -func New(value string) *Node { - return &Node{Value: value} +// creates a newNode node with the given value +func newNode(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) +func (parent *node) Add(value string) *node { + node := newNode(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 { +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 (root *node) Print(w io.Writer) error { + return print(w, "", "", "", root) } -func print(prefix string, itemMarker string, nextPrefix string, n *Node) { - fmt.Println(prefix + itemMarker + n.Value) +func print(w io.Writer, prefix string, itemMarker string, nextPrefix string, n *node) error { + if _, err := fmt.Fprintln(w, prefix+itemMarker+n.Value); err != nil { + return err + } nextItemPrefix := treeItemPrefix nextSubTreePrefix := subTreePrefix @@ -64,7 +67,11 @@ func print(prefix string, itemMarker string, nextPrefix string, n *Node) { nextItemPrefix = treeItemPrefixLast nextSubTreePrefix = subTreePrefixLast } - print(nextPrefix, nextItemPrefix, nextPrefix+nextSubTreePrefix, child) + if err := print(w, nextPrefix, nextItemPrefix, nextPrefix+nextSubTreePrefix, child); err != nil { + return err + } } } + + return nil } diff --git a/internal/tree/tree_test.go b/cmd/notation/internal/display/metadata/tree/tree_test.go similarity index 80% rename from internal/tree/tree_test.go rename to cmd/notation/internal/display/metadata/tree/tree_test.go index 1408e19fc..aaa156535 100644 --- a/internal/tree/tree_test.go +++ b/cmd/notation/internal/display/metadata/tree/tree_test.go @@ -14,21 +14,22 @@ package tree import ( + "os" "reflect" "testing" ) func TestNodeCreation(t *testing.T) { - node := New("root") - expected := Node{Value: "root"} + treeNode := newNode("root") + expected := node{Value: "root"} - if !reflect.DeepEqual(*node, expected) { - t.Fatalf("expected %+v, got %+v", expected, *node) + if !reflect.DeepEqual(*treeNode, expected) { + t.Fatalf("expected %+v, got %+v", expected, *treeNode) } } func TestNodeAdd(t *testing.T) { - root := New("root") + root := newNode("root") root.Add("child") if !root.ContainsChild("child") { @@ -38,7 +39,7 @@ func TestNodeAdd(t *testing.T) { } func TestNodeAddPair(t *testing.T) { - root := New("root") + root := newNode("root") root.AddPair("key", "value") if !root.ContainsChild("key: value") { @@ -48,18 +49,18 @@ func TestNodeAddPair(t *testing.T) { } func ExampleRootPrint() { - root := New("root") - root.Print() + root := newNode("root") + root.Print(os.Stdout) // Output: // root } func ExampleSingleLayerPrint() { - root := New("root") + root := newNode("root") root.Add("child1") root.Add("child2") - root.Print() + root.Print(os.Stdout) // Output: // root @@ -68,13 +69,13 @@ func ExampleSingleLayerPrint() { } func ExampleMultiLayerPrint() { - root := New("root") + root := newNode("root") child1 := root.Add("child1") child1.AddPair("key", "value") child2 := root.Add("child2") child2.Add("child2.1") child2.Add("child2.2") - root.Print() + root.Print(os.Stdout) // Output: // root @@ -85,7 +86,7 @@ func ExampleMultiLayerPrint() { // └── child2.2 } -func (n *Node) ContainsChild(value string) bool { +func (n *node) ContainsChild(value string) bool { for _, child := range n.Children { if child.Value == value { return true diff --git a/cmd/notation/internal/display/output/print.go b/cmd/notation/internal/display/output/print.go new file mode 100644 index 000000000..afdcf56b7 --- /dev/null +++ b/cmd/notation/internal/display/output/print.go @@ -0,0 +1,93 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// copied and adopted from https://github.com/oras-project/oras with +// modification +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package output provides the output tools for writing information to the +// output. +package output + +import ( + "encoding/json" + "fmt" + "io" + "sync" +) + +// Printer prints for status handlers. +type Printer struct { + out io.Writer + err io.Writer + lock sync.Mutex +} + +// NewPrinter creates a new Printer. +func NewPrinter(out io.Writer, err io.Writer) *Printer { + return &Printer{out: out, err: err} +} + +// Write implements the io.Writer interface. +func (p *Printer) Write(b []byte) (int, error) { + p.lock.Lock() + defer p.lock.Unlock() + + return p.out.Write(b) +} + +// Println prints objects concurrent-safely with newline. +func (p *Printer) Println(a ...any) error { + p.lock.Lock() + defer p.lock.Unlock() + + _, err := fmt.Fprintln(p.out, a...) + if err != nil { + err = fmt.Errorf("display output error: %w", err) + _, _ = fmt.Fprint(p.err, err) + return err + } + return nil +} + +// Printf prints objects concurrent-safely with newline. +func (p *Printer) Printf(format string, a ...any) error { + p.lock.Lock() + defer p.lock.Unlock() + + _, err := fmt.Fprintf(p.out, format, a...) + if err != nil { + err = fmt.Errorf("display output error: %w", err) + _, _ = fmt.Fprint(p.err, err) + return err + } + return nil +} + +// PrintPrettyJSON prints object to out in JSON format. +func PrintPrettyJSON(out io.Writer, object any) error { + encoder := json.NewEncoder(out) + encoder.SetIndent("", " ") + return encoder.Encode(object) +} diff --git a/cmd/notation/internal/display/output/print_test.go b/cmd/notation/internal/display/output/print_test.go new file mode 100644 index 000000000..0d07ff1f1 --- /dev/null +++ b/cmd/notation/internal/display/output/print_test.go @@ -0,0 +1,101 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// copied and adopted from https://github.com/oras-project/oras with +// modification +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package output + +import ( + "fmt" + "os" + "strings" + "testing" +) + +type mockWriter struct { + errorCount int + written string +} + +func (mw *mockWriter) Write(p []byte) (n int, err error) { + mw.written += string(p) + if strings.TrimSpace(string(p)) != "boom" { + return len(string(p)), nil + } + mw.errorCount++ + return 0, fmt.Errorf("boom %s", string(p)) +} + +func (mw *mockWriter) String() string { + return mw.written +} + +func TestPrinter_Print(t *testing.T) { + mockWriter := &mockWriter{} + printer := NewPrinter(mockWriter, os.Stderr) + + t.Run("Println success", func(t *testing.T) { + err := printer.Println("hello") + if err != nil { + t.Errorf("Expected no error got <%v>", err) + } + if mockWriter.String() != "hello\n" { + t.Errorf("Expected hello got <%s>", mockWriter.String()) + } + }) + t.Run("Println failed", func(t *testing.T) { + err := printer.Println("boom") + if mockWriter.errorCount != 1 { + t.Errorf("Expected one error actual <%d>", mockWriter.errorCount) + } + if err == nil { + t.Error("Expected error got ") + } + }) + t.Run("Printf failed", func(t *testing.T) { + err := printer.Printf("boom") + if mockWriter.errorCount != 2 { + t.Errorf("Expected two errors actual <%d>", mockWriter.errorCount) + } + if err == nil { + t.Error("Expected error got ") + } + }) +} + +func Test_PrintPrettyJSON(t *testing.T) { + builder := &strings.Builder{} + given := map[string]int{"bob": 5} + expected := "{\n \"bob\": 5\n}\n" + err := PrintPrettyJSON(builder, given) + if err != nil { + t.Error("Expected no error got <" + err.Error() + ">") + } + actual := builder.String() + if expected != actual { + t.Error("Expected <" + expected + "> not equal to actual <" + actual + ">") + } +} diff --git a/cmd/notation/internal/option/common.go b/cmd/notation/internal/option/common.go new file mode 100644 index 000000000..28289ce1d --- /dev/null +++ b/cmd/notation/internal/option/common.go @@ -0,0 +1,46 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// copied and adopted from https://github.com/oras-project/oras with +// modification +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package option abstracts the options and flags for commands to be used in +// methods, such as ApplyFlags and Parse. +package option + +import ( + "github.com/notaryproject/notation/cmd/notation/internal/display/output" + "github.com/spf13/cobra" +) + +// Common option struct. +type Common struct { + Printer *output.Printer +} + +// Parse gets target options from user input. +func (opts *Common) Parse(cmd *cobra.Command) { + opts.Printer = output.NewPrinter(cmd.OutOrStdout(), cmd.OutOrStderr()) +} diff --git a/cmd/notation/internal/option/format.go b/cmd/notation/internal/option/format.go new file mode 100644 index 000000000..7f7bd867f --- /dev/null +++ b/cmd/notation/internal/option/format.go @@ -0,0 +1,80 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// copied and adopted from https://github.com/oras-project/oras with +// modification +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// FormatType is the type of output format. +type FormatType string + +// format types +var ( + // FormatTypeJSON is the JSON format type. + FormatTypeJSON FormatType = "json" + // FormatTypeText is the text format type for human-readable output. + FormatTypeText FormatType = "text" +) + +// Format contains input and parsed options for formatted output flags. +type Format struct { + CurrentType string + allowedTypes []FormatType +} + +// ApplyFlags sets up the flags for the format option. +// +// The defaultType is the default format type. +// The otherTypes are additional format types that are allowed. +func (f *Format) ApplyFlags(fs *pflag.FlagSet, defaultType FormatType, otherTypes ...FormatType) { + f.CurrentType = string(defaultType) + f.allowedTypes = append(otherTypes, defaultType) + + var quotedAllowedTypes []string + for _, t := range f.allowedTypes { + quotedAllowedTypes = append(quotedAllowedTypes, fmt.Sprintf("'%s'", t)) + } + usage := fmt.Sprintf("output format, options: %s", strings.Join(quotedAllowedTypes, ", ")) + // apply flags + fs.StringVarP(&f.CurrentType, "output", "o", f.CurrentType, usage) +} + +// Parse parses the input format flag. +func (opts *Format) Parse(_ *cobra.Command) error { + if ok := slices.Contains(opts.allowedTypes, FormatType(opts.CurrentType)); !ok { + return fmt.Errorf("invalid format type: %q", opts.CurrentType) + } + return nil +} diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index a2bed995b..3c7c04640 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -121,15 +121,6 @@ var ( SetPflagReferrersTag = func(fs *pflag.FlagSet, p *bool, usage string) { fs.BoolVar(p, PflagReferrersTag.Name, true, 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 4f49ca0f8..98d4f51d9 100644 --- a/internal/envelope/envelope.go +++ b/internal/envelope/envelope.go @@ -61,21 +61,21 @@ func ValidatePayloadContentType(payload *signature.Payload) error { // 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) { +func DescriptorFromSignaturePayload(payload *signature.Payload) (ocispec.Descriptor, error) { if payload == nil { - return nil, errors.New("empty payload") + return ocispec.Descriptor{}, errors.New("empty payload") } err := ValidatePayloadContentType(payload) if err != nil { - return nil, err + return ocispec.Descriptor{}, 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 ocispec.Descriptor{}, errors.New("failed to unmarshall the payload content to Payload") } - return &parsedPayload.TargetArtifact, nil + return parsedPayload.TargetArtifact, nil } diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index a7a3bac6c..2eedb794e 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -14,7 +14,6 @@ package ioutil import ( - "encoding/json" "fmt" "io" "path/filepath" @@ -79,15 +78,3 @@ func PrintCertMap(w io.Writer, certPaths []string) error { } return tw.Flush() } - -// 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/test/e2e/suite/command/inspect.go b/test/e2e/suite/command/inspect.go index 8d04aa9e4..e896e1bff 100644 --- a/test/e2e/suite/command/inspect.go +++ b/test/e2e/suite/command/inspect.go @@ -24,17 +24,18 @@ import ( var ( inspectSuccessfully = []string{ + "Inspecting all signatures for signed artifact", "└── application/vnd.cncf.notary.signature", "└── sha256:", "├── media type:", "├── signature algorithm:", "├── signed attributes", - "signingTime:", - "signingScheme:", + "signing time:", + "signing scheme:", "├── user defined attributes", "│ └── (empty)", "├── unsigned attributes", - "│ └── signingAgent: notation-go/", + "│ └── signing agent: notation-go/", "├── certificates", "│ └── SHA256 fingerprint:", "issued to:", @@ -47,17 +48,18 @@ var ( } inspectSuccessfullyWithTimestamp = []string{ + "Inspecting all signatures for signed artifact", "└── application/vnd.cncf.notary.signature", "└── sha256:", "├── media type:", "├── signature algorithm:", "├── signed attributes", - "signingTime:", - "signingScheme:", + "signing time:", + "signing scheme:", "├── user defined attributes", "│ └── (empty)", "├── unsigned attributes", - "signingAgent: notation-go/", + "signing agent: notation-go/", "timestamp signature", "timestamp:", "certificates", @@ -172,4 +174,209 @@ var _ = Describe("notation inspect", func() { MatchKeyWords(inspectSuccessfullyWithTimestamp...) }) }) + + It("with timestamped oci layout", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + artifact := GenerateArtifact("e2e-with-timestamped-signature", "e2e-insepct-timestamped") + expectedOutput := `Inspecting all signatures for signed artifact +localhost:5000/e2e-insepct-timestamped@sha256:53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20 +└── application/vnd.cncf.notary.signature + └── sha256:6a5cd3a886707a317935dcaf13954fc80ef9aeb665262b6ae4fe469ad7ea3aea + ├── signature algorithm: RSASSA-PSS-SHA-256 + ├── signed attributes + │ ├── content type: application/vnd.cncf.notary.payload.v1+json + │ ├── signing scheme: notary.x509 + │ ├── signing time: Tue Jan 21 09:17:46 2025 + │ └── expiry: Tue Jan 21 12:39:46 2025 + ├── user defined attributes + │ └── purpose: e2e + ├── unsigned attributes + │ ├── signing agent: notation-go/1.3.0+unreleased + │ └── timestamp signature + │ ├── timestamp: [Tue Jan 21 09:17:46 2025, Tue Jan 21 09:17:47 2025] + │ └── certificates + │ ├── SHA256 fingerprint: 36e731cfa9bfd69dafb643809f6dec500902f7197daeaad86ea0159a2268a2b8 + │ │ ├── issued to: CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US + │ │ ├── issued by: CN=Microsoft Identity Verification Root Certificate Authority 2020,O=Microsoft Corporation,C=US + │ │ └── expiry: Mon Nov 19 20:42:31 2035 + │ └── SHA256 fingerprint: 59283806bbc4fec702339b6026633c2034333fd3cae5368323186209e406d522 + │ ├── issued to: CN=Microsoft Public RSA Time Stamping Authority,OU=Microsoft America Operations+OU=Thales TSS ESN:45D6-96C5-5E63,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US + │ ├── issued by: CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US + │ └── expiry: Wed Nov 19 18:48:54 2025 + ├── certificates + │ └── SHA256 fingerprint: 1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475 + │ ├── issued to: CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US + │ ├── issued by: CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US + │ └── expiry: Wed Jan 22 08:36:26 2025 + └── signed artifact + ├── media type: application/vnd.oci.image.manifest.v1+json + ├── digest: sha256:53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20 + └── size: 582 +` + + notation.Exec("inspect", artifact.ReferenceWithDigest()). + MatchContent(expectedOutput) + }) + }) + + It("with timestamped oci layout and output in JSON", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + artifact := GenerateArtifact("e2e-with-timestamped-signature", "e2e-inspect-timestamped-json") + expectedOutput := `{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "signatures": [ + { + "digest": "sha256:6a5cd3a886707a317935dcaf13954fc80ef9aeb665262b6ae4fe469ad7ea3aea", + "signatureAlgorithm": "RSASSA-PSS-SHA-256", + "signedAttributes": { + "contentType": "application/vnd.cncf.notary.payload.v1+json", + "expiry": "2025-01-21T12:39:46Z", + "signingScheme": "notary.x509", + "signingTime": "2025-01-21T09:17:46Z" + }, + "userDefinedAttributes": { + "purpose": "e2e" + }, + "unsignedAttributes": { + "signingAgent": "notation-go/1.3.0+unreleased", + "timestampSignature": { + "timestamp": "[2025-01-21T09:17:46.141Z, 2025-01-21T09:17:47.141Z]", + "certificates": [ + { + "SHA256Fingerprint": "36e731cfa9bfd69dafb643809f6dec500902f7197daeaad86ea0159a2268a2b8", + "issuedTo": "CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US", + "issuedBy": "CN=Microsoft Identity Verification Root Certificate Authority 2020,O=Microsoft Corporation,C=US", + "expiry": "2035-11-19T20:42:31Z" + }, + { + "SHA256Fingerprint": "59283806bbc4fec702339b6026633c2034333fd3cae5368323186209e406d522", + "issuedTo": "CN=Microsoft Public RSA Time Stamping Authority,OU=Microsoft America Operations+OU=Thales TSS ESN:45D6-96C5-5E63,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US", + "issuedBy": "CN=Microsoft Public RSA Timestamping CA 2020,O=Microsoft Corporation,C=US", + "expiry": "2025-11-19T18:48:54Z" + } + ] + } + }, + "certificates": [ + { + "SHA256Fingerprint": "1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475", + "issuedTo": "CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US", + "issuedBy": "CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US", + "expiry": "2025-01-22T08:36:26Z" + } + ], + "signedArtifact": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20", + "size": 582 + } + } + ] +} +` + + notation.Exec("inspect", artifact.ReferenceWithDigest(), "--output", "json"). + MatchContent(expectedOutput) + }) + }) + + It("with no signature in text format", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + artifact := GenerateArtifact("e2e", "e2e-inspect-no-signature") + expectedOutput := "localhost:5000/e2e-inspect-no-signature@sha256:b8479de3f88fb259a0a9ea82a5b2a052a1ef3c4ebbcfc61482d5ae4c831f8af9 has no associated signature\n" + notation.Exec("inspect", artifact.ReferenceWithDigest()). + MatchContent(expectedOutput) + }) + }) + + It("with no signature in JSON format", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + artifact := GenerateArtifact("e2e", "e2e-inspect-no-signature-json") + expectedOutput := `{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "signatures": [] +} +` + notation.Exec("inspect", "--output", "json", artifact.ReferenceWithDigest()). + MatchContent(expectedOutput) + }) + }) + + It("with invalid timestamp signature in text format", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + artifact := GenerateArtifact("e2e-with-invalid-timestamped-signature", "e2e-inspect-invalid-timstamped") + expectedOutput := `Inspecting all signatures for signed artifact +localhost:5000/e2e-inspect-invalid-timstamped@sha256:f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a +└── application/vnd.cncf.notary.signature + └── sha256:eee3eec7d2947f77713484753bea67879ff62c08a73a49a41151ed18c4d1c000 + ├── signature algorithm: RSASSA-PSS-SHA-256 + ├── signed attributes + │ ├── content type: application/vnd.cncf.notary.payload.v1+json + │ ├── signing scheme: notary.x509 + │ └── signing time: Tue Jan 21 08:41:17 2025 + ├── user defined attributes + │ └── purpose: e2e + ├── unsigned attributes + │ ├── signing agent: notation-go/1.3.0+unreleased + │ └── timestamp signature + │ └── error: failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length + ├── certificates + │ └── SHA256 fingerprint: 1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475 + │ ├── issued to: CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US + │ ├── issued by: CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US + │ └── expiry: Wed Jan 22 08:36:26 2025 + └── signed artifact + ├── media type: application/vnd.oci.image.manifest.v1+json + ├── digest: sha256:f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a + └── size: 582 +` + notation.Exec("inspect", artifact.ReferenceWithDigest()). + MatchContent(expectedOutput) + }) + }) + + It("with invalid timestamp signature in json format", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + artifact := GenerateArtifact("e2e-with-invalid-timestamped-signature", "e2e-inspect-invalid-timstamped") + expectedOutput := `{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "signatures": [ + { + "digest": "sha256:eee3eec7d2947f77713484753bea67879ff62c08a73a49a41151ed18c4d1c000", + "signatureAlgorithm": "RSASSA-PSS-SHA-256", + "signedAttributes": { + "contentType": "application/vnd.cncf.notary.payload.v1+json", + "signingScheme": "notary.x509", + "signingTime": "2025-01-21T08:41:17Z" + }, + "userDefinedAttributes": { + "purpose": "e2e" + }, + "unsignedAttributes": { + "signingAgent": "notation-go/1.3.0+unreleased", + "timestampSignature": { + "error": "failed to parse timestamp countersignature: cms: syntax error: invalid signed data: failed to convert from BER to DER: asn1: syntax error: decoding BER length octets: short form length octets value should be less or equal to the subsequent octets length" + } + }, + "certificates": [ + { + "SHA256Fingerprint": "1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475", + "issuedTo": "CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US", + "issuedBy": "CN=testcert7,O=Notary,L=Seattle,ST=WA,C=US", + "expiry": "2025-01-22T08:36:26Z" + } + ], + "signedArtifact": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a", + "size": 582 + } + } + ] +} +` + notation.Exec("inspect", "-o", "json", artifact.ReferenceWithDigest()). + MatchContent(expectedOutput) + }) + }) }) diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/8d2e70ffc2af68b7f2df6f30dfefcb7bcda1db5f9ac2566fc8c6c2248c8d4c5f b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/8d2e70ffc2af68b7f2df6f30dfefcb7bcda1db5f9ac2566fc8c6c2248c8d4c5f new file mode 100644 index 000000000..d470f2d74 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/8d2e70ffc2af68b7f2df6f30dfefcb7bcda1db5f9ac2566fc8c6c2248c8d4c5f @@ -0,0 +1,12 @@ +{ + "payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJhbm5vdGF0aW9ucyI6eyJwdXJwb3NlIjoiZTJlIn0sImRpZ2VzdCI6InNoYTI1NjpmMWRhOGNkNzBkNmQ4NTFmYTIzMTNjOGQ2NjE4Zjc5NTA4Y2YxZTg2ODc3ZWRmMWMwYmZlNDlhMWIwYTY0NjdhIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5tYW5pZmVzdC52MStqc29uIiwic2l6ZSI6NTgyfX0", + "protected": "eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI1LTAxLTIxVDA4OjQxOjE3WiJ9", + "header": { + "io.cncf.notary.timestampSignature": "aW52YWxpZAo=", + "x5c": [ + "MIIDRTCCAi2gAwIBAgICALYwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxEjAQBgNVBAMTCXRlc3RjZXJ0NzAeFw0yNTAxMjEwODM2MjZaFw0yNTAxMjIwODM2MjZaMFExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHU2VhdHRsZTEPMA0GA1UEChMGTm90YXJ5MRIwEAYDVQQDEwl0ZXN0Y2VydDcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXjPHvnk0IiQjPPc1Z6Dfk09kq3+RtFK9f+wll6205QE3jPgvkebSwal79+xtRdXJlzmqa0pMskegrilvheehj12l4UGP9SbjPt1oO0Vf1k8qVaFV2h309Sg1h0JJvQYKtCIHBiwzSORdJfFuCDKFy1y1ErC4BuhR49JbWva0GWcvVJtPGCccxMKErf9U3xV9T+cqubjdrM2cMcAzgjttXwzSljmA3c+Kj0M+CaoZYb/V3r3iU1rmCDtIgWFFjKN2eb6EDLLFZrnKUVEDtTrTeUNH5jLjAMAlDQM0Mmjsv9Jb0ZeN/hY3BbuacHNoQeTQhIUEZ9Tol1vmbCC3qQSLHAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAt9wF6UJ7eJBw9kcTLs88n0yAPqoD2YGw2ciRoMgdcFO8ncrojgqmrGNP+E9n+SZcgR5I0Q04aNnN6uiUX6sXUmJQLxptLD10nHF+953gQYfFNgpzONyunme7h/Xq5goZvJfBpM0551MUHmKVisKhZCMoF6puQ9GIDV6yDh7Vn+yBQcSq6maUF1/CNTSapgJA1ntEcbAo9Nd7w6+rnUmezdKwWbxPwzwL+EZ6EoQ8tywOJrjMLzZ4E5D3pvwK0e3j6VTcTlb8YCi/Xj8dAEdYn7+w4WjukdOGUVJtTOZ1WRPi7T3JoV96ry/LeNtnaQLrQU+mClRTa93OhnkuWNUw+Q==" + ], + "io.cncf.notary.signingAgent": "notation-go/1.3.0+unreleased" + }, + "signature": "uYBON0RNGgDI5U__12-0TkWk6ClgzcWHS9aCC5qtQ2yrCfl9a_NDkHYhkhEG_TvdA--8mWpw_kjiS1oQHbsjr0KZED29lux1Ow8cq_0h13VjKnVVGeL_4aATmypV6DA9zVuolOW_vzHpZg86un1qUFtLx6Gv5RYZ97SqwMv5lEIQuqk0BwO4XaBuidUrAw44g5SeNRix-TurQxUEUkXxPyp6NZ0MoAaJAMDVXZK8ExZ2UczYolOuOVxgqJdHxSRp9izKIZnJp9fgg3tOXWJ3y5sQ_sjmzgdlk2medRtEoum55H0cxGV5gQiYRMfiROcFacaK_y8IDhARFxjNethCOQ" +} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d new file mode 100644 index 000000000..d383c56ff --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d @@ -0,0 +1 @@ +testdata diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/eee3eec7d2947f77713484753bea67879ff62c08a73a49a41151ed18c4d1c000 b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/eee3eec7d2947f77713484753bea67879ff62c08a73a49a41151ed18c4d1c000 new file mode 100644 index 000000000..96e80f65f --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/eee3eec7d2947f77713484753bea67879ff62c08a73a49a41151ed18c4d1c000 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.notary.signature","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/jose+json","digest":"sha256:8d2e70ffc2af68b7f2df6f30dfefcb7bcda1db5f9ac2566fc8c6c2248c8d4c5f","size":2269}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a","size":582},"annotations":{"io.cncf.notary.x509chain.thumbprint#S256":"[\"1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475\"]","org.opencontainers.image.created":"2025-01-21T08:41:17Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a new file mode 100644 index 000000000..b7ba87f01 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/blobs/sha256/f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"application/vnd.unknown.artifact.v1","config":{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2,"data":"e30="},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d","size":9,"annotations":{"org.opencontainers.image.title":"file"}}],"annotations":{"org.opencontainers.image.created":"2025-01-21T08:40:36Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/index.json b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/index.json new file mode 100644 index 000000000..2a461366f --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:f1da8cd70d6d851fa2313c8d6618f79508cf1e86877edf1c0bfe49a1b0a6467a","size":582,"annotations":{"org.opencontainers.image.created":"2025-01-21T08:40:36Z","org.opencontainers.image.ref.name":"v1"},"artifactType":"application/vnd.unknown.artifact.v1"},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eee3eec7d2947f77713484753bea67879ff62c08a73a49a41151ed18c4d1c000","size":728,"annotations":{"io.cncf.notary.x509chain.thumbprint#S256":"[\"1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475\"]","org.opencontainers.image.created":"2025-01-21T08:41:17Z"}}]} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/oci-layout b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/oci-layout new file mode 100644 index 000000000..1343d370f --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-invalid-timestamped-signature/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion":"1.0.0"} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20 b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20 new file mode 100644 index 000000000..552763b11 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"application/vnd.unknown.artifact.v1","config":{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2,"data":"e30="},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d","size":9,"annotations":{"org.opencontainers.image.title":"file"}}],"annotations":{"org.opencontainers.image.created":"2025-01-21T09:17:17Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/603ff134e97a79cd7a15560b76137c2c4a10451daab11327a7c6f86dac250fee b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/603ff134e97a79cd7a15560b76137c2c4a10451daab11327a7c6f86dac250fee new file mode 100644 index 000000000..ebafdb8d1 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/603ff134e97a79cd7a15560b76137c2c4a10451daab11327a7c6f86dac250fee @@ -0,0 +1 @@ +{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJhbm5vdGF0aW9ucyI6eyJwdXJwb3NlIjoiZTJlIn0sImRpZ2VzdCI6InNoYTI1Njo1M2IwMTkxMjE4YWVkOWEzYzFmN2M2NjE3MzZhYzQwY2ZjOGViOTI4NjQyMzQ4ZmQ4NDNiYTNmMDQ4M2MwYzIwIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5tYW5pZmVzdC52MStqc29uIiwic2l6ZSI6NTgyfX0","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjAyNS0wMS0yMVQxMjozOTo0NloiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjUtMDEtMjFUMDk6MTc6NDZaIn0","header":{"io.cncf.notary.timestampSignature":"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","x5c":["MIIDRTCCAi2gAwIBAgICALYwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxEjAQBgNVBAMTCXRlc3RjZXJ0NzAeFw0yNTAxMjEwODM2MjZaFw0yNTAxMjIwODM2MjZaMFExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHU2VhdHRsZTEPMA0GA1UEChMGTm90YXJ5MRIwEAYDVQQDEwl0ZXN0Y2VydDcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXjPHvnk0IiQjPPc1Z6Dfk09kq3+RtFK9f+wll6205QE3jPgvkebSwal79+xtRdXJlzmqa0pMskegrilvheehj12l4UGP9SbjPt1oO0Vf1k8qVaFV2h309Sg1h0JJvQYKtCIHBiwzSORdJfFuCDKFy1y1ErC4BuhR49JbWva0GWcvVJtPGCccxMKErf9U3xV9T+cqubjdrM2cMcAzgjttXwzSljmA3c+Kj0M+CaoZYb/V3r3iU1rmCDtIgWFFjKN2eb6EDLLFZrnKUVEDtTrTeUNH5jLjAMAlDQM0Mmjsv9Jb0ZeN/hY3BbuacHNoQeTQhIUEZ9Tol1vmbCC3qQSLHAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAt9wF6UJ7eJBw9kcTLs88n0yAPqoD2YGw2ciRoMgdcFO8ncrojgqmrGNP+E9n+SZcgR5I0Q04aNnN6uiUX6sXUmJQLxptLD10nHF+953gQYfFNgpzONyunme7h/Xq5goZvJfBpM0551MUHmKVisKhZCMoF6puQ9GIDV6yDh7Vn+yBQcSq6maUF1/CNTSapgJA1ntEcbAo9Nd7w6+rnUmezdKwWbxPwzwL+EZ6EoQ8tywOJrjMLzZ4E5D3pvwK0e3j6VTcTlb8YCi/Xj8dAEdYn7+w4WjukdOGUVJtTOZ1WRPi7T3JoV96ry/LeNtnaQLrQU+mClRTa93OhnkuWNUw+Q=="],"io.cncf.notary.signingAgent":"notation-go/1.3.0+unreleased"},"signature":"V56oTNY5JLB7KFy4tytRS7R7xv5YOoACEFo9TC4_HIyTfWsyFxy00TbdrAdSupds7XSDlX3daouGpfUHF5XU6WEeGr1iqztNILj-MA_P69FBR3qZIZTo-fincym0UvhTgwY8LMCCCvq1mTZcdnyIUl67GoJZx_N7t6mpzISjQ-VXmX-oPSngjDi3FqJpKzRPFBOAbu-oQhic3D3CSnx1Ot1ckvQTw3WCL5WYEJ7WEz_fAzbqY35guATIgGrSZKR4H9NjX-_A79zewpmGVUu_tGSY1WpqVhshm3taBtaVwxYzs4HT19ql7ch_xdd656trWpEmm5R_P_MIeKl0heHYGw"} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/6a5cd3a886707a317935dcaf13954fc80ef9aeb665262b6ae4fe469ad7ea3aea b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/6a5cd3a886707a317935dcaf13954fc80ef9aeb665262b6ae4fe469ad7ea3aea new file mode 100644 index 000000000..fb9240479 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/6a5cd3a886707a317935dcaf13954fc80ef9aeb665262b6ae4fe469ad7ea3aea @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.notary.signature","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/jose+json","digest":"sha256:603ff134e97a79cd7a15560b76137c2c4a10451daab11327a7c6f86dac250fee","size":10314}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20","size":582},"annotations":{"io.cncf.notary.x509chain.thumbprint#S256":"[\"1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475\"]","org.opencontainers.image.created":"2025-01-21T09:17:46Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d new file mode 100644 index 000000000..d383c56ff --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/blobs/sha256/ce41e5246ead8bddd2a2b5bbb863db250f328be9dc5c3041481d778a32f8130d @@ -0,0 +1 @@ +testdata diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/index.json b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/index.json new file mode 100644 index 000000000..09234f337 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:53b0191218aed9a3c1f7c661736ac40cfc8eb928642348fd843ba3f0483c0c20","size":582,"annotations":{"org.opencontainers.image.created":"2025-01-21T09:17:17Z","org.opencontainers.image.ref.name":"v1"},"artifactType":"application/vnd.unknown.artifact.v1"},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:6a5cd3a886707a317935dcaf13954fc80ef9aeb665262b6ae4fe469ad7ea3aea","size":729,"annotations":{"io.cncf.notary.x509chain.thumbprint#S256":"[\"1717fa9d18f7e9c0f609499474adfe2b8e44172454f1d6e2183d5d04f79af475\"]","org.opencontainers.image.created":"2025-01-21T09:17:46Z"}}]} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/oci-layout b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/oci-layout new file mode 100644 index 000000000..1343d370f --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-with-timestamped-signature/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion":"1.0.0"} \ No newline at end of file