diff --git a/cmd/notation/cert_gen.go b/cmd/notation/cert_gen.go index ed96b9843..d27c434cc 100644 --- a/cmd/notation/cert_gen.go +++ b/cmd/notation/cert_gen.go @@ -64,7 +64,13 @@ func generateTestCert(ctx *cli.Context) error { return err } isDefault := ctx.Bool(keyDefaultFlag.Name) - keySuite := config.KeySuite{Name: name, X509KeyPair: &config.X509KeyPair{KeyPath: keyPath, CertificatePath: certPath}} + keySuite := config.KeySuite{ + Name: name, + X509KeyPair: &config.X509KeyPair{ + KeyPath: keyPath, + CertificatePath: certPath, + }, + } err = addKeyCore(cfg, keySuite, ctx.Bool(keyDefaultFlag.Name)) if err != nil { return err diff --git a/cmd/notation/key.go b/cmd/notation/key.go index 5cbfe3eaf..de2a45686 100644 --- a/cmd/notation/key.go +++ b/cmd/notation/key.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/ioutil" "github.com/notaryproject/notation/internal/slices" "github.com/notaryproject/notation/pkg/config" @@ -52,6 +53,7 @@ var ( Name: "id", Usage: "key id (required if --plugin is set)", }, + cmd.FlagPluginConfig, keyDefaultFlag, }, Action: addKey, @@ -125,7 +127,7 @@ func addExternalKey(ctx *cli.Context, pluginName, keyName string) (config.KeySui if id == "" { return config.KeySuite{}, errors.New("missing key id") } - mgr := manager.NewManager() + mgr := manager.New(config.PluginDirPath) p, err := mgr.Get(ctx.Context, pluginName) if err != nil { return config.KeySuite{}, err @@ -133,9 +135,17 @@ func addExternalKey(ctx *cli.Context, pluginName, keyName string) (config.KeySui if p.Err != nil { return config.KeySuite{}, fmt.Errorf("invalid plugin: %w", p.Err) } + pluginConfig, err := cmd.ParseFlagPluginConfig(ctx.StringSlice(cmd.FlagPluginConfig.Name)) + if err != nil { + return config.KeySuite{}, err + } return config.KeySuite{ - Name: keyName, - ExternalKey: &config.ExternalKey{ID: id, PluginName: pluginName}, + Name: keyName, + ExternalKey: &config.ExternalKey{ + ID: id, + PluginName: pluginName, + PluginConfig: pluginConfig, + }, }, nil } diff --git a/cmd/notation/plugin.go b/cmd/notation/plugin.go index 26554448c..47153ab74 100644 --- a/cmd/notation/plugin.go +++ b/cmd/notation/plugin.go @@ -5,6 +5,7 @@ import ( "github.com/notaryproject/notation-go/plugin/manager" "github.com/notaryproject/notation/internal/ioutil" + "github.com/notaryproject/notation/pkg/config" "github.com/urfave/cli/v2" ) @@ -26,7 +27,7 @@ var ( ) func listPlugins(ctx *cli.Context) error { - mgr := manager.NewManager() + mgr := manager.New(config.PluginDirPath) plugins, err := mgr.List(ctx.Context) if err != nil { return err diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 134c12301..776ef99b1 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/crypto/timestamp" "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/osutil" "github.com/notaryproject/notation/pkg/config" @@ -37,6 +38,7 @@ var signCommand = &cli.Command{ flagPassword, flagPlainHTTP, flagMediaType, + cmd.FlagPluginConfig, }, Action: runSign, } @@ -94,7 +96,19 @@ func prepareSigningContent(ctx *cli.Context) (notation.Descriptor, notation.Sign "identity": identity, } } + var tsa timestamp.Timestamper + if endpoint := ctx.String(cmd.FlagTimestamp.Name); endpoint != "" { + if tsa, err = timestamp.NewHTTPTimestamper(nil, endpoint); err != nil { + return notation.Descriptor{}, notation.SignOptions{}, err + } + } + pluginConfig, err := cmd.ParseFlagPluginConfig(ctx.StringSlice(cmd.FlagPluginConfig.Name)) + if err != nil { + return notation.Descriptor{}, notation.SignOptions{}, err + } return manifestDesc, notation.SignOptions{ - Expiry: cmd.GetExpiry(ctx), + Expiry: cmd.GetExpiry(ctx), + TSA: tsa, + PluginConfig: pluginConfig, }, nil } diff --git a/go.mod b/go.mod index 554a53806..0a6e9cc6c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3 github.com/docker/cli v20.10.14+incompatible - github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220504164459-182873bded16 + github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220518191708-407537596ed5 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.2 github.com/oras-project/artifacts-spec v1.0.0-draft.1.1 diff --git a/go.sum b/go.sum index ba8e80660..1c3f128ec 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220504164459-182873bded16 h1:pcT6WLHGv1iZ7Z/kflT2NJbuNIqLxuDj2qSfjxE5N3M= -github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220504164459-182873bded16/go.mod h1:KtNtijh22iUsC3y7KTzllwPoDKV7mJANYz/RunvOhqs= +github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220518191708-407537596ed5 h1:gOayVV8HsSFN4BYLizWAMBtjoxSn944tjIxvwDgYmAY= +github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220518191708-407537596ed5/go.mod h1:KtNtijh22iUsC3y7KTzllwPoDKV7mJANYz/RunvOhqs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 9983113a7..78ce8506c 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -1,7 +1,12 @@ // Package cmd contains common flags and routines for all CLIs. package cmd -import "github.com/urfave/cli/v2" +import ( + "fmt" + "strings" + + "github.com/urfave/cli/v2" +) var ( FlagKey = &cli.StringFlag{ @@ -39,4 +44,28 @@ var ( Aliases: []string{"r"}, Usage: "original reference", } + + FlagPluginConfig = &cli.StringSliceFlag{ + Name: "pluginConfig", + Aliases: []string{"pc"}, + Usage: "list of comma-separated {key}={value} pairs that are passed as is to the plugin, refer plugin documentation to set appropriate values", + } ) + +func ParseFlagPluginConfig(pluginConfigSlice []string) (map[string]string, error) { + if len(pluginConfigSlice) == 0 { + return nil, nil + } + m := make(map[string]string, len(pluginConfigSlice)) + for _, c := range pluginConfigSlice { + if k, v, ok := strings.Cut(c, "="); ok { + if _, exist := m[k]; exist { + return nil, fmt.Errorf("duplicated --pluginConfig entry %s", k) + } + m[k] = v + } else { + return nil, fmt.Errorf("malformed --pluginConfig entry %q", c) + } + } + return m, nil +} diff --git a/internal/cmd/flags_test.go b/internal/cmd/flags_test.go new file mode 100644 index 000000000..326cf54c2 --- /dev/null +++ b/internal/cmd/flags_test.go @@ -0,0 +1,39 @@ +// Package cmd contains common flags and routines for all CLIs. +package cmd + +import ( + "reflect" + "testing" +) + +func TestParseFlagPluginConfig(t *testing.T) { + type args struct { + s []string + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + {"nil", args{nil}, nil, false}, + {"empty", args{[]string{}}, nil, false}, + {"single", args{[]string{"a=b"}}, map[string]string{"a": "b"}, false}, + {"multiple", args{[]string{"a=b", "c=d"}}, map[string]string{"a": "b", "c": "d"}, false}, + {"quoted", args{[]string{"a=b", "\"c\"=d"}}, map[string]string{"a": "b", "\"c\"": "d"}, false}, + {"duplicated", args{[]string{"a=b", "a=d"}}, nil, true}, + {"malformed", args{[]string{"a=b", "c:d"}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseFlagPluginConfig(tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("ParseFlagPluginConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseFlagPluginConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/cmd/signer.go b/internal/cmd/signer.go index be315f7f7..025ff542b 100644 --- a/internal/cmd/signer.go +++ b/internal/cmd/signer.go @@ -5,7 +5,8 @@ import ( "time" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/crypto/timestamp" + "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation-go/signature/jws" "github.com/notaryproject/notation/pkg/config" "github.com/notaryproject/notation/pkg/signature" "github.com/urfave/cli/v2" @@ -13,34 +14,35 @@ import ( // GetSigner returns a signer according to the CLI context. func GetSigner(ctx *cli.Context) (notation.Signer, error) { - // read paths of the signing key and its corresponding cert. - var keyPath, certPath string - if path := ctx.String(FlagKeyFile.Name); path != "" { - keyPath = path - certPath = ctx.String(FlagCertFile.Name) - } else { - key, err := config.ResolveKey(ctx.String(FlagKey.Name)) - if err != nil { - return nil, err - } - if key.X509KeyPair == nil { - return nil, errors.New("invalid key type") - } - keyPath = key.KeyPath - certPath = key.CertificatePath + // Construct a signer from key and cert file if provided as CLI arguments + if keyPath := ctx.String(FlagKeyFile.Name); keyPath != "" { + certPath := ctx.String(FlagCertFile.Name) + return signature.NewSignerFromFiles(keyPath, certPath) } - - // construct signer - signer, err := signature.NewSignerFromFiles(keyPath, certPath) + // Construct a signer from preconfigured key pair in config.json + // if key name is provided as the CLI argument + key, err := config.ResolveKey(ctx.String(FlagKey.Name)) if err != nil { return nil, err } - if endpoint := ctx.String(FlagTimestamp.Name); endpoint != "" { - if signer.TSA, err = timestamp.NewHTTPTimestamper(nil, endpoint); err != nil { + if key.X509KeyPair != nil { + return signature.NewSignerFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath) + } + // Construct a plugin signer if key name provided as the CLI argument + // corresponds to an external key + if key.ExternalKey != nil { + mgr := manager.New(config.PluginDirPath) + runner, err := mgr.Runner(key.PluginName) + if err != nil { return nil, err } + return &jws.PluginSigner{ + Runner: runner, + KeyID: key.ExternalKey.ID, + PluginConfig: key.PluginConfig, + }, nil } - return signer, nil + return nil, errors.New("unsupported key, either provide a local key and certificate file paths, or a key name in config.json, check [DOC_PLACEHOLDER] for details") } // GetExpiry returns the signature expiry according to the CLI context. diff --git a/pkg/config/config.go b/pkg/config/config.go index 459fa7880..fc1bf1873 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,8 +15,9 @@ type X509KeyPair struct { // ExternalKey contains the necessary information to delegate // the signing operation to the named plugin. type ExternalKey struct { - ID string `json:"id,omitempty"` - PluginName string `json:"pluginName,omitempty"` + ID string `json:"id,omitempty"` + PluginName string `json:"pluginName,omitempty"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } // KeySuite is a named key suite. diff --git a/pkg/config/path.go b/pkg/config/path.go index 8926d1f0a..d8ff41747 100644 --- a/pkg/config/path.go +++ b/pkg/config/path.go @@ -31,6 +31,9 @@ const ( // CertificateExtension defines the extension of the certificate files CertificateExtension = ".crt" + + // PluginStoreDirName is the name of the plugin store directory + PluginStoreDirName = "plugins" ) var ( @@ -45,6 +48,9 @@ var ( // CertificateStoreDirPath is the path of the certificate store CertificateStoreDirPath string + + // PluginDirPath is the path of the plugin store + PluginDirPath string ) // init initialize the essential file paths @@ -66,6 +72,7 @@ func init() { SignatureStoreDirPath = filepath.Join(cacheDir, SignatureStoreDirName) KeyStoreDirPath = filepath.Join(configDir, KeyStoreDirName) CertificateStoreDirPath = filepath.Join(configDir, CertificateStoreDirName) + PluginDirPath = filepath.Join(configDir, PluginStoreDirName) } // SignatureRootPath returns the root path of signatures for a manifest diff --git a/pkg/signature/jws.go b/pkg/signature/jws.go index 53368804a..d73e99a78 100644 --- a/pkg/signature/jws.go +++ b/pkg/signature/jws.go @@ -24,6 +24,9 @@ func NewSignerFromFiles(keyPath, certPath string) (*jws.Signer, error) { if err != nil { return nil, err } + if len(cert.Certificate) == 0 { + return nil, fmt.Errorf("%q does not contain a signer certificate chain", certPath) + } // parse cert certs := make([]*x509.Certificate, len(cert.Certificate))