From f1173eb355520f3f764e1edb6cc9d4f8eeea4c44 Mon Sep 17 00:00:00 2001 From: Quim Muntal Date: Wed, 18 May 2022 12:29:37 +0200 Subject: [PATCH] Support managing plugin keys (#168) * support plugin keys * bump CI to go 1.18 * upgrade notation-go * update plugins branch * reduce plugin print verbosity * bump notation-go * make key and cert name required * simplify NewSignerFromFiles * Apply suggestions from code review * remove unused package * fix plugin aliases * return printkey errors Signed-off-by: qmuntal Co-authored-by: Shiwei Zhang --- .github/workflows/build.yml | 2 +- cmd/notation/cert.go | 42 +++------ cmd/notation/cert_gen.go | 6 +- cmd/notation/key.go | 155 +++++++++++++++++++-------------- cmd/notation/main.go | 1 + cmd/notation/plugin.go | 35 ++++++++ cmd/notation/verify.go | 7 +- go.mod | 8 +- go.sum | 13 ++- internal/cmd/signer.go | 9 +- internal/ioutil/print.go | 54 ++++++++++++ internal/slices/slices.go | 27 ++++++ internal/slices/slices_test.go | 65 ++++++++++++++ pkg/config/config.go | 41 ++++++++- pkg/config/map.go | 99 --------------------- pkg/config/util.go | 33 ++----- pkg/signature/jws.go | 39 +++------ 17 files changed, 370 insertions(+), 266 deletions(-) create mode 100644 cmd/notation/plugin.go create mode 100644 internal/ioutil/print.go create mode 100644 internal/slices/slices.go create mode 100644 internal/slices/slices_test.go delete mode 100644 pkg/config/map.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 355ef202e..ba22a020d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go-version: [1.17] + go-version: [1.18] fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} diff --git a/cmd/notation/cert.go b/cmd/notation/cert.go index 7cd07de43..0de44a6c0 100644 --- a/cmd/notation/cert.go +++ b/cmd/notation/cert.go @@ -3,10 +3,13 @@ package main import ( "errors" "fmt" + "os" "path/filepath" "time" "github.com/notaryproject/notation-go/crypto/cryptoutil" + "github.com/notaryproject/notation/internal/ioutil" + "github.com/notaryproject/notation/internal/slices" "github.com/notaryproject/notation/pkg/config" "github.com/urfave/cli/v2" ) @@ -96,9 +99,6 @@ func addCert(ctx *cli.Context) error { return err } name := ctx.String("name") - if name == "" { - name = nameFromPath(path) - } // check if the target path is a cert if _, err := cryptoutil.ReadCertificateFile(path); err != nil { @@ -123,9 +123,13 @@ func addCert(ctx *cli.Context) error { } func addCertCore(cfg *config.File, name, path string) error { - if ok := cfg.VerificationCertificates.Certificates.Append(name, path); !ok { + if slices.Contains(cfg.VerificationCertificates.Certificates, name) { return errors.New(name + ": already exists") } + cfg.VerificationCertificates.Certificates = append(cfg.VerificationCertificates.Certificates, config.CertificateReference{ + Name: name, + Path: path, + }) return nil } @@ -137,8 +141,7 @@ func listCerts(ctx *cli.Context) error { } // write out - printCertificateSet(cfg.VerificationCertificates.Certificates) - return nil + return ioutil.PrintCertificateMap(os.Stdout, cfg.VerificationCertificates.Certificates) } func removeCerts(ctx *cli.Context) error { @@ -156,9 +159,11 @@ func removeCerts(ctx *cli.Context) error { var removedNames []string for _, name := range names { - if ok := cfg.VerificationCertificates.Certificates.Remove(name); !ok { + idx := slices.Index(cfg.VerificationCertificates.Certificates, name) + if idx < 0 { return errors.New(name + ": not found") } + cfg.VerificationCertificates.Certificates = slices.Delete(cfg.VerificationCertificates.Certificates, idx) removedNames = append(removedNames, name) } if err := cfg.Save(); err != nil { @@ -171,26 +176,3 @@ func removeCerts(ctx *cli.Context) error { } return nil } - -func printCertificateSet(s config.CertificateMap) { - maxNameSize := 0 - for _, ref := range s { - if len(ref.Name) > maxNameSize { - maxNameSize = len(ref.Name) - } - } - format := fmt.Sprintf("%%-%ds\t%%s\n", maxNameSize) - fmt.Printf(format, "NAME", "PATH") - for _, ref := range s { - fmt.Printf(format, ref.Name, ref.Path) - } -} - -func nameFromPath(path string) string { - base := filepath.Base(path) - name := base[:len(base)-len(filepath.Ext(base))] - if name == "" { - return base - } - return name -} diff --git a/cmd/notation/cert_gen.go b/cmd/notation/cert_gen.go index 1db476534..ed96b9843 100644 --- a/cmd/notation/cert_gen.go +++ b/cmd/notation/cert_gen.go @@ -63,7 +63,9 @@ func generateTestCert(ctx *cli.Context) error { if err != nil { return err } - isDefaultKey, err := addKeyCore(cfg, name, keyPath, certPath, ctx.Bool(keyDefaultFlag.Name)) + isDefault := ctx.Bool(keyDefaultFlag.Name) + 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 } @@ -79,7 +81,7 @@ func generateTestCert(ctx *cli.Context) error { // write out fmt.Printf("%s: added to the key list\n", name) - if isDefaultKey { + if isDefault { fmt.Printf("%s: marked as default\n", name) } if trust { diff --git a/cmd/notation/key.go b/cmd/notation/key.go index 34ceeb965..5cbfe3eaf 100644 --- a/cmd/notation/key.go +++ b/cmd/notation/key.go @@ -4,8 +4,12 @@ import ( "crypto/tls" "errors" "fmt" + "os" "path/filepath" + "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation/internal/ioutil" + "github.com/notaryproject/notation/internal/slices" "github.com/notaryproject/notation/pkg/config" "github.com/urfave/cli/v2" ) @@ -31,12 +35,22 @@ var ( keyAddCommand = &cli.Command{ Name: "add", Usage: "Add key to signing key list", - ArgsUsage: " ", + ArgsUsage: "[ ]", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "key name", + Name: "name", + Aliases: []string{"n"}, + Usage: "key name", + Required: true, + }, + &cli.StringFlag{ + Name: "plugin", + Aliases: []string{"p"}, + Usage: "signing plugin name", + }, + &cli.StringFlag{ + Name: "id", + Usage: "key id (required if --plugin is set)", }, keyDefaultFlag, }, @@ -71,63 +85,97 @@ var ( ) func addKey(ctx *cli.Context) error { - // initialize - args := ctx.Args() - switch args.Len() { - case 0: - return errors.New("missing key and certificate paths") - case 1: - return errors.New("missing certificate path for the correspoding key") - } - - keyPath, err := filepath.Abs(args.Get(0)) - if err != nil { - return err - } - certPath, err := filepath.Abs(args.Get(1)) + cfg, err := config.LoadOrDefault() if err != nil { return err } + var key config.KeySuite + pluginName := ctx.String("plugin") name := ctx.String("name") - if name == "" { - name = nameFromPath(keyPath) - } - - // check key / cert pair - if _, err := tls.LoadX509KeyPair(certPath, keyPath); err != nil { - return err + if pluginName != "" { + key, err = addExternalKey(ctx, pluginName, name) + } else { + key, err = newX509KeyPair(ctx, name) } - - // core process - cfg, err := config.LoadOrDefault() if err != nil { return err } - isDefault, err := addKeyCore(cfg, name, keyPath, certPath, ctx.Bool(keyDefaultFlag.Name)) + + isDefault := ctx.Bool(keyDefaultFlag.Name) + err = addKeyCore(cfg, key, isDefault) if err != nil { return err } + if err := cfg.Save(); err != nil { return err } // write out if isDefault { - fmt.Printf("%s: marked as default\n", name) + fmt.Printf("%s: marked as default\n", key.Name) } else { - fmt.Println(name) + fmt.Println(key.Name) } return nil } -func addKeyCore(cfg *config.File, name, keyPath, certPath string, markDefault bool) (bool, error) { - if ok := cfg.SigningKeys.Keys.Append(name, keyPath, certPath); !ok { - return false, errors.New(name + ": already exists") +func addExternalKey(ctx *cli.Context, pluginName, keyName string) (config.KeySuite, error) { + id := ctx.String("id") + if id == "" { + return config.KeySuite{}, errors.New("missing key id") } + mgr := manager.NewManager() + p, err := mgr.Get(ctx.Context, pluginName) + if err != nil { + return config.KeySuite{}, err + } + if p.Err != nil { + return config.KeySuite{}, fmt.Errorf("invalid plugin: %w", p.Err) + } + return config.KeySuite{ + Name: keyName, + ExternalKey: &config.ExternalKey{ID: id, PluginName: pluginName}, + }, nil +} + +func newX509KeyPair(ctx *cli.Context, keyName string) (config.KeySuite, error) { + args := ctx.Args() + switch args.Len() { + case 0: + return config.KeySuite{}, errors.New("missing key and certificate paths") + case 1: + return config.KeySuite{}, errors.New("missing certificate path for the corresponding key") + } + + keyPath, err := filepath.Abs(args.Get(0)) + if err != nil { + return config.KeySuite{}, err + } + certPath, err := filepath.Abs(args.Get(1)) + if err != nil { + return config.KeySuite{}, err + } + + // check key / cert pair + if _, err := tls.LoadX509KeyPair(certPath, keyPath); err != nil { + return config.KeySuite{}, err + } + return config.KeySuite{ + Name: keyName, + X509KeyPair: &config.X509KeyPair{KeyPath: keyPath, CertificatePath: certPath}, + }, nil +} + +func addKeyCore(cfg *config.File, key config.KeySuite, markDefault bool) error { + if slices.Contains(cfg.SigningKeys.Keys, key.Name) { + return errors.New(key.Name + ": already exists") + } + cfg.SigningKeys.Keys = append(cfg.SigningKeys.Keys, key) if markDefault { - cfg.SigningKeys.Default = name + cfg.SigningKeys.Default = key.Name } - return cfg.SigningKeys.Default == name, nil + return nil } func updateKey(ctx *cli.Context) error { @@ -142,7 +190,7 @@ func updateKey(ctx *cli.Context) error { if err != nil { return err } - if _, _, ok := cfg.SigningKeys.Keys.Get(name); !ok { + if !slices.Contains(cfg.SigningKeys.Keys, name) { return errors.New(name + ": not found") } if !ctx.Bool(keyDefaultFlag.Name) { @@ -168,8 +216,7 @@ func listKeys(ctx *cli.Context) error { } // write out - printKeySet(cfg.SigningKeys.Default, cfg.SigningKeys.Keys) - return nil + return ioutil.PrintKeyMap(os.Stdout, cfg.SigningKeys.Default, cfg.SigningKeys.Keys) } func removeKeys(ctx *cli.Context) error { @@ -188,9 +235,11 @@ func removeKeys(ctx *cli.Context) error { prevDefault := cfg.SigningKeys.Default var removedNames []string for _, name := range names { - if ok := cfg.SigningKeys.Keys.Remove(name); !ok { + idx := slices.Index(cfg.SigningKeys.Keys, name) + if idx < 0 { return errors.New(name + ": not found") } + cfg.SigningKeys.Keys = slices.Delete(cfg.SigningKeys.Keys, idx) removedNames = append(removedNames, name) if prevDefault == name { cfg.SigningKeys.Default = "" @@ -210,29 +259,3 @@ func removeKeys(ctx *cli.Context) error { } return nil } - -func printKeySet(target string, s config.KeyMap) { - if len(s) == 0 { - fmt.Println("NAME\tPATH") - return - } - - var maxNameSize, maxKeyPathSize int - for _, ref := range s { - if len(ref.Name) > maxNameSize { - maxNameSize = len(ref.Name) - } - if len(ref.KeyPath) > maxKeyPathSize { - maxKeyPathSize = len(ref.KeyPath) - } - } - format := fmt.Sprintf("%%c %%-%ds\t%%-%ds\t%%s\n", maxNameSize, maxKeyPathSize) - fmt.Printf(format, ' ', "NAME", "KEY PATH", "CERTIFICATE PATH") - for _, ref := range s { - mark := ' ' - if ref.Name == target { - mark = '*' - } - fmt.Printf(format, mark, ref.Name, ref.KeyPath, ref.CertificatePath) - } -} diff --git a/cmd/notation/main.go b/cmd/notation/main.go index a51fa425d..e5d653f6e 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -27,6 +27,7 @@ func main() { certCommand, keyCommand, cacheCommand, + pluginCommand, }, } if err := app.Run(os.Args); err != nil { diff --git a/cmd/notation/plugin.go b/cmd/notation/plugin.go new file mode 100644 index 000000000..26554448c --- /dev/null +++ b/cmd/notation/plugin.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + + "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation/internal/ioutil" + "github.com/urfave/cli/v2" +) + +var ( + pluginCommand = &cli.Command{ + Name: "plugin", + Usage: "Manage plugins", + Subcommands: []*cli.Command{ + pluginListCommand, + }, + } + + pluginListCommand = &cli.Command{ + Name: "list", + Usage: "List registered plugins", + Aliases: []string{"ls"}, + Action: listPlugins, + } +) + +func listPlugins(ctx *cli.Context) error { + mgr := manager.NewManager() + plugins, err := mgr.List(ctx.Context) + if err != nil { + return err + } + return ioutil.PrintPlugins(os.Stdout, plugins) +} diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index 97c643c76..5b9642407 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -8,6 +8,7 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/internal/slices" "github.com/notaryproject/notation/pkg/cache" "github.com/notaryproject/notation/pkg/config" "github.com/notaryproject/notation/pkg/signature" @@ -137,11 +138,11 @@ func appendCertPathFromName(paths, names []string) ([]string, error) { if err != nil { return nil, err } - path, ok := cfg.VerificationCertificates.Certificates.Get(name) - if !ok { + idx := slices.Index(cfg.VerificationCertificates.Certificates, name) + if idx < 0 { return nil, errors.New("verification certificate not found: " + name) } - paths = append(paths, path) + paths = append(paths, cfg.VerificationCertificates.Certificates[idx].Path) } return paths, nil } diff --git a/go.mod b/go.mod index 019c1afd3..554a53806 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/notaryproject/notation -go 1.17 +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.7.0-alpha.1.0.20220422005131-96df588f2f6d + github.com/notaryproject/notation-go v0.8.0-alpha.1.0.20220504164459-182873bded16 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 @@ -17,11 +17,11 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/docker/docker v20.10.8+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect - github.com/golang-jwt/jwt/v4 v4.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.4.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 // indirect + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect gotest.tools/v3 v3.0.3 // indirect ) diff --git a/go.sum b/go.sum index 85a156e93..ba8e80660 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -42,8 +41,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= -github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -74,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.7.0-alpha.1.0.20220422005131-96df588f2f6d h1:qIylQ35uMNA2ZSgppopKTAFGJj34UelBJj5iCH2PgTY= -github.com/notaryproject/notation-go v0.7.0-alpha.1.0.20220422005131-96df588f2f6d/go.mod h1:B/JbgikwvLoD9gFeWYhFtxIEozRoFAs9q31mj3vz1i4= +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/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= @@ -141,8 +140,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 h1:QOQNt6vCjMpXE7JSK5VvAzJC1byuN3FgTNSBwf+CJgI= -golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/cmd/signer.go b/internal/cmd/signer.go index b50b2c6d2..be315f7f7 100644 --- a/internal/cmd/signer.go +++ b/internal/cmd/signer.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "time" "github.com/notaryproject/notation-go" @@ -18,11 +19,15 @@ func GetSigner(ctx *cli.Context) (notation.Signer, error) { keyPath = path certPath = ctx.String(FlagCertFile.Name) } else { - var err error - keyPath, certPath, err = config.ResolveKeyPath(ctx.String(FlagKey.Name)) + 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 signer diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go new file mode 100644 index 000000000..ec8530a25 --- /dev/null +++ b/internal/ioutil/print.go @@ -0,0 +1,54 @@ +package ioutil + +import ( + "fmt" + "io" + "text/tabwriter" + + "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation/pkg/config" +) + +func newTabWriter(w io.Writer) *tabwriter.Writer { + return tabwriter.NewWriter(w, 0, 0, 3, ' ', 0) +} + +func PrintPlugins(w io.Writer, v []*manager.Plugin) error { + tw := newTabWriter(w) + fmt.Fprintln(tw, "NAME\tDESCRIPTION\tVERSION\tCAPABILITIES\tERROR\t") + for _, p := range v { + fmt.Fprintf(tw, "%s\t%s\t%s\t%v\t%v\t\n", + p.Name, p.Description, p.Version, p.Capabilities, p.Err) + } + return tw.Flush() +} + +func PrintKeyMap(w io.Writer, target string, v []config.KeySuite) error { + tw := newTabWriter(w) + fmt.Fprintln(tw, "NAME\tKEY PATH\tCERTIFICATE PATH\tID\tPLUGIN NAME\t") + for _, key := range v { + name := key.Name + if key.Name == target { + name = "* " + name + } + kp := key.X509KeyPair + if kp == nil { + kp = &config.X509KeyPair{} + } + ext := key.ExternalKey + if ext == nil { + ext = &config.ExternalKey{} + } + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t\n", name, kp.KeyPath, kp.CertificatePath, ext.ID, ext.PluginName) + } + return tw.Flush() +} + +func PrintCertificateMap(w io.Writer, v []config.CertificateReference) error { + tw := newTabWriter(w) + fmt.Fprintln(tw, "NAME\tPATH\t") + for _, cert := range v { + fmt.Fprintf(tw, "%s\t%s\t\n", cert.Name, cert.Path) + } + return tw.Flush() +} diff --git a/internal/slices/slices.go b/internal/slices/slices.go new file mode 100644 index 000000000..1fffa0883 --- /dev/null +++ b/internal/slices/slices.go @@ -0,0 +1,27 @@ +package slices + +type isser interface { + Is(string) bool +} + +// Index returns the index of the first occurrence of name in s, +// or -1 if not present. +func Index[E isser](s []E, name string) int { + for i, v := range s { + if v.Is(name) { + return i + } + } + return -1 +} + +// Contains reports whether name is present in s. +func Contains[E isser](s []E, name string) bool { + return Index(s, name) >= 0 +} + +// Delete removes the elements s[i:i+1] from s, +// returning the modified slice. +func Delete[S ~[]E, E isser](s S, i int) S { + return append(s[:i], s[i+1:]...) +} diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go new file mode 100644 index 000000000..ae24e6313 --- /dev/null +++ b/internal/slices/slices_test.go @@ -0,0 +1,65 @@ +package slices + +import ( + "reflect" + "testing" +) + +type iss string + +func (i iss) Is(v string) bool { return string(i) == v } + +func TestIndex(t *testing.T) { + tests := []struct { + s []iss + v string + want int + }{ + {nil, "", -1}, + {[]iss{}, "", -1}, + {[]iss{"1", "2", "3"}, "2", 1}, + {[]iss{"1", "2", "2", "3"}, "2", 1}, + {[]iss{"1", "2", "3", "2"}, "2", 1}, + } + for _, tt := range tests { + if got := Index(tt.s, tt.v); got != tt.want { + t.Errorf("Index() = %v, want %v", got, tt.want) + } + } +} + +func TestContains(t *testing.T) { + tests := []struct { + s []iss + v string + want bool + }{ + {nil, "", false}, + {[]iss{}, "", false}, + {[]iss{"1", "2", "3"}, "2", true}, + {[]iss{"1", "2", "2", "3"}, "2", true}, + {[]iss{"1", "2", "3", "2"}, "2", true}, + } + for _, tt := range tests { + if got := Contains(tt.s, tt.v); got != tt.want { + t.Errorf("Index() = %v, want %v", got, tt.want) + } + } +} + +func TestDelete(t *testing.T) { + tests := []struct { + s []iss + i int + want []iss + }{ + {[]iss{"1", "2", "3"}, 1, []iss{"1", "3"}}, + {[]iss{"1", "2", "2", "3"}, 2, []iss{"1", "2", "3"}}, + {[]iss{"1", "2", "3", "2"}, 2, []iss{"1", "2", "2"}}, + } + for _, tt := range tests { + if got := Delete(tt.s, tt.i); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Delete() = %v, want %v", got, tt.want) + } + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index f8581faff..459fa7880 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,6 +6,41 @@ import ( "path/filepath" ) +// X509KeyPair contains the paths of a public/private key pair files. +type X509KeyPair struct { + KeyPath string `json:"keyPath,omitempty"` + CertificatePath string `json:"certPath,omitempty"` +} + +// 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"` +} + +// KeySuite is a named key suite. +type KeySuite struct { + Name string `json:"name"` + + *X509KeyPair + *ExternalKey +} + +func (k KeySuite) Is(name string) bool { + return k.Name == name +} + +// CertificateReference is a named file path. +type CertificateReference struct { + Name string `json:"name"` + Path string `json:"path"` +} + +func (c CertificateReference) Is(name string) bool { + return c.Name == name +} + // File reflects the config file. // Specification: https://github.com/notaryproject/notation/pull/76 type File struct { @@ -16,13 +51,13 @@ type File struct { // VerificationCertificates is a collection of public certs used for verification. type VerificationCertificates struct { - Certificates CertificateMap `json:"certs"` + Certificates []CertificateReference `json:"certs"` } // SigningKeys is a collection of signing keys. type SigningKeys struct { - Default string `json:"default"` - Keys KeyMap `json:"keys"` + Default string `json:"default"` + Keys []KeySuite `json:"keys"` } // New creates a new config file diff --git a/pkg/config/map.go b/pkg/config/map.go deleted file mode 100644 index cb269e8fa..000000000 --- a/pkg/config/map.go +++ /dev/null @@ -1,99 +0,0 @@ -package config - -// KeySuite is a named key suite with file paths. -type KeySuite struct { - Name string `json:"name"` - KeyPath string `json:"keyPath"` - CertificatePath string `json:"certPath"` -} - -// KeyMap is a set of KeySuite indexed by name. -// The overall performance is O(n) while the order of entries is persevered. -type KeyMap []KeySuite - -// Append appends a uniquely named KeySuite to the map. -// Return true if new values are appended. -func (m *KeyMap) Append(name, keyPath, certPath string) bool { - for _, ref := range *m { - if ref.Name == name { - return false - } - } - *m = append(*m, KeySuite{ - Name: name, - KeyPath: keyPath, - CertificatePath: certPath, - }) - return true -} - -// Remove removes a named path from the map. -// Return true if an entry is found and removed. -func (m *KeyMap) Remove(name string) bool { - for i, ref := range *m { - if ref.Name == name { - *m = append((*m)[:i], (*m)[i+1:]...) - return true - } - } - return false -} - -// Get return the paths of the given name. -// Return true if found. -func (m KeyMap) Get(name string) (string, string, bool) { - for _, ref := range m { - if ref.Name == name { - return ref.KeyPath, ref.CertificatePath, true - } - } - return "", "", false -} - -// CertificateReference is a named file path. -type CertificateReference struct { - Name string `json:"name"` - Path string `json:"path"` -} - -// CertificateMap is a set of CertificateReference indexed by name. -// The overall performance is O(n) while the order of entries is persevered. -type CertificateMap []CertificateReference - -// Append appends a uniquely named path to the map. -// Return true if new values are appended. -func (m *CertificateMap) Append(name, path string) bool { - for _, ref := range *m { - if ref.Name == name { - return false - } - } - *m = append(*m, CertificateReference{ - Name: name, - Path: path, - }) - return true -} - -// Remove removes a named path from the map. -// Return true if an entry is found and removed. -func (m *CertificateMap) Remove(name string) bool { - for i, ref := range *m { - if ref.Name == name { - *m = append((*m)[:i], (*m)[i+1:]...) - return true - } - } - return false -} - -// Get return the path of the given name. -// Return true if found. -func (m CertificateMap) Get(name string) (string, bool) { - for _, ref := range m { - if ref.Name == name { - return ref.Path, true - } - } - return "", false -} diff --git a/pkg/config/util.go b/pkg/config/util.go index 90961c5fa..d429ef789 100644 --- a/pkg/config/util.go +++ b/pkg/config/util.go @@ -3,14 +3,13 @@ package config import ( "errors" "strings" + + "github.com/notaryproject/notation/internal/slices" ) var ( // ErrKeyNotFound indicates that the signing key is not found. ErrKeyNotFound = errors.New("signing key not found") - - // ErrCertificateNotFound indicates that the verification certificate is not found. - ErrCertificateNotFound = errors.New("verification certificate not found") ) // IsRegistryInsecure checks whether the registry is in the list of insecure registries. @@ -27,33 +26,19 @@ func IsRegistryInsecure(target string) bool { return false } -// ResolveKeyPath resolves the key path by name along with -// its corresponding certificate path. +// ResolveKey resolves the key by name. // The default key is attempted if name is empty. -func ResolveKeyPath(name string) (string, string, error) { +func ResolveKey(name string) (KeySuite, error) { config, err := LoadOrDefaultOnce() if err != nil { - return "", "", err + return KeySuite{}, err } if name == "" { name = config.SigningKeys.Default } - keyPath, certPath, ok := config.SigningKeys.Keys.Get(name) - if !ok { - return "", "", ErrKeyNotFound - } - return keyPath, certPath, nil -} - -// ResolveCertificatePath resolves the certificate path by name. -func ResolveCertificatePath(name string) (string, error) { - config, err := LoadOrDefaultOnce() - if err != nil { - return "", err - } - path, ok := config.VerificationCertificates.Certificates.Get(name) - if !ok { - return "", ErrCertificateNotFound + idx := slices.Index(config.SigningKeys.Keys, name) + if idx < 0 { + return KeySuite{}, ErrKeyNotFound } - return path, nil + return config.SigningKeys.Keys[idx], nil } diff --git a/pkg/signature/jws.go b/pkg/signature/jws.go index 3db076355..53368804a 100644 --- a/pkg/signature/jws.go +++ b/pkg/signature/jws.go @@ -4,9 +4,9 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "os" - "github.com/notaryproject/notation-go/crypto/cryptoutil" "github.com/notaryproject/notation-go/signature/jws" ) @@ -20,47 +20,36 @@ func NewSignerFromFiles(keyPath, certPath string) (*jws.Signer, error) { } // read key / cert pair - keyPEM, err := os.ReadFile(keyPath) - if err != nil { - return nil, err - } - certPEM, err := os.ReadFile(certPath) - if err != nil { - return nil, err - } - keyPair, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - return nil, err - } - key := keyPair.PrivateKey - method, err := jws.SigningMethodFromKey(key) + cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return nil, err } // parse cert - certs, err := cryptoutil.ParseCertificatePEM(certPEM) - if err != nil { - return nil, err + certs := make([]*x509.Certificate, len(cert.Certificate)) + for i, c := range cert.Certificate { + certs[i], err = x509.ParseCertificate(c) + if err != nil { + return nil, err + } } // create signer - return jws.NewSignerWithCertificateChain(method, key, certs) + return jws.NewSigner(cert.PrivateKey, certs) } // NewSignerFromFiles creates a verifier from certificate files func NewVerifierFromFiles(certPaths []string) (*jws.Verifier, error) { - roots := x509.NewCertPool() + verifier := jws.NewVerifier() + verifier.VerifyOptions.Roots = x509.NewCertPool() for _, path := range certPaths { - bundledCerts, err := cryptoutil.ReadCertificateFile(path) + data, err := os.ReadFile(path) if err != nil { return nil, err } - for _, cert := range bundledCerts { - roots.AddCert(cert) + if !verifier.VerifyOptions.Roots.AppendCertsFromPEM(data) { + return nil, fmt.Errorf("failed to parse PEM certificate: %q", path) } } - verifier := jws.NewVerifier() - verifier.VerifyOptions.Roots = roots return verifier, nil }