Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract inspect rendering logic to be display handlers #1150

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
89adf15
refactor: add inpect display handler
JeyJeyGao Jan 16, 2025
f0756a8
fix: update code
JeyJeyGao Jan 16, 2025
578dea2
test: add unit test
JeyJeyGao Jan 17, 2025
24dea8b
test: add e2e test for tree and json
JeyJeyGao Jan 17, 2025
4031faf
refactor code
JeyJeyGao Jan 17, 2025
37b967a
fix: update E2E
JeyJeyGao Jan 20, 2025
c2697db
fix: resolve comments
JeyJeyGao Jan 20, 2025
edbfbad
fix: update license header
JeyJeyGao Jan 20, 2025
eb078e3
fix: update test
JeyJeyGao Jan 20, 2025
4d3aedd
test: add unit test
JeyJeyGao Jan 20, 2025
8fdc989
test: add unit test for inspect
JeyJeyGao Jan 20, 2025
0640165
test: update test
JeyJeyGao Jan 20, 2025
a1e411f
fix: resolve comemnts
JeyJeyGao Jan 21, 2025
84a1991
fix: test
JeyJeyGao Jan 21, 2025
8083c25
fix: remove envelope media type
JeyJeyGao Jan 21, 2025
5dd55ca
fix: e2e test
JeyJeyGao Jan 21, 2025
25bc6a0
fix: e2e test
JeyJeyGao Jan 21, 2025
fd74cf7
fix: update json output format
JeyJeyGao Jan 21, 2025
f1c2966
fix: update e2e
JeyJeyGao Jan 21, 2025
89b310f
fix: add content type in json output
JeyJeyGao Jan 21, 2025
58603ce
test: add e2e test for invalid timestamp signature
JeyJeyGao Jan 21, 2025
b3a97e5
fix: update e2e test
JeyJeyGao Jan 21, 2025
b108fcd
fix: unit test
JeyJeyGao Jan 21, 2025
721518a
fix: resolve comments
JeyJeyGao Jan 22, 2025
0997875
fix: resolve comments
JeyJeyGao Jan 23, 2025
aef5735
refactor: display package structure
JeyJeyGao Jan 23, 2025
5b28eda
fix: update to use *ocispec.Descriptor
JeyJeyGao Jan 23, 2025
8f5d88f
fix: restore package structure
JeyJeyGao Jan 23, 2025
1adc392
fix: restore "NewInpsectHandler" function name
JeyJeyGao Jan 23, 2025
da16190
fix: move internal/tree to tree handlers package
JeyJeyGao Jan 23, 2025
7cf59d4
fix: resolve comments
JeyJeyGao Feb 6, 2025
60035a7
fix: udpate new to newNode
JeyJeyGao Feb 7, 2025
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
265 changes: 18 additions & 247 deletions cmd/notation/inspect.go
JeyJeyGao marked this conversation as resolved.
Show resolved Hide resolved
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)
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
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
Loading