Skip to content

Commit 80cb6ee

Browse files
authored
refactor: extract inspect rendering logic to be display handlers (#1150)
Depends on spec change: #1156 Refactor: - Extract output formatting logic from the inspect command layer to an isolated display handler layer for processing rendering. - Add `json` and `tree` inspect handlers. Fix: - For `tree` output, make the key names with multiple words separated by space characters rather than capitalizing the words, which is defined in the [inspect command spec](https://github.com/notaryproject/notation/blob/v1.2.0/specs/commandline/inspect.md#inspect-signatures-on-the-supplied-oci-artifact-identified-by-the-digest). - For `json` output, default to rendering time in RFC3339 with nanoseconds (Notation expiry, signing time and certificate expiry are accurate to 1 second. Timestamp [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2) can have fraction-of-second time value). E2E Test: - inspect signature with timestamp, signature expiry and user metadata (text, json) - inspect signatures with invalid timestamp (test, json) - inspect with `-o` shorthand. Resolves part of #1151 --------- Signed-off-by: Junjie Gao <[email protected]>
1 parent ac77b58 commit 80cb6ee

33 files changed

+1403
-330
lines changed

cmd/notation/inspect.go

Lines changed: 18 additions & 247 deletions
Original file line numberDiff line numberDiff line change
@@ -14,68 +14,30 @@
1414
package main
1515

1616
import (
17-
"crypto/sha256"
18-
"crypto/x509"
19-
"encoding/hex"
2017
"errors"
2118
"fmt"
2219
"os"
23-
"strconv"
24-
"strings"
25-
"time"
2620

2721
"github.com/notaryproject/notation-core-go/signature"
28-
"github.com/notaryproject/notation-go/plugin/proto"
29-
"github.com/notaryproject/notation-go/registry"
22+
"github.com/notaryproject/notation/cmd/notation/internal/display"
3023
cmderr "github.com/notaryproject/notation/cmd/notation/internal/errors"
3124
"github.com/notaryproject/notation/cmd/notation/internal/experimental"
25+
"github.com/notaryproject/notation/cmd/notation/internal/option"
3226
"github.com/notaryproject/notation/internal/cmd"
33-
"github.com/notaryproject/notation/internal/envelope"
34-
"github.com/notaryproject/notation/internal/ioutil"
35-
"github.com/notaryproject/notation/internal/tree"
36-
"github.com/notaryproject/tspclient-go"
3727
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3828
"github.com/spf13/cobra"
3929
)
4030

4131
type inspectOpts struct {
4232
cmd.LoggingFlagOpts
4333
SecureFlagOpts
34+
option.Common
35+
option.Format
4436
reference string
45-
outputFormat string
4637
allowReferrersAPI bool
4738
maxSignatures int
4839
}
4940

50-
type inspectOutput struct {
51-
MediaType string `json:"mediaType"`
52-
Signatures []signatureOutput
53-
}
54-
55-
type signatureOutput struct {
56-
MediaType string `json:"mediaType"`
57-
Digest string `json:"digest"`
58-
SignatureAlgorithm string `json:"signatureAlgorithm"`
59-
SignedAttributes map[string]string `json:"signedAttributes"`
60-
UserDefinedAttributes map[string]string `json:"userDefinedAttributes"`
61-
UnsignedAttributes map[string]any `json:"unsignedAttributes"`
62-
Certificates []certificateOutput `json:"certificates"`
63-
SignedArtifact ocispec.Descriptor `json:"signedArtifact"`
64-
}
65-
66-
type certificateOutput struct {
67-
SHA256Fingerprint string `json:"SHA256Fingerprint"`
68-
IssuedTo string `json:"issuedTo"`
69-
IssuedBy string `json:"issuedBy"`
70-
Expiry string `json:"expiry"`
71-
}
72-
73-
type timestampOutput struct {
74-
Timestamp string `json:"timestamp,omitempty"`
75-
Certificates []certificateOutput `json:"certificates,omitempty"`
76-
Error string `json:"error,omitempty"`
77-
}
78-
7941
func inspectCommand(opts *inspectOpts) *cobra.Command {
8042
if opts == nil {
8143
opts = &inspectOpts{}
@@ -103,6 +65,10 @@ Example - Inspect signatures on an OCI artifact identified by a digest and outpu
10365
return nil
10466
},
10567
PreRunE: func(cmd *cobra.Command, args []string) error {
68+
if err := opts.Format.Parse(cmd); err != nil {
69+
return err
70+
}
71+
opts.Common.Parse(cmd)
10672
return experimental.CheckFlagsAndWarn(cmd, "allow-referrers-api")
10773
},
10874
RunE: func(cmd *cobra.Command, args []string) error {
@@ -118,18 +84,21 @@ Example - Inspect signatures on an OCI artifact identified by a digest and outpu
11884

11985
opts.LoggingFlagOpts.ApplyFlags(command.Flags())
12086
opts.SecureFlagOpts.ApplyFlags(command.Flags())
121-
cmd.SetPflagOutput(command.Flags(), &opts.outputFormat, cmd.PflagOutputUsage)
12287
command.Flags().IntVar(&opts.maxSignatures, "max-signatures", 100, "maximum number of signatures to evaluate or examine")
12388
cmd.SetPflagReferrersAPI(command.Flags(), &opts.allowReferrersAPI, fmt.Sprintf(cmd.PflagReferrersUsageFormat, "inspect"))
89+
90+
// set output format
91+
opts.Format.ApplyFlags(command.Flags(), option.FormatTypeText, option.FormatTypeJSON)
12492
return command
12593
}
12694

12795
func runInspect(command *cobra.Command, opts *inspectOpts) error {
12896
// set log level
12997
ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context())
13098

131-
if opts.outputFormat != cmd.OutputJSON && opts.outputFormat != cmd.OutputPlaintext {
132-
return fmt.Errorf("unrecognized output format %s", opts.outputFormat)
99+
displayHandler, err := display.NewInpsectHandler(opts.Printer, opts.Format)
100+
if err != nil {
101+
return err
133102
}
134103

135104
// initialize
@@ -144,7 +113,8 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
144113
if err != nil {
145114
return err
146115
}
147-
output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}}
116+
displayHandler.OnReferenceResolved(resolvedRef, manifestDesc.MediaType)
117+
148118
skippedSignatures := false
149119
err = listSignatures(ctx, sigRepo, manifestDesc, opts.maxSignatures, func(sigManifestDesc ocispec.Descriptor) error {
150120
sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc)
@@ -161,52 +131,19 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
161131
return nil
162132
}
163133

164-
envelopeContent, err := sigEnvelope.Content()
165-
if err != nil {
166-
logSkippedSignature(sigManifestDesc, err)
167-
skippedSignatures = true
168-
return nil
169-
}
170-
171-
signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload)
172-
if err != nil {
134+
if err := displayHandler.InspectSignature(sigManifestDesc, sigEnvelope); err != nil {
173135
logSkippedSignature(sigManifestDesc, err)
174136
skippedSignatures = true
175137
return nil
176138
}
177-
178-
signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm)
179-
if err != nil {
180-
logSkippedSignature(sigManifestDesc, err)
181-
skippedSignatures = true
182-
return nil
183-
}
184-
185-
sig := signatureOutput{
186-
MediaType: sigDesc.MediaType,
187-
Digest: sigManifestDesc.Digest.String(),
188-
SignatureAlgorithm: string(signatureAlgorithm),
189-
SignedAttributes: getSignedAttributes(opts.outputFormat, envelopeContent),
190-
UserDefinedAttributes: signedArtifactDesc.Annotations,
191-
UnsignedAttributes: getUnsignedAttributes(opts.outputFormat, envelopeContent),
192-
Certificates: getCertificates(opts.outputFormat, envelopeContent.SignerInfo.CertificateChain),
193-
SignedArtifact: *signedArtifactDesc,
194-
}
195-
196-
// clearing annotations from the SignedArtifact field since they're already
197-
// displayed as UserDefinedAttributes
198-
sig.SignedArtifact.Annotations = nil
199-
200-
output.Signatures = append(output.Signatures, sig)
201-
202139
return nil
203140
})
204141
var errorExceedMaxSignatures cmderr.ErrorExceedMaxSignatures
205142
if err != nil && !errors.As(err, &errorExceedMaxSignatures) {
206143
return err
207144
}
208145

209-
if err := printOutput(opts.outputFormat, resolvedRef, output); err != nil {
146+
if err := displayHandler.Render(); err != nil {
210147
return err
211148
}
212149

@@ -224,169 +161,3 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
224161
func logSkippedSignature(sigDesc ocispec.Descriptor, err error) {
225162
fmt.Fprintf(os.Stderr, "Warning: Skipping signature %s because of error: %v\n", sigDesc.Digest.String(), err)
226163
}
227-
228-
func getSignedAttributes(outputFormat string, envContent *signature.EnvelopeContent) map[string]string {
229-
signedAttributes := map[string]string{
230-
"signingScheme": string(envContent.SignerInfo.SignedAttributes.SigningScheme),
231-
"signingTime": formatTimestamp(outputFormat, envContent.SignerInfo.SignedAttributes.SigningTime),
232-
}
233-
expiry := envContent.SignerInfo.SignedAttributes.Expiry
234-
if !expiry.IsZero() {
235-
signedAttributes["expiry"] = formatTimestamp(outputFormat, expiry)
236-
}
237-
238-
for _, attribute := range envContent.SignerInfo.SignedAttributes.ExtendedAttributes {
239-
signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value)
240-
}
241-
242-
return signedAttributes
243-
}
244-
245-
func getUnsignedAttributes(outputFormat string, envContent *signature.EnvelopeContent) map[string]any {
246-
unsignedAttributes := make(map[string]any)
247-
248-
if envContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil {
249-
unsignedAttributes["timestampSignature"] = parseTimestamp(outputFormat, envContent.SignerInfo)
250-
}
251-
252-
if envContent.SignerInfo.UnsignedAttributes.SigningAgent != "" {
253-
unsignedAttributes["signingAgent"] = envContent.SignerInfo.UnsignedAttributes.SigningAgent
254-
}
255-
256-
return unsignedAttributes
257-
}
258-
259-
func formatTimestamp(outputFormat string, t time.Time) string {
260-
switch outputFormat {
261-
case cmd.OutputJSON:
262-
return t.Format(time.RFC3339)
263-
default:
264-
return t.Format(time.ANSIC)
265-
}
266-
}
267-
268-
func getCertificates(outputFormat string, certChain []*x509.Certificate) []certificateOutput {
269-
certificates := []certificateOutput{}
270-
271-
for _, cert := range certChain {
272-
h := sha256.Sum256(cert.Raw)
273-
fingerprint := strings.ToLower(hex.EncodeToString(h[:]))
274-
275-
certificate := certificateOutput{
276-
SHA256Fingerprint: fingerprint,
277-
IssuedTo: cert.Subject.String(),
278-
IssuedBy: cert.Issuer.String(),
279-
Expiry: formatTimestamp(outputFormat, cert.NotAfter),
280-
}
281-
282-
certificates = append(certificates, certificate)
283-
}
284-
285-
return certificates
286-
}
287-
288-
func printOutput(outputFormat string, ref string, output inspectOutput) error {
289-
if outputFormat == cmd.OutputJSON {
290-
return ioutil.PrintObjectAsJSON(output)
291-
}
292-
293-
if len(output.Signatures) == 0 {
294-
fmt.Printf("%s has no associated signature\n", ref)
295-
return nil
296-
}
297-
298-
fmt.Println("Inspecting all signatures for signed artifact")
299-
root := tree.New(ref)
300-
cncfSigNode := root.Add(registry.ArtifactTypeNotation)
301-
302-
for _, signature := range output.Signatures {
303-
sigNode := cncfSigNode.Add(signature.Digest)
304-
sigNode.AddPair("media type", signature.MediaType)
305-
sigNode.AddPair("signature algorithm", signature.SignatureAlgorithm)
306-
307-
signedAttributesNode := sigNode.Add("signed attributes")
308-
addMapToTree(signedAttributesNode, signature.SignedAttributes)
309-
310-
userDefinedAttributesNode := sigNode.Add("user defined attributes")
311-
addMapToTree(userDefinedAttributesNode, signature.UserDefinedAttributes)
312-
313-
unsignedAttributesNode := sigNode.Add("unsigned attributes")
314-
for k, v := range signature.UnsignedAttributes {
315-
switch value := v.(type) {
316-
case string:
317-
unsignedAttributesNode.AddPair(k, value)
318-
case timestampOutput:
319-
timestampNode := unsignedAttributesNode.Add("timestamp signature")
320-
if value.Error != "" {
321-
timestampNode.AddPair("error", value.Error)
322-
break
323-
}
324-
timestampNode.AddPair("timestamp", value.Timestamp)
325-
addCertificatesToTree(timestampNode, "certificates", value.Certificates)
326-
}
327-
}
328-
329-
addCertificatesToTree(sigNode, "certificates", signature.Certificates)
330-
331-
artifactNode := sigNode.Add("signed artifact")
332-
artifactNode.AddPair("media type", signature.SignedArtifact.MediaType)
333-
artifactNode.AddPair("digest", signature.SignedArtifact.Digest.String())
334-
artifactNode.AddPair("size", strconv.FormatInt(signature.SignedArtifact.Size, 10))
335-
}
336-
337-
root.Print()
338-
return nil
339-
}
340-
341-
func addMapToTree(node *tree.Node, m map[string]string) {
342-
if len(m) > 0 {
343-
for k, v := range m {
344-
node.AddPair(k, v)
345-
}
346-
} else {
347-
node.Add("(empty)")
348-
}
349-
}
350-
351-
func addCertificatesToTree(node *tree.Node, name string, certs []certificateOutput) {
352-
certListNode := node.Add(name)
353-
for _, cert := range certs {
354-
certNode := certListNode.AddPair("SHA256 fingerprint", cert.SHA256Fingerprint)
355-
certNode.AddPair("issued to", cert.IssuedTo)
356-
certNode.AddPair("issued by", cert.IssuedBy)
357-
certNode.AddPair("expiry", cert.Expiry)
358-
}
359-
}
360-
361-
func parseTimestamp(outputFormat string, signerInfo signature.SignerInfo) timestampOutput {
362-
signedToken, err := tspclient.ParseSignedToken(signerInfo.UnsignedAttributes.TimestampSignature)
363-
if err != nil {
364-
return timestampOutput{
365-
Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()),
366-
}
367-
}
368-
info, err := signedToken.Info()
369-
if err != nil {
370-
return timestampOutput{
371-
Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()),
372-
}
373-
}
374-
timestamp, err := info.Validate(signerInfo.Signature)
375-
if err != nil {
376-
return timestampOutput{
377-
Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()),
378-
}
379-
}
380-
certificates := getCertificates(outputFormat, signedToken.Certificates)
381-
var formatTimestamp string
382-
switch outputFormat {
383-
case cmd.OutputJSON:
384-
formatTimestamp = timestamp.Format(time.RFC3339)
385-
default:
386-
formatTimestamp = timestamp.Format(time.ANSIC)
387-
}
388-
return timestampOutput{
389-
Timestamp: formatTimestamp,
390-
Certificates: certificates,
391-
}
392-
}

0 commit comments

Comments
 (0)