forked from notaryproject/notation
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add implementation for
notation inspect
(notaryproject#528)
Adds support for `notation inspect` ([spechere](https://github.com/notaryproject/notation/blob/main/specs/commandline/inspect.md)) Example output: ``` chienb@a07817b52895 notation % ./bin/notation inspect $IMAGE localhost:5000/net-monitor@sha256:5a07385af4e6b6af81b0ebfd435aedccdfa3507f0609c658209e1aba57159b2b └── application/vnd.cncf.notary.signature └── sha256:34e5843a1a8b1607d2ba7da9b61c6a5b3953f5680751c160fb944df87b01b2b2 ├── signature algorithm : RSASSA-PSS-SHA-256 ├── signed attributes │ ├── expiry : 0001-01-01 00:00:00 +0000 UTC │ ├── signingScheme : notary.x509 │ └── signingTime : 2023-01-27 17:02:22 -0800 PST ├── user defined attributes │ └── io.wabbit-networks.buildId : 123 ├── unsigned attributes │ └── signingAgent : Notation/1.0.0 ├── certificates │ └── SHA1 fingerprint e1ef7b0f984d1f8222d6bf297e1ad10047997b54 │ ├── issued to : CN=byron.test,O=Notary,L=Seattle,ST=WA,C=US │ ├── issued by : CN=byron.test,O=Notary,L=Seattle,ST=WA,C=US │ └── expiry : 2023-01-29 01:02:13 +0000 UTC └── signed artifact ├── media type : application/vnd.docker.distribution.manifest.v2+json ├── digest : sha256:5a07385af4e6b6af81b0ebfd435aedccdfa3507f0609c658209e1aba57159b2b └── size : 942 ``` Signed-off-by: Byron Chien <[email protected]>
- Loading branch information
1 parent
85247a2
commit d62fd58
Showing
9 changed files
with
587 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
package main | ||
|
||
import ( | ||
"crypto/sha1" | ||
b64 "encoding/base64" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"strings" | ||
"strconv" | ||
"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/internal/cmd" | ||
"github.com/notaryproject/notation/internal/envelope" | ||
"github.com/notaryproject/notation/internal/ioutil" | ||
"github.com/notaryproject/notation/internal/tree" | ||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type inspectOpts struct { | ||
cmd.LoggingFlagOpts | ||
SecureFlagOpts | ||
reference string | ||
outputFormat string | ||
} | ||
|
||
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]string `json:"unsignedAttributes"` | ||
Certificates []certificateOutput `json:"certificates"` | ||
SignedArtifact ocispec.Descriptor `json:"signedArtifact"` | ||
} | ||
|
||
type certificateOutput struct { | ||
SHA1Fingerprint string `json:"SHA1Fingerprint"` | ||
IssuedTo string `json:"issuedTo"` | ||
IssuedBy string `json:"issuedBy"` | ||
Expiry string `json:"expiry"` | ||
} | ||
|
||
func inspectCommand(opts *inspectOpts) *cobra.Command { | ||
if opts == nil { | ||
opts = &inspectOpts{} | ||
} | ||
command := &cobra.Command{ | ||
Use: "inspect [reference]", | ||
Short: "Inspect all signatures associated with the signed artifact", | ||
Long: `Inspect all signatures associated with the signed artifact. | ||
Example - Inspect signatures on an OCI artifact identified by a digest: | ||
notation inspect <registry>/<repository>@<digest> | ||
Example - Inspect signatures on an OCI artifact identified by a tag (Notation will resolve tag to digest): | ||
notation inspect <registry>/<repository>:<tag> | ||
Example - Inspect signatures on an OCI artifact identified by a digest and output as json: | ||
notation inspect --output json <registry>/<repository>@<digest> | ||
`, | ||
Args: func(cmd *cobra.Command, args []string) error { | ||
if len(args) == 0 { | ||
return errors.New("missing reference") | ||
} | ||
opts.reference = args[0] | ||
return nil | ||
}, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return runInspect(cmd, opts) | ||
}, | ||
} | ||
|
||
opts.LoggingFlagOpts.ApplyFlags(command.Flags()) | ||
opts.SecureFlagOpts.ApplyFlags(command.Flags()) | ||
cmd.SetPflagOutput(command.Flags(), &opts.outputFormat, cmd.PflagOutputUsage) | ||
return command | ||
} | ||
|
||
func runInspect(command *cobra.Command, opts *inspectOpts) error { | ||
// set log level | ||
ctx := opts.LoggingFlagOpts.SetLoggerLevel(command.Context()) | ||
|
||
if opts.outputFormat != cmd.OutputJSON && opts.outputFormat != cmd.OutputPlaintext { | ||
return fmt.Errorf("unrecognized output format %s", opts.outputFormat) | ||
} | ||
|
||
// initialize | ||
reference := opts.reference | ||
sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// reference is a digest reference | ||
if err := ref.ValidateReferenceAsDigest(); err != nil { | ||
fmt.Fprintf(os.Stderr, "Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref.Reference) | ||
ref.Reference = manifestDesc.Digest.String() | ||
} | ||
|
||
output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}} | ||
skippedSignatures := false | ||
err = sigRepo.ListSignatures(ctx, manifestDesc, func(signatureManifests []ocispec.Descriptor) error { | ||
for _, sigManifestDesc := range signatureManifests { | ||
sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "Warning: unable to fetch signature %s due to error: %v\n", sigManifestDesc.Digest.String(), err) | ||
skippedSignatures = true | ||
continue | ||
} | ||
|
||
sigEnvelope, err := signature.ParseEnvelope(sigDesc.MediaType, sigBlob) | ||
if err != nil { | ||
logSkippedSignature(sigManifestDesc, err) | ||
skippedSignatures = true | ||
continue | ||
} | ||
|
||
envelopeContent, err := sigEnvelope.Content() | ||
if err != nil { | ||
logSkippedSignature(sigManifestDesc, err) | ||
skippedSignatures = true | ||
continue | ||
} | ||
|
||
signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload) | ||
if err != nil { | ||
logSkippedSignature(sigManifestDesc, err) | ||
skippedSignatures = true | ||
continue | ||
} | ||
|
||
signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm) | ||
if err != nil { | ||
logSkippedSignature(sigManifestDesc, err) | ||
skippedSignatures = true | ||
continue | ||
} | ||
|
||
sig := signatureOutput{ | ||
MediaType: sigDesc.MediaType, | ||
Digest: sigManifestDesc.Digest.String(), | ||
SignatureAlgorithm: string(signatureAlgorithm), | ||
SignedAttributes: getSignedAttributes(opts.outputFormat, envelopeContent), | ||
UserDefinedAttributes: signedArtifactDesc.Annotations, | ||
UnsignedAttributes: getUnsignedAttributes(envelopeContent), | ||
Certificates: getCertificates(opts.outputFormat, envelopeContent), | ||
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 | ||
}) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
err = printOutput(opts.outputFormat, ref.String(), output) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if skippedSignatures { | ||
return errors.New("at least one signature was skipped and not displayed") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
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": formatTimestamp(outputFormat, envContent.SignerInfo.SignedAttributes.Expiry), | ||
} | ||
|
||
for _, attribute := range envContent.SignerInfo.SignedAttributes.ExtendedAttributes { | ||
signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value) | ||
} | ||
|
||
return signedAttributes | ||
} | ||
|
||
func getUnsignedAttributes(envContent *signature.EnvelopeContent) map[string]string { | ||
unsignedAttributes := map[string]string{} | ||
|
||
if envContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil { | ||
unsignedAttributes["timestampSignature"] = b64.StdEncoding.EncodeToString(envContent.SignerInfo.UnsignedAttributes.TimestampSignature) | ||
} | ||
|
||
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, envContent *signature.EnvelopeContent) []certificateOutput { | ||
certificates := []certificateOutput{} | ||
|
||
for _, cert := range envContent.SignerInfo.CertificateChain { | ||
h := sha1.Sum(cert.Raw) | ||
fingerprint := strings.ToLower(hex.EncodeToString(h[:])) | ||
|
||
certificate := certificateOutput{ | ||
SHA1Fingerprint: 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) | ||
} | ||
|
||
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") | ||
addMapToTree(unsignedAttributesNode, signature.UnsignedAttributes) | ||
|
||
certListNode := sigNode.Add("certificates") | ||
for _, cert := range signature.Certificates { | ||
certNode := certListNode.AddPair("SHA1 fingerprint", cert.SHA1Fingerprint) | ||
certNode.AddPair("issued to", cert.IssuedTo) | ||
certNode.AddPair("issued by", cert.IssuedBy) | ||
certNode.AddPair("expiry", cert.Expiry) | ||
} | ||
|
||
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)") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/notaryproject/notation/internal/cmd" | ||
) | ||
|
||
func TestInspectCommand_SecretsFromArgs(t *testing.T) { | ||
opts := &inspectOpts{} | ||
command := inspectCommand(opts) | ||
expected := &inspectOpts{ | ||
reference: "ref", | ||
SecureFlagOpts: SecureFlagOpts{ | ||
Password: "password", | ||
PlainHTTP: true, | ||
Username: "user", | ||
}, | ||
outputFormat: cmd.OutputPlaintext, | ||
} | ||
if err := command.ParseFlags([]string{ | ||
"--password", expected.Password, | ||
expected.reference, | ||
"-u", expected.Username, | ||
"--plain-http", | ||
"--output", "text"}); err != nil { | ||
t.Fatalf("Parse Flag failed: %v", err) | ||
} | ||
if err := command.Args(command, command.Flags().Args()); err != nil { | ||
t.Fatalf("Parse Args failed: %v", err) | ||
} | ||
if *opts != *expected { | ||
t.Fatalf("Expect inspect opts: %v, got: %v", expected, opts) | ||
} | ||
} | ||
|
||
func TestInspectCommand_SecretsFromEnv(t *testing.T) { | ||
t.Setenv(defaultUsernameEnv, "user") | ||
t.Setenv(defaultPasswordEnv, "password") | ||
opts := &inspectOpts{} | ||
expected := &inspectOpts{ | ||
reference: "ref", | ||
SecureFlagOpts: SecureFlagOpts{ | ||
Password: "password", | ||
Username: "user", | ||
}, | ||
outputFormat: cmd.OutputJSON, | ||
} | ||
command := inspectCommand(opts) | ||
if err := command.ParseFlags([]string{ | ||
expected.reference, | ||
"--output", "json"}); err != nil { | ||
t.Fatalf("Parse Flag failed: %v", err) | ||
} | ||
if err := command.Args(command, command.Flags().Args()); err != nil { | ||
t.Fatalf("Parse Args failed: %v", err) | ||
} | ||
if *opts != *expected { | ||
t.Fatalf("Expect inspect opts: %v, got: %v", expected, opts) | ||
} | ||
} | ||
|
||
func TestInspectCommand_MissingArgs(t *testing.T) { | ||
command := inspectCommand(nil) | ||
if err := command.ParseFlags(nil); err != nil { | ||
t.Fatalf("Parse Flag failed: %v", err) | ||
} | ||
if err := command.Args(command, command.Flags().Args()); err == nil { | ||
t.Fatal("Parse Args expected error, but ok") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.