From 8991d78dd1b36909b17a2b3de17d0a2523c35c88 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 5 Jun 2024 11:27:03 +0200 Subject: [PATCH 01/27] Fixed some token logic --- pkg/handler/auth/middleware.go | 14 ++++---------- pkg/handler/tokenjar/tokenjar.go | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/pkg/handler/auth/middleware.go b/pkg/handler/auth/middleware.go index b892911..bccb70c 100644 --- a/pkg/handler/auth/middleware.go +++ b/pkg/handler/auth/middleware.go @@ -47,25 +47,19 @@ func (middleware *auth) Wrap(ctx context.Context, next http.HandlerFunc) http.Ha // Get token from the jar - check it is found and valid token := middleware.jar.GetWithValue(tokenValue) - authorized := true if token.IsZero() { - authorized = false httpresponse.Error(w, http.StatusUnauthorized, "invalid or missing token") + return } else if !token.IsValid() { - authorized = false - httpresponse.Error(w, http.StatusUnauthorized, "invalid or missing token") + httpresponse.Error(w, http.StatusUnauthorized, "invalid token") + return } else if token.IsScope(ScopeRoot) { // Allow - token is a super-user token } else if allowedScopes := router.Scope(r.Context()); len(allowedScopes) == 0 { // Allow - no scopes have been defined on this endpoint } else if !token.IsScope(allowedScopes...) { // Deny - token does not have the required scopes - authorized = false - httpresponse.Error(w, http.StatusUnauthorized, "required scope: ", strings.Join(allowedScopes, ",")) - } - - // Return unauthorized if token is not found or not valid - if !authorized { + httpresponse.Error(w, http.StatusUnauthorized, "required scope "+strings.Join(allowedScopes, ", ")) return } diff --git a/pkg/handler/tokenjar/tokenjar.go b/pkg/handler/tokenjar/tokenjar.go index 89b2569..50dedb4 100644 --- a/pkg/handler/tokenjar/tokenjar.go +++ b/pkg/handler/tokenjar/tokenjar.go @@ -8,6 +8,7 @@ import ( "encoding/json" "os" "path/filepath" + "sort" "sync" "time" @@ -119,8 +120,22 @@ func (jar *tokenjar) Modified() bool { } // Return all tokens +type tokenList []auth.Token + +func (l tokenList) Len() int { + return len(l) +} + +func (l tokenList) Less(i, j int) bool { + return l[i].Name < l[j].Name +} + +func (l tokenList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + func (jar *tokenjar) Tokens() []auth.Token { - var result []auth.Token + var result tokenList // Lock the jar for read jar.RLock() @@ -131,6 +146,9 @@ func (jar *tokenjar) Tokens() []auth.Token { result = append(result, *token) } + // Sort the tokens by name + sort.Sort(result) + // Return the result return result } From d4f61f03bd34ba1aa75876be731f5c8ffd0f361c Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 5 Jun 2024 20:24:01 +0200 Subject: [PATCH 02/27] Start of plugin loading --- cmd/http-server/main.go | 2 +- cmd/nginx-server/main.go | 2 +- cmd/run/main.go | 41 +++++ pkg/{middleware => handler}/logger/logger.go | 0 .../logger/middleware.go | 0 .../logger/responsewriter.go | 0 pkg/provider/plugin.go | 145 ++++++++++++++++++ plugin/logger/main.go | 2 +- 8 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 cmd/run/main.go rename pkg/{middleware => handler}/logger/logger.go (100%) rename pkg/{middleware => handler}/logger/middleware.go (100%) rename pkg/{middleware => handler}/logger/responsewriter.go (100%) create mode 100644 pkg/provider/plugin.go diff --git a/cmd/http-server/main.go b/cmd/http-server/main.go index 99b90dc..ed1acb6 100644 --- a/cmd/http-server/main.go +++ b/cmd/http-server/main.go @@ -12,10 +12,10 @@ import ( // Packages server "github.com/mutablelogic/go-server" ctx "github.com/mutablelogic/go-server/pkg/context" + logger "github.com/mutablelogic/go-server/pkg/handler/logger" router "github.com/mutablelogic/go-server/pkg/handler/router" static "github.com/mutablelogic/go-server/pkg/handler/static" httpserver "github.com/mutablelogic/go-server/pkg/httpserver" - logger "github.com/mutablelogic/go-server/pkg/middleware/logger" provider "github.com/mutablelogic/go-server/pkg/provider" ) diff --git a/cmd/nginx-server/main.go b/cmd/nginx-server/main.go index baba40f..adfbd2b 100644 --- a/cmd/nginx-server/main.go +++ b/cmd/nginx-server/main.go @@ -13,11 +13,11 @@ import ( server "github.com/mutablelogic/go-server" ctx "github.com/mutablelogic/go-server/pkg/context" auth "github.com/mutablelogic/go-server/pkg/handler/auth" + logger "github.com/mutablelogic/go-server/pkg/handler/logger" nginx "github.com/mutablelogic/go-server/pkg/handler/nginx" router "github.com/mutablelogic/go-server/pkg/handler/router" tokenjar "github.com/mutablelogic/go-server/pkg/handler/tokenjar" httpserver "github.com/mutablelogic/go-server/pkg/httpserver" - logger "github.com/mutablelogic/go-server/pkg/middleware/logger" provider "github.com/mutablelogic/go-server/pkg/provider" ) diff --git a/cmd/run/main.go b/cmd/run/main.go new file mode 100644 index 0000000..ee6f63e --- /dev/null +++ b/cmd/run/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/mutablelogic/go-server/pkg/provider" +) + +func main() { + var pluginPath string + name := filepath.Base(os.Args[0]) + flags := flag.NewFlagSet(name, flag.ContinueOnError) + flags.StringVar(&pluginPath, "plugin", "*.plugin", "Path to plugins") + if err := flags.Parse(os.Args[1:]); err != nil { + os.Exit(1) + } + + // If the path is relative, then make it absolute to the binary path + if !filepath.IsAbs(pluginPath) { + if strings.HasPrefix(pluginPath, ".") || strings.HasPrefix(pluginPath, "..") { + if wd, err := os.Getwd(); err == nil { + pluginPath = filepath.Clean(filepath.Join(wd, pluginPath)) + } + } else if exec, err := os.Executable(); err == nil { + pluginPath = filepath.Clean(filepath.Join(filepath.Dir(exec), pluginPath)) + } + } + + // Load plugins + plugins, err := provider.LoadPluginsForPattern(pluginPath) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + fmt.Println(plugins) +} diff --git a/pkg/middleware/logger/logger.go b/pkg/handler/logger/logger.go similarity index 100% rename from pkg/middleware/logger/logger.go rename to pkg/handler/logger/logger.go diff --git a/pkg/middleware/logger/middleware.go b/pkg/handler/logger/middleware.go similarity index 100% rename from pkg/middleware/logger/middleware.go rename to pkg/handler/logger/middleware.go diff --git a/pkg/middleware/logger/responsewriter.go b/pkg/handler/logger/responsewriter.go similarity index 100% rename from pkg/middleware/logger/responsewriter.go rename to pkg/handler/logger/responsewriter.go diff --git a/pkg/provider/plugin.go b/pkg/provider/plugin.go new file mode 100644 index 0000000..2459ff2 --- /dev/null +++ b/pkg/provider/plugin.go @@ -0,0 +1,145 @@ +package provider + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "plugin" + + // Packages + server "github.com/mutablelogic/go-server" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Plugin represents a plugin which can be loaded +type Plugin struct { + Path string `json:"path"` + Meta *PluginMeta `json:"meta"` + + // Private fields + plugin server.Plugin +} + +// pluginProvider is a list of plugins +type pluginProvider struct { + plugins map[string]*Plugin +} + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + configFunc = "Plugin" +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func New(plugins ...server.Plugin) *pluginProvider { + self := new(pluginProvider) + self.plugins = make(map[string]*Plugin, len(plugins)) + + // TODO + + return self +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// LoadPluginsForPattern will load plugins from filesystem +// for a given glob pattern +func (p *pluginProvider) LoadPluginsForPattern(pattern string) error { + plugins, err := loadPluginsForPattern(pattern) + if err != nil { + return err + } + + // TODO + + // Return success + return nil +} + +// Create a configuration object for a plugin with a label +func (p *pluginProvider) New(name, label string) (server.Plugin, error) { + // TODO +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (plugin *Plugin) String() string { + data, _ := json.MarshalIndent(plugin, "", " ") + return string(data) +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +// loadPluginsForPattern will load and return a list of plugins for a given glob pattern +func loadPluginsForPattern(pattern string) ([]*Plugin, error) { + var result error + var plugins []*Plugin + + // Seek plugins + files, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + if len(files) == 0 { + return nil, ErrNotFound.Withf("No plugins found for pattern: %q", pattern) + } + + // Load plugins, and create metadata object for the block + for _, path := range files { + plugin, err := pluginWithPath(path) + if err != nil { + result = errors.Join(result, err) + continue + } + meta, err := NewMeta(plugin) + if err != nil { + result = errors.Join(result, err) + continue + } + plugins = append(plugins, &Plugin{ + Path: filepath.Clean(path), + Meta: meta, + plugin: plugin, + }) + } + + // Return any errors + return plugins, result +} + +// Create a new plugin from a filepath +func pluginWithPath(path string) (server.Plugin, error) { + // Check path to make sure it's a regular file + if stat, err := os.Stat(path); err != nil { + return nil, err + } else if !stat.Mode().IsRegular() { + return nil, ErrBadParameter.Withf("Not a regular file: %q", path) + } + + // Load the plugin + if plugin, err := plugin.Open(path); err != nil { + return nil, err + } else if fn, err := plugin.Lookup(configFunc); err != nil { + return nil, err + } else if fn_, ok := fn.(func() server.Plugin); !ok { + _ = fn.(func() server.Plugin) + return nil, ErrInternalAppError.With("New returned nil: ", path) + } else if config := fn_(); config == nil { + return nil, ErrInternalAppError.With("New returned nil: ", path) + } else { + return config, nil + } +} diff --git a/plugin/logger/main.go b/plugin/logger/main.go index 158dd04..a810e08 100644 --- a/plugin/logger/main.go +++ b/plugin/logger/main.go @@ -3,7 +3,7 @@ package main import ( // Packages server "github.com/mutablelogic/go-server" - logger "github.com/mutablelogic/go-server/pkg/middleware/logger" + logger "github.com/mutablelogic/go-server/pkg/handler/logger" ) func Plugin() server.Plugin { From 424ae982b4db94565a5e517d977dec210a0186ee Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 08:13:26 +0200 Subject: [PATCH 03/27] Updated plugins --- cmd/run/main.go | 28 +++++++-- pkg/provider/plugin.go | 109 +++++++++++++++++++++++++--------- pkg/provider/provider_test.go | 21 +++++++ pkg/provider/reflect.go | 4 +- pkg/provider/reflect_test.go | 8 +-- pkg/types/label.go | 37 ++++++++++++ pkg/types/label_test.go | 34 +++++++++++ 7 files changed, 204 insertions(+), 37 deletions(-) create mode 100644 pkg/provider/provider_test.go create mode 100644 pkg/types/label.go create mode 100644 pkg/types/label_test.go diff --git a/cmd/run/main.go b/cmd/run/main.go index ee6f63e..7869de3 100644 --- a/cmd/run/main.go +++ b/cmd/run/main.go @@ -1,12 +1,14 @@ package main import ( + "errors" "flag" "fmt" "os" "path/filepath" "strings" + // Packages "github.com/mutablelogic/go-server/pkg/provider" ) @@ -19,7 +21,8 @@ func main() { os.Exit(1) } - // If the path is relative, then make it absolute to the binary path + // If the path is relative, then make it absolute to either the binary path + // or the current working directory if !filepath.IsAbs(pluginPath) { if strings.HasPrefix(pluginPath, ".") || strings.HasPrefix(pluginPath, "..") { if wd, err := os.Getwd(); err == nil { @@ -30,12 +33,29 @@ func main() { } } - // Load plugins - plugins, err := provider.LoadPluginsForPattern(pluginPath) + // Create a new provider, load plugins + provider, err := provider.New() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } + if err := provider.LoadPluginsForPattern(pluginPath); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Create configurations + var result error + for _, plugin := range []string{"logger", "httpserver", "router", "nginx-handler", "auth-handler", "tokenjar-handler"} { + if _, err := provider.New(plugin); err != nil { + result = errors.Join(result, err) + } + } + if result != nil { + fmt.Fprintln(os.Stderr, result) + os.Exit(1) + } - fmt.Println(plugins) + // Set parameters + // provider.Set("logger.flags", []string{"default", "prefix"}) } diff --git a/pkg/provider/plugin.go b/pkg/provider/plugin.go index 2459ff2..b05c650 100644 --- a/pkg/provider/plugin.go +++ b/pkg/provider/plugin.go @@ -6,9 +6,12 @@ import ( "os" "path/filepath" "plugin" + "reflect" + "strings" // Packages server "github.com/mutablelogic/go-server" + types "github.com/mutablelogic/go-server/pkg/types" // Namespace imports . "github.com/djthorpe/go-errors" @@ -18,17 +21,19 @@ import ( // TYPES // Plugin represents a plugin which can be loaded -type Plugin struct { - Path string `json:"path"` +type pluginMeta struct { + Path string `json:"path,omitempty"` + Name string `json:"name"` Meta *PluginMeta `json:"meta"` // Private fields plugin server.Plugin } -// pluginProvider is a list of plugins +// pluginProvider is a list of plugins and configurations type pluginProvider struct { - plugins map[string]*Plugin + plugins map[string]*pluginMeta + labels map[types.Label]server.Plugin } /////////////////////////////////////////////////////////////////////////////// @@ -41,13 +46,37 @@ const ( /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -func New(plugins ...server.Plugin) *pluginProvider { +func New(plugins ...server.Plugin) (*pluginProvider, error) { self := new(pluginProvider) - self.plugins = make(map[string]*Plugin, len(plugins)) + self.plugins = make(map[string]*pluginMeta, len(plugins)) + self.labels = make(map[types.Label]server.Plugin, len(plugins)) - // TODO + var result error + for _, plugin := range plugins { + if plugin_, err := NewPlugin(plugin, ""); err != nil { + result = errors.Join(result, err) + } else if _, exists := self.plugins[plugin_.Name]; exists { + result = errors.Join(result, ErrDuplicateEntry.With(plugin_.Name)) + } else { + self.plugins[plugin_.Name] = plugin_ + } + } + + // Return any errors + return self, result +} - return self +func NewPlugin(v server.Plugin, path string) (*pluginMeta, error) { + meta, err := NewPluginMeta(v) + if err != nil { + return nil, err + } + return &pluginMeta{ + Path: path, + Name: v.Name(), + Meta: meta, + plugin: v, + }, nil } /////////////////////////////////////////////////////////////////////////////// @@ -61,21 +90,50 @@ func (p *pluginProvider) LoadPluginsForPattern(pattern string) error { return err } - // TODO + var result error + for _, plugin := range plugins { + if plugin_, err := NewPlugin(plugin, ""); err != nil { + result = errors.Join(result, err) + } else if _, exists := p.plugins[plugin_.Name]; exists { + result = errors.Join(result, ErrDuplicateEntry.With(plugin_.Name)) + } else { + p.plugins[plugin_.Name] = plugin_ + } + } // Return success return nil } -// Create a configuration object for a plugin with a label -func (p *pluginProvider) New(name, label string) (server.Plugin, error) { - // TODO +// Create a configuration object for a plugin with label parts +func (p *pluginProvider) New(name string, suffix ...string) (server.Plugin, error) { + // Get the plugin + plugin, exists := p.plugins[name] + if !exists { + return nil, ErrNotFound.Withf("plugin %q", name) + } + + // Create the label + label := types.NewLabel(name, suffix...) + if label == "" { + return nil, ErrBadParameter.Withf("invalid label with suffix %q", strings.Join(suffix, types.LabelSeparator)) + } + + // Check for existing label + if _, exists := p.labels[label]; exists { + return nil, ErrDuplicateEntry.With(label) + } else { + p.labels[label] = plugin.new() + } + + // Create a new configuration + return p.labels[label], nil } /////////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (plugin *Plugin) String() string { +func (plugin *pluginMeta) String() string { data, _ := json.MarshalIndent(plugin, "", " ") return string(data) } @@ -83,10 +141,15 @@ func (plugin *Plugin) String() string { /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS +// new makes a new copy of the plugin +func (plugin *pluginMeta) new() server.Plugin { + rt := reflect.TypeOf(plugin.plugin) + return reflect.New(rt).Interface().(server.Plugin) +} + // loadPluginsForPattern will load and return a list of plugins for a given glob pattern -func loadPluginsForPattern(pattern string) ([]*Plugin, error) { - var result error - var plugins []*Plugin +func loadPluginsForPattern(pattern string) ([]server.Plugin, error) { + var plugins []server.Plugin // Seek plugins files, err := filepath.Glob(pattern) @@ -98,22 +161,14 @@ func loadPluginsForPattern(pattern string) ([]*Plugin, error) { } // Load plugins, and create metadata object for the block + var result error for _, path := range files { plugin, err := pluginWithPath(path) if err != nil { result = errors.Join(result, err) - continue - } - meta, err := NewMeta(plugin) - if err != nil { - result = errors.Join(result, err) - continue + } else { + plugins = append(plugins, plugin) } - plugins = append(plugins, &Plugin{ - Path: filepath.Clean(path), - Meta: meta, - plugin: plugin, - }) } // Return any errors diff --git a/pkg/provider/provider_test.go b/pkg/provider/provider_test.go new file mode 100644 index 0000000..198bc27 --- /dev/null +++ b/pkg/provider/provider_test.go @@ -0,0 +1,21 @@ +package provider_test + +import ( + "testing" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/logger" + "github.com/mutablelogic/go-server/pkg/handler/router" + "github.com/mutablelogic/go-server/pkg/httpserver" + "github.com/mutablelogic/go-server/pkg/provider" + "github.com/stretchr/testify/assert" +) + +func Test_provider_001(t *testing.T) { + assert := assert.New(t) + + provider, err := provider.New(httpserver.Config{}, router.Config{}, logger.Config{}) + assert.NoError(err) + + t.Log(provider) +} diff --git a/pkg/provider/reflect.go b/pkg/provider/reflect.go index f68e499..f1d4ba5 100644 --- a/pkg/provider/reflect.go +++ b/pkg/provider/reflect.go @@ -37,8 +37,8 @@ const ( /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// NewMeta returns metadata for a plugin -func NewMeta(v server.Plugin) (*PluginMeta, error) { +// NewPluginMeta returns metadata for a plugin +func NewPluginMeta(v server.Plugin) (*PluginMeta, error) { meta := &PluginMeta{ Name: v.Name(), Description: v.Description(), diff --git a/pkg/provider/reflect_test.go b/pkg/provider/reflect_test.go index 1d20556..55d24f1 100644 --- a/pkg/provider/reflect_test.go +++ b/pkg/provider/reflect_test.go @@ -4,9 +4,9 @@ import ( "testing" // Packages + "github.com/mutablelogic/go-server/pkg/handler/logger" "github.com/mutablelogic/go-server/pkg/handler/router" "github.com/mutablelogic/go-server/pkg/httpserver" - "github.com/mutablelogic/go-server/pkg/middleware/logger" "github.com/mutablelogic/go-server/pkg/provider" "github.com/stretchr/testify/assert" ) @@ -14,19 +14,19 @@ import ( func Test_reflect_001(t *testing.T) { assert := assert.New(t) - meta, err := provider.NewMeta(httpserver.Config{}) + meta, err := provider.NewPluginMeta(httpserver.Config{}) assert.NoError(err) for k, v := range meta.Fields { t.Log(meta.Name, k, v) } - meta, err = provider.NewMeta(router.Config{}) + meta, err = provider.NewPluginMeta(router.Config{}) assert.NoError(err) for k, v := range meta.Fields { t.Log(meta.Name, k, v) } - meta, err = provider.NewMeta(logger.Config{}) + meta, err = provider.NewPluginMeta(logger.Config{}) assert.NoError(err) for k, v := range meta.Fields { t.Log(meta.Name, k, v) diff --git a/pkg/types/label.go b/pkg/types/label.go new file mode 100644 index 0000000..d82ec17 --- /dev/null +++ b/pkg/types/label.go @@ -0,0 +1,37 @@ +package types + +import "strings" + +///////////////////////////////////////////////////////////////////// +// TYPES + +type Label string + +///////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + LabelSeparator = "." +) + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// NewLabel returns a new label which is a concatenation of the prefix +// and parts, separated by a period. Returns an empty string if the +// prefix or any of the parts are not valid identifiers +func NewLabel(prefix string, parts ...string) Label { + if !IsIdentifier(prefix) { + return "" + } + for _, part := range parts { + if !IsIdentifier(part) { + return "" + } + } + if len(parts) == 0 { + return Label(prefix) + } else { + return Label(prefix + LabelSeparator + strings.Join(parts, LabelSeparator)) + } +} diff --git a/pkg/types/label_test.go b/pkg/types/label_test.go new file mode 100644 index 0000000..611279e --- /dev/null +++ b/pkg/types/label_test.go @@ -0,0 +1,34 @@ +package types_test + +import ( + "testing" + + "github.com/mutablelogic/go-server/pkg/types" +) + +func Test_Label_000(t *testing.T) { + tests := []struct { + In []string + Out string + Err bool + }{ + {[]string{""}, "", true}, + {[]string{"a"}, "a", false}, + {[]string{"a", "b"}, "a.b", false}, + {[]string{"a", "", "b"}, "a..b", true}, + } + + for _, test := range tests { + t.Run(test.Out, func(t *testing.T) { + label := types.NewLabel(test.In[0], test.In[1:]...) + if label == "" { + if !test.Err { + t.Errorf("Expected label, got error for %q", test.Out) + } + return + } else if label != types.Label(test.Out) { + t.Errorf("Expected %v, got %v for %q", test.Out, label, test.In) + } + }) + } +} From b7d216ff6dcbb9c594bdbb0379da2a977de98785 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 08:36:23 +0200 Subject: [PATCH 04/27] Updated some notes --- cmd/run/main.go | 46 ++++++++++++++++++++++++++++++++++++++-- pkg/provider/plugin.go | 21 ++++++++++++++++++ pkg/provider/provider.go | 7 ++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/cmd/run/main.go b/cmd/run/main.go index 7869de3..eb8765b 100644 --- a/cmd/run/main.go +++ b/cmd/run/main.go @@ -56,6 +56,48 @@ func main() { os.Exit(1) } - // Set parameters - // provider.Set("logger.flags", []string{"default", "prefix"}) + // TODO: Set parameters from a JSON file + provider.Set("logger.flags", []string{"default", "prefix"}) } + +/* +{ + "logger": { + "flags": ["default", "prefix"] + }, + "nginx": { + "binary": "/usr/local/bin/nginx", + "data": "/var/run/nginx", + "group": "www-data", + }, + httpserver": { + "listen": "run/go-server.sock", + "group": "www-data", + "router": "${ router }", + }, + "router": { + "services": { + "nginx": { + "service": "${ nginx }", + "middleware": ["logger", "auth"] + }, + "auth": { + "service": "${ auth }", + "middleware": ["logger", "auth"] + }, + "router": { + "service": "${ router }", + "middleware": ["logger", "auth"] + }, + }, + "auth": { + "tokenjar": "${ tokenjar }", + "tokenbytes": 16, + "bearer": true, + }, + "tokenjar": { + "data": "run", + "writeinterval": "30s", + }, +} +*/ diff --git a/pkg/provider/plugin.go b/pkg/provider/plugin.go index b05c650..1068e37 100644 --- a/pkg/provider/plugin.go +++ b/pkg/provider/plugin.go @@ -3,6 +3,7 @@ package provider import ( "encoding/json" "errors" + "fmt" "os" "path/filepath" "plugin" @@ -130,6 +131,26 @@ func (p *pluginProvider) New(name string, suffix ...string) (server.Plugin, erro return p.labels[label], nil } +// Set a parameter for a plugin with label parts +func (p *pluginProvider) Set(label types.Label, value interface{}) error { + // Get the plugin + plugin, exists := p.labels[label] + if !exists { + return ErrNotFound.With(label) + } + + // TODO: Set the value + fmt.Println("TODO: Set", plugin, label, value) + + return ErrNotImplemented +} + +// Create tasks for all the plugins, in the order they should be created +func (p *pluginProvider) Tasks() ([]server.Task, error) { + // TODO + return nil, ErrNotImplemented +} + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 07ab953..0beb11d 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -70,6 +70,7 @@ func NewProvider(tasks ...server.Task) server.Provider { // when the context is cancelled. func (p *provider) Run(ctx context.Context) error { var result error + var wg sync.WaitGroup // Run all the tasks in parallel for i := range p.tasks { @@ -83,9 +84,11 @@ func (p *provider) Run(ctx context.Context) error { p.tasks[i].Add(1) // Run the task in a goroutine + wg.Add(1) go func(i int) { defer p.tasks[i].Done() defer p.tasks[i].Cancel() + defer wg.Done() p.Print(ctx, "Running") if err := p.tasks[i].Run(ctx); err != nil { @@ -106,8 +109,12 @@ func (p *provider) Run(ctx context.Context) error { p.tasks[i].Wait() // TODO: If the task is in the set of loggers, then remove it + // from the list of loggers } + // Wait for all the tasks to complete + wg.Wait() + // Return any errors return result } From 4e190d2579d0c8bb535a501fb2feae9d95ceab8c Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 08:45:16 +0200 Subject: [PATCH 05/27] Uodated JSON --- etc/json/README.md | 5 ++++ etc/json/nginx-proxy.json | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 etc/json/README.md create mode 100644 etc/json/nginx-proxy.json diff --git a/etc/json/README.md b/etc/json/README.md new file mode 100644 index 0000000..0b15bb4 --- /dev/null +++ b/etc/json/README.md @@ -0,0 +1,5 @@ + +# JSON configuration files + +Examples of JSON configuration files for the run command. This is not yet +implemented, but will be in the future. diff --git a/etc/json/nginx-proxy.json b/etc/json/nginx-proxy.json new file mode 100644 index 0000000..46056c2 --- /dev/null +++ b/etc/json/nginx-proxy.json @@ -0,0 +1,52 @@ +{ + "logger": { + "flags": [ + "default", + "prefix" + ] + }, + "nginx": { + "binary": "/usr/local/bin/nginx", + "data": "/var/run/nginx", + "group": "nginx" + }, + "tokenjar": { + "data": "run", + "writeinterval": "30s" + }, + "auth": { + "tokenjar": "${ tokenjar }", + "tokenbytes": 16, + "bearer": true + }, + "router": { + "services": { + "nginx": { + "service": "${ nginx }", + "middleware": [ + "logger", + "auth" + ] + }, + "auth": { + "service": "${ auth }", + "middleware": [ + "logger", + "auth" + ] + }, + "router": { + "service": "${ router }", + "middleware": [ + "logger", + "auth" + ] + } + } + }, + "httpserver": { + "listen": "run/go-server.sock", + "group": "nginx", + "router": "${ router }" + } +} From ba68fb689fdb17b4f0bf3a767c04a583dd7f5842 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 09:43:03 +0200 Subject: [PATCH 06/27] Added some cert methods --- pkg/handler/certmanager/cert/cert.go | 215 +++++++++++++++++++++++++++ pkg/handler/certmanager/cert/opts.go | 8 + pkg/handler/certmanager/config.go | 11 ++ 3 files changed, 234 insertions(+) create mode 100644 pkg/handler/certmanager/cert/cert.go create mode 100644 pkg/handler/certmanager/cert/opts.go create mode 100644 pkg/handler/certmanager/config.go diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go new file mode 100644 index 0000000..583fba5 --- /dev/null +++ b/pkg/handler/certmanager/cert/cert.go @@ -0,0 +1,215 @@ +package cert + +// Ref: https://go.dev/src/crypto/tls/generate_cert.go + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "math/big" + "net" + "strings" + "time" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/certmanager" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Cert struct { + ca bool + serial *big.Int + privateKey any + data []byte +} + +type KeyType int + +const ( + _ KeyType = iota + ED25519 + RSA2048 + P224 + P256 + P384 + P521 +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFE CYCLE + +func New(c certmanager.Config, k KeyType, years, months, days int, ca bool, host string, opts ...Opts) (*Cert, error) { + cert := new(Cert) + + // Random serial number + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } else { + cert.serial = serialNumber + } + + // Create the certificate template + template := &x509.Certificate{ + SerialNumber: cert.serial, + Subject: pkix.Name{ + Organization: []string{c.Organization}, + OrganizationalUnit: []string{c.OrganizationalUnit}, + Country: []string{c.Country}, + Locality: []string{c.Locality}, + Province: []string{c.Province}, + StreetAddress: []string{c.StreetAddress}, + PostalCode: []string{c.PostalCode}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(years, months, days), + IsCA: ca, + ExtKeyUsage: []x509.ExtKeyUsage{}, + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + KeyUsage: x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + + // Create new private key + public, private, err := generateKey(k) + if err != nil { + return nil, err + } else { + cert.privateKey = private + } + + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + if _, isRSA := private.(*rsa.PrivateKey); isRSA { + template.KeyUsage |= x509.KeyUsageKeyEncipherment + } + + // Set hosts + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + // Set CA + if ca { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + cert.ca = true + } else { + template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth) + } + + data, err := x509.CreateCertificate(rand.Reader, template, template, public, private) + if err != nil { + return nil, err + } else { + cert.data = data + } + + // Return success + return cert, nil +} + +/* +func LoadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey) { + cf, e := ioutil.ReadFile(certFile) + if e != nil { + fmt.Println("cfload:", e.Error()) + os.Exit(1) + } + + kf, e := ioutil.ReadFile(keyFile) + if e != nil { + fmt.Println("kfload:", e.Error()) + os.Exit(1) + } + cpb, cr := pem.Decode(cf) + fmt.Println(string(cr)) + kpb, kr := pem.Decode(kf) + fmt.Println(string(kr)) + crt, e := x509.ParseCertificate(cpb.Bytes) + + if e != nil { + fmt.Println("parsex509:", e.Error()) + os.Exit(1) + } + key, e := x509.ParsePKCS1PrivateKey(kpb.Bytes) + if e != nil { + fmt.Println("parsekey:", e.Error()) + os.Exit(1) + } + return crt, key +} +*/ + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the serial number of the certificate +func (c *Cert) Serial() string { + return c.serial.String() +} + +// Write a .pem file with the certificate +func (c *Cert) WriteCertificate(w io.Writer) error { + return pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: c.data}) +} + +// Write a .pem file with the private key +func (c *Cert) WritePrivateKey(w io.Writer) error { + if privBytes, err := x509.MarshalPKCS8PrivateKey(c.privateKey); err != nil { + return err + } else if err := pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { + return err + } + + // Return success + return nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +// ECDSA curve to use to generate a key. Valid values are P224, P256 (default), P384, P521 +// If empty, RSA keys will be generated instead +func generateKey(t KeyType) (any, any, error) { + switch t { + case ED25519: + return ed25519.GenerateKey(rand.Reader) + case RSA2048: + priv, err := rsa.GenerateKey(rand.Reader, 2048) + return &priv.PublicKey, priv, err + case P224: + priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + return &priv.PublicKey, priv, err + case P256: + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + return &priv.PublicKey, priv, err + case P384: + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + return &priv.PublicKey, priv, err + case P521: + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + return &priv.PublicKey, priv, err + default: + return nil, nil, ErrBadParameter + } +} diff --git a/pkg/handler/certmanager/cert/opts.go b/pkg/handler/certmanager/cert/opts.go new file mode 100644 index 0000000..268e17c --- /dev/null +++ b/pkg/handler/certmanager/cert/opts.go @@ -0,0 +1,8 @@ +package cert + +type Opts func(*opts) error + +type opts struct { + ecdsaCurve string // P224, P256 (default), P384, P521 + rsaBits int // 2048 (default), 4096 +} diff --git a/pkg/handler/certmanager/config.go b/pkg/handler/certmanager/config.go new file mode 100644 index 0000000..ba163df --- /dev/null +++ b/pkg/handler/certmanager/config.go @@ -0,0 +1,11 @@ +package certmanager + +type Config struct { + Organization string `json:"organization"` + OrganizationalUnit string `json:"organizational_unit,omitempty"` + Country string `json:"country,omitempty"` + Province string `json:"province,omitempty"` + Locality string `json:"locality,omitempty"` + StreetAddress string `json:"street_address,omitempty"` + PostalCode string `json:"postal_code,omitempty"` +} From fd065f06bbb03da61d851e5fa3674ec505c0d7ad Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 09:56:07 +0200 Subject: [PATCH 07/27] Updated cert --- pkg/handler/certmanager/cert/cert.go | 46 +++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index 583fba5..35fde52 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -28,8 +28,8 @@ import ( // TYPES type Cert struct { - ca bool serial *big.Int + publicKey any privateKey any data []byte } @@ -49,7 +49,7 @@ const ( /////////////////////////////////////////////////////////////////////////////// // LIFE CYCLE -func New(c certmanager.Config, k KeyType, years, months, days int, ca bool, host string, opts ...Opts) (*Cert, error) { +func New(c certmanager.Config, parent *x509.Certificate, parentPrivateKey any, k KeyType, years, months, days int, host string, opts ...Opts) (*Cert, error) { cert := new(Cert) // Random serial number @@ -73,29 +73,22 @@ func New(c certmanager.Config, k KeyType, years, months, days int, ca bool, host StreetAddress: []string{c.StreetAddress}, PostalCode: []string{c.PostalCode}, }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(years, months, days), - IsCA: ca, - ExtKeyUsage: []x509.ExtKeyUsage{}, - // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature - // KeyUsage bits set in the x509.Certificate template - KeyUsage: x509.KeyUsageDigitalSignature, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(years, months, days), + IsCA: parent == nil, + ExtKeyUsage: []x509.ExtKeyUsage{}, BasicConstraintsValid: true, } + // TODO: k needs to be the same as the key type of the parent if parentPrivateKey is set + // Create new private key - public, private, err := generateKey(k) + publicKey, privateKey, err := generateKey(k) if err != nil { return nil, err } else { - cert.privateKey = private - } - - // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In - // the context of TLS this KeyUsage is particular to RSA key exchange and - // authentication. - if _, isRSA := private.(*rsa.PrivateKey); isRSA { - template.KeyUsage |= x509.KeyUsageKeyEncipherment + cert.publicKey = publicKey + cert.privateKey = privateKey } // Set hosts @@ -108,16 +101,25 @@ func New(c certmanager.Config, k KeyType, years, months, days int, ca bool, host } } - // Set CA - if ca { + // Set CA flags or Server cert flags + if parent == nil { template.IsCA = true template.KeyUsage |= x509.KeyUsageCertSign - cert.ca = true } else { + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + template.KeyUsage |= x509.KeyUsageDigitalSignature template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth) } - data, err := x509.CreateCertificate(rand.Reader, template, template, public, private) + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + if _, isRSA := cert.privateKey.(*rsa.PrivateKey); isRSA { + template.KeyUsage |= x509.KeyUsageKeyEncipherment + } + + data, err := x509.CreateCertificate(rand.Reader, template, parent, cert.publicKey, parentPrivateKey) if err != nil { return nil, err } else { From 0bc65c44a9c227103e75d42ae2d783ceccd79ebe Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 10:55:44 +0200 Subject: [PATCH 08/27] Updated cert --- pkg/handler/certmanager/cert/cert.go | 241 ++++++++++++++-------- pkg/handler/certmanager/cert/cert.go_old | 217 +++++++++++++++++++ pkg/handler/certmanager/cert/cert_test.go | 69 +++++++ pkg/handler/certmanager/cert/opts.go | 90 +++++++- 4 files changed, 530 insertions(+), 87 deletions(-) create mode 100644 pkg/handler/certmanager/cert/cert.go_old create mode 100644 pkg/handler/certmanager/cert/cert_test.go diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index 35fde52..502a82f 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -13,8 +13,6 @@ import ( "encoding/pem" "io" "math/big" - "net" - "strings" "time" // Packages @@ -34,10 +32,10 @@ type Cert struct { data []byte } -type KeyType int +type keyType int const ( - _ KeyType = iota + _ keyType = iota ED25519 RSA2048 P224 @@ -47,82 +45,151 @@ const ( ) /////////////////////////////////////////////////////////////////////////////// -// LIFE CYCLE +// GLOBALS -func New(c certmanager.Config, parent *x509.Certificate, parentPrivateKey any, k KeyType, years, months, days int, host string, opts ...Opts) (*Cert, error) { - cert := new(Cert) +var ( + serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) +) - // Random serial number - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) +const ( + defaultYearsCA = 2 + defaultMonthsCert = 3 + defaultKey = RSA2048 +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new certificate authority with the given configuration +// and options +func NewCA(c certmanager.Config, opt ...Opt) (*Cert, error) { + var o opts + + // Set defaults + o.KeyType = defaultKey + o.Years = defaultYearsCA + + // Set options + for _, fn := range opt { + if err := fn(&o); err != nil { + return nil, err + } + } + + // Get serial number + var serial *big.Int + if o.Serial != 0 { + serial = big.NewInt(o.Serial) + } else if serial = SerialNumber(); serial == nil { + return nil, ErrInternalAppError.With("SerialNumber") + } + + // Create a new certificate with a template + template := x509TemplateFor(c, o, serial) + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + // Generate public, private keys + publicKey, privateKey, err := generateKey(o.KeyType) if err != nil { return nil, err - } else { - cert.serial = serialNumber } - // Create the certificate template - template := &x509.Certificate{ - SerialNumber: cert.serial, - Subject: pkix.Name{ - Organization: []string{c.Organization}, - OrganizationalUnit: []string{c.OrganizationalUnit}, - Country: []string{c.Country}, - Locality: []string{c.Locality}, - Province: []string{c.Province}, - StreetAddress: []string{c.StreetAddress}, - PostalCode: []string{c.PostalCode}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(years, months, days), - IsCA: parent == nil, - ExtKeyUsage: []x509.ExtKeyUsage{}, - BasicConstraintsValid: true, + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + if _, isRSA := privateKey.(*rsa.PrivateKey); isRSA { + template.KeyUsage |= x509.KeyUsageKeyEncipherment } - // TODO: k needs to be the same as the key type of the parent if parentPrivateKey is set - - // Create new private key - publicKey, privateKey, err := generateKey(k) + // Create self-signed CA + cert := new(Cert) + data, err := x509.CreateCertificate(rand.Reader, template, template, publicKey, privateKey) if err != nil { return nil, err } else { + cert.serial = serial cert.publicKey = publicKey cert.privateKey = privateKey + cert.data = data } - // Set hosts - hosts := strings.Split(host, ",") - for _, h := range hosts { - if ip := net.ParseIP(h); ip != nil { - template.IPAddresses = append(template.IPAddresses, ip) - } else { - template.DNSNames = append(template.DNSNames, h) + // Return success + return cert, nil +} + +// Create a new certificate, either self-signed (if ca is nil) or +// signed by the certificate authority with the given options +func NewCert(ca *Cert, opt ...Opt) (*Cert, error) { + var o opts + + // Set defaults + o.KeyType = defaultKey + o.Months = defaultMonthsCert + + // Set options + for _, fn := range opt { + if err := fn(&o); err != nil { + return nil, err } } - // Set CA flags or Server cert flags - if parent == nil { - template.IsCA = true - template.KeyUsage |= x509.KeyUsageCertSign - } else { - // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature - // KeyUsage bits set in the x509.Certificate template - template.KeyUsage |= x509.KeyUsageDigitalSignature - template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth) + // Get serial number + var serial *big.Int + if o.Serial != 0 { + serial = big.NewInt(o.Serial) + } else if serial = SerialNumber(); serial == nil { + return nil, ErrInternalAppError.With("SerialNumber") } + parent, err := x509.ParseCertificate(ca.data) + if err != nil { + return nil, err + } + template, err := x509.ParseCertificate(ca.data) + if err != nil { + return nil, err + } + if o.Name != nil { + template.Subject = *o.Name + } + if len(o.IPAddresses) > 0 { + template.IPAddresses = o.IPAddresses + } + if len(o.DNSNames) > 0 { + template.DNSNames = o.DNSNames + } + template.SerialNumber = serial + template.NotBefore = time.Now() + template.NotAfter = time.Now().AddDate(o.Years, o.Months, o.Days) + template.IsCA = false + template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + template.KeyUsage = x509.KeyUsageDigitalSignature + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In // the context of TLS this KeyUsage is particular to RSA key exchange and // authentication. - if _, isRSA := cert.privateKey.(*rsa.PrivateKey); isRSA { + if _, isRSA := ca.privateKey.(*rsa.PrivateKey); isRSA { template.KeyUsage |= x509.KeyUsageKeyEncipherment } - data, err := x509.CreateCertificate(rand.Reader, template, parent, cert.publicKey, parentPrivateKey) + // Generate public, private keys + publicKey, privateKey, err := generateKey(o.KeyType) + if err != nil { + return nil, err + } + + // Create cert signed by the CA + cert := new(Cert) + data, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, ca.privateKey) if err != nil { return nil, err } else { + cert.serial = serial + cert.publicKey = publicKey + cert.privateKey = privateKey cert.data = data } @@ -130,41 +197,20 @@ func New(c certmanager.Config, parent *x509.Certificate, parentPrivateKey any, k return cert, nil } -/* -func LoadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey) { - cf, e := ioutil.ReadFile(certFile) - if e != nil { - fmt.Println("cfload:", e.Error()) - os.Exit(1) - } - - kf, e := ioutil.ReadFile(keyFile) - if e != nil { - fmt.Println("kfload:", e.Error()) - os.Exit(1) - } - cpb, cr := pem.Decode(cf) - fmt.Println(string(cr)) - kpb, kr := pem.Decode(kf) - fmt.Println(string(kr)) - crt, e := x509.ParseCertificate(cpb.Bytes) - - if e != nil { - fmt.Println("parsex509:", e.Error()) - os.Exit(1) - } - key, e := x509.ParsePKCS1PrivateKey(kpb.Bytes) - if e != nil { - fmt.Println("parsekey:", e.Error()) - os.Exit(1) - } - return crt, key -} -*/ - /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS +// Create a new serial number for the certificate, or nil if there +// was an error +func SerialNumber() *big.Int { + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil + } else { + return serialNumber + } +} + // Return the serial number of the certificate func (c *Cert) Serial() string { return c.serial.String() @@ -190,9 +236,36 @@ func (c *Cert) WritePrivateKey(w io.Writer) error { /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS +func x509TemplateFor(c certmanager.Config, o opts, serial *big.Int) *x509.Certificate { + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + Organization: []string{c.Organization}, + OrganizationalUnit: []string{c.OrganizationalUnit}, + Country: []string{c.Country}, + Locality: []string{c.Locality}, + Province: []string{c.Province}, + StreetAddress: []string{c.StreetAddress}, + PostalCode: []string{c.PostalCode}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(o.Years, o.Months, o.Days), + ExtKeyUsage: []x509.ExtKeyUsage{}, + BasicConstraintsValid: true, + } + + // Set X509 Name + if o.Name != nil { + template.Subject = *o.Name + } + + // Return the template + return template +} + // ECDSA curve to use to generate a key. Valid values are P224, P256 (default), P384, P521 // If empty, RSA keys will be generated instead -func generateKey(t KeyType) (any, any, error) { +func generateKey(t keyType) (any, any, error) { switch t { case ED25519: return ed25519.GenerateKey(rand.Reader) diff --git a/pkg/handler/certmanager/cert/cert.go_old b/pkg/handler/certmanager/cert/cert.go_old new file mode 100644 index 0000000..35fde52 --- /dev/null +++ b/pkg/handler/certmanager/cert/cert.go_old @@ -0,0 +1,217 @@ +package cert + +// Ref: https://go.dev/src/crypto/tls/generate_cert.go + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "math/big" + "net" + "strings" + "time" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/certmanager" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Cert struct { + serial *big.Int + publicKey any + privateKey any + data []byte +} + +type KeyType int + +const ( + _ KeyType = iota + ED25519 + RSA2048 + P224 + P256 + P384 + P521 +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFE CYCLE + +func New(c certmanager.Config, parent *x509.Certificate, parentPrivateKey any, k KeyType, years, months, days int, host string, opts ...Opts) (*Cert, error) { + cert := new(Cert) + + // Random serial number + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } else { + cert.serial = serialNumber + } + + // Create the certificate template + template := &x509.Certificate{ + SerialNumber: cert.serial, + Subject: pkix.Name{ + Organization: []string{c.Organization}, + OrganizationalUnit: []string{c.OrganizationalUnit}, + Country: []string{c.Country}, + Locality: []string{c.Locality}, + Province: []string{c.Province}, + StreetAddress: []string{c.StreetAddress}, + PostalCode: []string{c.PostalCode}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(years, months, days), + IsCA: parent == nil, + ExtKeyUsage: []x509.ExtKeyUsage{}, + BasicConstraintsValid: true, + } + + // TODO: k needs to be the same as the key type of the parent if parentPrivateKey is set + + // Create new private key + publicKey, privateKey, err := generateKey(k) + if err != nil { + return nil, err + } else { + cert.publicKey = publicKey + cert.privateKey = privateKey + } + + // Set hosts + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + // Set CA flags or Server cert flags + if parent == nil { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } else { + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + template.KeyUsage |= x509.KeyUsageDigitalSignature + template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth) + } + + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + if _, isRSA := cert.privateKey.(*rsa.PrivateKey); isRSA { + template.KeyUsage |= x509.KeyUsageKeyEncipherment + } + + data, err := x509.CreateCertificate(rand.Reader, template, parent, cert.publicKey, parentPrivateKey) + if err != nil { + return nil, err + } else { + cert.data = data + } + + // Return success + return cert, nil +} + +/* +func LoadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey) { + cf, e := ioutil.ReadFile(certFile) + if e != nil { + fmt.Println("cfload:", e.Error()) + os.Exit(1) + } + + kf, e := ioutil.ReadFile(keyFile) + if e != nil { + fmt.Println("kfload:", e.Error()) + os.Exit(1) + } + cpb, cr := pem.Decode(cf) + fmt.Println(string(cr)) + kpb, kr := pem.Decode(kf) + fmt.Println(string(kr)) + crt, e := x509.ParseCertificate(cpb.Bytes) + + if e != nil { + fmt.Println("parsex509:", e.Error()) + os.Exit(1) + } + key, e := x509.ParsePKCS1PrivateKey(kpb.Bytes) + if e != nil { + fmt.Println("parsekey:", e.Error()) + os.Exit(1) + } + return crt, key +} +*/ + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the serial number of the certificate +func (c *Cert) Serial() string { + return c.serial.String() +} + +// Write a .pem file with the certificate +func (c *Cert) WriteCertificate(w io.Writer) error { + return pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: c.data}) +} + +// Write a .pem file with the private key +func (c *Cert) WritePrivateKey(w io.Writer) error { + if privBytes, err := x509.MarshalPKCS8PrivateKey(c.privateKey); err != nil { + return err + } else if err := pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { + return err + } + + // Return success + return nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +// ECDSA curve to use to generate a key. Valid values are P224, P256 (default), P384, P521 +// If empty, RSA keys will be generated instead +func generateKey(t KeyType) (any, any, error) { + switch t { + case ED25519: + return ed25519.GenerateKey(rand.Reader) + case RSA2048: + priv, err := rsa.GenerateKey(rand.Reader, 2048) + return &priv.PublicKey, priv, err + case P224: + priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + return &priv.PublicKey, priv, err + case P256: + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + return &priv.PublicKey, priv, err + case P384: + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + return &priv.PublicKey, priv, err + case P521: + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + return &priv.PublicKey, priv, err + default: + return nil, nil, ErrBadParameter + } +} diff --git a/pkg/handler/certmanager/cert/cert_test.go b/pkg/handler/certmanager/cert/cert_test.go new file mode 100644 index 0000000..80fd28a --- /dev/null +++ b/pkg/handler/certmanager/cert/cert_test.go @@ -0,0 +1,69 @@ +package cert_test + +import ( + "bytes" + "testing" + + "github.com/mutablelogic/go-server/pkg/handler/certmanager" + "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" + "github.com/stretchr/testify/assert" +) + +func Test_Cert_001(t *testing.T) { + assert := assert.New(t) + for i := 0; i < 100; i++ { + serial := cert.SerialNumber() + assert.NotNil(serial) + t.Log(serial) + } +} + +func Test_Cert_002(t *testing.T) { + assert := assert.New(t) + cert, err := cert.NewCA(certmanager.Config{}) + assert.NoError(err) + assert.NotNil(cert) + t.Log(cert) +} + +func Test_Cert_003(t *testing.T) { + assert := assert.New(t) + cert, err := cert.NewCA(certmanager.Config{}) + assert.NoError(err) + + public := new(bytes.Buffer) + err = cert.WriteCertificate(public) + assert.NoError(err) + assert.NotEmpty(public.String()) + t.Log(public.String()) + + private := new(bytes.Buffer) + err = cert.WritePrivateKey(private) + assert.NoError(err) + assert.NotEmpty(private.String()) + t.Log(private.String()) +} + +func Test_Cert_004(t *testing.T) { + assert := assert.New(t) + ca, err := cert.NewCA(certmanager.Config{ + Organization: "Test", + }, cert.OptKeyType("P521")) + assert.NoError(err) + + cert, err := cert.NewCert(ca, cert.OptKeyType("ED25519")) + assert.NoError(err) + assert.NotNil(cert) + + public := new(bytes.Buffer) + err = cert.WriteCertificate(public) + assert.NoError(err) + assert.NotEmpty(public.String()) + t.Log(public.String()) + + private := new(bytes.Buffer) + err = cert.WritePrivateKey(private) + assert.NoError(err) + assert.NotEmpty(private.String()) + t.Log(private.String()) +} diff --git a/pkg/handler/certmanager/cert/opts.go b/pkg/handler/certmanager/cert/opts.go index 268e17c..b33f577 100644 --- a/pkg/handler/certmanager/cert/opts.go +++ b/pkg/handler/certmanager/cert/opts.go @@ -1,8 +1,92 @@ package cert -type Opts func(*opts) error +import ( + "crypto/x509/pkix" + "net" + "strings" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Opt func(*opts) error type opts struct { - ecdsaCurve string // P224, P256 (default), P384, P521 - rsaBits int // 2048 (default), 4096 + Name *pkix.Name + Serial int64 + KeyType keyType + Years, Months, Days int + IPAddresses []net.IP + DNSNames []string +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func OptX509Name(v pkix.Name) Opt { + return func(o *opts) error { + o.Name = &v + return nil + } +} + +func OptSerial(serial int64) Opt { + return func(o *opts) error { + o.Serial = serial + return nil + } +} + +func OptKeyType(v string) Opt { + return func(o *opts) error { + switch strings.ToUpper(v) { + case "ED25519": + o.KeyType = ED25519 + case "RSA2048": + o.KeyType = RSA2048 + case "P224": + o.KeyType = P224 + case "P256": + o.KeyType = P256 + case "P384": + o.KeyType = P384 + case "P521": + o.KeyType = P521 + default: + return ErrBadParameter.Withf("invalid key type %q", v) + } + return nil + } +} + +func OptExpiry(years, months, days int) Opt { + return func(o *opts) error { + if years < 0 || months < 0 || days < 0 { + return ErrBadParameter.Withf("OptExpiry") + } + // Maximum expiry is 4 years, 12 months, 31 days + if years > 4 || months > 12 || days > 31 { + return ErrBadParameter.Withf("OptExpiry") + } + o.Years = years + o.Months = months + o.Days = days + return nil + } +} + +func OptHosts(v ...string) Opt { + return func(o *opts) error { + for _, v := range v { + if ip := net.ParseIP(v); ip != nil { + o.IPAddresses = append(o.IPAddresses, ip) + } else { + o.DNSNames = append(o.DNSNames, v) + } + } + return nil + } } From 5bb11f4f8ad4a75927d7a4c578d1134d039c3ed2 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 11:32:01 +0200 Subject: [PATCH 09/27] Added NewFromBytes --- pkg/handler/certmanager/cert/cert.go | 97 ++++++++++++++++++----- pkg/handler/certmanager/cert/cert_test.go | 41 +++++++--- pkg/handler/certmanager/cert/opts.go | 5 +- pkg/handler/certmanager/config.go | 6 +- 4 files changed, 118 insertions(+), 31 deletions(-) diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index 502a82f..9cb895e 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -10,9 +10,12 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "encoding/json" "encoding/pem" + "fmt" "io" "math/big" + "net" "time" // Packages @@ -26,10 +29,8 @@ import ( // TYPES type Cert struct { - serial *big.Int - publicKey any - privateKey any data []byte + privateKey any } type keyType int @@ -108,10 +109,8 @@ func NewCA(c certmanager.Config, opt ...Opt) (*Cert, error) { if err != nil { return nil, err } else { - cert.serial = serial - cert.publicKey = publicKey - cert.privateKey = privateKey cert.data = data + cert.privateKey = privateKey } // Return success @@ -120,6 +119,7 @@ func NewCA(c certmanager.Config, opt ...Opt) (*Cert, error) { // Create a new certificate, either self-signed (if ca is nil) or // signed by the certificate authority with the given options +// TODO: Add self-signing ability func NewCert(ca *Cert, opt ...Opt) (*Cert, error) { var o opts @@ -187,8 +187,6 @@ func NewCert(ca *Cert, opt ...Opt) (*Cert, error) { if err != nil { return nil, err } else { - cert.serial = serial - cert.publicKey = publicKey cert.privateKey = privateKey cert.data = data } @@ -197,6 +195,60 @@ func NewCert(ca *Cert, opt ...Opt) (*Cert, error) { return cert, nil } +// Import certificate from byte stream +func NewFromBytes(data []byte) (*Cert, error) { + public, rest := pem.Decode(data) + if public == nil { + return nil, ErrBadParameter.With("unable to decode certificate") + } + priv, _ := pem.Decode(rest) + if priv == nil { + return nil, ErrBadParameter.With("unable to decode private key") + } + + cert := new(Cert) + cert.data = public.Bytes + if privKey, err := x509.ParsePKCS8PrivateKey(priv.Bytes); err != nil { + return nil, err + } else { + cert.privateKey = privKey + } + + // Return success + return cert, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (c *Cert) String() string { + cert, err := x509.ParseCertificate(c.data) + if err != nil { + return fmt.Sprintf("{ %q: %q }", "error", err.Error()) + } + v := struct { + KeyType string `json:"key_type"` + Serial string `json:"serial"` + Subject string `json:"subject"` + IsCA bool `json:"is_ca,omitempty"` + NotBefore time.Time `json:"not_before"` + NotAfter time.Time `json:"not_after"` + IPAddresses []net.IP `json:"ip_addresses,omitempty"` + DNSNames []string `json:"dns_names,omitempty"` + }{ + KeyType: c.KeyType(), + Serial: cert.SerialNumber.String(), + Subject: cert.Subject.String(), + IsCA: cert.IsCA, + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + IPAddresses: cert.IPAddresses, + DNSNames: cert.DNSNames, + } + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -211,9 +263,18 @@ func SerialNumber() *big.Int { } } -// Return the serial number of the certificate -func (c *Cert) Serial() string { - return c.serial.String() +// Return the key type +func (c *Cert) KeyType() string { + switch v := c.privateKey.(type) { + case *rsa.PrivateKey: + return fmt.Sprintf("RSA%d", v.Size()*8) + case *ecdsa.PrivateKey: + return "ECDSA " + v.Curve.Params().Name + case ed25519.PrivateKey: + return "ED25519" + default: + return "UNKNOWN" + } } // Write a .pem file with the certificate @@ -240,13 +301,13 @@ func x509TemplateFor(c certmanager.Config, o opts, serial *big.Int) *x509.Certif template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ - Organization: []string{c.Organization}, - OrganizationalUnit: []string{c.OrganizationalUnit}, - Country: []string{c.Country}, - Locality: []string{c.Locality}, - Province: []string{c.Province}, - StreetAddress: []string{c.StreetAddress}, - PostalCode: []string{c.PostalCode}, + Organization: []string{c.X509Name.Organization}, + OrganizationalUnit: []string{c.X509Name.OrganizationalUnit}, + Country: []string{c.X509Name.Country}, + Locality: []string{c.X509Name.Locality}, + Province: []string{c.X509Name.Province}, + StreetAddress: []string{c.X509Name.StreetAddress}, + PostalCode: []string{c.X509Name.PostalCode}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(o.Years, o.Months, o.Days), diff --git a/pkg/handler/certmanager/cert/cert_test.go b/pkg/handler/certmanager/cert/cert_test.go index 80fd28a..4b4300c 100644 --- a/pkg/handler/certmanager/cert/cert_test.go +++ b/pkg/handler/certmanager/cert/cert_test.go @@ -47,23 +47,42 @@ func Test_Cert_003(t *testing.T) { func Test_Cert_004(t *testing.T) { assert := assert.New(t) ca, err := cert.NewCA(certmanager.Config{ - Organization: "Test", - }, cert.OptKeyType("P521")) + X509Name: certmanager.X509Name{ + OrganizationalUnit: "Test", + Organization: "Test", + Country: "DE", + }, + }) assert.NoError(err) - cert, err := cert.NewCert(ca, cert.OptKeyType("ED25519")) + t.Log(ca) + + cert, err := cert.NewCert(ca, cert.OptKeyType("P224")) assert.NoError(err) assert.NotNil(cert) - public := new(bytes.Buffer) - err = cert.WriteCertificate(public) + t.Log(cert) +} + +func Test_Cert_005(t *testing.T) { + assert := assert.New(t) + ca, err := cert.NewCA(certmanager.Config{ + X509Name: certmanager.X509Name{ + OrganizationalUnit: "Test", + Organization: "Test", + Country: "DE", + }, + }) assert.NoError(err) - assert.NotEmpty(public.String()) - t.Log(public.String()) - private := new(bytes.Buffer) - err = cert.WritePrivateKey(private) + both := new(bytes.Buffer) + err = ca.WriteCertificate(both) assert.NoError(err) - assert.NotEmpty(private.String()) - t.Log(private.String()) + err = ca.WritePrivateKey(both) + assert.NoError(err) + + cert2, err := cert.NewFromBytes(both.Bytes()) + assert.NoError(err) + + t.Log(cert2) } diff --git a/pkg/handler/certmanager/cert/opts.go b/pkg/handler/certmanager/cert/opts.go index b33f577..abf9ed0 100644 --- a/pkg/handler/certmanager/cert/opts.go +++ b/pkg/handler/certmanager/cert/opts.go @@ -35,6 +35,9 @@ func OptX509Name(v pkix.Name) Opt { func OptSerial(serial int64) Opt { return func(o *opts) error { + if serial < 1 { + return ErrBadParameter.Withf("OptSerial") + } o.Serial = serial return nil } @@ -56,7 +59,7 @@ func OptKeyType(v string) Opt { case "P521": o.KeyType = P521 default: - return ErrBadParameter.Withf("invalid key type %q", v) + return ErrBadParameter.Withf("OptKeyType %q", v) } return nil } diff --git a/pkg/handler/certmanager/config.go b/pkg/handler/certmanager/config.go index ba163df..d819104 100644 --- a/pkg/handler/certmanager/config.go +++ b/pkg/handler/certmanager/config.go @@ -1,6 +1,6 @@ package certmanager -type Config struct { +type X509Name struct { Organization string `json:"organization"` OrganizationalUnit string `json:"organizational_unit,omitempty"` Country string `json:"country,omitempty"` @@ -9,3 +9,7 @@ type Config struct { StreetAddress string `json:"street_address,omitempty"` PostalCode string `json:"postal_code,omitempty"` } + +type Config struct { + X509Name `json:"x509_name"` +} From 4cce08ec777372d74a916c5c2ccc6bf6174a4241 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 11:42:06 +0200 Subject: [PATCH 10/27] Updated cert --- pkg/handler/certmanager/cert/cert.go | 20 +++++++++++++------- pkg/handler/certmanager/cert/opts.go | 5 +++++ pkg/handler/certmanager/interface.go | 27 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 pkg/handler/certmanager/interface.go diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index 9cb895e..b3bfa9f 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -28,6 +28,8 @@ import ( /////////////////////////////////////////////////////////////////////////////// // TYPES +// Cert represents a certificate with a private key which can be used for +// signing other certificates type Cert struct { data []byte privateKey any @@ -35,6 +37,12 @@ type Cert struct { type keyType int +// Ensure that Cert implements the certmanager.Cert interface +var _ certmanager.Cert = (*Cert)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + const ( _ keyType = iota ED25519 @@ -45,19 +53,17 @@ const ( P521 ) -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) -) - const ( defaultYearsCA = 2 defaultMonthsCert = 3 defaultKey = RSA2048 ) +var ( + // Maximum is 128 bits + serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) +) + /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE diff --git a/pkg/handler/certmanager/cert/opts.go b/pkg/handler/certmanager/cert/opts.go index abf9ed0..f840878 100644 --- a/pkg/handler/certmanager/cert/opts.go +++ b/pkg/handler/certmanager/cert/opts.go @@ -26,6 +26,7 @@ type opts struct { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS +// Set X509 name func OptX509Name(v pkix.Name) Opt { return func(o *opts) error { o.Name = &v @@ -33,6 +34,7 @@ func OptX509Name(v pkix.Name) Opt { } } +// Set Serial number func OptSerial(serial int64) Opt { return func(o *opts) error { if serial < 1 { @@ -43,6 +45,7 @@ func OptSerial(serial int64) Opt { } } +// Set private key type func OptKeyType(v string) Opt { return func(o *opts) error { switch strings.ToUpper(v) { @@ -65,6 +68,7 @@ func OptKeyType(v string) Opt { } } +// Set certificate expiry func OptExpiry(years, months, days int) Opt { return func(o *opts) error { if years < 0 || months < 0 || days < 0 { @@ -81,6 +85,7 @@ func OptExpiry(years, months, days int) Opt { } } +// Set host or IP address restrictions func OptHosts(v ...string) Opt { return func(o *opts) error { for _, v := range v { diff --git a/pkg/handler/certmanager/interface.go b/pkg/handler/certmanager/interface.go new file mode 100644 index 0000000..852f818 --- /dev/null +++ b/pkg/handler/certmanager/interface.go @@ -0,0 +1,27 @@ +package certmanager + +import "io" + +// Cert interface represents a certificate or certificate authority +type Cert interface { + // Return Serial of the certificate + Serial() string + + // Return the subject of the certificate + Subject() string + + // Return true if the certificate is valid + IsValid() bool + + // Return true if the certificate is a CA + IsCA() bool + + // Return the key type + KeyType() string + + // Write a .pem file with the certificate + WriteCertificate(w io.Writer) error + + // Write a .pem file with the private key + WritePrivateKey(w io.Writer) error +} From 24adb9cfb103bba577413501586bbd7fe713fb53 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 6 Jun 2024 12:03:49 +0200 Subject: [PATCH 11/27] Update interface.go --- pkg/handler/certmanager/interface.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/handler/certmanager/interface.go b/pkg/handler/certmanager/interface.go index 852f818..e04ba64 100644 --- a/pkg/handler/certmanager/interface.go +++ b/pkg/handler/certmanager/interface.go @@ -25,3 +25,16 @@ type Cert interface { // Write a .pem file with the private key WritePrivateKey(w io.Writer) error } + +type CertJar interface { + server.Task + + // Return all certificates + List() []Cert + + // Create a new certificate + Write(Cert) error + + // Delete a certificate + Delete(Cert) error +} From 40be27b15f0b2f569898f954eb2664d295a2db98 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 11:35:17 +0200 Subject: [PATCH 12/27] Updated certmanager --- cmd/nginx-server/main.go | 26 ++- pkg/handler/certmanager/cert/cert.go | 209 ++++++++++------- pkg/handler/certmanager/cert/cert_test.go | 50 ++--- pkg/handler/certmanager/certmanager.go | 136 +++++++++++ .../certmanager/certstore/certstore.go | 212 ++++++++++++++++++ .../certmanager/certstore/certstore_test.go | 102 +++++++++ pkg/handler/certmanager/certstore/config.go | 99 ++++++++ pkg/handler/certmanager/certstore/doc.go | 4 + pkg/handler/certmanager/certstore/task.go | 34 +++ pkg/handler/certmanager/config.go | 55 ++++- pkg/handler/certmanager/endpoints.go | 50 +++++ pkg/handler/certmanager/interface.go | 39 +++- pkg/handler/certmanager/scope.go | 33 +++ pkg/handler/certmanager/task.go | 34 +++ 14 files changed, 956 insertions(+), 127 deletions(-) create mode 100644 pkg/handler/certmanager/certmanager.go create mode 100644 pkg/handler/certmanager/certstore/certstore.go create mode 100644 pkg/handler/certmanager/certstore/certstore_test.go create mode 100644 pkg/handler/certmanager/certstore/config.go create mode 100644 pkg/handler/certmanager/certstore/doc.go create mode 100644 pkg/handler/certmanager/certstore/task.go create mode 100644 pkg/handler/certmanager/endpoints.go create mode 100644 pkg/handler/certmanager/scope.go create mode 100644 pkg/handler/certmanager/task.go diff --git a/cmd/nginx-server/main.go b/cmd/nginx-server/main.go index adfbd2b..493b1f5 100644 --- a/cmd/nginx-server/main.go +++ b/cmd/nginx-server/main.go @@ -13,6 +13,8 @@ import ( server "github.com/mutablelogic/go-server" ctx "github.com/mutablelogic/go-server/pkg/context" auth "github.com/mutablelogic/go-server/pkg/handler/auth" + certmanager "github.com/mutablelogic/go-server/pkg/handler/certmanager" + certstore "github.com/mutablelogic/go-server/pkg/handler/certmanager/certstore" logger "github.com/mutablelogic/go-server/pkg/handler/logger" nginx "github.com/mutablelogic/go-server/pkg/handler/nginx" router "github.com/mutablelogic/go-server/pkg/handler/router" @@ -68,6 +70,21 @@ func main() { log.Fatal(err) } + // Cert Storage + certstore, err := certstore.Config{ + DataPath: filepath.Join(n.(nginx.Nginx).Config(), "cert"), + Group: *group, + }.New() + if err != nil { + log.Fatal(err) + } + certmanager, err := certmanager.Config{ + CertStorage: certstore.(certmanager.CertStorage), + }.New() + if err != nil { + log.Fatal(err) + } + // Location of the FCGI unix socket socket := filepath.Join(n.(nginx.Nginx).Config(), "run/go-server.sock") @@ -88,6 +105,13 @@ func main() { auth.(server.Middleware), }, }, + "cert": { // /api/cert/... + Service: certmanager.(server.ServiceEndpoints), + Middleware: []server.Middleware{ + logger.(server.Middleware), + auth.(server.Middleware), + }, + }, }, }.New() if err != nil { @@ -105,7 +129,7 @@ func main() { } // Run until we receive an interrupt - provider := provider.NewProvider(logger, n, jar, auth, router, httpserver) + provider := provider.NewProvider(logger, n, jar, auth, certstore, certmanager, router, httpserver) provider.Print(ctx, "Press CTRL+C to exit") if err := provider.Run(ctx); err != nil { log.Fatal(err) diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index b3bfa9f..cc0fa1c 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -19,7 +19,6 @@ import ( "time" // Packages - "github.com/mutablelogic/go-server/pkg/handler/certmanager" // Namespace imports . "github.com/djthorpe/go-errors" @@ -37,9 +36,6 @@ type Cert struct { type keyType int -// Ensure that Cert implements the certmanager.Cert interface -var _ certmanager.Cert = (*Cert)(nil) - /////////////////////////////////////////////////////////////////////////////// // GLOBALS @@ -67,9 +63,8 @@ var ( /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// Create a new certificate authority with the given configuration -// and options -func NewCA(c certmanager.Config, opt ...Opt) (*Cert, error) { +// Create a new certificate authority with the given options +func NewCA(commonName string, opt ...Opt) (*Cert, error) { var o opts // Set defaults @@ -92,9 +87,23 @@ func NewCA(c certmanager.Config, opt ...Opt) (*Cert, error) { } // Create a new certificate with a template - template := x509TemplateFor(c, o, serial) - template.IsCA = true - template.KeyUsage |= x509.KeyUsageCertSign + template := &x509.Certificate{ + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(o.Years, o.Months, o.Days), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{}, + BasicConstraintsValid: true, + } + + // Set subject + if o.Name != nil { + template.Subject = *o.Name + } else { + template.Subject = pkix.Name{} + } + template.Subject.CommonName = commonName // Generate public, private keys publicKey, privateKey, err := generateKey(o.KeyType) @@ -125,8 +134,7 @@ func NewCA(c certmanager.Config, opt ...Opt) (*Cert, error) { // Create a new certificate, either self-signed (if ca is nil) or // signed by the certificate authority with the given options -// TODO: Add self-signing ability -func NewCert(ca *Cert, opt ...Opt) (*Cert, error) { +func NewCert(commonName string, ca *Cert, opt ...Opt) (*Cert, error) { var o opts // Set defaults @@ -148,48 +156,76 @@ func NewCert(ca *Cert, opt ...Opt) (*Cert, error) { return nil, ErrInternalAppError.With("SerialNumber") } - parent, err := x509.ParseCertificate(ca.data) - if err != nil { - return nil, err + // Parse the CA certificate + var parent *x509.Certificate + if ca != nil { + var err error + parent, err = x509.ParseCertificate(ca.data) + if err != nil { + return nil, err + } + if !parent.IsCA { + return nil, ErrBadParameter.With("Invalid CA certificate") + } + if _, err := parent.Verify(x509.VerifyOptions{}); err != nil { + return nil, err + } } - template, err := x509.ParseCertificate(ca.data) - if err != nil { - return nil, err + + template := &x509.Certificate{ + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(o.Years, o.Months, o.Days), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, } + + // Set subject if o.Name != nil { template.Subject = *o.Name + } else if parent != nil { + template.Subject = parent.Subject } + + // Set common name + template.Subject.CommonName = commonName + + // Set IP Addresses and DNS Names if len(o.IPAddresses) > 0 { template.IPAddresses = o.IPAddresses } if len(o.DNSNames) > 0 { template.DNSNames = o.DNSNames } - template.SerialNumber = serial - template.NotBefore = time.Now() - template.NotAfter = time.Now().AddDate(o.Years, o.Months, o.Days) - template.IsCA = false - template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature - // KeyUsage bits set in the x509.Certificate template - template.KeyUsage = x509.KeyUsageDigitalSignature + + // Generate public, private keys + publicKey, privateKey, err := generateKey(o.KeyType) + if err != nil { + return nil, err + } + + // Who is going to sign the certificate? + signer, signerPrivateKey := template, privateKey + if parent != nil { + signer, signerPrivateKey = parent, ca.privateKey + } // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In // the context of TLS this KeyUsage is particular to RSA key exchange and // authentication. - if _, isRSA := ca.privateKey.(*rsa.PrivateKey); isRSA { + if _, isRSA := signerPrivateKey.(*rsa.PrivateKey); isRSA { template.KeyUsage |= x509.KeyUsageKeyEncipherment } - // Generate public, private keys - publicKey, privateKey, err := generateKey(o.KeyType) - if err != nil { - return nil, err + // Set authority key id + if parent != nil { + template.AuthorityKeyId = parent.SubjectKeyId } - // Create cert signed by the CA + // Create cert signed by the CA or self cert := new(Cert) - data, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, ca.privateKey) + data, err := x509.CreateCertificate(rand.Reader, template, signer, publicKey, signerPrivateKey) if err != nil { return nil, err } else { @@ -233,23 +269,27 @@ func (c *Cert) String() string { return fmt.Sprintf("{ %q: %q }", "error", err.Error()) } v := struct { - KeyType string `json:"key_type"` - Serial string `json:"serial"` - Subject string `json:"subject"` - IsCA bool `json:"is_ca,omitempty"` - NotBefore time.Time `json:"not_before"` - NotAfter time.Time `json:"not_after"` - IPAddresses []net.IP `json:"ip_addresses,omitempty"` - DNSNames []string `json:"dns_names,omitempty"` + KeyType string `json:"key_type"` + Serial string `json:"serial"` + Subject string `json:"subject"` + IsCA bool `json:"is_ca,omitempty"` + NotBefore time.Time `json:"not_before"` + NotAfter time.Time `json:"not_after"` + IPAddresses []net.IP `json:"ip_addresses,omitempty"` + DNSNames []string `json:"dns_names,omitempty"` + SubjectKeyId []byte `json:"subject_key_id,omitempty"` + AuthorityKeyId []byte `json:"authority_key_id,omitempty"` }{ - KeyType: c.KeyType(), - Serial: cert.SerialNumber.String(), - Subject: cert.Subject.String(), - IsCA: cert.IsCA, - NotBefore: cert.NotBefore, - NotAfter: cert.NotAfter, - IPAddresses: cert.IPAddresses, - DNSNames: cert.DNSNames, + KeyType: c.KeyType(), + Serial: cert.SerialNumber.String(), + Subject: cert.Subject.String(), + IsCA: cert.IsCA, + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + IPAddresses: cert.IPAddresses, + DNSNames: cert.DNSNames, + SubjectKeyId: cert.SubjectKeyId, + AuthorityKeyId: cert.AuthorityKeyId, } data, _ := json.MarshalIndent(v, "", " ") return string(data) @@ -283,6 +323,50 @@ func (c *Cert) KeyType() string { } } +func (c *Cert) IsCA() bool { + cert, err := x509.ParseCertificate(c.data) + if err != nil { + return false + } + return cert.IsCA +} + +func (c *Cert) Serial() string { + cert, err := x509.ParseCertificate(c.data) + if err != nil { + return "" + } + return cert.SerialNumber.String() +} + +func (c *Cert) Subject() string { + cert, err := x509.ParseCertificate(c.data) + if err != nil { + return "" + } + return cert.Subject.CommonName +} + +func (c *Cert) Expires() time.Time { + if cert, err := x509.ParseCertificate(c.data); err != nil { + return time.Time{} + } else { + return cert.NotAfter + } +} + +func (c *Cert) IsValid() error { + cert, err := x509.ParseCertificate(c.data) + if err != nil { + return err + } + if _, err := cert.Verify(x509.VerifyOptions{}); err != nil { + return err + } + // Return success + return nil +} + // Write a .pem file with the certificate func (c *Cert) WriteCertificate(w io.Writer) error { return pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: c.data}) @@ -303,33 +387,6 @@ func (c *Cert) WritePrivateKey(w io.Writer) error { /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS -func x509TemplateFor(c certmanager.Config, o opts, serial *big.Int) *x509.Certificate { - template := &x509.Certificate{ - SerialNumber: serial, - Subject: pkix.Name{ - Organization: []string{c.X509Name.Organization}, - OrganizationalUnit: []string{c.X509Name.OrganizationalUnit}, - Country: []string{c.X509Name.Country}, - Locality: []string{c.X509Name.Locality}, - Province: []string{c.X509Name.Province}, - StreetAddress: []string{c.X509Name.StreetAddress}, - PostalCode: []string{c.X509Name.PostalCode}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(o.Years, o.Months, o.Days), - ExtKeyUsage: []x509.ExtKeyUsage{}, - BasicConstraintsValid: true, - } - - // Set X509 Name - if o.Name != nil { - template.Subject = *o.Name - } - - // Return the template - return template -} - // ECDSA curve to use to generate a key. Valid values are P224, P256 (default), P384, P521 // If empty, RSA keys will be generated instead func generateKey(t keyType) (any, any, error) { diff --git a/pkg/handler/certmanager/cert/cert_test.go b/pkg/handler/certmanager/cert/cert_test.go index 4b4300c..3e4f709 100644 --- a/pkg/handler/certmanager/cert/cert_test.go +++ b/pkg/handler/certmanager/cert/cert_test.go @@ -2,9 +2,9 @@ package cert_test import ( "bytes" + "crypto/x509/pkix" "testing" - "github.com/mutablelogic/go-server/pkg/handler/certmanager" "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_Cert_001(t *testing.T) { func Test_Cert_002(t *testing.T) { assert := assert.New(t) - cert, err := cert.NewCA(certmanager.Config{}) + cert, err := cert.NewCA(t.Name()) assert.NoError(err) assert.NotNil(cert) t.Log(cert) @@ -28,7 +28,7 @@ func Test_Cert_002(t *testing.T) { func Test_Cert_003(t *testing.T) { assert := assert.New(t) - cert, err := cert.NewCA(certmanager.Config{}) + cert, err := cert.NewCA(t.Name()) assert.NoError(err) public := new(bytes.Buffer) @@ -46,43 +46,29 @@ func Test_Cert_003(t *testing.T) { func Test_Cert_004(t *testing.T) { assert := assert.New(t) - ca, err := cert.NewCA(certmanager.Config{ - X509Name: certmanager.X509Name{ - OrganizationalUnit: "Test", - Organization: "Test", - Country: "DE", - }, - }) + ca, err := cert.NewCA(t.Name(), cert.OptX509Name(pkix.Name{ + Organization: []string{"Test"}, + Country: []string{"DE"}, + })) assert.NoError(err) - t.Log(ca) + t.Log("ca=", ca) - cert, err := cert.NewCert(ca, cert.OptKeyType("P224")) + cert, err := cert.NewCert(t.Name(), ca, cert.OptKeyType("P224")) assert.NoError(err) assert.NotNil(cert) - t.Log(cert) + t.Log("cert=", cert) } -func Test_Cert_005(t *testing.T) { +func Test_Cert_006(t *testing.T) { assert := assert.New(t) - ca, err := cert.NewCA(certmanager.Config{ - X509Name: certmanager.X509Name{ - OrganizationalUnit: "Test", - Organization: "Test", - Country: "DE", - }, - }) - assert.NoError(err) - - both := new(bytes.Buffer) - err = ca.WriteCertificate(both) + // Self-signed certificate + cert, err := cert.NewCert(t.Name(), nil, cert.OptX509Name(pkix.Name{ + Organization: []string{"Test"}, + Country: []string{"DE"}, + })) assert.NoError(err) - err = ca.WritePrivateKey(both) - assert.NoError(err) - - cert2, err := cert.NewFromBytes(both.Bytes()) - assert.NoError(err) - - t.Log(cert2) + assert.NotNil(cert) + t.Log(cert) } diff --git a/pkg/handler/certmanager/certmanager.go b/pkg/handler/certmanager/certmanager.go new file mode 100644 index 0000000..fdada13 --- /dev/null +++ b/pkg/handler/certmanager/certmanager.go @@ -0,0 +1,136 @@ +package certmanager + +import ( // Packages + // Namespace imports + "crypto/x509/pkix" + + . "github.com/djthorpe/go-errors" + "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type certmanager struct { + name X509Name + store CertStorage +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new auth task from the configuration +func New(c Config) (*certmanager, error) { + task := new(certmanager) + task.name = c.X509Name + + // Set storage for certificates + if c.CertStorage == nil { + return nil, ErrInternalAppError.With("missing 'CertStorage'") + } else { + task.store = c.CertStorage + } + + // Return success + return task, nil +} + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// List all certificates +func (task *certmanager) List() []Cert { + certs, err := task.store.List() + if err != nil { + return nil + } + return certs +} + +// Return a certificate by serial number +func (task *certmanager) Read(serial string) (Cert, error) { + return task.store.Read(serial) +} + +// Delete a certificate +func (task *certmanager) Delete(cert Cert) error { + return task.store.Delete(cert) +} + +// Create a new Certificate Authority +func (task *certmanager) CreateCA(commonName string, opts ...cert.Opt) (Cert, error) { + // Default options + o := []cert.Opt{ + cert.OptX509Name(pkix.Name{ + OrganizationalUnit: []string{task.name.OrganizationalUnit}, + Organization: []string{task.name.Organization}, + Locality: []string{task.name.Locality}, + Province: []string{task.name.Province}, + Country: []string{task.name.Country}, + StreetAddress: []string{task.name.StreetAddress}, + PostalCode: []string{task.name.PostalCode}, + }), + } + + // Create the certificate and store it + cert, err := cert.NewCA(commonName, append(o, opts...)...) + if err != nil { + return nil, err + } else if err := task.store.Write(cert); err != nil { + return nil, err + } + + // Return success + return cert, nil +} + +// Create a new signed certificate. If ca is nil, the certificate is self-signed +func (task *certmanager) CreateSignedCert(commonName string, ca Cert, opts ...cert.Opt) (Cert, error) { + // Default options + o := []cert.Opt{ + cert.OptX509Name(pkix.Name{ + OrganizationalUnit: []string{task.name.OrganizationalUnit}, + Organization: []string{task.name.Organization}, + Locality: []string{task.name.Locality}, + Province: []string{task.name.Province}, + Country: []string{task.name.Country}, + StreetAddress: []string{task.name.StreetAddress}, + PostalCode: []string{task.name.PostalCode}, + }), + } + + // We should make the ca "concrete" by reading it + if ca != nil { + var err error + ca, err = task.store.Read(ca.Serial()) + if err != nil { + return nil, err + } + } + + // Check for valid CA + if ca != nil { + if !ca.IsCA() { + return nil, ErrBadParameter.With("Cannot sign without a valid CA") + } + if err := ca.IsValid(); err != nil { + return nil, err + } + } + + // Append KeyType to options + if ca != nil { + o = append(o, cert.OptKeyType(ca.KeyType())) + } + + // Create the certificate and store it + cert, err := cert.NewCert(commonName, ca.(*cert.Cert), append(o, opts...)...) + if err != nil { + return nil, err + } else if err := task.store.Write(cert); err != nil { + return nil, err + } + + // Return success + return cert, nil +} diff --git a/pkg/handler/certmanager/certstore/certstore.go b/pkg/handler/certmanager/certstore/certstore.go new file mode 100644 index 0000000..25fb2af --- /dev/null +++ b/pkg/handler/certmanager/certstore/certstore.go @@ -0,0 +1,212 @@ +package certstore + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/certmanager" + "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +// CertStore represents a certificate store +type certstore struct { + // Root of the file storage + dataPath string + + // Group for certificates on the file system + fileGroup int + + // Mode for files + fileMode os.FileMode +} + +// Check interfaces are satisfied +var _ certmanager.CertStorage = (*certstore)(nil) + +//////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func New(c Config) (*certstore, error) { + task := new(certstore) + + // Make data directory, set permissions + if c.DataPath == "" { + return nil, ErrBadParameter.With("missing 'data'") + } else if gid, err := c.GroupId(); err != nil { + return nil, err + } else if err := os.MkdirAll(c.DataPath, c.DirMode()); err != nil { + return nil, err + } else if err := os.Chown(c.DataPath, -1, gid); err != nil { + return nil, err + } else if err := isWritableFileAtPath(c.DataPath); err != nil { + return nil, ErrBadParameter.With("not writable: ", c.DataPath) + } else { + task.dataPath = c.DataPath + task.fileGroup = gid + task.fileMode = c.FileMode() + } + + // Return success + return task, nil +} + +//////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return all certificates +func (c *certstore) List() ([]certmanager.Cert, error) { + // Read entries, silently ignore errors + entries, err := os.ReadDir(c.dataPath) + if err != nil { + return nil, err + } + + var result error + certs := make([]certmanager.Cert, 0, len(entries)) + for _, entry := range entries { + // Check for valid certificates + if entry.IsDir() { + continue + } + if filepath.Ext(entry.Name()) != certExt { + continue + } + if strings.HasPrefix(entry.Name(), ".") { + continue + } + if isReadableFileAtPath(filepath.Join(c.dataPath, entry.Name())) != nil { + continue + } + + // Read certificates and accumulate any errors + serial := strings.TrimSuffix(entry.Name(), certExt) + if cert, err := c.Read(strings.TrimSuffix(entry.Name(), certExt)); err != nil { + result = errors.Join(result, fmt.Errorf("%q: %w", serial, err)) + } else if cert.Serial() != serial { + result = errors.Join(result, ErrBadParameter.With("serial mismatch")) + } else { + certs = append(certs, cert) + } + } + + // Return certificates and any errors + return certs, result +} + +// Read a certificate +func (c *certstore) Read(serial string) (certmanager.Cert, error) { + // Check for file + pathForCert, err := c.pathForKey(serial) + if err != nil { + return nil, err + } + + // Open file + fh, err := os.Open(pathForCert) + if err != nil { + return nil, err + } + defer fh.Close() + + // Read data + data, err := io.ReadAll(fh) + if err != nil { + return nil, err + } + + // Parse certificate + return cert.NewFromBytes(data) +} + +// Create a new certificate +func (c *certstore) Write(cert certmanager.Cert) error { + pathForCert, err := c.pathForKey(cert.Serial()) + if !errors.Is(err, os.ErrNotExist) { + if err == nil { + return ErrDuplicateEntry.With(cert.Serial()) + } else { + return err + } + } + + // Create the certificate + fh, err := os.Create(pathForCert) + if err != nil { + return err + } + defer fh.Close() + + // Write the certificate, set mode and group + if err := cert.WriteCertificate(fh); err != nil { + return errors.Join(err, os.Remove(pathForCert)) + } else if err := cert.WritePrivateKey(fh); err != nil { + return errors.Join(err, os.Remove(pathForCert)) + } else if err := os.Chown(pathForCert, -1, c.fileGroup); err != nil { + return errors.Join(err, os.Remove(pathForCert)) + } else if err := os.Chmod(pathForCert, c.fileMode); err != nil { + return errors.Join(err, os.Remove(pathForCert)) + } + + // Return success + return nil +} + +// Delete a certificate +func (c *certstore) Delete(cert certmanager.Cert) error { + pathForCert, err := c.pathForKey(cert.Serial()) + + // Silently ignore "not exist" errors + if errors.Is(err, os.ErrNotExist) { + return nil + } else if err != nil { + return err + } + + return os.Remove(pathForCert) +} + +//////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +// Returns the path for a certificate, and a boolean value which indicates +// if the folder exists +func (c *certstore) pathForKey(serial string) (string, error) { + path := filepath.Join(c.dataPath, serial+certExt) + if info, err := os.Stat(path); err != nil { + return path, err + } else if !info.Mode().IsRegular() { + return path, ErrBadParameter.With("not a regular file: ", path) + } else { + return path, nil + } +} + +// Returns boolean value which indicates if a file is readable by current +// user +func isReadableFileAtPath(path string) error { + return syscall.Access(path, R_OK) +} + +// Returns boolean value which indicates if a file is writable by current +// user +func isWritableFileAtPath(path string) error { + return syscall.Access(path, W_OK) +} + +// Returns boolean value which indicates if a file is executable by current +// user +func isExecutableFileAtPath(path string) error { + return syscall.Access(path, X_OK) +} diff --git a/pkg/handler/certmanager/certstore/certstore_test.go b/pkg/handler/certmanager/certstore/certstore_test.go new file mode 100644 index 0000000..deb5d18 --- /dev/null +++ b/pkg/handler/certmanager/certstore/certstore_test.go @@ -0,0 +1,102 @@ +package certstore_test + +import ( + "os/user" + "path/filepath" + "testing" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" + "github.com/mutablelogic/go-server/pkg/handler/certmanager/certstore" + "github.com/stretchr/testify/assert" +) + +func Test_certstore_001(t *testing.T) { + assert := assert.New(t) + + // What is my group + u, err := user.Current() + if !assert.NoError(err) { + t.SkipNow() + } + + store, err := certstore.New(certstore.Config{ + DataPath: filepath.Join(t.TempDir(), "a/b/c"), + Group: u.Gid, + }) + if !assert.NoError(err) { + t.SkipNow() + } + assert.NotNil(store) +} + +func Test_certstore_002(t *testing.T) { + assert := assert.New(t) + + store, err := certstore.New(certstore.Config{ + DataPath: t.TempDir(), + }) + if !assert.NoError(err) { + t.SkipNow() + } + + // Create a new certificate + cert, err := cert.NewCA(t.Name()) + if !assert.NoError(err) { + t.SkipNow() + } + + t.Log(cert) + + // Store the CA + if err := store.Write(cert); !assert.NoError(err) { + t.SkipNow() + } + + // Read the CA + if _, err := store.Read(cert.Serial()); !assert.NoError(err) { + t.SkipNow() + } + + // Delete the CA + if err := store.Delete(cert); !assert.NoError(err) { + t.SkipNow() + } +} + +func Test_certstore_003(t *testing.T) { + assert := assert.New(t) + + // What is my group + u, err := user.Current() + if !assert.NoError(err) { + t.SkipNow() + } + + store, err := certstore.New(certstore.Config{ + DataPath: t.TempDir(), + Group: u.Gid, + }) + if !assert.NoError(err) { + t.SkipNow() + } + + // Create 10 CA's + for i := 0; i < 10; i++ { + cert, err := cert.NewCA(t.Name()) + if !assert.NoError(err) { + t.SkipNow() + } + if err := store.Write(cert); !assert.NoError(err) { + t.SkipNow() + } + } + + // List all CA's + certs, err := store.List() + if !assert.NoError(err) { + t.SkipNow() + } + + t.Log(certs) +} diff --git a/pkg/handler/certmanager/certstore/config.go b/pkg/handler/certmanager/certstore/config.go new file mode 100644 index 0000000..109504c --- /dev/null +++ b/pkg/handler/certmanager/certstore/config.go @@ -0,0 +1,99 @@ +package certstore + +import ( + // Packages + "os" + "os/user" + "strconv" + + server "github.com/mutablelogic/go-server" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +type Config struct { + DataPath string `hcl:"data" description:"Data directory for certificates"` + Group string `hcl:"group" description:"Group ID for data directory"` +} + +// Check interfaces are satisfied +var _ server.Plugin = Config{} + +//////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + defaultName = "certstore" + defaultDirMode = os.FileMode(0700) + defaultFileMode = os.FileMode(0600) + groupDirMode = os.FileMode(0050) + groupFileMode = os.FileMode(0060) + allDirMode = os.FileMode(0777) + allFileMode = os.FileMode(0666) + certExt = ".pem" +) + +const ( + R_OK = 4 + W_OK = 2 + X_OK = 1 +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Name returns the name of the service +func (Config) Name() string { + return defaultName +} + +// Description returns the description of the service +func (Config) Description() string { + return "file-based storage for certificates and certificate authorities" +} + +// Create a new task from the configuration +func (c Config) New() (server.Task, error) { + return New(c) +} + +// Return the gid of the group +func (c Config) GroupId() (int, error) { + // No group change + if c.Group == "" { + return -1, nil + } + + // Check for numerical group ID + if gid, err := strconv.ParseUint(c.Group, 0, 32); err == nil { + return int(gid), nil + } + + // Check for group name + if group, err := user.LookupGroup(c.Group); err != nil { + return -1, err + } else if gid, err := strconv.ParseUint(group.Gid, 0, 32); err != nil { + return -1, err + } else { + return int(gid), nil + } +} + +// Return the dir mode for the data directory +func (c Config) DirMode() os.FileMode { + dirMode := defaultDirMode + if gid, err := c.GroupId(); gid != -1 && err == nil { + dirMode |= groupDirMode + } + return (dirMode & allDirMode) +} + +// Return the file mode for the data directory +func (c Config) FileMode() os.FileMode { + dirMode := defaultFileMode + if gid, err := c.GroupId(); gid != -1 && err == nil { + dirMode |= groupFileMode + } + return (dirMode & allFileMode) +} diff --git a/pkg/handler/certmanager/certstore/doc.go b/pkg/handler/certmanager/certstore/doc.go new file mode 100644 index 0000000..63b236e --- /dev/null +++ b/pkg/handler/certmanager/certstore/doc.go @@ -0,0 +1,4 @@ +/* +certstore implements file-based storage for certificates and certificate authorities. +*/ +package certstore diff --git a/pkg/handler/certmanager/certstore/task.go b/pkg/handler/certmanager/certstore/task.go new file mode 100644 index 0000000..b311608 --- /dev/null +++ b/pkg/handler/certmanager/certstore/task.go @@ -0,0 +1,34 @@ +package certstore + +import ( + "context" + + // Packages + server "github.com/mutablelogic/go-server" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Check interfaces are satisfied +var _ server.Task = (*certstore)(nil) + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the label +func (task *certstore) Label() string { + // TODO + return defaultName +} + +// Run the task until the context is cancelled +func (task *certstore) Run(ctx context.Context) error { + var result error + + // Run the task until cancelled + <-ctx.Done() + + // Return any errors + return result +} diff --git a/pkg/handler/certmanager/config.go b/pkg/handler/certmanager/config.go index d819104..e4d6006 100644 --- a/pkg/handler/certmanager/config.go +++ b/pkg/handler/certmanager/config.go @@ -1,15 +1,52 @@ package certmanager +import ( + // Packages + server "github.com/mutablelogic/go-server" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +type Config struct { + X509Name `hcl:"x509_name" description:"X509 name for certificate"` + CertStorage CertStorage `hcl:"cert_storage" description:"Certificate storage"` +} + type X509Name struct { - Organization string `json:"organization"` - OrganizationalUnit string `json:"organizational_unit,omitempty"` - Country string `json:"country,omitempty"` - Province string `json:"province,omitempty"` - Locality string `json:"locality,omitempty"` - StreetAddress string `json:"street_address,omitempty"` - PostalCode string `json:"postal_code,omitempty"` + OrganizationalUnit string `hcl:"organizational_unit,omitempty" description:"X509 Organizational Unit"` + Organization string `hcl:"organization" description:"X509 Organization"` + Locality string `hcl:"locality,omitempty" description:"X509 Locality"` + Province string `hcl:"province,omitempty" description:"X509 Province"` + Country string `hcl:"country,omitempty" description:"X509 Country"` + StreetAddress string `hcl:"street_address,omitempty" description:"X509 Street Address"` + PostalCode string `hcl:"postal_code,omitempty" description:"X509 Postal Code"` } -type Config struct { - X509Name `json:"x509_name"` +// Check interfaces are satisfied +var _ server.Plugin = Config{} + +//////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + defaultName = "certmanager" +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Name returns the name of the service +func (Config) Name() string { + return defaultName +} + +// Description returns the description of the service +func (Config) Description() string { + return "certfiicate manager service" +} + +// Create a new task from the configuration +func (c Config) New() (server.Task, error) { + return New(c) } diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go new file mode 100644 index 0000000..be947ea --- /dev/null +++ b/pkg/handler/certmanager/endpoints.go @@ -0,0 +1,50 @@ +package certmanager + +import ( + "context" + "net/http" + "regexp" + + // Packages + server "github.com/mutablelogic/go-server" + router "github.com/mutablelogic/go-server/pkg/handler/router" + httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Check interfaces are satisfied +var _ server.ServiceEndpoints = (*certmanager)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + jsonIndent = 2 +) + +var ( + reRoot = regexp.MustCompile(`^/?$`) +) + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - ENDPOINTS + +// Add endpoints to the router +func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { + // Path: / + // Methods: GET + // Scopes: read + // Description: Return all existing certificates + r.AddHandlerFuncRe(ctx, reRoot, service.ListCerts, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Get all certificates +func (service *certmanager) ListCerts(w http.ResponseWriter, r *http.Request) { + httpresponse.JSON(w, service.List(), http.StatusOK, jsonIndent) +} diff --git a/pkg/handler/certmanager/interface.go b/pkg/handler/certmanager/interface.go index e04ba64..0062051 100644 --- a/pkg/handler/certmanager/interface.go +++ b/pkg/handler/certmanager/interface.go @@ -1,6 +1,13 @@ package certmanager -import "io" +import ( + "io" + "time" + + // Packages + server "github.com/mutablelogic/go-server" + cert "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" +) // Cert interface represents a certificate or certificate authority type Cert interface { @@ -10,8 +17,13 @@ type Cert interface { // Return the subject of the certificate Subject() string - // Return true if the certificate is valid - IsValid() bool + // Return ErrExpired if the certificate has expired, + // or nil if the certificate is valid. Other error returns + // indicate other problems with the certificate + IsValid() error + + // Return the expiry date of the certificate + Expires() time.Time // Return true if the certificate is a CA IsCA() bool @@ -26,15 +38,24 @@ type Cert interface { WritePrivateKey(w io.Writer) error } -type CertJar interface { - server.Task - - // Return all certificates - List() []Cert +// CertStorage interface represents a storage for certificates +type CertStorage interface { + server.Task + + // Return all certificates. This may not return the certificates + // themselves, but the metadata for the certificates. Use Read + // to get the certificate itself + List() ([]Cert, error) - // Create a new certificate + // Read a certificate by serial number + Read(string) (Cert, error) + + // Write a certificate Write(Cert) error // Delete a certificate Delete(Cert) error } + +// Ensure that Cert implements the certmanager.Cert interface +var _ Cert = (*cert.Cert)(nil) diff --git a/pkg/handler/certmanager/scope.go b/pkg/handler/certmanager/scope.go new file mode 100644 index 0000000..900a82d --- /dev/null +++ b/pkg/handler/certmanager/scope.go @@ -0,0 +1,33 @@ +package certmanager + +import ( + // Packages + "github.com/mutablelogic/go-server/pkg/version" +) + +//////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +var ( + // Prefix + scopePrefix = version.GitSource + "/scope/" +) + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (service *certmanager) ScopeRead() []string { + // Return read (list, get) scopes + return []string{ + scopePrefix + service.Label() + "/read", + scopePrefix + defaultName + "/read", + } +} + +func (service *certmanager) ScopeWrite() []string { + // Return write (create, delete) scopes + return []string{ + scopePrefix + service.Label() + "/write", + scopePrefix + defaultName + "/write", + } +} diff --git a/pkg/handler/certmanager/task.go b/pkg/handler/certmanager/task.go new file mode 100644 index 0000000..4646ba3 --- /dev/null +++ b/pkg/handler/certmanager/task.go @@ -0,0 +1,34 @@ +package certmanager + +import ( + "context" + + // Packages + server "github.com/mutablelogic/go-server" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Check interfaces are satisfied +var _ server.Task = (*certmanager)(nil) + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the label +func (task *certmanager) Label() string { + // TODO + return defaultName +} + +// Run the task until the context is cancelled +func (task *certmanager) Run(ctx context.Context) error { + var result error + + // Run the task until cancelled + <-ctx.Done() + + // Return any errors + return result +} From 6a4e4c26116c23ee65dab18d68ea42e51cea7a7d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 11:44:43 +0200 Subject: [PATCH 13/27] Added CreateCA endpoint --- pkg/handler/certmanager/endpoints.go | 36 ++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index be947ea..3dc2a74 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -8,12 +8,17 @@ import ( // Packages server "github.com/mutablelogic/go-server" router "github.com/mutablelogic/go-server/pkg/handler/router" + "github.com/mutablelogic/go-server/pkg/httprequest" httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse" ) /////////////////////////////////////////////////////////////////////////////// // TYPES +type reqCreateCA struct { + CommonName string `json:"name"` +} + // Check interfaces are satisfied var _ server.ServiceEndpoints = (*certmanager)(nil) @@ -26,6 +31,7 @@ const ( var ( reRoot = regexp.MustCompile(`^/?$`) + reCA = regexp.MustCompile(`^/ca/?$`) ) /////////////////////////////////////////////////////////////////////////////// @@ -37,14 +43,40 @@ func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { // Methods: GET // Scopes: read // Description: Return all existing certificates - r.AddHandlerFuncRe(ctx, reRoot, service.ListCerts, http.MethodGet).(router.Route). + r.AddHandlerFuncRe(ctx, reRoot, service.reqListCerts, http.MethodGet).(router.Route). SetScope(service.ScopeRead()...) + + // Path: /ca + // Methods: POST + // Scopes: write + // Description: Create a new certificate authority + r.AddHandlerFuncRe(ctx, reCA, service.reqCreateCA, http.MethodPost).(router.Route). + SetScope(service.ScopeWrite()...) + } /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS // Get all certificates -func (service *certmanager) ListCerts(w http.ResponseWriter, r *http.Request) { +func (service *certmanager) reqListCerts(w http.ResponseWriter, r *http.Request) { httpresponse.JSON(w, service.List(), http.StatusOK, jsonIndent) } + +// Create a new certificate authority +func (service *certmanager) reqCreateCA(w http.ResponseWriter, r *http.Request) { + var req reqCreateCA + + // Get the request + if err := httprequest.Read(r, &req); err != nil { + httpresponse.Error(w, http.StatusBadRequest, err.Error()) + return + } + + // Create the CA + if ca, err := service.CreateCA(req.CommonName); err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + } else { + httpresponse.JSON(w, ca, http.StatusOK, jsonIndent) + } +} From 8417ef6daae3715b35abf09348f07aa8930afb87 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 11:56:23 +0200 Subject: [PATCH 14/27] Added JSON response for a cert --- pkg/handler/certmanager/cert/cert.go | 51 ++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index cc0fa1c..2d1fe33 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -263,15 +263,11 @@ func NewFromBytes(data []byte) (*Cert, error) { /////////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (c *Cert) String() string { - cert, err := x509.ParseCertificate(c.data) - if err != nil { - return fmt.Sprintf("{ %q: %q }", "error", err.Error()) - } - v := struct { - KeyType string `json:"key_type"` +func (c *Cert) MarshalJSON() ([]byte, error) { + type resp struct { Serial string `json:"serial"` - Subject string `json:"subject"` + KeyType string `json:"key_type"` + CommonName string `json:"name"` IsCA bool `json:"is_ca,omitempty"` NotBefore time.Time `json:"not_before"` NotAfter time.Time `json:"not_after"` @@ -279,19 +275,32 @@ func (c *Cert) String() string { DNSNames []string `json:"dns_names,omitempty"` SubjectKeyId []byte `json:"subject_key_id,omitempty"` AuthorityKeyId []byte `json:"authority_key_id,omitempty"` - }{ - KeyType: c.KeyType(), - Serial: cert.SerialNumber.String(), - Subject: cert.Subject.String(), - IsCA: cert.IsCA, - NotBefore: cert.NotBefore, - NotAfter: cert.NotAfter, - IPAddresses: cert.IPAddresses, - DNSNames: cert.DNSNames, - SubjectKeyId: cert.SubjectKeyId, - AuthorityKeyId: cert.AuthorityKeyId, - } - data, _ := json.MarshalIndent(v, "", " ") + } + type error struct { + Error string `json:"error"` + } + + cert, err := x509.ParseCertificate(c.data) + if err != nil { + return json.Marshal(error{Error: err.Error()}) + } else { + return json.Marshal(resp{ + Serial: cert.SerialNumber.String(), + KeyType: c.KeyType(), + CommonName: cert.Subject.CommonName, + IsCA: cert.IsCA, + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + IPAddresses: cert.IPAddresses, + DNSNames: cert.DNSNames, + SubjectKeyId: cert.SubjectKeyId, + AuthorityKeyId: cert.AuthorityKeyId, + }) + } +} + +func (c *Cert) String() string { + data, _ := json.MarshalIndent(c, "", " ") return string(data) } From 553d36514d01cf5087afbf789cb2df7e5c732e1a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 12:11:44 +0200 Subject: [PATCH 15/27] Updated CA testing --- pkg/handler/certmanager/cert/cert.go | 7 +++++-- pkg/handler/certmanager/cert/cert_test.go | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/handler/certmanager/cert/cert.go b/pkg/handler/certmanager/cert/cert.go index 2d1fe33..3ebffe1 100644 --- a/pkg/handler/certmanager/cert/cert.go +++ b/pkg/handler/certmanager/cert/cert.go @@ -167,8 +167,11 @@ func NewCert(commonName string, ca *Cert, opt ...Opt) (*Cert, error) { if !parent.IsCA { return nil, ErrBadParameter.With("Invalid CA certificate") } - if _, err := parent.Verify(x509.VerifyOptions{}); err != nil { - return nil, err + if parent.NotAfter.Before(time.Now()) { + return nil, ErrBadParameter.With("CA certificate has expired") + } + if parent.NotBefore.After(time.Now()) { + return nil, ErrBadParameter.With("CA certificate is not yet valid") } } diff --git a/pkg/handler/certmanager/cert/cert_test.go b/pkg/handler/certmanager/cert/cert_test.go index 3e4f709..fad0730 100644 --- a/pkg/handler/certmanager/cert/cert_test.go +++ b/pkg/handler/certmanager/cert/cert_test.go @@ -55,7 +55,9 @@ func Test_Cert_004(t *testing.T) { t.Log("ca=", ca) cert, err := cert.NewCert(t.Name(), ca, cert.OptKeyType("P224")) - assert.NoError(err) + if !assert.NoError(err) { + t.SkipNow() + } assert.NotNil(cert) t.Log("cert=", cert) From 3999abde0fb2802abf451fab198faf811c494101 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 12:21:17 +0200 Subject: [PATCH 16/27] Updated endpoints --- pkg/handler/certmanager/endpoints.go | 52 +++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index 3dc2a74..e040f42 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -8,7 +8,7 @@ import ( // Packages server "github.com/mutablelogic/go-server" router "github.com/mutablelogic/go-server/pkg/handler/router" - "github.com/mutablelogic/go-server/pkg/httprequest" + httprequest "github.com/mutablelogic/go-server/pkg/httprequest" httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse" ) @@ -17,6 +17,13 @@ import ( type reqCreateCA struct { CommonName string `json:"name"` + // Days int `json:"days"` +} + +type reqCreateCert struct { + reqCreateCA + CA string `json:"ca"` + // Hosts []string `json:"hosts"` } // Check interfaces are satisfied @@ -46,6 +53,20 @@ func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { r.AddHandlerFuncRe(ctx, reRoot, service.reqListCerts, http.MethodGet).(router.Route). SetScope(service.ScopeRead()...) + // Path: / + // Methods: POST + // Scopes: write + // Description: Create a new certificate + r.AddHandlerFuncRe(ctx, reRoot, service.reqCreateCert, http.MethodPost).(router.Route). + SetScope(service.ScopeWrite()...) + + // Path: /ca + // Methods: GET + // Scopes: read + // Description: Return all existing certificate authorities TODO: This should be a separate endpoint + r.AddHandlerFuncRe(ctx, reCA, service.reqListCerts, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) + // Path: /ca // Methods: POST // Scopes: write @@ -80,3 +101,32 @@ func (service *certmanager) reqCreateCA(w http.ResponseWriter, r *http.Request) httpresponse.JSON(w, ca, http.StatusOK, jsonIndent) } } + +// Create a new certificate +func (service *certmanager) reqCreateCert(w http.ResponseWriter, r *http.Request) { + var req reqCreateCert + + // Get the request + if err := httprequest.Read(r, &req); err != nil { + httpresponse.Error(w, http.StatusBadRequest, err.Error()) + return + } + + // Get the CA + var ca Cert + if req.CA != "" { + var err error + ca, err = service.Read(req.CA) + if err != nil { + httpresponse.Error(w, http.StatusBadRequest, err.Error()) + return + } + } + + // Create the Cert + if cert, err := service.CreateSignedCert(req.CommonName, ca); err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + } else { + httpresponse.JSON(w, cert, http.StatusOK, jsonIndent) + } +} From 0ce89a7b6ddfba5808d6e64201fb1836929aca10 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 12:36:54 +0200 Subject: [PATCH 17/27] Updated error returned --- pkg/handler/certmanager/certmanager.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/handler/certmanager/certmanager.go b/pkg/handler/certmanager/certmanager.go index fdada13..ab1cb21 100644 --- a/pkg/handler/certmanager/certmanager.go +++ b/pkg/handler/certmanager/certmanager.go @@ -3,6 +3,8 @@ package certmanager import ( // Packages // Namespace imports "crypto/x509/pkix" + "errors" + "os" . "github.com/djthorpe/go-errors" "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" @@ -103,7 +105,9 @@ func (task *certmanager) CreateSignedCert(commonName string, ca Cert, opts ...ce if ca != nil { var err error ca, err = task.store.Read(ca.Serial()) - if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, ErrNotFound.With(ca.Serial()) + } else if err != nil { return nil, err } } @@ -124,7 +128,8 @@ func (task *certmanager) CreateSignedCert(commonName string, ca Cert, opts ...ce } // Create the certificate and store it - cert, err := cert.NewCert(commonName, ca.(*cert.Cert), append(o, opts...)...) + ca_, _ := ca.(*cert.Cert) + cert, err := cert.NewCert(commonName, ca_, append(o, opts...)...) if err != nil { return nil, err } else if err := task.store.Write(cert); err != nil { From ab7dabfa8d24832c54bf363c938e6070c6d8856a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 12:43:23 +0200 Subject: [PATCH 18/27] Updated --- pkg/handler/certmanager/certmanager.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/handler/certmanager/certmanager.go b/pkg/handler/certmanager/certmanager.go index ab1cb21..7f719db 100644 --- a/pkg/handler/certmanager/certmanager.go +++ b/pkg/handler/certmanager/certmanager.go @@ -5,6 +5,7 @@ import ( // Packages "crypto/x509/pkix" "errors" "os" + "time" . "github.com/djthorpe/go-errors" "github.com/mutablelogic/go-server/pkg/handler/certmanager/cert" @@ -117,8 +118,8 @@ func (task *certmanager) CreateSignedCert(commonName string, ca Cert, opts ...ce if !ca.IsCA() { return nil, ErrBadParameter.With("Cannot sign without a valid CA") } - if err := ca.IsValid(); err != nil { - return nil, err + if ca.Expires().Before(time.Now()) { + return nil, ErrBadParameter.With("CA has expired") } } From 28416cf6997487ca81b83125b0f1460ca5fbf145 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 12:45:36 +0200 Subject: [PATCH 19/27] Updated errors --- pkg/handler/certmanager/certmanager.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/handler/certmanager/certmanager.go b/pkg/handler/certmanager/certmanager.go index 7f719db..8480aa9 100644 --- a/pkg/handler/certmanager/certmanager.go +++ b/pkg/handler/certmanager/certmanager.go @@ -52,7 +52,15 @@ func (task *certmanager) List() []Cert { // Return a certificate by serial number func (task *certmanager) Read(serial string) (Cert, error) { - return task.store.Read(serial) + if cert, err := task.store.Read(serial); err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, ErrNotFound.With(serial) + } else { + return nil, err + } + } else { + return cert, nil + } } // Delete a certificate @@ -105,10 +113,8 @@ func (task *certmanager) CreateSignedCert(commonName string, ca Cert, opts ...ce // We should make the ca "concrete" by reading it if ca != nil { var err error - ca, err = task.store.Read(ca.Serial()) - if errors.Is(err, os.ErrNotExist) { - return nil, ErrNotFound.With(ca.Serial()) - } else if err != nil { + ca, err = task.Read(ca.Serial()) + if err != nil { return nil, err } } From 80960c46bcdc32410a812cb32819fed58e44f62e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 12:56:41 +0200 Subject: [PATCH 20/27] Added certificate response --- pkg/handler/certmanager/endpoints.go | 50 +++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index e040f42..440e5f0 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -2,6 +2,7 @@ package certmanager import ( "context" + "errors" "net/http" "regexp" @@ -10,6 +11,9 @@ import ( router "github.com/mutablelogic/go-server/pkg/handler/router" httprequest "github.com/mutablelogic/go-server/pkg/httprequest" httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse" + + // Namespace imports + . "github.com/djthorpe/go-errors" ) /////////////////////////////////////////////////////////////////////////////// @@ -26,6 +30,13 @@ type reqCreateCert struct { // Hosts []string `json:"hosts"` } +type respCert struct { + Cert + Certificate string `json:"certificate,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + Error string `json:"error,omitempty"` +} + // Check interfaces are satisfied var _ server.ServiceEndpoints = (*certmanager)(nil) @@ -37,8 +48,9 @@ const ( ) var ( - reRoot = regexp.MustCompile(`^/?$`) - reCA = regexp.MustCompile(`^/ca/?$`) + reRoot = regexp.MustCompile(`^/?$`) + reCA = regexp.MustCompile(`^/ca/?$`) + reSerial = regexp.MustCompile(`^/([0-9]+)/?$`) ) /////////////////////////////////////////////////////////////////////////////// @@ -60,13 +72,6 @@ func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { r.AddHandlerFuncRe(ctx, reRoot, service.reqCreateCert, http.MethodPost).(router.Route). SetScope(service.ScopeWrite()...) - // Path: /ca - // Methods: GET - // Scopes: read - // Description: Return all existing certificate authorities TODO: This should be a separate endpoint - r.AddHandlerFuncRe(ctx, reCA, service.reqListCerts, http.MethodGet).(router.Route). - SetScope(service.ScopeRead()...) - // Path: /ca // Methods: POST // Scopes: write @@ -74,6 +79,12 @@ func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { r.AddHandlerFuncRe(ctx, reCA, service.reqCreateCA, http.MethodPost).(router.Route). SetScope(service.ScopeWrite()...) + // Path: / + // Methods: GET + // Scopes: read + // Description: Read a certificate by serial number + r.AddHandlerFuncRe(ctx, reCA, service.reqGetCert, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) } /////////////////////////////////////////////////////////////////////////////// @@ -84,6 +95,27 @@ func (service *certmanager) reqListCerts(w http.ResponseWriter, r *http.Request) httpresponse.JSON(w, service.List(), http.StatusOK, jsonIndent) } +// Get a certificate or CA +func (service *certmanager) reqGetCert(w http.ResponseWriter, r *http.Request) { + urlParameters := router.Params(r.Context()) + + // Get the certificate + cert, err := service.Read(urlParameters[0]) + if errors.Is(err, ErrNotFound) { + httpresponse.Error(w, http.StatusNotFound, err.Error()) + return + } else if err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + // Return the certificate + respCert := respCert{ + Cert: cert, + } + httpresponse.JSON(w, respCert, http.StatusOK, jsonIndent) +} + // Create a new certificate authority func (service *certmanager) reqCreateCA(w http.ResponseWriter, r *http.Request) { var req reqCreateCA From 0dd47283341655b2f3dc8e605731fd772bfa3519 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:01:03 +0200 Subject: [PATCH 21/27] Updated --- pkg/handler/certmanager/endpoints.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index 440e5f0..3ea68e3 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -83,7 +83,7 @@ func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { // Methods: GET // Scopes: read // Description: Read a certificate by serial number - r.AddHandlerFuncRe(ctx, reCA, service.reqGetCert, http.MethodGet).(router.Route). + r.AddHandlerFuncRe(ctx, reSerial, service.reqGetCert, http.MethodGet).(router.Route). SetScope(service.ScopeRead()...) } @@ -149,7 +149,10 @@ func (service *certmanager) reqCreateCert(w http.ResponseWriter, r *http.Request if req.CA != "" { var err error ca, err = service.Read(req.CA) - if err != nil { + if errors.Is(err, ErrNotFound) { + httpresponse.Error(w, http.StatusNotFound, err.Error()) + return + } else if err != nil { httpresponse.Error(w, http.StatusBadRequest, err.Error()) return } From e378f0a9cd7cb5ed5d772f98081ca7a279160ebe Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:06:26 +0200 Subject: [PATCH 22/27] Updated endpoints --- pkg/handler/certmanager/endpoints.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index 3ea68e3..6008b6d 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -1,6 +1,7 @@ package certmanager import ( + "bytes" "context" "errors" "net/http" @@ -113,6 +114,21 @@ func (service *certmanager) reqGetCert(w http.ResponseWriter, r *http.Request) { respCert := respCert{ Cert: cert, } + // Add any errors + if err := cert.IsValid(); err != nil { + respCert.Error = err.Error() + } + + // Add public key + var publicKey bytes.Buffer + if err := cert.WriteCertificate(&publicKey); err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + } else { + respCert.Certificate = publicKey.String() + } + + // TODO: Add private key if scope allows + httpresponse.JSON(w, respCert, http.StatusOK, jsonIndent) } From 2f849407aa4782fb7a72cfbc309f5926061ff085 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:09:50 +0200 Subject: [PATCH 23/27] Updated --- pkg/handler/certmanager/endpoints.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index 6008b6d..f6cc96a 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -114,9 +114,12 @@ func (service *certmanager) reqGetCert(w http.ResponseWriter, r *http.Request) { respCert := respCert{ Cert: cert, } + // Add any errors - if err := cert.IsValid(); err != nil { - respCert.Error = err.Error() + if !cert.IsCA() { + if err := cert.IsValid(); err != nil { + respCert.Error = err.Error() + } } // Add public key From 2d9916123270699c15563eaeb564419e239f03ad Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:13:55 +0200 Subject: [PATCH 24/27] Reverse middleware --- pkg/handler/router/reqrouter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/handler/router/reqrouter.go b/pkg/handler/router/reqrouter.go index c7e3bb2..8b3d38e 100644 --- a/pkg/handler/router/reqrouter.go +++ b/pkg/handler/router/reqrouter.go @@ -103,9 +103,9 @@ func (router *reqs) AddHandler(ctx context.Context, path string, handler http.Ha } func (router *reqs) AddHandlerRe(ctx context.Context, path *regexp.Regexp, handler http.HandlerFunc, methods ...string) *route { - // Add any middleware to the handler, in reverse order + // Add any middleware to the handler middleware := Middleware(ctx) - slices.Reverse(middleware) + //slices.Reverse(middleware) for _, middleware := range middleware { handler = middleware.Wrap(ctx, handler) } From da1e15f7f63b34d09938d5336d60cde53c48dfc3 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:15:17 +0200 Subject: [PATCH 25/27] Go back to previous --- pkg/handler/router/reqrouter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/handler/router/reqrouter.go b/pkg/handler/router/reqrouter.go index 8b3d38e..488ab3f 100644 --- a/pkg/handler/router/reqrouter.go +++ b/pkg/handler/router/reqrouter.go @@ -105,7 +105,7 @@ func (router *reqs) AddHandler(ctx context.Context, path string, handler http.Ha func (router *reqs) AddHandlerRe(ctx context.Context, path *regexp.Regexp, handler http.HandlerFunc, methods ...string) *route { // Add any middleware to the handler middleware := Middleware(ctx) - //slices.Reverse(middleware) + slices.Reverse(middleware) for _, middleware := range middleware { handler = middleware.Wrap(ctx, handler) } From f96dc9716e5774298fae21b987049f3cf75ce0ee Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:24:10 +0200 Subject: [PATCH 26/27] Updated --- pkg/handler/certmanager/endpoints.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index f6cc96a..80b4a47 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -32,9 +32,9 @@ type reqCreateCert struct { } type respCert struct { - Cert + Cert `json:"cert"` Certificate string `json:"certificate,omitempty"` - PrivateKey string `json:"private_key,omitempty"` + PrivateKey string `json:"key,omitempty"` Error string `json:"error,omitempty"` } @@ -122,16 +122,26 @@ func (service *certmanager) reqGetCert(w http.ResponseWriter, r *http.Request) { } } - // Add public key - var publicKey bytes.Buffer - if err := cert.WriteCertificate(&publicKey); err != nil { + // Add certificate + var certdata, keydata bytes.Buffer + if err := cert.WriteCertificate(&certdata); err != nil { httpresponse.Error(w, http.StatusInternalServerError, err.Error()) - } else { - respCert.Certificate = publicKey.String() + return + } + + // Add private key if it's not a CA + if !cert.IsCA() { + if err := cert.WritePrivateKey(&keydata); err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } } - // TODO: Add private key if scope allows + // TODO: Don't add private key if scope doesn't allow it? + respCert.Certificate = certdata.String() + respCert.PrivateKey = keydata.String() + // Respond httpresponse.JSON(w, respCert, http.StatusOK, jsonIndent) } From d9d7a64f21276a84578e564a8d1ce09d4a113592 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 13:29:21 +0200 Subject: [PATCH 27/27] Added validity response --- pkg/handler/certmanager/endpoints.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index 80b4a47..f53a861 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -32,10 +32,10 @@ type reqCreateCert struct { } type respCert struct { - Cert `json:"cert"` + Cert `json:"meta"` Certificate string `json:"certificate,omitempty"` PrivateKey string `json:"key,omitempty"` - Error string `json:"error,omitempty"` + Error string `json:"validity,omitempty"` } // Check interfaces are satisfied