From d0c0218ff2c2388b17fb13507c1fd54d4ac718c4 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 26 Apr 2022 20:47:39 +0200 Subject: [PATCH 01/58] implement plugin manager Signed-off-by: qmuntal --- plugin/integration_test.go | 71 +++++++++++ plugin/manager.go | 100 ++++++++++++++++ plugin/manager_test.go | 234 +++++++++++++++++++++++++++++++++++++ plugin/metadata.go | 58 +++++++++ plugin/metadata_test.go | 75 ++++++++++++ plugin/plugin.go | 56 +++++++++ plugin/testdata/main.go | 35 ++++++ 7 files changed, 629 insertions(+) create mode 100644 plugin/integration_test.go create mode 100644 plugin/manager.go create mode 100644 plugin/manager_test.go create mode 100644 plugin/metadata.go create mode 100644 plugin/metadata_test.go create mode 100644 plugin/plugin.go create mode 100644 plugin/testdata/main.go diff --git a/plugin/integration_test.go b/plugin/integration_test.go new file mode 100644 index 00000000..468bee41 --- /dev/null +++ b/plugin/integration_test.go @@ -0,0 +1,71 @@ +package plugin + +import ( + "io" + "os" + "os/exec" + "path/filepath" + "reflect" + "testing" +) + +func preparePlugin(t *testing.T) string { + root := t.TempDir() + src, err := os.Open("./testdata/main.go") + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(filepath.Join(root, "main.go")) + if err != nil { + t.Fatal(err) + } + defer dst.Close() + _, err = io.Copy(dst, src) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(filepath.Join(root, "go.mod"), []byte("module main"), 0666) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir(filepath.Join(root, "foo"), 0755) + if err != nil { + t.Fatal(err) + } + out := filepath.Join(root, "foo", "notation-foo") + out = addExeSuffix(out) + cmd := exec.Command("go", "build", "-o", out) + cmd.Dir = root + err = cmd.Run() + if err != nil { + t.Fatal(err) + } + return root +} + +func TestIntegration(t *testing.T) { + if _, err := exec.LookPath("go"); err != nil { + t.Skip() + } + root := preparePlugin(t) + mgr := &Manager{os.DirFS(root), rootedCommander{root}} + p, err := mgr.Get("foo") + if err != nil { + t.Fatal(err) + } + if p.Err != nil { + t.Fatal(p.Err) + } + list, err := mgr.List() + if err != nil { + t.Fatal(err) + } + if len(list) != 1 { + t.Fatalf("Manager.List() len got %d, want 1", len(list)) + } + if !reflect.DeepEqual(list[0].Metadata, p.Metadata) { + t.Errorf("Manager.List() got %v, want %v", list[0], p) + } +} diff --git a/plugin/manager.go b/plugin/manager.go new file mode 100644 index 00000000..0108bf93 --- /dev/null +++ b/plugin/manager.go @@ -0,0 +1,100 @@ +package plugin + +import ( + "io/fs" + "os" + "os/exec" + "path" +) + +// commander is defined for mocking purposes. +type commander interface { + Output(string, ...string) ([]byte, error) +} + +type rootedCommander struct { + root string +} + +func (c rootedCommander) Output(name string, args ...string) ([]byte, error) { + cmd := &exec.Cmd{ + Path: path.Join(c.root, name), + Args: append([]string{path.Base(name)}, args...), + } + return cmd.Output() +} + +// Manager manages plugins installed on the system. +type Manager struct { + fsys fs.FS + cmder commander +} + +// NewManager returns a new manager. +func NewManager() (*Manager, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + pluginDir := path.Join(homeDir, ".notation", "plugins") + return &Manager{os.DirFS(pluginDir), rootedCommander{pluginDir}}, nil +} + +// Get returns a plugin on the system by its name. +// The plugin might be incomplete if p.Err is not nil. +func (mgr *Manager) Get(name string) (*Plugin, error) { + fullname, ok := mgr.isCandidate(name) + if !ok { + return nil, ErrNotFound + } + p := newPlugin(mgr.cmder, fullname, name) + return p, nil +} + +// List produces a list of the plugins available on the system. +// Some plugins might be incomplete if their Err is not nil. +func (mgr *Manager) List() ([]*Plugin, error) { + var plugins []*Plugin + fs.WalkDir(mgr.fsys, ".", func(dir string, d fs.DirEntry, _ error) error { + if dir == "." || !d.IsDir() { + return nil + } + p, err := mgr.Get(d.Name()) + if err == nil { + plugins = append(plugins, p) + } + return fs.SkipDir + }) + return plugins, nil +} + +// Command returns an "os/exec".Cmd which when .Run() will execute the named plugin. +// The error returned is ErrNotFound if no plugin was found. +func (mgr *Manager) Command(name string, args ...string) (*exec.Cmd, error) { + p, err := mgr.Get(name) + if err != nil { + return nil, err + } + if p.Err != nil { + return nil, p.Err + } + return exec.Command(p.Path, args...), nil +} + +func (mgr *Manager) isCandidate(name string) (fullname string, ok bool) { + fullname = path.Join(name, "notation-"+name) + fi, err := fs.Stat(mgr.fsys, addExeSuffix(fullname)) + if err != nil { + // Ignore any file which we cannot Stat + // (e.g. due to permissions or anything else). + return "", false + } + switch fi.Mode().Type() { + case 0, fs.ModeSymlink: + // Regular file or symlink, keep going. + return fullname, true + default: + // Something else, ignore. + return "", false + } +} diff --git a/plugin/manager_test.go b/plugin/manager_test.go new file mode 100644 index 00000000..bd6b18d5 --- /dev/null +++ b/plugin/manager_test.go @@ -0,0 +1,234 @@ +package plugin + +import ( + "encoding/json" + "errors" + "io/fs" + "reflect" + "strings" + "testing" + "testing/fstest" +) + +type testCommander struct { + output []byte + err error +} + +func (t testCommander) Output(string, ...string) ([]byte, error) { + return t.output, t.err +} + +var validMetadata = Metadata{ + Name: "foo", Description: "friendly", Version: "1", URL: "example.com", + SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}, +} + +func TestManager_Get(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + mgr *Manager + args args + want *Plugin + err string + invalid string + }{ + {"empty fsys", &Manager{fstest.MapFS{}, nil}, args{"foo"}, nil, "plugin not found", ""}, + { + "plugin not found", + &Manager{fstest.MapFS{ + "baz": &fstest.MapFile{Mode: fs.ModeDir}, + }, nil}, + args{"foo"}, + nil, "plugin not found", "", + }, + { + "plugin executable does not exists", + &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + }, nil}, + args{"foo"}, + nil, "plugin not found", "", + }, + { + "plugin executable invalid mode", + &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): &fstest.MapFile{Mode: fs.ModeDir}, + }, testCommander{[]byte("content"), nil}}, + args{"foo"}, + nil, "plugin not found", "", + }, + { + "discover error", + &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{nil, errors.New("failed")}}, + args{"foo"}, + &Plugin{Path: addExeSuffix("foo/notation-foo")}, + "", "failed to fetch metadata", + }, + { + "invalid json", + &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{[]byte("content"), nil}}, + args{"foo"}, + &Plugin{Path: addExeSuffix("foo/notation-foo")}, + "", "metadata can't be decoded", + }, + { + "invalid metadata name", + &Manager{fstest.MapFS{ + "baz": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("baz/notation-baz"): new(fstest.MapFile), + }, testCommander{metadataJSON(validMetadata), nil}}, + args{"baz"}, + &Plugin{Metadata: Metadata{Name: "baz"}, Path: addExeSuffix("baz/notation-baz")}, + "", "metadata name \"baz\" is not valid, must be \"foo\"", + }, + { + "invalid metadata content", + &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{metadataJSON(Metadata{Name: "foo"}), nil}}, + args{"foo"}, + &Plugin{Metadata: Metadata{Name: "foo"}, Path: addExeSuffix("foo/notation-foo")}, + "", "invalid metadata", + }, + { + "valid", + &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{metadataJSON(validMetadata), nil}}, + args{"foo"}, + &Plugin{Metadata: validMetadata, Path: addExeSuffix("foo/notation-foo")}, "", "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.mgr.Get(tt.args.name) + if tt.err != "" { + if err == nil { + t.Fatalf("Manager.Get() error = nil, want %s", tt.err) + } + if !strings.Contains(err.Error(), tt.err) { + t.Fatalf("Manager.Get() error = %v, want %v", err, tt.err) + } + } else if tt.invalid != "" { + if err != nil { + t.Fatalf("Manager.Get() error = %v, want nil", err) + } + if !strings.Contains(got.Err.Error(), tt.invalid) { + t.Fatalf("Manager.Get() error = %v, want %v", got.Err, tt.invalid) + } + } else { + if err != nil { + t.Fatalf("Manager.Get() error = %v, want nil", err) + } + if got.Err != nil { + t.Fatalf("Manager.Get() error = %v, want nil", got.Err) + } + if !reflect.DeepEqual(got.Metadata, tt.want.Metadata) { + t.Errorf("Manager.Get() = %v, want %v", got, tt.want) + } + } + }) + } +} + +func metadataJSON(m Metadata) []byte { + d, err := json.Marshal(m) + if err != nil { + panic(err) + } + return d +} + +func TestManager_List(t *testing.T) { + tests := []struct { + name string + mgr *Manager + want []*Plugin + }{ + {"empty fsys", &Manager{fstest.MapFS{}, nil}, nil}, + {"fsys without plugins", &Manager{fstest.MapFS{"a.go": &fstest.MapFile{}}, nil}, nil}, + { + "fsys with some invalid plugins", &Manager{ + fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{metadataJSON(validMetadata), nil}}, + []*Plugin{{Metadata: validMetadata}}, + }, + { + "fsys with plugins", &Manager{ + fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + "baz": &fstest.MapFile{Mode: fs.ModeDir}, + }, testCommander{metadataJSON(validMetadata), nil}}, + []*Plugin{{Metadata: validMetadata}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := tt.mgr.List() + if len(got) != len(tt.want) { + t.Fatalf("Manager.List() len = %v, want len %v", len(got), len(tt.want)) + } + for i := 0; i < len(got); i++ { + if !reflect.DeepEqual(got[i].Metadata, tt.want[i].Metadata) { + t.Errorf("Manager.List() got %d = %v, want %v", i, got[i], tt.want[i]) + } + } + }) + } +} + +func TestManager_Command(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + mgr *Manager + args args + wantErr bool + }{ + {"empty fsys", &Manager{fstest.MapFS{}, nil}, args{"foo"}, true}, + { + "invalid plugin", &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{nil, errors.New("err")}}, + args{"foo"}, true, + }, + { + "valid", &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{metadataJSON(validMetadata), nil}}, + args{"foo"}, false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.mgr.Command(tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("Manager.Command() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got == nil { + t.Error("Manager.Command() want non-nil cmd") + } + }) + } +} diff --git a/plugin/metadata.go b/plugin/metadata.go new file mode 100644 index 00000000..9d993a8a --- /dev/null +++ b/plugin/metadata.go @@ -0,0 +1,58 @@ +package plugin + +import "errors" + +// Metadata provided by the plugin. +type Metadata struct { + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + URL string `json:"url"` + SupportedContractVersions []string `json:"supported-contract-versions"` + Capabilities []string `json:"capabilities"` +} + +// Validate checks if the metadata is correctly populated. +func (m *Metadata) Validate() error { + if m.Name == "" { + return errors.New("name must not be empty") + } + if m.Description == "" { + return errors.New("description name must not be empty") + } + if m.Version == "" { + return errors.New("version must not be empty") + } + if m.URL == "" { + return errors.New("url must not be empty") + } + if len(m.Capabilities) == 0 { + return errors.New("capabilities must not be empty") + } + if len(m.SupportedContractVersions) == 0 { + return errors.New("supported contract versions must not be empty") + } + return nil +} + +// HasCapability return true if the metadata states that the +// capability is supported. +func (m *Metadata) HasCapability(capability string) bool { + for _, c := range m.Capabilities { + if c == capability { + return true + } + } + return false +} + +// SupportsContract return true if the metadata states that the +// major contract version is supported. +func (m *Metadata) SupportsContract(major string) bool { + for _, v := range m.SupportedContractVersions { + if v == major { + return true + } + } + return false +} diff --git a/plugin/metadata_test.go b/plugin/metadata_test.go new file mode 100644 index 00000000..aa7bb26a --- /dev/null +++ b/plugin/metadata_test.go @@ -0,0 +1,75 @@ +package plugin + +import ( + "strconv" + "testing" +) + +func TestMetadata_Validate(t *testing.T) { + tests := []struct { + m *Metadata + wantErr bool + }{ + {&Metadata{}, true}, + {&Metadata{Name: "name"}, true}, + {&Metadata{Name: "name", Description: "friendly"}, true}, + {&Metadata{Name: "name", Description: "friendly", Version: "1"}, true}, + {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com"}, true}, + {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", Capabilities: []string{"cap"}}, true}, + {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}}, true}, + {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}}, false}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + if err := tt.m.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Metadata.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestMetadata_HasCapability(t *testing.T) { + type args struct { + capability string + } + tests := []struct { + name string + m *Metadata + args args + want bool + }{ + {"empty capabilities", &Metadata{}, args{"cap"}, false}, + {"other capabilities", &Metadata{Capabilities: []string{"foo", "baz"}}, args{"cap"}, false}, + {"found", &Metadata{Capabilities: []string{"foo", "baz"}}, args{"baz"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.m.HasCapability(tt.args.capability); got != tt.want { + t.Errorf("Metadata.HasCapability() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMetadata_SupportsContract(t *testing.T) { + type args struct { + major string + } + tests := []struct { + name string + m *Metadata + args args + want bool + }{ + {"empty versions", &Metadata{}, args{"2"}, false}, + {"other versions", &Metadata{SupportedContractVersions: []string{"1", "2"}}, args{"3"}, false}, + {"found", &Metadata{SupportedContractVersions: []string{"1", "2"}}, args{"2"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.m.SupportsContract(tt.args.major); got != tt.want { + t.Errorf("Metadata.SupportsContract() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 00000000..a9d0e2b5 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,56 @@ +package plugin + +import ( + "encoding/json" + "errors" + "fmt" + "runtime" +) + +// Plugin represents a potential plugin with all it's metadata. +type Plugin struct { + Metadata + + Path string `json:",omitempty"` + + // Err is non-nil if the plugin failed one of the candidate tests. + Err error `json:",omitempty"` +} + +// ErrNotFound by Manager.Get when the plugin is not found. +var ErrNotFound = errors.New("plugin not found") + +// newPlugin determines if the given candidate is valid +// and returns a Plugin. +func newPlugin(cmder commander, binPath, name string) *Plugin { + p := &Plugin{ + Path: addExeSuffix(binPath), + } + + meta, err := cmder.Output(p.Path, "get-plugin-metadata") + if err != nil { + p.Err = fmt.Errorf("failed to fetch metadata: %w", err) + return p + } + + if err := json.Unmarshal(meta, &p.Metadata); err != nil { + p.Err = fmt.Errorf("metadata can't be decoded: %w", err) + return p + } + if p.Name != name { + p.Err = fmt.Errorf("metadata name %q is not valid, must be %q", name, p.Name) + return p + } + if err := p.Metadata.Validate(); err != nil { + p.Err = fmt.Errorf("invalid metadata: %w", err) + return p + } + return p +} + +func addExeSuffix(s string) string { + if runtime.GOOS == "windows" { + s += ".exe" + } + return s +} diff --git a/plugin/testdata/main.go b/plugin/testdata/main.go new file mode 100644 index 00000000..43fc9c37 --- /dev/null +++ b/plugin/testdata/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "encoding/json" + "flag" + "os" +) + +var fail = flag.Bool("fail", false, "") + +func main() { + flag.Parse() + if *fail { + os.Exit(1) + } + if flag.NArg() < 1 { + os.Exit(1) + } + if flag.Arg(0) == "get-plugin-metadata" { + // This does not import notation-go/plugin to simplify testing setup. + m := struct { + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + URL string `json:"url"` + SupportedContractVersions []string `json:"supported-contract-versions"` + Capabilities []string `json:"capabilities"` + }{Name: "foo", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}} + data, err := json.Marshal(&m) + if err != nil { + panic(err) + } + os.Stdout.Write(data) + } +} From 2943a57c1ac60c2f0ff9a927e139db3a81caf5cf Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 26 Apr 2022 20:59:20 +0200 Subject: [PATCH 02/58] ignore symlinks Signed-off-by: qmuntal --- plugin/manager.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugin/manager.go b/plugin/manager.go index 0108bf93..d3fd7b72 100644 --- a/plugin/manager.go +++ b/plugin/manager.go @@ -89,12 +89,10 @@ func (mgr *Manager) isCandidate(name string) (fullname string, ok bool) { // (e.g. due to permissions or anything else). return "", false } - switch fi.Mode().Type() { - case 0, fs.ModeSymlink: - // Regular file or symlink, keep going. + if fi.Mode().Type() == 0 { + // Regular file, keep going. return fullname, true - default: - // Something else, ignore. - return "", false } + // Something else, ignore. + return "", false } From c33c35423586b9d7e64c062cf1ac7763d097e879 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 27 Apr 2022 09:08:25 +0200 Subject: [PATCH 03/58] fix Manager.Command Signed-off-by: qmuntal --- plugin/integration_test.go | 150 ++++++++++++++------------- plugin/manager.go | 204 +++++++++++++++++++------------------ plugin/metadata.go | 116 ++++++++++----------- plugin/plugin.go | 112 ++++++++++---------- plugin/testdata/main.go | 65 ++++++------ 5 files changed, 329 insertions(+), 318 deletions(-) diff --git a/plugin/integration_test.go b/plugin/integration_test.go index 468bee41..5bf0a381 100644 --- a/plugin/integration_test.go +++ b/plugin/integration_test.go @@ -1,71 +1,79 @@ -package plugin - -import ( - "io" - "os" - "os/exec" - "path/filepath" - "reflect" - "testing" -) - -func preparePlugin(t *testing.T) string { - root := t.TempDir() - src, err := os.Open("./testdata/main.go") - if err != nil { - t.Fatal(err) - } - defer src.Close() - - dst, err := os.Create(filepath.Join(root, "main.go")) - if err != nil { - t.Fatal(err) - } - defer dst.Close() - _, err = io.Copy(dst, src) - if err != nil { - t.Fatal(err) - } - err = os.WriteFile(filepath.Join(root, "go.mod"), []byte("module main"), 0666) - if err != nil { - t.Fatal(err) - } - err = os.Mkdir(filepath.Join(root, "foo"), 0755) - if err != nil { - t.Fatal(err) - } - out := filepath.Join(root, "foo", "notation-foo") - out = addExeSuffix(out) - cmd := exec.Command("go", "build", "-o", out) - cmd.Dir = root - err = cmd.Run() - if err != nil { - t.Fatal(err) - } - return root -} - -func TestIntegration(t *testing.T) { - if _, err := exec.LookPath("go"); err != nil { - t.Skip() - } - root := preparePlugin(t) - mgr := &Manager{os.DirFS(root), rootedCommander{root}} - p, err := mgr.Get("foo") - if err != nil { - t.Fatal(err) - } - if p.Err != nil { - t.Fatal(p.Err) - } - list, err := mgr.List() - if err != nil { - t.Fatal(err) - } - if len(list) != 1 { - t.Fatalf("Manager.List() len got %d, want 1", len(list)) - } - if !reflect.DeepEqual(list[0].Metadata, p.Metadata) { - t.Errorf("Manager.List() got %v, want %v", list[0], p) - } -} +package plugin + +import ( + "io" + "os" + "os/exec" + "path/filepath" + "reflect" + "testing" +) + +func preparePlugin(t *testing.T) string { + root := t.TempDir() + src, err := os.Open("./testdata/main.go") + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(filepath.Join(root, "main.go")) + if err != nil { + t.Fatal(err) + } + defer dst.Close() + _, err = io.Copy(dst, src) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(filepath.Join(root, "go.mod"), []byte("module main"), 0666) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir(filepath.Join(root, "foo"), 0755) + if err != nil { + t.Fatal(err) + } + out := filepath.Join(root, "foo", "notation-foo") + out = addExeSuffix(out) + cmd := exec.Command("go", "build", "-o", out) + cmd.Dir = root + err = cmd.Run() + if err != nil { + t.Fatal(err) + } + return root +} + +func TestIntegration(t *testing.T) { + if _, err := exec.LookPath("go"); err != nil { + t.Skip() + } + root := preparePlugin(t) + mgr := &Manager{rootedFS{os.DirFS(root), root}, execCommander{}} + p, err := mgr.Get("foo") + if err != nil { + t.Fatal(err) + } + if p.Err != nil { + t.Fatal(p.Err) + } + list, err := mgr.List() + if err != nil { + t.Fatal(err) + } + if len(list) != 1 { + t.Fatalf("Manager.List() len got %d, want 1", len(list)) + } + if !reflect.DeepEqual(list[0].Metadata, p.Metadata) { + t.Errorf("Manager.List() got %v, want %v", list[0], p) + } + cmd, err := mgr.Command("foo", "other") + if err != nil { + t.Fatal(err) + } + err = cmd.Run() + if err != nil { + t.Fatal(err) + } +} diff --git a/plugin/manager.go b/plugin/manager.go index d3fd7b72..e32a3b65 100644 --- a/plugin/manager.go +++ b/plugin/manager.go @@ -1,98 +1,106 @@ -package plugin - -import ( - "io/fs" - "os" - "os/exec" - "path" -) - -// commander is defined for mocking purposes. -type commander interface { - Output(string, ...string) ([]byte, error) -} - -type rootedCommander struct { - root string -} - -func (c rootedCommander) Output(name string, args ...string) ([]byte, error) { - cmd := &exec.Cmd{ - Path: path.Join(c.root, name), - Args: append([]string{path.Base(name)}, args...), - } - return cmd.Output() -} - -// Manager manages plugins installed on the system. -type Manager struct { - fsys fs.FS - cmder commander -} - -// NewManager returns a new manager. -func NewManager() (*Manager, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, err - } - pluginDir := path.Join(homeDir, ".notation", "plugins") - return &Manager{os.DirFS(pluginDir), rootedCommander{pluginDir}}, nil -} - -// Get returns a plugin on the system by its name. -// The plugin might be incomplete if p.Err is not nil. -func (mgr *Manager) Get(name string) (*Plugin, error) { - fullname, ok := mgr.isCandidate(name) - if !ok { - return nil, ErrNotFound - } - p := newPlugin(mgr.cmder, fullname, name) - return p, nil -} - -// List produces a list of the plugins available on the system. -// Some plugins might be incomplete if their Err is not nil. -func (mgr *Manager) List() ([]*Plugin, error) { - var plugins []*Plugin - fs.WalkDir(mgr.fsys, ".", func(dir string, d fs.DirEntry, _ error) error { - if dir == "." || !d.IsDir() { - return nil - } - p, err := mgr.Get(d.Name()) - if err == nil { - plugins = append(plugins, p) - } - return fs.SkipDir - }) - return plugins, nil -} - -// Command returns an "os/exec".Cmd which when .Run() will execute the named plugin. -// The error returned is ErrNotFound if no plugin was found. -func (mgr *Manager) Command(name string, args ...string) (*exec.Cmd, error) { - p, err := mgr.Get(name) - if err != nil { - return nil, err - } - if p.Err != nil { - return nil, p.Err - } - return exec.Command(p.Path, args...), nil -} - -func (mgr *Manager) isCandidate(name string) (fullname string, ok bool) { - fullname = path.Join(name, "notation-"+name) - fi, err := fs.Stat(mgr.fsys, addExeSuffix(fullname)) - if err != nil { - // Ignore any file which we cannot Stat - // (e.g. due to permissions or anything else). - return "", false - } - if fi.Mode().Type() == 0 { - // Regular file, keep going. - return fullname, true - } - // Something else, ignore. - return "", false -} +package plugin + +import ( + "io/fs" + "os" + "os/exec" + "path" + "path/filepath" +) + +// commander is defined for mocking purposes. +type commander interface { + Output(string, ...string) ([]byte, error) +} + +type execCommander struct { + root string +} + +func (c execCommander) Output(name string, args ...string) ([]byte, error) { + cmd := &exec.Cmd{ + Path: name, + Args: append([]string{name}, args...), + } + return cmd.Output() +} + +type rootedFS struct { + fs.FS + root string +} + +// Manager manages plugins installed on the system. +type Manager struct { + fsys fs.FS + cmder commander +} + +// NewManager returns a new manager. +func NewManager() (*Manager, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + pluginDir := path.Join(homeDir, ".notation", "plugins") + return &Manager{rootedFS{os.DirFS(pluginDir), pluginDir}, execCommander{}}, nil +} + +// Get returns a plugin on the system by its name. +// The plugin might be incomplete if p.Err is not nil. +func (mgr *Manager) Get(name string) (*Plugin, error) { + binPath, ok := mgr.isCandidate(name) + if !ok { + return nil, ErrNotFound + } + p := newPlugin(mgr.cmder, binPath, name) + return p, nil +} + +// List produces a list of the plugins available on the system. +// Some plugins might be incomplete if their Err is not nil. +func (mgr *Manager) List() ([]*Plugin, error) { + var plugins []*Plugin + fs.WalkDir(mgr.fsys, ".", func(dir string, d fs.DirEntry, _ error) error { + if dir == "." || !d.IsDir() { + return nil + } + p, err := mgr.Get(d.Name()) + if err == nil { + plugins = append(plugins, p) + } + return fs.SkipDir + }) + return plugins, nil +} + +// Command returns an "os/exec".Cmd which when .Run() will execute the named plugin. +// The error returned is ErrNotFound if no plugin was found. +func (mgr *Manager) Command(name string, args ...string) (*exec.Cmd, error) { + p, err := mgr.Get(name) + if err != nil { + return nil, err + } + if p.Err != nil { + return nil, p.Err + } + return exec.Command(p.Path, args...), nil +} + +func (mgr *Manager) isCandidate(name string) (string, bool) { + base := addExeSuffix("notation-" + name) + fi, err := fs.Stat(mgr.fsys, path.Join(name, base)) + if err != nil { + // Ignore any file which we cannot Stat + // (e.g. due to permissions or anything else). + return "", false + } + if fi.Mode().Type() != 0 { + // Ignore non-regular files. + return "", false + } + if fsys, ok := mgr.fsys.(rootedFS); ok { + return filepath.Join(fsys.root, name, base), true + } + return filepath.Join(name, base), true +} diff --git a/plugin/metadata.go b/plugin/metadata.go index 9d993a8a..bf7e2900 100644 --- a/plugin/metadata.go +++ b/plugin/metadata.go @@ -1,58 +1,58 @@ -package plugin - -import "errors" - -// Metadata provided by the plugin. -type Metadata struct { - Name string `json:"name"` - Description string `json:"description"` - Version string `json:"version"` - URL string `json:"url"` - SupportedContractVersions []string `json:"supported-contract-versions"` - Capabilities []string `json:"capabilities"` -} - -// Validate checks if the metadata is correctly populated. -func (m *Metadata) Validate() error { - if m.Name == "" { - return errors.New("name must not be empty") - } - if m.Description == "" { - return errors.New("description name must not be empty") - } - if m.Version == "" { - return errors.New("version must not be empty") - } - if m.URL == "" { - return errors.New("url must not be empty") - } - if len(m.Capabilities) == 0 { - return errors.New("capabilities must not be empty") - } - if len(m.SupportedContractVersions) == 0 { - return errors.New("supported contract versions must not be empty") - } - return nil -} - -// HasCapability return true if the metadata states that the -// capability is supported. -func (m *Metadata) HasCapability(capability string) bool { - for _, c := range m.Capabilities { - if c == capability { - return true - } - } - return false -} - -// SupportsContract return true if the metadata states that the -// major contract version is supported. -func (m *Metadata) SupportsContract(major string) bool { - for _, v := range m.SupportedContractVersions { - if v == major { - return true - } - } - return false -} +package plugin + +import "errors" + +// Metadata provided by the plugin. +type Metadata struct { + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + URL string `json:"url"` + SupportedContractVersions []string `json:"supported-contract-versions"` + Capabilities []string `json:"capabilities"` +} + +// Validate checks if the metadata is correctly populated. +func (m *Metadata) Validate() error { + if m.Name == "" { + return errors.New("name must not be empty") + } + if m.Description == "" { + return errors.New("description name must not be empty") + } + if m.Version == "" { + return errors.New("version must not be empty") + } + if m.URL == "" { + return errors.New("url must not be empty") + } + if len(m.Capabilities) == 0 { + return errors.New("capabilities must not be empty") + } + if len(m.SupportedContractVersions) == 0 { + return errors.New("supported contract versions must not be empty") + } + return nil +} + +// HasCapability return true if the metadata states that the +// capability is supported. +func (m *Metadata) HasCapability(capability string) bool { + for _, c := range m.Capabilities { + if c == capability { + return true + } + } + return false +} + +// SupportsContract return true if the metadata states that the +// major contract version is supported. +func (m *Metadata) SupportsContract(major string) bool { + for _, v := range m.SupportedContractVersions { + if v == major { + return true + } + } + return false +} diff --git a/plugin/plugin.go b/plugin/plugin.go index a9d0e2b5..0f42a30b 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,56 +1,56 @@ -package plugin - -import ( - "encoding/json" - "errors" - "fmt" - "runtime" -) - -// Plugin represents a potential plugin with all it's metadata. -type Plugin struct { - Metadata - - Path string `json:",omitempty"` - - // Err is non-nil if the plugin failed one of the candidate tests. - Err error `json:",omitempty"` -} - -// ErrNotFound by Manager.Get when the plugin is not found. -var ErrNotFound = errors.New("plugin not found") - -// newPlugin determines if the given candidate is valid -// and returns a Plugin. -func newPlugin(cmder commander, binPath, name string) *Plugin { - p := &Plugin{ - Path: addExeSuffix(binPath), - } - - meta, err := cmder.Output(p.Path, "get-plugin-metadata") - if err != nil { - p.Err = fmt.Errorf("failed to fetch metadata: %w", err) - return p - } - - if err := json.Unmarshal(meta, &p.Metadata); err != nil { - p.Err = fmt.Errorf("metadata can't be decoded: %w", err) - return p - } - if p.Name != name { - p.Err = fmt.Errorf("metadata name %q is not valid, must be %q", name, p.Name) - return p - } - if err := p.Metadata.Validate(); err != nil { - p.Err = fmt.Errorf("invalid metadata: %w", err) - return p - } - return p -} - -func addExeSuffix(s string) string { - if runtime.GOOS == "windows" { - s += ".exe" - } - return s -} +package plugin + +import ( + "encoding/json" + "errors" + "fmt" + "runtime" +) + +// Plugin represents a potential plugin with all it's metadata. +type Plugin struct { + Metadata + + Path string `json:",omitempty"` + + // Err is non-nil if the plugin failed one of the candidate tests. + Err error `json:",omitempty"` +} + +// ErrNotFound by Manager.Get when the plugin is not found. +var ErrNotFound = errors.New("plugin not found") + +// newPlugin determines if the given candidate is valid +// and returns a Plugin. +func newPlugin(cmder commander, binPath, name string) *Plugin { + p := &Plugin{ + Path: binPath, + } + + meta, err := cmder.Output(p.Path, "get-plugin-metadata") + if err != nil { + p.Err = fmt.Errorf("failed to fetch metadata: %w", err) + return p + } + + if err := json.Unmarshal(meta, &p.Metadata); err != nil { + p.Err = fmt.Errorf("metadata can't be decoded: %w", err) + return p + } + if p.Name != name { + p.Err = fmt.Errorf("metadata name %q is not valid, must be %q", name, p.Name) + return p + } + if err := p.Metadata.Validate(); err != nil { + p.Err = fmt.Errorf("invalid metadata: %w", err) + return p + } + return p +} + +func addExeSuffix(s string) string { + if runtime.GOOS == "windows" { + s += ".exe" + } + return s +} diff --git a/plugin/testdata/main.go b/plugin/testdata/main.go index 43fc9c37..ad914036 100644 --- a/plugin/testdata/main.go +++ b/plugin/testdata/main.go @@ -1,35 +1,30 @@ -package main - -import ( - "encoding/json" - "flag" - "os" -) - -var fail = flag.Bool("fail", false, "") - -func main() { - flag.Parse() - if *fail { - os.Exit(1) - } - if flag.NArg() < 1 { - os.Exit(1) - } - if flag.Arg(0) == "get-plugin-metadata" { - // This does not import notation-go/plugin to simplify testing setup. - m := struct { - Name string `json:"name"` - Description string `json:"description"` - Version string `json:"version"` - URL string `json:"url"` - SupportedContractVersions []string `json:"supported-contract-versions"` - Capabilities []string `json:"capabilities"` - }{Name: "foo", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}} - data, err := json.Marshal(&m) - if err != nil { - panic(err) - } - os.Stdout.Write(data) - } -} +package main + +import ( + "encoding/json" + "flag" + "os" +) + +func main() { + flag.Parse() + if flag.NArg() < 1 { + os.Exit(1) + } + if flag.Arg(0) == "get-plugin-metadata" { + // This does not import notation-go/plugin to simplify testing setup. + m := struct { + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + URL string `json:"url"` + SupportedContractVersions []string `json:"supported-contract-versions"` + Capabilities []string `json:"capabilities"` + }{Name: "foo", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}} + data, err := json.Marshal(&m) + if err != nil { + panic(err) + } + os.Stdout.Write(data) + } +} From 5bfff3353b2c26c63334976dd22603c46826105b Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 27 Apr 2022 09:15:49 +0200 Subject: [PATCH 04/58] export constants Signed-off-by: qmuntal --- plugin/integration_test.go | 2 +- plugin/manager.go | 2 +- plugin/plugin.go | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/plugin/integration_test.go b/plugin/integration_test.go index 5bf0a381..2b612a98 100644 --- a/plugin/integration_test.go +++ b/plugin/integration_test.go @@ -34,7 +34,7 @@ func preparePlugin(t *testing.T) string { if err != nil { t.Fatal(err) } - out := filepath.Join(root, "foo", "notation-foo") + out := filepath.Join(root, "foo", NamePrefix+"foo") out = addExeSuffix(out) cmd := exec.Command("go", "build", "-o", out) cmd.Dir = root diff --git a/plugin/manager.go b/plugin/manager.go index e32a3b65..d402888c 100644 --- a/plugin/manager.go +++ b/plugin/manager.go @@ -88,7 +88,7 @@ func (mgr *Manager) Command(name string, args ...string) (*exec.Cmd, error) { } func (mgr *Manager) isCandidate(name string) (string, bool) { - base := addExeSuffix("notation-" + name) + base := addExeSuffix(NamePrefix + name) fi, err := fs.Stat(mgr.fsys, path.Join(name, base)) if err != nil { // Ignore any file which we cannot Stat diff --git a/plugin/plugin.go b/plugin/plugin.go index 0f42a30b..42f4c366 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -7,6 +7,16 @@ import ( "runtime" ) +const ( + // NamePrefix is the prefix required on all plugin binary names. + NamePrefix = "notation-" + + // MetadataSubcommandName is the name of the plugin subcommand + // which must be supported by every plugin and returns the + // plugin metadata. + MetadataSubcommandName = "get-plugin-metadata" +) + // Plugin represents a potential plugin with all it's metadata. type Plugin struct { Metadata @@ -27,7 +37,7 @@ func newPlugin(cmder commander, binPath, name string) *Plugin { Path: binPath, } - meta, err := cmder.Output(p.Path, "get-plugin-metadata") + meta, err := cmder.Output(p.Path, MetadataSubcommandName) if err != nil { p.Err = fmt.Errorf("failed to fetch metadata: %w", err) return p From c09b4a1f3c471f7bc0ac8e1988d1063098ff5571 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 28 Apr 2022 09:27:30 +0200 Subject: [PATCH 05/58] pr feedback Signed-off-by: qmuntal --- plugin/manager.go | 9 ++++++--- plugin/metadata.go | 2 +- plugin/testdata/main.go | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugin/manager.go b/plugin/manager.go index d402888c..1af4d6b5 100644 --- a/plugin/manager.go +++ b/plugin/manager.go @@ -13,9 +13,10 @@ type commander interface { Output(string, ...string) ([]byte, error) } -type execCommander struct { - root string -} +// execCommander implements the commander interface +// using exec.Cmd without calling exec.Command() so +// it avoids calling the sometimes-unsafe exec.LookPath. +type execCommander struct{} func (c execCommander) Output(name string, args ...string) ([]byte, error) { cmd := &exec.Cmd{ @@ -25,6 +26,8 @@ func (c execCommander) Output(name string, args ...string) ([]byte, error) { return cmd.Output() } +// rootedFS is io.FS implementation used in NewManager. +// root is the root of the file system tree passed to os.DirFS. type rootedFS struct { fs.FS root string diff --git a/plugin/metadata.go b/plugin/metadata.go index bf7e2900..961bad17 100644 --- a/plugin/metadata.go +++ b/plugin/metadata.go @@ -8,7 +8,7 @@ type Metadata struct { Description string `json:"description"` Version string `json:"version"` URL string `json:"url"` - SupportedContractVersions []string `json:"supported-contract-versions"` + SupportedContractVersions []string `json:"supportedContractVersions"` Capabilities []string `json:"capabilities"` } diff --git a/plugin/testdata/main.go b/plugin/testdata/main.go index ad914036..3411b9fe 100644 --- a/plugin/testdata/main.go +++ b/plugin/testdata/main.go @@ -18,7 +18,7 @@ func main() { Description string `json:"description"` Version string `json:"version"` URL string `json:"url"` - SupportedContractVersions []string `json:"supported-contract-versions"` + SupportedContractVersions []string `json:"supportedContractVersions"` Capabilities []string `json:"capabilities"` }{Name: "foo", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}} data, err := json.Marshal(&m) From e1f2d4924cc07f586cc1d609c4b3567d9aaa3d0a Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 28 Apr 2022 09:37:04 +0200 Subject: [PATCH 06/58] improve error message Signed-off-by: qmuntal --- plugin/manager_test.go | 2 +- plugin/plugin.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin/manager_test.go b/plugin/manager_test.go index bd6b18d5..9e363dcd 100644 --- a/plugin/manager_test.go +++ b/plugin/manager_test.go @@ -90,7 +90,7 @@ func TestManager_Get(t *testing.T) { }, testCommander{metadataJSON(validMetadata), nil}}, args{"baz"}, &Plugin{Metadata: Metadata{Name: "baz"}, Path: addExeSuffix("baz/notation-baz")}, - "", "metadata name \"baz\" is not valid, must be \"foo\"", + "", "executable name must be", }, { "invalid metadata content", diff --git a/plugin/plugin.go b/plugin/plugin.go index 42f4c366..a710c2ff 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "path/filepath" "runtime" ) @@ -48,7 +49,7 @@ func newPlugin(cmder commander, binPath, name string) *Plugin { return p } if p.Name != name { - p.Err = fmt.Errorf("metadata name %q is not valid, must be %q", name, p.Name) + p.Err = fmt.Errorf("executable name must be %q instead of %q", addExeSuffix(NamePrefix+p.Name), filepath.Base(binPath)) return p } if err := p.Metadata.Validate(); err != nil { From 2cacb9f0250d481248a0d2399a46bf1e61ba8485 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 28 Apr 2022 18:58:00 +0200 Subject: [PATCH 07/58] move manager to its own package Signed-off-by: qmuntal --- plugin/{ => manager}/integration_test.go | 2 +- plugin/{ => manager}/manager.go | 2 +- plugin/{ => manager}/manager_test.go | 14 ++++++++------ plugin/{ => manager}/plugin.go | 6 ++++-- plugin/{ => manager}/testdata/main.go | 0 5 files changed, 14 insertions(+), 10 deletions(-) rename plugin/{ => manager}/integration_test.go (98%) rename plugin/{ => manager}/manager.go (99%) rename plugin/{ => manager}/manager_test.go (94%) rename plugin/{ => manager}/plugin.go (95%) rename plugin/{ => manager}/testdata/main.go (100%) diff --git a/plugin/integration_test.go b/plugin/manager/integration_test.go similarity index 98% rename from plugin/integration_test.go rename to plugin/manager/integration_test.go index 2b612a98..706b7862 100644 --- a/plugin/integration_test.go +++ b/plugin/manager/integration_test.go @@ -1,4 +1,4 @@ -package plugin +package manager import ( "io" diff --git a/plugin/manager.go b/plugin/manager/manager.go similarity index 99% rename from plugin/manager.go rename to plugin/manager/manager.go index 1af4d6b5..fd963f19 100644 --- a/plugin/manager.go +++ b/plugin/manager/manager.go @@ -1,4 +1,4 @@ -package plugin +package manager import ( "io/fs" diff --git a/plugin/manager_test.go b/plugin/manager/manager_test.go similarity index 94% rename from plugin/manager_test.go rename to plugin/manager/manager_test.go index 9e363dcd..689fdf5c 100644 --- a/plugin/manager_test.go +++ b/plugin/manager/manager_test.go @@ -1,4 +1,4 @@ -package plugin +package manager import ( "encoding/json" @@ -8,6 +8,8 @@ import ( "strings" "testing" "testing/fstest" + + "github.com/notaryproject/notation-go/plugin" ) type testCommander struct { @@ -19,7 +21,7 @@ func (t testCommander) Output(string, ...string) ([]byte, error) { return t.output, t.err } -var validMetadata = Metadata{ +var validMetadata = plugin.Metadata{ Name: "foo", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}, } @@ -89,7 +91,7 @@ func TestManager_Get(t *testing.T) { addExeSuffix("baz/notation-baz"): new(fstest.MapFile), }, testCommander{metadataJSON(validMetadata), nil}}, args{"baz"}, - &Plugin{Metadata: Metadata{Name: "baz"}, Path: addExeSuffix("baz/notation-baz")}, + &Plugin{Metadata: plugin.Metadata{Name: "baz"}, Path: addExeSuffix("baz/notation-baz")}, "", "executable name must be", }, { @@ -97,9 +99,9 @@ func TestManager_Get(t *testing.T) { &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(Metadata{Name: "foo"}), nil}}, + }, testCommander{metadataJSON(plugin.Metadata{Name: "foo"}), nil}}, args{"foo"}, - &Plugin{Metadata: Metadata{Name: "foo"}, Path: addExeSuffix("foo/notation-foo")}, + &Plugin{Metadata: plugin.Metadata{Name: "foo"}, Path: addExeSuffix("foo/notation-foo")}, "", "invalid metadata", }, { @@ -144,7 +146,7 @@ func TestManager_Get(t *testing.T) { } } -func metadataJSON(m Metadata) []byte { +func metadataJSON(m plugin.Metadata) []byte { d, err := json.Marshal(m) if err != nil { panic(err) diff --git a/plugin/plugin.go b/plugin/manager/plugin.go similarity index 95% rename from plugin/plugin.go rename to plugin/manager/plugin.go index a710c2ff..417747f9 100644 --- a/plugin/plugin.go +++ b/plugin/manager/plugin.go @@ -1,4 +1,4 @@ -package plugin +package manager import ( "encoding/json" @@ -6,6 +6,8 @@ import ( "fmt" "path/filepath" "runtime" + + "github.com/notaryproject/notation-go/plugin" ) const ( @@ -20,7 +22,7 @@ const ( // Plugin represents a potential plugin with all it's metadata. type Plugin struct { - Metadata + plugin.Metadata Path string `json:",omitempty"` diff --git a/plugin/testdata/main.go b/plugin/manager/testdata/main.go similarity index 100% rename from plugin/testdata/main.go rename to plugin/manager/testdata/main.go From 55fc95aeb16e74f64f60cb0b9176d58079770269 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 28 Apr 2022 20:27:36 +0200 Subject: [PATCH 08/58] change metadata error messages Signed-off-by: qmuntal --- plugin/metadata.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/metadata.go b/plugin/metadata.go index 961bad17..1c3c2c13 100644 --- a/plugin/metadata.go +++ b/plugin/metadata.go @@ -15,22 +15,22 @@ type Metadata struct { // Validate checks if the metadata is correctly populated. func (m *Metadata) Validate() error { if m.Name == "" { - return errors.New("name must not be empty") + return errors.New("empty name") } if m.Description == "" { - return errors.New("description name must not be empty") + return errors.New("empty description") } if m.Version == "" { - return errors.New("version must not be empty") + return errors.New("empty version") } if m.URL == "" { - return errors.New("url must not be empty") + return errors.New("empty url") } if len(m.Capabilities) == 0 { - return errors.New("capabilities must not be empty") + return errors.New("empty capabilities") } if len(m.SupportedContractVersions) == 0 { - return errors.New("supported contract versions must not be empty") + return errors.New("empty supported contract versions") } return nil } From f0636e164767948b697e52724180cb34e183b657 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 29 Apr 2022 17:28:53 +0200 Subject: [PATCH 09/58] improve the plugin manager interface Signed-off-by: qmuntal --- plugin/errors.go | 76 ++++++++++ plugin/errors_test.go | 90 ++++++++++++ plugin/manager/integration_test.go | 13 +- plugin/manager/manager.go | 203 ++++++++++++++++++++++----- plugin/manager/manager_test.go | 216 ++++++++++++++++++----------- plugin/manager/plugin.go | 69 --------- plugin/metadata.go | 18 ++- plugin/metadata_test.go | 11 +- plugin/plugin.go | 160 +++++++++++++++++++++ plugin/plugin_test.go | 72 ++++++++++ 10 files changed, 725 insertions(+), 203 deletions(-) create mode 100644 plugin/errors.go create mode 100644 plugin/errors_test.go delete mode 100644 plugin/manager/plugin.go create mode 100644 plugin/plugin.go create mode 100644 plugin/plugin_test.go diff --git a/plugin/errors.go b/plugin/errors.go new file mode 100644 index 00000000..02ac900e --- /dev/null +++ b/plugin/errors.go @@ -0,0 +1,76 @@ +package plugin + +import ( + "encoding/json" + "errors" + "fmt" +) + +var ErrUnknownCommand = errors.New("not a plugin command") + +type ErrorCode string + +const ( + // Any of the required request fields was empty, + // or a value was malformed/invalid. + ErrorCodeValidation ErrorCode = "VALIDATION_ERROR" + + // The contract version used in the request is unsupported. + ErrorCodeUnsupportedContractVersion ErrorCode = "UNSUPPORTED_CONTRACT_VERSION" + + // Authentication/authorization error to use given key. + ErrorCodeAccessDenied ErrorCode = "ACCESS_DENIED" + + // The operation to generate signature timed out + // and can be retried by Notation. + ErrorCodeTimeout ErrorCode = "TIMEOUT" + + // The operation to generate signature was throttles + // and can be retried by Notation. + ErrorCodeThrottled ErrorCode = "THROTTLED" + + // Any general error that does not fall into any categories. + ErrorCodeGeneric ErrorCode = "ERROR" +) + +type jsonErr struct { + Code ErrorCode `json:"errorCode"` + Message string `json:"errorMessage,omitempty"` + Metadata map[string]string `json:"errorMetadata,omitempty"` +} + +// RequestError is the common error response for any request. +type RequestError struct { + Code ErrorCode + Err error + Metadata map[string]string +} + +func (e RequestError) Error() string { + return fmt.Sprintf("%s: %v", e.Code, e.Err) +} + +func (e RequestError) Unwrap() error { + return e.Err +} + +func (e RequestError) MarshalJSON() ([]byte, error) { + var msg string + if e.Err != nil { + msg = e.Err.Error() + } + return json.Marshal(jsonErr{e.Code, msg, e.Metadata}) +} + +func (e *RequestError) UnmarshalJSON(data []byte) error { + var tmp jsonErr + err := json.Unmarshal(data, &tmp) + if err != nil { + return err + } + if tmp.Code == "" && tmp.Message == "" && tmp.Metadata == nil { + return errors.New("incomplete json") + } + *e = RequestError{tmp.Code, errors.New(tmp.Message), tmp.Metadata} + return nil +} diff --git a/plugin/errors_test.go b/plugin/errors_test.go new file mode 100644 index 00000000..4a9ffdce --- /dev/null +++ b/plugin/errors_test.go @@ -0,0 +1,90 @@ +package plugin + +import ( + "encoding/json" + "errors" + "reflect" + "testing" +) + +func TestRequestError_Error(t *testing.T) { + err := RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("an error")} + want := string(ErrorCodeAccessDenied) + ": an error" + if got := err.Error(); got != want { + t.Errorf("RequestError.Error() = %v, want %v", got, want) + } +} + +func TestRequestError_Unwrap(t *testing.T) { + want := errors.New("an error") + got := RequestError{Code: ErrorCodeAccessDenied, Err: want}.Unwrap() + if got != want { + t.Errorf("RequestError.Unwrap() = %v, want %v", got, want) + } +} + +func TestRequestError_MarshalJSON(t *testing.T) { + tests := []struct { + name string + e RequestError + want []byte + }{ + {"empty", RequestError{}, []byte("{\"errorCode\":\"\"}")}, + {"with code", RequestError{Code: ErrorCodeAccessDenied}, []byte("{\"errorCode\":\"ACCESS_DENIED\"}")}, + {"with message", RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("failed")}, []byte("{\"errorCode\":\"ACCESS_DENIED\",\"errorMessage\":\"failed\"}")}, + { + "with metadata", + RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("failed"), Metadata: map[string]string{"a": "b"}}, + []byte("{\"errorCode\":\"ACCESS_DENIED\",\"errorMessage\":\"failed\",\"errorMetadata\":{\"a\":\"b\"}}"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.e.MarshalJSON() + if err != nil { + t.Fatalf("RequestError.MarshalJSON() error = %v, wantErr false", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("RequestError.MarshalJSON() = %s, want %s", got, tt.want) + } + if tt.e.Code == "" { + return + } + var got1 RequestError + err = json.Unmarshal(got, &got1) + if err != nil { + t.Fatalf("RequestError.UnmarshalJSON() error = %v, wantErr false", err) + } + if got1.Code != tt.e.Code || !reflect.DeepEqual(got1.Metadata, tt.e.Metadata) { + t.Fatalf("RequestError.UnmarshalJSON() = %s, want %s", got1, tt.e) + } + }) + } +} + +func TestRequestError_UnmarshalJSON(t *testing.T) { + type args struct { + data []byte + } + tests := []struct { + name string + args args + want RequestError + wantErr bool + }{ + {"invalid", args{[]byte("")}, RequestError{}, true}, + {"empty", args{[]byte("{}")}, RequestError{}, true}, + {"with code", args{[]byte("{\"errorCode\":\"ACCESS_DENIED\"}")}, RequestError{Code: ErrorCodeAccessDenied}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var e RequestError + if err := e.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr { + t.Errorf("RequestError.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && (e.Code != tt.want.Code || !reflect.DeepEqual(e.Metadata, tt.want.Metadata)) { + t.Fatalf("RequestError.UnmarshalJSON() = %s, want %s", e, tt.want) + } + }) + } +} diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index 706b7862..acc9f0fe 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -1,12 +1,15 @@ package manager import ( + "context" "io" "os" "os/exec" "path/filepath" "reflect" "testing" + + "github.com/notaryproject/notation-go/plugin" ) func preparePlugin(t *testing.T) string { @@ -51,14 +54,14 @@ func TestIntegration(t *testing.T) { } root := preparePlugin(t) mgr := &Manager{rootedFS{os.DirFS(root), root}, execCommander{}} - p, err := mgr.Get("foo") + p, err := mgr.Get(context.Background(), "foo") if err != nil { t.Fatal(err) } if p.Err != nil { t.Fatal(p.Err) } - list, err := mgr.List() + list, err := mgr.List(context.Background()) if err != nil { t.Fatal(err) } @@ -68,11 +71,7 @@ func TestIntegration(t *testing.T) { if !reflect.DeepEqual(list[0].Metadata, p.Metadata) { t.Errorf("Manager.List() got %v, want %v", list[0], p) } - cmd, err := mgr.Command("foo", "other") - if err != nil { - t.Fatal(err) - } - err = cmd.Run() + _, err = mgr.Run(context.Background(), "foo", plugin.CommandGetMetadata) if err != nil { t.Fatal(err) } diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index fd963f19..cf92efe6 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -1,29 +1,55 @@ package manager import ( + "context" + "encoding/json" + "errors" + "fmt" "io/fs" "os" "os/exec" "path" "path/filepath" + "runtime" + + "github.com/notaryproject/notation-go/plugin" +) + +const ( + // NamePrefix is the prefix required on all plugin binary names. + NamePrefix = "notation-" ) +// Plugin represents a potential plugin with all it's metadata. +type Plugin struct { + plugin.Metadata + + Path string `json:",omitempty"` + + // Err is non-nil if the plugin failed one of the candidate tests. + Err error `json:",omitempty"` +} + +// ErrNotFound is returned by Manager.Get and Manager.Run when the plugin is not found. +var ErrNotFound = errors.New("plugin not found") + +// ErrNotCompliant is returned by Manager.Run when the plugin is found but not compliant. +var ErrNotCompliant = errors.New("plugin not compliant") + +// ErrNotCapable is returned by Manager.Run when the plugin is found and compliant, but is missing a necessary capability. +var ErrNotCapable = errors.New("plugin not capable") + // commander is defined for mocking purposes. type commander interface { - Output(string, ...string) ([]byte, error) + Output(context.Context, string, ...string) ([]byte, error) } // execCommander implements the commander interface -// using exec.Cmd without calling exec.Command() so -// it avoids calling the sometimes-unsafe exec.LookPath. +// using exec.Command(). type execCommander struct{} -func (c execCommander) Output(name string, args ...string) ([]byte, error) { - cmd := &exec.Cmd{ - Path: name, - Args: append([]string{name}, args...), - } - return cmd.Output() +func (c execCommander) Output(ctx context.Context, name string, args ...string) ([]byte, error) { + return exec.CommandContext(ctx, name, args...).Output() } // rootedFS is io.FS implementation used in NewManager. @@ -40,35 +66,36 @@ type Manager struct { } // NewManager returns a new manager. -func NewManager() (*Manager, error) { +func NewManager() *Manager { homeDir, err := os.UserHomeDir() if err != nil { - return nil, err + // Lets panic for now. + // Once the config is moved to notation-go we will move this code to + // the config package as a global initialization. + panic(err) } - pluginDir := path.Join(homeDir, ".notation", "plugins") - return &Manager{rootedFS{os.DirFS(pluginDir), pluginDir}, execCommander{}}, nil + pluginDir := filepath.Join(homeDir, ".notation", "plugins") + return &Manager{rootedFS{os.DirFS(pluginDir), pluginDir}, execCommander{}} } // Get returns a plugin on the system by its name. +// +// If the plugin is not found, the error is of type ErrNotFound. // The plugin might be incomplete if p.Err is not nil. -func (mgr *Manager) Get(name string) (*Plugin, error) { - binPath, ok := mgr.isCandidate(name) - if !ok { - return nil, ErrNotFound - } - p := newPlugin(mgr.cmder, binPath, name) - return p, nil +func (mgr *Manager) Get(ctx context.Context, name string) (*Plugin, error) { + return mgr.newPlugin(ctx, name) } // List produces a list of the plugins available on the system. +// // Some plugins might be incomplete if their Err is not nil. -func (mgr *Manager) List() ([]*Plugin, error) { +func (mgr *Manager) List(ctx context.Context) ([]*Plugin, error) { var plugins []*Plugin fs.WalkDir(mgr.fsys, ".", func(dir string, d fs.DirEntry, _ error) error { if dir == "." || !d.IsDir() { return nil } - p, err := mgr.Get(d.Name()) + p, err := mgr.newPlugin(ctx, d.Name()) if err == nil { plugins = append(plugins, p) } @@ -77,33 +104,137 @@ func (mgr *Manager) List() ([]*Plugin, error) { return plugins, nil } -// Command returns an "os/exec".Cmd which when .Run() will execute the named plugin. -// The error returned is ErrNotFound if no plugin was found. -func (mgr *Manager) Command(name string, args ...string) (*exec.Cmd, error) { - p, err := mgr.Get(name) +// Run executes the specified command against the named plugin and waits for it to complete. +// +// When the returned object is not nil, its type is guaranteed to remain always the same for a given Command. +// The type associated to each Command can be found at Command.NewResponse(). +// +// The returned error is nil if: +// - the plugin exists and is valid +// - the plugin supports the capability returned by cmd.Capability() +// - the command runs and exits with a zero exit status +// - the command stdout is a valid json object which can be unmarshal-ed into the object returned by cmd.NewResponse(). +// +// If the plugin is not found, the error is of type ErrNotFound. +// If the plugin metadata is not valid or stdout and stderr can't be decoded into a valid response, the error is of type ErrNotCompliant. +// If the plugin does not have the required capability, the error is of type ErrNotCapable. +// If the command starts but does not complete successfully, the error is of type RequestError wrapping a *exec.ExitError. +// Other error types may be returned for other situations. +func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, args ...string) (interface{}, error) { + p, err := mgr.newPlugin(ctx, name) if err != nil { return nil, err } if p.Err != nil { - return nil, p.Err + return nil, withErr(p.Err, ErrNotCompliant) + } + if c := cmd.Capability(); !p.HasCapability(c) { + return nil, ErrNotCapable + } + return run(ctx, mgr.cmder, p.Path, cmd, args...) +} + +// newPlugin determines if the given candidate is valid and returns a Plugin. +func (mgr *Manager) newPlugin(ctx context.Context, name string) (*Plugin, error) { + ok := isCandidate(mgr.fsys, name) + if !ok { + return nil, ErrNotFound + } + + p := &Plugin{Path: binPath(mgr.fsys, name)} + out, err := run(ctx, mgr.cmder, p.Path, plugin.CommandGetMetadata) + if err != nil { + p.Err = fmt.Errorf("failed to fetch metadata: %w", err) + return p, nil + } + p.Metadata = *out.(*plugin.Metadata) + if p.Name != name { + p.Err = fmt.Errorf("executable name must be %q instead of %q", addExeSuffix(NamePrefix+p.Name), filepath.Base(p.Path)) + } else if err := p.Metadata.Validate(); err != nil { + p.Err = fmt.Errorf("invalid metadata: %w", err) + } + return p, nil +} + +// run executes the command and decodes the response. +func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Command, args ...string) (interface{}, error) { + out, err := cmder.Output(ctx, pluginPath, append([]string{string(cmd)}, args...)...) + if err != nil { + if ee, ok := err.(*exec.ExitError); ok { + var re plugin.RequestError + err = json.Unmarshal(ee.Stderr, &re) + if err == nil { + return nil, re + } + return nil, withErr(plugin.RequestError{Code: plugin.ErrorCodeGeneric, Err: ee}, ErrNotCompliant) + } + return nil, err + } + resp := cmd.NewResponse() + err = json.Unmarshal(out, resp) + if err != nil { + err = fmt.Errorf("failed to decode json response: %w", err) + return nil, withErr(err, ErrNotCompliant) } - return exec.Command(p.Path, args...), nil + return resp, nil } -func (mgr *Manager) isCandidate(name string) (string, bool) { - base := addExeSuffix(NamePrefix + name) - fi, err := fs.Stat(mgr.fsys, path.Join(name, base)) +// isCandidate checks if the named plugin is a valid candidate. +func isCandidate(fsys fs.FS, name string) bool { + base := binName(name) + fi, err := fs.Stat(fsys, path.Join(name, base)) if err != nil { // Ignore any file which we cannot Stat // (e.g. due to permissions or anything else). - return "", false + return false } if fi.Mode().Type() != 0 { // Ignore non-regular files. - return "", false + return false } - if fsys, ok := mgr.fsys.(rootedFS); ok { - return filepath.Join(fsys.root, name, base), true + return true +} + +func binName(name string) string { + return addExeSuffix(NamePrefix + name) +} + +func binPath(fsys fs.FS, name string) string { + base := binName(name) + if fsys, ok := fsys.(rootedFS); ok { + return filepath.Join(fsys.root, name, base) } - return filepath.Join(name, base), true + return filepath.Join(name, base) +} + +func addExeSuffix(s string) string { + if runtime.GOOS == "windows" { + s += ".exe" + } + return s +} + +func withErr(err, other error) error { + return unionError{err: err, other: other} +} + +type unionError struct { + err error + other error +} + +func (u unionError) Error() string { + return fmt.Sprintf("%s: %s", u.other, u.err) +} + +func (u unionError) Is(target error) bool { + return errors.Is(u.other, target) +} + +func (u unionError) As(target interface{}) bool { + return errors.As(u.other, target) +} + +func (u unionError) Unwrap() error { + return u.err } diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 689fdf5c..41a204b4 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -1,9 +1,11 @@ package manager import ( + "context" "encoding/json" "errors" "io/fs" + "os/exec" "reflect" "strings" "testing" @@ -17,13 +19,72 @@ type testCommander struct { err error } -func (t testCommander) Output(string, ...string) ([]byte, error) { +func (t testCommander) Output(context.Context, string, ...string) ([]byte, error) { return t.output, t.err } +type testMultiCommander struct { + output [][]byte + err []error + n int +} + +func (t *testMultiCommander) Output(context.Context, string, ...string) ([]byte, error) { + defer func() { t.n++ }() + return t.output[t.n], t.err[t.n] +} + var validMetadata = plugin.Metadata{ Name: "foo", Description: "friendly", Version: "1", URL: "example.com", - SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}, + SupportedContractVersions: []string{"1"}, Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, +} + +func TestManager_Get_Empty(t *testing.T) { + mgr := &Manager{fstest.MapFS{}, nil} + got, err := mgr.Get(context.Background(), "foo") + if !errors.Is(err, ErrNotFound) { + t.Errorf("Manager.Get() error = %v, want %v", got, ErrNotFound) + } + if got != nil { + t.Errorf("Manager.Get() = %v, want nil", got) + } +} + +func TestManager_Get_NotFound(t *testing.T) { + check := func(got *Plugin, err error) { + t.Helper() + if !errors.Is(err, ErrNotFound) { + t.Errorf("Manager.Get() error = %v, want %v", got, ErrNotFound) + } + if got != nil { + t.Errorf("Manager.Get() = %v, want nil", got) + } + } + ctx := context.Background() + + // empty fsys. + mgr := Manager{fstest.MapFS{}, nil} + check(mgr.Get(ctx, "foo")) + + // plugin directory exists without executable. + mgr = Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + }, nil} + check(mgr.Get(ctx, "foo")) + + // plugin directory exists with symlinked executable. + mgr = Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): &fstest.MapFile{Mode: fs.ModeSymlink}, + }, nil} + check(mgr.Get(ctx, "foo")) + + // valid plugin exists but is not the target. + mgr = Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{metadataJSON(validMetadata), nil}} + check(mgr.Get(ctx, "baz")) } func TestManager_Get(t *testing.T) { @@ -31,48 +92,21 @@ func TestManager_Get(t *testing.T) { name string } tests := []struct { - name string - mgr *Manager - args args - want *Plugin - err string - invalid string + name string + mgr *Manager + args args + want *Plugin + err string }{ - {"empty fsys", &Manager{fstest.MapFS{}, nil}, args{"foo"}, nil, "plugin not found", ""}, - { - "plugin not found", - &Manager{fstest.MapFS{ - "baz": &fstest.MapFile{Mode: fs.ModeDir}, - }, nil}, - args{"foo"}, - nil, "plugin not found", "", - }, - { - "plugin executable does not exists", - &Manager{fstest.MapFS{ - "foo": &fstest.MapFile{Mode: fs.ModeDir}, - }, nil}, - args{"foo"}, - nil, "plugin not found", "", - }, - { - "plugin executable invalid mode", - &Manager{fstest.MapFS{ - "foo": &fstest.MapFile{Mode: fs.ModeDir}, - addExeSuffix("foo/notation-foo"): &fstest.MapFile{Mode: fs.ModeDir}, - }, testCommander{[]byte("content"), nil}}, - args{"foo"}, - nil, "plugin not found", "", - }, { - "discover error", + "command error", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), }, testCommander{nil, errors.New("failed")}}, args{"foo"}, &Plugin{Path: addExeSuffix("foo/notation-foo")}, - "", "failed to fetch metadata", + "failed to fetch metadata", }, { "invalid json", @@ -82,7 +116,7 @@ func TestManager_Get(t *testing.T) { }, testCommander{[]byte("content"), nil}}, args{"foo"}, &Plugin{Path: addExeSuffix("foo/notation-foo")}, - "", "metadata can't be decoded", + "failed to fetch metadata", }, { "invalid metadata name", @@ -91,8 +125,8 @@ func TestManager_Get(t *testing.T) { addExeSuffix("baz/notation-baz"): new(fstest.MapFile), }, testCommander{metadataJSON(validMetadata), nil}}, args{"baz"}, - &Plugin{Metadata: plugin.Metadata{Name: "baz"}, Path: addExeSuffix("baz/notation-baz")}, - "", "executable name must be", + &Plugin{Metadata: validMetadata, Path: addExeSuffix("baz/notation-baz")}, + "executable name must be", }, { "invalid metadata content", @@ -102,7 +136,7 @@ func TestManager_Get(t *testing.T) { }, testCommander{metadataJSON(plugin.Metadata{Name: "foo"}), nil}}, args{"foo"}, &Plugin{Metadata: plugin.Metadata{Name: "foo"}, Path: addExeSuffix("foo/notation-foo")}, - "", "invalid metadata", + "invalid metadata", }, { "valid", @@ -111,36 +145,26 @@ func TestManager_Get(t *testing.T) { addExeSuffix("foo/notation-foo"): new(fstest.MapFile), }, testCommander{metadataJSON(validMetadata), nil}}, args{"foo"}, - &Plugin{Metadata: validMetadata, Path: addExeSuffix("foo/notation-foo")}, "", "", + &Plugin{Metadata: validMetadata, Path: addExeSuffix("foo/notation-foo")}, "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.mgr.Get(tt.args.name) + got, err := tt.mgr.Get(context.Background(), tt.args.name) + if err != nil { + t.Fatalf("Manager.Get() error = %v, want nil", err) + } if tt.err != "" { - if err == nil { - t.Fatalf("Manager.Get() error = nil, want %s", tt.err) - } - if !strings.Contains(err.Error(), tt.err) { - t.Fatalf("Manager.Get() error = %v, want %v", err, tt.err) - } - } else if tt.invalid != "" { - if err != nil { - t.Fatalf("Manager.Get() error = %v, want nil", err) - } - if !strings.Contains(got.Err.Error(), tt.invalid) { - t.Fatalf("Manager.Get() error = %v, want %v", got.Err, tt.invalid) - } - } else { - if err != nil { - t.Fatalf("Manager.Get() error = %v, want nil", err) - } - if got.Err != nil { - t.Fatalf("Manager.Get() error = %v, want nil", got.Err) - } - if !reflect.DeepEqual(got.Metadata, tt.want.Metadata) { - t.Errorf("Manager.Get() = %v, want %v", got, tt.want) + if got.Err == nil { + t.Errorf("Manager.Get() got.Err = nil, want %v", tt.err) + } else if !strings.Contains(got.Err.Error(), tt.err) { + t.Errorf("Manager.Get() got.Err = %v, want %v", got.Err, tt.err) } + } else if got.Err != nil { + t.Errorf("Manager.Get() got.Err = %v, want nil", got.Err) + } + if !reflect.DeepEqual(got.Metadata, tt.want.Metadata) { + t.Errorf("Manager.Get() = %v, want %v", got, tt.want) } }) } @@ -182,7 +206,7 @@ func TestManager_List(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, _ := tt.mgr.List() + got, _ := tt.mgr.List(context.Background()) if len(got) != len(tt.want) { t.Fatalf("Manager.List() len = %v, want len %v", len(got), len(tt.want)) } @@ -195,42 +219,76 @@ func TestManager_List(t *testing.T) { } } -func TestManager_Command(t *testing.T) { +func TestManager_Run(t *testing.T) { + var errExec = errors.New("exec failed") type args struct { name string + cmd plugin.Command } tests := []struct { - name string - mgr *Manager - args args - wantErr bool + name string + mgr *Manager + args args + err error }{ - {"empty fsys", &Manager{fstest.MapFS{}, nil}, args{"foo"}, true}, + {"empty fsys", &Manager{fstest.MapFS{}, nil}, args{"foo", plugin.CommandGenerateSignature}, ErrNotFound}, { "invalid plugin", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), }, testCommander{nil, errors.New("err")}}, - args{"foo"}, true, + args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, + }, + { + "no capability", &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, testCommander{metadataJSON(validMetadata), nil}}, + args{"foo", plugin.CommandGenerateEnvelope}, ErrNotCapable, + }, + { + "exec error", &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), nil}, []error{nil, errExec}, 0}}, + args{"foo", plugin.CommandGenerateSignature}, errExec, + }, + { + "exit error", &Manager{fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), nil}, []error{nil, new(exec.ExitError)}, 0}}, + args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, }, { "valid", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), }, testCommander{metadataJSON(validMetadata), nil}}, - args{"foo"}, false, + args{"foo", plugin.CommandGenerateSignature}, nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.mgr.Command(tt.args.name) - if (err != nil) != tt.wantErr { - t.Errorf("Manager.Command() error = %v, wantErr %v", err, tt.wantErr) - return + got, err := tt.mgr.Run(context.Background(), tt.args.name, tt.args.cmd) + wantErr := tt.err != nil + if (err != nil) != wantErr { + t.Fatalf("Manager.Run() error = %v, wantErr %v", err, wantErr) } - if !tt.wantErr && got == nil { - t.Error("Manager.Command() want non-nil cmd") + if wantErr { + if !errors.Is(err, tt.err) { + t.Fatalf("Manager.Run() error = %v, want %v", err, tt.err) + } + } else if got == nil { + t.Error("Manager.Run() want non-nil output") } }) } } + +func TestNewManager(t *testing.T) { + mgr := NewManager() + if mgr == nil { + t.Error("NewManager() = nil") + } +} diff --git a/plugin/manager/plugin.go b/plugin/manager/plugin.go deleted file mode 100644 index 417747f9..00000000 --- a/plugin/manager/plugin.go +++ /dev/null @@ -1,69 +0,0 @@ -package manager - -import ( - "encoding/json" - "errors" - "fmt" - "path/filepath" - "runtime" - - "github.com/notaryproject/notation-go/plugin" -) - -const ( - // NamePrefix is the prefix required on all plugin binary names. - NamePrefix = "notation-" - - // MetadataSubcommandName is the name of the plugin subcommand - // which must be supported by every plugin and returns the - // plugin metadata. - MetadataSubcommandName = "get-plugin-metadata" -) - -// Plugin represents a potential plugin with all it's metadata. -type Plugin struct { - plugin.Metadata - - Path string `json:",omitempty"` - - // Err is non-nil if the plugin failed one of the candidate tests. - Err error `json:",omitempty"` -} - -// ErrNotFound by Manager.Get when the plugin is not found. -var ErrNotFound = errors.New("plugin not found") - -// newPlugin determines if the given candidate is valid -// and returns a Plugin. -func newPlugin(cmder commander, binPath, name string) *Plugin { - p := &Plugin{ - Path: binPath, - } - - meta, err := cmder.Output(p.Path, MetadataSubcommandName) - if err != nil { - p.Err = fmt.Errorf("failed to fetch metadata: %w", err) - return p - } - - if err := json.Unmarshal(meta, &p.Metadata); err != nil { - p.Err = fmt.Errorf("metadata can't be decoded: %w", err) - return p - } - if p.Name != name { - p.Err = fmt.Errorf("executable name must be %q instead of %q", addExeSuffix(NamePrefix+p.Name), filepath.Base(binPath)) - return p - } - if err := p.Metadata.Validate(); err != nil { - p.Err = fmt.Errorf("invalid metadata: %w", err) - return p - } - return p -} - -func addExeSuffix(s string) string { - if runtime.GOOS == "windows" { - s += ".exe" - } - return s -} diff --git a/plugin/metadata.go b/plugin/metadata.go index 1c3c2c13..08bc66fb 100644 --- a/plugin/metadata.go +++ b/plugin/metadata.go @@ -4,12 +4,12 @@ import "errors" // Metadata provided by the plugin. type Metadata struct { - Name string `json:"name"` - Description string `json:"description"` - Version string `json:"version"` - URL string `json:"url"` - SupportedContractVersions []string `json:"supportedContractVersions"` - Capabilities []string `json:"capabilities"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + URL string `json:"url"` + SupportedContractVersions []string `json:"supportedContractVersions"` + Capabilities []Capability `json:"capabilities"` } // Validate checks if the metadata is correctly populated. @@ -37,7 +37,11 @@ func (m *Metadata) Validate() error { // HasCapability return true if the metadata states that the // capability is supported. -func (m *Metadata) HasCapability(capability string) bool { +// Returns true if capability is empty. +func (m *Metadata) HasCapability(capability Capability) bool { + if capability == "" { + return true + } for _, c := range m.Capabilities { if c == capability { return true diff --git a/plugin/metadata_test.go b/plugin/metadata_test.go index aa7bb26a..31ee43a0 100644 --- a/plugin/metadata_test.go +++ b/plugin/metadata_test.go @@ -15,9 +15,9 @@ func TestMetadata_Validate(t *testing.T) { {&Metadata{Name: "name", Description: "friendly"}, true}, {&Metadata{Name: "name", Description: "friendly", Version: "1"}, true}, {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com"}, true}, - {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", Capabilities: []string{"cap"}}, true}, + {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", Capabilities: []Capability{"cap"}}, true}, {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}}, true}, - {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []string{"cap"}}, false}, + {&Metadata{Name: "name", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []Capability{"cap"}}, false}, } for i, tt := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { @@ -30,7 +30,7 @@ func TestMetadata_Validate(t *testing.T) { func TestMetadata_HasCapability(t *testing.T) { type args struct { - capability string + capability Capability } tests := []struct { name string @@ -39,8 +39,9 @@ func TestMetadata_HasCapability(t *testing.T) { want bool }{ {"empty capabilities", &Metadata{}, args{"cap"}, false}, - {"other capabilities", &Metadata{Capabilities: []string{"foo", "baz"}}, args{"cap"}, false}, - {"found", &Metadata{Capabilities: []string{"foo", "baz"}}, args{"baz"}, true}, + {"other capabilities", &Metadata{Capabilities: []Capability{"foo", "baz"}}, args{"cap"}, false}, + {"empty target capability", &Metadata{Capabilities: []Capability{"foo", "baz"}}, args{""}, true}, + {"found", &Metadata{Capabilities: []Capability{"foo", "baz"}}, args{"baz"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 00000000..aa715c25 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,160 @@ +package plugin + +import ( + "errors" +) + +// NamePrefix is the prefix required on all plugin binary names. +const NamePrefix = "notation-" + +// Command is a CLI command available in the plugin contract. +type Command string + +const ( + // CommandGetMetadata is the name of the plugin command + // which must be supported by every plugin and returns the + // plugin metadata. + CommandGetMetadata Command = "get-plugin-metadata" + + // CommandGenerateSignature is the name of the plugin command + // which must be supported by every plugin that has the + // SIGNATURE_GENERATOR capability. + CommandGenerateSignature Command = "generate-signature" + + // CommandGenerateEnvelope is the name of the plugin command + // which must be supported by every plugin that has the + // SIGNATURE_ENVELOPE_GENERATOR capability. + CommandGenerateEnvelope Command = "generate-envelope" +) + +// Capability returns the capability associated to the command. +func (c Command) Capability() Capability { + switch c { + case CommandGenerateSignature: + return CapabilitySignatureGenerator + case CommandGenerateEnvelope: + return CapabilityEnvelopeGenerator + default: + return "" + } +} + +// Capability returns the response associated to the command. +func (c Command) NewResponse() interface{} { + switch c { + case CommandGetMetadata: + return new(Metadata) + case CommandGenerateSignature: + return new(GenerateSignatureResponse) + case CommandGenerateEnvelope: + return new(GenerateEnvelopeResponse) + default: + return nil + } +} + +// Capability is a feature available in the plugin contract. +type Capability string + +const ( + // CapabilitySignatureGenerator is the name of the capability + // which should support a plugin to support generating signatures. + CapabilitySignatureGenerator Capability = "SIGNATURE_GENERATOR" + + // CapabilityEnvelopeGenerator is the name of the capability + // which should support a plugin to support generating envelope signatures. + CapabilityEnvelopeGenerator Capability = "SIGNATURE_ENVELOPE_GENERATOR" +) + +// GenerateSignatureRequest contains the parameters passed in a generate-signature request. +// All parameters are required. +type GenerateSignatureRequest struct { + ContractVersion string + KeyName string + KeyID string +} + +func (req *GenerateSignatureRequest) Command() Command { + return CommandGenerateSignature +} + +func (req *GenerateSignatureRequest) Validate() error { + if req == nil { + return errors.New("nil request") + } + if req.ContractVersion == "" { + return errors.New("empty contractVersion") + } + if req.KeyName == "" { + return errors.New("empty keyName") + } + if req.KeyID == "" { + return errors.New("empty keyId") + } + return nil +} + +// GenerateSignatureResponse is the response of a generate-signature request. +type GenerateSignatureResponse struct { + // The same key id as passed in the request. + KeyID string `json:"keyId"` + + // Base64 encoded signature. + Signature string `json:"signature"` + + // One of following supported signing algorithms: + // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection + SigningAlgorithm string `json:"signingAlgorithm"` + + // Ordered list of certificates starting with leaf certificate + // and ending with root certificate. + CertificateChain []string `json:"certificateChain"` +} + +// GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. +// All parameters are required. +type GenerateEnvelopeRequest struct { + ContractVersion string + KeyName string + KeyID string + PayloadType string + SignatureEnvelopeType string +} + +func (req *GenerateEnvelopeRequest) Command() Command { + return CommandGenerateEnvelope +} + + +func (req *GenerateEnvelopeRequest) Validate() error { + if req == nil { + return errors.New("nil request") + } + if req.ContractVersion == "" { + return errors.New("empty contractVersion") + } + if req.KeyName == "" { + return errors.New("empty keyName") + } + if req.KeyID == "" { + return errors.New("empty keyId") + } + if req.PayloadType == "" { + return errors.New("empty payloadType") + } + if req.SignatureEnvelopeType == "" { + return errors.New("empty envelopeType") + } + return nil +} + +// GenerateSignatureResponse is the response of a generate-envelop request. +type GenerateEnvelopeResponse struct { + // Base64 encoded signature envelope. + SignatureEnvelope string `json:"signatureEnvelope"` + + SignatureEnvelopeType string `json:"signatureEnvelopeType"` + + // Annotations to be appended to Signature Manifest annotations. + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go new file mode 100644 index 00000000..b6133b3d --- /dev/null +++ b/plugin/plugin_test.go @@ -0,0 +1,72 @@ +package plugin + +import ( + "reflect" + "testing" +) + +func TestGenerateSignatureRequest_Validate(t *testing.T) { + tests := []struct { + name string + req *GenerateSignatureRequest + wantErr bool + }{ + {"nil", nil, true}, + {"empty", &GenerateSignatureRequest{"", "", ""}, true}, + {"missing version", &GenerateSignatureRequest{"", "2", "3"}, true}, + {"missing key name", &GenerateSignatureRequest{"1", "", "3"}, true}, + {"missing key id", &GenerateSignatureRequest{"1", "2", ""}, true}, + {"valid", &GenerateSignatureRequest{"1", "2", "3"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.Validate(); (err != nil) != tt.wantErr { + t.Errorf("GenerateSignatureRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGenerateEnvelopeRequest_Validate(t *testing.T) { + tests := []struct { + name string + req *GenerateEnvelopeRequest + wantErr bool + }{ + {"nil", nil, true}, + {"empty", &GenerateEnvelopeRequest{"", "", "", "", ""}, true}, + {"missing version", &GenerateEnvelopeRequest{"", "2", "3", "4", "5"}, true}, + {"missing key name", &GenerateEnvelopeRequest{"1", "", "3", "4", "5"}, true}, + {"missing key id", &GenerateEnvelopeRequest{"1", "2", "", "4", "5"}, true}, + {"missing type", &GenerateEnvelopeRequest{"1", "2", "3", "", "5"}, true}, + {"missing envelop", &GenerateEnvelopeRequest{"1", "2", "3", "4", ""}, true}, + {"valid", &GenerateEnvelopeRequest{"1", "2", "3", "4", "5"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.Validate(); (err != nil) != tt.wantErr { + t.Errorf("GenerateEnvelopeRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCommand_NewResponse(t *testing.T) { + tests := []struct { + name string + c Command + want interface{} + }{ + {"empty", "", nil}, + {"metadata", CommandGetMetadata, new(Metadata)}, + {"sign", CommandGenerateSignature, new(GenerateSignatureResponse)}, + {"envelop", CommandGenerateEnvelope, new(GenerateEnvelopeResponse)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.NewResponse(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Command.NewResponse() = %v, want %v", got, tt.want) + } + }) + } +} From b4e505d78df84aa89fa721a44b92c30e76caa163 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Sat, 30 Apr 2022 18:53:53 +0200 Subject: [PATCH 10/58] implement plugin.Run Signed-off-by: qmuntal --- plugin/plugin.go | 26 +++++++ plugin/run.go | 164 +++++++++++++++++++++++++++++++++++++++++++++ plugin/run_test.go | 76 +++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 plugin/run.go create mode 100644 plugin/run_test.go diff --git a/plugin/plugin.go b/plugin/plugin.go index aa715c25..79543209 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -66,6 +66,15 @@ const ( CapabilityEnvelopeGenerator Capability = "SIGNATURE_ENVELOPE_GENERATOR" ) +// Command line argument names used in several requests. +const ( + ArgContractVersion = "--contract-version" + ArgKeyName = "--key-name" + ArgKeyID = "--key-id" + ArgPayloadType = "--payload-type" + ArgSignatureEnvelopeType = "--signature-envelop-type" +) + // GenerateSignatureRequest contains the parameters passed in a generate-signature request. // All parameters are required. type GenerateSignatureRequest struct { @@ -78,6 +87,14 @@ func (req *GenerateSignatureRequest) Command() Command { return CommandGenerateSignature } +func (req *GenerateSignatureRequest) Args() []string { + return []string{ + ArgContractVersion, req.ContractVersion, + ArgKeyName, req.KeyName, + ArgKeyID, req.KeyID, + } +} + func (req *GenerateSignatureRequest) Validate() error { if req == nil { return errors.New("nil request") @@ -125,6 +142,15 @@ func (req *GenerateEnvelopeRequest) Command() Command { return CommandGenerateEnvelope } +func (req *GenerateEnvelopeRequest) Args() []string { + return []string{ + ArgContractVersion, req.ContractVersion, + ArgKeyName, req.KeyName, + ArgKeyID, req.KeyID, + ArgPayloadType, req.PayloadType, + ArgSignatureEnvelopeType, req.SignatureEnvelopeType, + } +} func (req *GenerateEnvelopeRequest) Validate() error { if req == nil { diff --git a/plugin/run.go b/plugin/run.go new file mode 100644 index 00000000..baeff37d --- /dev/null +++ b/plugin/run.go @@ -0,0 +1,164 @@ +package plugin + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "os" +) + +// We use the following variables to allow mocking them in tests. +var ( + stdout = os.Stdout + stderr = os.Stderr +) + +type validator interface { + Validate() error +} + +type RunFunc func(command Command, req interface{}) (interface{}, error) + +func Run(metadata *Metadata, fn RunFunc) error { + return RunWithFlagSet(nil, metadata, fn, os.Args[1:]...) +} + +func RunWithFlagSet(flagset *flag.FlagSet, metadata *Metadata, fn RunFunc, args ...string) error { + if len(args) < 1 { + return ErrUnknownCommand + } + cmd := Command(args[0]) + switch cmd { + default: + // Not one of our commands. + return ErrUnknownCommand + case CommandGetMetadata: + // Fast path. + err := json.NewEncoder(stdout).Encode(metadata) + if err != nil { + return requestErr(err) + } + return nil + case CommandGenerateSignature, + CommandGenerateEnvelope: + // Lets continue. + } + + if c := cmd.Capability(); !metadata.HasCapability(c) { + return requestErr(RequestError{Code: ErrorCodeValidation, Err: errors.New("missing capability: " + string(c))}) + } + + req, err := parseArgs(flagset, cmd, args) + if err != nil { + return requestErr(err) + } + + resp, err := fn(cmd, req) + if err != nil { + return requestErr(err) + } + + err = validateResponse(cmd, resp) + if err != nil { + return requestErr(err) + } + + err = json.NewEncoder(stdout).Encode(resp) + if err != nil { + return requestErr(err) + } + return nil +} + +func parseArgs(flagset *flag.FlagSet, cmd Command, args []string) (validator, error) { + if flagset == nil { + flagset = flag.NewFlagSet(string(cmd), flag.ExitOnError) + } else { + flagset.Init(string(cmd), flagset.ErrorHandling()) + } + var req validator + switch cmd { + case CommandGenerateSignature: + req = generateSignatureFlags(flagset) + case CommandGenerateEnvelope: + req = generateEnvelopFlags(flagset) + default: + panic("unsupported command: " + cmd) + } + flagset.Parse(args[1:]) + err := req.Validate() + if err != nil { + return nil, RequestError{Code: ErrorCodeValidation, Err: fmt.Errorf("input parameters: %w", err)} + } + return req, nil +} + +func validateResponse(cmd Command, resp interface{}) error { + if resp == nil { + return errors.New("nil response") + } + var ok bool + switch cmd { + case CommandGenerateSignature: + _, ok = resp.(*GenerateSignatureResponse) + case CommandGenerateEnvelope: + _, ok = resp.(*GenerateEnvelopeResponse) + default: + panic("unsupported command: " + cmd) + } + if !ok { + return fmt.Errorf("invalid response type: %T", resp) + } + return nil +} + +func requestErr(err error) error { + if _, ok := err.(RequestError); !ok { + err = RequestError{Code: ErrorCodeGeneric, Err: err} + } + json.NewEncoder(stderr).Encode(err) + return err +} + +func generateSignatureFlags(flagset *flag.FlagSet) *GenerateSignatureRequest { + req := new(GenerateSignatureRequest) + flagset.Func(ArgContractVersion[2:], "contract version in the form of ", func(s string) error { + req.ContractVersion = s + return nil + }) + flagset.Func(ArgKeyName[2:], "signing key name", func(s string) error { + req.KeyName = s + return nil + }) + flagset.Func(ArgKeyID[2:], "signing key id", func(s string) error { + req.KeyID = s + return nil + }) + return req +} + +func generateEnvelopFlags(flagset *flag.FlagSet) *GenerateEnvelopeRequest { + req := new(GenerateEnvelopeRequest) + flagset.Func(ArgContractVersion[2:], "contract version in the form of ", func(s string) error { + req.ContractVersion = s + return nil + }) + flagset.Func(ArgKeyName[2:], "signing key name", func(s string) error { + req.KeyName = s + return nil + }) + flagset.Func(ArgKeyID[2:], "signing key id", func(s string) error { + req.KeyID = s + return nil + }) + flagset.Func(ArgPayloadType[2:], "payload type", func(s string) error { + req.PayloadType = s + return nil + }) + flagset.Func(ArgSignatureEnvelopeType[2:], "expected response signature envelope", func(s string) error { + req.SignatureEnvelopeType = s + return nil + }) + return req +} diff --git a/plugin/run_test.go b/plugin/run_test.go new file mode 100644 index 00000000..592f719e --- /dev/null +++ b/plugin/run_test.go @@ -0,0 +1,76 @@ +package plugin + +import ( + "errors" + "io" + "os" + "testing" +) + +var validMetadata = &Metadata{ + Name: "foo", Description: "friendly", Version: "1", URL: "example.com", + SupportedContractVersions: []string{"1"}, Capabilities: []Capability{"other"}, +} + +func withCapability(cap Capability) *Metadata { + m := *validMetadata + m.Capabilities = append(m.Capabilities, cap) + return &m +} + +func runFunc(resp interface{}, err error) RunFunc { + return func(command Command, req interface{}) (interface{}, error) { + return resp, err + } +} + +func TestRunWithFlagSet(t *testing.T) { + signArgs := append([]string{string(new(GenerateSignatureRequest).Command())}, (&GenerateSignatureRequest{"1", "2", "3"}).Args()...) + envelopArgs := append([]string{string(new(GenerateEnvelopeRequest).Command())}, (&GenerateEnvelopeRequest{"1", "2", "3", "4", "5"}).Args()...) + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + go func() { + defer w.Close() + io.Copy(io.Discard, r) + }() + stdout = w + stderr = w + type args struct { + metadata *Metadata + fn RunFunc + args []string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"empty args", args{validMetadata, runFunc(nil, nil), nil}, true}, + {"unknown command", args{validMetadata, runFunc(nil, nil), []string{"not-a-command"}}, true}, + {"metadata", args{validMetadata, runFunc(nil, nil), []string{string(CommandGetMetadata)}}, false}, + {"no sign capability", args{validMetadata, runFunc(nil, nil), signArgs}, true}, + {"invalid sign args", args{withCapability(CapabilitySignatureGenerator), runFunc(nil, nil), []string{string(CommandGenerateSignature)}}, true}, + {"nil response", args{withCapability(CapabilitySignatureGenerator), runFunc(nil, nil), signArgs}, true}, + {"error response", args{withCapability(CapabilitySignatureGenerator), runFunc(nil, errors.New("failed")), signArgs}, true}, + {"invalid response", args{withCapability(CapabilitySignatureGenerator), runFunc(1, nil), signArgs}, true}, + {"valid sign", args{withCapability(CapabilitySignatureGenerator), runFunc(&GenerateSignatureResponse{}, nil), signArgs}, false}, + {"valid envelop", args{withCapability(CapabilityEnvelopeGenerator), runFunc(&GenerateEnvelopeResponse{}, nil), envelopArgs}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := RunWithFlagSet(nil, tt.args.metadata, tt.args.fn, tt.args.args...) + if (err != nil) != tt.wantErr { + t.Fatalf("RunWithFlagSet() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRun(t *testing.T) { + err := Run(validMetadata, runFunc(nil, nil)) + if err == nil { + t.Errorf("Run() error = %v, wantErr true", err) + } +} From 295d0bc10b814c341eda5e7c25fd6bac6be71caa Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 2 May 2022 10:21:01 +0200 Subject: [PATCH 11/58] add plugin signer Signed-off-by: qmuntal --- plugin/run.go | 164 ---------------------------------------- plugin/run_test.go | 76 ------------------- signature/jws/plugin.go | 146 +++++++++++++++++++++++++++++++++++ signature/jws/signer.go | 6 +- 4 files changed, 150 insertions(+), 242 deletions(-) delete mode 100644 plugin/run.go delete mode 100644 plugin/run_test.go create mode 100644 signature/jws/plugin.go diff --git a/plugin/run.go b/plugin/run.go deleted file mode 100644 index baeff37d..00000000 --- a/plugin/run.go +++ /dev/null @@ -1,164 +0,0 @@ -package plugin - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "os" -) - -// We use the following variables to allow mocking them in tests. -var ( - stdout = os.Stdout - stderr = os.Stderr -) - -type validator interface { - Validate() error -} - -type RunFunc func(command Command, req interface{}) (interface{}, error) - -func Run(metadata *Metadata, fn RunFunc) error { - return RunWithFlagSet(nil, metadata, fn, os.Args[1:]...) -} - -func RunWithFlagSet(flagset *flag.FlagSet, metadata *Metadata, fn RunFunc, args ...string) error { - if len(args) < 1 { - return ErrUnknownCommand - } - cmd := Command(args[0]) - switch cmd { - default: - // Not one of our commands. - return ErrUnknownCommand - case CommandGetMetadata: - // Fast path. - err := json.NewEncoder(stdout).Encode(metadata) - if err != nil { - return requestErr(err) - } - return nil - case CommandGenerateSignature, - CommandGenerateEnvelope: - // Lets continue. - } - - if c := cmd.Capability(); !metadata.HasCapability(c) { - return requestErr(RequestError{Code: ErrorCodeValidation, Err: errors.New("missing capability: " + string(c))}) - } - - req, err := parseArgs(flagset, cmd, args) - if err != nil { - return requestErr(err) - } - - resp, err := fn(cmd, req) - if err != nil { - return requestErr(err) - } - - err = validateResponse(cmd, resp) - if err != nil { - return requestErr(err) - } - - err = json.NewEncoder(stdout).Encode(resp) - if err != nil { - return requestErr(err) - } - return nil -} - -func parseArgs(flagset *flag.FlagSet, cmd Command, args []string) (validator, error) { - if flagset == nil { - flagset = flag.NewFlagSet(string(cmd), flag.ExitOnError) - } else { - flagset.Init(string(cmd), flagset.ErrorHandling()) - } - var req validator - switch cmd { - case CommandGenerateSignature: - req = generateSignatureFlags(flagset) - case CommandGenerateEnvelope: - req = generateEnvelopFlags(flagset) - default: - panic("unsupported command: " + cmd) - } - flagset.Parse(args[1:]) - err := req.Validate() - if err != nil { - return nil, RequestError{Code: ErrorCodeValidation, Err: fmt.Errorf("input parameters: %w", err)} - } - return req, nil -} - -func validateResponse(cmd Command, resp interface{}) error { - if resp == nil { - return errors.New("nil response") - } - var ok bool - switch cmd { - case CommandGenerateSignature: - _, ok = resp.(*GenerateSignatureResponse) - case CommandGenerateEnvelope: - _, ok = resp.(*GenerateEnvelopeResponse) - default: - panic("unsupported command: " + cmd) - } - if !ok { - return fmt.Errorf("invalid response type: %T", resp) - } - return nil -} - -func requestErr(err error) error { - if _, ok := err.(RequestError); !ok { - err = RequestError{Code: ErrorCodeGeneric, Err: err} - } - json.NewEncoder(stderr).Encode(err) - return err -} - -func generateSignatureFlags(flagset *flag.FlagSet) *GenerateSignatureRequest { - req := new(GenerateSignatureRequest) - flagset.Func(ArgContractVersion[2:], "contract version in the form of ", func(s string) error { - req.ContractVersion = s - return nil - }) - flagset.Func(ArgKeyName[2:], "signing key name", func(s string) error { - req.KeyName = s - return nil - }) - flagset.Func(ArgKeyID[2:], "signing key id", func(s string) error { - req.KeyID = s - return nil - }) - return req -} - -func generateEnvelopFlags(flagset *flag.FlagSet) *GenerateEnvelopeRequest { - req := new(GenerateEnvelopeRequest) - flagset.Func(ArgContractVersion[2:], "contract version in the form of ", func(s string) error { - req.ContractVersion = s - return nil - }) - flagset.Func(ArgKeyName[2:], "signing key name", func(s string) error { - req.KeyName = s - return nil - }) - flagset.Func(ArgKeyID[2:], "signing key id", func(s string) error { - req.KeyID = s - return nil - }) - flagset.Func(ArgPayloadType[2:], "payload type", func(s string) error { - req.PayloadType = s - return nil - }) - flagset.Func(ArgSignatureEnvelopeType[2:], "expected response signature envelope", func(s string) error { - req.SignatureEnvelopeType = s - return nil - }) - return req -} diff --git a/plugin/run_test.go b/plugin/run_test.go deleted file mode 100644 index 592f719e..00000000 --- a/plugin/run_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package plugin - -import ( - "errors" - "io" - "os" - "testing" -) - -var validMetadata = &Metadata{ - Name: "foo", Description: "friendly", Version: "1", URL: "example.com", - SupportedContractVersions: []string{"1"}, Capabilities: []Capability{"other"}, -} - -func withCapability(cap Capability) *Metadata { - m := *validMetadata - m.Capabilities = append(m.Capabilities, cap) - return &m -} - -func runFunc(resp interface{}, err error) RunFunc { - return func(command Command, req interface{}) (interface{}, error) { - return resp, err - } -} - -func TestRunWithFlagSet(t *testing.T) { - signArgs := append([]string{string(new(GenerateSignatureRequest).Command())}, (&GenerateSignatureRequest{"1", "2", "3"}).Args()...) - envelopArgs := append([]string{string(new(GenerateEnvelopeRequest).Command())}, (&GenerateEnvelopeRequest{"1", "2", "3", "4", "5"}).Args()...) - r, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - go func() { - defer w.Close() - io.Copy(io.Discard, r) - }() - stdout = w - stderr = w - type args struct { - metadata *Metadata - fn RunFunc - args []string - } - tests := []struct { - name string - args args - wantErr bool - }{ - {"empty args", args{validMetadata, runFunc(nil, nil), nil}, true}, - {"unknown command", args{validMetadata, runFunc(nil, nil), []string{"not-a-command"}}, true}, - {"metadata", args{validMetadata, runFunc(nil, nil), []string{string(CommandGetMetadata)}}, false}, - {"no sign capability", args{validMetadata, runFunc(nil, nil), signArgs}, true}, - {"invalid sign args", args{withCapability(CapabilitySignatureGenerator), runFunc(nil, nil), []string{string(CommandGenerateSignature)}}, true}, - {"nil response", args{withCapability(CapabilitySignatureGenerator), runFunc(nil, nil), signArgs}, true}, - {"error response", args{withCapability(CapabilitySignatureGenerator), runFunc(nil, errors.New("failed")), signArgs}, true}, - {"invalid response", args{withCapability(CapabilitySignatureGenerator), runFunc(1, nil), signArgs}, true}, - {"valid sign", args{withCapability(CapabilitySignatureGenerator), runFunc(&GenerateSignatureResponse{}, nil), signArgs}, false}, - {"valid envelop", args{withCapability(CapabilityEnvelopeGenerator), runFunc(&GenerateEnvelopeResponse{}, nil), envelopArgs}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := RunWithFlagSet(nil, tt.args.metadata, tt.args.fn, tt.args.args...) - if (err != nil) != tt.wantErr { - t.Fatalf("RunWithFlagSet() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestRun(t *testing.T) { - err := Run(validMetadata, runFunc(nil, nil)) - if err == nil { - t.Errorf("Run() error = %v, wantErr true", err) - } -} diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go new file mode 100644 index 00000000..a0ee2aec --- /dev/null +++ b/signature/jws/plugin.go @@ -0,0 +1,146 @@ +package jws + +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/golang-jwt/jwt/v4" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/crypto/jwsutil" + "github.com/notaryproject/notation-go/plugin" +) + +var supportedAlgs = map[string]bool{ + jwt.SigningMethodES256.Name: true, + jwt.SigningMethodES384.Name: true, + jwt.SigningMethodES512.Name: true, + jwt.SigningMethodES256.Name: true, + jwt.SigningMethodES384.Name: true, + jwt.SigningMethodES512.Name: true, +} + +type PluginRunner interface { + Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) +} + +type PluginSigner struct { + Runner PluginRunner + PluginName string + KeyID string + KeyName string +} + +func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + + // Generate payload to be signed. + payload := packPayload(desc, opts) + if err := payload.Valid(); err != nil { + return nil, err + } + jsonPayload, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal signing payload: %w", err) + } + + // Execute plugin. + req := plugin.GenerateSignatureRequest{ + ContractVersion: "1", + KeyName: s.KeyName, + KeyID: s.KeyID, + Payload: string(jsonPayload), + } + out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGenerateSignature, req) + if err != nil { + return nil, fmt.Errorf("sign command failed: %w", err) + } + resp, ok := out.(*plugin.GenerateSignatureResponse) + if !ok { + return nil, fmt.Errorf("invalid sign response type %T", resp) + } + + // Check algorithm is supported. + if !supportedAlgs[resp.SigningAlgorithm] { + return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) + } + + // Check payload has not been modified. + sig, err := jwsutil.ParseCompact(resp.Signature) + if err != nil { + return nil, err + } + if sig.Payload != string(jsonPayload) { + return nil, errors.New("signing payload has been modified") + } + + // Verify the hash of the request payload against the response signature + // using the public key of the signing certificate. + certs, err := parseCertChainBase64(resp.CertificateChain) + if err != nil { + return nil, err + } + err = verifyJWT(resp.SigningAlgorithm, string(jsonPayload), resp.Signature, certs) + if err != nil { + return nil, err + } + + // Check the the certificate chain conforms to the spec. + err = checkCertChain(certs) + if err != nil { + return nil, err + } + + // Assemble the JWS signature envelope. + rawCerts := make([][]byte, len(certs)) + for i, c := range certs { + rawCerts[i] = c.Raw + } + return jwtEnvelop(ctx, opts, resp.Signature, rawCerts) +} + +func parseCertChainBase64(certChain []string) ([]*x509.Certificate, error) { + certs := make([]*x509.Certificate, len(certChain)) + for i, data := range certChain { + der, err := base64.RawStdEncoding.DecodeString(data) + if err != nil { + return nil, err + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + certs[i] = cert + } + return certs, nil +} + +func verifyJWT(sigAlg string, payload string, sig string, certChain []*x509.Certificate) error { + if len(certChain) == 0 { + return nil + } + signingCert := certChain[0] + // Verify the hash of req.payload against resp.signature using the public key if the leaf certificate. + method := jwt.GetSigningMethod(sigAlg) + err := method.Verify(payload, sig, signingCert.PublicKey) + return err +} + +func checkCertChain(certChain []*x509.Certificate) error { + if len(certChain) == 0 { + return nil + } + signingCert := certChain[0] + roots := x509.NewCertPool() + roots.AddCert(signingCert) + _, err := signingCert.Verify(x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }) + return err +} diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 0ccbaf79..92f8141d 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -91,7 +91,6 @@ func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notati if err := opts.Validate(); err != nil { return nil, err } - // generate JWT payload := packPayload(desc, opts) if err := payload.Valid(); err != nil { @@ -112,10 +111,13 @@ func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notati if err != nil { return nil, err } + return jwtEnvelop(ctx, opts, compact, s.certChain) +} +func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { // generate unprotected header header := unprotectedHeader{ - CertChain: s.certChain, + CertChain: certChain, } // timestamp JWT From e39e8ce127b4629a7a96ad06d9678dbe7d7b7d2a Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 2 May 2022 10:21:37 +0200 Subject: [PATCH 12/58] improve Manager.Run Signed-off-by: qmuntal --- plugin/manager/integration_test.go | 2 +- plugin/manager/manager.go | 58 ++++++++++++++++++++---------- plugin/manager/manager_test.go | 49 ++++++++++++------------- plugin/plugin.go | 53 ++++++--------------------- plugin/plugin_test.go | 24 ++++++------- 5 files changed, 88 insertions(+), 98 deletions(-) diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index acc9f0fe..d762cb13 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -71,7 +71,7 @@ func TestIntegration(t *testing.T) { if !reflect.DeepEqual(list[0].Metadata, p.Metadata) { t.Errorf("Manager.List() got %v, want %v", list[0], p) } - _, err = mgr.Run(context.Background(), "foo", plugin.CommandGetMetadata) + _, err = mgr.Run(context.Background(), "foo", plugin.CommandGetMetadata, nil) if err != nil { t.Fatal(err) } diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index cf92efe6..3138f406 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -1,6 +1,7 @@ package manager import ( + "bytes" "context" "encoding/json" "errors" @@ -41,15 +42,29 @@ var ErrNotCapable = errors.New("plugin not capable") // commander is defined for mocking purposes. type commander interface { - Output(context.Context, string, ...string) ([]byte, error) + // Output runs the command, passing req to the its stdin. + // It only returns an error if the binary can't be executed. + // Returns stdout if success is true, stderr if success is false. + Output(ctx context.Context, path string, command string, req []byte) (out []byte, success bool, err error) } -// execCommander implements the commander interface -// using exec.Command(). +// execCommander implements the commander interface using exec.Command(). type execCommander struct{} -func (c execCommander) Output(ctx context.Context, name string, args ...string) ([]byte, error) { - return exec.CommandContext(ctx, name, args...).Output() +func (c execCommander) Output(ctx context.Context, name string, command string, req []byte) ([]byte, bool, error) { + var stdout, stderr bytes.Buffer + cmd := exec.CommandContext(ctx, name, command) + cmd.Stdin = bytes.NewReader(req) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if _, ok := err.(*exec.ExitError); err != nil && !ok { + return nil, false, err + } + if !cmd.ProcessState.Success() { + return stderr.Bytes(), false, nil + } + return stdout.Bytes(), true, nil } // rootedFS is io.FS implementation used in NewManager. @@ -120,7 +135,7 @@ func (mgr *Manager) List(ctx context.Context) ([]*Plugin, error) { // If the plugin does not have the required capability, the error is of type ErrNotCapable. // If the command starts but does not complete successfully, the error is of type RequestError wrapping a *exec.ExitError. // Other error types may be returned for other situations. -func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, args ...string) (interface{}, error) { +func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, req interface{}) (interface{}, error) { p, err := mgr.newPlugin(ctx, name) if err != nil { return nil, err @@ -131,7 +146,14 @@ func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, ar if c := cmd.Capability(); !p.HasCapability(c) { return nil, ErrNotCapable } - return run(ctx, mgr.cmder, p.Path, cmd, args...) + var data []byte + if req != nil { + data, err = json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request object: %w", err) + } + } + return run(ctx, mgr.cmder, p.Path, cmd, data) } // newPlugin determines if the given candidate is valid and returns a Plugin. @@ -142,7 +164,7 @@ func (mgr *Manager) newPlugin(ctx context.Context, name string) (*Plugin, error) } p := &Plugin{Path: binPath(mgr.fsys, name)} - out, err := run(ctx, mgr.cmder, p.Path, plugin.CommandGetMetadata) + out, err := run(ctx, mgr.cmder, p.Path, plugin.CommandGetMetadata, nil) if err != nil { p.Err = fmt.Errorf("failed to fetch metadata: %w", err) return p, nil @@ -157,18 +179,18 @@ func (mgr *Manager) newPlugin(ctx context.Context, name string) (*Plugin, error) } // run executes the command and decodes the response. -func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Command, args ...string) (interface{}, error) { - out, err := cmder.Output(ctx, pluginPath, append([]string{string(cmd)}, args...)...) +func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Command, req []byte) (interface{}, error) { + out, ok, err := cmder.Output(ctx, pluginPath, string(cmd), req) if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - var re plugin.RequestError - err = json.Unmarshal(ee.Stderr, &re) - if err == nil { - return nil, re - } - return nil, withErr(plugin.RequestError{Code: plugin.ErrorCodeGeneric, Err: ee}, ErrNotCompliant) + return nil, fmt.Errorf("failed running the plugin: %w", err) + } + if !ok { + var re plugin.RequestError + err = json.Unmarshal(out, &re) + if err != nil { + return nil, withErr(plugin.RequestError{Code: plugin.ErrorCodeGeneric, Err: err}, ErrNotCompliant) } - return nil, err + return nil, re } resp := cmd.NewResponse() err = json.Unmarshal(out, resp) diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 41a204b4..406a3e0b 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "io/fs" - "os/exec" "reflect" "strings" "testing" @@ -15,23 +14,25 @@ import ( ) type testCommander struct { - output []byte - err error + output []byte + success bool + err error } -func (t testCommander) Output(context.Context, string, ...string) ([]byte, error) { - return t.output, t.err +func (t testCommander) Output(ctx context.Context, path string, command string, req []byte) (out []byte, success bool, err error) { + return t.output, t.success, t.err } type testMultiCommander struct { - output [][]byte - err []error - n int + output [][]byte + success []bool + err []error + n int } -func (t *testMultiCommander) Output(context.Context, string, ...string) ([]byte, error) { +func (t *testMultiCommander) Output(ctx context.Context, path string, command string, req []byte) (out []byte, success bool, err error) { defer func() { t.n++ }() - return t.output[t.n], t.err[t.n] + return t.output[t.n], t.success[t.n], t.err[t.n] } var validMetadata = plugin.Metadata{ @@ -83,7 +84,7 @@ func TestManager_Get_NotFound(t *testing.T) { mgr = Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), nil}} + }, testCommander{metadataJSON(validMetadata), true, nil}} check(mgr.Get(ctx, "baz")) } @@ -103,7 +104,7 @@ func TestManager_Get(t *testing.T) { &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{nil, errors.New("failed")}}, + }, testCommander{nil, false, errors.New("failed")}}, args{"foo"}, &Plugin{Path: addExeSuffix("foo/notation-foo")}, "failed to fetch metadata", @@ -113,7 +114,7 @@ func TestManager_Get(t *testing.T) { &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{[]byte("content"), nil}}, + }, testCommander{[]byte("content"), true, nil}}, args{"foo"}, &Plugin{Path: addExeSuffix("foo/notation-foo")}, "failed to fetch metadata", @@ -123,7 +124,7 @@ func TestManager_Get(t *testing.T) { &Manager{fstest.MapFS{ "baz": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("baz/notation-baz"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), nil}}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, args{"baz"}, &Plugin{Metadata: validMetadata, Path: addExeSuffix("baz/notation-baz")}, "executable name must be", @@ -133,7 +134,7 @@ func TestManager_Get(t *testing.T) { &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(plugin.Metadata{Name: "foo"}), nil}}, + }, testCommander{metadataJSON(plugin.Metadata{Name: "foo"}), true, nil}}, args{"foo"}, &Plugin{Metadata: plugin.Metadata{Name: "foo"}, Path: addExeSuffix("foo/notation-foo")}, "invalid metadata", @@ -143,7 +144,7 @@ func TestManager_Get(t *testing.T) { &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), nil}}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, args{"foo"}, &Plugin{Metadata: validMetadata, Path: addExeSuffix("foo/notation-foo")}, "", }, @@ -191,7 +192,7 @@ func TestManager_List(t *testing.T) { fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), nil}}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, []*Plugin{{Metadata: validMetadata}}, }, { @@ -200,7 +201,7 @@ func TestManager_List(t *testing.T) { "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), "baz": &fstest.MapFile{Mode: fs.ModeDir}, - }, testCommander{metadataJSON(validMetadata), nil}}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, []*Plugin{{Metadata: validMetadata}}, }, } @@ -236,41 +237,41 @@ func TestManager_Run(t *testing.T) { "invalid plugin", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{nil, errors.New("err")}}, + }, testCommander{nil, false, errors.New("err")}}, args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, }, { "no capability", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), nil}}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, args{"foo", plugin.CommandGenerateEnvelope}, ErrNotCapable, }, { "exec error", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), nil}, []error{nil, errExec}, 0}}, + }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), nil}, []bool{true, false}, []error{nil, errExec}, 0}}, args{"foo", plugin.CommandGenerateSignature}, errExec, }, { "exit error", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), nil}, []error{nil, new(exec.ExitError)}, 0}}, + }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), {}}, []bool{true, false}, []error{nil, nil}, 0}}, args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, }, { "valid", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), nil}}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, args{"foo", plugin.CommandGenerateSignature}, nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.mgr.Run(context.Background(), tt.args.name, tt.args.cmd) + got, err := tt.mgr.Run(context.Background(), tt.args.name, tt.args.cmd, "1") wantErr := tt.err != nil if (err != nil) != wantErr { t.Fatalf("Manager.Run() error = %v, wantErr %v", err, wantErr) diff --git a/plugin/plugin.go b/plugin/plugin.go index 79543209..628a9462 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -66,33 +66,13 @@ const ( CapabilityEnvelopeGenerator Capability = "SIGNATURE_ENVELOPE_GENERATOR" ) -// Command line argument names used in several requests. -const ( - ArgContractVersion = "--contract-version" - ArgKeyName = "--key-name" - ArgKeyID = "--key-id" - ArgPayloadType = "--payload-type" - ArgSignatureEnvelopeType = "--signature-envelop-type" -) - // GenerateSignatureRequest contains the parameters passed in a generate-signature request. // All parameters are required. type GenerateSignatureRequest struct { - ContractVersion string - KeyName string - KeyID string -} - -func (req *GenerateSignatureRequest) Command() Command { - return CommandGenerateSignature -} - -func (req *GenerateSignatureRequest) Args() []string { - return []string{ - ArgContractVersion, req.ContractVersion, - ArgKeyName, req.KeyName, - ArgKeyID, req.KeyID, - } + ContractVersion string `json:"contractVersion"` + KeyName string `json:"keyName"` + KeyID string `json:"keyId"` + Payload string `json:"payload"` } func (req *GenerateSignatureRequest) Validate() error { @@ -131,25 +111,12 @@ type GenerateSignatureResponse struct { // GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. // All parameters are required. type GenerateEnvelopeRequest struct { - ContractVersion string - KeyName string - KeyID string - PayloadType string - SignatureEnvelopeType string -} - -func (req *GenerateEnvelopeRequest) Command() Command { - return CommandGenerateEnvelope -} - -func (req *GenerateEnvelopeRequest) Args() []string { - return []string{ - ArgContractVersion, req.ContractVersion, - ArgKeyName, req.KeyName, - ArgKeyID, req.KeyID, - ArgPayloadType, req.PayloadType, - ArgSignatureEnvelopeType, req.SignatureEnvelopeType, - } + ContractVersion string `json:"contractVersion"` + KeyName string `json:"keyName"` + KeyID string `json:"keyId"` + PayloadType string `json:"payloadType"` + SignatureEnvelopeType string `json:"signatureEnvelopeType"` + Payload string `json:"payload"` } func (req *GenerateEnvelopeRequest) Validate() error { diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go index b6133b3d..2467d61e 100644 --- a/plugin/plugin_test.go +++ b/plugin/plugin_test.go @@ -12,11 +12,11 @@ func TestGenerateSignatureRequest_Validate(t *testing.T) { wantErr bool }{ {"nil", nil, true}, - {"empty", &GenerateSignatureRequest{"", "", ""}, true}, - {"missing version", &GenerateSignatureRequest{"", "2", "3"}, true}, - {"missing key name", &GenerateSignatureRequest{"1", "", "3"}, true}, - {"missing key id", &GenerateSignatureRequest{"1", "2", ""}, true}, - {"valid", &GenerateSignatureRequest{"1", "2", "3"}, false}, + {"empty", &GenerateSignatureRequest{"", "", "", ""}, true}, + {"missing version", &GenerateSignatureRequest{"", "2", "3", ""}, true}, + {"missing key name", &GenerateSignatureRequest{"1", "", "3", ""}, true}, + {"missing key id", &GenerateSignatureRequest{"1", "2", "", ""}, true}, + {"valid", &GenerateSignatureRequest{"1", "2", "3", ""}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -34,13 +34,13 @@ func TestGenerateEnvelopeRequest_Validate(t *testing.T) { wantErr bool }{ {"nil", nil, true}, - {"empty", &GenerateEnvelopeRequest{"", "", "", "", ""}, true}, - {"missing version", &GenerateEnvelopeRequest{"", "2", "3", "4", "5"}, true}, - {"missing key name", &GenerateEnvelopeRequest{"1", "", "3", "4", "5"}, true}, - {"missing key id", &GenerateEnvelopeRequest{"1", "2", "", "4", "5"}, true}, - {"missing type", &GenerateEnvelopeRequest{"1", "2", "3", "", "5"}, true}, - {"missing envelop", &GenerateEnvelopeRequest{"1", "2", "3", "4", ""}, true}, - {"valid", &GenerateEnvelopeRequest{"1", "2", "3", "4", "5"}, false}, + {"empty", &GenerateEnvelopeRequest{"", "", "", "", "", ""}, true}, + {"missing version", &GenerateEnvelopeRequest{"", "2", "3", "4", "5", ""}, true}, + {"missing key name", &GenerateEnvelopeRequest{"1", "", "3", "4", "5", ""}, true}, + {"missing key id", &GenerateEnvelopeRequest{"1", "2", "", "4", "5", ""}, true}, + {"missing type", &GenerateEnvelopeRequest{"1", "2", "3", "", "5", ""}, true}, + {"missing envelop", &GenerateEnvelopeRequest{"1", "2", "3", "4", "", ""}, true}, + {"valid", &GenerateEnvelopeRequest{"1", "2", "3", "4", "5", ""}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 630751492e58a5743a0b6fe799fd5c0278ca7b47 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 2 May 2022 12:28:23 +0200 Subject: [PATCH 13/58] base64 encode payload Signed-off-by: qmuntal --- signature/jws/plugin.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index a0ee2aec..df5b756d 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -49,12 +49,14 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts return nil, fmt.Errorf("failed to marshal signing payload: %w", err) } + encPayload := base64.RawURLEncoding.EncodeToString(jsonPayload) + // Execute plugin. req := plugin.GenerateSignatureRequest{ ContractVersion: "1", KeyName: s.KeyName, KeyID: s.KeyID, - Payload: string(jsonPayload), + Payload: encPayload, } out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGenerateSignature, req) if err != nil { @@ -75,7 +77,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts if err != nil { return nil, err } - if sig.Payload != string(jsonPayload) { + if sig.Payload != encPayload { return nil, errors.New("signing payload has been modified") } @@ -85,7 +87,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts if err != nil { return nil, err } - err = verifyJWT(resp.SigningAlgorithm, string(jsonPayload), resp.Signature, certs) + err = verifyJWT(resp.SigningAlgorithm, encPayload, resp.Signature, certs) if err != nil { return nil, err } From d0874d9cf4d5d6a374e7c20d27227d73f35eef46 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 2 May 2022 17:21:28 +0200 Subject: [PATCH 14/58] fix supported algs Signed-off-by: qmuntal --- signature/jws/plugin.go | 23 +++---------- signature/jws/signer.go | 7 ---- signature/jws/signer_test.go | 4 --- signature/jws/spec.go | 61 ++++++++++++++++++++++++++++++++++ signature/jws/verifier_test.go | 6 +--- 5 files changed, 66 insertions(+), 35 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index df5b756d..b6a34e1a 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -15,9 +15,9 @@ import ( ) var supportedAlgs = map[string]bool{ - jwt.SigningMethodES256.Name: true, - jwt.SigningMethodES384.Name: true, - jwt.SigningMethodES512.Name: true, + jwt.SigningMethodPS256.Name: true, + jwt.SigningMethodPS384.Name: true, + jwt.SigningMethodPS512.Name: true, jwt.SigningMethodES256.Name: true, jwt.SigningMethodES384.Name: true, jwt.SigningMethodES512.Name: true, @@ -129,20 +129,5 @@ func verifyJWT(sigAlg string, payload string, sig string, certChain []*x509.Cert signingCert := certChain[0] // Verify the hash of req.payload against resp.signature using the public key if the leaf certificate. method := jwt.GetSigningMethod(sigAlg) - err := method.Verify(payload, sig, signingCert.PublicKey) - return err -} - -func checkCertChain(certChain []*x509.Certificate) error { - if len(certChain) == 0 { - return nil - } - signingCert := certChain[0] - roots := x509.NewCertPool() - roots.AddCert(signingCert) - _, err := signingCert.Verify(x509.VerifyOptions{ - Roots: roots, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - }) - return err + return method.Verify(payload, sig, signingCert.PublicKey) } diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 92f8141d..d8c1308f 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -28,13 +28,6 @@ type Signer struct { // certChain contains the X.509 public key certificate or certificate chain corresponding // to the key used to generate the signature. certChain [][]byte - - // TSA is the TimeStamp Authority to timestamp the resulted signature if present. - TSA timestamp.Timestamper - - // TSARoots is the set of trusted root certificates for verifying the fetched timestamp - // signature. If nil, the system roots or the platform verifier are used. - TSARoots *x509.CertPool } // NewSigner creates a signer with the recommended signing method and a signing key bundled diff --git a/signature/jws/signer_test.go b/signature/jws/signer_test.go index 88b17b0c..38896479 100644 --- a/signature/jws/signer_test.go +++ b/signature/jws/signer_test.go @@ -66,10 +66,6 @@ func TestSignWithTimestamp(t *testing.T) { if err != nil { t.Fatalf("timestamptest.NewTSA() error = %v", err) } - s.TSA = tsa - tsaRoots := x509.NewCertPool() - tsaRoots.AddCert(tsa.Certificate()) - s.TSARoots = tsaRoots // sign content ctx := context.Background() diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 9dd739f5..535e145b 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -4,6 +4,11 @@ package jws import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" "time" "github.com/golang-jwt/jwt/v4" @@ -47,3 +52,59 @@ func packPayload(desc notation.Descriptor, opts notation.SignOptions) *payload { }, } } + +func checkCertChain(certChain []*x509.Certificate) error { + if len(certChain) == 0 { + return nil + } + if err := verifyCert(certChain[0], x509.ExtKeyUsageCodeSigning); err != nil { + return fmt.Errorf("signing certificate does not meet the minimum requirements: %w", err) + } + for _, c := range certChain[1:] { + for _, ext := range c.ExtKeyUsage { + if ext == x509.ExtKeyUsageTimeStamping { + if err := verifyCert(c, x509.ExtKeyUsageTimeStamping); err != nil { + return fmt.Errorf("timestamping certificate does not meet the minimum requirements: %w", err) + } + } + } + } + return nil +} + +// validateCert checks cert meets the requirements defined in +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#certificate-requirements. +func verifyCert(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { + if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + return errors.New("keyUsage must have the bit positions for digitalSignature set") + } + if len(cert.ExtKeyUsage) != 1 || cert.ExtKeyUsage[0] != extKeyUsage { + return fmt.Errorf("extKeyUsage must be %d", extKeyUsage) + } + for _, ext := range cert.Extensions { + switch ext.Id[3] { + case 15: + if !ext.Critical { + return errors.New("the keyUsage extension must be marked critical") + } + case 37: + if !ext.Critical { + return errors.New("the extKeyUsage extension must be marked critical") + } + } + } + if cert.BasicConstraintsValid && cert.IsCA { + return errors.New("if the basicConstraints extension is present, the cA field MUST be set false") + } + switch key := cert.PublicKey.(type) { + case *rsa.PublicKey: + if key.N.BitLen() < 2048 { + return errors.New("RSA public key length must be 2048 bits or higher") + } + case *ecdsa.PublicKey: + if key.Params().N.BitLen() < 256 { + return errors.New("ECDSA public key length must be 256 bits or higher") + } + } + return nil +} diff --git a/signature/jws/verifier_test.go b/signature/jws/verifier_test.go index ad1f52b6..c1277f65 100644 --- a/signature/jws/verifier_test.go +++ b/signature/jws/verifier_test.go @@ -76,10 +76,6 @@ func TestVerifyWithTimestamp(t *testing.T) { if err != nil { t.Fatalf("timestamptest.NewTSA() error = %v", err) } - s.TSA = tsa - tsaRoots := x509.NewCertPool() - tsaRoots.AddCert(tsa.Certificate()) - s.TSARoots = tsaRoots // sign content ctx := context.Background() @@ -103,7 +99,7 @@ func TestVerifyWithTimestamp(t *testing.T) { } // verify again with certificate trusted - v.TSARoots = tsaRoots + v.TSARoots = sOpts.TSAVerifyOptions.Roots got, err := v.Verify(ctx, sig, vOpts) if err != nil { t.Fatalf("Verify() error = %v", err) From 15d7713f40c569b696d9c7e7bee5ccbf25eda3cb Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 07:50:39 +0200 Subject: [PATCH 15/58] create envelope Signed-off-by: qmuntal --- signature/jws/plugin.go | 40 +++++++++++++++++++++------------------- signature/jws/spec.go | 15 +++++++++------ 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index b6a34e1a..ec185b9b 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -4,13 +4,11 @@ import ( "context" "crypto/x509" "encoding/base64" - "encoding/json" - "errors" "fmt" + "strings" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/crypto/jwsutil" "github.com/notaryproject/notation-go/plugin" ) @@ -44,19 +42,25 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts if err := payload.Valid(); err != nil { return nil, err } - jsonPayload, err := json.Marshal(payload) + token := &jwt.Token{ + Header: map[string]interface{}{ + "cty": MediaTypeNotationPayload, + "crit": []string{ + "cty", + }, + }, + Claims: payload, + } + signing, err := token.SigningString() if err != nil { return nil, fmt.Errorf("failed to marshal signing payload: %w", err) } - - encPayload := base64.RawURLEncoding.EncodeToString(jsonPayload) - // Execute plugin. req := plugin.GenerateSignatureRequest{ ContractVersion: "1", KeyName: s.KeyName, KeyID: s.KeyID, - Payload: encPayload, + Payload: base64.RawStdEncoding.EncodeToString([]byte(signing)), } out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGenerateSignature, req) if err != nil { @@ -72,22 +76,19 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) } - // Check payload has not been modified. - sig, err := jwsutil.ParseCompact(resp.Signature) + // Verify the hash of the request payload against the response signature + // using the public key of the signing certificate. + certs, err := parseCertChainBase64(resp.CertificateChain) if err != nil { return nil, err } - if sig.Payload != encPayload { - return nil, errors.New("signing payload has been modified") - } - // Verify the hash of the request payload against the response signature - // using the public key of the signing certificate. - certs, err := parseCertChainBase64(resp.CertificateChain) + signed, err := base64.RawStdEncoding.DecodeString(resp.Signature) if err != nil { return nil, err } - err = verifyJWT(resp.SigningAlgorithm, encPayload, resp.Signature, certs) + base64Signed := base64.RawURLEncoding.EncodeToString(signed) + err = verifyJWT(resp.SigningAlgorithm, signing, base64Signed, certs) if err != nil { return nil, err } @@ -103,7 +104,8 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts for i, c := range certs { rawCerts[i] = c.Raw } - return jwtEnvelop(ctx, opts, resp.Signature, rawCerts) + compact := strings.Join([]string{signing, resp.Signature}, ".") + return jwtEnvelop(ctx, opts, compact, rawCerts) } func parseCertChainBase64(certChain []string) ([]*x509.Certificate, error) { @@ -122,7 +124,7 @@ func parseCertChainBase64(certChain []string) ([]*x509.Certificate, error) { return certs, nil } -func verifyJWT(sigAlg string, payload string, sig string, certChain []*x509.Certificate) error { +func verifyJWT(sigAlg string, payload, sig string, certChain []*x509.Certificate) error { if len(certChain) == 0 { return nil } diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 535e145b..15c61fc6 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -78,8 +78,15 @@ func verifyCert(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { return errors.New("keyUsage must have the bit positions for digitalSignature set") } - if len(cert.ExtKeyUsage) != 1 || cert.ExtKeyUsage[0] != extKeyUsage { - return fmt.Errorf("extKeyUsage must be %d", extKeyUsage) + var hasExtKeyUsage bool + for _, ext := range cert.ExtKeyUsage { + if ext == extKeyUsage { + hasExtKeyUsage = true + break + } + } + if !hasExtKeyUsage { + return fmt.Errorf("extKeyUsage must contain be %d", extKeyUsage) } for _, ext := range cert.Extensions { switch ext.Id[3] { @@ -87,10 +94,6 @@ func verifyCert(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { if !ext.Critical { return errors.New("the keyUsage extension must be marked critical") } - case 37: - if !ext.Critical { - return errors.New("the extKeyUsage extension must be marked critical") - } } } if cert.BasicConstraintsValid && cert.IsCA { From d1f6564ce7b9ba922b5dcb512196db0dbb048c2a Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 08:34:43 +0200 Subject: [PATCH 16/58] pr feedback Signed-off-by: qmuntal --- plugin/errors.go | 2 -- plugin/manager/integration_test.go | 2 +- plugin/manager/manager.go | 36 +++++++++++++----------------- plugin/manager/manager_test.go | 7 ------ plugin/plugin.go | 4 ++-- 5 files changed, 19 insertions(+), 32 deletions(-) diff --git a/plugin/errors.go b/plugin/errors.go index 02ac900e..12fdef71 100644 --- a/plugin/errors.go +++ b/plugin/errors.go @@ -6,8 +6,6 @@ import ( "fmt" ) -var ErrUnknownCommand = errors.New("not a plugin command") - type ErrorCode string const ( diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index d762cb13..f8334b11 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -37,7 +37,7 @@ func preparePlugin(t *testing.T) string { if err != nil { t.Fatal(err) } - out := filepath.Join(root, "foo", NamePrefix+"foo") + out := filepath.Join(root, "foo", plugin.Prefix+"foo") out = addExeSuffix(out) cmd := exec.Command("go", "build", "-o", out) cmd.Dir = root diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 3138f406..3b7969b1 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -16,11 +16,6 @@ import ( "github.com/notaryproject/notation-go/plugin" ) -const ( - // NamePrefix is the prefix required on all plugin binary names. - NamePrefix = "notation-" -) - // Plugin represents a potential plugin with all it's metadata. type Plugin struct { plugin.Metadata @@ -37,9 +32,6 @@ var ErrNotFound = errors.New("plugin not found") // ErrNotCompliant is returned by Manager.Run when the plugin is found but not compliant. var ErrNotCompliant = errors.New("plugin not compliant") -// ErrNotCapable is returned by Manager.Run when the plugin is found and compliant, but is missing a necessary capability. -var ErrNotCapable = errors.New("plugin not capable") - // commander is defined for mocking purposes. type commander interface { // Output runs the command, passing req to the its stdin. @@ -82,14 +74,14 @@ type Manager struct { // NewManager returns a new manager. func NewManager() *Manager { - homeDir, err := os.UserHomeDir() + configDir, err := os.UserConfigDir() if err != nil { // Lets panic for now. // Once the config is moved to notation-go we will move this code to // the config package as a global initialization. panic(err) } - pluginDir := filepath.Join(homeDir, ".notation", "plugins") + pluginDir := filepath.Join(configDir, "notation", "plugins") return &Manager{rootedFS{os.DirFS(pluginDir), pluginDir}, execCommander{}} } @@ -132,28 +124,28 @@ func (mgr *Manager) List(ctx context.Context) ([]*Plugin, error) { // // If the plugin is not found, the error is of type ErrNotFound. // If the plugin metadata is not valid or stdout and stderr can't be decoded into a valid response, the error is of type ErrNotCompliant. -// If the plugin does not have the required capability, the error is of type ErrNotCapable. // If the command starts but does not complete successfully, the error is of type RequestError wrapping a *exec.ExitError. // Other error types may be returned for other situations. func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, req interface{}) (interface{}, error) { p, err := mgr.newPlugin(ctx, name) if err != nil { - return nil, err + return nil, pluginErr(name, err) } if p.Err != nil { - return nil, withErr(p.Err, ErrNotCompliant) - } - if c := cmd.Capability(); !p.HasCapability(c) { - return nil, ErrNotCapable + return nil, pluginErr(name, withErr(p.Err, ErrNotCompliant)) } var data []byte if req != nil { data, err = json.Marshal(req) if err != nil { - return nil, fmt.Errorf("failed to marshal request object: %w", err) + return nil, pluginErr(name, fmt.Errorf("failed to marshal request object: %w", err)) } } - return run(ctx, mgr.cmder, p.Path, cmd, data) + resp, err := run(ctx, mgr.cmder, p.Path, cmd, data) + if err != nil { + return nil, pluginErr(name, err) + } + return resp, nil } // newPlugin determines if the given candidate is valid and returns a Plugin. @@ -171,7 +163,7 @@ func (mgr *Manager) newPlugin(ctx context.Context, name string) (*Plugin, error) } p.Metadata = *out.(*plugin.Metadata) if p.Name != name { - p.Err = fmt.Errorf("executable name must be %q instead of %q", addExeSuffix(NamePrefix+p.Name), filepath.Base(p.Path)) + p.Err = fmt.Errorf("executable name must be %q instead of %q", addExeSuffix(plugin.Prefix+p.Name), filepath.Base(p.Path)) } else if err := p.Metadata.Validate(); err != nil { p.Err = fmt.Errorf("invalid metadata: %w", err) } @@ -201,6 +193,10 @@ func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Com return resp, nil } +func pluginErr(name string, err error) error { + return fmt.Errorf("%s: %w", name, err) +} + // isCandidate checks if the named plugin is a valid candidate. func isCandidate(fsys fs.FS, name string) bool { base := binName(name) @@ -218,7 +214,7 @@ func isCandidate(fsys fs.FS, name string) bool { } func binName(name string) string { - return addExeSuffix(NamePrefix + name) + return addExeSuffix(plugin.Prefix + name) } func binPath(fsys fs.FS, name string) string { diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 406a3e0b..61a8cb0c 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -240,13 +240,6 @@ func TestManager_Run(t *testing.T) { }, testCommander{nil, false, errors.New("err")}}, args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, }, - { - "no capability", &Manager{fstest.MapFS{ - "foo": &fstest.MapFile{Mode: fs.ModeDir}, - addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{metadataJSON(validMetadata), true, nil}}, - args{"foo", plugin.CommandGenerateEnvelope}, ErrNotCapable, - }, { "exec error", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, diff --git a/plugin/plugin.go b/plugin/plugin.go index 628a9462..304553ee 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -4,8 +4,8 @@ import ( "errors" ) -// NamePrefix is the prefix required on all plugin binary names. -const NamePrefix = "notation-" +// Prefix is the prefix required on all plugin binary names. +const Prefix = "notation-" // Command is a CLI command available in the plugin contract. type Command string From 74dc4772938326da714218d7cdfa09a7255287d4 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 09:15:08 +0200 Subject: [PATCH 17/58] remove Command.Capability() and Command.NewResponse() Signed-off-by: qmuntal --- plugin/manager/manager.go | 19 +++++++++++++++---- plugin/plugin.go | 26 -------------------------- plugin/plugin_test.go | 21 --------------------- 3 files changed, 15 insertions(+), 51 deletions(-) diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 3b7969b1..21aa38fd 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -114,13 +114,11 @@ func (mgr *Manager) List(ctx context.Context) ([]*Plugin, error) { // Run executes the specified command against the named plugin and waits for it to complete. // // When the returned object is not nil, its type is guaranteed to remain always the same for a given Command. -// The type associated to each Command can be found at Command.NewResponse(). // // The returned error is nil if: // - the plugin exists and is valid -// - the plugin supports the capability returned by cmd.Capability() // - the command runs and exits with a zero exit status -// - the command stdout is a valid json object which can be unmarshal-ed into the object returned by cmd.NewResponse(). +// - the command stdout contains a valid json object which can be unmarshal-ed. // // If the plugin is not found, the error is of type ErrNotFound. // If the plugin metadata is not valid or stdout and stderr can't be decoded into a valid response, the error is of type ErrNotCompliant. @@ -134,6 +132,9 @@ func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, re if p.Err != nil { return nil, pluginErr(name, withErr(p.Err, ErrNotCompliant)) } + if cmd == plugin.CommandGetMetadata { + return &p.Metadata, nil + } var data []byte if req != nil { data, err = json.Marshal(req) @@ -184,7 +185,17 @@ func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Com } return nil, re } - resp := cmd.NewResponse() + var resp interface{} + switch cmd { + case plugin.CommandGetMetadata: + resp = new(plugin.Metadata) + case plugin.CommandGenerateSignature: + resp = new(plugin.GenerateSignatureResponse) + case plugin.CommandGenerateEnvelope: + resp = new(plugin.GenerateEnvelopeResponse) + default: + return nil, fmt.Errorf("unsupported command: %s", cmd) + } err = json.Unmarshal(out, resp) if err != nil { err = fmt.Errorf("failed to decode json response: %w", err) diff --git a/plugin/plugin.go b/plugin/plugin.go index 304553ee..f718bb33 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -27,32 +27,6 @@ const ( CommandGenerateEnvelope Command = "generate-envelope" ) -// Capability returns the capability associated to the command. -func (c Command) Capability() Capability { - switch c { - case CommandGenerateSignature: - return CapabilitySignatureGenerator - case CommandGenerateEnvelope: - return CapabilityEnvelopeGenerator - default: - return "" - } -} - -// Capability returns the response associated to the command. -func (c Command) NewResponse() interface{} { - switch c { - case CommandGetMetadata: - return new(Metadata) - case CommandGenerateSignature: - return new(GenerateSignatureResponse) - case CommandGenerateEnvelope: - return new(GenerateEnvelopeResponse) - default: - return nil - } -} - // Capability is a feature available in the plugin contract. type Capability string diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go index 2467d61e..e2937371 100644 --- a/plugin/plugin_test.go +++ b/plugin/plugin_test.go @@ -1,7 +1,6 @@ package plugin import ( - "reflect" "testing" ) @@ -50,23 +49,3 @@ func TestGenerateEnvelopeRequest_Validate(t *testing.T) { }) } } - -func TestCommand_NewResponse(t *testing.T) { - tests := []struct { - name string - c Command - want interface{} - }{ - {"empty", "", nil}, - {"metadata", CommandGetMetadata, new(Metadata)}, - {"sign", CommandGenerateSignature, new(GenerateSignatureResponse)}, - {"envelop", CommandGenerateEnvelope, new(GenerateEnvelopeResponse)}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.c.NewResponse(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Command.NewResponse() = %v, want %v", got, tt.want) - } - }) - } -} From f37ca5b04021f954010d7448799086f8ffea0acd Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 12:03:52 +0200 Subject: [PATCH 18/58] add DescribeKey command Signed-off-by: qmuntal --- plugin/manager/manager.go | 2 ++ plugin/plugin.go | 21 +++++++++++++++++++++ signature/jws/plugin.go | 39 +++++++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 21aa38fd..e25946b2 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -193,6 +193,8 @@ func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Com resp = new(plugin.GenerateSignatureResponse) case plugin.CommandGenerateEnvelope: resp = new(plugin.GenerateEnvelopeResponse) + case plugin.CommandDescribeKey: + resp = new(plugin.DescribeKeyResponse) default: return nil, fmt.Errorf("unsupported command: %s", cmd) } diff --git a/plugin/plugin.go b/plugin/plugin.go index f718bb33..2c987806 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -16,6 +16,11 @@ const ( // plugin metadata. CommandGetMetadata Command = "get-plugin-metadata" + // CommandDescribeKey is the name of the plugin command + // which must be supported by every plugin that has the + // SIGNATURE_GENERATOR capability. + CommandDescribeKey Command = "describe-key" + // CommandGenerateSignature is the name of the plugin command // which must be supported by every plugin that has the // SIGNATURE_GENERATOR capability. @@ -40,6 +45,22 @@ const ( CapabilityEnvelopeGenerator Capability = "SIGNATURE_ENVELOPE_GENERATOR" ) +// DescribeKeyRequest contains the parameters passed in a describe-key request. +// All parameters are required. +type DescribeKeyRequest struct { + ContractVersion string `json:"contractVersion"` + KeyName string `json:"keyName"` + KeyID string `json:"keyId"` +} + +// GenerateSignatureResponse is the response of a describe-key request. +type DescribeKeyResponse struct { + // The same key id as passed in the request. + KeyID string `json:"keyId"` + + Algorithm string `json:"algorithm"` +} + // GenerateSignatureRequest contains the parameters passed in a generate-signature request. // All parameters are required. type GenerateSignatureRequest struct { diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index ec185b9b..16716ab2 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -33,17 +33,47 @@ type PluginSigner struct { } func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { - if err := opts.Validate(); err != nil { - return nil, err + out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGetMetadata, nil) + if err != nil { + return nil, fmt.Errorf("metadata command failed: %w", err) } + metadata := out.(*plugin.Metadata) // Generate payload to be signed. payload := packPayload(desc, opts) if err := payload.Valid(); err != nil { return nil, err } + + if metadata.HasCapability(plugin.CapabilitySignatureGenerator) { + return s.generateSignature(ctx, opts, payload) + } else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) { + + } + return nil, fmt.Errorf("plugin %q does not have signing capabilities", s.PluginName) +} + +func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResponse, error) { + req := plugin.DescribeKeyRequest{ + ContractVersion: "1", + KeyName: s.KeyName, + KeyID: s.KeyID, + } + out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandDescribeKey, req) + if err != nil { + return nil, fmt.Errorf("describe-key command failed: %w", err) + } + return out.(*plugin.DescribeKeyResponse), nil +} + +func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.SignOptions, payload *payload) ([]byte, error) { + key, err := s.describeKey(ctx) + if err != nil { + return nil, err + } token := &jwt.Token{ Header: map[string]interface{}{ + "alg": key.Algorithm, "cty": MediaTypeNotationPayload, "crit": []string{ "cty", @@ -66,10 +96,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts if err != nil { return nil, fmt.Errorf("sign command failed: %w", err) } - resp, ok := out.(*plugin.GenerateSignatureResponse) - if !ok { - return nil, fmt.Errorf("invalid sign response type %T", resp) - } + resp := out.(*plugin.GenerateSignatureResponse) // Check algorithm is supported. if !supportedAlgs[resp.SigningAlgorithm] { From 504ca160eeaeef49c9bbd260f0ecd191d604b299 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 12:05:47 +0200 Subject: [PATCH 19/58] remove command validation Signed-off-by: qmuntal --- plugin/plugin.go | 42 ----------------------------------- plugin/plugin_test.go | 51 ------------------------------------------- 2 files changed, 93 deletions(-) delete mode 100644 plugin/plugin_test.go diff --git a/plugin/plugin.go b/plugin/plugin.go index 2c987806..9023dd74 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,9 +1,5 @@ package plugin -import ( - "errors" -) - // Prefix is the prefix required on all plugin binary names. const Prefix = "notation-" @@ -70,22 +66,6 @@ type GenerateSignatureRequest struct { Payload string `json:"payload"` } -func (req *GenerateSignatureRequest) Validate() error { - if req == nil { - return errors.New("nil request") - } - if req.ContractVersion == "" { - return errors.New("empty contractVersion") - } - if req.KeyName == "" { - return errors.New("empty keyName") - } - if req.KeyID == "" { - return errors.New("empty keyId") - } - return nil -} - // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { // The same key id as passed in the request. @@ -114,28 +94,6 @@ type GenerateEnvelopeRequest struct { Payload string `json:"payload"` } -func (req *GenerateEnvelopeRequest) Validate() error { - if req == nil { - return errors.New("nil request") - } - if req.ContractVersion == "" { - return errors.New("empty contractVersion") - } - if req.KeyName == "" { - return errors.New("empty keyName") - } - if req.KeyID == "" { - return errors.New("empty keyId") - } - if req.PayloadType == "" { - return errors.New("empty payloadType") - } - if req.SignatureEnvelopeType == "" { - return errors.New("empty envelopeType") - } - return nil -} - // GenerateSignatureResponse is the response of a generate-envelop request. type GenerateEnvelopeResponse struct { // Base64 encoded signature envelope. diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go deleted file mode 100644 index e2937371..00000000 --- a/plugin/plugin_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package plugin - -import ( - "testing" -) - -func TestGenerateSignatureRequest_Validate(t *testing.T) { - tests := []struct { - name string - req *GenerateSignatureRequest - wantErr bool - }{ - {"nil", nil, true}, - {"empty", &GenerateSignatureRequest{"", "", "", ""}, true}, - {"missing version", &GenerateSignatureRequest{"", "2", "3", ""}, true}, - {"missing key name", &GenerateSignatureRequest{"1", "", "3", ""}, true}, - {"missing key id", &GenerateSignatureRequest{"1", "2", "", ""}, true}, - {"valid", &GenerateSignatureRequest{"1", "2", "3", ""}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.req.Validate(); (err != nil) != tt.wantErr { - t.Errorf("GenerateSignatureRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestGenerateEnvelopeRequest_Validate(t *testing.T) { - tests := []struct { - name string - req *GenerateEnvelopeRequest - wantErr bool - }{ - {"nil", nil, true}, - {"empty", &GenerateEnvelopeRequest{"", "", "", "", "", ""}, true}, - {"missing version", &GenerateEnvelopeRequest{"", "2", "3", "4", "5", ""}, true}, - {"missing key name", &GenerateEnvelopeRequest{"1", "", "3", "4", "5", ""}, true}, - {"missing key id", &GenerateEnvelopeRequest{"1", "2", "", "4", "5", ""}, true}, - {"missing type", &GenerateEnvelopeRequest{"1", "2", "3", "", "5", ""}, true}, - {"missing envelop", &GenerateEnvelopeRequest{"1", "2", "3", "4", "", ""}, true}, - {"valid", &GenerateEnvelopeRequest{"1", "2", "3", "4", "5", ""}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.req.Validate(); (err != nil) != tt.wantErr { - t.Errorf("GenerateEnvelopeRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} From 7beb5cd29b3eb463a4ae7966a718ce3c621c0885 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 14:10:18 +0200 Subject: [PATCH 20/58] plugin cleanup Signed-off-by: qmuntal --- notation.go | 5 ----- signature/jws/plugin.go | 34 ++++++++++++++++++++-------------- signature/jws/signer.go | 3 --- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/notation.go b/notation.go index 42c05228..abf72404 100644 --- a/notation.go +++ b/notation.go @@ -44,11 +44,6 @@ type SignOptions struct { TSAVerifyOptions x509.VerifyOptions } -// Validate does basic validation on SignOptions. -func (opts SignOptions) Validate() error { - return nil -} - // Signer is a generic interface for signing an artifact. // The interface allows signing with local or remote keys, // and packing in various signature formats. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 16716ab2..64fcc569 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/base64" + "errors" "fmt" "strings" @@ -67,17 +68,17 @@ func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResp } func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.SignOptions, payload *payload) ([]byte, error) { + // Get key info. key, err := s.describeKey(ctx) if err != nil { return nil, err } + // Generate signing string. token := &jwt.Token{ Header: map[string]interface{}{ - "alg": key.Algorithm, - "cty": MediaTypeNotationPayload, - "crit": []string{ - "cty", - }, + "alg": key.Algorithm, + "cty": MediaTypeSignatureEnvelope, + "crit": []string{"cty"}, }, Claims: payload, } @@ -85,7 +86,8 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign if err != nil { return nil, fmt.Errorf("failed to marshal signing payload: %w", err) } - // Execute plugin. + + // Execute plugin sign command. req := plugin.GenerateSignatureRequest{ ContractVersion: "1", KeyName: s.KeyName, @@ -103,19 +105,18 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) } - // Verify the hash of the request payload against the response signature - // using the public key of the signing certificate. - certs, err := parseCertChainBase64(resp.CertificateChain) + certs, err := parseCertChain(resp.CertificateChain) if err != nil { return nil, err } + // Verify the hash of the request payload against the response signature + // using the public key of the signing certificate. signed, err := base64.RawStdEncoding.DecodeString(resp.Signature) if err != nil { return nil, err } - base64Signed := base64.RawURLEncoding.EncodeToString(signed) - err = verifyJWT(resp.SigningAlgorithm, signing, base64Signed, certs) + err = verifyJWT(resp.SigningAlgorithm, signing, signed, certs) if err != nil { return nil, err } @@ -135,7 +136,11 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign return jwtEnvelop(ctx, opts, compact, rawCerts) } -func parseCertChainBase64(certChain []string) ([]*x509.Certificate, error) { +func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, opts notation.SignOptions, payload *payload) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func parseCertChain(certChain []string) ([]*x509.Certificate, error) { certs := make([]*x509.Certificate, len(certChain)) for i, data := range certChain { der, err := base64.RawStdEncoding.DecodeString(data) @@ -151,12 +156,13 @@ func parseCertChainBase64(certChain []string) ([]*x509.Certificate, error) { return certs, nil } -func verifyJWT(sigAlg string, payload, sig string, certChain []*x509.Certificate) error { +func verifyJWT(sigAlg string, payload string, sig []byte, certChain []*x509.Certificate) error { if len(certChain) == 0 { return nil } signingCert := certChain[0] // Verify the hash of req.payload against resp.signature using the public key if the leaf certificate. method := jwt.GetSigningMethod(sigAlg) - return method.Verify(payload, sig, signingCert.PublicKey) + encSig := base64.RawURLEncoding.EncodeToString(sig) + return method.Verify(payload, encSig, signingCert.PublicKey) } diff --git a/signature/jws/signer.go b/signature/jws/signer.go index d8c1308f..bfbcf815 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -81,9 +81,6 @@ func NewSignerWithCertificateChain(method jwt.SigningMethod, key crypto.PrivateK // Sign signs the artifact described by its descriptor, and returns the signature. func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { - if err := opts.Validate(); err != nil { - return nil, err - } // generate JWT payload := packPayload(desc, opts) if err := payload.Valid(); err != nil { From a3bdec68597e8d673a3da337022a655aeb78e458 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 14:14:01 +0200 Subject: [PATCH 21/58] fix compilation Signed-off-by: qmuntal --- plugin/plugin.go | 3 +++ signature/jws/plugin.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/plugin.go b/plugin/plugin.go index 9023dd74..d1288e23 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -54,6 +54,8 @@ type DescribeKeyResponse struct { // The same key id as passed in the request. KeyID string `json:"keyId"` + // One of following supported signing algorithms: + // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection Algorithm string `json:"algorithm"` } @@ -99,6 +101,7 @@ type GenerateEnvelopeResponse struct { // Base64 encoded signature envelope. SignatureEnvelope string `json:"signatureEnvelope"` + // The media type of the envelope of notation signature. SignatureEnvelopeType string `json:"signatureEnvelopeType"` // Annotations to be appended to Signature Manifest annotations. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 64fcc569..705b5c79 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -77,7 +77,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign token := &jwt.Token{ Header: map[string]interface{}{ "alg": key.Algorithm, - "cty": MediaTypeSignatureEnvelope, + "cty": MediaTypeNotationPayload, "crit": []string{"cty"}, }, Claims: payload, From 04da321d3750f204e425fcadeee26214f19eb764 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 3 May 2022 15:34:43 +0200 Subject: [PATCH 22/58] base64 encode signature Signed-off-by: qmuntal --- signature/jws/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 705b5c79..2e6fb34f 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -132,7 +132,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign for i, c := range certs { rawCerts[i] = c.Raw } - compact := strings.Join([]string{signing, resp.Signature}, ".") + compact := strings.Join([]string{signing, base64.RawURLEncoding.EncodeToString(signed)}, ".") return jwtEnvelop(ctx, opts, compact, rawCerts) } From 8f78a9485d3941dedc7edd258e872a1975ea64e2 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 4 May 2022 12:23:04 +0200 Subject: [PATCH 23/58] ignore symlinked plugin directories Signed-off-by: qmuntal --- plugin/manager/manager.go | 12 ++++++++++-- plugin/manager/manager_test.go | 9 +++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index e25946b2..ea16f657 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -99,7 +99,13 @@ func (mgr *Manager) Get(ctx context.Context, name string) (*Plugin, error) { func (mgr *Manager) List(ctx context.Context) ([]*Plugin, error) { var plugins []*Plugin fs.WalkDir(mgr.fsys, ".", func(dir string, d fs.DirEntry, _ error) error { - if dir == "." || !d.IsDir() { + if dir == "." { + // Ignore root dir. + return nil + } + typ := d.Type() + if !typ.IsDir() || typ&fs.ModeSymlink != 0 { + // Ignore non-directories and symlinked directories. return nil } p, err := mgr.newPlugin(ctx, d.Name()) @@ -219,7 +225,7 @@ func isCandidate(fsys fs.FS, name string) bool { // (e.g. due to permissions or anything else). return false } - if fi.Mode().Type() != 0 { + if !fi.Mode().IsRegular() { // Ignore non-regular files. return false } @@ -232,6 +238,8 @@ func binName(name string) string { func binPath(fsys fs.FS, name string) string { base := binName(name) + // NewManager() always instantiate a rootedFS. + // Other fs.FS implementations are only supported for testing purposes. if fsys, ok := fsys.(rootedFS); ok { return filepath.Join(fsys.root, name, base) } diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 61a8cb0c..18f85ede 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -187,6 +187,15 @@ func TestManager_List(t *testing.T) { }{ {"empty fsys", &Manager{fstest.MapFS{}, nil}, nil}, {"fsys without plugins", &Manager{fstest.MapFS{"a.go": &fstest.MapFile{}}, nil}, nil}, + { + "fsys with plugins but symlinked", &Manager{ + fstest.MapFS{ + "foo": &fstest.MapFile{Mode: fs.ModeDir | fs.ModeSymlink}, + addExeSuffix("foo/notation-foo"): new(fstest.MapFile), + "baz": &fstest.MapFile{Mode: fs.ModeDir}, + }, testCommander{metadataJSON(validMetadata), true, nil}}, + nil, + }, { "fsys with some invalid plugins", &Manager{ fstest.MapFS{ From bd10cfe31a462d9bbeb5b2b023697440cb98a6d3 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 4 May 2022 12:40:52 +0200 Subject: [PATCH 24/58] suuport plugin key spec Signed-off-by: qmuntal --- plugin/plugin.go | 11 ++++++----- signature/jws/plugin.go | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/plugin/plugin.go b/plugin/plugin.go index d1288e23..a933bc96 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -44,9 +44,10 @@ const ( // DescribeKeyRequest contains the parameters passed in a describe-key request. // All parameters are required. type DescribeKeyRequest struct { - ContractVersion string `json:"contractVersion"` - KeyName string `json:"keyName"` - KeyID string `json:"keyId"` + ContractVersion string `json:"contractVersion"` + KeyName string `json:"keyName"` + KeyID string `json:"keyId"` + PluginConfig map[string]string `json:"pluginConfig"` } // GenerateSignatureResponse is the response of a describe-key request. @@ -54,9 +55,9 @@ type DescribeKeyResponse struct { // The same key id as passed in the request. KeyID string `json:"keyId"` - // One of following supported signing algorithms: + // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - Algorithm string `json:"algorithm"` + KeySpec string `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 2e6fb34f..90361d54 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -27,10 +27,11 @@ type PluginRunner interface { } type PluginSigner struct { - Runner PluginRunner - PluginName string - KeyID string - KeyName string + Runner PluginRunner + PluginName string + KeyID string + KeyName string + PluginConfig map[string]string } func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { @@ -49,7 +50,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts if metadata.HasCapability(plugin.CapabilitySignatureGenerator) { return s.generateSignature(ctx, opts, payload) } else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) { - + return s.generateSignatureEnvelope(ctx, opts, payload) } return nil, fmt.Errorf("plugin %q does not have signing capabilities", s.PluginName) } @@ -59,6 +60,7 @@ func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResp ContractVersion: "1", KeyName: s.KeyName, KeyID: s.KeyID, + PluginConfig: s.PluginConfig, } out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandDescribeKey, req) if err != nil { @@ -73,10 +75,14 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign if err != nil { return nil, err } + alg := keySpecToAlg(key.KeySpec) + if alg == "" { + return nil, errors.New("unsupported key spec: " + key.KeySpec) + } // Generate signing string. token := &jwt.Token{ Header: map[string]interface{}{ - "alg": key.Algorithm, + "alg": alg, "cty": MediaTypeNotationPayload, "crit": []string{"cty"}, }, @@ -166,3 +172,21 @@ func verifyJWT(sigAlg string, payload string, sig []byte, certChain []*x509.Cert encSig := base64.RawURLEncoding.EncodeToString(sig) return method.Verify(payload, encSig, signingCert.PublicKey) } + +func keySpecToAlg(name string) string { + switch name { + case "RSA_2048": + return jwt.SigningMethodRS256.Alg() + case "RSA_3072": + return jwt.SigningMethodRS384.Alg() + case "RSA_4096": + return jwt.SigningMethodRS512.Alg() + case "EC_256": + return jwt.SigningMethodES256.Alg() + case "EC_384": + return jwt.SigningMethodES384.Alg() + case "EC_512": + return jwt.SigningMethodES512.Alg() + } + return "" +} From ceb9b9216d9c0f7d83499ad447ca025a43de3979 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 4 May 2022 13:16:23 +0200 Subject: [PATCH 25/58] pass signing payload as ascii string Signed-off-by: qmuntal --- signature/jws/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 90361d54..82cd7d39 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -98,7 +98,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign ContractVersion: "1", KeyName: s.KeyName, KeyID: s.KeyID, - Payload: base64.RawStdEncoding.EncodeToString([]byte(signing)), + Payload: signing, } out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGenerateSignature, req) if err != nil { From 26c6b916d783cb4b25ac447c92c5c64d87926ba9 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 4 May 2022 13:27:06 +0200 Subject: [PATCH 26/58] fix rsa method Signed-off-by: qmuntal --- signature/jws/plugin.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 82cd7d39..f606703d 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -176,11 +176,11 @@ func verifyJWT(sigAlg string, payload string, sig []byte, certChain []*x509.Cert func keySpecToAlg(name string) string { switch name { case "RSA_2048": - return jwt.SigningMethodRS256.Alg() + return jwt.SigningMethodPS256.Alg() case "RSA_3072": - return jwt.SigningMethodRS384.Alg() + return jwt.SigningMethodPS384.Alg() case "RSA_4096": - return jwt.SigningMethodRS512.Alg() + return jwt.SigningMethodPS512.Alg() case "EC_256": return jwt.SigningMethodES256.Alg() case "EC_384": From 34705570b9ff89fe1e28374d74b8e196c57037f0 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 4 May 2022 15:19:52 +0200 Subject: [PATCH 27/58] define KeySpec Signed-off-by: qmuntal --- plugin/plugin.go | 33 ++++++++++++++++++++++++++++++++- signature/jws/plugin.go | 16 ++++++++-------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/plugin/plugin.go b/plugin/plugin.go index a933bc96..67089042 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,8 +1,39 @@ package plugin +import ( + "crypto" +) + // Prefix is the prefix required on all plugin binary names. const Prefix = "notation-" +// KeySpec defines a key type and size. +type KeySpec string + +// One of following supported specs +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + RSA_2048 KeySpec = "RSA_2048" + RSA_3072 KeySpec = "RSA_3072" + RSA_4096 KeySpec = "RSA_4096" + EC_256 KeySpec = "EC_256" + EC_384 KeySpec = "EC_384" + EC_512 KeySpec = "EC_512" +) + +// HashFunc returns the Hash associated k. +func (k KeySpec) HashFunc() crypto.Hash { + switch k { + case RSA_2048, EC_256: + return crypto.SHA256 + case RSA_3072, EC_384: + return crypto.SHA384 + case RSA_4096, EC_512: + return crypto.SHA512 + } + return crypto.Hash(0) +} + // Command is a CLI command available in the plugin contract. type Command string @@ -57,7 +88,7 @@ type DescribeKeyResponse struct { // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - KeySpec string `json:"keySpec"` + KeySpec KeySpec `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index f606703d..82215afb 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -77,7 +77,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign } alg := keySpecToAlg(key.KeySpec) if alg == "" { - return nil, errors.New("unsupported key spec: " + key.KeySpec) + return nil, errors.New("unsupported key spec: " + string(key.KeySpec)) } // Generate signing string. token := &jwt.Token{ @@ -173,19 +173,19 @@ func verifyJWT(sigAlg string, payload string, sig []byte, certChain []*x509.Cert return method.Verify(payload, encSig, signingCert.PublicKey) } -func keySpecToAlg(name string) string { +func keySpecToAlg(name plugin.KeySpec) string { switch name { - case "RSA_2048": + case plugin.RSA_2048: return jwt.SigningMethodPS256.Alg() - case "RSA_3072": + case plugin.RSA_3072: return jwt.SigningMethodPS384.Alg() - case "RSA_4096": + case plugin.RSA_4096: return jwt.SigningMethodPS512.Alg() - case "EC_256": + case plugin.EC_256: return jwt.SigningMethodES256.Alg() - case "EC_384": + case plugin.EC_384: return jwt.SigningMethodES384.Alg() - case "EC_512": + case plugin.EC_512: return jwt.SigningMethodES512.Alg() } return "" From 4d98f6d049e2210839dbc8fecaa7e784411ca252 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 5 May 2022 11:26:54 +0200 Subject: [PATCH 28/58] add signing tests Signed-off-by: qmuntal --- plugin/plugin.go | 59 ++++++--- signature/jws/plugin.go | 68 +++++++---- signature/jws/plugin_test.go | 231 +++++++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 43 deletions(-) create mode 100644 signature/jws/plugin_test.go diff --git a/plugin/plugin.go b/plugin/plugin.go index 67089042..f5829a77 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -21,17 +21,39 @@ const ( EC_512 KeySpec = "EC_512" ) -// HashFunc returns the Hash associated k. -func (k KeySpec) HashFunc() crypto.Hash { +// Hash returns the Hash associated k. +func (k KeySpec) Hash() Hash { switch k { case RSA_2048, EC_256: - return crypto.SHA256 + return SHA256 case RSA_3072, EC_384: - return crypto.SHA384 + return SHA384 case RSA_4096, EC_512: + return SHA512 + } + return "" +} + +// Hash algorithm associated with the key spec. +type Hash string + +const ( + SHA256 Hash = "SHA_256" + SHA384 Hash = "SHA_384" + SHA512 Hash = "SHA_512" +) + +// HashFunc returns the Hash associated k. +func (h Hash) HashFunc() crypto.Hash { + switch h { + case SHA256: + return crypto.SHA256 + case SHA384: + return crypto.SHA384 + case SHA512: return crypto.SHA512 } - return crypto.Hash(0) + return 0 } // Command is a CLI command available in the plugin contract. @@ -78,7 +100,7 @@ type DescribeKeyRequest struct { ContractVersion string `json:"contractVersion"` KeyName string `json:"keyName"` KeyID string `json:"keyId"` - PluginConfig map[string]string `json:"pluginConfig"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } // GenerateSignatureResponse is the response of a describe-key request. @@ -94,10 +116,14 @@ type DescribeKeyResponse struct { // GenerateSignatureRequest contains the parameters passed in a generate-signature request. // All parameters are required. type GenerateSignatureRequest struct { - ContractVersion string `json:"contractVersion"` - KeyName string `json:"keyName"` - KeyID string `json:"keyId"` - Payload string `json:"payload"` + ContractVersion string `json:"contractVersion"` + KeyName string `json:"keyName"` + KeyID string `json:"keyId"` + KeySpec KeySpec `json:"keySpec"` + Hash Hash `json:"hashAlgorithm"` + SignatureEnvelopeType string `json:"signatureEnvelopeType"` + Payload string `json:"payload"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } // GenerateSignatureResponse is the response of a generate-signature request. @@ -120,12 +146,13 @@ type GenerateSignatureResponse struct { // GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. // All parameters are required. type GenerateEnvelopeRequest struct { - ContractVersion string `json:"contractVersion"` - KeyName string `json:"keyName"` - KeyID string `json:"keyId"` - PayloadType string `json:"payloadType"` - SignatureEnvelopeType string `json:"signatureEnvelopeType"` - Payload string `json:"payload"` + ContractVersion string `json:"contractVersion"` + KeyName string `json:"keyName"` + KeyID string `json:"keyId"` + PayloadType string `json:"payloadType"` + SignatureEnvelopeType string `json:"signatureEnvelopeType"` + Payload string `json:"payload"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } // GenerateSignatureResponse is the response of a generate-envelop request. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 82215afb..7db47109 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -22,10 +22,28 @@ var supportedAlgs = map[string]bool{ jwt.SigningMethodES512.Name: true, } +var keySpecToAlg = map[plugin.KeySpec]string{ + plugin.RSA_2048: jwt.SigningMethodPS256.Alg(), + plugin.RSA_3072: jwt.SigningMethodPS384.Alg(), + plugin.RSA_4096: jwt.SigningMethodPS512.Alg(), + plugin.EC_256: jwt.SigningMethodES256.Alg(), + plugin.EC_384: jwt.SigningMethodES384.Alg(), + plugin.EC_512: jwt.SigningMethodES512.Alg(), +} + +// PluginRunner is the interface implemented by plugin/manager.Manager, +// but which can be swapped by a custom third-party implementation +// if this constrains are meet: +// - Run fails if the plugin does not exist or is not valid +// - Run returns the appropriate type for each cmd type PluginRunner interface { Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) } +// PluginSigner signs artifacts and generates JWS signatures +// by delegating the one or both operations to the named plugin, +// as defined in +// https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces. type PluginSigner struct { Runner PluginRunner PluginName string @@ -34,6 +52,7 @@ type PluginSigner struct { PluginConfig map[string]string } +// Sign signs the artifact described by its descriptor, and returns the signature. func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGetMetadata, nil) if err != nil { @@ -56,7 +75,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts } func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResponse, error) { - req := plugin.DescribeKeyRequest{ + req := &plugin.DescribeKeyRequest{ ContractVersion: "1", KeyName: s.KeyName, KeyID: s.KeyID, @@ -75,9 +94,14 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign if err != nil { return nil, err } - alg := keySpecToAlg(key.KeySpec) + + // Check keyID is honored. + if s.KeyID != key.KeyID { + return nil, fmt.Errorf("keyID mismatch") + } + alg := keySpecToAlg[key.KeySpec] if alg == "" { - return nil, errors.New("unsupported key spec: " + string(key.KeySpec)) + return nil, fmt.Errorf("keySpec %q not supported: ", key.KeySpec) } // Generate signing string. token := &jwt.Token{ @@ -90,22 +114,30 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign } signing, err := token.SigningString() if err != nil { - return nil, fmt.Errorf("failed to marshal signing payload: %w", err) + return nil, fmt.Errorf("failed to marshal signing payload: %v", err) } // Execute plugin sign command. - req := plugin.GenerateSignatureRequest{ + req := &plugin.GenerateSignatureRequest{ ContractVersion: "1", KeyName: s.KeyName, KeyID: s.KeyID, + KeySpec: key.KeySpec, + Hash: key.KeySpec.Hash(), Payload: signing, + PluginConfig: s.PluginConfig, } out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGenerateSignature, req) if err != nil { - return nil, fmt.Errorf("sign command failed: %w", err) + return nil, fmt.Errorf("generate-signature command failed: %w", err) } resp := out.(*plugin.GenerateSignatureResponse) + // Check keyID is honored. + if s.KeyID != resp.KeyID { + return nil, fmt.Errorf("keyID mismatch") + } + // Check algorithm is supported. if !supportedAlgs[resp.SigningAlgorithm] { return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) @@ -120,11 +152,11 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign // using the public key of the signing certificate. signed, err := base64.RawStdEncoding.DecodeString(resp.Signature) if err != nil { - return nil, err + return nil, fmt.Errorf("signature not base64-encoded: %v", err) } err = verifyJWT(resp.SigningAlgorithm, signing, signed, certs) if err != nil { - return nil, err + return nil, fmt.Errorf("verification error: %v", err) } // Check the the certificate chain conforms to the spec. @@ -151,7 +183,7 @@ func parseCertChain(certChain []string) ([]*x509.Certificate, error) { for i, data := range certChain { der, err := base64.RawStdEncoding.DecodeString(data) if err != nil { - return nil, err + return nil, fmt.Errorf("certificate not base64-encoded: %v", err) } cert, err := x509.ParseCertificate(der) if err != nil { @@ -172,21 +204,3 @@ func verifyJWT(sigAlg string, payload string, sig []byte, certChain []*x509.Cert encSig := base64.RawURLEncoding.EncodeToString(sig) return method.Verify(payload, encSig, signingCert.PublicKey) } - -func keySpecToAlg(name plugin.KeySpec) string { - switch name { - case plugin.RSA_2048: - return jwt.SigningMethodPS256.Alg() - case plugin.RSA_3072: - return jwt.SigningMethodPS384.Alg() - case plugin.RSA_4096: - return jwt.SigningMethodPS512.Alg() - case plugin.EC_256: - return jwt.SigningMethodES256.Alg() - case plugin.EC_384: - return jwt.SigningMethodES384.Alg() - case plugin.EC_512: - return jwt.SigningMethodES512.Alg() - } - return "" -} diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go new file mode 100644 index 00000000..1ff2d3f0 --- /dev/null +++ b/signature/jws/plugin_test.go @@ -0,0 +1,231 @@ +package jws + +import ( + "context" + "encoding/base64" + "errors" + "strings" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/plugin" +) + +type mockRunner struct { + resp []interface{} + err []error + n int +} + +func (r *mockRunner) Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) { + defer func() { r.n++ }() + return r.resp[r.n], r.err[r.n] +} + +type mockSignerPlugin struct { + KeyID string + KeySpec plugin.KeySpec + Sign func(payload string) string + SigningAlg string + Cert string + n int +} + +func (s *mockSignerPlugin) Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) { + defer func() { s.n++ }() + switch s.n { + case 0: + return &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, nil + case 1: + return &plugin.DescribeKeyResponse{KeyID: s.KeyID, KeySpec: s.KeySpec}, nil + case 2: + var signed string + if s.Sign != nil { + signed = s.Sign(req.(*plugin.GenerateSignatureRequest).Payload) + } + return &plugin.GenerateSignatureResponse{ + KeyID: s.KeyID, + SigningAlgorithm: s.SigningAlg, + Signature: signed, + CertificateChain: []string{s.Cert}, + }, nil + } + panic("too many calls") +} + +func testPluginSignerError(t *testing.T, signer PluginSigner, wantEr string) { + t.Helper() + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) + if err == nil || !strings.Contains(err.Error(), wantEr) { + t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) + } +} + +func TestPluginSigner_Sign_RunMetadataFails(t *testing.T) { + signer := PluginSigner{ + Runner: &mockRunner{[]interface{}{nil}, []error{errors.New("failed")}, 0}, + } + testPluginSignerError(t, signer, "metadata command failed") +} + +func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { + signer := PluginSigner{ + Runner: &mockRunner{[]interface{}{ + &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, + }, []error{nil}, 0}, + } + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) + wantEr := "token is expired" + if err == nil || !strings.Contains(err.Error(), wantEr) { + t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) + } +} + +func TestPluginSigner_Sign_NoCapability(t *testing.T) { + signer := PluginSigner{ + Runner: &mockRunner{[]interface{}{ + &plugin.Metadata{Capabilities: []plugin.Capability{}}, + }, []error{nil}, 0}, + } + testPluginSignerError(t, signer, "does not have signing capabilities") +} + +func TestPluginSigner_Sign_DescribeKeyFailed(t *testing.T) { + signer := PluginSigner{ + Runner: &mockRunner{[]interface{}{ + &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, + nil, + }, []error{nil, errors.New("failed")}, 0}, + } + testPluginSignerError(t, signer, "describe-key command failed") +} + +func TestPluginSigner_Sign_DescribeKeyKeyIDMismatch(t *testing.T) { + signer := PluginSigner{ + Runner: &mockSignerPlugin{KeyID: "2", KeySpec: plugin.RSA_2048}, + KeyID: "1", + } + testPluginSignerError(t, signer, "keyID mismatch") +} + +func TestPluginSigner_Sign_KeySpecNotSupported(t *testing.T) { + signer := PluginSigner{ + Runner: &mockSignerPlugin{KeyID: "1", KeySpec: "custom"}, + KeyID: "1", + } + testPluginSignerError(t, signer, "keySpec \"custom\" not supported") +} + +func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { + signer := PluginSigner{ + Runner: &mockRunner{[]interface{}{ + &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, + &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: plugin.RSA_2048}, + &plugin.GenerateSignatureResponse{KeyID: "2"}, + }, []error{nil, nil, nil}, 0}, + KeyID: "1", + } + testPluginSignerError(t, signer, "keyID mismatch") +} + +func TestPluginSigner_Sign_UnsuportedAlgorithm(t *testing.T) { + signer := PluginSigner{ + Runner: &mockSignerPlugin{KeyID: "1", KeySpec: plugin.RSA_2048, SigningAlg: "custom"}, + KeyID: "1", + } + testPluginSignerError(t, signer, "signing algorithm \"custom\" not supported") +} + +func TestPluginSigner_Sign_CertNotBase64(t *testing.T) { + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodES256.Alg(), Cert: "r a w", + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "certificate not base64-encoded") +} + +func TestPluginSigner_Sign_InvalidCert(t *testing.T) { + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodES256.Alg(), + Cert: base64.RawStdEncoding.EncodeToString([]byte("mocked")), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "x509: malformed certificate") +} + +func TestPluginSigner_Sign_SignatureNotBase64(t *testing.T) { + _, cert, err := generateKeyCertPair() + if err != nil { + t.Fatalf("generateKeyCertPair() error = %v", err) + } + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodES256.Alg(), + Sign: func(payload string) string { return "r a w" }, + Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "signature not base64-encoded") +} + +func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { + _, cert, err := generateKeyCertPair() + if err != nil { + t.Fatalf("generateKeyCertPair() error = %v", err) + } + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodES256.Alg(), + Sign: func(payload string) string { return base64.RawStdEncoding.EncodeToString([]byte("r a w")) }, + Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "verification error") +} + +func TestPluginSigner_Sign_Valid(t *testing.T) { + key, cert, err := generateKeyCertPair() + if err != nil { + t.Fatal(err) + } + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodPS256.Alg(), + Sign: func(payload string) string { + signed, err := jwt.SigningMethodPS256.Sign(payload, key) + if err != nil { + t.Fatal(err) + } + encSigned, err := base64.RawURLEncoding.DecodeString(signed) + if err != nil { + t.Fatal(err) + } + return base64.RawStdEncoding.EncodeToString(encSigned) + }, + Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), + }, + KeyID: "1", + } + _, err = signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) + if err != nil { + t.Errorf("PluginSigner.Sign() error = %v, wantErr nil", err) + } +} From 01a45dbce260f603bcb6716bb9efef8e1043973c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 13:28:57 +0800 Subject: [PATCH 29/58] Bump actions/setup-go from 2 to 3 (#32) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: qmuntal --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9096c62b..3f9fc1cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: Check out code From 8ea75121315826d335beeae41a598f575ed50b89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 13:29:33 +0800 Subject: [PATCH 30/58] Bump actions/cache from 2 to 3.0.1 (#31) Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.0.1. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v2...v3.0.1) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: qmuntal --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f9fc1cf..adf717ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: - name: Check out code uses: actions/checkout@v3 - name: Cache Go modules - uses: actions/cache@v2 + uses: actions/cache@v3.0.1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} From c6553f8e2bcc1745b9c8020f4fb090e888bd9f54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 13:32:35 +0800 Subject: [PATCH 31/58] Bump github.com/golang-jwt/jwt/v4 from 4.3.0 to 4.4.1 (#29) Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.3.0 to 4.4.1. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md) - [Commits](https://github.com/golang-jwt/jwt/compare/v4.3.0...v4.4.1) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v4 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: qmuntal --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7ee534f9..e1d1ec6d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/notaryproject/notation-go go 1.17 require ( - github.com/golang-jwt/jwt/v4 v4.3.0 + github.com/golang-jwt/jwt/v4 v4.4.1 github.com/opencontainers/go-digest v1.0.0 ) diff --git a/go.sum b/go.sum index b864bbe3..437b5167 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= From 03b1a432516cf91a18f1928e5e9b532f425340ee Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 5 May 2022 18:20:01 +0200 Subject: [PATCH 32/58] check extension ID length and add tests Signed-off-by: qmuntal --- signature/jws/plugin_test.go | 124 +++++++++++++++++++++++++++++++---- signature/jws/spec.go | 16 +++-- 2 files changed, 121 insertions(+), 19 deletions(-) diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 1ff2d3f0..91a533fc 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -2,8 +2,13 @@ package jws import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" "encoding/base64" "errors" + "math/big" "strings" "testing" "time" @@ -150,7 +155,7 @@ func TestPluginSigner_Sign_CertNotBase64(t *testing.T) { testPluginSignerError(t, signer, "certificate not base64-encoded") } -func TestPluginSigner_Sign_InvalidCert(t *testing.T) { +func TestPluginSigner_Sign_MalformedCert(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", @@ -199,6 +204,109 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { testPluginSignerError(t, signer, "verification error") } +func validSign(t *testing.T, key interface{}) func(string) string { + t.Helper() + return func(payload string) string { + signed, err := jwt.SigningMethodPS256.Sign(payload, key) + if err != nil { + t.Fatal(err) + } + encSigned, err := base64.RawURLEncoding.DecodeString(signed) + if err != nil { + t.Fatal(err) + } + return base64.RawStdEncoding.EncodeToString(encSigned) + } +} + +func TestPluginSigner_Sign_CertWithoutDigitalSignatureBit(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(0), + Subject: pkix.Name{CommonName: "test"}, + KeyUsage: x509.KeyUsageEncipherOnly, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + } + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + t.Fatal(err) + } + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodPS256.Alg(), + Sign: validSign(t, key), + Cert: base64.RawStdEncoding.EncodeToString(certBytes), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "keyUsage must have the bit positions for digitalSignature set") +} + +func TestPluginSigner_Sign_CertWithout_idkpcodeSigning(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(0), + Subject: pkix.Name{CommonName: "test"}, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + t.Fatal(err) + } + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodPS256.Alg(), + Sign: validSign(t, key), + Cert: base64.RawStdEncoding.EncodeToString(certBytes), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "extKeyUsage must contain") +} + +func TestPluginSigner_Sign_CertBasicConstraintCA(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(0), + Subject: pkix.Name{CommonName: "test"}, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + IsCA: true, + } + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + t.Fatal(err) + } + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodPS256.Alg(), + Sign: validSign(t, key), + Cert: base64.RawStdEncoding.EncodeToString(certBytes), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "if the basicConstraints extension is present, the CA field MUST be set false") +} + func TestPluginSigner_Sign_Valid(t *testing.T) { key, cert, err := generateKeyCertPair() if err != nil { @@ -209,18 +317,8 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { KeyID: "1", KeySpec: plugin.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), - Sign: func(payload string) string { - signed, err := jwt.SigningMethodPS256.Sign(payload, key) - if err != nil { - t.Fatal(err) - } - encSigned, err := base64.RawURLEncoding.DecodeString(signed) - if err != nil { - t.Fatal(err) - } - return base64.RawStdEncoding.EncodeToString(encSigned) - }, - Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), + Sign: validSign(t, key), + Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), }, KeyID: "1", } diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 15c61fc6..64992f77 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -72,6 +72,10 @@ func checkCertChain(certChain []*x509.Certificate) error { return nil } +var ( + oidExtensionKeyUsage = []int{2, 5, 29, 15} +) + // validateCert checks cert meets the requirements defined in // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#certificate-requirements. func verifyCert(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { @@ -86,18 +90,18 @@ func verifyCert(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { } } if !hasExtKeyUsage { - return fmt.Errorf("extKeyUsage must contain be %d", extKeyUsage) + return fmt.Errorf("extKeyUsage must contain %d", extKeyUsage) } - for _, ext := range cert.Extensions { - switch ext.Id[3] { - case 15: - if !ext.Critical { + for _, e := range cert.Extensions { + if e.Id.Equal(oidExtensionKeyUsage) { + if !e.Critical { return errors.New("the keyUsage extension must be marked critical") } + break } } if cert.BasicConstraintsValid && cert.IsCA { - return errors.New("if the basicConstraints extension is present, the cA field MUST be set false") + return errors.New("if the basicConstraints extension is present, the CA field MUST be set false") } switch key := cert.PublicKey.(type) { case *rsa.PublicKey: From 7bbec7e947c043ab1daf57fd8ab2619aa009db40 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 5 May 2022 19:13:09 +0200 Subject: [PATCH 33/58] fail on empty certificate chain Signed-off-by: qmuntal --- signature/jws/plugin.go | 13 +++++++------ signature/jws/plugin_test.go | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 7db47109..329f0b3b 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -143,6 +143,11 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) } + // Check certificate chain is not empty. + if len(resp.CertificateChain) == 0 { + return nil, errors.New("empty certificate chain") + } + certs, err := parseCertChain(resp.CertificateChain) if err != nil { return nil, err @@ -154,7 +159,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign if err != nil { return nil, fmt.Errorf("signature not base64-encoded: %v", err) } - err = verifyJWT(resp.SigningAlgorithm, signing, signed, certs) + err = verifyJWT(resp.SigningAlgorithm, signing, signed, certs[0]) if err != nil { return nil, fmt.Errorf("verification error: %v", err) } @@ -194,11 +199,7 @@ func parseCertChain(certChain []string) ([]*x509.Certificate, error) { return certs, nil } -func verifyJWT(sigAlg string, payload string, sig []byte, certChain []*x509.Certificate) error { - if len(certChain) == 0 { - return nil - } - signingCert := certChain[0] +func verifyJWT(sigAlg string, payload string, sig []byte, signingCert *x509.Certificate) error { // Verify the hash of req.payload against resp.signature using the public key if the leaf certificate. method := jwt.GetSigningMethod(sigAlg) encSig := base64.RawURLEncoding.EncodeToString(sig) diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 91a533fc..313be751 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -39,6 +39,10 @@ type mockSignerPlugin struct { } func (s *mockSignerPlugin) Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) { + var chain []string + if s.Cert != "" { + chain = append(chain, s.Cert) + } defer func() { s.n++ }() switch s.n { case 0: @@ -54,7 +58,7 @@ func (s *mockSignerPlugin) Run(ctx context.Context, pluginName string, cmd plugi KeyID: s.KeyID, SigningAlgorithm: s.SigningAlg, Signature: signed, - CertificateChain: []string{s.Cert}, + CertificateChain: chain, }, nil } panic("too many calls") @@ -143,12 +147,25 @@ func TestPluginSigner_Sign_UnsuportedAlgorithm(t *testing.T) { testPluginSignerError(t, signer, "signing algorithm \"custom\" not supported") } +func TestPluginSigner_Sign_NoCertChain(t *testing.T) { + signer := PluginSigner{ + Runner: &mockSignerPlugin{ + KeyID: "1", + KeySpec: plugin.RSA_2048, + SigningAlg: jwt.SigningMethodES256.Alg(), + }, + KeyID: "1", + } + testPluginSignerError(t, signer, "empty certificate chain") +} + func TestPluginSigner_Sign_CertNotBase64(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: plugin.RSA_2048, - SigningAlg: jwt.SigningMethodES256.Alg(), Cert: "r a w", + SigningAlg: jwt.SigningMethodES256.Alg(), + Cert: "r a w", }, KeyID: "1", } From c3265c36c0a8d1c932d533efb3f7d32e3d532f73 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 5 May 2022 20:06:29 +0200 Subject: [PATCH 34/58] create and use spec package Signed-off-by: qmuntal --- notation.go | 26 +------ plugin/manager/integration_test.go | 2 +- plugin/manager/manager.go | 2 +- plugin/manager/manager_test.go | 2 +- signature/jws/plugin.go | 25 +++---- signature/jws/plugin_test.go | 35 ++++----- signature/jws/signer.go | 10 ++- signature/jws/signer_test.go | 7 +- signature/jws/spec.go | 35 +++------ signature/jws/verifier.go | 26 ++++--- {plugin => spec/v1/plugin}/errors.go | 0 {plugin => spec/v1/plugin}/errors_test.go | 0 {plugin => spec/v1/plugin}/metadata.go | 0 {plugin => spec/v1/plugin}/metadata_test.go | 0 {plugin => spec/v1/plugin}/plugin.go | 61 ++-------------- spec/v1/signature/jws.go | 56 +++++++++++++++ spec/v1/signature/types.go | 80 +++++++++++++++++++++ 17 files changed, 208 insertions(+), 159 deletions(-) rename {plugin => spec/v1/plugin}/errors.go (100%) rename {plugin => spec/v1/plugin}/errors_test.go (100%) rename {plugin => spec/v1/plugin}/metadata.go (100%) rename {plugin => spec/v1/plugin}/metadata_test.go (100%) rename {plugin => spec/v1/plugin}/plugin.go (78%) create mode 100644 spec/v1/signature/jws.go create mode 100644 spec/v1/signature/types.go diff --git a/notation.go b/notation.go index abf72404..b8a78931 100644 --- a/notation.go +++ b/notation.go @@ -6,29 +6,9 @@ import ( "time" "github.com/notaryproject/notation-go/crypto/timestamp" - "github.com/opencontainers/go-digest" + "github.com/notaryproject/notation-go/spec/v1/signature" ) -// Descriptor describes the content signed or to be signed. -type Descriptor struct { - // MediaType is the media type of the targeted content. - MediaType string `json:"mediaType"` - - // Digest is the digest of the targeted content. - Digest digest.Digest `json:"digest"` - - // Size specifies the size in bytes of the blob. - Size int64 `json:"size"` - - // Annotations contains optional user defined attributes. - Annotations map[string]string `json:"annotations,omitempty"` -} - -// Equal reports whether d and t points to the same content. -func (d Descriptor) Equal(t Descriptor) bool { - return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size -} - // SignOptions contains parameters for Signer.Sign. type SignOptions struct { // Expiry identifies the expiration time of the resulted signature. @@ -50,7 +30,7 @@ type SignOptions struct { type Signer interface { // Sign signs the artifact described by its descriptor, // and returns the signature. - Sign(ctx context.Context, desc Descriptor, opts SignOptions) ([]byte, error) + Sign(ctx context.Context, desc signature.Descriptor, opts SignOptions) ([]byte, error) } // VerifyOptions contains parameters for Verifier.Verify. @@ -65,7 +45,7 @@ func (opts VerifyOptions) Validate() error { type Verifier interface { // Verify verifies the signature and returns the verified descriptor and // metadata of the signed artifact. - Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, error) + Verify(ctx context.Context, signature []byte, opts VerifyOptions) (signature.Descriptor, error) } // Service combines the signing and verification services. diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index f8334b11..6e6a4605 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -9,7 +9,7 @@ import ( "reflect" "testing" - "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/spec/v1/plugin" ) func preparePlugin(t *testing.T) string { diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index ea16f657..19f1c910 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -13,7 +13,7 @@ import ( "path/filepath" "runtime" - "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/spec/v1/plugin" ) // Plugin represents a potential plugin with all it's metadata. diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 18f85ede..ad083ddf 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -10,7 +10,7 @@ import ( "testing" "testing/fstest" - "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/spec/v1/plugin" ) type testCommander struct { diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 329f0b3b..c58f239a 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -10,7 +10,8 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/spec/v1/plugin" + "github.com/notaryproject/notation-go/spec/v1/signature" ) var supportedAlgs = map[string]bool{ @@ -22,13 +23,13 @@ var supportedAlgs = map[string]bool{ jwt.SigningMethodES512.Name: true, } -var keySpecToAlg = map[plugin.KeySpec]string{ - plugin.RSA_2048: jwt.SigningMethodPS256.Alg(), - plugin.RSA_3072: jwt.SigningMethodPS384.Alg(), - plugin.RSA_4096: jwt.SigningMethodPS512.Alg(), - plugin.EC_256: jwt.SigningMethodES256.Alg(), - plugin.EC_384: jwt.SigningMethodES384.Alg(), - plugin.EC_512: jwt.SigningMethodES512.Alg(), +var keySpecToAlg = map[signature.Key]string{ + signature.RSA_2048: jwt.SigningMethodPS256.Alg(), + signature.RSA_3072: jwt.SigningMethodPS384.Alg(), + signature.RSA_4096: jwt.SigningMethodPS512.Alg(), + signature.EC_256: jwt.SigningMethodES256.Alg(), + signature.EC_384: jwt.SigningMethodES384.Alg(), + signature.EC_512: jwt.SigningMethodES512.Alg(), } // PluginRunner is the interface implemented by plugin/manager.Manager, @@ -53,7 +54,7 @@ type PluginSigner struct { } // Sign signs the artifact described by its descriptor, and returns the signature. -func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { +func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGetMetadata, nil) if err != nil { return nil, fmt.Errorf("metadata command failed: %w", err) @@ -88,7 +89,7 @@ func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResp return out.(*plugin.DescribeKeyResponse), nil } -func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.SignOptions, payload *payload) ([]byte, error) { +func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.SignOptions, payload jwt.Claims) ([]byte, error) { // Get key info. key, err := s.describeKey(ctx) if err != nil { @@ -107,7 +108,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign token := &jwt.Token{ Header: map[string]interface{}{ "alg": alg, - "cty": MediaTypeNotationPayload, + "cty": signature.MediaTypeJWSEnvelope, "crit": []string{"cty"}, }, Claims: payload, @@ -179,7 +180,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign return jwtEnvelop(ctx, opts, compact, rawCerts) } -func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, opts notation.SignOptions, payload *payload) ([]byte, error) { +func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, opts notation.SignOptions, payload jwt.Claims) ([]byte, error) { return nil, errors.New("not implemented") } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 313be751..2687dd31 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -15,7 +15,8 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/spec/v1/plugin" + "github.com/notaryproject/notation-go/spec/v1/signature" ) type mockRunner struct { @@ -31,7 +32,7 @@ func (r *mockRunner) Run(ctx context.Context, pluginName string, cmd plugin.Comm type mockSignerPlugin struct { KeyID string - KeySpec plugin.KeySpec + KeySpec signature.Key Sign func(payload string) string SigningAlg string Cert string @@ -66,7 +67,7 @@ func (s *mockSignerPlugin) Run(ctx context.Context, pluginName string, cmd plugi func testPluginSignerError(t *testing.T, signer PluginSigner, wantEr string) { t.Helper() - _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) + _, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{}) if err == nil || !strings.Contains(err.Error(), wantEr) { t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) } @@ -85,7 +86,7 @@ func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, }, []error{nil}, 0}, } - _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) + _, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) wantEr := "token is expired" if err == nil || !strings.Contains(err.Error(), wantEr) { t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) @@ -113,7 +114,7 @@ func TestPluginSigner_Sign_DescribeKeyFailed(t *testing.T) { func TestPluginSigner_Sign_DescribeKeyKeyIDMismatch(t *testing.T) { signer := PluginSigner{ - Runner: &mockSignerPlugin{KeyID: "2", KeySpec: plugin.RSA_2048}, + Runner: &mockSignerPlugin{KeyID: "2", KeySpec: signature.RSA_2048}, KeyID: "1", } testPluginSignerError(t, signer, "keyID mismatch") @@ -131,7 +132,7 @@ func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, - &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: plugin.RSA_2048}, + &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signature.RSA_2048}, &plugin.GenerateSignatureResponse{KeyID: "2"}, }, []error{nil, nil, nil}, 0}, KeyID: "1", @@ -141,7 +142,7 @@ func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { func TestPluginSigner_Sign_UnsuportedAlgorithm(t *testing.T) { signer := PluginSigner{ - Runner: &mockSignerPlugin{KeyID: "1", KeySpec: plugin.RSA_2048, SigningAlg: "custom"}, + Runner: &mockSignerPlugin{KeyID: "1", KeySpec: signature.RSA_2048, SigningAlg: "custom"}, KeyID: "1", } testPluginSignerError(t, signer, "signing algorithm \"custom\" not supported") @@ -151,7 +152,7 @@ func TestPluginSigner_Sign_NoCertChain(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), }, KeyID: "1", @@ -163,7 +164,7 @@ func TestPluginSigner_Sign_CertNotBase64(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), Cert: "r a w", }, @@ -176,7 +177,7 @@ func TestPluginSigner_Sign_MalformedCert(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), Cert: base64.RawStdEncoding.EncodeToString([]byte("mocked")), }, @@ -193,7 +194,7 @@ func TestPluginSigner_Sign_SignatureNotBase64(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), Sign: func(payload string) string { return "r a w" }, Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), @@ -211,7 +212,7 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), Sign: func(payload string) string { return base64.RawStdEncoding.EncodeToString([]byte("r a w")) }, Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), @@ -255,7 +256,7 @@ func TestPluginSigner_Sign_CertWithoutDigitalSignatureBit(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), Cert: base64.RawStdEncoding.EncodeToString(certBytes), @@ -284,7 +285,7 @@ func TestPluginSigner_Sign_CertWithout_idkpcodeSigning(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), Cert: base64.RawStdEncoding.EncodeToString(certBytes), @@ -314,7 +315,7 @@ func TestPluginSigner_Sign_CertBasicConstraintCA(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), Cert: base64.RawStdEncoding.EncodeToString(certBytes), @@ -332,14 +333,14 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: plugin.RSA_2048, + KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), }, KeyID: "1", } - _, err = signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) + _, err = signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{}) if err != nil { t.Errorf("PluginSigner.Sign() error = %v, wantErr nil", err) } diff --git a/signature/jws/signer.go b/signature/jws/signer.go index bfbcf815..ab87a50a 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -14,6 +14,7 @@ import ( "github.com/notaryproject/notation-go/crypto/jwsutil" "github.com/notaryproject/notation-go/crypto/timestamp" "github.com/notaryproject/notation-go/internal/crypto/pki" + "github.com/notaryproject/notation-go/spec/v1/signature" ) // Signer signs artifacts and generates JWS signatures. @@ -80,7 +81,7 @@ func NewSignerWithCertificateChain(method jwt.SigningMethod, key crypto.PrivateK } // Sign signs the artifact described by its descriptor, and returns the signature. -func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { +func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { // generate JWT payload := packPayload(desc, opts) if err := payload.Valid(); err != nil { @@ -89,10 +90,7 @@ func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notati token := &jwt.Token{ Header: map[string]interface{}{ "alg": s.method.Alg(), - "cty": MediaTypeNotationPayload, - "crit": []string{ - "cty", - }, + "cty": signature.MediaTypeJWSEnvelope, }, Claims: payload, Method: s.method, @@ -106,7 +104,7 @@ func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notati func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { // generate unprotected header - header := unprotectedHeader{ + header := signature.JWSUnprotectedHeader{ CertChain: certChain, } diff --git a/signature/jws/signer_test.go b/signature/jws/signer_test.go index 38896479..5d6d1ab4 100644 --- a/signature/jws/signer_test.go +++ b/signature/jws/signer_test.go @@ -13,6 +13,7 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp/timestamptest" + "github.com/notaryproject/notation-go/spec/v1/signature" "github.com/opencontainers/go-digest" ) @@ -115,11 +116,11 @@ func TestSignWithoutExpiry(t *testing.T) { } // generateSigningContent generates common signing content with options for testing. -func generateSigningContent(tsa *timestamptest.TSA) (notation.Descriptor, notation.SignOptions) { +func generateSigningContent(tsa *timestamptest.TSA) (signature.Descriptor, notation.SignOptions) { content := "hello world" - desc := notation.Descriptor{ + desc := signature.Descriptor{ MediaType: "test media type", - Digest: digest.Canonical.FromString(content), + Digest: string(digest.Canonical.FromString(content)), Size: int64(len(content)), Annotations: map[string]string{ "identity": "test.registry.io/test:example", diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 64992f77..76887e13 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -13,43 +13,24 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/spec/v1/signature" ) -// unprotectedHeader contains the header parameters that are not integrity protected. -type unprotectedHeader struct { - TimeStampToken []byte `json:"timestamp,omitempty"` - CertChain [][]byte `json:"x5c,omitempty"` -} - -// MediaTypeNotationPayload describes the media type of the payload of notation signature. -const MediaTypeNotationPayload = "application/vnd.cncf.notary.v2.jws.v1" - -// payload contains the subject manifest and other attributes that have to be integrity -// protected. -type payload struct { - Notation notationClaim `json:"notary"` - jwt.RegisteredClaims -} - -// notationClaim is the top level node and private claim, encapsulating the notary v2 data. -type notationClaim struct { - Subject notation.Descriptor `json:"subject"` -} - // packPayload generates JWS payload according the signing content and options. -func packPayload(desc notation.Descriptor, opts notation.SignOptions) *payload { +func packPayload(desc signature.Descriptor, opts notation.SignOptions) jwt.Claims { var expiresAt *jwt.NumericDate if !opts.Expiry.IsZero() { expiresAt = jwt.NewNumericDate(opts.Expiry) } - return &payload{ - Notation: notationClaim{ - Subject: desc, - }, + return struct { + jwt.RegisteredClaims + Notary signature.JWSNotaryClaim `json:"notary"` + }{ RegisteredClaims: jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: expiresAt, + IssuedAt: jwt.NewNumericDate(time.Now()), }, + Notary: signature.JWSNotaryClaim{Subject: desc}, } } diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index 66485082..8acc07c6 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -14,6 +14,7 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/jwsutil" "github.com/notaryproject/notation-go/crypto/timestamp" + "github.com/notaryproject/notation-go/spec/v1/signature" ) // maxTimestampAccuracy specifies the max acceptable accuracy for timestamp. @@ -55,23 +56,23 @@ func NewVerifier() *Verifier { // Verify verifies the signature and returns the verified descriptor and // metadata of the signed artifact. -func (v *Verifier) Verify(ctx context.Context, signature []byte, opts notation.VerifyOptions) (notation.Descriptor, error) { +func (v *Verifier) Verify(ctx context.Context, sig []byte, opts notation.VerifyOptions) (signature.Descriptor, error) { // unpack envelope - sig, err := openEnvelope(signature) + envelope, err := openEnvelope(sig) if err != nil { - return notation.Descriptor{}, err + return signature.Descriptor{}, err } // verify signing identity - method, key, err := v.verifySigner(&sig.Signature) + method, key, err := v.verifySigner(&envelope.Signature) if err != nil { - return notation.Descriptor{}, err + return signature.Descriptor{}, err } // verify JWT - claim, err := v.verifyJWT(method, key, sig.SerializeCompact()) + claim, err := v.verifyJWT(method, key, envelope.SerializeCompact()) if err != nil { - return notation.Descriptor{}, err + return signature.Descriptor{}, err } return claim.Subject, nil @@ -79,7 +80,7 @@ func (v *Verifier) Verify(ctx context.Context, signature []byte, opts notation.V // verifySigner verifies the signing identity and returns the verification key. func (v *Verifier) verifySigner(sig *jwsutil.Signature) (jwt.SigningMethod, crypto.PublicKey, error) { - var header unprotectedHeader + var header signature.JWSUnprotectedHeader if err := json.Unmarshal(sig.Unprotected, &header); err != nil { return nil, nil, err } @@ -160,12 +161,15 @@ func (v *Verifier) verifyTimestamp(tokenBytes []byte, encodedSig string) (time.T // verifyJWT verifies the JWT token against the specified verification key, and // returns notation claim. -func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tokenString string) (*notationClaim, error) { +func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tokenString string) (*signature.JWSNotaryClaim, error) { // parse and verify token parser := &jwt.Parser{ ValidMethods: v.ValidMethods, } - var claims payload + var claims struct { + jwt.RegisteredClaims + Notary signature.JWSNotaryClaim `json:"notary"` + } if _, err := parser.ParseWithClaims(tokenString, &claims, func(t *jwt.Token) (interface{}, error) { alg := t.Method.Alg() if expectedAlg := method.Alg(); alg != expectedAlg { @@ -184,7 +188,7 @@ func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tok if claims.IssuedAt == nil { return nil, errors.New("missing iat") } - return &claims.Notation, nil + return &claims.Notary, nil } // openEnvelope opens the signature envelope and get the embedded signature. diff --git a/plugin/errors.go b/spec/v1/plugin/errors.go similarity index 100% rename from plugin/errors.go rename to spec/v1/plugin/errors.go diff --git a/plugin/errors_test.go b/spec/v1/plugin/errors_test.go similarity index 100% rename from plugin/errors_test.go rename to spec/v1/plugin/errors_test.go diff --git a/plugin/metadata.go b/spec/v1/plugin/metadata.go similarity index 100% rename from plugin/metadata.go rename to spec/v1/plugin/metadata.go diff --git a/plugin/metadata_test.go b/spec/v1/plugin/metadata_test.go similarity index 100% rename from plugin/metadata_test.go rename to spec/v1/plugin/metadata_test.go diff --git a/plugin/plugin.go b/spec/v1/plugin/plugin.go similarity index 78% rename from plugin/plugin.go rename to spec/v1/plugin/plugin.go index f5829a77..28795b7a 100644 --- a/plugin/plugin.go +++ b/spec/v1/plugin/plugin.go @@ -1,61 +1,10 @@ package plugin -import ( - "crypto" -) +import "github.com/notaryproject/notation-go/spec/v1/signature" // Prefix is the prefix required on all plugin binary names. const Prefix = "notation-" -// KeySpec defines a key type and size. -type KeySpec string - -// One of following supported specs -// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection -const ( - RSA_2048 KeySpec = "RSA_2048" - RSA_3072 KeySpec = "RSA_3072" - RSA_4096 KeySpec = "RSA_4096" - EC_256 KeySpec = "EC_256" - EC_384 KeySpec = "EC_384" - EC_512 KeySpec = "EC_512" -) - -// Hash returns the Hash associated k. -func (k KeySpec) Hash() Hash { - switch k { - case RSA_2048, EC_256: - return SHA256 - case RSA_3072, EC_384: - return SHA384 - case RSA_4096, EC_512: - return SHA512 - } - return "" -} - -// Hash algorithm associated with the key spec. -type Hash string - -const ( - SHA256 Hash = "SHA_256" - SHA384 Hash = "SHA_384" - SHA512 Hash = "SHA_512" -) - -// HashFunc returns the Hash associated k. -func (h Hash) HashFunc() crypto.Hash { - switch h { - case SHA256: - return crypto.SHA256 - case SHA384: - return crypto.SHA384 - case SHA512: - return crypto.SHA512 - } - return 0 -} - // Command is a CLI command available in the plugin contract. type Command string @@ -110,17 +59,16 @@ type DescribeKeyResponse struct { // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - KeySpec KeySpec `json:"keySpec"` + KeySpec signature.Key `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. -// All parameters are required. type GenerateSignatureRequest struct { ContractVersion string `json:"contractVersion"` KeyName string `json:"keyName"` KeyID string `json:"keyId"` - KeySpec KeySpec `json:"keySpec"` - Hash Hash `json:"hashAlgorithm"` + KeySpec signature.Key `json:"keySpec"` + Hash signature.Hash `json:"hashAlgorithm"` SignatureEnvelopeType string `json:"signatureEnvelopeType"` Payload string `json:"payload"` PluginConfig map[string]string `json:"pluginConfig,omitempty"` @@ -144,7 +92,6 @@ type GenerateSignatureResponse struct { } // GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. -// All parameters are required. type GenerateEnvelopeRequest struct { ContractVersion string `json:"contractVersion"` KeyName string `json:"keyName"` diff --git a/spec/v1/signature/jws.go b/spec/v1/signature/jws.go new file mode 100644 index 00000000..d4b70f4f --- /dev/null +++ b/spec/v1/signature/jws.go @@ -0,0 +1,56 @@ +package signature + +const ( + // MediaTypeJWSEnvelope describes the media type of the JWS envelope. + MediaTypeJWSEnvelope = "application/vnd.cncf.notary.v2.jws.v1" +) + +// JWSNotaryClaim is a Notary private claim. +type JWSNotaryClaim struct { + Subject Descriptor `json:"subject"` +} + +// JWSPayload contains the set of claims used by Notary V2. +type JWSPayload struct { + // Private claim. + Notary JWSNotaryClaim `json:"notary"` + + // Identifies the number of seconds since Epoch at which the signature was issued. + IssuedAt int64 `json:"iat"` + + // Identifies the number of seconds since Epoch at which the signature must not be considered valid. + ExpiresAt int64 `json:"exp,omitempty"` +} + +// JWSProtectedHeader contains the set of protected headers. +type JWSProtectedHeader struct { + // Defines which algorithm was used to generate the signature. + Algorithm string `json:"alg"` + + // Media type of the secured content (the payload). + ContentType string `json:"cty"` +} + +// JWSUnprotectedHeader contains the set of unprotected headers. +type JWSUnprotectedHeader struct { + // RFC3161 time stamp token Base64-encoded. + TimeStampToken []byte `json:"timestamp,omitempty"` + + // List of X.509 certificates, each one Base64-encoded. + CertChain [][]byte `json:"x5c"` +} + +// JWSEnvelope is the final signature envelope. +type JWSEnvelope struct { + // JWSPayload Base64URL-encoded. + Payload string + + // JWSProtectedHeader Base64URL-encoded. + Protected string + + // Signature metadata that is not integrity protected + Header JWSUnprotectedHeader `json:"header,omitempty"` + + // Base64URL-encoded signature. + Signature string +} diff --git a/spec/v1/signature/types.go b/spec/v1/signature/types.go new file mode 100644 index 00000000..0f8e895f --- /dev/null +++ b/spec/v1/signature/types.go @@ -0,0 +1,80 @@ +package signature + +import "crypto" + +const ( + // MediaTypeDescriptor describes the media type of the descriptor. + MediaTypeDescriptor = "application/vnd.oci.descriptor.v1+json" +) + +// Descriptor describes the content signed or to be signed. +type Descriptor struct { + // The media type of the targeted content. + MediaType string `json:"mediaType"` + + // The digest of the targeted content. + Digest string `json:"digest"` + + // Specifies the size in bytes of the blob. + Size int64 `json:"size"` + + // Contains optional user defined attributes. + Annotations map[string]string `json:"annotations,omitempty"` + + // The artifact type of the targeted content. + ArtifactType string `json:"artifactType,omitempty"` +} + +// Equal reports whether d and t points to the same content. +func (d Descriptor) Equal(t Descriptor) bool { + return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size +} + +// Key defines a key type and size. +type Key string + +// One of following supported specs +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + RSA_2048 Key = "RSA_2048" + RSA_3072 Key = "RSA_3072" + RSA_4096 Key = "RSA_4096" + EC_256 Key = "EC_256" + EC_384 Key = "EC_384" + EC_512 Key = "EC_512" +) + +// Hash returns the Hash associated k. +func (k Key) Hash() Hash { + switch k { + case RSA_2048, EC_256: + return SHA256 + case RSA_3072, EC_384: + return SHA384 + case RSA_4096, EC_512: + return SHA512 + } + return "" +} + +// Hash algorithm associated with the key spec. +type Hash string + +const ( + SHA256 Hash = "SHA_256" + SHA384 Hash = "SHA_384" + SHA512 Hash = "SHA_512" +) + +// HashFunc returns the Hash associated k. +func (h Hash) HashFunc() crypto.Hash { + switch h { + case SHA256: + return crypto.SHA256 + case SHA384: + return crypto.SHA384 + case SHA512: + return crypto.SHA512 + } + return 0 +} From f50b24243a16be821e9b71bf7f9302c0154ed95a Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 5 May 2022 20:15:44 +0200 Subject: [PATCH 35/58] dedup notaryClaim Signed-off-by: qmuntal --- signature/jws/spec.go | 10 ++++++---- signature/jws/verifier.go | 5 +---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 76887e13..9d226c80 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -16,16 +16,18 @@ import ( "github.com/notaryproject/notation-go/spec/v1/signature" ) +type notaryClaim struct { + jwt.RegisteredClaims + Notary signature.JWSNotaryClaim `json:"notary"` +} + // packPayload generates JWS payload according the signing content and options. func packPayload(desc signature.Descriptor, opts notation.SignOptions) jwt.Claims { var expiresAt *jwt.NumericDate if !opts.Expiry.IsZero() { expiresAt = jwt.NewNumericDate(opts.Expiry) } - return struct { - jwt.RegisteredClaims - Notary signature.JWSNotaryClaim `json:"notary"` - }{ + return notaryClaim{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: expiresAt, IssuedAt: jwt.NewNumericDate(time.Now()), diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index 8acc07c6..a04adf91 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -166,10 +166,7 @@ func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tok parser := &jwt.Parser{ ValidMethods: v.ValidMethods, } - var claims struct { - jwt.RegisteredClaims - Notary signature.JWSNotaryClaim `json:"notary"` - } + var claims notaryClaim if _, err := parser.ParseWithClaims(tokenString, &claims, func(t *jwt.Token) (interface{}, error) { alg := t.Method.Alg() if expectedAlg := method.Alg(); alg != expectedAlg { From c9afb7ce1069050409dc5b4712a9c9c0eaedddf3 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 6 May 2022 08:51:42 +0200 Subject: [PATCH 36/58] remove support for multiple signature envelope Signed-off-by: qmuntal --- crypto/jwsutil/envelope.go | 53 ----------------- crypto/jwsutil/envelope_test.go | 102 -------------------------------- crypto/jwsutil/errors.go | 9 --- crypto/jwsutil/signature.go | 56 ------------------ signature/jws/signer.go | 32 +++++----- signature/jws/verifier.go | 30 ++++------ 6 files changed, 26 insertions(+), 256 deletions(-) delete mode 100644 crypto/jwsutil/envelope.go delete mode 100644 crypto/jwsutil/envelope_test.go delete mode 100644 crypto/jwsutil/errors.go delete mode 100644 crypto/jwsutil/signature.go diff --git a/crypto/jwsutil/envelope.go b/crypto/jwsutil/envelope.go deleted file mode 100644 index 951ab0be..00000000 --- a/crypto/jwsutil/envelope.go +++ /dev/null @@ -1,53 +0,0 @@ -package jwsutil - -import "encoding/json" - -// Envelope contains a common payload signed by multiple signatures. -type Envelope struct { - Payload string `json:"payload,omitempty"` - Signatures []Signature `json:"signatures,omitempty"` -} - -// Size returns the number of enclosed signatures. -func (e Envelope) Size() int { - return len(e.Signatures) -} - -// Open opens the evelope and returns the first or default complete signature. -func (e Envelope) Open() CompleteSignature { - if len(e.Signatures) == 0 { - return CompleteSignature{ - Payload: e.Payload, - } - } - return CompleteSignature{ - Payload: e.Payload, - Signature: e.Signatures[0], - } -} - -// UnmarshalJSON parses the JSON serialized JWS. -// Reference: RFC 7515 7.2 JWS JSON Serialization. -func (e *Envelope) UnmarshalJSON(data []byte) error { - var combined struct { - CompleteSignature - Signatures []Signature `json:"signatures"` - } - if err := json.Unmarshal(data, &combined); err != nil { - return ErrInvalidJSONSerialization - } - if len(combined.Signatures) == 0 { - *e = Envelope{ - Payload: combined.Payload, - Signatures: []Signature{ - combined.Signature, - }, - } - } else { - *e = Envelope{ - Payload: combined.Payload, - Signatures: combined.Signatures, - } - } - return nil -} diff --git a/crypto/jwsutil/envelope_test.go b/crypto/jwsutil/envelope_test.go deleted file mode 100644 index 7e142ec1..00000000 --- a/crypto/jwsutil/envelope_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package jwsutil - -import ( - "encoding/json" - "reflect" - "testing" -) - -func TestEnvelope_UnmarshalJSON(t *testing.T) { - tests := []struct { - name string - data string - want Envelope - }{ - { - name: "General JWS JSON Serialization Syntax (multiple signatures)", - data: `{ - "payload": "test payload", - "signatures": [ - { - "protected": "protected foo", - "header": {"unprotected": "foo"}, - "signature": "signature foo" - }, - { - "protected": "protected bar", - "header": {"unprotected": "bar"}, - "signature": "signature bar" - } - ] - }`, - want: Envelope{ - Payload: "test payload", - Signatures: []Signature{ - { - Protected: "protected foo", - Unprotected: []byte(`{"unprotected": "foo"}`), - Signature: "signature foo", - }, - { - Protected: "protected bar", - Unprotected: []byte(`{"unprotected": "bar"}`), - Signature: "signature bar", - }, - }, - }, - }, - { - name: "General JWS JSON Serialization Syntax (single signature)", - data: `{ - "payload": "test payload", - "signatures": [ - { - "protected": "protected foo", - "header": {"unprotected": "foo"}, - "signature": "signature foo" - } - ] - }`, - want: Envelope{ - Payload: "test payload", - Signatures: []Signature{ - { - Protected: "protected foo", - Unprotected: []byte(`{"unprotected": "foo"}`), - Signature: "signature foo", - }, - }, - }, - }, - { - name: "Flattened JWS JSON Serialization Syntax", - data: `{ - "payload": "test payload", - "protected": "protected foo", - "header": {"unprotected": "foo"}, - "signature": "signature foo" - }`, - want: Envelope{ - Payload: "test payload", - Signatures: []Signature{ - { - Protected: "protected foo", - Unprotected: []byte(`{"unprotected": "foo"}`), - Signature: "signature foo", - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var got Envelope - if err := json.Unmarshal([]byte(tt.data), &got); err != nil { - t.Fatalf("Envelope.UnmarshalJSON() error = %v", err) - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Envelope.UnmarshalJSON() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/crypto/jwsutil/errors.go b/crypto/jwsutil/errors.go deleted file mode 100644 index a64c217d..00000000 --- a/crypto/jwsutil/errors.go +++ /dev/null @@ -1,9 +0,0 @@ -package jwsutil - -import "errors" - -// Common errors -var ( - ErrInvalidCompactSerialization = errors.New("invalid compact serialization") - ErrInvalidJSONSerialization = errors.New("invalid JSON serialization") -) diff --git a/crypto/jwsutil/signature.go b/crypto/jwsutil/signature.go deleted file mode 100644 index 1d965fa8..00000000 --- a/crypto/jwsutil/signature.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package jwsutil provides serialization utilities for JWT libraries to comfort JWS. -// Reference: RFC 7515 JSON Web Signature (JWS). -package jwsutil - -import ( - "encoding/json" - "strings" -) - -// Signature represents a detached signature. -type Signature struct { - Protected string `json:"protected,omitempty"` - Unprotected json.RawMessage `json:"header,omitempty"` - Signature string `json:"signature,omitempty"` -} - -// CompleteSignature represents a clear signed signature. -// A CompleteSignature can be viewed as an envelope with a single signature in -// flattened JWS JSON serialization syntax. -// Reference: RFC 7515 7.2 JWS JSON Serialization. -type CompleteSignature struct { - Payload string `json:"payload,omitempty"` - Signature -} - -// Parse parses the compact serialized JWS. -// Reference: RFC 7515 7.1 JWS Compact Serialization. -func ParseCompact(serialized string) (CompleteSignature, error) { - parts := strings.Split(serialized, ".") - if len(parts) != 3 { - return CompleteSignature{}, ErrInvalidCompactSerialization - } - return CompleteSignature{ - Payload: parts[1], - Signature: Signature{ - Protected: parts[0], - Signature: parts[2], - }, - }, nil -} - -// SerializeCompact serialize the signature in JWS Compact Serialization -// Reference: RFC 7515 7.1 JWS Compact Serialization. -func (s CompleteSignature) SerializeCompact() string { - return strings.Join([]string{s.Protected, s.Payload, s.Signature.Signature}, ".") -} - -// Enclose packs the signature into an envelope. -func (s CompleteSignature) Enclose() Envelope { - return Envelope{ - Payload: s.Payload, - Signatures: []Signature{ - s.Signature, - }, - } -} diff --git a/signature/jws/signer.go b/signature/jws/signer.go index ab87a50a..76791749 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -8,10 +8,10 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/crypto/jwsutil" "github.com/notaryproject/notation-go/crypto/timestamp" "github.com/notaryproject/notation-go/internal/crypto/pki" "github.com/notaryproject/notation-go/spec/v1/signature" @@ -103,32 +103,30 @@ func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notat } func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { - // generate unprotected header - header := signature.JWSUnprotectedHeader{ - CertChain: certChain, + parts := strings.Split(compact, ".") + if len(parts) != 3 { + return nil, errors.New("invalid compact serialization") + } + envelope := signature.JWSEnvelope{ + Protected: parts[0], + Payload: parts[1], + Signature: parts[2], + Header: signature.JWSUnprotectedHeader{ + CertChain: certChain, + }, } // timestamp JWT - sig, err := jwsutil.ParseCompact(compact) - if err != nil { - return nil, err - } if opts.TSA != nil { - token, err := timestampSignature(ctx, sig.Signature.Signature, opts.TSA, opts.TSAVerifyOptions) + token, err := timestampSignature(ctx, envelope.Signature, opts.TSA, opts.TSAVerifyOptions) if err != nil { return nil, fmt.Errorf("timestamp failed: %w", err) } - header.TimeStampToken = token - } - - // finalize unprotected header - sig.Unprotected, err = json.Marshal(header) - if err != nil { - return nil, err + envelope.Header.TimeStampToken = token } // encode in flatten JWS JSON serialization - return json.Marshal(sig) + return json.Marshal(envelope) } // timestampSignature sends a request to the TSA for timestamping the signature. diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index a04adf91..3fb8fe24 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -8,11 +8,11 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/crypto/jwsutil" "github.com/notaryproject/notation-go/crypto/timestamp" "github.com/notaryproject/notation-go/spec/v1/signature" ) @@ -64,13 +64,14 @@ func (v *Verifier) Verify(ctx context.Context, sig []byte, opts notation.VerifyO } // verify signing identity - method, key, err := v.verifySigner(&envelope.Signature) + method, key, err := v.verifySigner(envelope) if err != nil { return signature.Descriptor{}, err } // verify JWT - claim, err := v.verifyJWT(method, key, envelope.SerializeCompact()) + compact := strings.Join([]string{envelope.Protected, envelope.Payload, envelope.Signature}, ".") + claim, err := v.verifyJWT(method, key, compact) if err != nil { return signature.Descriptor{}, err } @@ -79,16 +80,11 @@ func (v *Verifier) Verify(ctx context.Context, sig []byte, opts notation.VerifyO } // verifySigner verifies the signing identity and returns the verification key. -func (v *Verifier) verifySigner(sig *jwsutil.Signature) (jwt.SigningMethod, crypto.PublicKey, error) { - var header signature.JWSUnprotectedHeader - if err := json.Unmarshal(sig.Unprotected, &header); err != nil { - return nil, nil, err - } - - if len(header.CertChain) == 0 { +func (v *Verifier) verifySigner(sig *signature.JWSEnvelope) (jwt.SigningMethod, crypto.PublicKey, error) { + if len(sig.Header.CertChain) == 0 { return nil, nil, errors.New("signer certificates not found") } - return v.verifySignerFromCertChain(header.CertChain, header.TimeStampToken, sig.Signature) + return v.verifySignerFromCertChain(sig.Header.CertChain, sig.Header.TimeStampToken, sig.Signature) } // verifySignerFromCertChain verifies the signing identity from the provided certificate @@ -189,16 +185,12 @@ func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tok } // openEnvelope opens the signature envelope and get the embedded signature. -func openEnvelope(signature []byte) (*jwsutil.CompleteSignature, error) { - var envelope jwsutil.Envelope - if err := json.Unmarshal(signature, &envelope); err != nil { +func openEnvelope(sig []byte) (*signature.JWSEnvelope, error) { + var envelope signature.JWSEnvelope + if err := json.Unmarshal(sig, &envelope); err != nil { return nil, err } - if len(envelope.Signatures) != 1 { - return nil, errors.New("single signature envelope expected") - } - sig := envelope.Open() - return &sig, nil + return &envelope, nil } // verifyTimestamp verifies the timestamp token and returns stamped time. From 70877b223bc9494eea4c35c9938560727acdbb9d Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 6 May 2022 11:11:42 +0200 Subject: [PATCH 37/58] move payload creation to where it's needed Signed-off-by: qmuntal --- signature/jws/plugin.go | 23 +++++++++++++---------- signature/jws/plugin_test.go | 28 +++++++++++++++------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index c58f239a..4ec1e515 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -61,16 +61,10 @@ func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts } metadata := out.(*plugin.Metadata) - // Generate payload to be signed. - payload := packPayload(desc, opts) - if err := payload.Valid(); err != nil { - return nil, err - } - if metadata.HasCapability(plugin.CapabilitySignatureGenerator) { - return s.generateSignature(ctx, opts, payload) + return s.generateSignature(ctx, desc, opts) } else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) { - return s.generateSignatureEnvelope(ctx, opts, payload) + return s.generateSignatureEnvelope(ctx, desc, opts) } return nil, fmt.Errorf("plugin %q does not have signing capabilities", s.PluginName) } @@ -89,7 +83,7 @@ func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResp return out.(*plugin.DescribeKeyResponse), nil } -func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.SignOptions, payload jwt.Claims) ([]byte, error) { +func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { // Get key info. key, err := s.describeKey(ctx) if err != nil { @@ -100,10 +94,19 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign if s.KeyID != key.KeyID { return nil, fmt.Errorf("keyID mismatch") } + + // Get algorithm associated to key. alg := keySpecToAlg[key.KeySpec] if alg == "" { return nil, fmt.Errorf("keySpec %q not supported: ", key.KeySpec) } + + // Generate payload to be signed. + payload := packPayload(desc, opts) + if err := payload.Valid(); err != nil { + return nil, err + } + // Generate signing string. token := &jwt.Token{ Header: map[string]interface{}{ @@ -180,7 +183,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, opts notation.Sign return jwtEnvelop(ctx, opts, compact, rawCerts) } -func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, opts notation.SignOptions, payload jwt.Claims) ([]byte, error) { +func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { return nil, errors.New("not implemented") } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 2687dd31..5c91f088 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -80,19 +80,6 @@ func TestPluginSigner_Sign_RunMetadataFails(t *testing.T) { testPluginSignerError(t, signer, "metadata command failed") } -func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { - signer := PluginSigner{ - Runner: &mockRunner{[]interface{}{ - &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, - }, []error{nil}, 0}, - } - _, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) - wantEr := "token is expired" - if err == nil || !strings.Contains(err.Error(), wantEr) { - t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) - } -} - func TestPluginSigner_Sign_NoCapability(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ @@ -128,6 +115,21 @@ func TestPluginSigner_Sign_KeySpecNotSupported(t *testing.T) { testPluginSignerError(t, signer, "keySpec \"custom\" not supported") } +func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { + signer := PluginSigner{ + Runner: &mockRunner{[]interface{}{ + &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, + &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signature.RSA_2048}, + }, []error{nil, nil}, 0}, + KeyID: "1", + } + _, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) + wantEr := "token is expired" + if err == nil || !strings.Contains(err.Error(), wantEr) { + t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) + } +} + func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ From a4981c067d7f81e49d7f2e2d020c74254258a519 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 6 May 2022 11:24:33 +0200 Subject: [PATCH 38/58] add missing json tags Signed-off-by: qmuntal --- spec/v1/signature/jws.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/v1/signature/jws.go b/spec/v1/signature/jws.go index d4b70f4f..e3e46202 100644 --- a/spec/v1/signature/jws.go +++ b/spec/v1/signature/jws.go @@ -43,14 +43,14 @@ type JWSUnprotectedHeader struct { // JWSEnvelope is the final signature envelope. type JWSEnvelope struct { // JWSPayload Base64URL-encoded. - Payload string + Payload string `json:"payload"` // JWSProtectedHeader Base64URL-encoded. - Protected string + Protected string `json:"protected"` // Signature metadata that is not integrity protected - Header JWSUnprotectedHeader `json:"header,omitempty"` + Header JWSUnprotectedHeader `json:"header"` // Base64URL-encoded signature. - Signature string + Signature string `json:"signature"` } From a2fff4881a3fcfa98050b1e0a14a1a0218f4f9d7 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 6 May 2022 15:56:13 +0200 Subject: [PATCH 39/58] use correct header encoding Signed-off-by: qmuntal --- signature/jws/plugin.go | 18 ++++++++---------- signature/jws/plugin_test.go | 31 ++++++++++++++++++++++++++++++- signature/jws/signer.go | 12 ++++++------ signature/jws/verifier.go | 14 +++++++++++--- spec/v1/signature/jws.go | 4 ++-- 5 files changed, 57 insertions(+), 22 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 4ec1e515..aa3727ff 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "errors" "fmt" - "strings" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" @@ -163,7 +162,8 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des if err != nil { return nil, fmt.Errorf("signature not base64-encoded: %v", err) } - err = verifyJWT(resp.SigningAlgorithm, signing, signed, certs[0]) + signed64Url := base64.RawURLEncoding.EncodeToString(signed) + err = verifyJWT(resp.SigningAlgorithm, signing, signed64Url, certs[0]) if err != nil { return nil, fmt.Errorf("verification error: %v", err) } @@ -175,12 +175,11 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Assemble the JWS signature envelope. - rawCerts := make([][]byte, len(certs)) - for i, c := range certs { - rawCerts[i] = c.Raw + rawCerts := make([]string, len(certs)) + for i, cert := range certs { + rawCerts[i] = base64.RawStdEncoding.EncodeToString(cert.Raw) } - compact := strings.Join([]string{signing, base64.RawURLEncoding.EncodeToString(signed)}, ".") - return jwtEnvelop(ctx, opts, compact, rawCerts) + return jwtEnvelop(ctx, opts, signing+"."+signed64Url, rawCerts) } func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { @@ -203,9 +202,8 @@ func parseCertChain(certChain []string) ([]*x509.Certificate, error) { return certs, nil } -func verifyJWT(sigAlg string, payload string, sig []byte, signingCert *x509.Certificate) error { +func verifyJWT(sigAlg string, payload string, sig string, signingCert *x509.Certificate) error { // Verify the hash of req.payload against resp.signature using the public key if the leaf certificate. method := jwt.GetSigningMethod(sigAlg) - encSig := base64.RawURLEncoding.EncodeToString(sig) - return method.Verify(payload, encSig, signingCert.PublicKey) + return method.Verify(payload, sig, signingCert.PublicKey) } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 5c91f088..181e0214 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -7,8 +7,10 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/base64" + "encoding/json" "errors" "math/big" + "reflect" "strings" "testing" "time" @@ -342,8 +344,35 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { }, KeyID: "1", } - _, err = signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{}) + data, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{}) if err != nil { t.Errorf("PluginSigner.Sign() error = %v, wantErr nil", err) } + var got signature.JWSEnvelope + err = json.Unmarshal(data, &got) + if err != nil { + t.Fatal(err) + } + want := signature.JWSEnvelope{ + Protected: "eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiY3R5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS52Mi5qd3MudjEifQ", + Header: signature.JWSUnprotectedHeader{ + CertChain: []string{base64.RawStdEncoding.EncodeToString(cert.Raw)}, + }, + } + if got.Protected != want.Protected { + t.Errorf("PluginSigner.Sign() Protected %v, want %v", got.Protected, want.Protected) + } + if _, err = base64.RawURLEncoding.DecodeString(got.Signature); err != nil { + t.Errorf("PluginSigner.Sign() Signature %v is not encoded as Base64URL", got.Signature) + } + if !reflect.DeepEqual(got.Header, want.Header) { + t.Errorf("PluginSigner.Sign() Header %v, want %v", got.Header, want.Header) + } + v := NewVerifier() + roots := x509.NewCertPool() + roots.AddCert(cert) + v.VerifyOptions.Roots = roots + if _, err := v.Verify(context.Background(), data, notation.VerifyOptions{}); err != nil { + t.Fatalf("Verify() error = %v", err) + } } diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 76791749..81c3d621 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -28,7 +28,7 @@ type Signer struct { // certChain contains the X.509 public key certificate or certificate chain corresponding // to the key used to generate the signature. - certChain [][]byte + certChain []string } // NewSigner creates a signer with the recommended signing method and a signing key bundled @@ -69,9 +69,9 @@ func NewSignerWithCertificateChain(method jwt.SigningMethod, key crypto.PrivateK return nil, err } - rawCerts := make([][]byte, 0, len(certChain)) - for _, cert := range certChain { - rawCerts = append(rawCerts, cert.Raw) + rawCerts := make([]string, len(certChain)) + for i, cert := range certChain { + rawCerts[i] = base64.RawStdEncoding.EncodeToString(cert.Raw) } return &Signer{ method: method, @@ -102,7 +102,7 @@ func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notat return jwtEnvelop(ctx, opts, compact, s.certChain) } -func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { +func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain []string) ([]byte, error) { parts := strings.Split(compact, ".") if len(parts) != 3 { return nil, errors.New("invalid compact serialization") @@ -122,7 +122,7 @@ func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, if err != nil { return nil, fmt.Errorf("timestamp failed: %w", err) } - envelope.Header.TimeStampToken = token + envelope.Header.TimeStampToken = base64.RawStdEncoding.EncodeToString(token) } // encode in flatten JWS JSON serialization diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index 3fb8fe24..fd2f3cab 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -84,18 +84,26 @@ func (v *Verifier) verifySigner(sig *signature.JWSEnvelope) (jwt.SigningMethod, if len(sig.Header.CertChain) == 0 { return nil, nil, errors.New("signer certificates not found") } - return v.verifySignerFromCertChain(sig.Header.CertChain, sig.Header.TimeStampToken, sig.Signature) + token, err := base64.RawStdEncoding.DecodeString(sig.Header.TimeStampToken) + if err != nil { + return nil, nil, err + } + return v.verifySignerFromCertChain(sig.Header.CertChain, token, sig.Signature) } // verifySignerFromCertChain verifies the signing identity from the provided certificate // chain and returns the verification key. The first certificate of the certificate chain // contains the key, which used to sign the artifact. // Reference: RFC 7515 4.1.6 "x5c" (X.509 Certificate Chain) Header Parameter. -func (v *Verifier) verifySignerFromCertChain(certChain [][]byte, timeStampToken []byte, encodedSig string) (jwt.SigningMethod, crypto.PublicKey, error) { +func (v *Verifier) verifySignerFromCertChain(certChain []string, timeStampToken []byte, encodedSig string) (jwt.SigningMethod, crypto.PublicKey, error) { // prepare for certificate verification certs := make([]*x509.Certificate, 0, len(certChain)) for _, certBytes := range certChain { - cert, err := x509.ParseCertificate(certBytes) + raw, err := base64.RawStdEncoding.DecodeString(certBytes) + if err != nil { + return nil, nil, err + } + cert, err := x509.ParseCertificate(raw) if err != nil { return nil, nil, err } diff --git a/spec/v1/signature/jws.go b/spec/v1/signature/jws.go index e3e46202..086700e5 100644 --- a/spec/v1/signature/jws.go +++ b/spec/v1/signature/jws.go @@ -34,10 +34,10 @@ type JWSProtectedHeader struct { // JWSUnprotectedHeader contains the set of unprotected headers. type JWSUnprotectedHeader struct { // RFC3161 time stamp token Base64-encoded. - TimeStampToken []byte `json:"timestamp,omitempty"` + TimeStampToken string `json:"timestamp,omitempty"` // List of X.509 certificates, each one Base64-encoded. - CertChain [][]byte `json:"x5c"` + CertChain []string `json:"x5c"` } // JWSEnvelope is the final signature envelope. From eb16900ee79d05b60dea82fb1b6529120e902e9b Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 9 May 2022 10:33:33 +0200 Subject: [PATCH 40/58] rename NewManager to New and add root param Signed-off-by: qmuntal --- plugin/manager/integration_test.go | 13 +++++++++++-- plugin/manager/manager.go | 21 ++++++++------------- plugin/manager/manager_test.go | 6 +++--- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index 6e6a4605..b7df0aa2 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -1,4 +1,4 @@ -package manager +package manager_test import ( "context" @@ -7,8 +7,10 @@ import ( "os/exec" "path/filepath" "reflect" + "runtime" "testing" + "github.com/notaryproject/notation-go/plugin/manager" "github.com/notaryproject/notation-go/spec/v1/plugin" ) @@ -53,7 +55,7 @@ func TestIntegration(t *testing.T) { t.Skip() } root := preparePlugin(t) - mgr := &Manager{rootedFS{os.DirFS(root), root}, execCommander{}} + mgr := manager.New(root) p, err := mgr.Get(context.Background(), "foo") if err != nil { t.Fatal(err) @@ -76,3 +78,10 @@ func TestIntegration(t *testing.T) { t.Fatal(err) } } + +func addExeSuffix(s string) string { + if runtime.GOOS == "windows" { + s += ".exe" + } + return s +} diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 19f1c910..3f2aa547 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -59,7 +59,7 @@ func (c execCommander) Output(ctx context.Context, name string, command string, return stdout.Bytes(), true, nil } -// rootedFS is io.FS implementation used in NewManager. +// rootedFS is io.FS implementation used in New. // root is the root of the file system tree passed to os.DirFS. type rootedFS struct { fs.FS @@ -72,17 +72,12 @@ type Manager struct { cmder commander } -// NewManager returns a new manager. -func NewManager() *Manager { - configDir, err := os.UserConfigDir() - if err != nil { - // Lets panic for now. - // Once the config is moved to notation-go we will move this code to - // the config package as a global initialization. - panic(err) - } - pluginDir := filepath.Join(configDir, "notation", "plugins") - return &Manager{rootedFS{os.DirFS(pluginDir), pluginDir}, execCommander{}} +// New returns a new manager rooted at root. +// +// root is the path of the directory where plugins are stored +// following the {root}/{plugin-name}/notation-{plugin-name}[.exe] pattern. +func New(root string) *Manager { + return &Manager{rootedFS{os.DirFS(root), root}, execCommander{}} } // Get returns a plugin on the system by its name. @@ -238,7 +233,7 @@ func binName(name string) string { func binPath(fsys fs.FS, name string) string { base := binName(name) - // NewManager() always instantiate a rootedFS. + // New() always instantiate a rootedFS. // Other fs.FS implementations are only supported for testing purposes. if fsys, ok := fsys.(rootedFS); ok { return filepath.Join(fsys.root, name, base) diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index ad083ddf..aaf407ee 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -289,9 +289,9 @@ func TestManager_Run(t *testing.T) { } } -func TestNewManager(t *testing.T) { - mgr := NewManager() +func TestNew(t *testing.T) { + mgr := New("") if mgr == nil { - t.Error("NewManager() = nil") + t.Error("New() = nil") } } From 2a6e59788ae26918a35791c2606dd2c5530d8de9 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 9 May 2022 10:35:38 +0200 Subject: [PATCH 41/58] remove artifact type from descriptor Signed-off-by: qmuntal --- spec/v1/signature/types.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/v1/signature/types.go b/spec/v1/signature/types.go index 0f8e895f..046f1f4d 100644 --- a/spec/v1/signature/types.go +++ b/spec/v1/signature/types.go @@ -20,9 +20,6 @@ type Descriptor struct { // Contains optional user defined attributes. Annotations map[string]string `json:"annotations,omitempty"` - - // The artifact type of the targeted content. - ArtifactType string `json:"artifactType,omitempty"` } // Equal reports whether d and t points to the same content. From a453d9eef6bcb2f5076ea01b5075b2aa3f89224d Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 9 May 2022 10:53:19 +0200 Subject: [PATCH 42/58] dedup jwt.Token creation Signed-off-by: qmuntal --- signature/jws/plugin.go | 9 +-------- signature/jws/plugin_test.go | 2 +- signature/jws/signer.go | 20 ++++++++++++-------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index aa3727ff..6a0a8395 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -107,14 +107,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Generate signing string. - token := &jwt.Token{ - Header: map[string]interface{}{ - "alg": alg, - "cty": signature.MediaTypeJWSEnvelope, - "crit": []string{"cty"}, - }, - Claims: payload, - } + token := jwtToken(alg, payload) signing, err := token.SigningString() if err != nil { return nil, fmt.Errorf("failed to marshal signing payload: %v", err) diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 181e0214..b702e489 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -354,7 +354,7 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { t.Fatal(err) } want := signature.JWSEnvelope{ - Protected: "eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiY3R5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS52Mi5qd3MudjEifQ", + Protected: "eyJhbGciOiJQUzI1NiIsImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS52Mi5qd3MudjEifQ", Header: signature.JWSUnprotectedHeader{ CertChain: []string{base64.RawStdEncoding.EncodeToString(cert.Raw)}, }, diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 81c3d621..46b9ddc8 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -87,14 +87,8 @@ func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notat if err := payload.Valid(); err != nil { return nil, err } - token := &jwt.Token{ - Header: map[string]interface{}{ - "alg": s.method.Alg(), - "cty": signature.MediaTypeJWSEnvelope, - }, - Claims: payload, - Method: s.method, - } + token := jwtToken(s.method.Alg(), payload) + token.Method = s.method compact, err := token.SignedString(s.key) if err != nil { return nil, err @@ -102,6 +96,16 @@ func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notat return jwtEnvelop(ctx, opts, compact, s.certChain) } +func jwtToken(alg string, claims jwt.Claims) *jwt.Token { + return &jwt.Token{ + Header: map[string]interface{}{ + "alg": alg, + "cty": signature.MediaTypeJWSEnvelope, + }, + Claims: claims, + } +} + func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain []string) ([]byte, error) { parts := strings.Split(compact, ".") if len(parts) != 3 { From a284a1e5eb770708804001255967a76cd8db9956 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 9 May 2022 12:51:01 +0200 Subject: [PATCH 43/58] simpligy running plugin command Signed-off-by: qmuntal --- plugin/manager/integration_test.go | 6 ++- plugin/manager/manager.go | 64 ++++++++++++++---------------- plugin/manager/manager_test.go | 50 ++++++++++------------- signature/jws/plugin.go | 24 ++++------- signature/jws/plugin_test.go | 26 ++++++------ spec/v1/plugin/errors.go | 18 ++++++++- spec/v1/plugin/errors_test.go | 26 ++++++++++++ spec/v1/plugin/plugin.go | 22 +++++++++- 8 files changed, 142 insertions(+), 94 deletions(-) diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index b7df0aa2..4fb6d1b2 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -73,7 +73,11 @@ func TestIntegration(t *testing.T) { if !reflect.DeepEqual(list[0].Metadata, p.Metadata) { t.Errorf("Manager.List() got %v, want %v", list[0], p) } - _, err = mgr.Run(context.Background(), "foo", plugin.CommandGetMetadata, nil) + r, err := mgr.Runner("foo") + if err != nil { + t.Fatal(err) + } + _, err = r.Run(context.Background(), plugin.CommandGetMetadata, nil) if err != nil { t.Fatal(err) } diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 3f2aa547..94b29473 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -112,42 +112,16 @@ func (mgr *Manager) List(ctx context.Context) ([]*Plugin, error) { return plugins, nil } -// Run executes the specified command against the named plugin and waits for it to complete. +// Runner returns a plugin.Runner. // -// When the returned object is not nil, its type is guaranteed to remain always the same for a given Command. -// -// The returned error is nil if: -// - the plugin exists and is valid -// - the command runs and exits with a zero exit status -// - the command stdout contains a valid json object which can be unmarshal-ed. -// -// If the plugin is not found, the error is of type ErrNotFound. -// If the plugin metadata is not valid or stdout and stderr can't be decoded into a valid response, the error is of type ErrNotCompliant. -// If the command starts but does not complete successfully, the error is of type RequestError wrapping a *exec.ExitError. -// Other error types may be returned for other situations. -func (mgr *Manager) Run(ctx context.Context, name string, cmd plugin.Command, req interface{}) (interface{}, error) { - p, err := mgr.newPlugin(ctx, name) - if err != nil { - return nil, pluginErr(name, err) - } - if p.Err != nil { - return nil, pluginErr(name, withErr(p.Err, ErrNotCompliant)) - } - if cmd == plugin.CommandGetMetadata { - return &p.Metadata, nil - } - var data []byte - if req != nil { - data, err = json.Marshal(req) - if err != nil { - return nil, pluginErr(name, fmt.Errorf("failed to marshal request object: %w", err)) - } - } - resp, err := run(ctx, mgr.cmder, p.Path, cmd, data) - if err != nil { - return nil, pluginErr(name, err) +// If the plugin is not found or is not a valid candidate, the error is of type ErrNotFound. +func (mgr *Manager) Runner(name string) (plugin.Runner, error) { + ok := isCandidate(mgr.fsys, name) + if !ok { + return nil, ErrNotFound } - return resp, nil + + return pluginRunner{name: name, path: binPath(mgr.fsys, name), cmder: mgr.cmder}, nil } // newPlugin determines if the given candidate is valid and returns a Plugin. @@ -172,6 +146,28 @@ func (mgr *Manager) newPlugin(ctx context.Context, name string) (*Plugin, error) return p, nil } +type pluginRunner struct { + name string + path string + cmder commander +} + +func (p pluginRunner) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { + var data []byte + if req != nil { + var err error + data, err = json.Marshal(req) + if err != nil { + return nil, pluginErr(p.name, fmt.Errorf("failed to marshal request object: %w", err)) + } + } + resp, err := run(ctx, p.cmder, p.path, cmd, data) + if err != nil { + return nil, pluginErr(p.name, err) + } + return resp, nil +} + // run executes the command and decodes the response. func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Command, req []byte) (interface{}, error) { out, ok, err := cmder.Output(ctx, pluginPath, string(cmd), req) diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index aaf407ee..05d2fc0d 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -23,18 +23,6 @@ func (t testCommander) Output(ctx context.Context, path string, command string, return t.output, t.success, t.err } -type testMultiCommander struct { - output [][]byte - success []bool - err []error - n int -} - -func (t *testMultiCommander) Output(ctx context.Context, path string, command string, req []byte) (out []byte, success bool, err error) { - defer func() { t.n++ }() - return t.output[t.n], t.success[t.n], t.err[t.n] -} - var validMetadata = plugin.Metadata{ Name: "foo", Description: "friendly", Version: "1", URL: "example.com", SupportedContractVersions: []string{"1"}, Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, @@ -229,7 +217,15 @@ func TestManager_List(t *testing.T) { } } -func TestManager_Run(t *testing.T) { +func TestManager_Runner_Run_NotFound(t *testing.T) { + mgr := &Manager{fstest.MapFS{}, nil} + _, err := mgr.Runner("foo") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("Manager.Runner() error = %v, want %v", err, ErrNotFound) + } +} + +func TestManager_Runner_Run(t *testing.T) { var errExec = errors.New("exec failed") type args struct { name string @@ -241,27 +237,19 @@ func TestManager_Run(t *testing.T) { args args err error }{ - {"empty fsys", &Manager{fstest.MapFS{}, nil}, args{"foo", plugin.CommandGenerateSignature}, ErrNotFound}, - { - "invalid plugin", &Manager{fstest.MapFS{ - "foo": &fstest.MapFile{Mode: fs.ModeDir}, - addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, testCommander{nil, false, errors.New("err")}}, - args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, - }, { "exec error", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), nil}, []bool{true, false}, []error{nil, errExec}, 0}}, + }, &testCommander{nil, false, errExec}}, args{"foo", plugin.CommandGenerateSignature}, errExec, }, { - "exit error", &Manager{fstest.MapFS{ + "request error", &Manager{fstest.MapFS{ "foo": &fstest.MapFile{Mode: fs.ModeDir}, addExeSuffix("foo/notation-foo"): new(fstest.MapFile), - }, &testMultiCommander{[][]byte{metadataJSON(validMetadata), {}}, []bool{true, false}, []error{nil, nil}, 0}}, - args{"foo", plugin.CommandGenerateSignature}, ErrNotCompliant, + }, &testCommander{[]byte("{\"errorCode\": \"ERROR\"}"), false, nil}}, + args{"foo", plugin.CommandGenerateSignature}, plugin.RequestError{Code: plugin.ErrorCodeGeneric}, }, { "valid", &Manager{fstest.MapFS{ @@ -273,17 +261,21 @@ func TestManager_Run(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.mgr.Run(context.Background(), tt.args.name, tt.args.cmd, "1") + runner, err := tt.mgr.Runner(tt.args.name) + if err != nil { + t.Fatalf("Manager.Runner() error = %v, want nil", err) + } + got, err := runner.Run(context.Background(), tt.args.cmd, "1") wantErr := tt.err != nil if (err != nil) != wantErr { - t.Fatalf("Manager.Run() error = %v, wantErr %v", err, wantErr) + t.Fatalf("Runner.Run() error = %v, wantErr %v", err, wantErr) } if wantErr { if !errors.Is(err, tt.err) { - t.Fatalf("Manager.Run() error = %v, want %v", err, tt.err) + t.Fatalf("Runner.Run() error = %v, want %v", err, tt.err) } } else if got == nil { - t.Error("Manager.Run() want non-nil output") + t.Error("Runner.Run() want non-nil output") } }) } diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 6a0a8395..266c91da 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -31,22 +31,12 @@ var keySpecToAlg = map[signature.Key]string{ signature.EC_512: jwt.SigningMethodES512.Alg(), } -// PluginRunner is the interface implemented by plugin/manager.Manager, -// but which can be swapped by a custom third-party implementation -// if this constrains are meet: -// - Run fails if the plugin does not exist or is not valid -// - Run returns the appropriate type for each cmd -type PluginRunner interface { - Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) -} - // PluginSigner signs artifacts and generates JWS signatures // by delegating the one or both operations to the named plugin, // as defined in // https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces. type PluginSigner struct { - Runner PluginRunner - PluginName string + Runner plugin.Runner KeyID string KeyName string PluginConfig map[string]string @@ -54,18 +44,20 @@ type PluginSigner struct { // Sign signs the artifact described by its descriptor, and returns the signature. func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { - out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGetMetadata, nil) + out, err := s.Runner.Run(ctx, plugin.CommandGetMetadata, nil) if err != nil { return nil, fmt.Errorf("metadata command failed: %w", err) } metadata := out.(*plugin.Metadata) - + if err := metadata.Validate(); err != nil { + return nil, fmt.Errorf("invalid plugin metadata: %w", err) + } if metadata.HasCapability(plugin.CapabilitySignatureGenerator) { return s.generateSignature(ctx, desc, opts) } else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) { return s.generateSignatureEnvelope(ctx, desc, opts) } - return nil, fmt.Errorf("plugin %q does not have signing capabilities", s.PluginName) + return nil, fmt.Errorf("plugin does not have signing capabilities") } func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResponse, error) { @@ -75,7 +67,7 @@ func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResp KeyID: s.KeyID, PluginConfig: s.PluginConfig, } - out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandDescribeKey, req) + out, err := s.Runner.Run(ctx, plugin.CommandDescribeKey, req) if err != nil { return nil, fmt.Errorf("describe-key command failed: %w", err) } @@ -123,7 +115,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des Payload: signing, PluginConfig: s.PluginConfig, } - out, err := s.Runner.Run(ctx, s.PluginName, plugin.CommandGenerateSignature, req) + out, err := s.Runner.Run(ctx, plugin.CommandGenerateSignature, req) if err != nil { return nil, fmt.Errorf("generate-signature command failed: %w", err) } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index b702e489..f3fa358a 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -21,13 +21,18 @@ import ( "github.com/notaryproject/notation-go/spec/v1/signature" ) +var validMetadata = plugin.Metadata{ + Name: "foo", Description: "friendly", Version: "1", URL: "example.com", + SupportedContractVersions: []string{"1"}, Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, +} + type mockRunner struct { resp []interface{} err []error n int } -func (r *mockRunner) Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) { +func (r *mockRunner) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { defer func() { r.n++ }() return r.resp[r.n], r.err[r.n] } @@ -41,7 +46,7 @@ type mockSignerPlugin struct { n int } -func (s *mockSignerPlugin) Run(ctx context.Context, pluginName string, cmd plugin.Command, req interface{}) (interface{}, error) { +func (s *mockSignerPlugin) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { var chain []string if s.Cert != "" { chain = append(chain, s.Cert) @@ -49,7 +54,7 @@ func (s *mockSignerPlugin) Run(ctx context.Context, pluginName string, cmd plugi defer func() { s.n++ }() switch s.n { case 0: - return &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, nil + return &validMetadata, nil case 1: return &plugin.DescribeKeyResponse{KeyID: s.KeyID, KeySpec: s.KeySpec}, nil case 2: @@ -83,20 +88,17 @@ func TestPluginSigner_Sign_RunMetadataFails(t *testing.T) { } func TestPluginSigner_Sign_NoCapability(t *testing.T) { + m := validMetadata + m.Capabilities = []plugin.Capability{""} signer := PluginSigner{ - Runner: &mockRunner{[]interface{}{ - &plugin.Metadata{Capabilities: []plugin.Capability{}}, - }, []error{nil}, 0}, + Runner: &mockRunner{[]interface{}{&m}, []error{nil}, 0}, } testPluginSignerError(t, signer, "does not have signing capabilities") } func TestPluginSigner_Sign_DescribeKeyFailed(t *testing.T) { signer := PluginSigner{ - Runner: &mockRunner{[]interface{}{ - &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, - nil, - }, []error{nil, errors.New("failed")}, 0}, + Runner: &mockRunner{[]interface{}{&validMetadata, nil}, []error{nil, errors.New("failed")}, 0}, } testPluginSignerError(t, signer, "describe-key command failed") } @@ -120,7 +122,7 @@ func TestPluginSigner_Sign_KeySpecNotSupported(t *testing.T) { func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ - &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, + &validMetadata, &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signature.RSA_2048}, }, []error{nil, nil}, 0}, KeyID: "1", @@ -135,7 +137,7 @@ func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ - &plugin.Metadata{Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}}, + &validMetadata, &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signature.RSA_2048}, &plugin.GenerateSignatureResponse{KeyID: "2"}, }, []error{nil, nil, nil}, 0}, diff --git a/spec/v1/plugin/errors.go b/spec/v1/plugin/errors.go index 12fdef71..ad59b75c 100644 --- a/spec/v1/plugin/errors.go +++ b/spec/v1/plugin/errors.go @@ -52,6 +52,19 @@ func (e RequestError) Unwrap() error { return e.Err } +func (e RequestError) Is(target error) bool { + if et, ok := target.(RequestError); ok { + if e.Code != et.Code { + return false + } + if e.Err == et.Err { + return true + } + return e.Err != nil && et.Err != nil && e.Err.Error() == et.Err.Error() + } + return false +} + func (e RequestError) MarshalJSON() ([]byte, error) { var msg string if e.Err != nil { @@ -69,6 +82,9 @@ func (e *RequestError) UnmarshalJSON(data []byte) error { if tmp.Code == "" && tmp.Message == "" && tmp.Metadata == nil { return errors.New("incomplete json") } - *e = RequestError{tmp.Code, errors.New(tmp.Message), tmp.Metadata} + *e = RequestError{Code: tmp.Code, Metadata: tmp.Metadata} + if tmp.Message != "" { + e.Err = errors.New(tmp.Message) + } return nil } diff --git a/spec/v1/plugin/errors_test.go b/spec/v1/plugin/errors_test.go index 4a9ffdce..1b8bda85 100644 --- a/spec/v1/plugin/errors_test.go +++ b/spec/v1/plugin/errors_test.go @@ -88,3 +88,29 @@ func TestRequestError_UnmarshalJSON(t *testing.T) { }) } } + +func TestRequestError_Is(t *testing.T) { + type args struct { + target error + } + tests := []struct { + name string + e RequestError + args args + want bool + }{ + {"nil", RequestError{}, args{nil}, false}, + {"not same type", RequestError{Err: errors.New("foo")}, args{errors.New("foo")}, false}, + {"only same code", RequestError{Code: ErrorCodeGeneric, Err: errors.New("foo")}, args{RequestError{Code: ErrorCodeGeneric, Err: errors.New("bar")}}, false}, + {"only same message", RequestError{Code: ErrorCodeTimeout, Err: errors.New("foo")}, args{RequestError{Code: ErrorCodeGeneric, Err: errors.New("foo")}}, false}, + {"same with nil message", RequestError{Code: ErrorCodeGeneric}, args{RequestError{Code: ErrorCodeGeneric}}, true}, + {"same", RequestError{Code: ErrorCodeGeneric, Err: errors.New("foo")}, args{RequestError{Code: ErrorCodeGeneric, Err: errors.New("foo")}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.e.Is(tt.args.target); got != tt.want { + t.Errorf("RequestError.Is() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/spec/v1/plugin/plugin.go b/spec/v1/plugin/plugin.go index 28795b7a..d9858fd2 100644 --- a/spec/v1/plugin/plugin.go +++ b/spec/v1/plugin/plugin.go @@ -1,6 +1,10 @@ package plugin -import "github.com/notaryproject/notation-go/spec/v1/signature" +import ( + "context" + + "github.com/notaryproject/notation-go/spec/v1/signature" +) // Prefix is the prefix required on all plugin binary names. const Prefix = "notation-" @@ -113,3 +117,19 @@ type GenerateEnvelopeResponse struct { // Annotations to be appended to Signature Manifest annotations. Annotations map[string]string `json:"annotations,omitempty"` } + +// Runner is an interface for running commands against a plugin. +type Runner interface { + // Run executes the specified command and waits for it to complete. + // + // When the returned object is not nil, its type is guaranteed to remain always the same for a given Command. + // + // The returned error is nil if: + // - the plugin exists + // - the command runs and exits with a zero exit status + // - the command stdout contains a valid json object which can be unmarshal-ed. + // + // If the command starts but does not complete successfully, the error is of type RequestError wrapping a *exec.ExitError. + // Other error types may be returned for other situations. + Run(ctx context.Context, cmd Command, req interface{}) (interface{}, error) +} From c55f36f215443bb0d3fab3fea3cd7ea5293db55e Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 11 May 2022 09:41:44 +0200 Subject: [PATCH 44/58] update plugin spec Signed-off-by: qmuntal --- notation.go | 3 +++ signature/jws/plugin.go | 25 ++++++++++++++++++------- spec/v1/plugin/plugin.go | 4 ---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/notation.go b/notation.go index b8a78931..0f0c1984 100644 --- a/notation.go +++ b/notation.go @@ -22,6 +22,9 @@ type SignOptions struct { // the certificates in the fetched timestamp signature. // An empty list of `KeyUsages` in the verify options implies ExtKeyUsageTimeStamping. TSAVerifyOptions x509.VerifyOptions + + // Sets or overrides the plugin configuration. + PluginConfig map[string]string } // Signer is a generic interface for signing an artifact. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 266c91da..37b01b54 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -38,7 +38,6 @@ var keySpecToAlg = map[signature.Key]string{ type PluginSigner struct { Runner plugin.Runner KeyID string - KeyName string PluginConfig map[string]string } @@ -60,12 +59,11 @@ func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts return nil, fmt.Errorf("plugin does not have signing capabilities") } -func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResponse, error) { +func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string) (*plugin.DescribeKeyResponse, error) { req := &plugin.DescribeKeyRequest{ ContractVersion: "1", - KeyName: s.KeyName, KeyID: s.KeyID, - PluginConfig: s.PluginConfig, + PluginConfig: config, } out, err := s.Runner.Run(ctx, plugin.CommandDescribeKey, req) if err != nil { @@ -75,8 +73,9 @@ func (s *PluginSigner) describeKey(ctx context.Context) (*plugin.DescribeKeyResp } func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { + config := s.mergeConfig(opts.PluginConfig) // Get key info. - key, err := s.describeKey(ctx) + key, err := s.describeKey(ctx, config) if err != nil { return nil, err } @@ -108,12 +107,11 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des // Execute plugin sign command. req := &plugin.GenerateSignatureRequest{ ContractVersion: "1", - KeyName: s.KeyName, KeyID: s.KeyID, KeySpec: key.KeySpec, Hash: key.KeySpec.Hash(), Payload: signing, - PluginConfig: s.PluginConfig, + PluginConfig: config, } out, err := s.Runner.Run(ctx, plugin.CommandGenerateSignature, req) if err != nil { @@ -167,6 +165,19 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des return jwtEnvelop(ctx, opts, signing+"."+signed64Url, rawCerts) } +func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string { + c := make(map[string]string, len(s.PluginConfig)+len(config)) + // First clone s.PluginConfig. + for k, v := range s.PluginConfig { + c[k] = v + } + // Then set or override entries from config. + for k, v := range config { + c[k] = v + } + return c +} + func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { return nil, errors.New("not implemented") } diff --git a/spec/v1/plugin/plugin.go b/spec/v1/plugin/plugin.go index d9858fd2..84e82233 100644 --- a/spec/v1/plugin/plugin.go +++ b/spec/v1/plugin/plugin.go @@ -48,10 +48,8 @@ const ( ) // DescribeKeyRequest contains the parameters passed in a describe-key request. -// All parameters are required. type DescribeKeyRequest struct { ContractVersion string `json:"contractVersion"` - KeyName string `json:"keyName"` KeyID string `json:"keyId"` PluginConfig map[string]string `json:"pluginConfig,omitempty"` } @@ -69,7 +67,6 @@ type DescribeKeyResponse struct { // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { ContractVersion string `json:"contractVersion"` - KeyName string `json:"keyName"` KeyID string `json:"keyId"` KeySpec signature.Key `json:"keySpec"` Hash signature.Hash `json:"hashAlgorithm"` @@ -98,7 +95,6 @@ type GenerateSignatureResponse struct { // GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. type GenerateEnvelopeRequest struct { ContractVersion string `json:"contractVersion"` - KeyName string `json:"keyName"` KeyID string `json:"keyId"` PayloadType string `json:"payloadType"` SignatureEnvelopeType string `json:"signatureEnvelopeType"` From 92366fbd2e4c6ebf7437b78cc4c78a116f7578d6 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 11 May 2022 15:58:47 +0200 Subject: [PATCH 45/58] PR feedback Signed-off-by: qmuntal --- signature/jws/plugin.go | 24 ++++--------- signature/jws/plugin_test.go | 65 ++++++++++-------------------------- signature/jws/signer.go | 12 +++---- signature/jws/spec.go | 8 ++--- signature/jws/verifier.go | 14 ++------ spec/v1/plugin/plugin.go | 26 +++++---------- spec/v1/signature/jws.go | 4 +-- spec/v1/signature/types.go | 18 +++++----- 8 files changed, 56 insertions(+), 115 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 37b01b54..09551d80 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -22,7 +22,7 @@ var supportedAlgs = map[string]bool{ jwt.SigningMethodES512.Name: true, } -var keySpecToAlg = map[signature.Key]string{ +var keySpecToAlg = map[signature.KeyType]string{ signature.RSA_2048: jwt.SigningMethodPS256.Alg(), signature.RSA_3072: jwt.SigningMethodPS384.Alg(), signature.RSA_4096: jwt.SigningMethodPS512.Alg(), @@ -141,11 +141,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des // Verify the hash of the request payload against the response signature // using the public key of the signing certificate. - signed, err := base64.RawStdEncoding.DecodeString(resp.Signature) - if err != nil { - return nil, fmt.Errorf("signature not base64-encoded: %v", err) - } - signed64Url := base64.RawURLEncoding.EncodeToString(signed) + signed64Url := base64.RawURLEncoding.EncodeToString(resp.Signature) err = verifyJWT(resp.SigningAlgorithm, signing, signed64Url, certs[0]) if err != nil { return nil, fmt.Errorf("verification error: %v", err) @@ -158,11 +154,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Assemble the JWS signature envelope. - rawCerts := make([]string, len(certs)) - for i, cert := range certs { - rawCerts[i] = base64.RawStdEncoding.EncodeToString(cert.Raw) - } - return jwtEnvelop(ctx, opts, signing+"."+signed64Url, rawCerts) + return jwtEnvelope(ctx, opts, signing+"."+signed64Url, resp.CertificateChain) } func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string { @@ -182,14 +174,10 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc signa return nil, errors.New("not implemented") } -func parseCertChain(certChain []string) ([]*x509.Certificate, error) { +func parseCertChain(certChain [][]byte) ([]*x509.Certificate, error) { certs := make([]*x509.Certificate, len(certChain)) - for i, data := range certChain { - der, err := base64.RawStdEncoding.DecodeString(data) - if err != nil { - return nil, fmt.Errorf("certificate not base64-encoded: %v", err) - } - cert, err := x509.ParseCertificate(der) + for i, cert := range certChain { + cert, err := x509.ParseCertificate(cert) if err != nil { return nil, err } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index f3fa358a..b2ee7f23 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -39,16 +39,16 @@ func (r *mockRunner) Run(ctx context.Context, cmd plugin.Command, req interface{ type mockSignerPlugin struct { KeyID string - KeySpec signature.Key - Sign func(payload string) string + KeySpec signature.KeyType + Sign func(payload string) []byte SigningAlg string - Cert string + Cert []byte n int } func (s *mockSignerPlugin) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { - var chain []string - if s.Cert != "" { + var chain [][]byte + if len(s.Cert) != 0 { chain = append(chain, s.Cert) } defer func() { s.n++ }() @@ -58,7 +58,7 @@ func (s *mockSignerPlugin) Run(ctx context.Context, cmd plugin.Command, req inte case 1: return &plugin.DescribeKeyResponse{KeyID: s.KeyID, KeySpec: s.KeySpec}, nil case 2: - var signed string + var signed []byte if s.Sign != nil { signed = s.Sign(req.(*plugin.GenerateSignatureRequest).Payload) } @@ -166,50 +166,19 @@ func TestPluginSigner_Sign_NoCertChain(t *testing.T) { testPluginSignerError(t, signer, "empty certificate chain") } -func TestPluginSigner_Sign_CertNotBase64(t *testing.T) { - signer := PluginSigner{ - Runner: &mockSignerPlugin{ - KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodES256.Alg(), - Cert: "r a w", - }, - KeyID: "1", - } - testPluginSignerError(t, signer, "certificate not base64-encoded") -} - func TestPluginSigner_Sign_MalformedCert(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), - Cert: base64.RawStdEncoding.EncodeToString([]byte("mocked")), + Cert: []byte("mocked"), }, KeyID: "1", } testPluginSignerError(t, signer, "x509: malformed certificate") } -func TestPluginSigner_Sign_SignatureNotBase64(t *testing.T) { - _, cert, err := generateKeyCertPair() - if err != nil { - t.Fatalf("generateKeyCertPair() error = %v", err) - } - signer := PluginSigner{ - Runner: &mockSignerPlugin{ - KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodES256.Alg(), - Sign: func(payload string) string { return "r a w" }, - Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), - }, - KeyID: "1", - } - testPluginSignerError(t, signer, "signature not base64-encoded") -} - func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { _, cert, err := generateKeyCertPair() if err != nil { @@ -220,17 +189,17 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { KeyID: "1", KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodES256.Alg(), - Sign: func(payload string) string { return base64.RawStdEncoding.EncodeToString([]byte("r a w")) }, - Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), + Sign: func(payload string) []byte { return []byte("r a w") }, + Cert: cert.Raw, }, KeyID: "1", } testPluginSignerError(t, signer, "verification error") } -func validSign(t *testing.T, key interface{}) func(string) string { +func validSign(t *testing.T, key interface{}) func(string) []byte { t.Helper() - return func(payload string) string { + return func(payload string) []byte { signed, err := jwt.SigningMethodPS256.Sign(payload, key) if err != nil { t.Fatal(err) @@ -239,7 +208,7 @@ func validSign(t *testing.T, key interface{}) func(string) string { if err != nil { t.Fatal(err) } - return base64.RawStdEncoding.EncodeToString(encSigned) + return encSigned } } @@ -265,7 +234,7 @@ func TestPluginSigner_Sign_CertWithoutDigitalSignatureBit(t *testing.T) { KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), - Cert: base64.RawStdEncoding.EncodeToString(certBytes), + Cert: certBytes, }, KeyID: "1", } @@ -294,7 +263,7 @@ func TestPluginSigner_Sign_CertWithout_idkpcodeSigning(t *testing.T) { KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), - Cert: base64.RawStdEncoding.EncodeToString(certBytes), + Cert: certBytes, }, KeyID: "1", } @@ -324,7 +293,7 @@ func TestPluginSigner_Sign_CertBasicConstraintCA(t *testing.T) { KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), - Cert: base64.RawStdEncoding.EncodeToString(certBytes), + Cert: certBytes, }, KeyID: "1", } @@ -342,7 +311,7 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { KeySpec: signature.RSA_2048, SigningAlg: jwt.SigningMethodPS256.Alg(), Sign: validSign(t, key), - Cert: base64.RawStdEncoding.EncodeToString(cert.Raw), + Cert: cert.Raw, }, KeyID: "1", } @@ -358,7 +327,7 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { want := signature.JWSEnvelope{ Protected: "eyJhbGciOiJQUzI1NiIsImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS52Mi5qd3MudjEifQ", Header: signature.JWSUnprotectedHeader{ - CertChain: []string{base64.RawStdEncoding.EncodeToString(cert.Raw)}, + CertChain: [][]byte{cert.Raw}, }, } if got.Protected != want.Protected { diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 46b9ddc8..88fefaec 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -28,7 +28,7 @@ type Signer struct { // certChain contains the X.509 public key certificate or certificate chain corresponding // to the key used to generate the signature. - certChain []string + certChain [][]byte } // NewSigner creates a signer with the recommended signing method and a signing key bundled @@ -69,9 +69,9 @@ func NewSignerWithCertificateChain(method jwt.SigningMethod, key crypto.PrivateK return nil, err } - rawCerts := make([]string, len(certChain)) + rawCerts := make([][]byte, len(certChain)) for i, cert := range certChain { - rawCerts[i] = base64.RawStdEncoding.EncodeToString(cert.Raw) + rawCerts[i] = cert.Raw } return &Signer{ method: method, @@ -93,7 +93,7 @@ func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notat if err != nil { return nil, err } - return jwtEnvelop(ctx, opts, compact, s.certChain) + return jwtEnvelope(ctx, opts, compact, s.certChain) } func jwtToken(alg string, claims jwt.Claims) *jwt.Token { @@ -106,7 +106,7 @@ func jwtToken(alg string, claims jwt.Claims) *jwt.Token { } } -func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, certChain []string) ([]byte, error) { +func jwtEnvelope(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { parts := strings.Split(compact, ".") if len(parts) != 3 { return nil, errors.New("invalid compact serialization") @@ -126,7 +126,7 @@ func jwtEnvelop(ctx context.Context, opts notation.SignOptions, compact string, if err != nil { return nil, fmt.Errorf("timestamp failed: %w", err) } - envelope.Header.TimeStampToken = base64.RawStdEncoding.EncodeToString(token) + envelope.Header.TimeStampToken = token } // encode in flatten JWS JSON serialization diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 9d226c80..34cdd2b4 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -40,13 +40,13 @@ func checkCertChain(certChain []*x509.Certificate) error { if len(certChain) == 0 { return nil } - if err := verifyCert(certChain[0], x509.ExtKeyUsageCodeSigning); err != nil { + if err := verifyCertExtKeyUsage(certChain[0], x509.ExtKeyUsageCodeSigning); err != nil { return fmt.Errorf("signing certificate does not meet the minimum requirements: %w", err) } for _, c := range certChain[1:] { for _, ext := range c.ExtKeyUsage { if ext == x509.ExtKeyUsageTimeStamping { - if err := verifyCert(c, x509.ExtKeyUsageTimeStamping); err != nil { + if err := verifyCertExtKeyUsage(c, x509.ExtKeyUsageTimeStamping); err != nil { return fmt.Errorf("timestamping certificate does not meet the minimum requirements: %w", err) } } @@ -59,9 +59,9 @@ var ( oidExtensionKeyUsage = []int{2, 5, 29, 15} ) -// validateCert checks cert meets the requirements defined in +// verifyCertExtKeyUsage checks cert meets the requirements defined in // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#certificate-requirements. -func verifyCert(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { +func verifyCertExtKeyUsage(cert *x509.Certificate, extKeyUsage x509.ExtKeyUsage) error { if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { return errors.New("keyUsage must have the bit positions for digitalSignature set") } diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index fd2f3cab..3fb8fe24 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -84,26 +84,18 @@ func (v *Verifier) verifySigner(sig *signature.JWSEnvelope) (jwt.SigningMethod, if len(sig.Header.CertChain) == 0 { return nil, nil, errors.New("signer certificates not found") } - token, err := base64.RawStdEncoding.DecodeString(sig.Header.TimeStampToken) - if err != nil { - return nil, nil, err - } - return v.verifySignerFromCertChain(sig.Header.CertChain, token, sig.Signature) + return v.verifySignerFromCertChain(sig.Header.CertChain, sig.Header.TimeStampToken, sig.Signature) } // verifySignerFromCertChain verifies the signing identity from the provided certificate // chain and returns the verification key. The first certificate of the certificate chain // contains the key, which used to sign the artifact. // Reference: RFC 7515 4.1.6 "x5c" (X.509 Certificate Chain) Header Parameter. -func (v *Verifier) verifySignerFromCertChain(certChain []string, timeStampToken []byte, encodedSig string) (jwt.SigningMethod, crypto.PublicKey, error) { +func (v *Verifier) verifySignerFromCertChain(certChain [][]byte, timeStampToken []byte, encodedSig string) (jwt.SigningMethod, crypto.PublicKey, error) { // prepare for certificate verification certs := make([]*x509.Certificate, 0, len(certChain)) for _, certBytes := range certChain { - raw, err := base64.RawStdEncoding.DecodeString(certBytes) - if err != nil { - return nil, nil, err - } - cert, err := x509.ParseCertificate(raw) + cert, err := x509.ParseCertificate(certBytes) if err != nil { return nil, nil, err } diff --git a/spec/v1/plugin/plugin.go b/spec/v1/plugin/plugin.go index 84e82233..f7a8cca3 100644 --- a/spec/v1/plugin/plugin.go +++ b/spec/v1/plugin/plugin.go @@ -61,14 +61,14 @@ type DescribeKeyResponse struct { // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - KeySpec signature.Key `json:"keySpec"` + KeySpec signature.KeyType `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { ContractVersion string `json:"contractVersion"` KeyID string `json:"keyId"` - KeySpec signature.Key `json:"keySpec"` + KeySpec signature.KeyType `json:"keySpec"` Hash signature.Hash `json:"hashAlgorithm"` SignatureEnvelopeType string `json:"signatureEnvelopeType"` Payload string `json:"payload"` @@ -77,11 +77,8 @@ type GenerateSignatureRequest struct { // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { - // The same key id as passed in the request. - KeyID string `json:"keyId"` - - // Base64 encoded signature. - Signature string `json:"signature"` + KeyID string `json:"keyId"` + Signature []byte `json:"signature"` // One of following supported signing algorithms: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection @@ -89,7 +86,7 @@ type GenerateSignatureResponse struct { // Ordered list of certificates starting with leaf certificate // and ending with root certificate. - CertificateChain []string `json:"certificateChain"` + CertificateChain [][]byte `json:"certificateChain"` } // GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. @@ -98,20 +95,15 @@ type GenerateEnvelopeRequest struct { KeyID string `json:"keyId"` PayloadType string `json:"payloadType"` SignatureEnvelopeType string `json:"signatureEnvelopeType"` - Payload string `json:"payload"` + Payload []byte `json:"payload"` PluginConfig map[string]string `json:"pluginConfig,omitempty"` } // GenerateSignatureResponse is the response of a generate-envelop request. type GenerateEnvelopeResponse struct { - // Base64 encoded signature envelope. - SignatureEnvelope string `json:"signatureEnvelope"` - - // The media type of the envelope of notation signature. - SignatureEnvelopeType string `json:"signatureEnvelopeType"` - - // Annotations to be appended to Signature Manifest annotations. - Annotations map[string]string `json:"annotations,omitempty"` + SignatureEnvelope []byte `json:"signatureEnvelope"` + SignatureEnvelopeType string `json:"signatureEnvelopeType"` + Annotations map[string]string `json:"annotations,omitempty"` } // Runner is an interface for running commands against a plugin. diff --git a/spec/v1/signature/jws.go b/spec/v1/signature/jws.go index 086700e5..e3e46202 100644 --- a/spec/v1/signature/jws.go +++ b/spec/v1/signature/jws.go @@ -34,10 +34,10 @@ type JWSProtectedHeader struct { // JWSUnprotectedHeader contains the set of unprotected headers. type JWSUnprotectedHeader struct { // RFC3161 time stamp token Base64-encoded. - TimeStampToken string `json:"timestamp,omitempty"` + TimeStampToken []byte `json:"timestamp,omitempty"` // List of X.509 certificates, each one Base64-encoded. - CertChain []string `json:"x5c"` + CertChain [][]byte `json:"x5c"` } // JWSEnvelope is the final signature envelope. diff --git a/spec/v1/signature/types.go b/spec/v1/signature/types.go index 046f1f4d..4027aabf 100644 --- a/spec/v1/signature/types.go +++ b/spec/v1/signature/types.go @@ -27,22 +27,22 @@ func (d Descriptor) Equal(t Descriptor) bool { return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size } -// Key defines a key type and size. -type Key string +// KeyType defines a key type and size. +type KeyType string // One of following supported specs // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection const ( - RSA_2048 Key = "RSA_2048" - RSA_3072 Key = "RSA_3072" - RSA_4096 Key = "RSA_4096" - EC_256 Key = "EC_256" - EC_384 Key = "EC_384" - EC_512 Key = "EC_512" + RSA_2048 KeyType = "RSA_2048" + RSA_3072 KeyType = "RSA_3072" + RSA_4096 KeyType = "RSA_4096" + EC_256 KeyType = "EC_256" + EC_384 KeyType = "EC_384" + EC_512 KeyType = "EC_512" ) // Hash returns the Hash associated k. -func (k Key) Hash() Hash { +func (k KeyType) Hash() Hash { switch k { case RSA_2048, EC_256: return SHA256 From fc5592fce3517a0bbdb8559987f1eef4660c33e6 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 11 May 2022 16:04:28 +0200 Subject: [PATCH 46/58] check runner response type Signed-off-by: qmuntal --- signature/jws/plugin.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 09551d80..6a4c42d2 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -47,7 +47,10 @@ func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts if err != nil { return nil, fmt.Errorf("metadata command failed: %w", err) } - metadata := out.(*plugin.Metadata) + metadata, ok := out.(*plugin.Metadata) + if !ok { + return nil, fmt.Errorf("plugin runner returned incorrect get-plugin-metadata response type '%T'", out) + } if err := metadata.Validate(); err != nil { return nil, fmt.Errorf("invalid plugin metadata: %w", err) } @@ -69,7 +72,11 @@ func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string if err != nil { return nil, fmt.Errorf("describe-key command failed: %w", err) } - return out.(*plugin.DescribeKeyResponse), nil + resp, ok := out.(*plugin.DescribeKeyResponse) + if !ok { + return nil, fmt.Errorf("plugin runner returned incorrect describe-key response type '%T'", out) + } + return resp, nil } func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { @@ -117,7 +124,10 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des if err != nil { return nil, fmt.Errorf("generate-signature command failed: %w", err) } - resp := out.(*plugin.GenerateSignatureResponse) + resp, ok := out.(*plugin.GenerateSignatureResponse) + if !ok { + return nil, fmt.Errorf("plugin runner returned incorrect generate-signature response type '%T'", out) + } // Check keyID is honored. if s.KeyID != resp.KeyID { From b2c02f5dd5a38cfc92f44df29975a9a814a722d2 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 12 May 2022 08:43:09 +0200 Subject: [PATCH 47/58] remove spec/v1 directory Signed-off-by: qmuntal --- notation.go | 2 +- plugin/manager/integration_test.go | 2 +- plugin/manager/manager.go | 2 +- plugin/manager/manager_test.go | 2 +- signature/jws/plugin.go | 4 ++-- signature/jws/plugin_test.go | 4 ++-- signature/jws/signer.go | 2 +- signature/jws/signer_test.go | 2 +- signature/jws/spec.go | 2 +- signature/jws/verifier.go | 2 +- spec/{v1 => }/plugin/errors.go | 0 spec/{v1 => }/plugin/errors_test.go | 0 spec/{v1 => }/plugin/metadata.go | 0 spec/{v1 => }/plugin/metadata_test.go | 0 spec/{v1 => }/plugin/plugin.go | 2 +- spec/{v1 => }/signature/jws.go | 0 spec/{v1 => }/signature/types.go | 0 17 files changed, 13 insertions(+), 13 deletions(-) rename spec/{v1 => }/plugin/errors.go (100%) rename spec/{v1 => }/plugin/errors_test.go (100%) rename spec/{v1 => }/plugin/metadata.go (100%) rename spec/{v1 => }/plugin/metadata_test.go (100%) rename spec/{v1 => }/plugin/plugin.go (98%) rename spec/{v1 => }/signature/jws.go (100%) rename spec/{v1 => }/signature/types.go (100%) diff --git a/notation.go b/notation.go index 0f0c1984..1ac6fcdb 100644 --- a/notation.go +++ b/notation.go @@ -6,7 +6,7 @@ import ( "time" "github.com/notaryproject/notation-go/crypto/timestamp" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/signature" ) // SignOptions contains parameters for Signer.Sign. diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index 4fb6d1b2..038f728e 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/notaryproject/notation-go/plugin/manager" - "github.com/notaryproject/notation-go/spec/v1/plugin" + "github.com/notaryproject/notation-go/spec/plugin" ) func preparePlugin(t *testing.T) string { diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 94b29473..df1f8187 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -13,7 +13,7 @@ import ( "path/filepath" "runtime" - "github.com/notaryproject/notation-go/spec/v1/plugin" + "github.com/notaryproject/notation-go/spec/plugin" ) // Plugin represents a potential plugin with all it's metadata. diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 05d2fc0d..1a668096 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -10,7 +10,7 @@ import ( "testing" "testing/fstest" - "github.com/notaryproject/notation-go/spec/v1/plugin" + "github.com/notaryproject/notation-go/spec/plugin" ) type testCommander struct { diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 6a4c42d2..9f6e1030 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -9,8 +9,8 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/spec/v1/plugin" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/plugin" + "github.com/notaryproject/notation-go/spec/signature" ) var supportedAlgs = map[string]bool{ diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index b2ee7f23..141c954a 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -17,8 +17,8 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/spec/v1/plugin" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/plugin" + "github.com/notaryproject/notation-go/spec/signature" ) var validMetadata = plugin.Metadata{ diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 88fefaec..08da8e49 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -14,7 +14,7 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp" "github.com/notaryproject/notation-go/internal/crypto/pki" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/signature" ) // Signer signs artifacts and generates JWS signatures. diff --git a/signature/jws/signer_test.go b/signature/jws/signer_test.go index 5d6d1ab4..b84a826b 100644 --- a/signature/jws/signer_test.go +++ b/signature/jws/signer_test.go @@ -13,7 +13,7 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp/timestamptest" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/signature" "github.com/opencontainers/go-digest" ) diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 34cdd2b4..a5c9935b 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -13,7 +13,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/signature" ) type notaryClaim struct { diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index 3fb8fe24..1cb8e339 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -14,7 +14,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/signature" ) // maxTimestampAccuracy specifies the max acceptable accuracy for timestamp. diff --git a/spec/v1/plugin/errors.go b/spec/plugin/errors.go similarity index 100% rename from spec/v1/plugin/errors.go rename to spec/plugin/errors.go diff --git a/spec/v1/plugin/errors_test.go b/spec/plugin/errors_test.go similarity index 100% rename from spec/v1/plugin/errors_test.go rename to spec/plugin/errors_test.go diff --git a/spec/v1/plugin/metadata.go b/spec/plugin/metadata.go similarity index 100% rename from spec/v1/plugin/metadata.go rename to spec/plugin/metadata.go diff --git a/spec/v1/plugin/metadata_test.go b/spec/plugin/metadata_test.go similarity index 100% rename from spec/v1/plugin/metadata_test.go rename to spec/plugin/metadata_test.go diff --git a/spec/v1/plugin/plugin.go b/spec/plugin/plugin.go similarity index 98% rename from spec/v1/plugin/plugin.go rename to spec/plugin/plugin.go index f7a8cca3..73cc0f16 100644 --- a/spec/v1/plugin/plugin.go +++ b/spec/plugin/plugin.go @@ -3,7 +3,7 @@ package plugin import ( "context" - "github.com/notaryproject/notation-go/spec/v1/signature" + "github.com/notaryproject/notation-go/spec/signature" ) // Prefix is the prefix required on all plugin binary names. diff --git a/spec/v1/signature/jws.go b/spec/signature/jws.go similarity index 100% rename from spec/v1/signature/jws.go rename to spec/signature/jws.go diff --git a/spec/v1/signature/types.go b/spec/signature/types.go similarity index 100% rename from spec/v1/signature/types.go rename to spec/signature/types.go From b9246e9064caff0ed74434f06c20bbf8e3fe69d4 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 12 May 2022 08:50:17 +0200 Subject: [PATCH 48/58] Revert "remove support for multiple signature envelope" This reverts commit c9afb7ce1069050409dc5b4712a9c9c0eaedddf3. Signed-off-by: qmuntal --- crypto/jwsutil/envelope.go | 53 +++++++++++++++++ crypto/jwsutil/envelope_test.go | 102 ++++++++++++++++++++++++++++++++ crypto/jwsutil/errors.go | 9 +++ crypto/jwsutil/signature.go | 56 ++++++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 crypto/jwsutil/envelope.go create mode 100644 crypto/jwsutil/envelope_test.go create mode 100644 crypto/jwsutil/errors.go create mode 100644 crypto/jwsutil/signature.go diff --git a/crypto/jwsutil/envelope.go b/crypto/jwsutil/envelope.go new file mode 100644 index 00000000..951ab0be --- /dev/null +++ b/crypto/jwsutil/envelope.go @@ -0,0 +1,53 @@ +package jwsutil + +import "encoding/json" + +// Envelope contains a common payload signed by multiple signatures. +type Envelope struct { + Payload string `json:"payload,omitempty"` + Signatures []Signature `json:"signatures,omitempty"` +} + +// Size returns the number of enclosed signatures. +func (e Envelope) Size() int { + return len(e.Signatures) +} + +// Open opens the evelope and returns the first or default complete signature. +func (e Envelope) Open() CompleteSignature { + if len(e.Signatures) == 0 { + return CompleteSignature{ + Payload: e.Payload, + } + } + return CompleteSignature{ + Payload: e.Payload, + Signature: e.Signatures[0], + } +} + +// UnmarshalJSON parses the JSON serialized JWS. +// Reference: RFC 7515 7.2 JWS JSON Serialization. +func (e *Envelope) UnmarshalJSON(data []byte) error { + var combined struct { + CompleteSignature + Signatures []Signature `json:"signatures"` + } + if err := json.Unmarshal(data, &combined); err != nil { + return ErrInvalidJSONSerialization + } + if len(combined.Signatures) == 0 { + *e = Envelope{ + Payload: combined.Payload, + Signatures: []Signature{ + combined.Signature, + }, + } + } else { + *e = Envelope{ + Payload: combined.Payload, + Signatures: combined.Signatures, + } + } + return nil +} diff --git a/crypto/jwsutil/envelope_test.go b/crypto/jwsutil/envelope_test.go new file mode 100644 index 00000000..7e142ec1 --- /dev/null +++ b/crypto/jwsutil/envelope_test.go @@ -0,0 +1,102 @@ +package jwsutil + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestEnvelope_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + data string + want Envelope + }{ + { + name: "General JWS JSON Serialization Syntax (multiple signatures)", + data: `{ + "payload": "test payload", + "signatures": [ + { + "protected": "protected foo", + "header": {"unprotected": "foo"}, + "signature": "signature foo" + }, + { + "protected": "protected bar", + "header": {"unprotected": "bar"}, + "signature": "signature bar" + } + ] + }`, + want: Envelope{ + Payload: "test payload", + Signatures: []Signature{ + { + Protected: "protected foo", + Unprotected: []byte(`{"unprotected": "foo"}`), + Signature: "signature foo", + }, + { + Protected: "protected bar", + Unprotected: []byte(`{"unprotected": "bar"}`), + Signature: "signature bar", + }, + }, + }, + }, + { + name: "General JWS JSON Serialization Syntax (single signature)", + data: `{ + "payload": "test payload", + "signatures": [ + { + "protected": "protected foo", + "header": {"unprotected": "foo"}, + "signature": "signature foo" + } + ] + }`, + want: Envelope{ + Payload: "test payload", + Signatures: []Signature{ + { + Protected: "protected foo", + Unprotected: []byte(`{"unprotected": "foo"}`), + Signature: "signature foo", + }, + }, + }, + }, + { + name: "Flattened JWS JSON Serialization Syntax", + data: `{ + "payload": "test payload", + "protected": "protected foo", + "header": {"unprotected": "foo"}, + "signature": "signature foo" + }`, + want: Envelope{ + Payload: "test payload", + Signatures: []Signature{ + { + Protected: "protected foo", + Unprotected: []byte(`{"unprotected": "foo"}`), + Signature: "signature foo", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got Envelope + if err := json.Unmarshal([]byte(tt.data), &got); err != nil { + t.Fatalf("Envelope.UnmarshalJSON() error = %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Envelope.UnmarshalJSON() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/crypto/jwsutil/errors.go b/crypto/jwsutil/errors.go new file mode 100644 index 00000000..a64c217d --- /dev/null +++ b/crypto/jwsutil/errors.go @@ -0,0 +1,9 @@ +package jwsutil + +import "errors" + +// Common errors +var ( + ErrInvalidCompactSerialization = errors.New("invalid compact serialization") + ErrInvalidJSONSerialization = errors.New("invalid JSON serialization") +) diff --git a/crypto/jwsutil/signature.go b/crypto/jwsutil/signature.go new file mode 100644 index 00000000..1d965fa8 --- /dev/null +++ b/crypto/jwsutil/signature.go @@ -0,0 +1,56 @@ +// Package jwsutil provides serialization utilities for JWT libraries to comfort JWS. +// Reference: RFC 7515 JSON Web Signature (JWS). +package jwsutil + +import ( + "encoding/json" + "strings" +) + +// Signature represents a detached signature. +type Signature struct { + Protected string `json:"protected,omitempty"` + Unprotected json.RawMessage `json:"header,omitempty"` + Signature string `json:"signature,omitempty"` +} + +// CompleteSignature represents a clear signed signature. +// A CompleteSignature can be viewed as an envelope with a single signature in +// flattened JWS JSON serialization syntax. +// Reference: RFC 7515 7.2 JWS JSON Serialization. +type CompleteSignature struct { + Payload string `json:"payload,omitempty"` + Signature +} + +// Parse parses the compact serialized JWS. +// Reference: RFC 7515 7.1 JWS Compact Serialization. +func ParseCompact(serialized string) (CompleteSignature, error) { + parts := strings.Split(serialized, ".") + if len(parts) != 3 { + return CompleteSignature{}, ErrInvalidCompactSerialization + } + return CompleteSignature{ + Payload: parts[1], + Signature: Signature{ + Protected: parts[0], + Signature: parts[2], + }, + }, nil +} + +// SerializeCompact serialize the signature in JWS Compact Serialization +// Reference: RFC 7515 7.1 JWS Compact Serialization. +func (s CompleteSignature) SerializeCompact() string { + return strings.Join([]string{s.Protected, s.Payload, s.Signature.Signature}, ".") +} + +// Enclose packs the signature into an envelope. +func (s CompleteSignature) Enclose() Envelope { + return Envelope{ + Payload: s.Payload, + Signatures: []Signature{ + s.Signature, + }, + } +} From cbc16f91894706ef76b46fbe3894f8567f2cd124 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 12 May 2022 09:01:15 +0200 Subject: [PATCH 49/58] don't check timestamp certificate Signed-off-by: qmuntal --- signature/jws/plugin.go | 5 ++--- signature/jws/spec.go | 19 ------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 9f6e1030..50199bf2 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -158,9 +158,8 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Check the the certificate chain conforms to the spec. - err = checkCertChain(certs) - if err != nil { - return nil, err + if err := verifyCertExtKeyUsage(certs[0], x509.ExtKeyUsageCodeSigning); err != nil { + return nil, fmt.Errorf("signing certificate does not meet the minimum requirements: %w", err) } // Assemble the JWS signature envelope. diff --git a/signature/jws/spec.go b/signature/jws/spec.go index a5c9935b..0d66f338 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -36,25 +36,6 @@ func packPayload(desc signature.Descriptor, opts notation.SignOptions) jwt.Claim } } -func checkCertChain(certChain []*x509.Certificate) error { - if len(certChain) == 0 { - return nil - } - if err := verifyCertExtKeyUsage(certChain[0], x509.ExtKeyUsageCodeSigning); err != nil { - return fmt.Errorf("signing certificate does not meet the minimum requirements: %w", err) - } - for _, c := range certChain[1:] { - for _, ext := range c.ExtKeyUsage { - if ext == x509.ExtKeyUsageTimeStamping { - if err := verifyCertExtKeyUsage(c, x509.ExtKeyUsageTimeStamping); err != nil { - return fmt.Errorf("timestamping certificate does not meet the minimum requirements: %w", err) - } - } - } - } - return nil -} - var ( oidExtensionKeyUsage = []int{2, 5, 29, 15} ) From 5e2d8ca203f3515e8993fde7dda322f7d47c3810 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 12 May 2022 10:09:24 +0200 Subject: [PATCH 50/58] define SignatureAlgorithm in spec Signed-off-by: qmuntal --- signature/jws/plugin.go | 29 +++++------------------ signature/jws/plugin_test.go | 16 ++++++------- spec/plugin/plugin.go | 9 +++----- spec/signature/jws.go | 39 +++++++++++++++++++++++++++++++ spec/signature/types.go | 45 ++++++++++++++++++++++++++++++------ 5 files changed, 94 insertions(+), 44 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 50199bf2..ebb71c23 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -13,24 +13,6 @@ import ( "github.com/notaryproject/notation-go/spec/signature" ) -var supportedAlgs = map[string]bool{ - jwt.SigningMethodPS256.Name: true, - jwt.SigningMethodPS384.Name: true, - jwt.SigningMethodPS512.Name: true, - jwt.SigningMethodES256.Name: true, - jwt.SigningMethodES384.Name: true, - jwt.SigningMethodES512.Name: true, -} - -var keySpecToAlg = map[signature.KeyType]string{ - signature.RSA_2048: jwt.SigningMethodPS256.Alg(), - signature.RSA_3072: jwt.SigningMethodPS384.Alg(), - signature.RSA_4096: jwt.SigningMethodPS512.Alg(), - signature.EC_256: jwt.SigningMethodES256.Alg(), - signature.EC_384: jwt.SigningMethodES384.Alg(), - signature.EC_512: jwt.SigningMethodES512.Alg(), -} - // PluginSigner signs artifacts and generates JWS signatures // by delegating the one or both operations to the named plugin, // as defined in @@ -93,7 +75,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Get algorithm associated to key. - alg := keySpecToAlg[key.KeySpec] + alg := key.KeySpec.SignatureAlgorithm() if alg == "" { return nil, fmt.Errorf("keySpec %q not supported: ", key.KeySpec) } @@ -105,7 +87,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Generate signing string. - token := jwtToken(alg, payload) + token := jwtToken(alg.JWS(), payload) signing, err := token.SigningString() if err != nil { return nil, fmt.Errorf("failed to marshal signing payload: %v", err) @@ -116,7 +98,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des ContractVersion: "1", KeyID: s.KeyID, KeySpec: key.KeySpec, - Hash: key.KeySpec.Hash(), + Hash: alg.Hash(), Payload: signing, PluginConfig: config, } @@ -135,7 +117,8 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des } // Check algorithm is supported. - if !supportedAlgs[resp.SigningAlgorithm] { + jwsAlg := resp.SigningAlgorithm.JWS() + if jwsAlg == "" { return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) } @@ -152,7 +135,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des // Verify the hash of the request payload against the response signature // using the public key of the signing certificate. signed64Url := base64.RawURLEncoding.EncodeToString(resp.Signature) - err = verifyJWT(resp.SigningAlgorithm, signing, signed64Url, certs[0]) + err = verifyJWT(jwsAlg, signing, signed64Url, certs[0]) if err != nil { return nil, fmt.Errorf("verification error: %v", err) } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 141c954a..eb7564ec 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -41,7 +41,7 @@ type mockSignerPlugin struct { KeyID string KeySpec signature.KeyType Sign func(payload string) []byte - SigningAlg string + SigningAlg signature.SignatureAlgorithm Cert []byte n int } @@ -159,7 +159,7 @@ func TestPluginSigner_Sign_NoCertChain(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodES256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, }, KeyID: "1", } @@ -171,7 +171,7 @@ func TestPluginSigner_Sign_MalformedCert(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodES256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, Cert: []byte("mocked"), }, KeyID: "1", @@ -188,7 +188,7 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodES256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, Sign: func(payload string) []byte { return []byte("r a w") }, Cert: cert.Raw, }, @@ -232,7 +232,7 @@ func TestPluginSigner_Sign_CertWithoutDigitalSignatureBit(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodPS256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: certBytes, }, @@ -261,7 +261,7 @@ func TestPluginSigner_Sign_CertWithout_idkpcodeSigning(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodPS256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: certBytes, }, @@ -291,7 +291,7 @@ func TestPluginSigner_Sign_CertBasicConstraintCA(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodPS256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: certBytes, }, @@ -309,7 +309,7 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { Runner: &mockSignerPlugin{ KeyID: "1", KeySpec: signature.RSA_2048, - SigningAlg: jwt.SigningMethodPS256.Alg(), + SigningAlg: signature.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: cert.Raw, }, diff --git a/spec/plugin/plugin.go b/spec/plugin/plugin.go index 73cc0f16..aea49bcc 100644 --- a/spec/plugin/plugin.go +++ b/spec/plugin/plugin.go @@ -77,12 +77,9 @@ type GenerateSignatureRequest struct { // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { - KeyID string `json:"keyId"` - Signature []byte `json:"signature"` - - // One of following supported signing algorithms: - // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - SigningAlgorithm string `json:"signingAlgorithm"` + KeyID string `json:"keyId"` + Signature []byte `json:"signature"` + SigningAlgorithm signature.SignatureAlgorithm `json:"signingAlgorithm"` // Ordered list of certificates starting with leaf certificate // and ending with root certificate. diff --git a/spec/signature/jws.go b/spec/signature/jws.go index e3e46202..8100eb72 100644 --- a/spec/signature/jws.go +++ b/spec/signature/jws.go @@ -54,3 +54,42 @@ type JWSEnvelope struct { // Base64URL-encoded signature. Signature string `json:"signature"` } + +// JWS returns the JWS algorithm name. +func (s SignatureAlgorithm) JWS() string { + switch s { + case RSASSA_PSS_SHA_256: + return "PS256" + case RSASSA_PSS_SHA_384: + return "PS384" + case RSASSA_PSS_SHA_512: + return "PS512" + case ECDSA_SHA_256: + return "ES256" + case ECDSA_SHA_384: + return "ES384" + case ECDSA_SHA_512: + return "ES512" + } + return "" +} + +// NewSignatureAlgorithmJWS returns the algorithm associated to alg. +// It returns an empty string if alg is not supported. +func NewSignatureAlgorithmJWS(alg string) SignatureAlgorithm { + switch alg { + case "PS256": + return RSASSA_PSS_SHA_256 + case "PS384": + return RSASSA_PSS_SHA_384 + case "PS512": + return RSASSA_PSS_SHA_512 + case "ES256": + return ECDSA_SHA_256 + case "ES384": + return ECDSA_SHA_384 + case "ES512": + return ECDSA_SHA_512 + } + return "" +} diff --git a/spec/signature/types.go b/spec/signature/types.go index 4027aabf..174059c9 100644 --- a/spec/signature/types.go +++ b/spec/signature/types.go @@ -42,14 +42,20 @@ const ( ) // Hash returns the Hash associated k. -func (k KeyType) Hash() Hash { +func (k KeyType) SignatureAlgorithm() SignatureAlgorithm { switch k { - case RSA_2048, EC_256: - return SHA256 - case RSA_3072, EC_384: - return SHA384 - case RSA_4096, EC_512: - return SHA512 + case RSA_2048: + return RSASSA_PSS_SHA_256 + case RSA_3072: + return RSASSA_PSS_SHA_384 + case RSA_4096: + return RSASSA_PSS_SHA_512 + case EC_256: + return ECDSA_SHA_256 + case EC_384: + return ECDSA_SHA_384 + case EC_512: + return ECDSA_SHA_512 } return "" } @@ -75,3 +81,28 @@ func (h Hash) HashFunc() crypto.Hash { } return 0 } + +// SignatureAlgorithm defines the supported signature algorithms. +type SignatureAlgorithm string + +const ( + RSASSA_PSS_SHA_256 SignatureAlgorithm = "RSASSA_PSS_SHA_256" + RSASSA_PSS_SHA_384 SignatureAlgorithm = "RSASSA_PSS_SHA_384" + RSASSA_PSS_SHA_512 SignatureAlgorithm = "RSASSA_PSS_SHA_512" + ECDSA_SHA_256 SignatureAlgorithm = "ECDSA_SHA_256" + ECDSA_SHA_384 SignatureAlgorithm = "ECDSA_SHA_384" + ECDSA_SHA_512 SignatureAlgorithm = "ECDSA_SHA_512" +) + +// Hash returns the Hash associated s. +func (s SignatureAlgorithm) Hash() Hash { + switch s { + case RSASSA_PSS_SHA_256, ECDSA_SHA_256: + return SHA256 + case RSASSA_PSS_SHA_384, ECDSA_SHA_384: + return SHA384 + case RSASSA_PSS_SHA_512, ECDSA_SHA_512: + return SHA512 + } + return "" +} From a24ae78a1bcc8f9a57b801ca6f935f97330b10cf Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 13 May 2022 11:41:29 +0200 Subject: [PATCH 51/58] Update plugin implementation with latest spec Signed-off-by: qmuntal --- signature/jws/plugin.go | 28 +++++++++++++++++++--------- signature/jws/plugin_test.go | 21 ++++++++++++++++----- signature/jws/spec.go | 4 ++-- signature/jws/verifier.go | 10 +++++----- spec/plugin/plugin.go | 16 +++++++++------- spec/signature/jws.go | 7 +------ 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index ebb71c23..b73429d5 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -25,6 +25,19 @@ type PluginSigner struct { // Sign signs the artifact described by its descriptor, and returns the signature. func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { + metadata, err := s.getMetadata(ctx) + if err != nil { + return nil, err + } + if metadata.HasCapability(plugin.CapabilitySignatureGenerator) { + return s.generateSignature(ctx, desc, opts) + } else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) { + return s.generateSignatureEnvelope(ctx, desc, opts) + } + return nil, fmt.Errorf("plugin does not have signing capabilities") +} + +func (s *PluginSigner) getMetadata(ctx context.Context) (*plugin.Metadata, error) { out, err := s.Runner.Run(ctx, plugin.CommandGetMetadata, nil) if err != nil { return nil, fmt.Errorf("metadata command failed: %w", err) @@ -36,17 +49,12 @@ func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts if err := metadata.Validate(); err != nil { return nil, fmt.Errorf("invalid plugin metadata: %w", err) } - if metadata.HasCapability(plugin.CapabilitySignatureGenerator) { - return s.generateSignature(ctx, desc, opts) - } else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) { - return s.generateSignatureEnvelope(ctx, desc, opts) - } - return nil, fmt.Errorf("plugin does not have signing capabilities") + return metadata, nil } func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string) (*plugin.DescribeKeyResponse, error) { req := &plugin.DescribeKeyRequest{ - ContractVersion: "1", + ContractVersion: plugin.ContractVersion, KeyID: s.KeyID, PluginConfig: config, } @@ -95,11 +103,11 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des // Execute plugin sign command. req := &plugin.GenerateSignatureRequest{ - ContractVersion: "1", + ContractVersion: plugin.ContractVersion, KeyID: s.KeyID, KeySpec: key.KeySpec, Hash: alg.Hash(), - Payload: signing, + Payload: []byte(signing), PluginConfig: config, } out, err := s.Runner.Run(ctx, plugin.CommandGenerateSignature, req) @@ -134,6 +142,8 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des // Verify the hash of the request payload against the response signature // using the public key of the signing certificate. + // At this point, resp.Signature is not base64-encoded, + // but verifyJWT expects a base64URL encoded string. signed64Url := base64.RawURLEncoding.EncodeToString(resp.Signature) err = verifyJWT(jwsAlg, signing, signed64Url, certs[0]) if err != nil { diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index eb7564ec..dccbad2c 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -40,7 +40,7 @@ func (r *mockRunner) Run(ctx context.Context, cmd plugin.Command, req interface{ type mockSignerPlugin struct { KeyID string KeySpec signature.KeyType - Sign func(payload string) []byte + Sign func(payload []byte) []byte SigningAlg signature.SignatureAlgorithm Cert []byte n int @@ -51,6 +51,17 @@ func (s *mockSignerPlugin) Run(ctx context.Context, cmd plugin.Command, req inte if len(s.Cert) != 0 { chain = append(chain, s.Cert) } + if req != nil { + // Test json roundtrip. + jsonReq, err := json.Marshal(req) + if err != nil { + return nil, err + } + err = json.Unmarshal(jsonReq, req) + if err != nil { + return nil, err + } + } defer func() { s.n++ }() switch s.n { case 0: @@ -189,7 +200,7 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { KeyID: "1", KeySpec: signature.RSA_2048, SigningAlg: signature.RSASSA_PSS_SHA_256, - Sign: func(payload string) []byte { return []byte("r a w") }, + Sign: func(payload []byte) []byte { return []byte("r a w") }, Cert: cert.Raw, }, KeyID: "1", @@ -197,10 +208,10 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { testPluginSignerError(t, signer, "verification error") } -func validSign(t *testing.T, key interface{}) func(string) []byte { +func validSign(t *testing.T, key interface{}) func([]byte) []byte { t.Helper() - return func(payload string) []byte { - signed, err := jwt.SigningMethodPS256.Sign(payload, key) + return func(payload []byte) []byte { + signed, err := jwt.SigningMethodPS256.Sign(string(payload), key) if err != nil { t.Fatal(err) } diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 0d66f338..593cd671 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -18,7 +18,7 @@ import ( type notaryClaim struct { jwt.RegisteredClaims - Notary signature.JWSNotaryClaim `json:"notary"` + Subject signature.Descriptor `json:"subject"` } // packPayload generates JWS payload according the signing content and options. @@ -32,7 +32,7 @@ func packPayload(desc signature.Descriptor, opts notation.SignOptions) jwt.Claim ExpiresAt: expiresAt, IssuedAt: jwt.NewNumericDate(time.Now()), }, - Notary: signature.JWSNotaryClaim{Subject: desc}, + Subject: desc, } } diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index 1cb8e339..823c1ce1 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -76,7 +76,7 @@ func (v *Verifier) Verify(ctx context.Context, sig []byte, opts notation.VerifyO return signature.Descriptor{}, err } - return claim.Subject, nil + return claim, nil } // verifySigner verifies the signing identity and returns the verification key. @@ -157,7 +157,7 @@ func (v *Verifier) verifyTimestamp(tokenBytes []byte, encodedSig string) (time.T // verifyJWT verifies the JWT token against the specified verification key, and // returns notation claim. -func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tokenString string) (*signature.JWSNotaryClaim, error) { +func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tokenString string) (signature.Descriptor, error) { // parse and verify token parser := &jwt.Parser{ ValidMethods: v.ValidMethods, @@ -173,15 +173,15 @@ func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tok t.Method = method return key, nil }); err != nil { - return nil, err + return signature.Descriptor{}, err } // ensure required claims exist. // Note: the registered claims are already verified by parser.ParseWithClaims(). if claims.IssuedAt == nil { - return nil, errors.New("missing iat") + return signature.Descriptor{}, errors.New("missing iat") } - return &claims.Notary, nil + return claims.Subject, nil } // openEnvelope opens the signature envelope and get the embedded signature. diff --git a/spec/plugin/plugin.go b/spec/plugin/plugin.go index aea49bcc..a79758c4 100644 --- a/spec/plugin/plugin.go +++ b/spec/plugin/plugin.go @@ -9,6 +9,9 @@ import ( // Prefix is the prefix required on all plugin binary names. const Prefix = "notation-" +// ContractVersion is the . version of the plugin contract. +const ContractVersion = "1.0" + // Command is a CLI command available in the plugin contract. type Command string @@ -66,13 +69,12 @@ type DescribeKeyResponse struct { // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { - ContractVersion string `json:"contractVersion"` - KeyID string `json:"keyId"` - KeySpec signature.KeyType `json:"keySpec"` - Hash signature.Hash `json:"hashAlgorithm"` - SignatureEnvelopeType string `json:"signatureEnvelopeType"` - Payload string `json:"payload"` - PluginConfig map[string]string `json:"pluginConfig,omitempty"` + ContractVersion string `json:"contractVersion"` + KeyID string `json:"keyId"` + KeySpec signature.KeyType `json:"keySpec"` + Hash signature.Hash `json:"hashAlgorithm"` + Payload []byte `json:"payload"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } // GenerateSignatureResponse is the response of a generate-signature request. diff --git a/spec/signature/jws.go b/spec/signature/jws.go index 8100eb72..3d17227d 100644 --- a/spec/signature/jws.go +++ b/spec/signature/jws.go @@ -5,15 +5,10 @@ const ( MediaTypeJWSEnvelope = "application/vnd.cncf.notary.v2.jws.v1" ) -// JWSNotaryClaim is a Notary private claim. -type JWSNotaryClaim struct { - Subject Descriptor `json:"subject"` -} - // JWSPayload contains the set of claims used by Notary V2. type JWSPayload struct { // Private claim. - Notary JWSNotaryClaim `json:"notary"` + Subject Descriptor `json:"subject"` // Identifies the number of seconds since Epoch at which the signature was issued. IssuedAt int64 `json:"iat"` From 35b4fac2d58eec6c60804f84265d8a7a33ad271d Mon Sep 17 00:00:00 2001 From: qmuntal Date: Sat, 14 May 2022 19:53:42 +0200 Subject: [PATCH 52/58] improve plugin error report Signed-off-by: qmuntal --- plugin/manager/manager.go | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index df1f8187..36f91450 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -178,7 +178,7 @@ func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Com var re plugin.RequestError err = json.Unmarshal(out, &re) if err != nil { - return nil, withErr(plugin.RequestError{Code: plugin.ErrorCodeGeneric, Err: err}, ErrNotCompliant) + return nil, plugin.RequestError{Code: plugin.ErrorCodeGeneric, Err: fmt.Errorf("failed to decode json response: %w", ErrNotCompliant)} } return nil, re } @@ -197,8 +197,7 @@ func run(ctx context.Context, cmder commander, pluginPath string, cmd plugin.Com } err = json.Unmarshal(out, resp) if err != nil { - err = fmt.Errorf("failed to decode json response: %w", err) - return nil, withErr(err, ErrNotCompliant) + return nil, fmt.Errorf("failed to decode json response: %w", ErrNotCompliant) } return resp, nil } @@ -243,28 +242,3 @@ func addExeSuffix(s string) string { } return s } - -func withErr(err, other error) error { - return unionError{err: err, other: other} -} - -type unionError struct { - err error - other error -} - -func (u unionError) Error() string { - return fmt.Sprintf("%s: %s", u.other, u.err) -} - -func (u unionError) Is(target error) bool { - return errors.Is(u.other, target) -} - -func (u unionError) As(target interface{}) bool { - return errors.As(u.other, target) -} - -func (u unionError) Unwrap() error { - return u.err -} From 8177f1c3b73f598641e462cec7606fb04528b4e2 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 18 May 2022 07:32:40 +0200 Subject: [PATCH 53/58] add x5c comment Signed-off-by: qmuntal --- spec/signature/jws.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/signature/jws.go b/spec/signature/jws.go index 3d17227d..8962aa57 100644 --- a/spec/signature/jws.go +++ b/spec/signature/jws.go @@ -31,7 +31,8 @@ type JWSUnprotectedHeader struct { // RFC3161 time stamp token Base64-encoded. TimeStampToken []byte `json:"timestamp,omitempty"` - // List of X.509 certificates, each one Base64-encoded. + // List of X.509 Base64-DER-encoded certificates + // as defined at https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6. CertChain [][]byte `json:"x5c"` } From 979142f6fb077d472877ec3be61dee8bd9d15db2 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 18 May 2022 09:42:03 +0200 Subject: [PATCH 54/58] simplify runner interface Signed-off-by: qmuntal --- plugin/manager/integration_test.go | 2 +- plugin/manager/manager.go | 4 ++-- plugin/manager/manager_test.go | 8 +++++++- signature/jws/plugin.go | 6 +++--- signature/jws/plugin_test.go | 4 ++-- spec/plugin/metadata.go | 4 ++++ spec/plugin/plugin.go | 26 +++++++++++++++++++++++++- 7 files changed, 44 insertions(+), 10 deletions(-) diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index 038f728e..cb0e0442 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -77,7 +77,7 @@ func TestIntegration(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = r.Run(context.Background(), plugin.CommandGetMetadata, nil) + _, err = r.Run(context.Background(), plugin.GetMetadataRequest{}) if err != nil { t.Fatal(err) } diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 36f91450..56cb99ef 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -152,7 +152,7 @@ type pluginRunner struct { cmder commander } -func (p pluginRunner) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { +func (p pluginRunner) Run(ctx context.Context, req plugin.Request) (interface{}, error) { var data []byte if req != nil { var err error @@ -161,7 +161,7 @@ func (p pluginRunner) Run(ctx context.Context, cmd plugin.Command, req interface return nil, pluginErr(p.name, fmt.Errorf("failed to marshal request object: %w", err)) } } - resp, err := run(ctx, p.cmder, p.path, cmd, data) + resp, err := run(ctx, p.cmder, p.path, req.Command(), data) if err != nil { return nil, pluginErr(p.name, err) } diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 1a668096..7e27323b 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -265,7 +265,7 @@ func TestManager_Runner_Run(t *testing.T) { if err != nil { t.Fatalf("Manager.Runner() error = %v, want nil", err) } - got, err := runner.Run(context.Background(), tt.args.cmd, "1") + got, err := runner.Run(context.Background(), requester(tt.args.cmd)) wantErr := tt.err != nil if (err != nil) != wantErr { t.Fatalf("Runner.Run() error = %v, wantErr %v", err, wantErr) @@ -281,6 +281,12 @@ func TestManager_Runner_Run(t *testing.T) { } } +type requester plugin.Command + +func (r requester) Command() plugin.Command { + return plugin.Command(r) +} + func TestNew(t *testing.T) { mgr := New("") if mgr == nil { diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index b73429d5..1eb2dc2e 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -38,7 +38,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts } func (s *PluginSigner) getMetadata(ctx context.Context) (*plugin.Metadata, error) { - out, err := s.Runner.Run(ctx, plugin.CommandGetMetadata, nil) + out, err := s.Runner.Run(ctx, new(plugin.GetMetadataRequest)) if err != nil { return nil, fmt.Errorf("metadata command failed: %w", err) } @@ -58,7 +58,7 @@ func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string KeyID: s.KeyID, PluginConfig: config, } - out, err := s.Runner.Run(ctx, plugin.CommandDescribeKey, req) + out, err := s.Runner.Run(ctx, req) if err != nil { return nil, fmt.Errorf("describe-key command failed: %w", err) } @@ -110,7 +110,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Des Payload: []byte(signing), PluginConfig: config, } - out, err := s.Runner.Run(ctx, plugin.CommandGenerateSignature, req) + out, err := s.Runner.Run(ctx, req) if err != nil { return nil, fmt.Errorf("generate-signature command failed: %w", err) } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index dccbad2c..3c4995f6 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -32,7 +32,7 @@ type mockRunner struct { n int } -func (r *mockRunner) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { +func (r *mockRunner) Run(ctx context.Context, req plugin.Request) (interface{}, error) { defer func() { r.n++ }() return r.resp[r.n], r.err[r.n] } @@ -46,7 +46,7 @@ type mockSignerPlugin struct { n int } -func (s *mockSignerPlugin) Run(ctx context.Context, cmd plugin.Command, req interface{}) (interface{}, error) { +func (s *mockSignerPlugin) Run(ctx context.Context, req plugin.Request) (interface{}, error) { var chain [][]byte if len(s.Cert) != 0 { chain = append(chain, s.Cert) diff --git a/spec/plugin/metadata.go b/spec/plugin/metadata.go index 08bc66fb..15b5603a 100644 --- a/spec/plugin/metadata.go +++ b/spec/plugin/metadata.go @@ -35,6 +35,10 @@ func (m *Metadata) Validate() error { return nil } +func (Metadata) Command() Command { + return CommandGetMetadata +} + // HasCapability return true if the metadata states that the // capability is supported. // Returns true if capability is empty. diff --git a/spec/plugin/plugin.go b/spec/plugin/plugin.go index a79758c4..44ae9f28 100644 --- a/spec/plugin/plugin.go +++ b/spec/plugin/plugin.go @@ -50,6 +50,13 @@ const ( CapabilityEnvelopeGenerator Capability = "SIGNATURE_ENVELOPE_GENERATOR" ) +// GetMetadataRequest contains the parameters passed in a get-plugin-metadata request. +type GetMetadataRequest struct{} + +func (GetMetadataRequest) Command() Command { + return CommandGetMetadata +} + // DescribeKeyRequest contains the parameters passed in a describe-key request. type DescribeKeyRequest struct { ContractVersion string `json:"contractVersion"` @@ -57,6 +64,10 @@ type DescribeKeyRequest struct { PluginConfig map[string]string `json:"pluginConfig,omitempty"` } +func (DescribeKeyRequest) Command() Command { + return CommandDescribeKey +} + // GenerateSignatureResponse is the response of a describe-key request. type DescribeKeyResponse struct { // The same key id as passed in the request. @@ -77,6 +88,10 @@ type GenerateSignatureRequest struct { PluginConfig map[string]string `json:"pluginConfig,omitempty"` } +func (GenerateSignatureRequest) Command() Command { + return CommandGenerateSignature +} + // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { KeyID string `json:"keyId"` @@ -98,6 +113,10 @@ type GenerateEnvelopeRequest struct { PluginConfig map[string]string `json:"pluginConfig,omitempty"` } +func (GenerateEnvelopeRequest) Command() Command { + return CommandGenerateSignature +} + // GenerateSignatureResponse is the response of a generate-envelop request. type GenerateEnvelopeResponse struct { SignatureEnvelope []byte `json:"signatureEnvelope"` @@ -105,6 +124,11 @@ type GenerateEnvelopeResponse struct { Annotations map[string]string `json:"annotations,omitempty"` } +// Request defines a plugin request, which is always associated to a command. +type Request interface { + Command() Command +} + // Runner is an interface for running commands against a plugin. type Runner interface { // Run executes the specified command and waits for it to complete. @@ -118,5 +142,5 @@ type Runner interface { // // If the command starts but does not complete successfully, the error is of type RequestError wrapping a *exec.ExitError. // Other error types may be returned for other situations. - Run(ctx context.Context, cmd Command, req interface{}) (interface{}, error) + Run(ctx context.Context, req Request) (interface{}, error) } From 6fe2523c56ef6eab51fece9829a20d139a2a110c Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 18 May 2022 09:50:39 +0200 Subject: [PATCH 55/58] remove spec package Signed-off-by: qmuntal --- spec/signature/jws.go => jws.go | 2 +- notation.go | 107 +++++++++++++++++++++- {spec/plugin => plugin}/errors.go | 0 {spec/plugin => plugin}/errors_test.go | 0 plugin/manager/integration_test.go | 2 +- plugin/manager/manager.go | 2 +- plugin/manager/manager_test.go | 2 +- {spec/plugin => plugin}/metadata.go | 0 {spec/plugin => plugin}/metadata_test.go | 0 {spec/plugin => plugin}/plugin.go | 14 +-- signature/jws/plugin.go | 9 +- signature/jws/plugin_test.go | 55 ++++++------ signature/jws/signer.go | 9 +- signature/jws/signer_test.go | 7 +- signature/jws/spec.go | 5 +- signature/jws/verifier.go | 21 +++-- spec/signature/types.go | 108 ----------------------- 17 files changed, 165 insertions(+), 178 deletions(-) rename spec/signature/jws.go => jws.go (99%) rename {spec/plugin => plugin}/errors.go (100%) rename {spec/plugin => plugin}/errors_test.go (100%) rename {spec/plugin => plugin}/metadata.go (100%) rename {spec/plugin => plugin}/metadata_test.go (100%) rename {spec/plugin => plugin}/plugin.go (92%) delete mode 100644 spec/signature/types.go diff --git a/spec/signature/jws.go b/jws.go similarity index 99% rename from spec/signature/jws.go rename to jws.go index 8962aa57..1f11d4f8 100644 --- a/spec/signature/jws.go +++ b/jws.go @@ -1,4 +1,4 @@ -package signature +package notation const ( // MediaTypeJWSEnvelope describes the media type of the JWS envelope. diff --git a/notation.go b/notation.go index 1ac6fcdb..3966f685 100644 --- a/notation.go +++ b/notation.go @@ -2,13 +2,34 @@ package notation import ( "context" + "crypto" "crypto/x509" "time" "github.com/notaryproject/notation-go/crypto/timestamp" - "github.com/notaryproject/notation-go/spec/signature" + "github.com/opencontainers/go-digest" ) +// Descriptor describes the content signed or to be signed. +type Descriptor struct { + // The media type of the targeted content. + MediaType string `json:"mediaType"` + + // The digest of the targeted content. + Digest digest.Digest `json:"digest"` + + // Specifies the size in bytes of the blob. + Size int64 `json:"size"` + + // Contains optional user defined attributes. + Annotations map[string]string `json:"annotations,omitempty"` +} + +// Equal reports whether d and t points to the same content. +func (d Descriptor) Equal(t Descriptor) bool { + return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size +} + // SignOptions contains parameters for Signer.Sign. type SignOptions struct { // Expiry identifies the expiration time of the resulted signature. @@ -33,7 +54,7 @@ type SignOptions struct { type Signer interface { // Sign signs the artifact described by its descriptor, // and returns the signature. - Sign(ctx context.Context, desc signature.Descriptor, opts SignOptions) ([]byte, error) + Sign(ctx context.Context, desc Descriptor, opts SignOptions) ([]byte, error) } // VerifyOptions contains parameters for Verifier.Verify. @@ -48,7 +69,7 @@ func (opts VerifyOptions) Validate() error { type Verifier interface { // Verify verifies the signature and returns the verified descriptor and // metadata of the signed artifact. - Verify(ctx context.Context, signature []byte, opts VerifyOptions) (signature.Descriptor, error) + Verify(ctx context.Context, signature []byte, opts VerifyOptions) (Descriptor, error) } // Service combines the signing and verification services. @@ -56,3 +77,83 @@ type Service interface { Signer Verifier } + +// KeyType defines a key type and size. +type KeyType string + +// One of following supported specs +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + RSA_2048 KeyType = "RSA_2048" + RSA_3072 KeyType = "RSA_3072" + RSA_4096 KeyType = "RSA_4096" + EC_256 KeyType = "EC_256" + EC_384 KeyType = "EC_384" + EC_512 KeyType = "EC_512" +) + +// Hash returns the Hash associated k. +func (k KeyType) SignatureAlgorithm() SignatureAlgorithm { + switch k { + case RSA_2048: + return RSASSA_PSS_SHA_256 + case RSA_3072: + return RSASSA_PSS_SHA_384 + case RSA_4096: + return RSASSA_PSS_SHA_512 + case EC_256: + return ECDSA_SHA_256 + case EC_384: + return ECDSA_SHA_384 + case EC_512: + return ECDSA_SHA_512 + } + return "" +} + +// Hash algorithm associated with the key spec. +type Hash string + +const ( + SHA256 Hash = "SHA_256" + SHA384 Hash = "SHA_384" + SHA512 Hash = "SHA_512" +) + +// HashFunc returns the Hash associated k. +func (h Hash) HashFunc() crypto.Hash { + switch h { + case SHA256: + return crypto.SHA256 + case SHA384: + return crypto.SHA384 + case SHA512: + return crypto.SHA512 + } + return 0 +} + +// SignatureAlgorithm defines the supported signature algorithms. +type SignatureAlgorithm string + +const ( + RSASSA_PSS_SHA_256 SignatureAlgorithm = "RSASSA_PSS_SHA_256" + RSASSA_PSS_SHA_384 SignatureAlgorithm = "RSASSA_PSS_SHA_384" + RSASSA_PSS_SHA_512 SignatureAlgorithm = "RSASSA_PSS_SHA_512" + ECDSA_SHA_256 SignatureAlgorithm = "ECDSA_SHA_256" + ECDSA_SHA_384 SignatureAlgorithm = "ECDSA_SHA_384" + ECDSA_SHA_512 SignatureAlgorithm = "ECDSA_SHA_512" +) + +// Hash returns the Hash associated s. +func (s SignatureAlgorithm) Hash() Hash { + switch s { + case RSASSA_PSS_SHA_256, ECDSA_SHA_256: + return SHA256 + case RSASSA_PSS_SHA_384, ECDSA_SHA_384: + return SHA384 + case RSASSA_PSS_SHA_512, ECDSA_SHA_512: + return SHA512 + } + return "" +} diff --git a/spec/plugin/errors.go b/plugin/errors.go similarity index 100% rename from spec/plugin/errors.go rename to plugin/errors.go diff --git a/spec/plugin/errors_test.go b/plugin/errors_test.go similarity index 100% rename from spec/plugin/errors_test.go rename to plugin/errors_test.go diff --git a/plugin/manager/integration_test.go b/plugin/manager/integration_test.go index cb0e0442..aeecfbb1 100644 --- a/plugin/manager/integration_test.go +++ b/plugin/manager/integration_test.go @@ -10,8 +10,8 @@ import ( "runtime" "testing" + "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" - "github.com/notaryproject/notation-go/spec/plugin" ) func preparePlugin(t *testing.T) string { diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 56cb99ef..deb40f2d 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -13,7 +13,7 @@ import ( "path/filepath" "runtime" - "github.com/notaryproject/notation-go/spec/plugin" + "github.com/notaryproject/notation-go/plugin" ) // Plugin represents a potential plugin with all it's metadata. diff --git a/plugin/manager/manager_test.go b/plugin/manager/manager_test.go index 7e27323b..235bee28 100644 --- a/plugin/manager/manager_test.go +++ b/plugin/manager/manager_test.go @@ -10,7 +10,7 @@ import ( "testing" "testing/fstest" - "github.com/notaryproject/notation-go/spec/plugin" + "github.com/notaryproject/notation-go/plugin" ) type testCommander struct { diff --git a/spec/plugin/metadata.go b/plugin/metadata.go similarity index 100% rename from spec/plugin/metadata.go rename to plugin/metadata.go diff --git a/spec/plugin/metadata_test.go b/plugin/metadata_test.go similarity index 100% rename from spec/plugin/metadata_test.go rename to plugin/metadata_test.go diff --git a/spec/plugin/plugin.go b/plugin/plugin.go similarity index 92% rename from spec/plugin/plugin.go rename to plugin/plugin.go index 44ae9f28..bbd9e39a 100644 --- a/spec/plugin/plugin.go +++ b/plugin/plugin.go @@ -3,7 +3,7 @@ package plugin import ( "context" - "github.com/notaryproject/notation-go/spec/signature" + "github.com/notaryproject/notation-go" ) // Prefix is the prefix required on all plugin binary names. @@ -75,15 +75,15 @@ type DescribeKeyResponse struct { // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - KeySpec signature.KeyType `json:"keySpec"` + KeySpec notation.KeyType `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { ContractVersion string `json:"contractVersion"` KeyID string `json:"keyId"` - KeySpec signature.KeyType `json:"keySpec"` - Hash signature.Hash `json:"hashAlgorithm"` + KeySpec notation.KeyType `json:"keySpec"` + Hash notation.Hash `json:"hashAlgorithm"` Payload []byte `json:"payload"` PluginConfig map[string]string `json:"pluginConfig,omitempty"` } @@ -94,9 +94,9 @@ func (GenerateSignatureRequest) Command() Command { // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { - KeyID string `json:"keyId"` - Signature []byte `json:"signature"` - SigningAlgorithm signature.SignatureAlgorithm `json:"signingAlgorithm"` + KeyID string `json:"keyId"` + Signature []byte `json:"signature"` + SigningAlgorithm notation.SignatureAlgorithm `json:"signingAlgorithm"` // Ordered list of certificates starting with leaf certificate // and ending with root certificate. diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 1eb2dc2e..4dea6e7c 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -9,8 +9,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/spec/plugin" - "github.com/notaryproject/notation-go/spec/signature" + "github.com/notaryproject/notation-go/plugin" ) // PluginSigner signs artifacts and generates JWS signatures @@ -24,7 +23,7 @@ type PluginSigner struct { } // Sign signs the artifact described by its descriptor, and returns the signature. -func (s *PluginSigner) Sign(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { +func (s *PluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { metadata, err := s.getMetadata(ctx) if err != nil { return nil, err @@ -69,7 +68,7 @@ func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string return resp, nil } -func (s *PluginSigner) generateSignature(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { +func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { config := s.mergeConfig(opts.PluginConfig) // Get key info. key, err := s.describeKey(ctx, config) @@ -172,7 +171,7 @@ func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string { return c } -func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { +func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { return nil, errors.New("not implemented") } diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 3c4995f6..91ff8c50 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -17,8 +17,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/spec/plugin" - "github.com/notaryproject/notation-go/spec/signature" + "github.com/notaryproject/notation-go/plugin" ) var validMetadata = plugin.Metadata{ @@ -39,9 +38,9 @@ func (r *mockRunner) Run(ctx context.Context, req plugin.Request) (interface{}, type mockSignerPlugin struct { KeyID string - KeySpec signature.KeyType + KeySpec notation.KeyType Sign func(payload []byte) []byte - SigningAlg signature.SignatureAlgorithm + SigningAlg notation.SignatureAlgorithm Cert []byte n int } @@ -85,7 +84,7 @@ func (s *mockSignerPlugin) Run(ctx context.Context, req plugin.Request) (interfa func testPluginSignerError(t *testing.T, signer PluginSigner, wantEr string) { t.Helper() - _, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{}) + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) if err == nil || !strings.Contains(err.Error(), wantEr) { t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) } @@ -116,7 +115,7 @@ func TestPluginSigner_Sign_DescribeKeyFailed(t *testing.T) { func TestPluginSigner_Sign_DescribeKeyKeyIDMismatch(t *testing.T) { signer := PluginSigner{ - Runner: &mockSignerPlugin{KeyID: "2", KeySpec: signature.RSA_2048}, + Runner: &mockSignerPlugin{KeyID: "2", KeySpec: notation.RSA_2048}, KeyID: "1", } testPluginSignerError(t, signer, "keyID mismatch") @@ -134,11 +133,11 @@ func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ &validMetadata, - &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signature.RSA_2048}, + &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: notation.RSA_2048}, }, []error{nil, nil}, 0}, KeyID: "1", } - _, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) wantEr := "token is expired" if err == nil || !strings.Contains(err.Error(), wantEr) { t.Errorf("PluginSigner.Sign() error = %v, wantErr %v", err, wantEr) @@ -149,7 +148,7 @@ func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { signer := PluginSigner{ Runner: &mockRunner{[]interface{}{ &validMetadata, - &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signature.RSA_2048}, + &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: notation.RSA_2048}, &plugin.GenerateSignatureResponse{KeyID: "2"}, }, []error{nil, nil, nil}, 0}, KeyID: "1", @@ -159,7 +158,7 @@ func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { func TestPluginSigner_Sign_UnsuportedAlgorithm(t *testing.T) { signer := PluginSigner{ - Runner: &mockSignerPlugin{KeyID: "1", KeySpec: signature.RSA_2048, SigningAlg: "custom"}, + Runner: &mockSignerPlugin{KeyID: "1", KeySpec: notation.RSA_2048, SigningAlg: "custom"}, KeyID: "1", } testPluginSignerError(t, signer, "signing algorithm \"custom\" not supported") @@ -169,8 +168,8 @@ func TestPluginSigner_Sign_NoCertChain(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, }, KeyID: "1", } @@ -181,8 +180,8 @@ func TestPluginSigner_Sign_MalformedCert(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, Cert: []byte("mocked"), }, KeyID: "1", @@ -198,8 +197,8 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, Sign: func(payload []byte) []byte { return []byte("r a w") }, Cert: cert.Raw, }, @@ -242,8 +241,8 @@ func TestPluginSigner_Sign_CertWithoutDigitalSignatureBit(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: certBytes, }, @@ -271,8 +270,8 @@ func TestPluginSigner_Sign_CertWithout_idkpcodeSigning(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: certBytes, }, @@ -301,8 +300,8 @@ func TestPluginSigner_Sign_CertBasicConstraintCA(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: certBytes, }, @@ -319,25 +318,25 @@ func TestPluginSigner_Sign_Valid(t *testing.T) { signer := PluginSigner{ Runner: &mockSignerPlugin{ KeyID: "1", - KeySpec: signature.RSA_2048, - SigningAlg: signature.RSASSA_PSS_SHA_256, + KeySpec: notation.RSA_2048, + SigningAlg: notation.RSASSA_PSS_SHA_256, Sign: validSign(t, key), Cert: cert.Raw, }, KeyID: "1", } - data, err := signer.Sign(context.Background(), signature.Descriptor{}, notation.SignOptions{}) + data, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) if err != nil { t.Errorf("PluginSigner.Sign() error = %v, wantErr nil", err) } - var got signature.JWSEnvelope + var got notation.JWSEnvelope err = json.Unmarshal(data, &got) if err != nil { t.Fatal(err) } - want := signature.JWSEnvelope{ + want := notation.JWSEnvelope{ Protected: "eyJhbGciOiJQUzI1NiIsImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS52Mi5qd3MudjEifQ", - Header: signature.JWSUnprotectedHeader{ + Header: notation.JWSUnprotectedHeader{ CertChain: [][]byte{cert.Raw}, }, } diff --git a/signature/jws/signer.go b/signature/jws/signer.go index 08da8e49..eb80eeae 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -14,7 +14,6 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp" "github.com/notaryproject/notation-go/internal/crypto/pki" - "github.com/notaryproject/notation-go/spec/signature" ) // Signer signs artifacts and generates JWS signatures. @@ -81,7 +80,7 @@ func NewSignerWithCertificateChain(method jwt.SigningMethod, key crypto.PrivateK } // Sign signs the artifact described by its descriptor, and returns the signature. -func (s *Signer) Sign(ctx context.Context, desc signature.Descriptor, opts notation.SignOptions) ([]byte, error) { +func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { // generate JWT payload := packPayload(desc, opts) if err := payload.Valid(); err != nil { @@ -100,7 +99,7 @@ func jwtToken(alg string, claims jwt.Claims) *jwt.Token { return &jwt.Token{ Header: map[string]interface{}{ "alg": alg, - "cty": signature.MediaTypeJWSEnvelope, + "cty": notation.MediaTypeJWSEnvelope, }, Claims: claims, } @@ -111,11 +110,11 @@ func jwtEnvelope(ctx context.Context, opts notation.SignOptions, compact string, if len(parts) != 3 { return nil, errors.New("invalid compact serialization") } - envelope := signature.JWSEnvelope{ + envelope := notation.JWSEnvelope{ Protected: parts[0], Payload: parts[1], Signature: parts[2], - Header: signature.JWSUnprotectedHeader{ + Header: notation.JWSUnprotectedHeader{ CertChain: certChain, }, } diff --git a/signature/jws/signer_test.go b/signature/jws/signer_test.go index b84a826b..38896479 100644 --- a/signature/jws/signer_test.go +++ b/signature/jws/signer_test.go @@ -13,7 +13,6 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp/timestamptest" - "github.com/notaryproject/notation-go/spec/signature" "github.com/opencontainers/go-digest" ) @@ -116,11 +115,11 @@ func TestSignWithoutExpiry(t *testing.T) { } // generateSigningContent generates common signing content with options for testing. -func generateSigningContent(tsa *timestamptest.TSA) (signature.Descriptor, notation.SignOptions) { +func generateSigningContent(tsa *timestamptest.TSA) (notation.Descriptor, notation.SignOptions) { content := "hello world" - desc := signature.Descriptor{ + desc := notation.Descriptor{ MediaType: "test media type", - Digest: string(digest.Canonical.FromString(content)), + Digest: digest.Canonical.FromString(content), Size: int64(len(content)), Annotations: map[string]string{ "identity": "test.registry.io/test:example", diff --git a/signature/jws/spec.go b/signature/jws/spec.go index 593cd671..a881640a 100644 --- a/signature/jws/spec.go +++ b/signature/jws/spec.go @@ -13,16 +13,15 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/spec/signature" ) type notaryClaim struct { jwt.RegisteredClaims - Subject signature.Descriptor `json:"subject"` + Subject notation.Descriptor `json:"subject"` } // packPayload generates JWS payload according the signing content and options. -func packPayload(desc signature.Descriptor, opts notation.SignOptions) jwt.Claims { +func packPayload(desc notation.Descriptor, opts notation.SignOptions) jwt.Claims { var expiresAt *jwt.NumericDate if !opts.Expiry.IsZero() { expiresAt = jwt.NewNumericDate(opts.Expiry) diff --git a/signature/jws/verifier.go b/signature/jws/verifier.go index 823c1ce1..6422f9b2 100644 --- a/signature/jws/verifier.go +++ b/signature/jws/verifier.go @@ -14,7 +14,6 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp" - "github.com/notaryproject/notation-go/spec/signature" ) // maxTimestampAccuracy specifies the max acceptable accuracy for timestamp. @@ -56,31 +55,31 @@ func NewVerifier() *Verifier { // Verify verifies the signature and returns the verified descriptor and // metadata of the signed artifact. -func (v *Verifier) Verify(ctx context.Context, sig []byte, opts notation.VerifyOptions) (signature.Descriptor, error) { +func (v *Verifier) Verify(ctx context.Context, sig []byte, opts notation.VerifyOptions) (notation.Descriptor, error) { // unpack envelope envelope, err := openEnvelope(sig) if err != nil { - return signature.Descriptor{}, err + return notation.Descriptor{}, err } // verify signing identity method, key, err := v.verifySigner(envelope) if err != nil { - return signature.Descriptor{}, err + return notation.Descriptor{}, err } // verify JWT compact := strings.Join([]string{envelope.Protected, envelope.Payload, envelope.Signature}, ".") claim, err := v.verifyJWT(method, key, compact) if err != nil { - return signature.Descriptor{}, err + return notation.Descriptor{}, err } return claim, nil } // verifySigner verifies the signing identity and returns the verification key. -func (v *Verifier) verifySigner(sig *signature.JWSEnvelope) (jwt.SigningMethod, crypto.PublicKey, error) { +func (v *Verifier) verifySigner(sig *notation.JWSEnvelope) (jwt.SigningMethod, crypto.PublicKey, error) { if len(sig.Header.CertChain) == 0 { return nil, nil, errors.New("signer certificates not found") } @@ -157,7 +156,7 @@ func (v *Verifier) verifyTimestamp(tokenBytes []byte, encodedSig string) (time.T // verifyJWT verifies the JWT token against the specified verification key, and // returns notation claim. -func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tokenString string) (signature.Descriptor, error) { +func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tokenString string) (notation.Descriptor, error) { // parse and verify token parser := &jwt.Parser{ ValidMethods: v.ValidMethods, @@ -173,20 +172,20 @@ func (v *Verifier) verifyJWT(method jwt.SigningMethod, key crypto.PublicKey, tok t.Method = method return key, nil }); err != nil { - return signature.Descriptor{}, err + return notation.Descriptor{}, err } // ensure required claims exist. // Note: the registered claims are already verified by parser.ParseWithClaims(). if claims.IssuedAt == nil { - return signature.Descriptor{}, errors.New("missing iat") + return notation.Descriptor{}, errors.New("missing iat") } return claims.Subject, nil } // openEnvelope opens the signature envelope and get the embedded signature. -func openEnvelope(sig []byte) (*signature.JWSEnvelope, error) { - var envelope signature.JWSEnvelope +func openEnvelope(sig []byte) (*notation.JWSEnvelope, error) { + var envelope notation.JWSEnvelope if err := json.Unmarshal(sig, &envelope); err != nil { return nil, err } diff --git a/spec/signature/types.go b/spec/signature/types.go deleted file mode 100644 index 174059c9..00000000 --- a/spec/signature/types.go +++ /dev/null @@ -1,108 +0,0 @@ -package signature - -import "crypto" - -const ( - // MediaTypeDescriptor describes the media type of the descriptor. - MediaTypeDescriptor = "application/vnd.oci.descriptor.v1+json" -) - -// Descriptor describes the content signed or to be signed. -type Descriptor struct { - // The media type of the targeted content. - MediaType string `json:"mediaType"` - - // The digest of the targeted content. - Digest string `json:"digest"` - - // Specifies the size in bytes of the blob. - Size int64 `json:"size"` - - // Contains optional user defined attributes. - Annotations map[string]string `json:"annotations,omitempty"` -} - -// Equal reports whether d and t points to the same content. -func (d Descriptor) Equal(t Descriptor) bool { - return d.MediaType == t.MediaType && d.Digest == t.Digest && d.Size == t.Size -} - -// KeyType defines a key type and size. -type KeyType string - -// One of following supported specs -// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection -const ( - RSA_2048 KeyType = "RSA_2048" - RSA_3072 KeyType = "RSA_3072" - RSA_4096 KeyType = "RSA_4096" - EC_256 KeyType = "EC_256" - EC_384 KeyType = "EC_384" - EC_512 KeyType = "EC_512" -) - -// Hash returns the Hash associated k. -func (k KeyType) SignatureAlgorithm() SignatureAlgorithm { - switch k { - case RSA_2048: - return RSASSA_PSS_SHA_256 - case RSA_3072: - return RSASSA_PSS_SHA_384 - case RSA_4096: - return RSASSA_PSS_SHA_512 - case EC_256: - return ECDSA_SHA_256 - case EC_384: - return ECDSA_SHA_384 - case EC_512: - return ECDSA_SHA_512 - } - return "" -} - -// Hash algorithm associated with the key spec. -type Hash string - -const ( - SHA256 Hash = "SHA_256" - SHA384 Hash = "SHA_384" - SHA512 Hash = "SHA_512" -) - -// HashFunc returns the Hash associated k. -func (h Hash) HashFunc() crypto.Hash { - switch h { - case SHA256: - return crypto.SHA256 - case SHA384: - return crypto.SHA384 - case SHA512: - return crypto.SHA512 - } - return 0 -} - -// SignatureAlgorithm defines the supported signature algorithms. -type SignatureAlgorithm string - -const ( - RSASSA_PSS_SHA_256 SignatureAlgorithm = "RSASSA_PSS_SHA_256" - RSASSA_PSS_SHA_384 SignatureAlgorithm = "RSASSA_PSS_SHA_384" - RSASSA_PSS_SHA_512 SignatureAlgorithm = "RSASSA_PSS_SHA_512" - ECDSA_SHA_256 SignatureAlgorithm = "ECDSA_SHA_256" - ECDSA_SHA_384 SignatureAlgorithm = "ECDSA_SHA_384" - ECDSA_SHA_512 SignatureAlgorithm = "ECDSA_SHA_512" -) - -// Hash returns the Hash associated s. -func (s SignatureAlgorithm) Hash() Hash { - switch s { - case RSASSA_PSS_SHA_256, ECDSA_SHA_256: - return SHA256 - case RSASSA_PSS_SHA_384, ECDSA_SHA_384: - return SHA384 - case RSASSA_PSS_SHA_512, ECDSA_SHA_512: - return SHA512 - } - return "" -} From ddf72d9bda4bb521db469cb1947dc6de428b2500 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 18 May 2022 09:59:33 +0200 Subject: [PATCH 56/58] pr feedback Signed-off-by: qmuntal --- notation.go | 34 +++++++++++++++++----------------- plugin/plugin.go | 18 +++++++++--------- signature/jws/plugin.go | 2 +- signature/jws/plugin_test.go | 2 +- signature/jws/signer.go | 4 ++-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/notation.go b/notation.go index 3966f685..825000dd 100644 --- a/notation.go +++ b/notation.go @@ -78,22 +78,22 @@ type Service interface { Verifier } -// KeyType defines a key type and size. -type KeyType string +// KeySpec defines a key type and size. +type KeySpec string // One of following supported specs // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection const ( - RSA_2048 KeyType = "RSA_2048" - RSA_3072 KeyType = "RSA_3072" - RSA_4096 KeyType = "RSA_4096" - EC_256 KeyType = "EC_256" - EC_384 KeyType = "EC_384" - EC_512 KeyType = "EC_512" + RSA_2048 KeySpec = "RSA_2048" + RSA_3072 KeySpec = "RSA_3072" + RSA_4096 KeySpec = "RSA_4096" + EC_256 KeySpec = "EC_256" + EC_384 KeySpec = "EC_384" + EC_512 KeySpec = "EC_512" ) -// Hash returns the Hash associated k. -func (k KeyType) SignatureAlgorithm() SignatureAlgorithm { +// SignatureAlgorithm returns the signing algorithm associated with KeyType k. +func (k KeySpec) SignatureAlgorithm() SignatureAlgorithm { switch k { case RSA_2048: return RSASSA_PSS_SHA_256 @@ -111,17 +111,17 @@ func (k KeyType) SignatureAlgorithm() SignatureAlgorithm { return "" } -// Hash algorithm associated with the key spec. -type Hash string +// HashAlgorithm algorithm associated with the key spec. +type HashAlgorithm string const ( - SHA256 Hash = "SHA_256" - SHA384 Hash = "SHA_384" - SHA512 Hash = "SHA_512" + SHA256 HashAlgorithm = "SHA_256" + SHA384 HashAlgorithm = "SHA_384" + SHA512 HashAlgorithm = "SHA_512" ) // HashFunc returns the Hash associated k. -func (h Hash) HashFunc() crypto.Hash { +func (h HashAlgorithm) HashFunc() crypto.Hash { switch h { case SHA256: return crypto.SHA256 @@ -146,7 +146,7 @@ const ( ) // Hash returns the Hash associated s. -func (s SignatureAlgorithm) Hash() Hash { +func (s SignatureAlgorithm) Hash() HashAlgorithm { switch s { case RSASSA_PSS_SHA_256, ECDSA_SHA_256: return SHA256 diff --git a/plugin/plugin.go b/plugin/plugin.go index bbd9e39a..05a06106 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -75,17 +75,17 @@ type DescribeKeyResponse struct { // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - KeySpec notation.KeyType `json:"keySpec"` + KeySpec notation.KeySpec `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { - ContractVersion string `json:"contractVersion"` - KeyID string `json:"keyId"` - KeySpec notation.KeyType `json:"keySpec"` - Hash notation.Hash `json:"hashAlgorithm"` - Payload []byte `json:"payload"` - PluginConfig map[string]string `json:"pluginConfig,omitempty"` + ContractVersion string `json:"contractVersion"` + KeyID string `json:"keyId"` + KeySpec notation.KeySpec `json:"keySpec"` + Hash notation.HashAlgorithm `json:"hashAlgorithm"` + Payload []byte `json:"payload"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } func (GenerateSignatureRequest) Command() Command { @@ -103,7 +103,7 @@ type GenerateSignatureResponse struct { CertificateChain [][]byte `json:"certificateChain"` } -// GenerateEnvelopeRequest contains the parameters passed in a generate-envelop request. +// GenerateEnvelopeRequest contains the parameters passed in a generate-envelope request. type GenerateEnvelopeRequest struct { ContractVersion string `json:"contractVersion"` KeyID string `json:"keyId"` @@ -117,7 +117,7 @@ func (GenerateEnvelopeRequest) Command() Command { return CommandGenerateSignature } -// GenerateSignatureResponse is the response of a generate-envelop request. +// GenerateSignatureResponse is the response of a generate-envelope request. type GenerateEnvelopeResponse struct { SignatureEnvelope []byte `json:"signatureEnvelope"` SignatureEnvelopeType string `json:"signatureEnvelopeType"` diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 4dea6e7c..ee80b536 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -155,7 +155,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc } // Assemble the JWS signature envelope. - return jwtEnvelope(ctx, opts, signing+"."+signed64Url, resp.CertificateChain) + return jwsEnvelope(ctx, opts, signing+"."+signed64Url, resp.CertificateChain) } func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string { diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index 91ff8c50..d8ee45ae 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -38,7 +38,7 @@ func (r *mockRunner) Run(ctx context.Context, req plugin.Request) (interface{}, type mockSignerPlugin struct { KeyID string - KeySpec notation.KeyType + KeySpec notation.KeySpec Sign func(payload []byte) []byte SigningAlg notation.SignatureAlgorithm Cert []byte diff --git a/signature/jws/signer.go b/signature/jws/signer.go index eb80eeae..9281dc12 100644 --- a/signature/jws/signer.go +++ b/signature/jws/signer.go @@ -92,7 +92,7 @@ func (s *Signer) Sign(ctx context.Context, desc notation.Descriptor, opts notati if err != nil { return nil, err } - return jwtEnvelope(ctx, opts, compact, s.certChain) + return jwsEnvelope(ctx, opts, compact, s.certChain) } func jwtToken(alg string, claims jwt.Claims) *jwt.Token { @@ -105,7 +105,7 @@ func jwtToken(alg string, claims jwt.Claims) *jwt.Token { } } -func jwtEnvelope(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { +func jwsEnvelope(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) { parts := strings.Split(compact, ".") if len(parts) != 3 { return nil, errors.New("invalid compact serialization") From cca70c97c26ca48db3b5c1e2a078f60af513f9fe Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 18 May 2022 10:02:34 +0200 Subject: [PATCH 57/58] Apply suggestions from code review Co-authored-by: Milind Gokarn Signed-off-by: qmuntal --- signature/jws/plugin.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index ee80b536..59f67300 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -78,13 +78,13 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc // Check keyID is honored. if s.KeyID != key.KeyID { - return nil, fmt.Errorf("keyID mismatch") + return nil, fmt.Errorf("keyID in describeKey response %q does not match request %q", key.KeyID, s.KeyID) } // Get algorithm associated to key. alg := key.KeySpec.SignatureAlgorithm() if alg == "" { - return nil, fmt.Errorf("keySpec %q not supported: ", key.KeySpec) + return nil, fmt.Errorf("keySpec %q for key %q is not supported", key.KeySpec, key.KeyID) } // Generate payload to be signed. @@ -120,18 +120,18 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc // Check keyID is honored. if s.KeyID != resp.KeyID { - return nil, fmt.Errorf("keyID mismatch") + return nil, fmt.Errorf("keyID in generateSignature response %q does not match request %q",resp.KeyID, s.KeyID") } // Check algorithm is supported. jwsAlg := resp.SigningAlgorithm.JWS() if jwsAlg == "" { - return nil, fmt.Errorf("signing algorithm %q not supported", resp.SigningAlgorithm) + return nil, fmt.Errorf("signing algorithm %q in generateSignature response is not supported", resp.SigningAlgorithm) } // Check certificate chain is not empty. if len(resp.CertificateChain) == 0 { - return nil, errors.New("empty certificate chain") + return nil, errors.New("generateSignature response has empty certificate chain") } certs, err := parseCertChain(resp.CertificateChain) @@ -146,12 +146,12 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc signed64Url := base64.RawURLEncoding.EncodeToString(resp.Signature) err = verifyJWT(jwsAlg, signing, signed64Url, certs[0]) if err != nil { - return nil, fmt.Errorf("verification error: %v", err) + return nil, fmt.Errorf("signature returned by generateSignature cannot be verified: %v", err) } // Check the the certificate chain conforms to the spec. if err := verifyCertExtKeyUsage(certs[0], x509.ExtKeyUsageCodeSigning); err != nil { - return nil, fmt.Errorf("signing certificate does not meet the minimum requirements: %w", err) + return nil, fmt.Errorf("signing certificate in generateSignature response.CertificateChain does not meet the minimum requirements: %w", err) } // Assemble the JWS signature envelope. @@ -188,7 +188,7 @@ func parseCertChain(certChain [][]byte) ([]*x509.Certificate, error) { } func verifyJWT(sigAlg string, payload string, sig string, signingCert *x509.Certificate) error { - // Verify the hash of req.payload against resp.signature using the public key if the leaf certificate. + // Verify the hash of req.payload against resp.signature using the public key in the leaf certificate. method := jwt.GetSigningMethod(sigAlg) return method.Verify(payload, sig, signingCert.PublicKey) } From 07fe9b87498273a7ddfb4672b50bf412ced65ed1 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 18 May 2022 10:08:55 +0200 Subject: [PATCH 58/58] fix build Signed-off-by: qmuntal --- signature/jws/plugin.go | 10 +++++----- signature/jws/plugin_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/signature/jws/plugin.go b/signature/jws/plugin.go index 59f67300..c966ce32 100644 --- a/signature/jws/plugin.go +++ b/signature/jws/plugin.go @@ -95,7 +95,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc // Generate signing string. token := jwtToken(alg.JWS(), payload) - signing, err := token.SigningString() + payloadToSign, err := token.SigningString() if err != nil { return nil, fmt.Errorf("failed to marshal signing payload: %v", err) } @@ -106,7 +106,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc KeyID: s.KeyID, KeySpec: key.KeySpec, Hash: alg.Hash(), - Payload: []byte(signing), + Payload: []byte(payloadToSign), PluginConfig: config, } out, err := s.Runner.Run(ctx, req) @@ -120,7 +120,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc // Check keyID is honored. if s.KeyID != resp.KeyID { - return nil, fmt.Errorf("keyID in generateSignature response %q does not match request %q",resp.KeyID, s.KeyID") + return nil, fmt.Errorf("keyID in generateSignature response %q does not match request %q", resp.KeyID, s.KeyID) } // Check algorithm is supported. @@ -144,7 +144,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc // At this point, resp.Signature is not base64-encoded, // but verifyJWT expects a base64URL encoded string. signed64Url := base64.RawURLEncoding.EncodeToString(resp.Signature) - err = verifyJWT(jwsAlg, signing, signed64Url, certs[0]) + err = verifyJWT(jwsAlg, payloadToSign, signed64Url, certs[0]) if err != nil { return nil, fmt.Errorf("signature returned by generateSignature cannot be verified: %v", err) } @@ -155,7 +155,7 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc notation.Desc } // Assemble the JWS signature envelope. - return jwsEnvelope(ctx, opts, signing+"."+signed64Url, resp.CertificateChain) + return jwsEnvelope(ctx, opts, payloadToSign+"."+signed64Url, resp.CertificateChain) } func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string { diff --git a/signature/jws/plugin_test.go b/signature/jws/plugin_test.go index d8ee45ae..e8e97e70 100644 --- a/signature/jws/plugin_test.go +++ b/signature/jws/plugin_test.go @@ -118,7 +118,7 @@ func TestPluginSigner_Sign_DescribeKeyKeyIDMismatch(t *testing.T) { Runner: &mockSignerPlugin{KeyID: "2", KeySpec: notation.RSA_2048}, KeyID: "1", } - testPluginSignerError(t, signer, "keyID mismatch") + testPluginSignerError(t, signer, "keyID in describeKey response \"2\" does not match request \"1\"") } func TestPluginSigner_Sign_KeySpecNotSupported(t *testing.T) { @@ -126,7 +126,7 @@ func TestPluginSigner_Sign_KeySpecNotSupported(t *testing.T) { Runner: &mockSignerPlugin{KeyID: "1", KeySpec: "custom"}, KeyID: "1", } - testPluginSignerError(t, signer, "keySpec \"custom\" not supported") + testPluginSignerError(t, signer, "keySpec \"custom\" for key \"1\" is not supported") } func TestPluginSigner_Sign_PayloadNotValid(t *testing.T) { @@ -153,7 +153,7 @@ func TestPluginSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { }, []error{nil, nil, nil}, 0}, KeyID: "1", } - testPluginSignerError(t, signer, "keyID mismatch") + testPluginSignerError(t, signer, "keyID in generateSignature response \"2\" does not match request \"1\"") } func TestPluginSigner_Sign_UnsuportedAlgorithm(t *testing.T) { @@ -161,7 +161,7 @@ func TestPluginSigner_Sign_UnsuportedAlgorithm(t *testing.T) { Runner: &mockSignerPlugin{KeyID: "1", KeySpec: notation.RSA_2048, SigningAlg: "custom"}, KeyID: "1", } - testPluginSignerError(t, signer, "signing algorithm \"custom\" not supported") + testPluginSignerError(t, signer, "signing algorithm \"custom\" in generateSignature response is not supported") } func TestPluginSigner_Sign_NoCertChain(t *testing.T) {