Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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...)
})
})
})