From dc9ad63c5e70ae13af2f0dcf8f062c356ce62ae4 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 30 Jul 2024 10:04:16 +0800 Subject: [PATCH] feat: update inspect command with timestamping (#998) Signed-off-by: Patrick Zheng --- cmd/notation/inspect.go | 91 +++++++++++++++++++++++++------ cmd/notation/inspect_test.go | 20 +++++++ specs/commandline/inspect.md | 62 ++++++++++++++++++--- test/e2e/suite/command/inspect.go | 85 +++++++++++++++++++++-------- 4 files changed, 210 insertions(+), 48 deletions(-) diff --git a/cmd/notation/inspect.go b/cmd/notation/inspect.go index df91dbea0..a55595c51 100644 --- a/cmd/notation/inspect.go +++ b/cmd/notation/inspect.go @@ -15,7 +15,7 @@ package main import ( "crypto/sha256" - b64 "encoding/base64" + "crypto/x509" "encoding/hex" "errors" "fmt" @@ -33,6 +33,7 @@ import ( "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" ) @@ -57,7 +58,7 @@ type signatureOutput struct { SignatureAlgorithm string `json:"signatureAlgorithm"` SignedAttributes map[string]string `json:"signedAttributes"` UserDefinedAttributes map[string]string `json:"userDefinedAttributes"` - UnsignedAttributes map[string]string `json:"unsignedAttributes"` + UnsignedAttributes map[string]any `json:"unsignedAttributes"` Certificates []certificateOutput `json:"certificates"` SignedArtifact ocispec.Descriptor `json:"signedArtifact"` } @@ -69,6 +70,12 @@ type certificateOutput struct { 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{} @@ -181,8 +188,8 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error { SignatureAlgorithm: string(signatureAlgorithm), SignedAttributes: getSignedAttributes(opts.outputFormat, envelopeContent), UserDefinedAttributes: signedArtifactDesc.Annotations, - UnsignedAttributes: getUnsignedAttributes(envelopeContent), - Certificates: getCertificates(opts.outputFormat, envelopeContent), + UnsignedAttributes: getUnsignedAttributes(opts.outputFormat, envelopeContent), + Certificates: getCertificates(opts.outputFormat, envelopeContent.SignerInfo.CertificateChain), SignedArtifact: *signedArtifactDesc, } @@ -235,11 +242,11 @@ func getSignedAttributes(outputFormat string, envContent *signature.EnvelopeCont return signedAttributes } -func getUnsignedAttributes(envContent *signature.EnvelopeContent) map[string]string { - unsignedAttributes := map[string]string{} +func getUnsignedAttributes(outputFormat string, envContent *signature.EnvelopeContent) map[string]any { + unsignedAttributes := make(map[string]any) if envContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil { - unsignedAttributes["timestampSignature"] = b64.StdEncoding.EncodeToString(envContent.SignerInfo.UnsignedAttributes.TimestampSignature) + unsignedAttributes["timestampSignature"] = parseTimestamp(outputFormat, envContent.SignerInfo) } if envContent.SignerInfo.UnsignedAttributes.SigningAgent != "" { @@ -258,10 +265,10 @@ func formatTimestamp(outputFormat string, t time.Time) string { } } -func getCertificates(outputFormat string, envContent *signature.EnvelopeContent) []certificateOutput { +func getCertificates(outputFormat string, certChain []*x509.Certificate) []certificateOutput { certificates := []certificateOutput{} - for _, cert := range envContent.SignerInfo.CertificateChain { + for _, cert := range certChain { h := sha256.Sum256(cert.Raw) fingerprint := strings.ToLower(hex.EncodeToString(h[:])) @@ -304,16 +311,23 @@ func printOutput(outputFormat string, ref string, output inspectOutput) error { 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("SHA256 fingerprint", cert.SHA256Fingerprint) - certNode.AddPair("issued to", cert.IssuedTo) - certNode.AddPair("issued by", cert.IssuedBy) - certNode.AddPair("expiry", cert.Expiry) + 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()) @@ -333,3 +347,46 @@ func addMapToTree(node *tree.Node, m map[string]string) { 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 f1309fef9..a6c20f596 100644 --- a/cmd/notation/inspect_test.go +++ b/cmd/notation/inspect_test.go @@ -16,6 +16,7 @@ package main import ( "testing" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation/internal/cmd" ) @@ -84,3 +85,22 @@ func TestInspectCommand_MissingArgs(t *testing.T) { t.Fatal("Parse Args expected error, but ok") } } + +func TestGetUnsignedAttributes(t *testing.T) { + envContent := &signature.EnvelopeContent{ + SignerInfo: signature.SignerInfo{ + UnsignedAttributes: signature.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(cmd.OutputPlaintext, envContent) + val, ok := unsignedAttr["timestampSignature"].(timestampOutput) + if !ok { + t.Fatal("expected to have timestampSignature") + } + if val.Error != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, val.Error) + } +} diff --git a/specs/commandline/inspect.md b/specs/commandline/inspect.md index b068bff2c..708b48ee7 100644 --- a/specs/commandline/inspect.md +++ b/specs/commandline/inspect.md @@ -78,7 +78,13 @@ localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac4efe37a5380ee │ ├── user defined attributes │ │ └── io.wabbit-networks.buildId: 123 //user defined metadata │ ├── unsigned attributes - │ │ ├── io.cncf.notary.timestampSignature: //TSA response + │ │ ├── timestamp signature //TSA response + | │ │ ├── timestamp: [Fri Jun 23 22:04:31 2023, Fri Jun 23 22:04:31 2023] + | │ │ └── certificates + | │ │ └── SHA256 fingerprint: d2f6e46ded7422ccd1d440576841366f828ada559aae3316af4d1a9ad40c7828 + | │ │ ├── issued to: wabbit-com Software Timestamp + | │ │ ├── issued by: wabbit-com Software Trusted Timestamping + | │ │ └── expiry: Fri Oct 13 23:59:59 2034 │ │ └── io.cncf.notary.signingAgent: notation/1.0.0 //client version │ ├── certificates │ │ ├── SHA256 fingerprint: E8C15B4C98AD91E051EE5AF5F524A8729050B2A @@ -106,7 +112,13 @@ localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac4efe37a5380ee │ ├── expiry: Sat Jun 29 22:04:01 2024 │ └── io.cncf.notary.verificationPlugin: com.example.nv2plugin ├── unsigned attributes - │ ├── io.cncf.notary.timestampSignature: + │ ├── timestamp signature + │ │ ├── timestamp: [Fri Jun 23 22:04:31 2023, Fri Jun 23 22:04:31 2023] + │ │ └── certificates + │ │ └── SHA256 fingerprint: d2f6e46ded7422ccd1d440576841366f828ada559aae3316af4d1a9ad40c7828 + │ │ ├── issued to: wabbit-com Software Timestamp + │ │ ├── issued by: wabbit-com Software Trusted Timestamping + │ │ └── expiry: Fri Oct 13 23:59:59 2034 │ └── io.cncf.notary.signingAgent: notation/1.0.0 ├── certificates │ ├── SHA256 fingerprint: b13a843be16b1f461f08d61c14f3eab7d87c073570da077217541a7eb31c084d @@ -157,7 +169,13 @@ localhost:5000/net-monitor@sha256:ca5427b5567d3e06a72e52d7da7dabfac484efe37a5380 │ ├── user defined attributes │ │ └── io.wabbit-networks.buildId: 123 │ ├── unsigned attributes - │ │ ├── io.cncf.notary.timestampSignature: + │ │ ├── timestamp signature + | │ │ ├── timestamp: [Fri Jun 23 22:04:31 2023, Fri Jun 23 22:04:31 2023] + | │ │ └── certificates + | │ │ └── SHA256 fingerprint: d2f6e46ded7422ccd1d440576841366f828ada559aae3316af4d1a9ad40c7828 + | │ │ ├── issued to: wabbit-com Software Timestamp + | │ │ ├── issued by: wabbit-com Software Trusted Timestamping + | │ │ └── expiry: Fri Oct 13 23:59:59 2034 │ │ └── io.cncf.notary.signingAgent: notation/1.0.0 │ ├── certificates │ │ ├── SHA256 fingerprint: b13a843be16b1f461f08d61c14f3eab7d87c073570da077217541a7eb31c084d @@ -185,7 +203,13 @@ localhost:5000/net-monitor@sha256:ca5427b5567d3e06a72e52d7da7dabfac484efe37a5380 │ ├── expiry: Sat Jun 29 22:04:01 2024 │ └── io.cncf.notary.verificationPlugin: com.example.nv2plugin ├── unsigned attributes - │ ├── io.cncf.notary.timestampSignature: + │ ├── timestamp signature + │ │ ├── timestamp: [Fri Jun 23 22:04:31 2023, Fri Jun 23 22:04:31 2023] + │ │ └── certificates + │ │ └── SHA256 fingerprint: d2f6e46ded7422ccd1d440576841366f828ada559aae3316af4d1a9ad40c7828 + │ │ ├── issued to: wabbit-com Software Timestamp + │ │ ├── issued by: wabbit-com Software Trusted Timestamping + │ │ └── expiry: Fri Oct 13 23:59:59 2034 │ └── io.cncf.notary.signingAgent: notation/1.0.0 ├── certificates │ ├── SHA256 fingerprint: b13a843be16b1f461f08d61c14f3eab7d87c073570da077217541a7eb31c084d @@ -230,8 +254,18 @@ An example output: "io.wabbit-networks.buildId": "123" }, "unsignedAttributes": { - "io.cncf.notary.timestampSignature": "", - "io.cncf.notary.signingAgent": "notation/1.0.0" + "timestampSignature": { + "timestamp": "[2022-02-06T20:50:37Z, 2022-02-06T20:50:37Z]", + "certificates": [ + { + "SHA256Fingerprint": "d2f6e46ded7422ccd1d440576841366f828ada559aae3316af4d1a9ad40c7828", + "issuedTo": "wabbit-com Software Timestamp", + "issuedBy": "wabbit-com Software Trusted Timestamping", + "expiry": "2034-10-13T23:59:59Z" + } + ] + }, + "signingAgent": "notation/1.0.0" }, "certificates": [ { @@ -269,9 +303,19 @@ An example output: "expiry": "2023-02-06T20:50:17Z", "io.cncf.notary.verificationPlugin": "com.example.nv2plugin" }, - "unsignedAttributes": { - "io.cncf.notary.timestampSignature": "", - "io.cncf.notary.signingAgent": "notation/1.0.0" + "unsignedAttributes": { + "timestampSignature": { + "timestamp": "[2022-02-06T20:50:37Z, 2022-02-06T20:50:37Z]", + "certificates": [ + { + "SHA256Fingerprint": "d2f6e46ded7422ccd1d440576841366f828ada559aae3316af4d1a9ad40c7828", + "issuedTo": "wabbit-com Software Timestamp", + "issuedBy": "wabbit-com Software Trusted Timestamping", + "expiry": "2034-10-13T23:59:59Z" + } + ] + }, + "signingAgent": "notation/1.0.0" }, "certificates": [ { diff --git a/test/e2e/suite/command/inspect.go b/test/e2e/suite/command/inspect.go index 49ba33e77..a6f6c1880 100644 --- a/test/e2e/suite/command/inspect.go +++ b/test/e2e/suite/command/inspect.go @@ -14,34 +14,65 @@ package command import ( + "path/filepath" + . "github.com/notaryproject/notation/test/e2e/internal/notation" "github.com/notaryproject/notation/test/e2e/internal/utils" . "github.com/notaryproject/notation/test/e2e/suite/common" . "github.com/onsi/ginkgo/v2" ) -var inspectSuccessfully = []string{ - "└── application/vnd.cncf.notary.signature", - "└── sha256:", - "├── media type:", - "├── signature algorithm:", - "├── signed attributes", - "signingTime:", - "signingScheme:", - "├── user defined attributes", - "│ └── (empty)", - "├── unsigned attributes", - "│ └── signingAgent: Notation/", - "├── certificates", - "│ └── SHA256 fingerprint:", - "issued to:", - "issued by:", - "expiry:", - "└── signed artifact", - "media type:", - "digest:", - "size:", -} +var ( + inspectSuccessfully = []string{ + "└── application/vnd.cncf.notary.signature", + "└── sha256:", + "├── media type:", + "├── signature algorithm:", + "├── signed attributes", + "signingTime:", + "signingScheme:", + "├── user defined attributes", + "│ └── (empty)", + "├── unsigned attributes", + "│ └── signingAgent: Notation/", + "├── certificates", + "│ └── SHA256 fingerprint:", + "issued to:", + "issued by:", + "expiry:", + "└── signed artifact", + "media type:", + "digest:", + "size:", + } + + inspectSuccessfullyWithTimestamp = []string{ + "└── application/vnd.cncf.notary.signature", + "└── sha256:", + "├── media type:", + "├── signature algorithm:", + "├── signed attributes", + "signingTime:", + "signingScheme:", + "├── user defined attributes", + "│ └── (empty)", + "├── unsigned attributes", + "signingAgent: Notation/", + "timestamp signature", + "timestamp:", + "certificates", + "SHA256 fingerprint:", + "├── certificates", + "│ └── SHA256 fingerprint:", + "issued to:", + "issued by:", + "expiry:", + "└── signed artifact", + "media type:", + "digest:", + "size:", + } +) var _ = Describe("notation inspect", func() { It("all signatures of an image", func() { @@ -131,4 +162,14 @@ var _ = Describe("notation inspect", func() { MatchKeyWords(inspectSuccessfully...) }) }) + + It("with timestamping", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", "--timestamp-url", "http://rfc3161timestamp.globalsign.com/advanced", "--timestamp-root-cert", filepath.Join(NotationE2EConfigPath, "timestamp", "globalsignTSARoot.cer"), artifact.ReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("inspect", artifact.ReferenceWithDigest()). + MatchKeyWords(inspectSuccessfullyWithTimestamp...) + }) + }) })