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

feat: blob verify command #1137

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1619d18
blob signing
Two-Hearts Dec 24, 2024
5870423
blob signing
Two-Hearts Dec 25, 2024
c6da7dc
update
Two-Hearts Dec 26, 2024
c13744e
Merge branch 'notaryproject:main' into blobsign
Two-Hearts Dec 26, 2024
97c3fcd
add tests
Two-Hearts Dec 26, 2024
f0db92f
fix test
Two-Hearts Dec 26, 2024
9cf65b8
fix test
Two-Hearts Dec 26, 2024
e8fe8c6
fix e2e test
Two-Hearts Dec 26, 2024
cb997c7
add e2e tests
Two-Hearts Dec 26, 2024
07d0ffb
add e2e tests
Two-Hearts Dec 26, 2024
cf40ba2
fix e2e tests
Two-Hearts Dec 26, 2024
1051ad4
fix e2e test
Two-Hearts Dec 26, 2024
cf5fecb
add more e2e tests
Two-Hearts Dec 27, 2024
fc4dcfc
add more e2e tests
Two-Hearts Dec 27, 2024
576a286
fix e2e test
Two-Hearts Dec 27, 2024
0c984a0
add more tests
Two-Hearts Dec 27, 2024
8474d58
Merge branch 'notaryproject:main' into blobsign
Two-Hearts Dec 31, 2024
14a1f3e
update
Two-Hearts Dec 31, 2024
752afe6
fix e2e test
Two-Hearts Dec 31, 2024
c30e199
resolve conflicts
Two-Hearts Jan 6, 2025
bc8f710
update
Two-Hearts Jan 6, 2025
9ce5ac9
initial commit
Two-Hearts Jan 6, 2025
9e84a3f
update
Two-Hearts Jan 7, 2025
2659ff7
fix tests
Two-Hearts Jan 7, 2025
a1d8562
update
Two-Hearts Jan 9, 2025
d6eab1b
fix test
Two-Hearts Jan 9, 2025
94b586a
fix tests
Two-Hearts Jan 9, 2025
ce63a53
fix tests
Two-Hearts Jan 9, 2025
3602591
update
Two-Hearts Jan 10, 2025
eeaf02c
added e2e tests
Two-Hearts Jan 10, 2025
8cc2aeb
fix e2e tests
Two-Hearts Jan 10, 2025
32b009b
fix e2e tests
Two-Hearts Jan 10, 2025
54ecf6e
fix e2e tests
Two-Hearts Jan 10, 2025
fa50e18
add more e2e tests
Two-Hearts Jan 10, 2025
07c66b4
add more e2e tests
Two-Hearts Jan 10, 2025
4c07098
added more e2e tests
Two-Hearts Jan 10, 2025
2bbf48c
resolve conflicts
Two-Hearts Jan 14, 2025
1294938
update
Two-Hearts Jan 14, 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
3 changes: 1 addition & 2 deletions cmd/notation/blob/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ 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),
)

return command
}
162 changes: 162 additions & 0 deletions cmd/notation/blob/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/internal/ioutil"
"github.com/spf13/cobra"
)

type blobVerifyOpts struct {
cmd.LoggingFlagOpts
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")
}

Check warning on line 78 in cmd/notation/blob/verify.go

View check run for this annotation

Codecov / codecov/patch

cmd/notation/blob/verify.go#L77-L78

Added lines #L77 - L78 were not covered by tests
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
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 := cmd.GetVerifier(ctx, true)
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.PrintVerificationFailure(outcomes, cmdOpts.blobPath, err, true)
if err != nil {
return err
}
ioutil.PrintVerificationSuccess(outcomes, cmdOpts.blobPath)
return nil
}

// 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)
format := strings.Split(signatureFileName, ".")[1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line may panic.

switch format {
case "cose":
return cose.MediaTypeEnvelope, nil
case "jws":
return jws.MediaTypeEnvelope, nil
}
return "", fmt.Errorf("unsupported signature format %s", format)
}
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")
}
}
106 changes: 4 additions & 102 deletions cmd/notation/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,15 @@
package main

import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"reflect"

"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
"github.com/notaryproject/notation/cmd/notation/internal/experimental"
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/internal/ioutil"
"github.com/spf13/cobra"

clirev "github.com/notaryproject/notation/internal/revocation"
)

type verifyOpts struct {
Expand Down Expand Up @@ -117,12 +106,12 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context())

// initialize
sigVerifier, err := getVerifier(ctx)
sigVerifier, err := cmd.GetVerifier(ctx, false)
if err != nil {
return err
}

// set up verification plugin config.
// set up verification plugin config
configs, err := cmd.ParseFlagMap(opts.pluginConfig, cmd.PflagPluginConfig.Name)
if err != nil {
return err
Expand Down Expand Up @@ -155,97 +144,10 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
UserMetadata: userMetadata,
}
_, outcomes, err := notation.Verify(ctx, sigVerifier, sigRepo, verifyOpts)
err = checkVerificationFailure(outcomes, resolvedRef, err)
err = ioutil.PrintVerificationFailure(outcomes, resolvedRef, err, false)
if err != nil {
return err
}
reportVerificationSuccess(outcomes, resolvedRef)
ioutil.PrintVerificationSuccess(outcomes, resolvedRef)
return nil
}

func checkVerificationFailure(outcomes []*notation.VerificationOutcome, printOut string, err error) error {
// write out on failure
if err != nil || len(outcomes) == 0 {
if err != nil {
var errTrustStore truststore.TrustStoreError
if errors.As(err, &errTrustStore) {
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("%w. Use command 'notation cert add' to create and add trusted certificates to the trust store", errTrustStore)
} else {
return fmt.Errorf("%w. %w", errTrustStore, errTrustStore.InnerError)
}
}

var errCertificate truststore.CertificateError
if errors.As(err, &errCertificate) {
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("%w. Use command 'notation cert add' to create and add trusted certificates to the trust store", errCertificate)
} else {
return fmt.Errorf("%w. %w", errCertificate, errCertificate.InnerError)
}
}

var errorVerificationFailed notation.ErrorVerificationFailed
if !errors.As(err, &errorVerificationFailed) {
return fmt.Errorf("signature verification failed: %w", err)
}
}
return fmt.Errorf("signature verification failed for all the signatures associated with %s", printOut)
}
return nil
}

func reportVerificationSuccess(outcomes []*notation.VerificationOutcome, printout string) {
// write out on success
outcome := outcomes[0]
// print out warning for any failed result with logged verification action
for _, result := range outcome.VerificationResults {
if result.Error != nil {
// at this point, the verification action has to be logged and
// it's failed
fmt.Fprintf(os.Stderr, "Warning: %v was set to %q and failed with error: %v\n", result.Type, result.Action, result.Error)
}
}
if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) {
fmt.Println("Trust policy is configured to skip signature verification for", printout)
} else {
fmt.Println("Successfully verified signature for", printout)
printMetadataIfPresent(outcome)
}
}

func printMetadataIfPresent(outcome *notation.VerificationOutcome) {
// the signature envelope is parsed as part of verification.
// since user metadata is only printed on successful verification,
// this error can be ignored
metadata, _ := outcome.UserMetadata()

if len(metadata) > 0 {
fmt.Println("\nThe artifact was signed with the following user metadata.")
ioutil.PrintMetadataMap(os.Stdout, metadata)
}
}

func getVerifier(ctx context.Context) (notation.Verifier, error) {
// revocation check
revocationCodeSigningValidator, err := clirev.NewRevocationValidator(ctx, purpose.CodeSigning)
if err != nil {
return nil, err
}
revocationTimestampingValidator, err := clirev.NewRevocationValidator(ctx, purpose.Timestamping)
if err != nil {
return nil, err
}

// trust policy and trust store
policyDocument, err := trustpolicy.LoadOCIDocument()
if err != nil {
return nil, err
}
x509TrustStore := truststore.NewX509TrustStore(dir.ConfigFS())

return verifier.NewVerifierWithOptions(policyDocument, nil, x509TrustStore, plugin.NewCLIManager(dir.PluginFS()), verifier.VerifierOptions{
RevocationCodeSigningValidator: revocationCodeSigningValidator,
RevocationTimestampingValidator: revocationTimestampingValidator,
})
}
Loading
Loading