Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
145 changes: 34 additions & 111 deletions cmd/notation/verify.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
package main

import (
"context"
"errors"
"fmt"
"os"
"strings"

"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/signature"
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/internal/envelope"
"github.com/notaryproject/notation/internal/slices"
"github.com/notaryproject/notation/pkg/cache"
"github.com/notaryproject/notation/pkg/configutil"
"github.com/opencontainers/go-digest"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verification"
"github.com/notaryproject/notation/internal/ioutil"

orasregistry "oras.land/oras-go/v2/registry"

"github.com/spf13/cobra"
)

type verifyOpts struct {
RemoteFlagOpts
signatures []string
certs []string
certFiles []string
pull bool
reference string
SecureFlagOpts
reference string
config []string
}

func verifyCommand(opts *verifyOpts) *cobra.Command {
if opts == nil {
opts = &verifyOpts{}
}
command := &cobra.Command{
Use: "verify [reference]",
Use: "verify <reference>",
Short: "Verifies OCI Artifacts",
Long: `Verifies OCI Artifacts:
notation verify [--config <key>=<value>] [--username <username>] [--password <password>] <reference>`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("missing reference")
Expand All @@ -46,120 +41,48 @@ func verifyCommand(opts *verifyOpts) *cobra.Command {
return runVerify(cmd, opts)
},
}
setFlagSignature(command.Flags(), &opts.signatures)
command.Flags().StringSliceVarP(&opts.certs, "cert", "c", []string{}, "certificate names for verification")
command.Flags().StringSliceVar(&opts.certFiles, cmd.PflagCertFile.Name, []string{}, "certificate files for verification")
command.Flags().BoolVar(&opts.pull, "pull", true, "pull remote signatures before verification")
opts.ApplyFlags(command.Flags())
command.Flags().StringSliceVar(&opts.config, "config", nil, "verification plugin config (accepts multiple inputs)")
return command
}

func runVerify(command *cobra.Command, opts *verifyOpts) error {
// initialize
// initialize.
verifier, err := getVerifier(opts)
if err != nil {
return err
}
manifestDesc, err := getManifestDescriptorFromContext(command.Context(), &opts.RemoteFlagOpts, opts.reference)
if err != nil {
return err
}

sigPaths := opts.signatures
if len(sigPaths) == 0 {
if !opts.Local && opts.pull {
if err := pullSignatures(command, opts.reference, &opts.SecureFlagOpts, digest.Digest(manifestDesc.Digest)); err != nil {
return err
}
}
manifestDigest := digest.Digest(manifestDesc.Digest)
sigDigests, err := cache.SignatureDigests(manifestDigest)
if err != nil {
return err
}
for _, sigDigest := range sigDigests {
sigPaths = append(sigPaths, dir.Path.CachedSignature(manifestDigest, sigDigest))
// set up verification plugin config.
configs := make(map[string]string)
for _, c := range opts.config {
parts := strings.Split(c, "=")
if len(parts) != 2 {
return fmt.Errorf("invalid config option: %s", c)
}
configs[parts[0]] = parts[1]
}
ctx := verification.WithPluginConfig(command.Context(), configs)

// core process
if err := verifySignatures(command.Context(), verifier, manifestDesc, sigPaths); err != nil {
return err
}
// core verify process.
outcomes, err := verifier.Verify(ctx, opts.reference)

// write out
fmt.Println(manifestDesc.Digest)
return nil
// write out.
return ioutil.PrintVerificationResults(os.Stdout, outcomes, err)
}

func verifySignatures(ctx context.Context, verifier notation.Verifier, manifestDesc notation.Descriptor, sigPaths []string) error {
if len(sigPaths) == 0 {
return errors.New("verification failure: no signatures found")
}

var lastErr error
for _, path := range sigPaths {
sig, err := os.ReadFile(path)
if err != nil {
lastErr = fmt.Errorf("verification failure: %v", err)
continue
}
// pass in nonempty annotations if needed
// TODO: understand media type in a better way
sigMediaType, err := envelope.SpeculateSignatureEnvelopeFormat(sig)
if err != nil {
lastErr = fmt.Errorf("verification failure: %v", err)
continue
}
opts := notation.VerifyOptions{
SignatureMediaType: sigMediaType,
}
desc, err := verifier.Verify(ctx, sig, opts)
if err != nil {
lastErr = fmt.Errorf("verification failure: %v", err)
continue
}

if !desc.Equal(manifestDesc) {
lastErr = fmt.Errorf("verification failure: %s", manifestDesc.Digest)
continue
}
return nil
func getVerifier(opts *verifyOpts) (*verification.Verifier, error) {
ref, err := orasregistry.ParseReference(opts.reference)
if err != nil {
return nil, err
}
return lastErr
}

func getVerifier(opts *verifyOpts) (notation.Verifier, error) {
certPaths, err := appendCertPathFromName(opts.certFiles, opts.certs)
authClient, plainHTTP, err := getAuthClient(&opts.SecureFlagOpts, ref)
if err != nil {
return nil, err
}
if len(certPaths) == 0 {
cfg, err := configutil.LoadConfigOnce()
if err != nil {
return nil, err
}
if len(cfg.VerificationCertificates.Certificates) == 0 {
return nil, errors.New("trust certificate not specified")
}
for _, ref := range cfg.VerificationCertificates.Certificates {
certPaths = append(certPaths, ref.Path)
}
}
return signature.NewVerifierFromFiles(certPaths)
}

func appendCertPathFromName(paths, names []string) ([]string, error) {
for _, name := range names {
cfg, err := configutil.LoadConfigOnce()
if err != nil {
return nil, err
}
idx := slices.Index(cfg.VerificationCertificates.Certificates, name)
if idx < 0 {
return nil, errors.New("verification certificate not found: " + name)
}
paths = append(paths, cfg.VerificationCertificates.Certificates[idx].Path)
}
return paths, nil
repo := registry.NewRepositoryClient(authClient, ref, plainHTTP)

return verification.NewVerifier(repo)
}
41 changes: 9 additions & 32 deletions cmd/notation/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,15 @@ func TestVerifyCommand_BasicArgs(t *testing.T) {
command := verifyCommand(opts)
expected := &verifyOpts{
reference: "ref",
RemoteFlagOpts: RemoteFlagOpts{
SecureFlagOpts: SecureFlagOpts{
Username: "user",
Password: "password",
},
CommonFlagOpts: CommonFlagOpts{
MediaType: defaultMediaType,
},
SecureFlagOpts: SecureFlagOpts{
Username: "user",
Password: "password",
},
certs: []string{"cert0", "cert1"},
certFiles: []string{"certfile0", "certfile1"},
signatures: []string{"sig0", "sig1"},
pull: true,
}
if err := command.ParseFlags([]string{
expected.reference,
"--username", expected.Username,
"--password", expected.Password,
"-c", expected.certs[0],
"--cert", expected.certs[1],
"--cert-file", expected.certFiles[0],
"--cert-file", expected.certFiles[1],
"--signature", expected.signatures[0],
"-s", expected.signatures[1]}); err != nil {
"--password", expected.Password}); err != nil {
t.Fatalf("Parse Flag failed: %v", err)
}
if err := command.Args(command, command.Flags().Args()); err != nil {
Expand All @@ -49,24 +34,16 @@ func TestVerifyCommand_MoreArgs(t *testing.T) {
command := verifyCommand(opts)
expected := &verifyOpts{
reference: "ref",
RemoteFlagOpts: RemoteFlagOpts{
SecureFlagOpts: SecureFlagOpts{
PlainHTTP: true,
},
CommonFlagOpts: CommonFlagOpts{
MediaType: "mediaT",
},
SecureFlagOpts: SecureFlagOpts{
PlainHTTP: true,
},
certs: []string{},
certFiles: []string{},
signatures: []string{},
pull: false,
config: []string{"key1=val1", "key2=val2"},
}
if err := command.ParseFlags([]string{
expected.reference,
"--plain-http",
"--pull=false",
"--media-type=mediaT"}); err != nil {
"--config", expected.config[0],
"--config", expected.config[1]}); err != nil {
t.Fatalf("Parse Flag failed: %v", err)
}
if err := command.Args(command, command.Flags().Args()); err != nil {
Expand Down
33 changes: 31 additions & 2 deletions docs/hello-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ To get things started quickly, the Notation cli supports self-signed certificate
```bash
notation sign --envelope-type cose $IMAGE
```
To save the generated digest
```
export DIGEST=$(notation sign $IMAGE)
```

- List the image, and any associated signatures

Expand All @@ -107,12 +111,37 @@ To get things started quickly, the Notation cli supports self-signed certificate

## Verify a Container Image Using Notation Signatures

Notation provides a trust policy for users to specify trusted identities which will sign the artifiacts, and level of signature verification to use. A trust policy is a JSON document, below example works for the current case.

```
{
"version": "1.0",
"trustPolicies": [
{
"name": "wabbit-networks-images",
"registryScopes": [
"localhost:5000/net-monitor"
],
"signatureVerification": {
"level": "strict"
},
"trustStores": [
"ca:wabbit-networks.io"
],
"trustedIdentities": [
"x509.subject: C=US, ST=WA, L=Seattle, O=Notary"
]
}
]
}
```

To avoid a Trojan Horse attack, and before pulling an artifact into an environment, it is important to verify that the artifact was unmodified after it was created (integrity), and from an trusted entity (authenticity). Notation uses a set of configured public keys that represent trusted entities, to verify the content. The `notation cert generate-test` command created the public key, however it must be explicitly added for verification to succeed.

- Attempt to verify the $IMAGE notation signature

```bash
notation verify $IMAGE
notation verify --plain-http $REPO@$DIGEST
```

*The above verification should fail, as you haven't yet configured the keys to trust.*
Expand All @@ -131,7 +160,7 @@ To avoid a Trojan Horse attack, and before pulling an artifact into an environme
- Verify the `net-monitor:v1` notation signature

```bash
notation verify $IMAGE
notation verify --plain-http $REPO@$DIGEST
```

This should now succeed because the image is signed with a trusted public key
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ require (
)

require (
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-ldap/ldap/v3 v3.4.4 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/oras-project/artifacts-spec v1.0.0-rc.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
)
Loading