Skip to content

Commit

Permalink
feat: update inspect command with timestamping (#998)
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Zheng <[email protected]>
  • Loading branch information
Two-Hearts authored Jul 30, 2024
1 parent 9c15eec commit dc9ad63
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 48 deletions.
91 changes: 74 additions & 17 deletions cmd/notation/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ package main

import (
"crypto/sha256"
b64 "encoding/base64"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
Expand All @@ -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"
)
Expand All @@ -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"`
}
Expand All @@ -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{}
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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[:]))

Expand Down Expand Up @@ -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())
Expand All @@ -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,
}
}
20 changes: 20 additions & 0 deletions cmd/notation/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main
import (
"testing"

"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation/internal/cmd"
)

Expand Down Expand Up @@ -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)
}
}
62 changes: 53 additions & 9 deletions specs/commandline/inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <Base64(TimeStampToken)> //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
Expand Down Expand Up @@ -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: <Base64(TimeStampToken)>
│ ├── 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
Expand Down Expand Up @@ -157,7 +169,13 @@ localhost:5000/net-monitor@sha256:ca5427b5567d3e06a72e52d7da7dabfac484efe37a5380
│ ├── user defined attributes
│ │ └── io.wabbit-networks.buildId: 123
│ ├── unsigned attributes
│ │ ├── io.cncf.notary.timestampSignature: <Base64(TimeStampToken)>
│ │ ├── 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
Expand Down Expand Up @@ -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: <Base64(TimeStampToken)>
│ ├── 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
Expand Down Expand Up @@ -230,8 +254,18 @@ An example output:
"io.wabbit-networks.buildId": "123"
},
"unsignedAttributes": {
"io.cncf.notary.timestampSignature": "<Base64(TimeStampToken)>",
"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": [
{
Expand Down Expand Up @@ -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": "<Base64(TimeStampToken)>",
"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": [
{
Expand Down
85 changes: 63 additions & 22 deletions test/e2e/suite/command/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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...)
})
})
})

0 comments on commit dc9ad63

Please sign in to comment.