Skip to content

Commit

Permalink
refactor: extract inspect rendering logic to be display handlers (#1150)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
JeyJeyGao authored Feb 7, 2025
1 parent ac77b58 commit 80cb6ee
Show file tree
Hide file tree
Showing 33 changed files with 1,403 additions and 330 deletions.
265 changes: 18 additions & 247 deletions cmd/notation/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,68 +14,30 @@
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"
)

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{}
Expand Down Expand Up @@ -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 {
Expand All @@ -118,18 +84,21 @@ 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
}

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
Expand All @@ -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)
Expand All @@ -161,52 +131,19 @@ 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
if err != nil && !errors.As(err, &errorExceedMaxSignatures) {
return err
}

if err := printOutput(opts.outputFormat, resolvedRef, output); err != nil {
if err := displayHandler.Render(); err != nil {
return err
}

Expand All @@ -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,
}
}
Loading

0 comments on commit 80cb6ee

Please sign in to comment.