Skip to content

Commit

Permalink
feat: blob verify command (#1137)
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Zheng <[email protected]>
  • Loading branch information
Two-Hearts authored Feb 14, 2025
1 parent 3657482 commit 4808e08
Show file tree
Hide file tree
Showing 46 changed files with 1,209 additions and 312 deletions.
3 changes: 1 addition & 2 deletions cmd/notation/blob/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ func Cmd() *cobra.Command {
Short: "Commands for blob",
Long: "Sign, verify, inspect signatures of blob. Configure blob trust policy.",
}

command.AddCommand(
signCommand(nil),
verifyCommand(nil),
policy.Cmd(),
)

return command
}
3 changes: 2 additions & 1 deletion cmd/notation/blob/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation/cmd/notation/internal/cmdutil"
"github.com/notaryproject/notation/cmd/notation/internal/signer"
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/internal/envelope"
"github.com/notaryproject/notation/internal/httputil"
Expand Down Expand Up @@ -138,7 +139,7 @@ func runBlobSign(command *cobra.Command, cmdOpts *blobSignOpts) error {
ctx := cmdOpts.LoggingFlagOpts.InitializeLogger(command.Context())
logger := log.GetLogger(ctx)

blobSigner, err := cmd.GetSigner(ctx, &cmdOpts.SignerFlagOpts)
blobSigner, err := signer.GetSigner(ctx, &cmdOpts.SignerFlagOpts)
if err != nil {
return err
}
Expand Down
170 changes: 170 additions & 0 deletions cmd/notation/blob/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// 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 blob

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation/cmd/notation/internal/display"
"github.com/notaryproject/notation/cmd/notation/internal/option"
"github.com/notaryproject/notation/cmd/notation/internal/verifier"
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/internal/envelope"
"github.com/notaryproject/notation/internal/ioutil"
"github.com/spf13/cobra"
)

type blobVerifyOpts struct {
cmd.LoggingFlagOpts
option.Common
blobPath string
signaturePath string
pluginConfig []string
userMetadata []string
policyStatementName string
blobMediaType string
}

func verifyCommand(opts *blobVerifyOpts) *cobra.Command {
if opts == nil {
opts = &blobVerifyOpts{}
}
longMessage := `Verify a signature associated with a blob.
Prerequisite: added a certificate into trust store and created a trust policy.
Example - Verify a signature on a blob artifact:
notation blob verify --signature <signature_path> <blob_path>
Example - Verify the signature on a blob artifact with user metadata:
notation blob verify --user-metadata <metadata> --signature <signature_path> <blob_path>
Example - Verify the signature on a blob artifact with media type:
notation blob verify --media-type <media_type> --signature <signature_path> <blob_path>
Example - Verify the signature on a blob artifact using a policy statement name:
notation blob verify --policy-name <policy_name> --signature <signature_path> <blob_path>
`
command := &cobra.Command{
Use: "verify [flags] --signature <signature_path> <blob_path>",
Short: "Verify a signature associated with a blob",
Long: longMessage,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("missing path to the blob artifact: use `notation blob verify --help` to see what parameters are required")
}
opts.blobPath = args[0]
return nil
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.signaturePath == "" {
return errors.New("filepath of the signature cannot be empty")
}
if cmd.Flags().Changed("media-type") && opts.blobMediaType == "" {
return errors.New("--media-type is set but with empty value")
}
opts.Common.Parse(cmd)
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runVerify(cmd, opts)
},
}
opts.LoggingFlagOpts.ApplyFlags(command.Flags())
command.Flags().StringVar(&opts.signaturePath, "signature", "", "filepath of the signature to be verified")
command.Flags().StringArrayVar(&opts.pluginConfig, "plugin-config", nil, "{key}={value} pairs that are passed as it is to a plugin, if the verification is associated with a verification plugin, refer plugin documentation to set appropriate values")
command.Flags().StringVar(&opts.blobMediaType, "media-type", "", "media type of the blob to verify")
command.Flags().StringVar(&opts.policyStatementName, "policy-name", "", "policy name to verify against. If not provided, the global policy is used if exists")
cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataVerifyUsage)
command.MarkFlagRequired("signature")
return command
}

func runVerify(command *cobra.Command, cmdOpts *blobVerifyOpts) error {
// set log level
ctx := cmdOpts.LoggingFlagOpts.InitializeLogger(command.Context())

// initialize
displayHandler := display.NewBlobVerifyHandler(cmdOpts.Printer)
blobFile, err := os.Open(cmdOpts.blobPath)
if err != nil {
return err
}
defer blobFile.Close()

signatureBytes, err := os.ReadFile(cmdOpts.signaturePath)
if err != nil {
return err
}
blobVerifier, err := verifier.GetBlobVerifier(ctx)
if err != nil {
return err
}

// set up verification plugin config
pluginConfigs, err := cmd.ParseFlagMap(cmdOpts.pluginConfig, cmd.PflagPluginConfig.Name)
if err != nil {
return err
}

// set up user metadata
userMetadata, err := cmd.ParseFlagMap(cmdOpts.userMetadata, cmd.PflagUserMetadata.Name)
if err != nil {
return err
}
signatureMediaType, err := parseSignatureMediaType(cmdOpts.signaturePath)
if err != nil {
return err
}
verifyBlobOpts := notation.VerifyBlobOptions{
BlobVerifierVerifyOptions: notation.BlobVerifierVerifyOptions{
SignatureMediaType: signatureMediaType,
PluginConfig: pluginConfigs,
UserMetadata: userMetadata,
TrustPolicyName: cmdOpts.policyStatementName,
},
ContentMediaType: cmdOpts.blobMediaType,
}
_, outcome, err := notation.VerifyBlob(ctx, blobVerifier, blobFile, signatureBytes, verifyBlobOpts)
outcomes := []*notation.VerificationOutcome{outcome}
err = ioutil.ComposeBlobVerificationFailurePrintout(outcomes, cmdOpts.blobPath, err)
if err != nil {
return err
}
displayHandler.OnVerifySucceeded(outcomes, cmdOpts.blobPath)
return displayHandler.Render()
}

// parseSignatureMediaType returns the media type of the signature file.
// `application/jose+json` and `application/cose` are supported.
func parseSignatureMediaType(signaturePath string) (string, error) {
signatureFileName := filepath.Base(signaturePath)
if strings.ToLower(filepath.Ext(signatureFileName)) != ".sig" {
return "", fmt.Errorf("invalid signature filename %s. The file extension must be .sig", signatureFileName)
}
sigFilenameArr := strings.Split(signatureFileName, ".")

// a valid signature file name has at least 3 parts.
// for example, `myFile.jws.sig`
if len(sigFilenameArr) < 3 {
return "", fmt.Errorf("invalid signature filename %s. A valid signature file name must contain signature format and .sig file extension", signatureFileName)
}
sigFormat := sigFilenameArr[len(sigFilenameArr)-2]
return envelope.GetEnvelopeMediaType(strings.ToLower(sigFormat))
}
73 changes: 73 additions & 0 deletions cmd/notation/blob/verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 blob

import (
"reflect"
"testing"
)

func TestVerifyCommand_BasicArgs(t *testing.T) {
opts := &blobVerifyOpts{}
command := verifyCommand(opts)
expected := &blobVerifyOpts{
blobPath: "blob_path",
signaturePath: "sig_path",
}
if err := command.ParseFlags([]string{
expected.blobPath,
"--signature", expected.signaturePath}); 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 !reflect.DeepEqual(*expected, *opts) {
t.Fatalf("Expect blob verify opts: %v, got: %v", expected, opts)
}
}

func TestVerifyCommand_MoreArgs(t *testing.T) {
opts := &blobVerifyOpts{}
command := verifyCommand(opts)
expected := &blobVerifyOpts{
blobPath: "blob_path",
signaturePath: "sig_path",
pluginConfig: []string{"key1=val1", "key2=val2"},
}
if err := command.ParseFlags([]string{
expected.blobPath,
"--signature", expected.signaturePath,
"--plugin-config", "key1=val1",
"--plugin-config", "key2=val2",
}); 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 !reflect.DeepEqual(*expected, *opts) {
t.Fatalf("Expect verify opts: %v, got: %v", expected, opts)
}
}

func TestVerifyCommand_MissingArgs(t *testing.T) {
cmd := verifyCommand(nil)
if err := cmd.ParseFlags(nil); err != nil {
t.Fatalf("Parse Flag failed: %v", err)
}
if err := cmd.Args(cmd, cmd.Flags().Args()); err == nil {
t.Fatal("Parse Args expected error, but ok")
}
}
8 changes: 7 additions & 1 deletion cmd/notation/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ func NewInpsectHandler(printer *output.Printer, format option.Format) (metadata.
}

// NewVerifyHandler creates a new metadata VerifyHandler for printing
// veriifcation result and warnings.
// verification result and warnings.
func NewVerifyHandler(printer *output.Printer) metadata.VerifyHandler {
return text.NewVerifyHandler(printer)
}

// NewBlobVerifyHandler creates a new metadata BlobVerifyHandler for printing
// blob verification result and warnings.
func NewBlobVerifyHandler(printer *output.Printer) metadata.BlobVerifyHandler {
return text.NewBlobVerifyHandler(printer)
}
13 changes: 13 additions & 0 deletions cmd/notation/internal/display/metadata/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,16 @@ type VerifyHandler interface {
// outcomes must not be nil or empty.
OnVerifySucceeded(outcomes []*notation.VerificationOutcome, digestReference string)
}

// BlobVerifyHandler is a handler for rendering metadata information of
// blob verification outcome.
//
// It only supports text format for now.
type BlobVerifyHandler interface {
Renderer

// OnVerifySucceeded sets the successful verification result for the handler.
//
// outcomes must not be nil or empty.
OnVerifySucceeded(outcomes []*notation.VerificationOutcome, blobPath string)
}
48 changes: 48 additions & 0 deletions cmd/notation/internal/display/metadata/text/blobverify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 text

import (
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
)

// BlobVerifyHandler is a handler for rendering output for blob verify command
// in human-readable format.
// It implements metadata/BlobVerifyHandler.
type BlobVerifyHandler struct {
printer *output.Printer
outcome *notation.VerificationOutcome
blobPath string
}

// NewBlobVerifyHandler creates a new BlobVerifyHandler.
func NewBlobVerifyHandler(printer *output.Printer) *BlobVerifyHandler {
return &BlobVerifyHandler{
printer: printer,
}
}

// OnVerifySucceeded sets the successful verification result for the handler.
//
// outcomes must not be nil or empty.
func (h *BlobVerifyHandler) OnVerifySucceeded(outcomes []*notation.VerificationOutcome, blobPath string) {
h.outcome = outcomes[0]
h.blobPath = blobPath
}

// Render prints out the verification results in human-readable format.
func (h *BlobVerifyHandler) Render() error {
return printVerificationSuccess(h.printer, h.outcome, h.blobPath, false)
}
Loading

0 comments on commit 4808e08

Please sign in to comment.