Skip to content

Commit

Permalink
refactor: oci inspect command
Browse files Browse the repository at this point in the history
Signed-off-by: Junjie Gao <[email protected]>
  • Loading branch information
JeyJeyGao committed Jan 6, 2025
1 parent f9e1a73 commit 577e8c6
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 241 deletions.
3 changes: 1 addition & 2 deletions cmd/notation/blob/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ func runInspect(opts *inspectOpts) error {
case cmd.OutputJSON:
return ioutil.PrintObjectAsJSON(sig)
case cmd.OutputPlaintext:
treeNode := sig.ToTreeNode(opts.sigPath)
treeNode.Print()
sig.SignatureNode(opts.sigPath).Print()

Check warning on line 87 in cmd/notation/blob/inspect.go

View check run for this annotation

Codecov / codecov/patch

cmd/notation/blob/inspect.go#L81-L87

Added lines #L81 - L87 were not covered by tests
}
return nil

Check warning on line 89 in cmd/notation/blob/inspect.go

View check run for this annotation

Codecov / codecov/patch

cmd/notation/blob/inspect.go#L89

Added line #L89 was not covered by tests
}
Expand Down
222 changes: 8 additions & 214 deletions cmd/notation/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,17 @@
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"
cmderr "github.com/notaryproject/notation/cmd/notation/internal/errors"
"github.com/notaryproject/notation/cmd/notation/internal/experimental"
"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"
)
Expand All @@ -48,32 +39,8 @@ type inspectOpts struct {
}

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"`
MediaType string `json:"mediaType"`
Signatures []*envelope.SignatureInfo `json:"signatures"`
}

func inspectCommand(opts *inspectOpts) *cobra.Command {
Expand Down Expand Up @@ -144,7 +111,7 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
if err != nil {
return err
}
output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}}
output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []*envelope.SignatureInfo{}}
skippedSignatures := false
err = listSignatures(ctx, sigRepo, manifestDesc, opts.maxSignatures, func(sigManifestDesc ocispec.Descriptor) error {
sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc)
Expand All @@ -154,49 +121,20 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
return nil
}

sigEnvelope, err := signature.ParseEnvelope(sigDesc.MediaType, sigBlob)
if err != nil {
logSkippedSignature(sigManifestDesc, err)
skippedSignatures = true
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 {
logSkippedSignature(sigManifestDesc, err)
skippedSignatures = true
return nil
}

signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm)
sig, err := envelope.Parse(sigBlob, sigDesc.MediaType)
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

// adding digest to the signature info
sig.Digest = sigManifestDesc.Digest.String()

output.Signatures = append(output.Signatures, sig)

return nil
Expand Down Expand Up @@ -225,66 +163,6 @@ 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)
Expand All @@ -300,93 +178,9 @@ func printOutput(outputFormat string, ref string, output inspectOutput) error {
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))
cncfSigNode.Children = append(cncfSigNode.Children, signature.SignatureNode(signature.Digest))
}

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

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

Expand Down Expand Up @@ -85,22 +84,3 @@ 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)
}
}
9 changes: 4 additions & 5 deletions internal/envelope/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (
// SignatureInfo is the signature envelope with human readable fields.
type SignatureInfo struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Digest string `json:"digest,omitempty"`
SignatureAlgorithm plugin.SignatureAlgorithm `json:"signatureAlgorithm"`
SignedAttributes map[string]any `json:"signedAttributes"`
UserDefinedAttributes map[string]string `json:"userDefinedAttributes"`
Expand Down Expand Up @@ -158,15 +158,14 @@ func parseTimestamp(signerInfo signature.SignerInfo) TimestampInfo {
Error: fmt.Sprintf("failed to parse timestamp countersignature: %s", err.Error()),
}
}

Check warning on line 160 in internal/envelope/signature.go

View check run for this annotation

Codecov / codecov/patch

internal/envelope/signature.go#L157-L160

Added lines #L157 - L160 were not covered by tests
certificates := getCertificates(signedToken.Certificates)

return TimestampInfo{
Timestamp: ioutil.Timestamp(*timestamp),
Certificates: certificates,
Certificates: getCertificates(signedToken.Certificates),
}
}

func (s *SignatureInfo) ToTreeNode(sigName string) *tree.Node {
// SignatureNode returns a tree node that represents the signature.
func (s *SignatureInfo) SignatureNode(sigName string) *tree.Node {
sigNode := tree.New(sigName)
sigNode.AddPair("signature algorithm", s.SignatureAlgorithm)
sigNode.AddPair("signature envelope type", s.MediaType)
Expand Down
39 changes: 39 additions & 0 deletions internal/envelope/signature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package envelope

import (
"testing"

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

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(envContent)
val, ok := unsignedAttr["timestampSignature"].(TimestampInfo)
if !ok {
t.Fatal("expected to have timestampSignature")
}
if val.Error != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, val.Error)
}
}

0 comments on commit 577e8c6

Please sign in to comment.