From 18726b133971f5d165da7c2686a12f1156b7c61f Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Thu, 14 Jan 2021 13:26:16 +0100 Subject: [PATCH 01/27] added baseline funcs from old repo --- crypto/api/v1/api.go | 155 +++++++++ crypto/api/v1/api_test.go | 254 ++++++++++++++ crypto/api/v1/client.go | 148 ++++++++ crypto/api/v1/client_test.go | 135 ++++++++ crypto/api/v1/generated.go | 576 ++++++++++++++++++++++++++++++++ crypto/api/v1/generated_test.go | 143 ++++++++ crypto/client/client.go | 42 +++ crypto/client/client_test.go | 25 ++ crypto/crypto.go | 220 ++++++++++++ crypto/crypto_test.go | 238 +++++++++++++ crypto/engine/engine.go | 150 +++++++++ crypto/engine/engine_test.go | 154 +++++++++ crypto/interface.go | 40 +++ crypto/jwx.go | 115 +++++++ crypto/jwx_test.go | 157 +++++++++ crypto/log/log.go | 29 ++ crypto/storage/fs.go | 200 +++++++++++ crypto/storage/fs_test.go | 98 ++++++ crypto/storage/storage.go | 33 ++ crypto/test.go | 27 ++ crypto/test/broken.pem | 6 + crypto/test/ec.sk | 6 + crypto/test/ed25519.sk | 3 + crypto/test/keys.go | 21 ++ crypto/test/public_2048.pem | 9 + crypto/test/publickey.pem | 9 + crypto/test/rsa.sk | 27 ++ crypto/test/sk.pem | 28 ++ crypto/types/types.go | 38 +++ crypto/util/common.go | 52 +++ crypto/util/jwk.go | 97 ++++++ crypto/util/jwk_test.go | 146 ++++++++ crypto/util/kid.go | 33 ++ crypto/util/pem.go | 100 ++++++ crypto/util/pem_test.go | 115 +++++++ docs/_static/crypto/v1.yaml | 104 ++++++ docs/pages/development.rst | 4 +- go.mod | 10 + go.sum | 69 ++++ mock/mock_crypto.go | 124 +++++++ 40 files changed, 3938 insertions(+), 2 deletions(-) create mode 100644 crypto/api/v1/api.go create mode 100644 crypto/api/v1/api_test.go create mode 100644 crypto/api/v1/client.go create mode 100644 crypto/api/v1/client_test.go create mode 100644 crypto/api/v1/generated.go create mode 100644 crypto/api/v1/generated_test.go create mode 100644 crypto/client/client.go create mode 100644 crypto/client/client_test.go create mode 100644 crypto/crypto.go create mode 100644 crypto/crypto_test.go create mode 100644 crypto/engine/engine.go create mode 100644 crypto/engine/engine_test.go create mode 100644 crypto/interface.go create mode 100644 crypto/jwx.go create mode 100644 crypto/jwx_test.go create mode 100644 crypto/log/log.go create mode 100644 crypto/storage/fs.go create mode 100644 crypto/storage/fs_test.go create mode 100644 crypto/storage/storage.go create mode 100644 crypto/test.go create mode 100644 crypto/test/broken.pem create mode 100644 crypto/test/ec.sk create mode 100644 crypto/test/ed25519.sk create mode 100644 crypto/test/keys.go create mode 100644 crypto/test/public_2048.pem create mode 100644 crypto/test/publickey.pem create mode 100644 crypto/test/rsa.sk create mode 100644 crypto/test/sk.pem create mode 100644 crypto/types/types.go create mode 100644 crypto/util/common.go create mode 100644 crypto/util/jwk.go create mode 100644 crypto/util/jwk_test.go create mode 100644 crypto/util/kid.go create mode 100644 crypto/util/pem.go create mode 100644 crypto/util/pem_test.go create mode 100644 docs/_static/crypto/v1.yaml create mode 100644 mock/mock_crypto.go diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go new file mode 100644 index 0000000000..fb9957c751 --- /dev/null +++ b/crypto/api/v1/api.go @@ -0,0 +1,155 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package v1 + +import ( + crypto2 "crypto" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "mime" + "net/http" + + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/log" + "github.com/nuts-foundation/nuts-node/crypto/util" + + "github.com/labstack/echo/v4" + "github.com/lestrrat-go/jwx/jwk" + "github.com/nuts-foundation/nuts-node/crypto/storage" +) + +// Wrapper implements the generated interface from oapi-codegen +type Wrapper struct { + C crypto.Client +} + +// GenerateKeyPair is the implementation of the REST service call POST /crypto/generate +// It returns the public key for the given legal entity in either PEM or JWK format depending on the accept-header. Default is PEM (backwards compatibility) +func (w *Wrapper) GenerateKeyPair(ctx echo.Context) error { + var publicKey crypto2.PublicKey + var err error + + if publicKey, err = w.C.GenerateKeyPair(); err != nil { + return err + } + + acceptHeader := ctx.Request().Header.Get("Accept") + + if ct, _, _ := mime.ParseMediaType(acceptHeader); ct == "application/json" { + var j jwk.Key + var err error + if j, err = jwk.New(publicKey); err != nil { + return err + } + + return ctx.JSON(http.StatusOK, j) + } + + // backwards compatible PEM format is the default + pub, err := util.PublicKeyToPem(publicKey) + if err != nil { + return err + } + + return ctx.String(http.StatusOK, pub) +} + +func (w *Wrapper) SignJwt(ctx echo.Context) error { + buf, err := readBody(ctx) + if err != nil { + return err + } + + var signRequest = &SignJwtRequest{} + err = json.Unmarshal(buf, signRequest) + + if err != nil { + log.Logger().Error(err.Error()) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if len(signRequest.Kid) == 0 { + return echo.NewHTTPError(http.StatusBadRequest, "missing kid") + } + + if len(signRequest.Claims) == 0 { + return echo.NewHTTPError(http.StatusBadRequest, "missing claims") + } + + sig, err := w.C.SignJWT(signRequest.Claims, signRequest.Kid) + + if err != nil { + log.Logger().Error(err.Error()) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return ctx.String(http.StatusOK, sig) +} + +// PublicKey returns a public key for the given urn. The urn represents a legal entity. The api returns the public key either in PEM or JWK format. +// It uses the accept header to determine this. Default is PEM (text/plain), only when application/json is requested will it return JWK. +func (w *Wrapper) PublicKey(ctx echo.Context, kid string) error { + acceptHeader := ctx.Request().Header.Get("Accept") + + // starts with so we can ignore any + + if ct, _, _ := mime.ParseMediaType(acceptHeader); ct == "application/json" { + jwk, err := w.C.GetPublicKeyAsJWK(kid) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return ctx.NoContent(404) + } + log.Logger().Error(err.Error()) + return err + } + + return ctx.JSON(http.StatusOK, jwk) + } + + // backwards compatible PEM format is the default + pub, err := w.C.GetPublicKeyAsPEM(kid) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return ctx.NoContent(404) + } + log.Logger().Error(err.Error()) + return err + } + + return ctx.String(http.StatusOK, pub) +} + +func readBody(ctx echo.Context) ([]byte, error) { + req := ctx.Request() + if req.Body == nil { + msg := "missing body in request" + log.Logger().Error(msg) + return nil, echo.NewHTTPError(http.StatusBadRequest, msg) + } + + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + msg := fmt.Sprintf("error reading request: %v", err) + log.Logger().Error(msg) + return nil, echo.NewHTTPError(http.StatusBadRequest, msg) + } + + return buf, nil +} diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go new file mode 100644 index 0000000000..b9758f0e5d --- /dev/null +++ b/crypto/api/v1/api_test.go @@ -0,0 +1,254 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package v1 + +import ( + "bytes" + "crypto/ecdsa" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/nuts-foundation/nuts-go-test/io" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/storage" + "github.com/nuts-foundation/nuts-node/crypto/util" + "github.com/nuts-foundation/nuts-node/mock" + "github.com/stretchr/testify/assert" +) + +type pubKeyMatcher struct { +} + +func (p pubKeyMatcher) Matches(x interface{}) bool { + s := x.(string) + + return strings.Contains(s, "-----BEGIN PUBLIC KEY-----") +} + +func (p pubKeyMatcher) String() string { + return "Public Key Matcher" +} + +type jwkMatcher struct { +} + +func (p jwkMatcher) Matches(x interface{}) bool { + key := x.(jwk.Key) + + return key.KeyType() == jwa.EC +} + +func (p jwkMatcher) String() string { + return "JWK Matcher" +} + +func TestWrapper_GenerateKeyPair(t *testing.T) { + + t.Run("GenerateKeyPairAPI call returns 200 with pub in PEM format", func(t *testing.T) { + se := apiWrapper(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + echo.EXPECT().Request().Return(&http.Request{}) + echo.EXPECT().String(http.StatusOK, pubKeyMatcher{}) + + se.GenerateKeyPair(echo) + }) + + t.Run("GenerateKeyPairAPI call returns 200 with pub in JWK format", func(t *testing.T) { + se := apiWrapper(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) + echo.EXPECT().JSON(http.StatusOK, jwkMatcher{}) + + se.GenerateKeyPair(echo) + }) +} + +func TestWrapper_SignJwt(t *testing.T) { + client := apiWrapper(t) + + publicKey, _ := client.C.GenerateKeyPair() + kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + + t.Run("Missing claims returns 400", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + jsonRequest := SignJwtRequest{ + Kid: kid, + } + + json, _ := json.Marshal(jsonRequest) + request := &http.Request{ + Body: ioutil.NopCloser(bytes.NewReader(json)), + } + + echo.EXPECT().Request().Return(request) + + err := client.SignJwt(echo) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "code=400, message=missing claims") + }) + + t.Run("Missing kid returns 400", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + jsonRequest := SignJwtRequest{ + Claims: map[string]interface{}{"iss": "nuts"}, + } + + json, _ := json.Marshal(jsonRequest) + request := &http.Request{ + Body: ioutil.NopCloser(bytes.NewReader(json)), + } + + echo.EXPECT().Request().Return(request) + + err := client.SignJwt(echo) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "code=400, message=missing kid") + }) + + t.Run("All OK returns 200", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + jsonRequest := SignJwtRequest{ + Kid: kid, + Claims: map[string]interface{}{"iss": "nuts"}, + } + + json, _ := json.Marshal(jsonRequest) + request := &http.Request{ + Body: ioutil.NopCloser(bytes.NewReader(json)), + } + + echo.EXPECT().Request().Return(request) + echo.EXPECT().String(http.StatusOK, gomock.Any()) + + err := client.SignJwt(echo) + + assert.Nil(t, err) + }) + + t.Run("Missing body gives 400", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + request := &http.Request{} + + echo.EXPECT().Request().Return(request) + + err := client.SignJwt(echo) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "code=400, message=missing body in request") + }) +} + +func TestWrapper_PublicKey(t *testing.T) { + client := apiWrapper(t) + + publicKey, _ := client.C.GenerateKeyPair() + kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + + t.Run("PublicKey API call returns 200", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + echo.EXPECT().Request().Return(&http.Request{}) + echo.EXPECT().String(http.StatusOK, gomock.Any()) + + _ = client.PublicKey(echo, kid) + }) + + t.Run("PublicKey API call returns JWK", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) + echo.EXPECT().JSON(http.StatusOK, gomock.Any()) + + _ = client.PublicKey(echo, kid) + }) + + t.Run("PublicKey API call returns 404 for unknown", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + echo.EXPECT().Request().Return(&http.Request{}) + echo.EXPECT().NoContent(http.StatusNotFound) + + _ = client.PublicKey(echo, "not") + }) + + t.Run("PublicKey API call returns 404 for unknown, JWK requested", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockContext(ctrl) + + echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) + echo.EXPECT().NoContent(http.StatusNotFound) + + _ = client.PublicKey(echo, "not") + }) +} + +func apiWrapper(t *testing.T) *Wrapper { + backend, _ := storage.NewFileSystemBackend(io.TestDirectory(t)) + crypto := crypto.Crypto{ + Storage: backend, + Config: crypto.DefaultCryptoConfig(), + } + crypto.Config.Keysize = 1024 + + return &Wrapper{C: &crypto} +} + +type errorCloser struct{} + +func (errorCloser) Read([]byte) (n int, err error) { + return 0, errors.New("error") +} + +func (errorCloser) Close() error { + return errors.New("error") +} diff --git a/crypto/api/v1/client.go b/crypto/api/v1/client.go new file mode 100644 index 0000000000..593558aed0 --- /dev/null +++ b/crypto/api/v1/client.go @@ -0,0 +1,148 @@ +/* + * Nuts crypto + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import ( + "bytes" + "context" + "crypto" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/lestrrat-go/jwx/jwk" +) + +// ErrNotImplemented indicates that this client API call is not implemented. +var ErrNotImplemented = errors.New("operation not implemented") + +// HttpClient holds the server address and other basic settings for the http client +type HttpClient struct { + ServerAddress string + Timeout time.Duration +} + +func (hb HttpClient) clientWithRequestEditor(fn RequestEditorFn) ClientInterface { + url := hb.ServerAddress + if !strings.Contains(url, "http") { + url = fmt.Sprintf("http://%v", hb.ServerAddress) + } + + response, err := NewClientWithResponses(url, WithRequestEditorFn(fn)) + if err != nil { + panic(err) + } + return response +} + +func (hb HttpClient) client() ClientInterface { + return hb.clientWithRequestEditor(nil) +} + +func (hb HttpClient) GenerateKeyPair() (crypto.PublicKey, error) { + ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) + defer cancel() + response, err := hb.client().GenerateKeyPair(ctx) + if err != nil { + return nil, err + } + if err := testResponseCode(http.StatusOK, response); err != nil { + return nil, err + } + jwkSet, err := jwk.Parse(response.Body) + if err != nil { + return nil, err + } + return jwkSet.Keys[0], nil +} + +func (hb HttpClient) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { + ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) + defer cancel() + httpClient := hb.clientWithRequestEditor(func(ctx context.Context, req *http.Request) error { + req.Header.Add("Accept", "application/json") + return nil + }) + response, err := httpClient.PublicKey(ctx, kid) + if err != nil { + return nil, err + } + if err := testResponseCode(http.StatusOK, response); err != nil { + return nil, err + } + jwkSet, err := jwk.Parse(response.Body) + if err != nil { + return nil, err + } + return jwkSet.Keys[0], nil +} + +func (hb HttpClient) GetPrivateKey(kid string) (crypto.Signer, error) { + panic(ErrNotImplemented) +} + +func (hb HttpClient) GetPublicKeyAsPEM(kid string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) + defer cancel() + httpClient := hb.clientWithRequestEditor(func(ctx context.Context, req *http.Request) error { + req.Header.Add("Accept", "text/plain") + return nil + }) + response, err := httpClient.PublicKey(ctx, kid) + if err != nil { + return "", err + } + if err := testResponseCode(http.StatusOK, response); err != nil { + return "", err + } + pemBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + return string(pemBytes), nil +} + +func (hb HttpClient) SignJWT(claims map[string]interface{}, kid string) (string, error) { + panic(ErrNotImplemented) +} + +func (hb HttpClient) PrivateKeyExists(key string) bool { + panic(ErrNotImplemented) +} + +func readResponse(response *http.Response) ([]byte, error) { + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(response.Body); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func testResponseCode(expectedStatusCode int, response *http.Response) error { + if response.StatusCode != expectedStatusCode { + responseData, _ := ioutil.ReadAll(response.Body) + return fmt.Errorf("server returned HTTP %d (expected: %d), response: %s", + response.StatusCode, expectedStatusCode, string(responseData)) + } + return nil +} diff --git a/crypto/api/v1/client_test.go b/crypto/api/v1/client_test.go new file mode 100644 index 0000000000..0e2d2a1e54 --- /dev/null +++ b/crypto/api/v1/client_test.go @@ -0,0 +1,135 @@ +/* + * Nuts crypto + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type handler struct { + statusCode int + responseData []byte +} + +func (h handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(h.statusCode) + writer.Write(h.responseData) +} + +var genericError = []byte("failed") + +var jwkAsString = ` +{ + "kty" : "RSA", + "n" : "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w", + "e" : "AQAB" +}` +var jwkAsBytes = []byte(jwkAsString) + +func TestHttpClient_GenerateKeyPair(t *testing.T) { + t.Run("ok", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GenerateKeyPair() + if !assert.NoError(t, err) { + return + } + assert.NotNil(t, res) + }) + t.Run("error - response not HTTP OK", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError, responseData: genericError}) + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GenerateKeyPair() + assert.EqualError(t, err, "server returned HTTP 500 (expected: 200), response: failed") + assert.Nil(t, res) + }) + t.Run("error - server not running", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusOK}) + s.Close() + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GenerateKeyPair() + assert.Contains(t, err.Error(), "connection refused") + assert.Nil(t, res) + }) +} + +func TestHttpClient_GetPublicKeyAsJWK(t *testing.T) { + t.Run("ok", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GetPublicKeyAsJWK("kid") + if !assert.NoError(t, err) { + return + } + assert.NotNil(t, res) + }) + t.Run("error - server returned non-JWK", func(t *testing.T) { + csrBytes, _ := ioutil.ReadFile("../test/broken.pem") + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: csrBytes}) + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GetPublicKeyAsJWK("kid") + assert.Contains(t, err.Error(), "failed to unmarshal JWK:") + assert.Nil(t, res) + }) + t.Run("error - response not HTTP OK", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError, responseData: genericError}) + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GetPublicKeyAsJWK("kid") + assert.EqualError(t, err, "server returned HTTP 500 (expected: 200), response: failed") + assert.Nil(t, res) + }) + t.Run("error - server not running", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusOK}) + s.Close() + c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + res, err := c.GetPublicKeyAsJWK("kid") + assert.Contains(t, err.Error(), "connection refused") + assert.Nil(t, res) + }) +} + +func TestHttpClient_NonImplemented(t *testing.T) { + c := HttpClient{ServerAddress: "foo", Timeout: time.Second} + + funcs := map[string]func(){ + "GetPrivateKey": func() { + c.GetPrivateKey("") + }, + "SignJWT": func() { + c.SignJWT(nil, "") + }, + "PrivateKeyExists": func() { + c.PrivateKeyExists("") + }, + } + for fnName, fn := range funcs { + t.Run(fnName+" should panic", func(t *testing.T) { + assert.PanicsWithValue(t, ErrNotImplemented, func() { + fn() + }) + }) + } +} diff --git a/crypto/api/v1/generated.go b/crypto/api/v1/generated.go new file mode 100644 index 0000000000..25e7ccda1b --- /dev/null +++ b/crypto/api/v1/generated.go @@ -0,0 +1,576 @@ +// Package v1 provides primitives to interact the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. +package v1 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/deepmap/oapi-codegen/pkg/runtime" + "github.com/labstack/echo/v4" +) + +// JWK defines model for JWK. +type JWK map[string]interface{} + +// PublicKey defines model for PublicKey. +type PublicKey string + +// SignJwtRequest defines model for SignJwtRequest. +type SignJwtRequest struct { + Claims map[string]interface{} `json:"claims"` + Kid string `json:"kid"` +} + +// SignJwtJSONBody defines parameters for SignJwt. +type SignJwtJSONBody SignJwtRequest + +// SignJwtRequestBody defines body for SignJwt for application/json ContentType. +type SignJwtJSONRequestBody SignJwtJSONBody + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A callback for modifying requests which are generated before sending over + // the network. + RequestEditor RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = http.DefaultClient + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditor = fn + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GenerateKeyPair request + GenerateKeyPair(ctx context.Context) (*http.Response, error) + + // PublicKey request + PublicKey(ctx context.Context, kid string) (*http.Response, error) + + // SignJwt request with any body + SignJwtWithBody(ctx context.Context, contentType string, body io.Reader) (*http.Response, error) + + SignJwt(ctx context.Context, body SignJwtJSONRequestBody) (*http.Response, error) +} + +func (c *Client) GenerateKeyPair(ctx context.Context) (*http.Response, error) { + req, err := NewGenerateKeyPairRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +func (c *Client) PublicKey(ctx context.Context, kid string) (*http.Response, error) { + req, err := NewPublicKeyRequest(c.Server, kid) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +func (c *Client) SignJwtWithBody(ctx context.Context, contentType string, body io.Reader) (*http.Response, error) { + req, err := NewSignJwtRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +func (c *Client) SignJwt(ctx context.Context, body SignJwtJSONRequestBody) (*http.Response, error) { + req, err := NewSignJwtRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +// NewGenerateKeyPairRequest generates requests for GenerateKeyPair +func NewGenerateKeyPairRequest(server string) (*http.Request, error) { + var err error + + queryUrl, err := url.Parse(server) + if err != nil { + return nil, err + } + + basePath := fmt.Sprintf("/internal/crypto/v1/generate") + if basePath[0] == '/' { + basePath = basePath[1:] + } + + queryUrl, err = queryUrl.Parse(basePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryUrl.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPublicKeyRequest generates requests for PublicKey +func NewPublicKeyRequest(server string, kid string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParam("simple", false, "kid", kid) + if err != nil { + return nil, err + } + + queryUrl, err := url.Parse(server) + if err != nil { + return nil, err + } + + basePath := fmt.Sprintf("/internal/crypto/v1/public_key/%s", pathParam0) + if basePath[0] == '/' { + basePath = basePath[1:] + } + + queryUrl, err = queryUrl.Parse(basePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryUrl.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewSignJwtRequest calls the generic SignJwt builder with application/json body +func NewSignJwtRequest(server string, body SignJwtJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewSignJwtRequestWithBody(server, "application/json", bodyReader) +} + +// NewSignJwtRequestWithBody generates requests for SignJwt with any type of body +func NewSignJwtRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + queryUrl, err := url.Parse(server) + if err != nil { + return nil, err + } + + basePath := fmt.Sprintf("/internal/crypto/v1/sign_jwt") + if basePath[0] == '/' { + basePath = basePath[1:] + } + + queryUrl, err = queryUrl.Parse(basePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryUrl.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + return req, nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GenerateKeyPair request + GenerateKeyPairWithResponse(ctx context.Context) (*GenerateKeyPairResponse, error) + + // PublicKey request + PublicKeyWithResponse(ctx context.Context, kid string) (*PublicKeyResponse, error) + + // SignJwt request with any body + SignJwtWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader) (*SignJwtResponse, error) + + SignJwtWithResponse(ctx context.Context, body SignJwtJSONRequestBody) (*SignJwtResponse, error) +} + +type GenerateKeyPairResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GenerateKeyPairResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GenerateKeyPairResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PublicKeyResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *JWK +} + +// Status returns HTTPResponse.Status +func (r PublicKeyResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PublicKeyResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type SignJwtResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r SignJwtResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SignJwtResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GenerateKeyPairWithResponse request returning *GenerateKeyPairResponse +func (c *ClientWithResponses) GenerateKeyPairWithResponse(ctx context.Context) (*GenerateKeyPairResponse, error) { + rsp, err := c.GenerateKeyPair(ctx) + if err != nil { + return nil, err + } + return ParseGenerateKeyPairResponse(rsp) +} + +// PublicKeyWithResponse request returning *PublicKeyResponse +func (c *ClientWithResponses) PublicKeyWithResponse(ctx context.Context, kid string) (*PublicKeyResponse, error) { + rsp, err := c.PublicKey(ctx, kid) + if err != nil { + return nil, err + } + return ParsePublicKeyResponse(rsp) +} + +// SignJwtWithBodyWithResponse request with arbitrary body returning *SignJwtResponse +func (c *ClientWithResponses) SignJwtWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader) (*SignJwtResponse, error) { + rsp, err := c.SignJwtWithBody(ctx, contentType, body) + if err != nil { + return nil, err + } + return ParseSignJwtResponse(rsp) +} + +func (c *ClientWithResponses) SignJwtWithResponse(ctx context.Context, body SignJwtJSONRequestBody) (*SignJwtResponse, error) { + rsp, err := c.SignJwt(ctx, body) + if err != nil { + return nil, err + } + return ParseSignJwtResponse(rsp) +} + +// ParseGenerateKeyPairResponse parses an HTTP response from a GenerateKeyPairWithResponse call +func ParseGenerateKeyPairResponse(rsp *http.Response) (*GenerateKeyPairResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GenerateKeyPairResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + } + + return response, nil +} + +// ParsePublicKeyResponse parses an HTTP response from a PublicKeyWithResponse call +func ParsePublicKeyResponse(rsp *http.Response) (*PublicKeyResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &PublicKeyResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest JWK + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case rsp.StatusCode == 200: + // Content-type (text/plain) unsupported + + } + + return response, nil +} + +// ParseSignJwtResponse parses an HTTP response from a SignJwtWithResponse call +func ParseSignJwtResponse(rsp *http.Response) (*SignJwtResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &SignJwtResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Generate a new keypair and store the private key + // (POST /internal/crypto/v1/generate) + GenerateKeyPair(ctx echo.Context) error + // get the public key for a given kid. It returns the key in PEM or JWK format. This depends on the accept header used (text/plain vs application/json) + // (GET /internal/crypto/v1/public_key/{kid}) + PublicKey(ctx echo.Context, kid string) error + // sign a JWT payload with the private key of the given kid + // (POST /internal/crypto/v1/sign_jwt) + SignJwt(ctx echo.Context) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// GenerateKeyPair converts echo context to params. +func (w *ServerInterfaceWrapper) GenerateKeyPair(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GenerateKeyPair(ctx) + return err +} + +// PublicKey converts echo context to params. +func (w *ServerInterfaceWrapper) PublicKey(ctx echo.Context) error { + var err error + // ------------- Path parameter "kid" ------------- + var kid string + + err = runtime.BindStyledParameter("simple", false, "kid", ctx.Param("kid"), &kid) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter kid: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.PublicKey(ctx, kid) + return err +} + +// SignJwt converts echo context to params. +func (w *ServerInterfaceWrapper) SignJwt(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.SignJwt(ctx) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.POST(baseURL+"/internal/crypto/v1/generate", wrapper.GenerateKeyPair) + router.GET(baseURL+"/internal/crypto/v1/public_key/:kid", wrapper.PublicKey) + router.POST(baseURL+"/internal/crypto/v1/sign_jwt", wrapper.SignJwt) + +} + diff --git a/crypto/api/v1/generated_test.go b/crypto/api/v1/generated_test.go new file mode 100644 index 0000000000..f4bca72583 --- /dev/null +++ b/crypto/api/v1/generated_test.go @@ -0,0 +1,143 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package v1 + +import ( + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/golang/mock/gomock" + "github.com/labstack/echo/v4" + "github.com/magiconair/properties/assert" + "github.com/nuts-foundation/nuts-node/mock" +) + +type testServerInterface struct { + err error +} + +func (t *testServerInterface) GenerateKeyPair(ctx echo.Context) error { + return t.err +} + +func (t *testServerInterface) PublicKey(ctx echo.Context, urn string) error { + return t.err +} + +func (t *testServerInterface) SignJwt(ctx echo.Context) error { + return t.err +} + +var siws = []*ServerInterfaceWrapper{ + serverInterfaceWrapper(nil), serverInterfaceWrapper(errors.New("Server error")), +} + +func TestServerInterfaceWrapper_PublicKey(t *testing.T) { + for _, siw := range siws { + t.Run("PublicKey call returns expected error", func(t *testing.T) { + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := echo.New().NewContext(req, rec) + c.SetParamNames("kid") + c.SetParamValues("1") + + err := siw.PublicKey(c) + tsi := siw.Handler.(*testServerInterface) + assert.Equal(t, tsi.err, err) + }) + } +} + +func TestServerInterfaceWrapper_SignJwt(t *testing.T) { + for _, siw := range siws { + t.Run("SignJWT call returns expected error", func(t *testing.T) { + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := echo.New().NewContext(req, rec) + + err := siw.SignJwt(c) + tsi := siw.Handler.(*testServerInterface) + assert.Equal(t, tsi.err, err) + }) + } +} + +func TestServerInterfaceWrapper_GenerateKeyPair(t *testing.T) { + t.Run("GenerateKeyPairAPI call returns no error", func(t *testing.T) { + // given + siw := serverInterfaceWrapper(nil) + q := make(url.Values) + q.Set("legalEntity", "le") + req := httptest.NewRequest(echo.POST, "/?"+q.Encode(), nil) + rec := httptest.NewRecorder() + c := echo.New().NewContext(req, rec) + + // then + if err := siw.GenerateKeyPair(c); err != nil { + t.Errorf("Got err during call: %s", err.Error()) + } + + if rec.Code != http.StatusOK { + t.Errorf("Got status=%d, want %d", rec.Code, http.StatusOK) + } + }) + + t.Run("Server error is returned", func(t *testing.T) { + // given + siw := serverInterfaceWrapper(errors.New("Server error")) + q := make(url.Values) + q.Set("legalEntity", "le") + req := httptest.NewRequest(echo.POST, "/?"+q.Encode(), nil) + rec := httptest.NewRecorder() + c := echo.New().NewContext(req, rec) + + // then + if err := siw.GenerateKeyPair(c); err != nil { + expected := "Server error" + if err.Error() != expected { + t.Errorf("Expected error [%s], got [%s]", expected, err.Error()) + } + } else { + t.Errorf("Expected error for bad request") + } + }) +} + +func TestRegisterHandlers(t *testing.T) { + t.Run("Registers routes for crypto module", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockEchoRouter(ctrl) + + echo.EXPECT().POST("/internal/crypto/v1/generate", gomock.Any()) + echo.EXPECT().POST("/internal/crypto/v1/sign_jwt", gomock.Any()) + echo.EXPECT().GET("/internal/crypto/v1/public_key/:kid", gomock.Any()) + + RegisterHandlers(echo, &testServerInterface{}) + }) +} + +func serverInterfaceWrapper(err error) *ServerInterfaceWrapper { + return &ServerInterfaceWrapper{ + Handler: &testServerInterface{err: err}, + } +} diff --git a/crypto/client/client.go b/crypto/client/client.go new file mode 100644 index 0000000000..c8d1a1fbbe --- /dev/null +++ b/crypto/client/client.go @@ -0,0 +1,42 @@ +/* + * Nuts registry + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client + +import ( + "time" + + "github.com/nuts-foundation/nuts-node/core" + + api "github.com/nuts-foundation/nuts-node/crypto/api/v1" + "github.com/nuts-foundation/nuts-node/crypto" +) + +// NewCryptoClient creates a new local or remote client, depending on engine configuration. +func NewCryptoClient() crypto.Client { + instance := crypto.Instance() + if core.NutsConfig().GetEngineMode(instance.Config.Mode) == core.ServerEngineMode { + return instance + } + + return api.HttpClient{ + ServerAddress: instance.Config.Address, + Timeout: time.Duration(instance.Config.ClientTimeout) * time.Second, + } +} diff --git a/crypto/client/client_test.go b/crypto/client/client_test.go new file mode 100644 index 0000000000..f7d4e2067d --- /dev/null +++ b/crypto/client/client_test.go @@ -0,0 +1,25 @@ +package client + +import ( + "os" + "testing" + + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + v1 "github.com/nuts-foundation/nuts-node/crypto/api/v1" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestNewCryptoClient_ServerMode(t *testing.T) { + _, ok := NewCryptoClient().(*crypto.Crypto) + assert.True(t, ok) +} + +func TestNewCryptoClient_ClientMode(t *testing.T) { + os.Setenv("NUTS_MODE", "cli") + defer os.Unsetenv("NUTS_MODE") + core.NutsConfig().Load(&cobra.Command{}) + _, ok := NewCryptoClient().(v1.HttpClient) + assert.True(t, ok) +} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 0000000000..3435460b8f --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,220 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "io" + "sync" + + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto/storage" + "github.com/nuts-foundation/nuts-node/crypto/util" +) + +// MinRSAKeySize defines the minimum RSA key size +const MinRSAKeySize = 2048 + +// MinECKeySize defines the minimum EC key size +const MinECKeySize = 256 + +// ErrInvalidKeySize is returned when the keySize for new keys is too short +var ErrInvalidKeySize = errors.New(fmt.Sprintf("invalid keySize, needs to be at least %d bits for RSA and %d bits for EC", MinRSAKeySize, MinECKeySize)) + +// ErrInvalidKeyIdentifier is returned when the provided key identifier isn't valid +var ErrInvalidKeyIdentifier = errors.New("invalid key identifier") + +// ErrInvalidAlgorithm indicates an invalid public key was used +var ErrInvalidAlgorithm = errors.New("invalid algorithm for public key") + +// ErrKeyAlreadyExists indicates that the key already exists. +var ErrKeyAlreadyExists = errors.New("key already exists") + +// CryptoConfig holds the values for the crypto engine +type CryptoConfig struct { + Mode string + Address string + ClientTimeout int + Keysize int + Storage string + Fspath string +} + +func (cc CryptoConfig) getFSPath() string { + if cc.Fspath == "" { + return DefaultCryptoConfig().Fspath + } + + return cc.Fspath +} + +func DefaultCryptoConfig() CryptoConfig { + return CryptoConfig{ + Address: "localhost:1323", + ClientTimeout: 10, + Keysize: 2048, + Storage: "fs", + Fspath: "./", + } +} + +// default implementation for Instance +type Crypto struct { + Storage storage.Storage + Config CryptoConfig + configOnce sync.Once + configDone bool +} + +type opaquePrivateKey struct { + publicKey crypto.PublicKey + signFn func(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) +} + +func (k opaquePrivateKey) Public() crypto.PublicKey { + return k.publicKey +} + +func (k opaquePrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return k.signFn(rand, digest, opts) +} + +// Shutdown stops the certificate monitors +func (client *Crypto) Shutdown() error { + return nil +} + +// GetPrivateKey returns the specified private key. It can be used for signing, but cannot be exported. +func (client *Crypto) GetPrivateKey(kid string) (crypto.Signer, error) { + priv, err := client.Storage.GetPrivateKey(kid) + if err != nil { + return nil, err + } + return opaquePrivateKey{publicKey: priv.Public(), signFn: priv.Sign}, nil +} + +var instance *Crypto + +var oneBackend sync.Once + +// Instance returns the same instance of Crypto every time +func Instance() *Crypto { + if instance != nil { + return instance + } + oneBackend.Do(func() { + instance = NewInstance(DefaultCryptoConfig()) + }) + return instance +} + +func NewInstance(config CryptoConfig) *Crypto { + return &Crypto{ + Config: config, + } +} + +// Configure loads the given configurations in the engine. Any wrong combination will return an error +func (client *Crypto) Configure() error { + var err error + client.configOnce.Do(func() { + if core.NutsConfig().GetEngineMode(client.Config.Mode) != core.ServerEngineMode { + return + } + if err = client.doConfigure(); err == nil { + client.configDone = true + } + }) + return err +} + +func (client *Crypto) doConfigure() error { + if err := client.verifyKeySize(client.Config.Keysize); err != nil { + return err + } + if client.Config.Storage != "fs" && client.Config.Storage != "" { + return errors.New("only fs backend available for now") + } + var err error + if client.Storage, err = storage.NewFileSystemBackend(client.Config.getFSPath()); err != nil { + return err + } + return nil +} + +// GenerateKeyPair generates a new key pair. If a key pair with the same identifier already exists, it is overwritten. +func (client *Crypto) GenerateKeyPair() (crypto.PublicKey, error) { + privateKey, err := client.generateAndStoreKeyPair() + if err != nil { + return nil, err + } + return util.PrivateKeyToPublicKey(privateKey) +} + +func (client *Crypto) generateAndStoreKeyPair() (crypto.PrivateKey, error) { + keyPair, err := generateECKeyPair() + if err != nil { + return nil, err + } + + kid := util.Fingerprint(keyPair.PublicKey) + + if err = client.Storage.SavePrivateKey(kid, keyPair); err != nil { + return nil, err + } + + return keyPair, nil +} + +func (client *Crypto) generateKeyPair() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, client.Config.Keysize) +} + +func generateECKeyPair() (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +} + +// PrivateKeyExists checks storage for an entry for the given legal entity and returns true if it exists +func (client *Crypto) PrivateKeyExists(kid string) bool { + return client.Storage.PrivateKeyExists(kid) +} + +// PublicKeyInPEM loads the key from storage and returns it as PEM encoded. Only supports RSA style keys +func (client *Crypto) GetPublicKeyAsPEM(kid string) (string, error) { + pubKey, err := client.Storage.GetPublicKey(kid) + + if err != nil { + return "", err + } + + return util.PublicKeyToPem(pubKey) +} + +func (client *Crypto) verifyKeySize(keySize int) error { + if keySize < MinRSAKeySize && core.NutsConfig().InStrictMode() { + return ErrInvalidKeySize + } + return nil +} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 0000000000..1dd57ac418 --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,238 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package crypto + +import ( + "crypto/ecdsa" + "crypto/rsa" + "errors" + "os" + "reflect" + "testing" + + "github.com/nuts-foundation/nuts-go-test/io" + "github.com/nuts-foundation/nuts-node/crypto/util" + + "github.com/nuts-foundation/nuts-node/core" + "github.com/spf13/cobra" + + "github.com/nuts-foundation/nuts-node/crypto/storage" + "github.com/stretchr/testify/assert" +) + +func TestCryptoBackend(t *testing.T) { + t.Run("Instance always returns same instance", func(t *testing.T) { + client := Instance() + client2 := Instance() + + if client != client2 { + t.Error("Expected instances to be the same") + } + }) +} + +func TestDefaultCryptoBackend_GenerateKeyPair(t *testing.T) { + createCrypto(t) + + client := createCrypto(t) + + t.Run("A new key pair is stored at config location", func(t *testing.T) { + _, err := client.GenerateKeyPair() + + if err != nil { + t.Errorf("Expected no error, Got %s", err.Error()) + } + }) +} + +func TestCrypto_PublicKeyInPem(t *testing.T) { + client := createCrypto(t) + createCrypto(t) + + publicKey, _ := client.GenerateKeyPair() + kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + + t.Run("Public key is returned from storage", func(t *testing.T) { + pub, err := client.GetPublicKeyAsPEM(kid) + + assert.Nil(t, err) + assert.NotEmpty(t, pub) + }) + + t.Run("Public key for unknown entity returns error", func(t *testing.T) { + _, err := client.GetPublicKeyAsPEM("unknown") + + if assert.Error(t, err) { + assert.True(t, errors.Is(err, storage.ErrNotFound)) + } + }) +} + +func TestCrypto_GetPrivateKey(t *testing.T) { + client := createCrypto(t) + createCrypto(t) + + t.Run("private key not found", func(t *testing.T) { + pk, err := client.GetPrivateKey("unknown") + assert.Nil(t, pk) + assert.Error(t, err) + }) + t.Run("get private key, assert non-exportable", func(t *testing.T) { + publicKey, _ := client.GenerateKeyPair() + kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + + pk, err := client.GetPrivateKey(kid) + if !assert.NoError(t, err) { + return + } + if !assert.NotNil(t, pk) { + return + } + // Assert that we don't accidentally return the actual RSA/ECDSA key, because they should stay in the storage + // and be non-exportable. + _, ok := pk.(*rsa.PrivateKey) + assert.False(t, ok) + _, ok = pk.(*ecdsa.PrivateKey) + assert.False(t, ok) + }) +} + +func TestCrypto_KeyExistsFor(t *testing.T) { + client := createCrypto(t) + createCrypto(t) + + pub, _ := client.GenerateKeyPair() + kid := util.Fingerprint(*(pub.(*ecdsa.PublicKey))) + + t.Run("returns true for existing key", func(t *testing.T) { + assert.True(t, client.PrivateKeyExists(string(kid))) + }) + + t.Run("returns false for non-existing key", func(t *testing.T) { + assert.False(t, client.PrivateKeyExists("does_not_exists")) + }) +} + +func TestCrypto_GenerateKeyPair(t *testing.T) { + client := createCrypto(t) + createCrypto(t) + + t.Run("ok", func(t *testing.T) { + publicKey, err := client.GenerateKeyPair() + assert.NoError(t, err) + assert.NotNil(t, publicKey) + }) +} + +func TestCrypto_doConfigure(t *testing.T) { + t.Run("ok", func(t *testing.T) { + e := createCrypto(t) + err := e.doConfigure() + assert.NoError(t, err) + }) + t.Run("ok - default = fs backend", func(t *testing.T) { + client := createCrypto(t) + err := client.doConfigure() + if !assert.NoError(t, err) { + return + } + storageType := reflect.TypeOf(client.Storage).String() + assert.Equal(t, "*storage.fileSystemBackend", storageType) + }) + t.Run("error - unknown backend", func(t *testing.T) { + client := createCrypto(t) + client.Config.Storage = "unknown" + err := client.doConfigure() + assert.EqualErrorf(t, err, "only fs backend available for now", "expected error") + }) + t.Run("error - keySize is too small", func(t *testing.T) { + // Switch to strict mode just for this test + os.Setenv("NUTS_STRICTMODE", "true") + core.NutsConfig().Load(&cobra.Command{}) + defer core.NutsConfig().Load(&cobra.Command{}) + defer os.Unsetenv("NUTS_STRICTMODE") + e := createCrypto(t) + e.Config.Keysize = 2047 + err := e.doConfigure() + assert.EqualError(t, err, ErrInvalidKeySize.Error()) + }) +} + +func TestCrypto_Configure(t *testing.T) { + createCrypto(t) + + t.Run("ok - configOnce", func(t *testing.T) { + e := createCrypto(t) + assert.False(t, e.configDone) + err := e.Configure() + if !assert.NoError(t, err) { + return + } + assert.True(t, e.configDone) + err = e.Configure() + if !assert.NoError(t, err) { + return + } + assert.True(t, e.configDone) + }) + t.Run("ok - server mode", func(t *testing.T) { + e := createCrypto(t) + e.Config.Keysize = 4096 + err := e.Configure() + assert.NoError(t, err) + }) + t.Run("ok - client mode", func(t *testing.T) { + e := createCrypto(t) + e.Storage = nil + e.Config.Mode = core.ClientEngineMode + err := e.Configure() + assert.NoError(t, err) + // Assert server-mode services aren't initialized in client mode + assert.Nil(t, e.Storage) + }) + t.Run("error - keySize is too small", func(t *testing.T) { + e := createCrypto(t) + assert.False(t, e.configDone) + err := e.Configure() + if !assert.NoError(t, err) { + return + } + assert.True(t, e.configDone) + err = e.Configure() + if !assert.NoError(t, err) { + return + } + assert.True(t, e.configDone) + }) +} + +func createCrypto(t *testing.T) *Crypto { + if err := core.NutsConfig().Load(&cobra.Command{}); err != nil { + panic(err) + } + dir := io.TestDirectory(t) + backend, _ := storage.NewFileSystemBackend(dir) + crypto := Crypto{ + Storage: backend, + Config: TestCryptoConfig(dir), + } + crypto.Config.Keysize = 1024 + + return &crypto +} diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go new file mode 100644 index 0000000000..3f667201b1 --- /dev/null +++ b/crypto/engine/engine.go @@ -0,0 +1,150 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package engine + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/nuts-foundation/nuts-node/core" + crypto2 "github.com/nuts-foundation/nuts-node/crypto" + api "github.com/nuts-foundation/nuts-node/crypto/api/v1" + "github.com/nuts-foundation/nuts-node/crypto/client" + "github.com/nuts-foundation/nuts-node/crypto/types" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// NewCryptoEngine the engine configuration for nuts-go. +func NewCryptoEngine() *core.Engine { + cb := crypto2.Instance() + + return &core.Engine{ + Cmd: cmd(), + Config: &cb.Config, + ConfigKey: "crypto", + Configure: cb.Configure, + FlagSet: flagSet(), + Name: "Crypto", + Routes: func(router core.EchoRouter) { + api.RegisterHandlers(router, &api.Wrapper{C: cb}) + }, + Shutdown: cb.Shutdown, + } +} + +// FlagSet returns the configuration flags for crypto +func flagSet() *pflag.FlagSet { + flags := pflag.NewFlagSet("crypto", pflag.ContinueOnError) + + defs := crypto2.DefaultCryptoConfig() + flags.String(types.ConfigMode, defs.Mode, fmt.Sprintf("Server or client, when client it uses the HttpClient, default: %s", defs.Mode)) + flags.String(types.ConfigAddress, defs.Address, fmt.Sprintf("Interface and port for http server to bind to, default: %s", defs.Address)) + flags.Int(types.ConfigClientTimeout, defs.ClientTimeout, fmt.Sprintf("Time-out for the client in seconds (e.g. when using the CLI), default: %d", defs.ClientTimeout)) + flags.String(types.ConfigStorage, defs.Storage, fmt.Sprintf("Storage to use, 'fs' for file system, default: %s", defs.Storage)) + flags.String(types.ConfigFSPath, defs.Fspath, fmt.Sprintf("When file system is used as storage, this configures the path where key material and the truststore are persisted, default: %v", defs.Fspath)) + flags.Int(types.ConfigKeySize, defs.Keysize, fmt.Sprintf("Number of bits to use when creating new RSA keys, default: %d", defs.Keysize)) + + return flags +} + +// Cmd gives the sub-commands made available through crypto: +// * generateKeyPair: generate a new keyPair for a given legalEntity +// * publicKey: retrieve the keyPair for a given legalEntity +func cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "crypto", + Short: "crypto commands", + } + + cmd.AddCommand(&cobra.Command{ + Use: "server", + Short: "Run standalone crypto server", + Run: func(cmd *cobra.Command, args []string) { + cryptoEngine := crypto2.Instance() + echoServer := echo.New() + echoServer.HideBanner = true + echoServer.Use(middleware.Logger()) + api.RegisterHandlers(echoServer, &api.Wrapper{C: cryptoEngine}) + logrus.Fatal(echoServer.Start(":1324")) + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "generateKeyPair", + Short: "generate a new keyPair", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + cc := client.NewCryptoClient() + if _, err := cc.GenerateKeyPair(); err != nil { + cmd.Printf("Error generating keyPair: %v\n", err) + } else { + cmd.Println("KeyPair generated") + } + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "publicKey [kid]", + Short: "views the publicKey for a given kid", + Long: "views the publicKey for a given kid. It'll output a JWK encoded public key and a PEM encoded public key.", + + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("requires a kid argument") + } + + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + cc := client.NewCryptoClient() + kid := args[0] + + // printout in JWK + jwk, err := cc.GetPublicKeyAsJWK(kid) + if err != nil { + cmd.Printf("Error printing publicKey: %v", err) + return + } + asJSON, err := json.MarshalIndent(jwk, "", " ") + if err != nil { + cmd.Printf("Error printing publicKey: %v\n", err) + return + } + cmd.Println("Public key in JWK:") + cmd.Println(string(asJSON)) + cmd.Println("") + + // printout in PEM + publicKeyAsPEM, err := cc.GetPublicKeyAsPEM(kid) + if err != nil { + cmd.Printf("Error printing publicKey: %v\n", err) + return + } + cmd.Println("Public key in PEM:") + cmd.Println(publicKeyAsPEM) + }, + }) + + return cmd +} diff --git a/crypto/engine/engine_test.go b/crypto/engine/engine_test.go new file mode 100644 index 0000000000..bde7f2468f --- /dev/null +++ b/crypto/engine/engine_test.go @@ -0,0 +1,154 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package engine + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/nuts-foundation/nuts-go-test/io" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/mock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestNewCryptoEngine(t *testing.T) { + t.Run("New returns an engine with Cmd and Routes", func(t *testing.T) { + client := NewCryptoEngine() + + if client.Cmd == nil { + t.Errorf("Expected Engine to have Cmd") + } + + if client.Routes == nil { + t.Errorf("Expected Engine to have Routes") + } + }) +} + +func TestNewCryptoEngine_Routes(t *testing.T) { + t.Run("Registers the available routes", func(t *testing.T) { + ce := NewCryptoEngine() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + echo := mock.NewMockEchoRouter(ctrl) + + echo.EXPECT().POST("/internal/crypto/v1/generate", gomock.Any()) + echo.EXPECT().POST("/internal/crypto/v1/sign_jwt", gomock.Any()) + echo.EXPECT().GET("/internal/crypto/v1/public_key/:kid", gomock.Any()) + + ce.Routes(echo) + }) +} + +func TestNewCryptoEngine_Cmd(t *testing.T) { + os.Setenv("NUTS_IDENTITY", "urn:oid:1.3.6.1.4.1.54851.4:4") + defer os.Unsetenv("NUTS_IDENTITY") + core.NutsConfig().Load(&cobra.Command{}) + + createCmd := func(t *testing.T) (*cobra.Command, *crypto.Crypto) { + testDirectory := io.TestDirectory(t) + instance := crypto.NewTestCryptoInstance(testDirectory) + return NewCryptoEngine().Cmd, instance + } + + t.Run("generateKeyPair", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { + cmd, _ := createCmd(t) + buf := new(bytes.Buffer) + cmd.SetArgs([]string{"generateKeyPair"}) + cmd.SetOut(buf) + err := cmd.Execute() + + if assert.NoError(t, err) { + assert.Contains(t, buf.String(), "KeyPair generated") + } + }) + }) + + t.Run("publicKey", func(t *testing.T) { + t.Run("error - too few arguments", func(t *testing.T) { + cmd, _ := createCmd(t) + cmd.SetArgs([]string{"publicKey"}) + cmd.SetOut(ioutil.Discard) + err := cmd.Execute() + + if assert.Error(t, err) { + assert.Equal(t, "requires a kid argument", err.Error()) + } + }) + + t.Run("error - public key does not exist", func(t *testing.T) { + cmd, _ := createCmd(t) + buf := new(bytes.Buffer) + cmd.SetArgs([]string{"publicKey", "unknown"}) + cmd.SetOut(buf) + err := cmd.Execute() + if !assert.NoError(t, err) { + return + } + }) + }) +} + +func TestNewCryptoEngine_FlagSet(t *testing.T) { + t.Run("Cobra help should list flags", func(t *testing.T) { + e := NewCryptoEngine() + cmd := newRootCommand() + cmd.Flags().AddFlagSet(e.FlagSet) + cmd.SetArgs([]string{"--help"}) + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + + _, err := cmd.ExecuteC() + + if err != nil { + t.Errorf("Expected no error, got %s", err.Error()) + } + + result := buf.String() + + if !strings.Contains(result, "--storage") { + t.Errorf("Expected --storage to be command line flag") + } + + if !strings.Contains(result, "--fspath") { + t.Errorf("Expected --fspath to be command line flag") + } + + }) +} + +func newRootCommand() *cobra.Command { + testRootCommand := &cobra.Command{ + Use: "root", + Run: func(cmd *cobra.Command, args []string) { + + }, + } + + return testRootCommand +} diff --git a/crypto/interface.go b/crypto/interface.go new file mode 100644 index 0000000000..9e8ffa0187 --- /dev/null +++ b/crypto/interface.go @@ -0,0 +1,40 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package crypto + +import ( + "crypto" + + "github.com/lestrrat-go/jwx/jwk" +) + +// Client defines the functions than can be called by a Cmd, Direct or via rest call. +type Client interface { + GenerateKeyPair() (crypto.PublicKey, error) + // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. + GetPrivateKey(kid string) (crypto.Signer, error) + // GetPublicKeyAsPEM returns the PEM encoded PublicKey + GetPublicKeyAsPEM(kid string) (string, error) + // GetPublicKeyAsJWK returns the JWK encoded PublicKey for a given legal entity + GetPublicKeyAsJWK(kid string) (jwk.Key, error) + // SignJWT creates a signed JWT using the given key and map of claims (private key must be present). + SignJWT(claims map[string]interface{}, kid string) (string, error) + // PrivateKeyExists returns if the specified private key exists. + PrivateKeyExists(key string) bool +} diff --git a/crypto/jwx.go b/crypto/jwx.go new file mode 100644 index 0000000000..00c71066af --- /dev/null +++ b/crypto/jwx.go @@ -0,0 +1,115 @@ +/* + * Nuts crypto + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "errors" + + "github.com/dgrijalva/jwt-go" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" +) + +// ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported +var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported") + +// jwsAlgorithm holds the supported (required) JWS signing algorithm +const jwsAlgorithm = jwa.RS256 + +// PublicKeyInJWK loads the key from storage and wraps it in a Key format. Supports RSA, ECDSA and Symmetric style keys +func (client *Crypto) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { + pubKey, err := client.Storage.GetPublicKey(kid) + + if err != nil { + return nil, err + } + + return jwk.New(pubKey) +} + +// SignJwtFor creates a signed JWT given a legalEntity and map of claims +func (client *Crypto) SignJWT(claims map[string]interface{}, kid string) (token string, err error) { + privateKey, err := client.Storage.GetPrivateKey(kid) + + if err != nil { + return "", err + } + additionalHeaders := map[string]interface{}{ + "kid": kid, + } + + token, err = SignJWT(privateKey, claims, additionalHeaders) + return +} + +// SignJWT signs claims with the signer and returns the compacted token. The headers param can be used to add additional headers +func SignJWT(signer crypto.Signer, claims map[string]interface{}, headers map[string]interface{}) (sig string, err error) { + c := jwt.MapClaims{} + for k, v := range claims { + c[k] = v + } + + // the current version of the used JWT lib doesn't support the crypto.Signer interface. The 4.0.0 version will. + switch signer.(type) { + case *rsa.PrivateKey: + token := jwt.NewWithClaims(jwt.SigningMethodPS256, c) + addHeaders(token, headers) + sig, err = token.SignedString(signer.(*rsa.PrivateKey)) + case *ecdsa.PrivateKey: + key := signer.(*ecdsa.PrivateKey) + var method *jwt.SigningMethodECDSA + if method, err = ecSigningMethod(key); err != nil { + return + } + token := jwt.NewWithClaims(method, c) + addHeaders(token, headers) + sig, err = token.SignedString(signer.(*ecdsa.PrivateKey)) + default: + err = errors.New("unsupported signing private key") + } + + return +} + +func addHeaders(token *jwt.Token, headers map[string]interface{}) { + if headers == nil { + return + } + + for k, v := range headers { + token.Header[k] = v + } +} + +func ecSigningMethod(key *ecdsa.PrivateKey) (method *jwt.SigningMethodECDSA, err error) { + switch key.Params().BitSize { + case 256: + method = jwt.SigningMethodES256 + case 384: + method = jwt.SigningMethodES384 + case 521: + method = jwt.SigningMethodES512 + default: + err = ErrUnsupportedSigningKey + } + return +} diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go new file mode 100644 index 0000000000..d4dda18de8 --- /dev/null +++ b/crypto/jwx_test.go @@ -0,0 +1,157 @@ +/* + * Nuts crypto + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package crypto + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + rsa2 "crypto/rsa" + "fmt" + "testing" + + "github.com/dgrijalva/jwt-go" + "github.com/lestrrat-go/jwx/jwa" + "github.com/nuts-foundation/nuts-node/crypto/storage" + "github.com/nuts-foundation/nuts-node/crypto/util" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestSignJWT(t *testing.T) { + claims := map[string]interface{}{"iss": "nuts"} + t.Run("creates valid JWT using rsa keys", func(t *testing.T) { + key, _ := rsa2.GenerateKey(rand.Reader, 2048) + tokenString, err := SignJWT(key, claims, nil) + + assert.Nil(t, err) + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return key.Public(), nil + }) + + assert.True(t, token.Valid) + assert.Equal(t, "nuts", token.Claims.(jwt.MapClaims)["iss"]) + }) + + t.Run("creates valid JWT using ec keys", func(t *testing.T) { + p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + p521, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + + keys := []*ecdsa.PrivateKey{p256, p384, p521} + + for _, key := range keys { + name := fmt.Sprintf("using %s", key.Params().Name) + t.Run(name, func(t *testing.T) { + tokenString, err := SignJWT(key, claims, nil) + + if assert.Nil(t, err) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return key.Public(), nil + }) + + if assert.Nil(t, err) { + assert.True(t, token.Valid) + assert.Equal(t, "nuts", token.Claims.(jwt.MapClaims)["iss"]) + } + } + }) + } + }) + + t.Run("sets correct headers", func(t *testing.T) { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + raw, _ := SignJWT(key, claims, map[string]interface{}{"x5c": []string{"BASE64"}}) + token, _ := jwt.Parse(raw, func(token *jwt.Token) (interface{}, error) { + return key.Public(), nil + }) + + assert.Equal(t, "JWT", token.Header["typ"]) + assert.Equal(t, "ES256", token.Header["alg"]) + assert.Equal(t, []interface{}{"BASE64"}, token.Header["x5c"]) + }) + + t.Run("returns error on unknown curve", func(t *testing.T) { + key, _ := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + _, err := SignJWT(key, claims, nil) + + assert.NotNil(t, err) + }) + + t.Run("returns error on unsupported crypto", func(t *testing.T) { + _, key, _ := ed25519.GenerateKey(rand.Reader) + _, err := SignJWT(key, claims, nil) + + assert.NotNil(t, err) + }) +} + +func TestCrypto_PublicKeyInJWK(t *testing.T) { + client := createCrypto(t) + createCrypto(t) + + publicKey, _ := client.GenerateKeyPair() + kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + + t.Run("Public key is returned from storage", func(t *testing.T) { + pub, err := client.GetPublicKeyAsJWK(kid) + + assert.NoError(t, err) + assert.NotNil(t, pub) + assert.Equal(t, jwa.EC, pub.KeyType()) + }) + + t.Run("Public key for unknown entity returns error", func(t *testing.T) { + _, err := client.GetPublicKeyAsJWK("unknown") + + if assert.Error(t, err) { + assert.True(t, errors.Is(err, storage.ErrNotFound)) + } + }) +} + +func TestCrypto_SignJWT(t *testing.T) { + client := createCrypto(t) + createCrypto(t) + + publicKey, _ := client.GenerateKeyPair() + kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + + t.Run("creates valid JWT", func(t *testing.T) { + tokenString, err := client.SignJWT(map[string]interface{}{"iss": "nuts"}, kid) + + assert.Nil(t, err) + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + pubKey, _ := client.Storage.GetPublicKey(kid) + return pubKey, nil + }) + + assert.True(t, token.Valid) + assert.Equal(t, "nuts", token.Claims.(jwt.MapClaims)["iss"]) + }) + + t.Run("returns error for not found", func(t *testing.T) { + _, err := client.SignJWT(map[string]interface{}{"iss": "nuts"}, "unknown") + + assert.True(t, errors.Is(err, storage.ErrNotFound)) + }) +} diff --git a/crypto/log/log.go b/crypto/log/log.go new file mode 100644 index 0000000000..6c9dcea693 --- /dev/null +++ b/crypto/log/log.go @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package log + +import ( + "github.com/sirupsen/logrus" +) + +var _logger = logrus.StandardLogger().WithField("module", "Crypto") + +func Logger() *logrus.Entry { + return _logger +} diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go new file mode 100644 index 0000000000..ded072d453 --- /dev/null +++ b/crypto/storage/fs.go @@ -0,0 +1,200 @@ +/* + * Nuts crypto + * Copyright (C) 2019 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package storage + +import ( + "bytes" + "crypto" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/nuts-foundation/nuts-node/crypto/util" +) + +type entryType string + +const ( + privateKeyEntry entryType = "private.pem" + publicKeyEntry entryType = "public.pem" +) + +type FileOpenError struct { + filePath string + kid string + err error +} + +// ErrNotFound indicates that the specified crypto storage entry couldn't be found. +var ErrNotFound = errors.New("entry not found") + +// ErrInvalidDuration is given when a period duration is 0 or negative +var ErrInvalidDuration = errors.New("given time period is invalid") + +// Error returns the string representation +func (f *FileOpenError) Error() string { + return fmt.Sprintf("could not open entry %s with filename %s: %v", f.kid, f.filePath, f.err) +} + +// UnWrap is needed for FileOpenError to be UnWrapped +func (f *FileOpenError) Unwrap() error { + return f.err +} + +type fileSystemBackend struct { + fspath string +} + +// Create a new filesystem backend, all directories will be created for the given path +// Using a filesystem backend in production is not recommended! +func NewFileSystemBackend(fspath string) (*fileSystemBackend, error) { + if fspath == "" { + return nil, errors.New("filesystem path is empty") + } + fsc := &fileSystemBackend{ + fspath, + } + + err := fsc.createDirs() + + if err != nil { + return nil, err + } + + return fsc, nil +} + +func (fsc *fileSystemBackend) PrivateKeyExists(kid string) bool { + _, err := os.Stat(fsc.getEntryPath(kid, privateKeyEntry)) + return err == nil +} + +// Load the privatekey for the given legalEntity from disk. Since a legalEntity has a URI as identifier, the URI is base64 encoded and postfixed with '_private.pem'. Keys are stored in pem format and are 2k RSA keys. +func (fsc *fileSystemBackend) GetPrivateKey(kid string) (crypto.Signer, error) { + data, err := fsc.readEntry(kid, privateKeyEntry) + if err != nil { + return nil, err + } + privateKey, err := util.PemToPrivateKey(data) + if err != nil { + return nil, err + } + return privateKey, nil +} + +// Load the public key from disk, it load the private key and extract the public key from it. +func (fsc *fileSystemBackend) GetPublicKey(kid string) (crypto.PublicKey, error) { + data, err := fsc.readEntry(kid, publicKeyEntry) + if err != nil { + return nil, err + } + publicKey, err := util.PemToPublicKey(data) + if err != nil { + return nil, err + } + return publicKey, nil +} + +// Save the private key for the given key to disk. Files are postfixed with '_private.pem'. Keys are stored in pem format. It also store the public key +func (fsc *fileSystemBackend) SavePrivateKey(kid string, key crypto.PrivateKey) error { + filenamePath := fsc.getEntryPath(kid, privateKeyEntry) + outFile, err := os.Create(filenamePath) + + if err != nil { + return err + } + + defer outFile.Close() + + pem, err := util.PrivateKeyToPem(key) + if err != nil { + return err + } + + _, err = outFile.Write([]byte(pem)) + if err != nil { + return err + } + + return fsc.SavePublicKey(kid, key) +} + +// Save the private key for the given key to disk. Files are postfixed with '_private.pem'. Keys are stored in pem format. It also store the public key +func (fsc *fileSystemBackend) SavePublicKey(kid string, key crypto.PrivateKey) error { + filenamePath := fsc.getEntryPath(kid, publicKeyEntry) + outFile, err := os.Create(filenamePath) + + if err != nil { + return err + } + + publicKey, err := util.PrivateKeyToPublicKey(key) + + defer outFile.Close() + + pem, err := util.PublicKeyToPem(publicKey) + if err != nil { + return err + } + + _, err = outFile.Write([]byte(pem)) + + return err +} + +func (fsc fileSystemBackend) readEntry(kid string, entryType entryType) ([]byte, error) { + filePath := fsc.getEntryPath(kid, entryType) + data, err := ioutil.ReadFile(filePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, &FileOpenError{kid: kid, filePath: filePath, err: ErrNotFound} + } + return nil, &FileOpenError{kid: kid, filePath: filePath, err: err} + } + return data, nil +} + +func (fsc fileSystemBackend) getEntryPath(key string, entryType entryType) string { + return filepath.Join(fsc.fspath, getEntryFileName(key, entryType)) +} + +func (fsc *fileSystemBackend) createDirs() error { + f, err := os.Open(fsc.fspath) + + if f != nil { + f.Close() + } + + if err != nil { + err = os.MkdirAll(fsc.fspath, os.ModePerm) + } + + return err +} + +func getEntryFileName(kid string, entryType entryType) string { + buffer := new(bytes.Buffer) + encoder := base64.NewEncoder(base64.StdEncoding, buffer) + encoder.Write([]byte(kid)) + encoder.Close() + return fmt.Sprintf("%s_%s", buffer.String(), entryType) +} diff --git a/crypto/storage/fs_test.go b/crypto/storage/fs_test.go new file mode 100644 index 0000000000..ee0b6d7853 --- /dev/null +++ b/crypto/storage/fs_test.go @@ -0,0 +1,98 @@ +package storage + +import ( + "os" + "testing" + + "github.com/nuts-foundation/nuts-go-test/io" + "github.com/nuts-foundation/nuts-node/crypto/test" + "github.com/nuts-foundation/nuts-node/crypto/util" + + "github.com/stretchr/testify/assert" +) + +func Test_NewFileSystemBackend(t *testing.T) { + t.Run("error - path is empty", func(t *testing.T) { + storage, err := NewFileSystemBackend("") + assert.EqualError(t, err, "filesystem path is empty") + assert.Nil(t, storage) + }) +} + +func Test_fs_GetPublicKey(t *testing.T) { + t.Run("non-existing entry", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + key, err := storage.GetPublicKey("unknown") + assert.Contains(t, err.Error(), "could not open entry unknown with filename") + assert.Nil(t, key) + }) + t.Run("ok", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + pk := test.GenerateECKey() + kid := util.Fingerprint(pk.PublicKey) + err := storage.SavePublicKey(kid, pk) + if !assert.NoError(t, err) { + return + } + key, err := storage.GetPublicKey(kid) + assert.NoError(t, err) + if !assert.NotNil(t, key) { + return + } + assert.Equal(t, &pk.PublicKey, key) + }) +} + +func Test_fs_GetPrivateKey(t *testing.T) { + t.Run("non-existing entry", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + key, err := storage.GetPrivateKey("unknown") + assert.Contains(t, err.Error(), "could not open entry unknown with filename") + assert.Nil(t, key) + }) + t.Run("private key invalid", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + pk := test.GenerateECKey() + kid := util.Fingerprint(pk.PublicKey) + + path := storage.getEntryPath(kid, privateKeyEntry) + file, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + _, err := file.WriteString("hello world") + if !assert.NoError(t, err) { + return + } + key, err := storage.GetPrivateKey(kid) + assert.Nil(t, key) + assert.Error(t, err) + }) + t.Run("ok", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + pk := test.GenerateECKey() + kid := util.Fingerprint(pk.PublicKey) + + err := storage.SavePrivateKey(kid, pk) + if !assert.NoError(t, err) { + return + } + key, err := storage.GetPrivateKey(kid) + assert.NoError(t, err) + if !assert.NotNil(t, key) { + return + } + assert.Equal(t, pk, key) + }) +} + +func Test_fs_KeyExistsFor(t *testing.T) { + t.Run("non-existing entry", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + assert.False(t, storage.PrivateKeyExists("unknown")) + }) + t.Run("existing entry", func(t *testing.T) { + storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + pk := test.GenerateECKey() + kid := util.Fingerprint(pk.PublicKey) + storage.SavePrivateKey(kid, pk) + assert.True(t, storage.PrivateKeyExists(kid)) + }) +} diff --git a/crypto/storage/storage.go b/crypto/storage/storage.go new file mode 100644 index 0000000000..3aa0600e0b --- /dev/null +++ b/crypto/storage/storage.go @@ -0,0 +1,33 @@ +/* + * Nuts crypto + * Copyright (C) 2019 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// The backend package contains the various options for storing the actual private keys. +// Currently only a file backend is supported +package storage + +import ( + "crypto" +) + +// Storage interface containing functions for storing and retrieving keys +type Storage interface { + GetPrivateKey(kid string) (crypto.Signer, error) + GetPublicKey(kid string) (crypto.PublicKey, error) + PrivateKeyExists(kid string) bool + SavePrivateKey(kid string, key crypto.PrivateKey) error +} diff --git a/crypto/test.go b/crypto/test.go new file mode 100644 index 0000000000..546d45c7d6 --- /dev/null +++ b/crypto/test.go @@ -0,0 +1,27 @@ +package crypto + +import ( + "path" + + "github.com/sirupsen/logrus" +) + +// NewTestCryptoInstance returns a new Crypto instance to be used for integration tests. Any data is stored in the +// specified test directory. +func NewTestCryptoInstance(testDirectory string) *Crypto { + config := TestCryptoConfig(testDirectory) + newInstance := NewInstance(config) + if err := newInstance.Configure(); err != nil { + logrus.Fatal(err) + } + instance = newInstance + return newInstance +} + +// TestCryptoConfig returns CryptoConfig to be used in integration/unit tests. +func TestCryptoConfig(testDirectory string) CryptoConfig { + config := DefaultCryptoConfig() + config.Fspath = path.Join(testDirectory, "crypto") + config.Keysize = 1024 + return config +} diff --git a/crypto/test/broken.pem b/crypto/test/broken.pem new file mode 100644 index 0000000000..0d8fe47f4c --- /dev/null +++ b/crypto/test/broken.pem @@ -0,0 +1,6 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+2FbJ7Gct/oZMb9pEjI +ZX7ouh77hOBUyB1kqglFDZMTg4RaT5wFyAFtL+LJMQ4Alzsm0XXpyO1Aw253AJD6 +FQPkc0BBGeqXiXplsznIPujDjJ4na+znzuqnV7hNQ5frhAc8c5Zv3EKoh+lO4Dqn +21kkNYvd3iD2qVafEQEKlQmiEmjVTo/jkQvI833KbbR14HJAtYXrGN8xPqg0rAXU +kRAa6nmP2lLW7QpH8aEYR3EAlL4dzRWggvIaX0qG6/JHph0n0MHlFwryIndlir42 +bujRuOsp0qGirwj4DY6usgNOVUv9WClQpdlYyyZd/gnYby9P7wF5tch2Gtwfxpk0 diff --git a/crypto/test/ec.sk b/crypto/test/ec.sk new file mode 100644 index 0000000000..d263186e15 --- /dev/null +++ b/crypto/test/ec.sk @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDDkxTzFUFbIvBZWH6UlibvdXrhqYOzDb1lKTVSWX8P8OLbuu7Dzp9Ru +1LGfzY1juuygBwYFK4EEACKhZANiAATAncgtph3ACHSPXWyvyYop/71skjBK6Q1T +UB6WFs6pusiUD1pYDMZ01IjBx/cMJaJP/VoyYl24Wbf2/mBnKt1lfDzYYVf0kFxT +dtTkGJrJAzbtHuysgU+GrEdjYSfhDKc= +-----END EC PRIVATE KEY----- diff --git a/crypto/test/ed25519.sk b/crypto/test/ed25519.sk new file mode 100644 index 0000000000..bca7a3b30b --- /dev/null +++ b/crypto/test/ed25519.sk @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIC4ylI95t5FYJ6vcOhUSWB6YARTgyANe8bjAJJh9IerJ +-----END PRIVATE KEY----- diff --git a/crypto/test/keys.go b/crypto/test/keys.go new file mode 100644 index 0000000000..90585edce6 --- /dev/null +++ b/crypto/test/keys.go @@ -0,0 +1,21 @@ +package test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" +) + +func GenerateRSAKey() *rsa.PrivateKey { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + return privateKey +} + +func GenerateECKey() (*ecdsa.PrivateKey) { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + return key +} diff --git a/crypto/test/public_2048.pem b/crypto/test/public_2048.pem new file mode 100644 index 0000000000..d23e9fb9d3 --- /dev/null +++ b/crypto/test/public_2048.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr8LufriX1x/jrXrGp3Ph +yuEca6wm0pe9AG7Ufwgb8FwA3clKPhiPH8DgO2BiEmARM40+JErU9funhdlgmYEj +YOWJVLRyK2LzjxqqxSeJfTAxXeK6c5Ia6oFXFJu0yBH83YYIuVKtjGeHtJJJsnLh +lrbZ/MAGBcw8hXJiSCSOZSPMe7uwDO8luLtiz5v8Olg4fhzkM269SWn2VbyZwMxp +wDh5pMvAIVrWq8xB3vUeeHJednRz3D+xWceLtWvji6P8SMoc9pvQSRX/mUTXNvjR +GYO4MPGWD/cgvIzUL72A4npfYAY3dXEYT7j9qSyvA0oaynsx3TXbsopnHoX2Nerc +RQIDAQAB +-----END PUBLIC KEY----- diff --git a/crypto/test/publickey.pem b/crypto/test/publickey.pem new file mode 100644 index 0000000000..ef3ccaccc7 --- /dev/null +++ b/crypto/test/publickey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+2FbJ7Gct/oZMb9pEjI +ZX7ouh77hOBUyB1kqglFDZMTg4RaT5wFyAFtL+LJMQ4Alzsm0XXpyO1Aw253AJD6 +FQPkc0BBGeqXiXplsznIPujDjJ4na+znzuqnV7hNQ5frhAc8c5Zv3EKoh+lO4Dqn +21kkNYvd3iD2qVafEQEKlQmiEmjVTo/jkQvI833KbbR14HJAtYXrGN8xPqg0rAXU +kRAa6nmP2lLW7QpH8aEYR3EAlL4dzRWggvIaX0qG6/JHph0n0MHlFwryIndlir42 +bujRuOsp0qGirwj4DY6usgNOVUv9WClQpdlYyyZd/gnYby9P7wF5tch2Gtwfxpk0 +DQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/crypto/test/rsa.sk b/crypto/test/rsa.sk new file mode 100644 index 0000000000..0bf6bf48b7 --- /dev/null +++ b/crypto/test/rsa.sk @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAvI38T8kibml2OZdPOenptshXfHQjcBIVHI3ag5na8zaxu/A8 +QYW0n8jLbtecaGaxFxoPYzxUoEqyKegdaPq3IMPnmWgM+cleI5npUB7/lWFOi0qs +VJ5oEKYTHiAVm0UdqMNyVIEAS+Q9NM5gYQn332kQeKD9yw4+1tj1ZBbhgIgVNerO +LNRNIexgqBHgDBrBV7GtxtjB4bnSC50eUm2DaxLqjGGONz6wr5qlH1Nz/sK+1qkf +uiS/BBcLMjX7qX42qBKSWljXQoON7scRKq7ng8UXkgD9eFM4dngmDsYhGu3OlbUe +X9DUHx5tzWjK0fsIN5+xJrsnibt7GTSA0+BpIwIDAQABAoIBAF+6koQLcPCvmHdq +y61yhdbn5groh+lpNNC3cF5qKZBbj2cEdAMsF+Ubs7UFwIH8ySIad/+A7NIoukWu +c+gNihYXgCkRe60BwErA5IRqCIbVzWPIuK+JcPoq5v/feDjJoWJCQHSIvhbJvQ1n +TeVWg0Mo+1TTH6cNB3ha1FNJjpmLHPcnteYx67Z9nawHEK3Gmxm0BhFnD1Fag0zu +EETtl80j86inRsL4HnPHAgi8acu3jUFtYwcixIqscDclG6Vvmm6S00P1fhTeW3zs +jvmi5ibD8d4ygArQHDDBA+o8KrIzy1/H6Le41BuxCX6mim76HGxbipmJCvp4yXdD +GPCedtkCgYEAxAkVAvg8tj4aYAt/ovPmyk/yb7C7D2Q8HFFOqVSdwAEsEFacADTu +8/FOFqr7bUu4z3cZdA00cutgHUDDOmzcpPlVVsvuOZ2zkiCa1C8l+dACF/0YSuJH +J6A7nktV+cJhbQPIKhbAYDqaKahGBnOQi3vI5I5rJgii2AzpRSvvJscCgYEA9jsa +J2UQtTlSBTcfBES3xUN5WkgGiHCRCihRsdHXg2ESL6V87bY+F4RgKXs5+jAjrdwT +JBLifzBCFSmMxQSgCAOILyu2hBzRpIvftxdjSp3bOrdSMeMtdRb6sDFuCdRDTrcp +eoBHhPoS7O/TBs1H//8Hx3B/y+1xpAZXloIR3sUCgYAL8y3Ht5Aj39dFwY2vRkTs +UkFKE7DjeE29wCsWYWUYXjnsaQsrbA6g6jXDZfrbp8EFTJJNo7xtwPFj9x2vgxFU +MSrFlrrX4kgfAUPO6WzcNJTcF36SmgaSYM8hkCAWkIXV2mQqRKbHdusM3Qgfvo2y +IwKVBCV99QrQNsFFiS8T4wKBgDBKVQ3G12kDTd+x+MZIh9YLLqCTIZzensNkNult +4xtkDUIE7aRdKn5IOufHwA4eJNEzKRnZDkytdThbRr1Y892+e5Xst8XfNQpVWFG7 +J4D5xoYUb+1SxZaCJDYr643H8E9ewqbAw8YDmXSYcEWUOvus06S8noOrFK97gvAE +oGaVAoGBAKpfPLP8hit0hHd4baHRn117K/CRsEyXXuW2wObO+hmvEolvJ77KuqGf +lRKjXO2gbKrN/DlUwhdUAuBkxLDhWB2aaMlBUsu7lHKqFgH3RkDTFE2qy+8iw/gC +AOExzvIXyUGMiK4T7esSYA10Gjx037KOGBY4YHvsucKP5Y8yKN7n +-----END RSA PRIVATE KEY----- diff --git a/crypto/test/sk.pem b/crypto/test/sk.pem new file mode 100644 index 0000000000..86ade6e98e --- /dev/null +++ b/crypto/test/sk.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwMR1Stjz9CFiZ +ZX9TfbigNNpj/UdLapdi5j7ZMrCydHSdMX4+cvJHYHZ7UUrVM9N/Pn8OJpTqBQi/ +wH/9EPnu5r4KGKKK+1i+FlFf8AZPs6aiH7EIgZGJWAxQfxIVvViMJmcDnAiDNYk9 +uyPCunm+eT4Q30Lka6gbT/Q33MhngaLqd82i/w5amuNcxmcnsl6yJwvoRp9gAKM+ +a8E9ds3qYPDDeUW6R3XMSVw+9S+FePiLx4hOA6JDSiAGhzcKVRaJHnWE3+dMUSyK +wW/tNUUsoTGBVsGzcDjeuxXHhJlROsQuDSyukeeKRmHOvM/qXP7D+RPjK2eXVtnX +cKaf3CBPAgMBAAECggEBAI4KPXEAXCi2ms57+0QAgQIcv1mKGnM64bOWvWvsUhn2 +TE/5SNLdXvFe1jIu7LYQI+6HotNXdZC+0WG0EPwy4dqpkZCe6OCPqvcec5jsdI8F +inEtlJ+XH928t+uSebtpccfPnfPAfjg5nKNo28j4Ra6iPFX18bTrBUrBiYbPhaPP +HISw/FiZeBsvzY9/dbx3/i36W3R0utqgeQ6cZWxwIT5RVInazLo+s+E4w6hOb9bO +HeO7jtLu8SLaJZwqooZ0cI5abYpDNJI9OU4J489M3zo6ZJiWMwhvwUwok15JKbZ/ +MsrekHaiu7iUd50D9Mirg6iB0bJDR3xQmpN2tZIcnCECgYEA/0Et0h1vG2wmG2VL +gt3b8HU4GKF5UkPjksYEys/HLXuW3oNygiqJWWZNwy8pDnHqd4urunxuFK/prMxm +43eWEzQIHdEK0rfpfak1FUP4FGlOMp3vMvw0yuPVvaRzpRN94SHirYUVwDR5lrZ0 +T9+j3LpSfcTB31eGOkn/ln6lEOUCgYEA8OSs0b0P9R7Bc7ZliOwwCRvt24gCcJky +yPqtlm7P2Z6fP854Yvu0nItP+6kVFB8dz4o7VammOYQwYs3Lrg92poVXDNGazUIJ +YxT/JPAY+d1WwVvee/BdKJ+mOda6+lIGEQi440QY0aCtiQEdPGXt0NE0RYFvM4s/ +k0rZtmwvfSMCgYEA3yZONpiA38pmbiDaKOhoNQllJzNTavXq6A+xdNS83ihjttfX +rbAeL0fex7pc/EHepvA2C2xomDFJ6kUv1cBgNR2R0u9DtQAPYkohHBw1rzJ4qIul +6D7QsGcKHya76x7lN4J2NxhX8ZZujbGocYOkL328TDNNAkH0GNVEWn8RM3kCgYA4 +EaG/97d9IDl6y1t6sS7FEAEe9dtLhfzyFpbMyuIKDweV/GK890UkorBtLP/A/TUd +F1mUKLaN8JyqgqgDzYmaXLLUQv07BUHWFA8G8/N8RO5qdw2j32BvkilIkRhYJztO +P695BmKYeEOr/dxmMHtX/Tmja+sMHj8f824VLb0n7QKBgEuAqdCao4fm9WKFhUPV +XdqXqgqoRgvOVP6qwleIhv/TwFEdW9ZVC1K1h7nvXhlvW71Cm95jyv8DDCfvqlG5 +Bupi1fe4UHfb7h5vldOzY2qr8wfJ5+I+vcqo7p4s8Y7P0ZmTEO/1bVG0GFVa32Gq +B6Moz2dS/7ICo0Itc2OI7RC8 +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/types/types.go b/crypto/types/types.go new file mode 100644 index 0000000000..ddc90f1eb4 --- /dev/null +++ b/crypto/types/types.go @@ -0,0 +1,38 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// types and interfaces used by all other packages +package types + +// --mode config flag +const ConfigMode string = "mode" + +// --address config flag +const ConfigAddress string = "address" + +// --clientTimeout config flag +const ConfigClientTimeout string = "clientTimeout" + +// --storage config flag +const ConfigStorage string = "storage" + +// --fspath config flagclient.getStoragePath() +const ConfigFSPath string = "fspath" + +// --keysize config flag +const ConfigKeySize string = "keysize" diff --git a/crypto/util/common.go b/crypto/util/common.go new file mode 100644 index 0000000000..c7d68297e4 --- /dev/null +++ b/crypto/util/common.go @@ -0,0 +1,52 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "errors" +) + +// ErrWrongPublicKey indicates a wrong public key format +var ErrWrongPublicKey = errors.New("failed to decode PEM block containing public key, key is of the wrong type") + +// ErrWrongPrivateKey indicates a wrong private key format +var ErrWrongPrivateKey = errors.New("failed to decode PEM block containing private key") + +// ErrRsaPubKeyConversion indicates a public key could not be converted to an RSA public key +var ErrRsaPubKeyConversion = errors.New("Unable to convert public key to RSA public key") + + +func PrivateKeyToPublicKey(privateKey crypto.PrivateKey) (publicKey crypto.PublicKey, err error) { + switch privateKey.(type) { + case *rsa.PrivateKey: + publicKey = privateKey.(*rsa.PrivateKey).Public() + case *ecdsa.PrivateKey: + publicKey = privateKey.(*ecdsa.PrivateKey).Public() + case ed25519.PrivateKey: + publicKey = privateKey.(ed25519.PrivateKey).Public() + default: + err = errors.New("unsupported private key type") + } + + return +} diff --git a/crypto/util/jwk.go b/crypto/util/jwk.go new file mode 100644 index 0000000000..dd39272698 --- /dev/null +++ b/crypto/util/jwk.go @@ -0,0 +1,97 @@ +/* + * Nuts node + * Copyright (C) 2021 Nuts community + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "context" + "encoding/json" + "errors" + + "github.com/lestrrat-go/jwx/jwk" + errors2 "github.com/pkg/errors" +) + +// MapToJwk transforms a Jwk in map structure to a Jwk Key. The map structure is a typical result from json deserialization. +func MapToJwk(jwkAsMap map[string]interface{}) (jwk.Key, error) { + set, err := MapsToJwkSet([]map[string]interface{}{jwkAsMap}) + if err != nil { + return nil, err + } + return set.Keys[0], nil +} + +// MapsToJwkSet transforms JWKs in map structures to a JWK set, just like MapToJwk. +func MapsToJwkSet(maps []map[string]interface{}) (*jwk.Set, error) { + set := &jwk.Set{Keys: make([]jwk.Key, len(maps))} + for i, m := range maps { + jwkBytes, err := json.Marshal(m) + if err != nil { + return nil, err + } + key, err := jwk.ParseKey(jwkBytes) + if err != nil { + return nil, err + } + set.Keys[i] = key + } + return set, nil +} + +// ValidateJWK tests whether the given map (all) can is a parsable representation of a JWK. If not, an error is returned. +// If nil is returned, all supplied maps are parsable as JWK. +func ValidateJWK(maps ...interface{}) error { + var stringMaps []map[string]interface{} + for _, currMap := range maps { + keyAsMap, ok := currMap.(map[string]interface{}) + if !ok { + return errors.New("invalid JWK, it is not map[string]interface{}") + } + stringMaps = append(stringMaps, keyAsMap) + } + if _, err := MapsToJwkSet(stringMaps); err != nil { + return errors2.Wrap(err, "invalid JWK") + } + return nil +} + +// deepCopyMap is needed since the jwkSet.extractMap consumes the contents +func deepCopyMap(m map[string]interface{}) map[string]interface{} { + cp := make(map[string]interface{}) + for k, v := range m { + vm, ok := v.(map[string]interface{}) + if ok { + cp[k] = deepCopyMap(vm) + } else { + cp[k] = v + } + } + return cp +} + +// JwkToMap transforms a Jwk key to a map. Can be used for json serialization +func JwkToMap(key jwk.Key) (map[string]interface{}, error) { + return key.AsMap(context.Background()) +} + +// PemToJwk transforms pem to jwk for PublicKey +func PemToJwk(pub []byte) (jwk.Key, error) { + pk, err := PemToPublicKey(pub) + if err != nil { + return nil, err + } + + return jwk.New(pk) +} diff --git a/crypto/util/jwk_test.go b/crypto/util/jwk_test.go new file mode 100644 index 0000000000..92711eae77 --- /dev/null +++ b/crypto/util/jwk_test.go @@ -0,0 +1,146 @@ +/* + * Nuts node + * Copyright (C) 2021 Nuts community + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/nuts-foundation/nuts-node/crypto/test" + "github.com/stretchr/testify/assert" +) + +func TestJwkToMap(t *testing.T) { + t.Run("Generates map for RSA key", func(t *testing.T) { + rsa := test.GenerateRSAKey() + jwk, _ := jwk.New(rsa) + + jwkMap, err := JwkToMap(jwk) + + if assert.NoError(t, err) { + assert.Equal(t, jwa.KeyType("RSA"), jwkMap["kty"]) + } + }) +} + +func TestMapToJwk(t *testing.T) { + t.Run("Generates Jwk from map", func(t *testing.T) { + jwkAsJSON := `{"d":"Ce3obeVsZeU3QaKBTQ-Qn-EaUfhEVViHbnP3gnLDrXNbiUf09s0Ti3RXd4601G8fAJ3zKlZmdEop59mK5BjAE8NOBmvP4uI7PYlJsDAE76mKghVxvN94qb-KwW4p0wix9RoC8TEtoE3EYCr428v-k4nTpMWXQcC_xkHVIfpoA6E","dp":"LGJtrCIxo2DlCSccu0ivH8YzUS9uUbsKyOgNEpV3IB3vqZToi_k8TkwN9XNXCMXkRYIGtRwkxvp9TWLtIEKMtQ","dq":"XhBVCRvFE_ccZ7rxzfu7LToeSNBPW07v68tM94pEV2MFfVBHdWJd-gHbIPGVwC55Th9vAh9dDmv0TvBVkiblkQ","e":"AQAB","kty":"RSA","n":"n5KqvPI1MPDhazTKXLYn4_we09e3iEccb7QJ8dRxApN1rpxTymRWabUafC56fArDF0lvIZ7fZl0LzX5Z_3mrqulebEPTFRrbdDwwcqa2KZ7Tctfh6MgUFm5xOAwRG33NlX3Ny1dP-Ek2irXJOHt9AecbEZFZKmpgrsrTyG6Ekfs","p":"1LoOk3MFiJpsjJCkMkaDb0TXXMxuZ5f9-iMVgR1ZoammzQziBj-72CrD21Rxmuuc6en8w4HtHLSOlPQtcOKzMw","q":"wAiSzr1NVdsYulhGYAa1ONZSKVxlFS7N_UAjPQgFf-xTYog2RbZfolheDv92mJp2qqFJdVMzQkbeMeTj9xqmGQ","qi":"eFqCOgR0wnpkjZGwh63pV8aNhh1-GfhYjqF2jSrh6rnsVHnhz3LRROSzUDarms7LjW3eHiygyHHSF2-ejTMMKQ"}` + + jwkMap := map[string]interface{}{} + json.Unmarshal([]byte(jwkAsJSON), &jwkMap) + + jwk, err := MapToJwk(jwkMap) + + if !assert.NoError(t, err) { + return + } + assert.Equal(t, jwa.KeyType("RSA"), jwk.KeyType()) + assert.NotNil(t, jwkMap["d"], "function altered input map") + }) + + t.Run("with missing data", func(t *testing.T) { + jwkMap := map[string]interface{}{} + _, err := MapToJwk(jwkMap) + + assert.Error(t, err) + }) +} + +func TestMapsToJwkSet(t *testing.T) { + t.Run("Generates set from maps", func(t *testing.T) { + jwkAsJSON := `{"d":"Ce3obeVsZeU3QaKBTQ-Qn-EaUfhEVViHbnP3gnLDrXNbiUf09s0Ti3RXd4601G8fAJ3zKlZmdEop59mK5BjAE8NOBmvP4uI7PYlJsDAE76mKghVxvN94qb-KwW4p0wix9RoC8TEtoE3EYCr428v-k4nTpMWXQcC_xkHVIfpoA6E","dp":"LGJtrCIxo2DlCSccu0ivH8YzUS9uUbsKyOgNEpV3IB3vqZToi_k8TkwN9XNXCMXkRYIGtRwkxvp9TWLtIEKMtQ","dq":"XhBVCRvFE_ccZ7rxzfu7LToeSNBPW07v68tM94pEV2MFfVBHdWJd-gHbIPGVwC55Th9vAh9dDmv0TvBVkiblkQ","e":"AQAB","kty":"RSA","n":"n5KqvPI1MPDhazTKXLYn4_we09e3iEccb7QJ8dRxApN1rpxTymRWabUafC56fArDF0lvIZ7fZl0LzX5Z_3mrqulebEPTFRrbdDwwcqa2KZ7Tctfh6MgUFm5xOAwRG33NlX3Ny1dP-Ek2irXJOHt9AecbEZFZKmpgrsrTyG6Ekfs","p":"1LoOk3MFiJpsjJCkMkaDb0TXXMxuZ5f9-iMVgR1ZoammzQziBj-72CrD21Rxmuuc6en8w4HtHLSOlPQtcOKzMw","q":"wAiSzr1NVdsYulhGYAa1ONZSKVxlFS7N_UAjPQgFf-xTYog2RbZfolheDv92mJp2qqFJdVMzQkbeMeTj9xqmGQ","qi":"eFqCOgR0wnpkjZGwh63pV8aNhh1-GfhYjqF2jSrh6rnsVHnhz3LRROSzUDarms7LjW3eHiygyHHSF2-ejTMMKQ"}` + + jwkMap := map[string]interface{}{} + json.Unmarshal([]byte(jwkAsJSON), &jwkMap) + + set, err := MapsToJwkSet([]map[string]interface{}{jwkMap}) + + if !assert.NoError(t, err) { + return + } + assert.Len(t, set.Keys, 1) + assert.NotNil(t, jwkMap["d"], "function altered input map") + }) + + t.Run("with missing data", func(t *testing.T) { + jwkMap := map[string]interface{}{} + _, err := MapsToJwkSet([]map[string]interface{}{jwkMap}) + + assert.Error(t, err) + }) +} + +func TestPemToJwk(t *testing.T) { + t.Run("generated jwk from pem", func(t *testing.T) { + pub := "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9wJQN59PYsvIsTrFuTqS\nLoUBgwdRfpJxOa5L8nOALxNk41MlAg7xnPbvnYrOHFucfWBTDOMTKBMSmD4WDkaF\ndVrXAML61z85Le8qsXfX6f7TbKMDm2u1O3cye+KdJe8zclK9sTFzSD0PP0wfw7wf\nlACe+PfwQgeOLPUWHaR6aDfaA64QEdfIzk/IL3S595ixaEn0huxMHgXFX35Vok+o\nQdbnclSTo6HUinkqsHUu/hGHApkE3UfT6GD6SaLiB9G4rAhlrDQ71ai872t4FfoK\n7skhe8sP2DstzAQRMf9FcetrNeTxNL7Zt4F/qKm80cchRZiFYPMCYyjQphyBCoJf\n0wIDAQAB\n-----END PUBLIC KEY-----" + + jwk, err := PemToJwk([]byte(pub)) + + if assert.NoError(t, err) { + assert.Equal(t, jwa.KeyType("RSA"), jwk.KeyType()) + } + }) + t.Run("invalid PEM", func(t *testing.T) { + _, err := PemToJwk([]byte("hello world")) + assert.Error(t, err) + }) +} + +func Test_deepCopyMap(t *testing.T) { + expected := map[string]interface{}{} + expected["flat"] = "foobar" + expected["nested"] = map[string]interface{}{ + "nested": map[string]interface{}{ + "nested": map[string]interface{}{ + "value": "ok", + }, + }, + } + + actual := deepCopyMap(expected) + assert.True(t, reflect.DeepEqual(actual, expected)) + // Assert it's actually a copy + delete(expected, "flat") + assert.False(t, reflect.DeepEqual(actual, expected)) +} + +func TestValidateJWK(t *testing.T) { + jwkAsJSON := `{ + "e": "AQAB", + "kty": "RSA", + "n": "n5KqvPI1MPDhazTKXLYn4_we09e3iEccb7QJ8dRxApN1rpxTymRWabUafC56fArDF0lvIZ7fZl0LzX5Z_3mrqulebEPTFRrbdDwwcqa2KZ7Tctfh6MgUFm5xOAwRG33NlX3Ny1dP-Ek2irXJOHt9AecbEZFZKmpgrsrTyG6Ekfs", + "x5c": ["MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYwMTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5jb20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3HKrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQmVZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpRSgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRTcDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEuMB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDSkdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEBBCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0fBD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUHAgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IGOgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMUA2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTXRE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuHqDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWVU+4="] +}` + key := map[string]interface{}{} + json.Unmarshal([]byte(jwkAsJSON), &key) + t.Run("ok", func(t *testing.T) { + assert.NoError(t, ValidateJWK(key, key)) + }) + t.Run("error - invalid Go type", func(t *testing.T) { + invalidMap := map[bool]interface{}{} + assert.Error(t, ValidateJWK(key, invalidMap)) + }) + t.Run("error - invalid JWK", func(t *testing.T) { + invalidMap := map[string]interface{}{ + "kty": "foobar", + } + assert.Error(t, ValidateJWK(key, invalidMap)) + }) +} diff --git a/crypto/util/kid.go b/crypto/util/kid.go new file mode 100644 index 0000000000..2d2c428784 --- /dev/null +++ b/crypto/util/kid.go @@ -0,0 +1,33 @@ +/* + * Nuts node + * Copyright (C) 2021 Nuts community + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + + "github.com/mr-tron/base58" +) + +// Fingerprint calculates the key fingerprint which is used as kid +func Fingerprint(publicKey ecdsa.PublicKey) string { + // calculate kid as BASE-58(SHA-256(raw-public-key-bytes)) + keyBytes := elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y) + sha := sha256.Sum256(keyBytes) + + return base58.Encode(sha[:]) +} diff --git a/crypto/util/pem.go b/crypto/util/pem.go new file mode 100644 index 0000000000..f4b68d59b0 --- /dev/null +++ b/crypto/util/pem.go @@ -0,0 +1,100 @@ +/* + * Nuts node + * Copyright (C) 2021 Nuts community + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" +) + +// PemToPublicKey converts a PEM encoded public key to a crypto.PublicKey +func PemToPublicKey(pub []byte) (crypto.PublicKey, error) { + block, _ := pem.Decode(pub) + if block == nil || block.Type != "PUBLIC KEY" { + return nil, ErrWrongPublicKey + } + + b := block.Bytes + key, err := x509.ParsePKIXPublicKey(b) + if err != nil { + return nil, err + } + return key, nil +} + +// PublicKeyToPem converts an public key to PEM encoding +func PublicKeyToPem(pub crypto.PublicKey) (string, error) { + pubASN1, err := x509.MarshalPKIXPublicKey(pub) + + if err != nil { + return "", err + } + + pubBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubASN1, + }) + + return string(pubBytes), err +} + +// PublicKeyToPem converts an public key to PEM encoding +func PrivateKeyToPem(pub crypto.PrivateKey) (string, error) { + pubASN1, err := x509.MarshalPKCS8PrivateKey(pub) + + if err != nil { + return "", err + } + + pubBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: pubASN1, + }) + + return string(pubBytes), err +} + +// PemToPrivateKey converts a PEM encoded private key to a Signer interface. It supports EC, RSA and PKIX PEM encoded strings +func PemToPrivateKey(bytes []byte) (signer crypto.Signer, err error) { + block, _ := pem.Decode(bytes) + if block == nil { + err = ErrWrongPrivateKey + return + } + + switch block.Type { + case "RSA PRIVATE KEY": + signer, err = x509.ParsePKCS1PrivateKey(block.Bytes) + case "EC PRIVATE KEY": + signer, err = x509.ParseECPrivateKey(block.Bytes) + case "PRIVATE KEY": + var key interface{} + key, err = x509.ParsePKCS8PrivateKey(block.Bytes) + switch key.(type) { + case *rsa.PrivateKey: + signer = key.(*rsa.PrivateKey) + case *ecdsa.PrivateKey: + signer = key.(*ecdsa.PrivateKey) + case ed25519.PrivateKey: + signer = key.(ed25519.PrivateKey) + } + } + return +} diff --git a/crypto/util/pem_test.go b/crypto/util/pem_test.go new file mode 100644 index 0000000000..b7d8159d03 --- /dev/null +++ b/crypto/util/pem_test.go @@ -0,0 +1,115 @@ +/* + * Nuts crypto + * Copyright (C) 2019. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "crypto/ecdsa" + "crypto/rsa" + "encoding/pem" + "io/ioutil" + "testing" + + "github.com/nuts-foundation/nuts-node/crypto/test" + + "github.com/stretchr/testify/assert" +) + +func TestCrypto_PublicKeyToPem(t *testing.T) { + t.Run("ok", func(t *testing.T) { + key := test.GenerateRSAKey() + result, err := PublicKeyToPem(&key.PublicKey) + if !assert.NoError(t, err) { + return + } + assert.NotNil(t, result) + assert.Contains(t, result, "-----BEGIN PUBLIC KEY-----") + assert.Contains(t, result, "-----END PUBLIC KEY-----") + decoded, rest := pem.Decode([]byte(result)) + assert.Len(t, rest, 0) + assert.NotNil(t, decoded) + }) + t.Run("wrong public key gives error", func(t *testing.T) { + _, err := PublicKeyToPem(&rsa.PublicKey{}) + assert.Error(t, err) + }) +} + +func TestCrypto_pemToPublicKey(t *testing.T) { + t.Run("wrong PEM block gives error", func(t *testing.T) { + _, err := PemToPublicKey([]byte{}) + + if err == nil { + t.Errorf("Expected error, Got nothing") + return + } + + expected := "failed to decode PEM block containing public key, key is of the wrong type" + if err.Error() != expected { + t.Errorf("Expected error [%s], got [%s]", expected, err.Error()) + } + }) + + t.Run("converts EC public key", func(t *testing.T) { + pem := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEny33KMxU+mtPxSBMIztm69lehhNo\nCQD632dFAYSzDGh2LqemmYx9EKFzuzvCqbw87BD3spzbakjj5R315qV0gw==\n-----END PUBLIC KEY-----" + + pk, err := PemToPublicKey([]byte(pem)) + if !assert.NoError(t, err) { + return + } + + assert.IsType(t, &ecdsa.PublicKey{}, pk) + }) +} + +func TestPemToSigner(t *testing.T) { + t.Run("Convert ED25519 key", func(t *testing.T) { + pem, _ := ioutil.ReadFile("../test/ed25519.sk") + signer, err := PemToPrivateKey(pem) + assert.NoError(t, err) + assert.NotNil(t, signer) + }) + + t.Run("Convert EC key", func(t *testing.T) { + pem, _ := ioutil.ReadFile("../test/ec.sk") + signer, err := PemToPrivateKey(pem) + assert.NoError(t, err) + assert.NotNil(t, signer) + }) + + t.Run("Convert RSA key", func(t *testing.T) { + pem, _ := ioutil.ReadFile("../test/rsa.sk") + signer, err := PemToPrivateKey(pem) + assert.NoError(t, err) + assert.NotNil(t, signer) + }) + + t.Run("Convert PKIX key", func(t *testing.T) { + pem, _ := ioutil.ReadFile("../test/sk.pem") + signer, err := PemToPrivateKey(pem) + assert.NoError(t, err) + assert.NotNil(t, signer) + }) + + t.Run("Convert garbage", func(t *testing.T) { + _, err := PemToPrivateKey([]byte{}) + if assert.Error(t, err) { + assert.Equal(t, ErrWrongPrivateKey, err) + } + }) +} diff --git a/docs/_static/crypto/v1.yaml b/docs/_static/crypto/v1.yaml new file mode 100644 index 0000000000..2919ac2c82 --- /dev/null +++ b/docs/_static/crypto/v1.yaml @@ -0,0 +1,104 @@ +openapi: "3.0.0" +info: + title: Crypto + description: API specification for crypto services available within nuts node + version: 1.0.0 + license: + name: GPLv3 +paths: + /internal/crypto/v1/generate: + post: + summary: "Generate a new keypair and store the private key" + operationId: generateKeyPair + tags: + - crypto + responses: + '200': + description: "OK response, body holds public key in PEM format when accept format is text/plain and JWK format if accept equals application/json" + content: + text/plain: + schema: + $ref: '#/components/schemas/PublicKey' + application/json: + schema: + $ref: '#/components/schemas/JWK' + /internal/crypto/v1/public_key/{kid}: + get: + summary: "get the public key for a given kid. It returns the key in PEM or JWK format. This depends on the accept header used (text/plain vs application/json)" + operationId: publicKey + tags: + - crypto + parameters: + - name: kid + in: path + schema: + type: string + description: "key identifier in did form" + example: "did:nuts:e3cacd5c2d931295a64f6c3bb3f6ea58c3a9b253b990e32c5abce43c2f94c564#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw" + required: true + responses: + '200': + description: "OK response, body holds public key in PEM/JWK format" + content: + text/plain: + schema: + $ref: '#/components/schemas/PublicKey' + application/json: + schema: + $ref: '#/components/schemas/JWK' + '404': + description: "not found" + content: + text/plain: + example: + "key not found" + + /internal/crypto/v1/sign_jwt: + post: + summary: "sign a JWT payload with the private key of the given kid" + operationId: signJwt + tags: + - crypto + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SignJwtRequest' + responses: + '200': + description: "OK response, body holds JWT" + content: + text/plain: + schema: + example: "aa==.bb==.cc==" + '400': + description: "incorrect data" + content: + text/plain: + example: + "unknown kid" +components: + schemas: + SignJwtRequest: + required: + - claims + - kid + properties: + kid: + type: string + claims: + type: object + PublicKey: + type: string + description: "PEM encoded public key" + example: "-----BEGIN PUBLIC KEY----- .... -----END PUBLIC KEY-----" + JWK: + type: object + description: as described by https://tools.ietf.org/html/rfc7517. Modelled as object so libraries can parse the tokens themselves. + example: { "kty": "EC", + "crv": "P-256", + "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", + "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", + "kid": "Public key used in JWS spec Appendix A.3 example" + } diff --git a/docs/pages/development.rst b/docs/pages/development.rst index 655c726602..f4c908ad61 100644 --- a/docs/pages/development.rst +++ b/docs/pages/development.rst @@ -28,7 +28,7 @@ The server and client API is generated from the open-api spec: .. code-block:: shell - oapi-codegen -generate types,server,client -package api docs/_static/example.yaml > api/generated.go + oapi-codegen -generate types,server,client -package v1 docs/_static/crypto/v1.yaml > api/crypto/v1/generated.go Generating Mocks **************** @@ -37,7 +37,7 @@ These mocks are used by other modules .. code-block:: shell - mockgen -destination=mock/mock_example.go -package=mock -source=example.go + mockgen -destination=mock/mock_crypto.go -package=mock -source=crypto/interface.go Client README ****** diff --git a/go.mod b/go.mod index 9b044c0002..4e5ed35595 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,18 @@ module github.com/nuts-foundation/nuts-node go 1.15 require ( + github.com/deepmap/oapi-codegen v1.4.2 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/golang/mock v1.4.4 + github.com/itchyny/base58-go v0.1.0 github.com/labstack/echo/v4 v4.1.17 + github.com/lestrrat-go/jwx v1.0.7 + github.com/magiconair/properties v1.8.4 + github.com/mr-tron/base58 v1.2.0 + github.com/nuts-foundation/nuts-crypto v0.16.0 + github.com/nuts-foundation/nuts-go-core v0.16.0 + github.com/nuts-foundation/nuts-go-test v0.16.0 + github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 github.com/sirupsen/logrus v1.7.0 github.com/spf13/cobra v1.0.0 diff --git a/go.sum b/go.sum index 5ecbfd7902..6752cb7836 100644 --- a/go.sum +++ b/go.sum @@ -43,16 +43,22 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepmap/oapi-codegen v1.4.1/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= +github.com/deepmap/oapi-codegen v1.4.2 h1:boVMuW+o0sOnEDB8oWRobBm1BrD5d9bUtIQVcjwMsSk= +github.com/deepmap/oapi-codegen v1.4.2/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getkin/kin-openapi v0.26.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -80,6 +86,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -126,6 +133,9 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/itchyny/base58-go v0.1.0 h1:zF5spLDo956exUAD17o+7GamZTRkXOZlqJjRciZwd1I= +github.com/itchyny/base58-go v0.1.0/go.mod h1:SrMWPE3DFuJJp1M/RUhu4fccp/y9AlB8AL3o3duPToU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -142,20 +152,37 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo= github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lestrrat-go/backoff/v2 v2.0.3 h1:2ABaTa5ifB1L90aoRMjaPa97p0WzzVe93Vggv8oZftw= +github.com/lestrrat-go/backoff/v2 v2.0.3/go.mod h1:mU93bMXuG27/Y5erI5E9weqavpTX5qiVFZI4uXAX0xk= +github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3 h1:e52qvXxpJPV/Kb2ovtuYgcRFjNmf9ntcn8BPIbpRM4k= +github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.0.7 h1:wmd6LGb9mYPesObp7oipuZPE1aYYJ1VeUin8LTVNy24= +github.com/lestrrat-go/jwx v1.0.7/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= +github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -174,18 +201,30 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nuts-foundation/nuts-crypto v0.16.0 h1:PzNDcoawuGxl4KRoD3nKnz+1sFiMbEG/S95mYbbso6k= +github.com/nuts-foundation/nuts-crypto v0.16.0/go.mod h1:NS4akgWLGO8RwtzMj2gXDEpucV0S6QDeyLxEkzGSyd4= +github.com/nuts-foundation/nuts-go-core v0.16.0 h1:bHKH0eREWecXLWUexC676RACsLdz/B2tuhkNFzi21FM= +github.com/nuts-foundation/nuts-go-core v0.16.0/go.mod h1:biWHwDgHorQ6diimNbfFFqM/bub27g5/a6IZdUdojS4= +github.com/nuts-foundation/nuts-go-test v0.16.0 h1:jfbh2z44FAsRSPXquTZWviOlB9xb9YeJDzBfPMR5Xz0= +github.com/nuts-foundation/nuts-go-test v0.16.0/go.mod h1:aZ5Urj7v1lH8AD2TIejEiPhRX8t6YG9PVXhuRyNIAII= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -225,12 +264,17 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -242,6 +286,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= @@ -251,10 +296,12 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -266,9 +313,14 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 h1:3wPMTskHO3+O6jqTEXyFcsnuxMQOqYSaHsDxcbUXpqA= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -287,6 +339,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -303,8 +356,12 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200927032502-5d4f70055728 h1:5wtQIAulKU5AbLQOkjxl32UufnIOqgBX72pS0AV14H0= +golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -330,7 +387,9 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -338,6 +397,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200928205150-006507a75852 h1:sXxgOAXy8JwHhZnPuItAlUtwIlxrlEqi28mKhUR+zZY= +golang.org/x/sys v0.0.0-20200928205150-006507a75852/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -362,7 +424,11 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -409,6 +475,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/mock/mock_crypto.go b/mock/mock_crypto.go new file mode 100644 index 0000000000..82194e2f07 --- /dev/null +++ b/mock/mock_crypto.go @@ -0,0 +1,124 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: crypto/interface.go + +// Package mock is a generated GoMock package. +package mock + +import ( + crypto "crypto" + gomock "github.com/golang/mock/gomock" + jwk "github.com/lestrrat-go/jwx/jwk" + reflect "reflect" +) + +// MockClient is a mock of Client interface +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// GenerateKeyPair mocks base method +func (m *MockClient) GenerateKeyPair() (crypto.PublicKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateKeyPair") + ret0, _ := ret[0].(crypto.PublicKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateKeyPair indicates an expected call of GenerateKeyPair +func (mr *MockClientMockRecorder) GenerateKeyPair() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKeyPair", reflect.TypeOf((*MockClient)(nil).GenerateKeyPair)) +} + +// GetPrivateKey mocks base method +func (m *MockClient) GetPrivateKey(kid string) (crypto.Signer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPrivateKey", kid) + ret0, _ := ret[0].(crypto.Signer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPrivateKey indicates an expected call of GetPrivateKey +func (mr *MockClientMockRecorder) GetPrivateKey(kid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateKey", reflect.TypeOf((*MockClient)(nil).GetPrivateKey), kid) +} + +// GetPublicKeyAsPEM mocks base method +func (m *MockClient) GetPublicKeyAsPEM(kid string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPublicKeyAsPEM", kid) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPublicKeyAsPEM indicates an expected call of GetPublicKeyAsPEM +func (mr *MockClientMockRecorder) GetPublicKeyAsPEM(kid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeyAsPEM", reflect.TypeOf((*MockClient)(nil).GetPublicKeyAsPEM), kid) +} + +// GetPublicKeyAsJWK mocks base method +func (m *MockClient) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPublicKeyAsJWK", kid) + ret0, _ := ret[0].(jwk.Key) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPublicKeyAsJWK indicates an expected call of GetPublicKeyAsJWK +func (mr *MockClientMockRecorder) GetPublicKeyAsJWK(kid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeyAsJWK", reflect.TypeOf((*MockClient)(nil).GetPublicKeyAsJWK), kid) +} + +// SignJWT mocks base method +func (m *MockClient) SignJWT(claims map[string]interface{}, kid string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignJWT", claims, kid) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignJWT indicates an expected call of SignJWT +func (mr *MockClientMockRecorder) SignJWT(claims, kid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignJWT", reflect.TypeOf((*MockClient)(nil).SignJWT), claims, kid) +} + +// PrivateKeyExists mocks base method +func (m *MockClient) PrivateKeyExists(key string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrivateKeyExists", key) + ret0, _ := ret[0].(bool) + return ret0 +} + +// PrivateKeyExists indicates an expected call of PrivateKeyExists +func (mr *MockClientMockRecorder) PrivateKeyExists(key interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivateKeyExists", reflect.TypeOf((*MockClient)(nil).PrivateKeyExists), key) +} From e19fe9e86ce4a4d0fecce0bd39a7b9e5792172ae Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Thu, 14 Jan 2021 16:29:38 +0100 Subject: [PATCH 02/27] added todo --- crypto/util/kid.go | 1 + go.mod | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crypto/util/kid.go b/crypto/util/kid.go index 2d2c428784..968b780f0d 100644 --- a/crypto/util/kid.go +++ b/crypto/util/kid.go @@ -24,6 +24,7 @@ import ( ) // Fingerprint calculates the key fingerprint which is used as kid +// todo use jwk lib for fingerprint func Fingerprint(publicKey ecdsa.PublicKey) string { // calculate kid as BASE-58(SHA-256(raw-public-key-bytes)) keyBytes := elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y) diff --git a/go.mod b/go.mod index 4e5ed35595..7f04ed191f 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,10 @@ require ( github.com/deepmap/oapi-codegen v1.4.2 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/golang/mock v1.4.4 - github.com/itchyny/base58-go v0.1.0 github.com/labstack/echo/v4 v4.1.17 github.com/lestrrat-go/jwx v1.0.7 github.com/magiconair/properties v1.8.4 github.com/mr-tron/base58 v1.2.0 - github.com/nuts-foundation/nuts-crypto v0.16.0 - github.com/nuts-foundation/nuts-go-core v0.16.0 github.com/nuts-foundation/nuts-go-test v0.16.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 From 1885c5632a9431bd9a78c34c4e815ddc37bfc89a Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Thu, 14 Jan 2021 17:39:09 +0100 Subject: [PATCH 03/27] some cleanup in crypto --- crypto/api/v1/api.go | 19 +++++++------ crypto/api/v1/api_test.go | 26 +++-------------- crypto/api/v1/client.go | 38 +++---------------------- crypto/api/v1/client_test.go | 16 +++++------ crypto/crypto.go | 55 ++++++------------------------------ crypto/crypto_test.go | 48 ++++++------------------------- crypto/engine/engine.go | 14 ++++++--- crypto/engine/engine_test.go | 3 -- crypto/interface.go | 9 ++---- crypto/jwx.go | 16 ----------- crypto/jwx_test.go | 14 +++++---- crypto/storage/fs.go | 3 +- crypto/storage/fs_test.go | 17 +++++++---- crypto/test.go | 1 - crypto/types/types.go | 6 ---- crypto/util/common.go | 20 ------------- crypto/util/kid.go | 27 ++++++++++-------- 17 files changed, 96 insertions(+), 236 deletions(-) diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index fb9957c751..c17a1349ff 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -109,13 +109,19 @@ func (w *Wrapper) SignJwt(ctx echo.Context) error { func (w *Wrapper) PublicKey(ctx echo.Context, kid string) error { acceptHeader := ctx.Request().Header.Get("Accept") + pubKey, err := w.C.GetPublicKey(kid) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return ctx.NoContent(404) + } + log.Logger().Error(err.Error()) + return err + } + // starts with so we can ignore any + if ct, _, _ := mime.ParseMediaType(acceptHeader); ct == "application/json" { - jwk, err := w.C.GetPublicKeyAsJWK(kid) + jwk, err := jwk.New(pubKey) if err != nil { - if errors.Is(err, storage.ErrNotFound) { - return ctx.NoContent(404) - } log.Logger().Error(err.Error()) return err } @@ -124,11 +130,8 @@ func (w *Wrapper) PublicKey(ctx echo.Context, kid string) error { } // backwards compatible PEM format is the default - pub, err := w.C.GetPublicKeyAsPEM(kid) + pub, err := util.PublicKeyToPem(pubKey) if err != nil { - if errors.Is(err, storage.ErrNotFound) { - return ctx.NoContent(404) - } log.Logger().Error(err.Error()) return err } diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go index b9758f0e5d..7e907bc81d 100644 --- a/crypto/api/v1/api_test.go +++ b/crypto/api/v1/api_test.go @@ -20,9 +20,7 @@ package v1 import ( "bytes" - "crypto/ecdsa" "encoding/json" - "errors" "io/ioutil" "net/http" "strings" @@ -33,7 +31,6 @@ import ( "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-go-test/io" "github.com/nuts-foundation/nuts-node/crypto" - "github.com/nuts-foundation/nuts-node/crypto/storage" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/nuts-foundation/nuts-node/mock" "github.com/stretchr/testify/assert" @@ -96,7 +93,7 @@ func TestWrapper_SignJwt(t *testing.T) { client := apiWrapper(t) publicKey, _ := client.C.GenerateKeyPair() - kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + kid, _ := util.Fingerprint(publicKey) t.Run("Missing claims returns 400", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -185,7 +182,7 @@ func TestWrapper_PublicKey(t *testing.T) { client := apiWrapper(t) publicKey, _ := client.C.GenerateKeyPair() - kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + kid, _ := util.Fingerprint(publicKey) t.Run("PublicKey API call returns 200", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -233,22 +230,7 @@ func TestWrapper_PublicKey(t *testing.T) { } func apiWrapper(t *testing.T) *Wrapper { - backend, _ := storage.NewFileSystemBackend(io.TestDirectory(t)) - crypto := crypto.Crypto{ - Storage: backend, - Config: crypto.DefaultCryptoConfig(), - } - crypto.Config.Keysize = 1024 - - return &Wrapper{C: &crypto} -} - -type errorCloser struct{} - -func (errorCloser) Read([]byte) (n int, err error) { - return 0, errors.New("error") -} + crypto := crypto.NewTestCryptoInstance(io.TestDirectory(t)) -func (errorCloser) Close() error { - return errors.New("error") + return &Wrapper{C: crypto} } diff --git a/crypto/api/v1/client.go b/crypto/api/v1/client.go index 593558aed0..8cc3857d5f 100644 --- a/crypto/api/v1/client.go +++ b/crypto/api/v1/client.go @@ -20,7 +20,6 @@ package v1 import ( - "bytes" "context" "crypto" "errors" @@ -76,7 +75,7 @@ func (hb HttpClient) GenerateKeyPair() (crypto.PublicKey, error) { return jwkSet.Keys[0], nil } -func (hb HttpClient) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { +func (hb HttpClient) GetPublicKey(kid string) (crypto.PublicKey, error) { ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) defer cancel() httpClient := hb.clientWithRequestEditor(func(ctx context.Context, req *http.Request) error { @@ -97,47 +96,18 @@ func (hb HttpClient) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { return jwkSet.Keys[0], nil } -func (hb HttpClient) GetPrivateKey(kid string) (crypto.Signer, error) { +func (hb HttpClient) GetPrivateKey(string) (crypto.Signer, error) { panic(ErrNotImplemented) } -func (hb HttpClient) GetPublicKeyAsPEM(kid string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) - defer cancel() - httpClient := hb.clientWithRequestEditor(func(ctx context.Context, req *http.Request) error { - req.Header.Add("Accept", "text/plain") - return nil - }) - response, err := httpClient.PublicKey(ctx, kid) - if err != nil { - return "", err - } - if err := testResponseCode(http.StatusOK, response); err != nil { - return "", err - } - pemBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - return "", err - } - return string(pemBytes), nil -} - -func (hb HttpClient) SignJWT(claims map[string]interface{}, kid string) (string, error) { +func (hb HttpClient) SignJWT(map[string]interface{}, string) (string, error) { panic(ErrNotImplemented) } -func (hb HttpClient) PrivateKeyExists(key string) bool { +func (hb HttpClient) PrivateKeyExists(string) bool { panic(ErrNotImplemented) } -func readResponse(response *http.Response) ([]byte, error) { - buf := new(bytes.Buffer) - if _, err := buf.ReadFrom(response.Body); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - func testResponseCode(expectedStatusCode int, response *http.Response) error { if response.StatusCode != expectedStatusCode { responseData, _ := ioutil.ReadAll(response.Body) diff --git a/crypto/api/v1/client_test.go b/crypto/api/v1/client_test.go index 0e2d2a1e54..866932aa21 100644 --- a/crypto/api/v1/client_test.go +++ b/crypto/api/v1/client_test.go @@ -76,11 +76,11 @@ func TestHttpClient_GenerateKeyPair(t *testing.T) { }) } -func TestHttpClient_GetPublicKeyAsJWK(t *testing.T) { +func TestHttpClient_GetPublicKey(t *testing.T) { t.Run("ok", func(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GetPublicKeyAsJWK("kid") + res, err := c.GetPublicKey("kid") if !assert.NoError(t, err) { return } @@ -90,14 +90,14 @@ func TestHttpClient_GetPublicKeyAsJWK(t *testing.T) { csrBytes, _ := ioutil.ReadFile("../test/broken.pem") s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: csrBytes}) c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GetPublicKeyAsJWK("kid") + res, err := c.GetPublicKey("kid") assert.Contains(t, err.Error(), "failed to unmarshal JWK:") assert.Nil(t, res) }) t.Run("error - response not HTTP OK", func(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError, responseData: genericError}) c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GetPublicKeyAsJWK("kid") + res, err := c.GetPublicKey("kid") assert.EqualError(t, err, "server returned HTTP 500 (expected: 200), response: failed") assert.Nil(t, res) }) @@ -105,7 +105,7 @@ func TestHttpClient_GetPublicKeyAsJWK(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusOK}) s.Close() c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GetPublicKeyAsJWK("kid") + res, err := c.GetPublicKey("kid") assert.Contains(t, err.Error(), "connection refused") assert.Nil(t, res) }) @@ -116,13 +116,13 @@ func TestHttpClient_NonImplemented(t *testing.T) { funcs := map[string]func(){ "GetPrivateKey": func() { - c.GetPrivateKey("") + c.GetPrivateKey("kid") }, "SignJWT": func() { - c.SignJWT(nil, "") + c.SignJWT(nil, "kid") }, "PrivateKeyExists": func() { - c.PrivateKeyExists("") + c.PrivateKeyExists("kid") }, } for fnName, fn := range funcs { diff --git a/crypto/crypto.go b/crypto/crypto.go index 3435460b8f..92a7bdc624 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -23,41 +23,21 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/rsa" "errors" - "fmt" "io" "sync" + "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto/storage" "github.com/nuts-foundation/nuts-node/crypto/util" ) -// MinRSAKeySize defines the minimum RSA key size -const MinRSAKeySize = 2048 - -// MinECKeySize defines the minimum EC key size -const MinECKeySize = 256 - -// ErrInvalidKeySize is returned when the keySize for new keys is too short -var ErrInvalidKeySize = errors.New(fmt.Sprintf("invalid keySize, needs to be at least %d bits for RSA and %d bits for EC", MinRSAKeySize, MinECKeySize)) - -// ErrInvalidKeyIdentifier is returned when the provided key identifier isn't valid -var ErrInvalidKeyIdentifier = errors.New("invalid key identifier") - -// ErrInvalidAlgorithm indicates an invalid public key was used -var ErrInvalidAlgorithm = errors.New("invalid algorithm for public key") - -// ErrKeyAlreadyExists indicates that the key already exists. -var ErrKeyAlreadyExists = errors.New("key already exists") - // CryptoConfig holds the values for the crypto engine type CryptoConfig struct { Mode string Address string ClientTimeout int - Keysize int Storage string Fspath string } @@ -74,7 +54,6 @@ func DefaultCryptoConfig() CryptoConfig { return CryptoConfig{ Address: "localhost:1323", ClientTimeout: 10, - Keysize: 2048, Storage: "fs", Fspath: "./", } @@ -151,9 +130,6 @@ func (client *Crypto) Configure() error { } func (client *Crypto) doConfigure() error { - if err := client.verifyKeySize(client.Config.Keysize); err != nil { - return err - } if client.Config.Storage != "fs" && client.Config.Storage != "" { return errors.New("only fs backend available for now") } @@ -170,7 +146,8 @@ func (client *Crypto) GenerateKeyPair() (crypto.PublicKey, error) { if err != nil { return nil, err } - return util.PrivateKeyToPublicKey(privateKey) + + return jwk.PublicKeyOf(privateKey) } func (client *Crypto) generateAndStoreKeyPair() (crypto.PrivateKey, error) { @@ -179,7 +156,10 @@ func (client *Crypto) generateAndStoreKeyPair() (crypto.PrivateKey, error) { return nil, err } - kid := util.Fingerprint(keyPair.PublicKey) + kid, err := util.Fingerprint(keyPair.PublicKey) + if err != nil { + return nil, err + } if err = client.Storage.SavePrivateKey(kid, keyPair); err != nil { return nil, err @@ -188,10 +168,6 @@ func (client *Crypto) generateAndStoreKeyPair() (crypto.PrivateKey, error) { return keyPair, nil } -func (client *Crypto) generateKeyPair() (*rsa.PrivateKey, error) { - return rsa.GenerateKey(rand.Reader, client.Config.Keysize) -} - func generateECKeyPair() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) } @@ -202,19 +178,6 @@ func (client *Crypto) PrivateKeyExists(kid string) bool { } // PublicKeyInPEM loads the key from storage and returns it as PEM encoded. Only supports RSA style keys -func (client *Crypto) GetPublicKeyAsPEM(kid string) (string, error) { - pubKey, err := client.Storage.GetPublicKey(kid) - - if err != nil { - return "", err - } - - return util.PublicKeyToPem(pubKey) -} - -func (client *Crypto) verifyKeySize(keySize int) error { - if keySize < MinRSAKeySize && core.NutsConfig().InStrictMode() { - return ErrInvalidKeySize - } - return nil +func (client *Crypto) GetPublicKey(kid string) (crypto.PublicKey, error) { + return client.Storage.GetPublicKey(kid) } diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 1dd57ac418..93b39704a7 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -22,7 +22,6 @@ import ( "crypto/ecdsa" "crypto/rsa" "errors" - "os" "reflect" "testing" @@ -47,36 +46,21 @@ func TestCryptoBackend(t *testing.T) { }) } -func TestDefaultCryptoBackend_GenerateKeyPair(t *testing.T) { - createCrypto(t) - +func TestCrypto_PublicKey(t *testing.T) { client := createCrypto(t) - t.Run("A new key pair is stored at config location", func(t *testing.T) { - _, err := client.GenerateKeyPair() - - if err != nil { - t.Errorf("Expected no error, Got %s", err.Error()) - } - }) -} - -func TestCrypto_PublicKeyInPem(t *testing.T) { - client := createCrypto(t) - createCrypto(t) - publicKey, _ := client.GenerateKeyPair() - kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + kid, _ := util.Fingerprint(publicKey) t.Run("Public key is returned from storage", func(t *testing.T) { - pub, err := client.GetPublicKeyAsPEM(kid) + pub, err := client.GetPublicKey(kid) assert.Nil(t, err) assert.NotEmpty(t, pub) }) t.Run("Public key for unknown entity returns error", func(t *testing.T) { - _, err := client.GetPublicKeyAsPEM("unknown") + _, err := client.GetPublicKey("unknown") if assert.Error(t, err) { assert.True(t, errors.Is(err, storage.ErrNotFound)) @@ -86,7 +70,6 @@ func TestCrypto_PublicKeyInPem(t *testing.T) { func TestCrypto_GetPrivateKey(t *testing.T) { client := createCrypto(t) - createCrypto(t) t.Run("private key not found", func(t *testing.T) { pk, err := client.GetPrivateKey("unknown") @@ -95,7 +78,7 @@ func TestCrypto_GetPrivateKey(t *testing.T) { }) t.Run("get private key, assert non-exportable", func(t *testing.T) { publicKey, _ := client.GenerateKeyPair() - kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + kid, _ := util.Fingerprint(publicKey) pk, err := client.GetPrivateKey(kid) if !assert.NoError(t, err) { @@ -115,23 +98,21 @@ func TestCrypto_GetPrivateKey(t *testing.T) { func TestCrypto_KeyExistsFor(t *testing.T) { client := createCrypto(t) - createCrypto(t) pub, _ := client.GenerateKeyPair() - kid := util.Fingerprint(*(pub.(*ecdsa.PublicKey))) + kid, _ := util.Fingerprint(pub) t.Run("returns true for existing key", func(t *testing.T) { - assert.True(t, client.PrivateKeyExists(string(kid))) + assert.True(t, client.PrivateKeyExists(kid)) }) t.Run("returns false for non-existing key", func(t *testing.T) { - assert.False(t, client.PrivateKeyExists("does_not_exists")) + assert.False(t, client.PrivateKeyExists("unknown")) }) } func TestCrypto_GenerateKeyPair(t *testing.T) { client := createCrypto(t) - createCrypto(t) t.Run("ok", func(t *testing.T) { publicKey, err := client.GenerateKeyPair() @@ -161,17 +142,6 @@ func TestCrypto_doConfigure(t *testing.T) { err := client.doConfigure() assert.EqualErrorf(t, err, "only fs backend available for now", "expected error") }) - t.Run("error - keySize is too small", func(t *testing.T) { - // Switch to strict mode just for this test - os.Setenv("NUTS_STRICTMODE", "true") - core.NutsConfig().Load(&cobra.Command{}) - defer core.NutsConfig().Load(&cobra.Command{}) - defer os.Unsetenv("NUTS_STRICTMODE") - e := createCrypto(t) - e.Config.Keysize = 2047 - err := e.doConfigure() - assert.EqualError(t, err, ErrInvalidKeySize.Error()) - }) } func TestCrypto_Configure(t *testing.T) { @@ -193,7 +163,6 @@ func TestCrypto_Configure(t *testing.T) { }) t.Run("ok - server mode", func(t *testing.T) { e := createCrypto(t) - e.Config.Keysize = 4096 err := e.Configure() assert.NoError(t, err) }) @@ -232,7 +201,6 @@ func createCrypto(t *testing.T) *Crypto { Storage: backend, Config: TestCryptoConfig(dir), } - crypto.Config.Keysize = 1024 return &crypto } diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go index 3f667201b1..c629ea8fb9 100644 --- a/crypto/engine/engine.go +++ b/crypto/engine/engine.go @@ -25,11 +25,13 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/core" crypto2 "github.com/nuts-foundation/nuts-node/crypto" api "github.com/nuts-foundation/nuts-node/crypto/api/v1" "github.com/nuts-foundation/nuts-node/crypto/client" "github.com/nuts-foundation/nuts-node/crypto/types" + "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -59,11 +61,9 @@ func flagSet() *pflag.FlagSet { defs := crypto2.DefaultCryptoConfig() flags.String(types.ConfigMode, defs.Mode, fmt.Sprintf("Server or client, when client it uses the HttpClient, default: %s", defs.Mode)) - flags.String(types.ConfigAddress, defs.Address, fmt.Sprintf("Interface and port for http server to bind to, default: %s", defs.Address)) flags.Int(types.ConfigClientTimeout, defs.ClientTimeout, fmt.Sprintf("Time-out for the client in seconds (e.g. when using the CLI), default: %d", defs.ClientTimeout)) flags.String(types.ConfigStorage, defs.Storage, fmt.Sprintf("Storage to use, 'fs' for file system, default: %s", defs.Storage)) flags.String(types.ConfigFSPath, defs.Fspath, fmt.Sprintf("When file system is used as storage, this configures the path where key material and the truststore are persisted, default: %v", defs.Fspath)) - flags.Int(types.ConfigKeySize, defs.Keysize, fmt.Sprintf("Number of bits to use when creating new RSA keys, default: %d", defs.Keysize)) return flags } @@ -120,8 +120,14 @@ func cmd() *cobra.Command { cc := client.NewCryptoClient() kid := args[0] + pubKey, err := cc.GetPublicKey(kid) + if err != nil { + cmd.Printf("Error printing publicKey: %v", err) + return + } + // printout in JWK - jwk, err := cc.GetPublicKeyAsJWK(kid) + jwk, err := jwk.New(pubKey) if err != nil { cmd.Printf("Error printing publicKey: %v", err) return @@ -136,7 +142,7 @@ func cmd() *cobra.Command { cmd.Println("") // printout in PEM - publicKeyAsPEM, err := cc.GetPublicKeyAsPEM(kid) + publicKeyAsPEM, err := util.PublicKeyToPem(pubKey) if err != nil { cmd.Printf("Error printing publicKey: %v\n", err) return diff --git a/crypto/engine/engine_test.go b/crypto/engine/engine_test.go index bde7f2468f..68887e09d3 100644 --- a/crypto/engine/engine_test.go +++ b/crypto/engine/engine_test.go @@ -21,7 +21,6 @@ package engine import ( "bytes" "io/ioutil" - "os" "strings" "testing" @@ -64,8 +63,6 @@ func TestNewCryptoEngine_Routes(t *testing.T) { } func TestNewCryptoEngine_Cmd(t *testing.T) { - os.Setenv("NUTS_IDENTITY", "urn:oid:1.3.6.1.4.1.54851.4:4") - defer os.Unsetenv("NUTS_IDENTITY") core.NutsConfig().Load(&cobra.Command{}) createCmd := func(t *testing.T) (*cobra.Command, *crypto.Crypto) { diff --git a/crypto/interface.go b/crypto/interface.go index 9e8ffa0187..f7ed2f2f88 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -20,19 +20,16 @@ package crypto import ( "crypto" - - "github.com/lestrrat-go/jwx/jwk" ) // Client defines the functions than can be called by a Cmd, Direct or via rest call. type Client interface { + // GenerateKeyPair generates a keypair, stores the private key and returns the public key. GenerateKeyPair() (crypto.PublicKey, error) // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. GetPrivateKey(kid string) (crypto.Signer, error) - // GetPublicKeyAsPEM returns the PEM encoded PublicKey - GetPublicKeyAsPEM(kid string) (string, error) - // GetPublicKeyAsJWK returns the JWK encoded PublicKey for a given legal entity - GetPublicKeyAsJWK(kid string) (jwk.Key, error) + // GetPublicKey returns the PublicKey + GetPublicKey(kid string) (crypto.PublicKey, error) // SignJWT creates a signed JWT using the given key and map of claims (private key must be present). SignJWT(claims map[string]interface{}, kid string) (string, error) // PrivateKeyExists returns if the specified private key exists. diff --git a/crypto/jwx.go b/crypto/jwx.go index 00c71066af..491cb9ed69 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -25,27 +25,11 @@ import ( "errors" "github.com/dgrijalva/jwt-go" - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwk" ) // ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported") -// jwsAlgorithm holds the supported (required) JWS signing algorithm -const jwsAlgorithm = jwa.RS256 - -// PublicKeyInJWK loads the key from storage and wraps it in a Key format. Supports RSA, ECDSA and Symmetric style keys -func (client *Crypto) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { - pubKey, err := client.Storage.GetPublicKey(kid) - - if err != nil { - return nil, err - } - - return jwk.New(pubKey) -} - // SignJwtFor creates a signed JWT given a legalEntity and map of claims func (client *Crypto) SignJWT(claims map[string]interface{}, kid string) (token string, err error) { privateKey, err := client.Storage.GetPrivateKey(kid) diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index d4dda18de8..eafda01f70 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -29,6 +29,7 @@ import ( "github.com/dgrijalva/jwt-go" "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/crypto/storage" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/pkg/errors" @@ -109,18 +110,21 @@ func TestCrypto_PublicKeyInJWK(t *testing.T) { createCrypto(t) publicKey, _ := client.GenerateKeyPair() - kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + kid, _ := util.Fingerprint(publicKey) t.Run("Public key is returned from storage", func(t *testing.T) { - pub, err := client.GetPublicKeyAsJWK(kid) + pub, err := client.GetPublicKey(kid) assert.NoError(t, err) assert.NotNil(t, pub) - assert.Equal(t, jwa.EC, pub.KeyType()) + + jwkKey, _ := jwk.New(pub) + + assert.Equal(t, jwa.EC, jwkKey.KeyType()) }) t.Run("Public key for unknown entity returns error", func(t *testing.T) { - _, err := client.GetPublicKeyAsJWK("unknown") + _, err := client.GetPublicKey("unknown") if assert.Error(t, err) { assert.True(t, errors.Is(err, storage.ErrNotFound)) @@ -133,7 +137,7 @@ func TestCrypto_SignJWT(t *testing.T) { createCrypto(t) publicKey, _ := client.GenerateKeyPair() - kid := util.Fingerprint(*publicKey.(*ecdsa.PublicKey)) + kid, _ := util.Fingerprint(publicKey) t.Run("creates valid JWT", func(t *testing.T) { tokenString, err := client.SignJWT(map[string]interface{}{"iss": "nuts"}, kid) diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go index ded072d453..fd1ba720f9 100644 --- a/crypto/storage/fs.go +++ b/crypto/storage/fs.go @@ -28,6 +28,7 @@ import ( "os" "path/filepath" + "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/crypto/util" ) @@ -147,7 +148,7 @@ func (fsc *fileSystemBackend) SavePublicKey(kid string, key crypto.PrivateKey) e return err } - publicKey, err := util.PrivateKeyToPublicKey(key) + publicKey, err := jwk.PublicKeyOf(key) defer outFile.Close() diff --git a/crypto/storage/fs_test.go b/crypto/storage/fs_test.go index ee0b6d7853..77b5de3f89 100644 --- a/crypto/storage/fs_test.go +++ b/crypto/storage/fs_test.go @@ -29,8 +29,10 @@ func Test_fs_GetPublicKey(t *testing.T) { t.Run("ok", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid := util.Fingerprint(pk.PublicKey) + kid, _ := util.Fingerprint(pk) + err := storage.SavePublicKey(kid, pk) + if !assert.NoError(t, err) { return } @@ -46,35 +48,40 @@ func Test_fs_GetPublicKey(t *testing.T) { func Test_fs_GetPrivateKey(t *testing.T) { t.Run("non-existing entry", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) + key, err := storage.GetPrivateKey("unknown") + assert.Contains(t, err.Error(), "could not open entry unknown with filename") assert.Nil(t, key) }) t.Run("private key invalid", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid := util.Fingerprint(pk.PublicKey) - + kid, _ := util.Fingerprint(pk.PublicKey) path := storage.getEntryPath(kid, privateKeyEntry) file, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) _, err := file.WriteString("hello world") if !assert.NoError(t, err) { return } + key, err := storage.GetPrivateKey(kid) + assert.Nil(t, key) assert.Error(t, err) }) t.Run("ok", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid := util.Fingerprint(pk.PublicKey) + kid, _ := util.Fingerprint(pk) err := storage.SavePrivateKey(kid, pk) if !assert.NoError(t, err) { return } + key, err := storage.GetPrivateKey(kid) + assert.NoError(t, err) if !assert.NotNil(t, key) { return @@ -91,7 +98,7 @@ func Test_fs_KeyExistsFor(t *testing.T) { t.Run("existing entry", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid := util.Fingerprint(pk.PublicKey) + kid, _ := util.Fingerprint(pk) storage.SavePrivateKey(kid, pk) assert.True(t, storage.PrivateKeyExists(kid)) }) diff --git a/crypto/test.go b/crypto/test.go index 546d45c7d6..0f588daf01 100644 --- a/crypto/test.go +++ b/crypto/test.go @@ -22,6 +22,5 @@ func NewTestCryptoInstance(testDirectory string) *Crypto { func TestCryptoConfig(testDirectory string) CryptoConfig { config := DefaultCryptoConfig() config.Fspath = path.Join(testDirectory, "crypto") - config.Keysize = 1024 return config } diff --git a/crypto/types/types.go b/crypto/types/types.go index ddc90f1eb4..733ec3f1a8 100644 --- a/crypto/types/types.go +++ b/crypto/types/types.go @@ -22,9 +22,6 @@ package types // --mode config flag const ConfigMode string = "mode" -// --address config flag -const ConfigAddress string = "address" - // --clientTimeout config flag const ConfigClientTimeout string = "clientTimeout" @@ -33,6 +30,3 @@ const ConfigStorage string = "storage" // --fspath config flagclient.getStoragePath() const ConfigFSPath string = "fspath" - -// --keysize config flag -const ConfigKeySize string = "keysize" diff --git a/crypto/util/common.go b/crypto/util/common.go index c7d68297e4..8070c7fe8f 100644 --- a/crypto/util/common.go +++ b/crypto/util/common.go @@ -19,10 +19,6 @@ package util import ( - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/rsa" "errors" ) @@ -34,19 +30,3 @@ var ErrWrongPrivateKey = errors.New("failed to decode PEM block containing priva // ErrRsaPubKeyConversion indicates a public key could not be converted to an RSA public key var ErrRsaPubKeyConversion = errors.New("Unable to convert public key to RSA public key") - - -func PrivateKeyToPublicKey(privateKey crypto.PrivateKey) (publicKey crypto.PublicKey, err error) { - switch privateKey.(type) { - case *rsa.PrivateKey: - publicKey = privateKey.(*rsa.PrivateKey).Public() - case *ecdsa.PrivateKey: - publicKey = privateKey.(*ecdsa.PrivateKey).Public() - case ed25519.PrivateKey: - publicKey = privateKey.(ed25519.PrivateKey).Public() - default: - err = errors.New("unsupported private key type") - } - - return -} diff --git a/crypto/util/kid.go b/crypto/util/kid.go index 968b780f0d..99b15b57fe 100644 --- a/crypto/util/kid.go +++ b/crypto/util/kid.go @@ -16,19 +16,24 @@ package util import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/sha256" + "crypto" + "encoding/base64" - "github.com/mr-tron/base58" + "github.com/lestrrat-go/jwx/jwk" ) -// Fingerprint calculates the key fingerprint which is used as kid -// todo use jwk lib for fingerprint -func Fingerprint(publicKey ecdsa.PublicKey) string { - // calculate kid as BASE-58(SHA-256(raw-public-key-bytes)) - keyBytes := elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y) - sha := sha256.Sum256(keyBytes) +// Thumbprint returns the JWK thumbprint using the indicated +// hashing algorithm, according to RFC 7638 +func Fingerprint(key interface{}) (string, error) { + k, err := jwk.New(key) + if err != nil { + return "", err + } - return base58.Encode(sha[:]) + tp, err := k.Thumbprint(crypto.SHA256) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(tp), nil } From 7d93bed962b6d16117584d1c2e40ce032bf37da5 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 08:24:26 +0100 Subject: [PATCH 04/27] removed generateKeys from API, removed Client interface --- crypto/api/v1/api_test.go | 63 ++---------------- crypto/api/v1/client.go | 29 --------- crypto/api/v1/client_test.go | 50 --------------- crypto/api/v1/generated.go | 110 -------------------------------- crypto/api/v1/generated_test.go | 44 ------------- crypto/client/client.go | 42 ------------ crypto/client/client_test.go | 25 -------- crypto/crypto.go | 7 +- crypto/crypto_test.go | 28 -------- crypto/engine/engine.go | 31 ++++----- crypto/engine/engine_test.go | 15 ----- docs/_static/crypto/v1.yaml | 16 ----- 12 files changed, 21 insertions(+), 439 deletions(-) delete mode 100644 crypto/client/client.go delete mode 100644 crypto/client/client_test.go diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go index 7e907bc81d..1c6ac14751 100644 --- a/crypto/api/v1/api_test.go +++ b/crypto/api/v1/api_test.go @@ -23,73 +23,24 @@ import ( "encoding/json" "io/ioutil" "net/http" - "strings" + "os" "testing" "github.com/golang/mock/gomock" - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-go-test/io" + "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/nuts-foundation/nuts-node/mock" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) -type pubKeyMatcher struct { -} - -func (p pubKeyMatcher) Matches(x interface{}) bool { - s := x.(string) - - return strings.Contains(s, "-----BEGIN PUBLIC KEY-----") -} - -func (p pubKeyMatcher) String() string { - return "Public Key Matcher" -} - -type jwkMatcher struct { -} - -func (p jwkMatcher) Matches(x interface{}) bool { - key := x.(jwk.Key) - - return key.KeyType() == jwa.EC -} - -func (p jwkMatcher) String() string { - return "JWK Matcher" -} - -func TestWrapper_GenerateKeyPair(t *testing.T) { - - t.Run("GenerateKeyPairAPI call returns 200 with pub in PEM format", func(t *testing.T) { - se := apiWrapper(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) - - echo.EXPECT().Request().Return(&http.Request{}) - echo.EXPECT().String(http.StatusOK, pubKeyMatcher{}) - - se.GenerateKeyPair(echo) - }) - - t.Run("GenerateKeyPairAPI call returns 200 with pub in JWK format", func(t *testing.T) { - se := apiWrapper(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) - - echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) - echo.EXPECT().JSON(http.StatusOK, jwkMatcher{}) - - se.GenerateKeyPair(echo) - }) -} - func TestWrapper_SignJwt(t *testing.T) { + os.Setenv("NUTS_MODE", "server") + defer os.Unsetenv("NUTS_MODE") + core.NutsConfig().Load(&cobra.Command{}) + client := apiWrapper(t) publicKey, _ := client.C.GenerateKeyPair() diff --git a/crypto/api/v1/client.go b/crypto/api/v1/client.go index 8cc3857d5f..b6f64b2df9 100644 --- a/crypto/api/v1/client.go +++ b/crypto/api/v1/client.go @@ -58,23 +58,6 @@ func (hb HttpClient) client() ClientInterface { return hb.clientWithRequestEditor(nil) } -func (hb HttpClient) GenerateKeyPair() (crypto.PublicKey, error) { - ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) - defer cancel() - response, err := hb.client().GenerateKeyPair(ctx) - if err != nil { - return nil, err - } - if err := testResponseCode(http.StatusOK, response); err != nil { - return nil, err - } - jwkSet, err := jwk.Parse(response.Body) - if err != nil { - return nil, err - } - return jwkSet.Keys[0], nil -} - func (hb HttpClient) GetPublicKey(kid string) (crypto.PublicKey, error) { ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) defer cancel() @@ -96,18 +79,6 @@ func (hb HttpClient) GetPublicKey(kid string) (crypto.PublicKey, error) { return jwkSet.Keys[0], nil } -func (hb HttpClient) GetPrivateKey(string) (crypto.Signer, error) { - panic(ErrNotImplemented) -} - -func (hb HttpClient) SignJWT(map[string]interface{}, string) (string, error) { - panic(ErrNotImplemented) -} - -func (hb HttpClient) PrivateKeyExists(string) bool { - panic(ErrNotImplemented) -} - func testResponseCode(expectedStatusCode int, response *http.Response) error { if response.StatusCode != expectedStatusCode { responseData, _ := ioutil.ReadAll(response.Body) diff --git a/crypto/api/v1/client_test.go b/crypto/api/v1/client_test.go index 866932aa21..41f48cda50 100644 --- a/crypto/api/v1/client_test.go +++ b/crypto/api/v1/client_test.go @@ -49,33 +49,6 @@ var jwkAsString = ` }` var jwkAsBytes = []byte(jwkAsString) -func TestHttpClient_GenerateKeyPair(t *testing.T) { - t.Run("ok", func(t *testing.T) { - s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GenerateKeyPair() - if !assert.NoError(t, err) { - return - } - assert.NotNil(t, res) - }) - t.Run("error - response not HTTP OK", func(t *testing.T) { - s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError, responseData: genericError}) - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GenerateKeyPair() - assert.EqualError(t, err, "server returned HTTP 500 (expected: 200), response: failed") - assert.Nil(t, res) - }) - t.Run("error - server not running", func(t *testing.T) { - s := httptest.NewServer(handler{statusCode: http.StatusOK}) - s.Close() - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} - res, err := c.GenerateKeyPair() - assert.Contains(t, err.Error(), "connection refused") - assert.Nil(t, res) - }) -} - func TestHttpClient_GetPublicKey(t *testing.T) { t.Run("ok", func(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) @@ -110,26 +83,3 @@ func TestHttpClient_GetPublicKey(t *testing.T) { assert.Nil(t, res) }) } - -func TestHttpClient_NonImplemented(t *testing.T) { - c := HttpClient{ServerAddress: "foo", Timeout: time.Second} - - funcs := map[string]func(){ - "GetPrivateKey": func() { - c.GetPrivateKey("kid") - }, - "SignJWT": func() { - c.SignJWT(nil, "kid") - }, - "PrivateKeyExists": func() { - c.PrivateKeyExists("kid") - }, - } - for fnName, fn := range funcs { - t.Run(fnName+" should panic", func(t *testing.T) { - assert.PanicsWithValue(t, ErrNotImplemented, func() { - fn() - }) - }) - } -} diff --git a/crypto/api/v1/generated.go b/crypto/api/v1/generated.go index 25e7ccda1b..cee6a0d18a 100644 --- a/crypto/api/v1/generated.go +++ b/crypto/api/v1/generated.go @@ -109,9 +109,6 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { - // GenerateKeyPair request - GenerateKeyPair(ctx context.Context) (*http.Response, error) - // PublicKey request PublicKey(ctx context.Context, kid string) (*http.Response, error) @@ -121,21 +118,6 @@ type ClientInterface interface { SignJwt(ctx context.Context, body SignJwtJSONRequestBody) (*http.Response, error) } -func (c *Client) GenerateKeyPair(ctx context.Context) (*http.Response, error) { - req, err := NewGenerateKeyPairRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if c.RequestEditor != nil { - err = c.RequestEditor(ctx, req) - if err != nil { - return nil, err - } - } - return c.Client.Do(req) -} - func (c *Client) PublicKey(ctx context.Context, kid string) (*http.Response, error) { req, err := NewPublicKeyRequest(c.Server, kid) if err != nil { @@ -181,33 +163,6 @@ func (c *Client) SignJwt(ctx context.Context, body SignJwtJSONRequestBody) (*htt return c.Client.Do(req) } -// NewGenerateKeyPairRequest generates requests for GenerateKeyPair -func NewGenerateKeyPairRequest(server string) (*http.Request, error) { - var err error - - queryUrl, err := url.Parse(server) - if err != nil { - return nil, err - } - - basePath := fmt.Sprintf("/internal/crypto/v1/generate") - if basePath[0] == '/' { - basePath = basePath[1:] - } - - queryUrl, err = queryUrl.Parse(basePath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryUrl.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - // NewPublicKeyRequest generates requests for PublicKey func NewPublicKeyRequest(server string, kid string) (*http.Request, error) { var err error @@ -310,9 +265,6 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { - // GenerateKeyPair request - GenerateKeyPairWithResponse(ctx context.Context) (*GenerateKeyPairResponse, error) - // PublicKey request PublicKeyWithResponse(ctx context.Context, kid string) (*PublicKeyResponse, error) @@ -322,27 +274,6 @@ type ClientWithResponsesInterface interface { SignJwtWithResponse(ctx context.Context, body SignJwtJSONRequestBody) (*SignJwtResponse, error) } -type GenerateKeyPairResponse struct { - Body []byte - HTTPResponse *http.Response -} - -// Status returns HTTPResponse.Status -func (r GenerateKeyPairResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r GenerateKeyPairResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - type PublicKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -386,15 +317,6 @@ func (r SignJwtResponse) StatusCode() int { return 0 } -// GenerateKeyPairWithResponse request returning *GenerateKeyPairResponse -func (c *ClientWithResponses) GenerateKeyPairWithResponse(ctx context.Context) (*GenerateKeyPairResponse, error) { - rsp, err := c.GenerateKeyPair(ctx) - if err != nil { - return nil, err - } - return ParseGenerateKeyPairResponse(rsp) -} - // PublicKeyWithResponse request returning *PublicKeyResponse func (c *ClientWithResponses) PublicKeyWithResponse(ctx context.Context, kid string) (*PublicKeyResponse, error) { rsp, err := c.PublicKey(ctx, kid) @@ -421,25 +343,6 @@ func (c *ClientWithResponses) SignJwtWithResponse(ctx context.Context, body Sign return ParseSignJwtResponse(rsp) } -// ParseGenerateKeyPairResponse parses an HTTP response from a GenerateKeyPairWithResponse call -func ParseGenerateKeyPairResponse(rsp *http.Response) (*GenerateKeyPairResponse, error) { - bodyBytes, err := ioutil.ReadAll(rsp.Body) - defer rsp.Body.Close() - if err != nil { - return nil, err - } - - response := &GenerateKeyPairResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - } - - return response, nil -} - // ParsePublicKeyResponse parses an HTTP response from a PublicKeyWithResponse call func ParsePublicKeyResponse(rsp *http.Response) (*PublicKeyResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) @@ -490,9 +393,6 @@ func ParseSignJwtResponse(rsp *http.Response) (*SignJwtResponse, error) { // ServerInterface represents all server handlers. type ServerInterface interface { - // Generate a new keypair and store the private key - // (POST /internal/crypto/v1/generate) - GenerateKeyPair(ctx echo.Context) error // get the public key for a given kid. It returns the key in PEM or JWK format. This depends on the accept header used (text/plain vs application/json) // (GET /internal/crypto/v1/public_key/{kid}) PublicKey(ctx echo.Context, kid string) error @@ -506,15 +406,6 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } -// GenerateKeyPair converts echo context to params. -func (w *ServerInterfaceWrapper) GenerateKeyPair(ctx echo.Context) error { - var err error - - // Invoke the callback with all the unmarshalled arguments - err = w.Handler.GenerateKeyPair(ctx) - return err -} - // PublicKey converts echo context to params. func (w *ServerInterfaceWrapper) PublicKey(ctx echo.Context) error { var err error @@ -568,7 +459,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL Handler: si, } - router.POST(baseURL+"/internal/crypto/v1/generate", wrapper.GenerateKeyPair) router.GET(baseURL+"/internal/crypto/v1/public_key/:kid", wrapper.PublicKey) router.POST(baseURL+"/internal/crypto/v1/sign_jwt", wrapper.SignJwt) diff --git a/crypto/api/v1/generated_test.go b/crypto/api/v1/generated_test.go index f4bca72583..b353a55257 100644 --- a/crypto/api/v1/generated_test.go +++ b/crypto/api/v1/generated_test.go @@ -20,9 +20,7 @@ package v1 import ( "errors" - "net/http" "net/http/httptest" - "net/url" "testing" "github.com/golang/mock/gomock" @@ -81,54 +79,12 @@ func TestServerInterfaceWrapper_SignJwt(t *testing.T) { } } -func TestServerInterfaceWrapper_GenerateKeyPair(t *testing.T) { - t.Run("GenerateKeyPairAPI call returns no error", func(t *testing.T) { - // given - siw := serverInterfaceWrapper(nil) - q := make(url.Values) - q.Set("legalEntity", "le") - req := httptest.NewRequest(echo.POST, "/?"+q.Encode(), nil) - rec := httptest.NewRecorder() - c := echo.New().NewContext(req, rec) - - // then - if err := siw.GenerateKeyPair(c); err != nil { - t.Errorf("Got err during call: %s", err.Error()) - } - - if rec.Code != http.StatusOK { - t.Errorf("Got status=%d, want %d", rec.Code, http.StatusOK) - } - }) - - t.Run("Server error is returned", func(t *testing.T) { - // given - siw := serverInterfaceWrapper(errors.New("Server error")) - q := make(url.Values) - q.Set("legalEntity", "le") - req := httptest.NewRequest(echo.POST, "/?"+q.Encode(), nil) - rec := httptest.NewRecorder() - c := echo.New().NewContext(req, rec) - - // then - if err := siw.GenerateKeyPair(c); err != nil { - expected := "Server error" - if err.Error() != expected { - t.Errorf("Expected error [%s], got [%s]", expected, err.Error()) - } - } else { - t.Errorf("Expected error for bad request") - } - }) -} - func TestRegisterHandlers(t *testing.T) { t.Run("Registers routes for crypto module", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() echo := mock.NewMockEchoRouter(ctrl) - echo.EXPECT().POST("/internal/crypto/v1/generate", gomock.Any()) echo.EXPECT().POST("/internal/crypto/v1/sign_jwt", gomock.Any()) echo.EXPECT().GET("/internal/crypto/v1/public_key/:kid", gomock.Any()) diff --git a/crypto/client/client.go b/crypto/client/client.go deleted file mode 100644 index c8d1a1fbbe..0000000000 --- a/crypto/client/client.go +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Nuts registry - * Copyright (C) 2020. Nuts community - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package client - -import ( - "time" - - "github.com/nuts-foundation/nuts-node/core" - - api "github.com/nuts-foundation/nuts-node/crypto/api/v1" - "github.com/nuts-foundation/nuts-node/crypto" -) - -// NewCryptoClient creates a new local or remote client, depending on engine configuration. -func NewCryptoClient() crypto.Client { - instance := crypto.Instance() - if core.NutsConfig().GetEngineMode(instance.Config.Mode) == core.ServerEngineMode { - return instance - } - - return api.HttpClient{ - ServerAddress: instance.Config.Address, - Timeout: time.Duration(instance.Config.ClientTimeout) * time.Second, - } -} diff --git a/crypto/client/client_test.go b/crypto/client/client_test.go deleted file mode 100644 index f7d4e2067d..0000000000 --- a/crypto/client/client_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package client - -import ( - "os" - "testing" - - "github.com/nuts-foundation/nuts-node/core" - "github.com/nuts-foundation/nuts-node/crypto" - v1 "github.com/nuts-foundation/nuts-node/crypto/api/v1" - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" -) - -func TestNewCryptoClient_ServerMode(t *testing.T) { - _, ok := NewCryptoClient().(*crypto.Crypto) - assert.True(t, ok) -} - -func TestNewCryptoClient_ClientMode(t *testing.T) { - os.Setenv("NUTS_MODE", "cli") - defer os.Unsetenv("NUTS_MODE") - core.NutsConfig().Load(&cobra.Command{}) - _, ok := NewCryptoClient().(v1.HttpClient) - assert.True(t, ok) -} diff --git a/crypto/crypto.go b/crypto/crypto.go index 92a7bdc624..9de8241d79 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -35,9 +35,6 @@ import ( // CryptoConfig holds the values for the crypto engine type CryptoConfig struct { - Mode string - Address string - ClientTimeout int Storage string Fspath string } @@ -52,8 +49,6 @@ func (cc CryptoConfig) getFSPath() string { func DefaultCryptoConfig() CryptoConfig { return CryptoConfig{ - Address: "localhost:1323", - ClientTimeout: 10, Storage: "fs", Fspath: "./", } @@ -119,7 +114,7 @@ func NewInstance(config CryptoConfig) *Crypto { func (client *Crypto) Configure() error { var err error client.configOnce.Do(func() { - if core.NutsConfig().GetEngineMode(client.Config.Mode) != core.ServerEngineMode { + if core.NutsConfig().Mode() != core.ServerEngineMode { return } if err = client.doConfigure(); err == nil { diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 93b39704a7..1244b67cc6 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -161,34 +161,6 @@ func TestCrypto_Configure(t *testing.T) { } assert.True(t, e.configDone) }) - t.Run("ok - server mode", func(t *testing.T) { - e := createCrypto(t) - err := e.Configure() - assert.NoError(t, err) - }) - t.Run("ok - client mode", func(t *testing.T) { - e := createCrypto(t) - e.Storage = nil - e.Config.Mode = core.ClientEngineMode - err := e.Configure() - assert.NoError(t, err) - // Assert server-mode services aren't initialized in client mode - assert.Nil(t, e.Storage) - }) - t.Run("error - keySize is too small", func(t *testing.T) { - e := createCrypto(t) - assert.False(t, e.configDone) - err := e.Configure() - if !assert.NoError(t, err) { - return - } - assert.True(t, e.configDone) - err = e.Configure() - if !assert.NoError(t, err) { - return - } - assert.True(t, e.configDone) - }) } func createCrypto(t *testing.T) *Crypto { diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go index c629ea8fb9..6b944da430 100644 --- a/crypto/engine/engine.go +++ b/crypto/engine/engine.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -29,7 +30,6 @@ import ( "github.com/nuts-foundation/nuts-node/core" crypto2 "github.com/nuts-foundation/nuts-node/crypto" api "github.com/nuts-foundation/nuts-node/crypto/api/v1" - "github.com/nuts-foundation/nuts-node/crypto/client" "github.com/nuts-foundation/nuts-node/crypto/types" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/sirupsen/logrus" @@ -60,8 +60,6 @@ func flagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("crypto", pflag.ContinueOnError) defs := crypto2.DefaultCryptoConfig() - flags.String(types.ConfigMode, defs.Mode, fmt.Sprintf("Server or client, when client it uses the HttpClient, default: %s", defs.Mode)) - flags.Int(types.ConfigClientTimeout, defs.ClientTimeout, fmt.Sprintf("Time-out for the client in seconds (e.g. when using the CLI), default: %d", defs.ClientTimeout)) flags.String(types.ConfigStorage, defs.Storage, fmt.Sprintf("Storage to use, 'fs' for file system, default: %s", defs.Storage)) flags.String(types.ConfigFSPath, defs.Fspath, fmt.Sprintf("When file system is used as storage, this configures the path where key material and the truststore are persisted, default: %v", defs.Fspath)) @@ -90,20 +88,6 @@ func cmd() *cobra.Command { }, }) - cmd.AddCommand(&cobra.Command{ - Use: "generateKeyPair", - Short: "generate a new keyPair", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cc := client.NewCryptoClient() - if _, err := cc.GenerateKeyPair(); err != nil { - cmd.Printf("Error generating keyPair: %v\n", err) - } else { - cmd.Println("KeyPair generated") - } - }, - }) - cmd.AddCommand(&cobra.Command{ Use: "publicKey [kid]", Short: "views the publicKey for a given kid", @@ -117,7 +101,7 @@ func cmd() *cobra.Command { return nil }, Run: func(cmd *cobra.Command, args []string) { - cc := client.NewCryptoClient() + cc := newCryptoClient(cmd) kid := args[0] pubKey, err := cc.GetPublicKey(kid) @@ -154,3 +138,14 @@ func cmd() *cobra.Command { return cmd } + +// newCryptoClient creates a remote client +func newCryptoClient(cmd *cobra.Command) api.HttpClient { + cfg := core.NutsConfig() + cfg.Load(cmd) + + return api.HttpClient{ + ServerAddress: cfg.ServerAddress(), + Timeout: 10 * time.Second, + } +} diff --git a/crypto/engine/engine_test.go b/crypto/engine/engine_test.go index 68887e09d3..922bdb0a82 100644 --- a/crypto/engine/engine_test.go +++ b/crypto/engine/engine_test.go @@ -54,7 +54,6 @@ func TestNewCryptoEngine_Routes(t *testing.T) { defer ctrl.Finish() echo := mock.NewMockEchoRouter(ctrl) - echo.EXPECT().POST("/internal/crypto/v1/generate", gomock.Any()) echo.EXPECT().POST("/internal/crypto/v1/sign_jwt", gomock.Any()) echo.EXPECT().GET("/internal/crypto/v1/public_key/:kid", gomock.Any()) @@ -71,20 +70,6 @@ func TestNewCryptoEngine_Cmd(t *testing.T) { return NewCryptoEngine().Cmd, instance } - t.Run("generateKeyPair", func(t *testing.T) { - t.Run("ok", func(t *testing.T) { - cmd, _ := createCmd(t) - buf := new(bytes.Buffer) - cmd.SetArgs([]string{"generateKeyPair"}) - cmd.SetOut(buf) - err := cmd.Execute() - - if assert.NoError(t, err) { - assert.Contains(t, buf.String(), "KeyPair generated") - } - }) - }) - t.Run("publicKey", func(t *testing.T) { t.Run("error - too few arguments", func(t *testing.T) { cmd, _ := createCmd(t) diff --git a/docs/_static/crypto/v1.yaml b/docs/_static/crypto/v1.yaml index 2919ac2c82..71c07f73ec 100644 --- a/docs/_static/crypto/v1.yaml +++ b/docs/_static/crypto/v1.yaml @@ -6,22 +6,6 @@ info: license: name: GPLv3 paths: - /internal/crypto/v1/generate: - post: - summary: "Generate a new keypair and store the private key" - operationId: generateKeyPair - tags: - - crypto - responses: - '200': - description: "OK response, body holds public key in PEM format when accept format is text/plain and JWK format if accept equals application/json" - content: - text/plain: - schema: - $ref: '#/components/schemas/PublicKey' - application/json: - schema: - $ref: '#/components/schemas/JWK' /internal/crypto/v1/public_key/{kid}: get: summary: "get the public key for a given kid. It returns the key in PEM or JWK format. This depends on the accept header used (text/plain vs application/json)" From 25ba8051c3e90b31f3a199e363838117638eb68e Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 08:25:33 +0100 Subject: [PATCH 05/27] renamed crypto interface to KeyStore --- crypto/api/v1/api.go | 2 +- crypto/interface.go | 4 +- docs/pages/development.rst | 2 +- mock/mock_crypto.go | 76 +++++++++++++++----------------------- 4 files changed, 34 insertions(+), 50 deletions(-) diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index c17a1349ff..eaef7a91bf 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -38,7 +38,7 @@ import ( // Wrapper implements the generated interface from oapi-codegen type Wrapper struct { - C crypto.Client + C crypto.KeyStore } // GenerateKeyPair is the implementation of the REST service call POST /crypto/generate diff --git a/crypto/interface.go b/crypto/interface.go index f7ed2f2f88..faeb6b70f0 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -22,8 +22,8 @@ import ( "crypto" ) -// Client defines the functions than can be called by a Cmd, Direct or via rest call. -type Client interface { +// KeyStore defines the functions than can be called by a Cmd, Direct or via rest call. +type KeyStore interface { // GenerateKeyPair generates a keypair, stores the private key and returns the public key. GenerateKeyPair() (crypto.PublicKey, error) // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. diff --git a/docs/pages/development.rst b/docs/pages/development.rst index f4c908ad61..4a7f961d09 100644 --- a/docs/pages/development.rst +++ b/docs/pages/development.rst @@ -37,7 +37,7 @@ These mocks are used by other modules .. code-block:: shell - mockgen -destination=mock/mock_crypto.go -package=mock -source=crypto/interface.go Client + mockgen -destination=mock/mock_crypto.go -package=mock -source=crypto/interface.go KeyStore README ****** diff --git a/mock/mock_crypto.go b/mock/mock_crypto.go index 82194e2f07..72f54b210c 100644 --- a/mock/mock_crypto.go +++ b/mock/mock_crypto.go @@ -7,35 +7,34 @@ package mock import ( crypto "crypto" gomock "github.com/golang/mock/gomock" - jwk "github.com/lestrrat-go/jwx/jwk" reflect "reflect" ) -// MockClient is a mock of Client interface -type MockClient struct { +// MockKeyStore is a mock of KeyStore interface +type MockKeyStore struct { ctrl *gomock.Controller - recorder *MockClientMockRecorder + recorder *MockKeyStoreMockRecorder } -// MockClientMockRecorder is the mock recorder for MockClient -type MockClientMockRecorder struct { - mock *MockClient +// MockKeyStoreMockRecorder is the mock recorder for MockKeyStore +type MockKeyStoreMockRecorder struct { + mock *MockKeyStore } -// NewMockClient creates a new mock instance -func NewMockClient(ctrl *gomock.Controller) *MockClient { - mock := &MockClient{ctrl: ctrl} - mock.recorder = &MockClientMockRecorder{mock} +// NewMockKeyStore creates a new mock instance +func NewMockKeyStore(ctrl *gomock.Controller) *MockKeyStore { + mock := &MockKeyStore{ctrl: ctrl} + mock.recorder = &MockKeyStoreMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockClient) EXPECT() *MockClientMockRecorder { +func (m *MockKeyStore) EXPECT() *MockKeyStoreMockRecorder { return m.recorder } // GenerateKeyPair mocks base method -func (m *MockClient) GenerateKeyPair() (crypto.PublicKey, error) { +func (m *MockKeyStore) GenerateKeyPair() (crypto.PublicKey, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GenerateKeyPair") ret0, _ := ret[0].(crypto.PublicKey) @@ -44,13 +43,13 @@ func (m *MockClient) GenerateKeyPair() (crypto.PublicKey, error) { } // GenerateKeyPair indicates an expected call of GenerateKeyPair -func (mr *MockClientMockRecorder) GenerateKeyPair() *gomock.Call { +func (mr *MockKeyStoreMockRecorder) GenerateKeyPair() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKeyPair", reflect.TypeOf((*MockClient)(nil).GenerateKeyPair)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKeyPair", reflect.TypeOf((*MockKeyStore)(nil).GenerateKeyPair)) } // GetPrivateKey mocks base method -func (m *MockClient) GetPrivateKey(kid string) (crypto.Signer, error) { +func (m *MockKeyStore) GetPrivateKey(kid string) (crypto.Signer, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPrivateKey", kid) ret0, _ := ret[0].(crypto.Signer) @@ -59,43 +58,28 @@ func (m *MockClient) GetPrivateKey(kid string) (crypto.Signer, error) { } // GetPrivateKey indicates an expected call of GetPrivateKey -func (mr *MockClientMockRecorder) GetPrivateKey(kid interface{}) *gomock.Call { +func (mr *MockKeyStoreMockRecorder) GetPrivateKey(kid interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateKey", reflect.TypeOf((*MockClient)(nil).GetPrivateKey), kid) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateKey", reflect.TypeOf((*MockKeyStore)(nil).GetPrivateKey), kid) } -// GetPublicKeyAsPEM mocks base method -func (m *MockClient) GetPublicKeyAsPEM(kid string) (string, error) { +// GetPublicKey mocks base method +func (m *MockKeyStore) GetPublicKey(kid string) (crypto.PublicKey, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPublicKeyAsPEM", kid) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPublicKeyAsPEM indicates an expected call of GetPublicKeyAsPEM -func (mr *MockClientMockRecorder) GetPublicKeyAsPEM(kid interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeyAsPEM", reflect.TypeOf((*MockClient)(nil).GetPublicKeyAsPEM), kid) -} - -// GetPublicKeyAsJWK mocks base method -func (m *MockClient) GetPublicKeyAsJWK(kid string) (jwk.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPublicKeyAsJWK", kid) - ret0, _ := ret[0].(jwk.Key) + ret := m.ctrl.Call(m, "GetPublicKey", kid) + ret0, _ := ret[0].(crypto.PublicKey) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetPublicKeyAsJWK indicates an expected call of GetPublicKeyAsJWK -func (mr *MockClientMockRecorder) GetPublicKeyAsJWK(kid interface{}) *gomock.Call { +// GetPublicKey indicates an expected call of GetPublicKey +func (mr *MockKeyStoreMockRecorder) GetPublicKey(kid interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeyAsJWK", reflect.TypeOf((*MockClient)(nil).GetPublicKeyAsJWK), kid) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKey", reflect.TypeOf((*MockKeyStore)(nil).GetPublicKey), kid) } // SignJWT mocks base method -func (m *MockClient) SignJWT(claims map[string]interface{}, kid string) (string, error) { +func (m *MockKeyStore) SignJWT(claims map[string]interface{}, kid string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SignJWT", claims, kid) ret0, _ := ret[0].(string) @@ -104,13 +88,13 @@ func (m *MockClient) SignJWT(claims map[string]interface{}, kid string) (string, } // SignJWT indicates an expected call of SignJWT -func (mr *MockClientMockRecorder) SignJWT(claims, kid interface{}) *gomock.Call { +func (mr *MockKeyStoreMockRecorder) SignJWT(claims, kid interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignJWT", reflect.TypeOf((*MockClient)(nil).SignJWT), claims, kid) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignJWT", reflect.TypeOf((*MockKeyStore)(nil).SignJWT), claims, kid) } // PrivateKeyExists mocks base method -func (m *MockClient) PrivateKeyExists(key string) bool { +func (m *MockKeyStore) PrivateKeyExists(key string) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PrivateKeyExists", key) ret0, _ := ret[0].(bool) @@ -118,7 +102,7 @@ func (m *MockClient) PrivateKeyExists(key string) bool { } // PrivateKeyExists indicates an expected call of PrivateKeyExists -func (mr *MockClientMockRecorder) PrivateKeyExists(key interface{}) *gomock.Call { +func (mr *MockKeyStoreMockRecorder) PrivateKeyExists(key interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivateKeyExists", reflect.TypeOf((*MockClient)(nil).PrivateKeyExists), key) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivateKeyExists", reflect.TypeOf((*MockKeyStore)(nil).PrivateKeyExists), key) } From 9e7ce431b2967e8fba9ca96523af2c19695d421c Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 08:27:34 +0100 Subject: [PATCH 06/27] removed nuts-go-test dep --- crypto/api/v1/api_test.go | 2 +- crypto/crypto_test.go | 2 +- crypto/engine/engine_test.go | 2 +- crypto/storage/fs_test.go | 2 +- go.mod | 2 -- go.sum | 24 +----------------------- 6 files changed, 5 insertions(+), 29 deletions(-) diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go index 1c6ac14751..0cdebe9ae5 100644 --- a/crypto/api/v1/api_test.go +++ b/crypto/api/v1/api_test.go @@ -27,11 +27,11 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/nuts-foundation/nuts-go-test/io" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/nuts-foundation/nuts-node/mock" + "github.com/nuts-foundation/nuts-node/test/io" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 1244b67cc6..c55bcbf61c 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -25,8 +25,8 @@ import ( "reflect" "testing" - "github.com/nuts-foundation/nuts-go-test/io" "github.com/nuts-foundation/nuts-node/crypto/util" + "github.com/nuts-foundation/nuts-node/test/io" "github.com/nuts-foundation/nuts-node/core" "github.com/spf13/cobra" diff --git a/crypto/engine/engine_test.go b/crypto/engine/engine_test.go index 922bdb0a82..146162a5cd 100644 --- a/crypto/engine/engine_test.go +++ b/crypto/engine/engine_test.go @@ -25,10 +25,10 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/nuts-foundation/nuts-go-test/io" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/mock" + "github.com/nuts-foundation/nuts-node/test/io" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) diff --git a/crypto/storage/fs_test.go b/crypto/storage/fs_test.go index 77b5de3f89..8543c38905 100644 --- a/crypto/storage/fs_test.go +++ b/crypto/storage/fs_test.go @@ -4,9 +4,9 @@ import ( "os" "testing" - "github.com/nuts-foundation/nuts-go-test/io" "github.com/nuts-foundation/nuts-node/crypto/test" "github.com/nuts-foundation/nuts-node/crypto/util" + "github.com/nuts-foundation/nuts-node/test/io" "github.com/stretchr/testify/assert" ) diff --git a/go.mod b/go.mod index 7f04ed191f..08d9de0b13 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,6 @@ require ( github.com/labstack/echo/v4 v4.1.17 github.com/lestrrat-go/jwx v1.0.7 github.com/magiconair/properties v1.8.4 - github.com/mr-tron/base58 v1.2.0 - github.com/nuts-foundation/nuts-go-test v0.16.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 github.com/sirupsen/logrus v1.7.0 diff --git a/go.sum b/go.sum index 6752cb7836..e890b9b161 100644 --- a/go.sum +++ b/go.sum @@ -47,7 +47,6 @@ github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deepmap/oapi-codegen v1.4.1/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= github.com/deepmap/oapi-codegen v1.4.2 h1:boVMuW+o0sOnEDB8oWRobBm1BrD5d9bUtIQVcjwMsSk= github.com/deepmap/oapi-codegen v1.4.2/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -133,9 +132,6 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/itchyny/base58-go v0.1.0 h1:zF5spLDo956exUAD17o+7GamZTRkXOZlqJjRciZwd1I= -github.com/itchyny/base58-go v0.1.0/go.mod h1:SrMWPE3DFuJJp1M/RUhu4fccp/y9AlB8AL3o3duPToU= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -167,6 +163,7 @@ github.com/lestrrat-go/jwx v1.0.7 h1:wmd6LGb9mYPesObp7oipuZPE1aYYJ1VeUin8LTVNy24 github.com/lestrrat-go/jwx v1.0.7/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho= github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10= github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087 h1:T5Wh8C/p5nWoGuEUBQj+daEXkj1CScB9GshvvsBJhpg= github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= @@ -201,15 +198,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nuts-foundation/nuts-crypto v0.16.0 h1:PzNDcoawuGxl4KRoD3nKnz+1sFiMbEG/S95mYbbso6k= -github.com/nuts-foundation/nuts-crypto v0.16.0/go.mod h1:NS4akgWLGO8RwtzMj2gXDEpucV0S6QDeyLxEkzGSyd4= -github.com/nuts-foundation/nuts-go-core v0.16.0 h1:bHKH0eREWecXLWUexC676RACsLdz/B2tuhkNFzi21FM= -github.com/nuts-foundation/nuts-go-core v0.16.0/go.mod h1:biWHwDgHorQ6diimNbfFFqM/bub27g5/a6IZdUdojS4= -github.com/nuts-foundation/nuts-go-test v0.16.0 h1:jfbh2z44FAsRSPXquTZWviOlB9xb9YeJDzBfPMR5Xz0= -github.com/nuts-foundation/nuts-go-test v0.16.0/go.mod h1:aZ5Urj7v1lH8AD2TIejEiPhRX8t6YG9PVXhuRyNIAII= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= @@ -224,7 +213,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -264,17 +252,12 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -360,8 +343,6 @@ golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728 h1:5wtQIAulKU5AbLQOkjxl32UufnIOqgBX72pS0AV14H0= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -397,8 +378,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200928205150-006507a75852 h1:sXxgOAXy8JwHhZnPuItAlUtwIlxrlEqi28mKhUR+zZY= -golang.org/x/sys v0.0.0-20200928205150-006507a75852/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -475,7 +454,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= From 1fb6380a5a9b740da70309512e0619e3b1f005bc Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 10:15:17 +0100 Subject: [PATCH 07/27] removed obsolete jwt lib --- crypto/jwx.go | 110 +++++++++++++++++++++++--------- crypto/jwx_test.go | 146 ++++++++++++++++++------------------------- crypto/storage/fs.go | 8 +-- crypto/util/kid.go | 3 +- go.mod | 1 - 5 files changed, 143 insertions(+), 125 deletions(-) diff --git a/crypto/jwx.go b/crypto/jwx.go index 491cb9ed69..d1f3dadd9a 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -24,7 +24,10 @@ import ( "crypto/rsa" "errors" - "github.com/dgrijalva/jwt-go" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" + "github.com/lestrrat-go/jwx/jwt" ) // ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported @@ -37,61 +40,108 @@ func (client *Crypto) SignJWT(claims map[string]interface{}, kid string) (token if err != nil { return "", err } - additionalHeaders := map[string]interface{}{ - "kid": kid, + key, err := jwkKey(privateKey) + if err != nil { + return "", err + } + + if err = jwk.AssignKeyID(key); err != nil { + return "", err } - token, err = SignJWT(privateKey, claims, additionalHeaders) + token, err = SignJWT(key, claims, nil) return } -// SignJWT signs claims with the signer and returns the compacted token. The headers param can be used to add additional headers -func SignJWT(signer crypto.Signer, claims map[string]interface{}, headers map[string]interface{}) (sig string, err error) { - c := jwt.MapClaims{} - for k, v := range claims { - c[k] = v +func jwkKey(signer crypto.Signer) (key jwk.Key, err error) { + key, err = jwk.New(signer) + if err != nil { + return nil, err } - // the current version of the used JWT lib doesn't support the crypto.Signer interface. The 4.0.0 version will. switch signer.(type) { case *rsa.PrivateKey: - token := jwt.NewWithClaims(jwt.SigningMethodPS256, c) - addHeaders(token, headers) - sig, err = token.SignedString(signer.(*rsa.PrivateKey)) + key.Set(jwk.AlgorithmKey, jwa.PS256) case *ecdsa.PrivateKey: - key := signer.(*ecdsa.PrivateKey) - var method *jwt.SigningMethodECDSA - if method, err = ecSigningMethod(key); err != nil { - return - } - token := jwt.NewWithClaims(method, c) - addHeaders(token, headers) - sig, err = token.SignedString(signer.(*ecdsa.PrivateKey)) + ecKey := signer.(*ecdsa.PrivateKey) + var alg jwa.SignatureAlgorithm + alg, err = ecAlg(ecKey) + key.Set(jwk.AlgorithmKey, alg) default: err = errors.New("unsupported signing private key") } + return +} + +// SignJWT signs claims with the signer and returns the compacted token. The headers param can be used to add additional headers +func SignJWT(key jwk.Key, claims map[string]interface{}, headers map[string]interface{}) (token string, err error) { + var sig []byte + t := jwt.New() + + for k, v := range claims { + t.Set(k, v) + } + hdr := convertHeaders(headers) + + sig, err = jwt.Sign(t, jwa.SignatureAlgorithm(key.Algorithm()), key, jws.WithHeaders(hdr)) + token = string(sig) return } -func addHeaders(token *jwt.Token, headers map[string]interface{}) { - if headers == nil { - return +// JWTAlg parses a JWT, does not validate it and returns the 'kid' and 'alg' headers +func JWTKidAlg(tokenString string) (string, jwa.SignatureAlgorithm, error) { + j, err := jws.ParseString(tokenString) + if err != nil { + return "", "", err } - for k, v := range headers { - token.Header[k] = v + if len(j.Signatures()) != 1 { + return "", "", errors.New("incorrect amount of signatures in JWT") } + + sig := j.Signatures()[0] + hdrs := sig.ProtectedHeaders() + return hdrs.KeyID(), hdrs.Algorithm(), nil +} + +// PublicKeyFunc defines a function that resolves a public key based on a kid +type PublicKeyFunc func(kid string) (crypto.PublicKey, error) + +// ParseJWT parses a token, validates and verifies it. +func ParseJWT(tokenString string, f PublicKeyFunc) (jwt.Token, error){ + kid, alg, err := JWTKidAlg(tokenString) + if err != nil { + return nil, err + } + + key, err := f(kid) + if err != nil { + return nil, err + } + + return jwt.ParseString(tokenString, jwt.WithVerify(alg, key), jwt.WithValidate(true)) +} + +func convertHeaders(headers map[string]interface{}) (hdr jws.Headers) { + hdr = jws.NewHeaders() + + if headers != nil { + for k, v := range headers { + hdr.Set(k, v) + } + } + return } -func ecSigningMethod(key *ecdsa.PrivateKey) (method *jwt.SigningMethodECDSA, err error) { +func ecAlg(key *ecdsa.PrivateKey) (alg jwa.SignatureAlgorithm, err error) { switch key.Params().BitSize { case 256: - method = jwt.SigningMethodES256 + alg = jwa.ES256 case 384: - method = jwt.SigningMethodES384 + alg = jwa.ES384 case 521: - method = jwt.SigningMethodES512 + alg = jwa.ES512 default: err = ErrUnsupportedSigningKey } diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index eafda01f70..5e6c2cd690 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -19,17 +19,17 @@ package crypto import ( + "crypto" "crypto/ecdsa" - "crypto/ed25519" "crypto/elliptic" "crypto/rand" - rsa2 "crypto/rsa" + "crypto/rsa" "fmt" "testing" - "github.com/dgrijalva/jwt-go" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" "github.com/nuts-foundation/nuts-node/crypto/storage" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/pkg/errors" @@ -39,118 +39,92 @@ import ( func TestSignJWT(t *testing.T) { claims := map[string]interface{}{"iss": "nuts"} t.Run("creates valid JWT using rsa keys", func(t *testing.T) { - key, _ := rsa2.GenerateKey(rand.Reader, 2048) + rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + key, _ := jwkKey(rsaKey) tokenString, err := SignJWT(key, claims, nil) assert.Nil(t, err) - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return key.Public(), nil + token, err := ParseJWT(tokenString, func(kid string) (crypto.PublicKey, error) { + return rsaKey.Public(), nil }) - assert.True(t, token.Valid) - assert.Equal(t, "nuts", token.Claims.(jwt.MapClaims)["iss"]) - }) - - t.Run("creates valid JWT using ec keys", func(t *testing.T) { - p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - p521, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - - keys := []*ecdsa.PrivateKey{p256, p384, p521} - - for _, key := range keys { - name := fmt.Sprintf("using %s", key.Params().Name) - t.Run(name, func(t *testing.T) { - tokenString, err := SignJWT(key, claims, nil) - - if assert.Nil(t, err) { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return key.Public(), nil - }) - - if assert.Nil(t, err) { - assert.True(t, token.Valid) - assert.Equal(t, "nuts", token.Claims.(jwt.MapClaims)["iss"]) - } - } - }) + if !assert.NoError(t, err) { + return } - }) - t.Run("sets correct headers", func(t *testing.T) { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - raw, _ := SignJWT(key, claims, map[string]interface{}{"x5c": []string{"BASE64"}}) - token, _ := jwt.Parse(raw, func(token *jwt.Token) (interface{}, error) { - return key.Public(), nil - }) - - assert.Equal(t, "JWT", token.Header["typ"]) - assert.Equal(t, "ES256", token.Header["alg"]) - assert.Equal(t, []interface{}{"BASE64"}, token.Header["x5c"]) + assert.Equal(t, "nuts", token.Issuer()) }) - t.Run("returns error on unknown curve", func(t *testing.T) { - key, _ := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) - _, err := SignJWT(key, claims, nil) - - assert.NotNil(t, err) - }) - - t.Run("returns error on unsupported crypto", func(t *testing.T) { - _, key, _ := ed25519.GenerateKey(rand.Reader) - _, err := SignJWT(key, claims, nil) - - assert.NotNil(t, err) - }) -} - -func TestCrypto_PublicKeyInJWK(t *testing.T) { - client := createCrypto(t) - createCrypto(t) - - publicKey, _ := client.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) - - t.Run("Public key is returned from storage", func(t *testing.T) { - pub, err := client.GetPublicKey(kid) - - assert.NoError(t, err) - assert.NotNil(t, pub) + t.Run("creates valid JWT using ec keys", func(t *testing.T) { + p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + p521, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + + keys := []*ecdsa.PrivateKey{p256, p384, p521} + + for _, ecKey := range keys { + name := fmt.Sprintf("using %s", ecKey.Params().Name) + t.Run(name, func(t *testing.T) { + key, _ := jwkKey(ecKey) + tokenString, err := SignJWT(key, claims, nil) + + if !assert.NoError(t, err) { + return + } + + token, err := ParseJWT(tokenString, func(kid string) (crypto.PublicKey, error) { + return ecKey.Public(), nil + }) + + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "nuts", token.Issuer()) + }) + } + }) - jwkKey, _ := jwk.New(pub) + t.Run("sets correct headers", func(t *testing.T) { + ecKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + key, _ := jwkKey(ecKey) + tokenString, err := SignJWT(key, claims, nil) - assert.Equal(t, jwa.EC, jwkKey.KeyType()) - }) + if !assert.NoError(t, err) { +return + } - t.Run("Public key for unknown entity returns error", func(t *testing.T) { - _, err := client.GetPublicKey("unknown") + msg, err := jws.ParseString(tokenString) - if assert.Error(t, err) { - assert.True(t, errors.Is(err, storage.ErrNotFound)) - } + hdrs := msg.Signatures()[0].ProtectedHeaders() + alg, _ := hdrs.Get(jwk.AlgorithmKey) + assert.Equal(t, jwa.ES256, alg) }) } func TestCrypto_SignJWT(t *testing.T) { client := createCrypto(t) - createCrypto(t) publicKey, _ := client.GenerateKeyPair() kid, _ := util.Fingerprint(publicKey) t.Run("creates valid JWT", func(t *testing.T) { tokenString, err := client.SignJWT(map[string]interface{}{"iss": "nuts"}, kid) + println(tokenString) - assert.Nil(t, err) + if !assert.NoError(t, err) { + return + } - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - pubKey, _ := client.Storage.GetPublicKey(kid) - return pubKey, nil + token, err := ParseJWT(tokenString, func(kid string) (crypto.PublicKey, error) { + return client.GetPublicKey(kid) }) - assert.True(t, token.Valid) - assert.Equal(t, "nuts", token.Claims.(jwt.MapClaims)["iss"]) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "nuts", token.Issuer()) }) t.Run("returns error for not found", func(t *testing.T) { diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go index fd1ba720f9..5f4fc0b9c5 100644 --- a/crypto/storage/fs.go +++ b/crypto/storage/fs.go @@ -19,9 +19,7 @@ package storage import ( - "bytes" "crypto" - "encoding/base64" "errors" "fmt" "io/ioutil" @@ -193,9 +191,5 @@ func (fsc *fileSystemBackend) createDirs() error { } func getEntryFileName(kid string, entryType entryType) string { - buffer := new(bytes.Buffer) - encoder := base64.NewEncoder(base64.StdEncoding, buffer) - encoder.Write([]byte(kid)) - encoder.Close() - return fmt.Sprintf("%s_%s", buffer.String(), entryType) + return fmt.Sprintf("%s_%s", kid, entryType) } diff --git a/crypto/util/kid.go b/crypto/util/kid.go index 99b15b57fe..8b2bf86ab8 100644 --- a/crypto/util/kid.go +++ b/crypto/util/kid.go @@ -35,5 +35,6 @@ func Fingerprint(key interface{}) (string, error) { return "", err } - return base64.URLEncoding.EncodeToString(tp), nil + // trailing '=' not allowed in kid + return base64.RawURLEncoding.EncodeToString(tp), nil } diff --git a/go.mod b/go.mod index 08d9de0b13..066a483e19 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.15 require ( github.com/deepmap/oapi-codegen v1.4.2 - github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/golang/mock v1.4.4 github.com/labstack/echo/v4 v4.1.17 github.com/lestrrat-go/jwx v1.0.7 From 69abcf24d9bd7b098bc0f8d4c7becf25feb5061c Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 10:38:43 +0100 Subject: [PATCH 08/27] gofmt --- cmd/root_test.go | 2 +- core/config.go | 1 - core/constants.go | 2 +- crypto/api/v1/api_test.go | 4 +-- crypto/api/v1/generated.go | 1 - crypto/crypto.go | 16 +++++------ crypto/crypto_test.go | 4 +-- crypto/jwx.go | 2 +- crypto/jwx_test.go | 56 +++++++++++++++++++------------------- crypto/storage/fs.go | 2 +- crypto/test/keys.go | 2 +- test/io/io_test.go | 2 +- 12 files changed, 46 insertions(+), 48 deletions(-) diff --git a/cmd/root_test.go b/cmd/root_test.go index 9240993c30..7a439f9d0c 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -20,4 +20,4 @@ func Test_rootCmd(t *testing.T) { assert.NoError(t, CreateCommand().Execute()) assert.False(t, routesCalled, "engine.Routes was called") }) -} \ No newline at end of file +} diff --git a/core/config.go b/core/config.go index 1c4d6aa27e..d8e3c8f5ce 100644 --- a/core/config.go +++ b/core/config.go @@ -131,7 +131,6 @@ func (ngc NutsGlobalConfig) Mode() string { return ngc.v.GetString(modeFlag) } - // GetEngineMode configures an engine mode if not already configured. If the application is started in 'cli' mode, // its engines are configured to run in 'client' mode. This function returns the proper mode for the engine in and should be used as follows: // engineConfig.Mode = GetEngineMode(engineConfig.Mode) diff --git a/core/constants.go b/core/constants.go index 9aad08839c..8e6ed0c9a3 100644 --- a/core/constants.go +++ b/core/constants.go @@ -26,4 +26,4 @@ const NutsOID = "1.3.6.1.4.1.54851" const NutsConsentClassesOID = NutsOID + ".1" // NutsVendorOID is the sub-OID used for vendor identifiers -const NutsVendorOID = NutsOID + ".4" \ No newline at end of file +const NutsVendorOID = NutsOID + ".4" diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go index 0cdebe9ae5..3dc910db3e 100644 --- a/crypto/api/v1/api_test.go +++ b/crypto/api/v1/api_test.go @@ -96,8 +96,8 @@ func TestWrapper_SignJwt(t *testing.T) { echo := mock.NewMockContext(ctrl) jsonRequest := SignJwtRequest{ - Kid: kid, - Claims: map[string]interface{}{"iss": "nuts"}, + Kid: kid, + Claims: map[string]interface{}{"iss": "nuts"}, } json, _ := json.Marshal(jsonRequest) diff --git a/crypto/api/v1/generated.go b/crypto/api/v1/generated.go index cee6a0d18a..e39292278a 100644 --- a/crypto/api/v1/generated.go +++ b/crypto/api/v1/generated.go @@ -463,4 +463,3 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/internal/crypto/v1/sign_jwt", wrapper.SignJwt) } - diff --git a/crypto/crypto.go b/crypto/crypto.go index 9de8241d79..73c236c1a0 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -35,8 +35,8 @@ import ( // CryptoConfig holds the values for the crypto engine type CryptoConfig struct { - Storage string - Fspath string + Storage string + Fspath string } func (cc CryptoConfig) getFSPath() string { @@ -49,17 +49,17 @@ func (cc CryptoConfig) getFSPath() string { func DefaultCryptoConfig() CryptoConfig { return CryptoConfig{ - Storage: "fs", - Fspath: "./", + Storage: "fs", + Fspath: "./", } } // default implementation for Instance type Crypto struct { - Storage storage.Storage - Config CryptoConfig - configOnce sync.Once - configDone bool + Storage storage.Storage + Config CryptoConfig + configOnce sync.Once + configDone bool } type opaquePrivateKey struct { diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index c55bcbf61c..34398ae5d2 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -170,8 +170,8 @@ func createCrypto(t *testing.T) *Crypto { dir := io.TestDirectory(t) backend, _ := storage.NewFileSystemBackend(dir) crypto := Crypto{ - Storage: backend, - Config: TestCryptoConfig(dir), + Storage: backend, + Config: TestCryptoConfig(dir), } return &crypto diff --git a/crypto/jwx.go b/crypto/jwx.go index d1f3dadd9a..021748edab 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -109,7 +109,7 @@ func JWTKidAlg(tokenString string) (string, jwa.SignatureAlgorithm, error) { type PublicKeyFunc func(kid string) (crypto.PublicKey, error) // ParseJWT parses a token, validates and verifies it. -func ParseJWT(tokenString string, f PublicKeyFunc) (jwt.Token, error){ +func ParseJWT(tokenString string, f PublicKeyFunc) (jwt.Token, error) { kid, alg, err := JWTKidAlg(tokenString) if err != nil { return nil, err diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 5e6c2cd690..297502b65d 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -57,33 +57,33 @@ func TestSignJWT(t *testing.T) { }) t.Run("creates valid JWT using ec keys", func(t *testing.T) { - p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - p521, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - - keys := []*ecdsa.PrivateKey{p256, p384, p521} - - for _, ecKey := range keys { - name := fmt.Sprintf("using %s", ecKey.Params().Name) - t.Run(name, func(t *testing.T) { - key, _ := jwkKey(ecKey) - tokenString, err := SignJWT(key, claims, nil) - - if !assert.NoError(t, err) { - return - } - - token, err := ParseJWT(tokenString, func(kid string) (crypto.PublicKey, error) { - return ecKey.Public(), nil - }) - - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "nuts", token.Issuer()) - }) - } - }) + p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + p521, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + + keys := []*ecdsa.PrivateKey{p256, p384, p521} + + for _, ecKey := range keys { + name := fmt.Sprintf("using %s", ecKey.Params().Name) + t.Run(name, func(t *testing.T) { + key, _ := jwkKey(ecKey) + tokenString, err := SignJWT(key, claims, nil) + + if !assert.NoError(t, err) { + return + } + + token, err := ParseJWT(tokenString, func(kid string) (crypto.PublicKey, error) { + return ecKey.Public(), nil + }) + + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "nuts", token.Issuer()) + }) + } + }) t.Run("sets correct headers", func(t *testing.T) { ecKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -91,7 +91,7 @@ func TestSignJWT(t *testing.T) { tokenString, err := SignJWT(key, claims, nil) if !assert.NoError(t, err) { -return + return } msg, err := jws.ParseString(tokenString) diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go index 5f4fc0b9c5..08010e3807 100644 --- a/crypto/storage/fs.go +++ b/crypto/storage/fs.go @@ -33,7 +33,7 @@ import ( type entryType string const ( - privateKeyEntry entryType = "private.pem" + privateKeyEntry entryType = "private.pem" publicKeyEntry entryType = "public.pem" ) diff --git a/crypto/test/keys.go b/crypto/test/keys.go index 90585edce6..6df67dccc3 100644 --- a/crypto/test/keys.go +++ b/crypto/test/keys.go @@ -15,7 +15,7 @@ func GenerateRSAKey() *rsa.PrivateKey { return privateKey } -func GenerateECKey() (*ecdsa.PrivateKey) { +func GenerateECKey() *ecdsa.PrivateKey { key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) return key } diff --git a/test/io/io_test.go b/test/io/io_test.go index 6dca9e99bd..8e4ee7ac44 100644 --- a/test/io/io_test.go +++ b/test/io/io_test.go @@ -11,4 +11,4 @@ func Test_normalizeTestName(t *testing.T) { assert.Equal(t, "Test_normalizeTestName_level_1_____3_level_2__", normalizeTestName(t)) }) }) -} \ No newline at end of file +} From 75d02dea093e22fe4aea15e026a5945b7053497e Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 10:48:29 +0100 Subject: [PATCH 09/27] cleanup --- crypto/api/v1/api.go | 1 + crypto/api/v1/client.go | 11 ++++++----- crypto/api/v1/client_test.go | 8 ++++---- crypto/crypto.go | 27 +++++++++++++-------------- crypto/engine/engine.go | 4 ++-- crypto/jwx.go | 2 +- crypto/jwx_test.go | 4 ++-- crypto/log/log.go | 1 + crypto/storage/fs.go | 17 +++++++---------- crypto/test.go | 8 +++++--- crypto/test/keys.go | 2 ++ 11 files changed, 44 insertions(+), 41 deletions(-) diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index eaef7a91bf..5b7f36b1bb 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -72,6 +72,7 @@ func (w *Wrapper) GenerateKeyPair(ctx echo.Context) error { return ctx.String(http.StatusOK, pub) } +// SignJwt handles api calls for signing a Jwt func (w *Wrapper) SignJwt(ctx echo.Context) error { buf, err := readBody(ctx) if err != nil { diff --git a/crypto/api/v1/client.go b/crypto/api/v1/client.go index b6f64b2df9..2397d2b25e 100644 --- a/crypto/api/v1/client.go +++ b/crypto/api/v1/client.go @@ -35,13 +35,13 @@ import ( // ErrNotImplemented indicates that this client API call is not implemented. var ErrNotImplemented = errors.New("operation not implemented") -// HttpClient holds the server address and other basic settings for the http client -type HttpClient struct { +// HTTPClient holds the server address and other basic settings for the http client +type HTTPClient struct { ServerAddress string Timeout time.Duration } -func (hb HttpClient) clientWithRequestEditor(fn RequestEditorFn) ClientInterface { +func (hb HTTPClient) clientWithRequestEditor(fn RequestEditorFn) ClientInterface { url := hb.ServerAddress if !strings.Contains(url, "http") { url = fmt.Sprintf("http://%v", hb.ServerAddress) @@ -54,11 +54,12 @@ func (hb HttpClient) clientWithRequestEditor(fn RequestEditorFn) ClientInterface return response } -func (hb HttpClient) client() ClientInterface { +func (hb HTTPClient) client() ClientInterface { return hb.clientWithRequestEditor(nil) } -func (hb HttpClient) GetPublicKey(kid string) (crypto.PublicKey, error) { +// GetPublicKey returns a PrivateKey from the server given a kid +func (hb HTTPClient) GetPublicKey(kid string) (crypto.PublicKey, error) { ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) defer cancel() httpClient := hb.clientWithRequestEditor(func(ctx context.Context, req *http.Request) error { diff --git a/crypto/api/v1/client_test.go b/crypto/api/v1/client_test.go index 41f48cda50..9d03ace1ee 100644 --- a/crypto/api/v1/client_test.go +++ b/crypto/api/v1/client_test.go @@ -52,7 +52,7 @@ var jwkAsBytes = []byte(jwkAsString) func TestHttpClient_GetPublicKey(t *testing.T) { t.Run("ok", func(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + c := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} res, err := c.GetPublicKey("kid") if !assert.NoError(t, err) { return @@ -62,14 +62,14 @@ func TestHttpClient_GetPublicKey(t *testing.T) { t.Run("error - server returned non-JWK", func(t *testing.T) { csrBytes, _ := ioutil.ReadFile("../test/broken.pem") s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: csrBytes}) - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + c := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} res, err := c.GetPublicKey("kid") assert.Contains(t, err.Error(), "failed to unmarshal JWK:") assert.Nil(t, res) }) t.Run("error - response not HTTP OK", func(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError, responseData: genericError}) - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + c := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} res, err := c.GetPublicKey("kid") assert.EqualError(t, err, "server returned HTTP 500 (expected: 200), response: failed") assert.Nil(t, res) @@ -77,7 +77,7 @@ func TestHttpClient_GetPublicKey(t *testing.T) { t.Run("error - server not running", func(t *testing.T) { s := httptest.NewServer(handler{statusCode: http.StatusOK}) s.Close() - c := HttpClient{ServerAddress: s.URL, Timeout: time.Second} + c := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} res, err := c.GetPublicKey("kid") assert.Contains(t, err.Error(), "connection refused") assert.Nil(t, res) diff --git a/crypto/crypto.go b/crypto/crypto.go index 73c236c1a0..2c451380ad 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -33,13 +33,13 @@ import ( "github.com/nuts-foundation/nuts-node/crypto/util" ) -// CryptoConfig holds the values for the crypto engine -type CryptoConfig struct { +// Config holds the values for the crypto engine +type Config struct { Storage string Fspath string } -func (cc CryptoConfig) getFSPath() string { +func (cc Config) getFSPath() string { if cc.Fspath == "" { return DefaultCryptoConfig().Fspath } @@ -47,8 +47,9 @@ func (cc CryptoConfig) getFSPath() string { return cc.Fspath } -func DefaultCryptoConfig() CryptoConfig { - return CryptoConfig{ +// DefaultCryptoConfig returns a Config with sane defaults +func DefaultCryptoConfig() Config { + return Config{ Storage: "fs", Fspath: "./", } @@ -57,7 +58,7 @@ func DefaultCryptoConfig() CryptoConfig { // default implementation for Instance type Crypto struct { Storage storage.Storage - Config CryptoConfig + Config Config configOnce sync.Once configDone bool } @@ -67,10 +68,12 @@ type opaquePrivateKey struct { signFn func(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) } +// Public returns the public key func (k opaquePrivateKey) Public() crypto.PublicKey { return k.publicKey } +// Sign signs some data with the signer func (k opaquePrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { return k.signFn(rand, digest, opts) } @@ -99,17 +102,13 @@ func Instance() *Crypto { return instance } oneBackend.Do(func() { - instance = NewInstance(DefaultCryptoConfig()) + instance = &Crypto{ + Config: DefaultCryptoConfig(), + } }) return instance } -func NewInstance(config CryptoConfig) *Crypto { - return &Crypto{ - Config: config, - } -} - // Configure loads the given configurations in the engine. Any wrong combination will return an error func (client *Crypto) Configure() error { var err error @@ -172,7 +171,7 @@ func (client *Crypto) PrivateKeyExists(kid string) bool { return client.Storage.PrivateKeyExists(kid) } -// PublicKeyInPEM loads the key from storage and returns it as PEM encoded. Only supports RSA style keys +// GetPublicKey loads the key from storage and returns it as PEM encoded. Only supports RSA style keys func (client *Crypto) GetPublicKey(kid string) (crypto.PublicKey, error) { return client.Storage.GetPublicKey(kid) } diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go index 6b944da430..a5e3934086 100644 --- a/crypto/engine/engine.go +++ b/crypto/engine/engine.go @@ -140,11 +140,11 @@ func cmd() *cobra.Command { } // newCryptoClient creates a remote client -func newCryptoClient(cmd *cobra.Command) api.HttpClient { +func newCryptoClient(cmd *cobra.Command) api.HTTPClient { cfg := core.NutsConfig() cfg.Load(cmd) - return api.HttpClient{ + return api.HTTPClient{ ServerAddress: cfg.ServerAddress(), Timeout: 10 * time.Second, } diff --git a/crypto/jwx.go b/crypto/jwx.go index 021748edab..2fd312b636 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -89,7 +89,7 @@ func SignJWT(key jwk.Key, claims map[string]interface{}, headers map[string]inte return } -// JWTAlg parses a JWT, does not validate it and returns the 'kid' and 'alg' headers +// JWTKidAlg parses a JWT, does not validate it and returns the 'kid' and 'alg' headers func JWTKidAlg(tokenString string) (string, jwa.SignatureAlgorithm, error) { j, err := jws.ParseString(tokenString) if err != nil { diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 297502b65d..994e887984 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -23,7 +23,6 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/rsa" "fmt" "testing" @@ -31,6 +30,7 @@ import ( "github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jws" "github.com/nuts-foundation/nuts-node/crypto/storage" + "github.com/nuts-foundation/nuts-node/crypto/test" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -39,7 +39,7 @@ import ( func TestSignJWT(t *testing.T) { claims := map[string]interface{}{"iss": "nuts"} t.Run("creates valid JWT using rsa keys", func(t *testing.T) { - rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + rsaKey := test.GenerateRSAKey() key, _ := jwkKey(rsaKey) tokenString, err := SignJWT(key, claims, nil) diff --git a/crypto/log/log.go b/crypto/log/log.go index 6c9dcea693..5aaaa6ee87 100644 --- a/crypto/log/log.go +++ b/crypto/log/log.go @@ -24,6 +24,7 @@ import ( var _logger = logrus.StandardLogger().WithField("module", "Crypto") +// Logger returns a logger with the module field set to 'Crypto' func Logger() *logrus.Entry { return _logger } diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go index 08010e3807..82f2ec22ba 100644 --- a/crypto/storage/fs.go +++ b/crypto/storage/fs.go @@ -37,7 +37,7 @@ const ( publicKeyEntry entryType = "public.pem" ) -type FileOpenError struct { +type fileOpenError struct { filePath string kid string err error @@ -46,16 +46,13 @@ type FileOpenError struct { // ErrNotFound indicates that the specified crypto storage entry couldn't be found. var ErrNotFound = errors.New("entry not found") -// ErrInvalidDuration is given when a period duration is 0 or negative -var ErrInvalidDuration = errors.New("given time period is invalid") - // Error returns the string representation -func (f *FileOpenError) Error() string { +func (f *fileOpenError) Error() string { return fmt.Sprintf("could not open entry %s with filename %s: %v", f.kid, f.filePath, f.err) } -// UnWrap is needed for FileOpenError to be UnWrapped -func (f *FileOpenError) Unwrap() error { +// Unwrap is needed for fileOpenError to be UnWrapped +func (f *fileOpenError) Unwrap() error { return f.err } @@ -63,7 +60,7 @@ type fileSystemBackend struct { fspath string } -// Create a new filesystem backend, all directories will be created for the given path +// NewFileSystemBackend creates a new filesystem backend, all directories will be created for the given path // Using a filesystem backend in production is not recommended! func NewFileSystemBackend(fspath string) (*fileSystemBackend, error) { if fspath == "" { @@ -165,9 +162,9 @@ func (fsc fileSystemBackend) readEntry(kid string, entryType entryType) ([]byte, data, err := ioutil.ReadFile(filePath) if err != nil { if errors.Is(err, os.ErrNotExist) { - return nil, &FileOpenError{kid: kid, filePath: filePath, err: ErrNotFound} + return nil, &fileOpenError{kid: kid, filePath: filePath, err: ErrNotFound} } - return nil, &FileOpenError{kid: kid, filePath: filePath, err: err} + return nil, &fileOpenError{kid: kid, filePath: filePath, err: err} } return data, nil } diff --git a/crypto/test.go b/crypto/test.go index 0f588daf01..2a8d39d387 100644 --- a/crypto/test.go +++ b/crypto/test.go @@ -10,7 +10,9 @@ import ( // specified test directory. func NewTestCryptoInstance(testDirectory string) *Crypto { config := TestCryptoConfig(testDirectory) - newInstance := NewInstance(config) + newInstance := &Crypto{ + Config: config, + } if err := newInstance.Configure(); err != nil { logrus.Fatal(err) } @@ -18,8 +20,8 @@ func NewTestCryptoInstance(testDirectory string) *Crypto { return newInstance } -// TestCryptoConfig returns CryptoConfig to be used in integration/unit tests. -func TestCryptoConfig(testDirectory string) CryptoConfig { +// TestCryptoConfig returns Config to be used in integration/unit tests. +func TestCryptoConfig(testDirectory string) Config { config := DefaultCryptoConfig() config.Fspath = path.Join(testDirectory, "crypto") return config diff --git a/crypto/test/keys.go b/crypto/test/keys.go index 6df67dccc3..8b6ef2eb1b 100644 --- a/crypto/test/keys.go +++ b/crypto/test/keys.go @@ -7,6 +7,7 @@ import ( "crypto/rsa" ) +// GenerateRSAKey generates a 1024 bits RSA key func GenerateRSAKey() *rsa.PrivateKey { privateKey, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { @@ -15,6 +16,7 @@ func GenerateRSAKey() *rsa.PrivateKey { return privateKey } +// GenerateECKey generates a P-256 EC key func GenerateECKey() *ecdsa.PrivateKey { key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) return key From a2c249429ad841135fe7ab60c5146e2ab2c306b7 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 10:49:44 +0100 Subject: [PATCH 10/27] cleanup --- crypto/types/types.go | 10 ++-------- crypto/util/kid.go | 2 +- crypto/util/pem.go | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/crypto/types/types.go b/crypto/types/types.go index 733ec3f1a8..45aa2b0f74 100644 --- a/crypto/types/types.go +++ b/crypto/types/types.go @@ -19,14 +19,8 @@ // types and interfaces used by all other packages package types -// --mode config flag -const ConfigMode string = "mode" - -// --clientTimeout config flag -const ConfigClientTimeout string = "clientTimeout" - -// --storage config flag +// ConfigStorage is used as --storage config flag const ConfigStorage string = "storage" -// --fspath config flagclient.getStoragePath() +// ConfigFSPath is used as --fspath config flagclient.getStoragePath() const ConfigFSPath string = "fspath" diff --git a/crypto/util/kid.go b/crypto/util/kid.go index 8b2bf86ab8..5c39cdac8e 100644 --- a/crypto/util/kid.go +++ b/crypto/util/kid.go @@ -22,7 +22,7 @@ import ( "github.com/lestrrat-go/jwx/jwk" ) -// Thumbprint returns the JWK thumbprint using the indicated +// Fingerprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 func Fingerprint(key interface{}) (string, error) { k, err := jwk.New(key) diff --git a/crypto/util/pem.go b/crypto/util/pem.go index f4b68d59b0..6a3b905a89 100644 --- a/crypto/util/pem.go +++ b/crypto/util/pem.go @@ -55,7 +55,7 @@ func PublicKeyToPem(pub crypto.PublicKey) (string, error) { return string(pubBytes), err } -// PublicKeyToPem converts an public key to PEM encoding +// PrivateKeyToPem converts an public key to PEM encoding func PrivateKeyToPem(pub crypto.PrivateKey) (string, error) { pubASN1, err := x509.MarshalPKCS8PrivateKey(pub) From 784ae9956401f63b8761fb8a7ec4d793d39cfca3 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:23:19 +0100 Subject: [PATCH 11/27] api test cleanup --- crypto/api/v1/api.go | 61 +------------ crypto/api/v1/api_test.go | 186 ++++++++++++++++++++------------------ 2 files changed, 99 insertions(+), 148 deletions(-) diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index 5b7f36b1bb..719e95baad 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -19,11 +19,7 @@ package v1 import ( - crypto2 "crypto" - "encoding/json" "errors" - "fmt" - "io/ioutil" "mime" "net/http" @@ -41,47 +37,10 @@ type Wrapper struct { C crypto.KeyStore } -// GenerateKeyPair is the implementation of the REST service call POST /crypto/generate -// It returns the public key for the given legal entity in either PEM or JWK format depending on the accept-header. Default is PEM (backwards compatibility) -func (w *Wrapper) GenerateKeyPair(ctx echo.Context) error { - var publicKey crypto2.PublicKey - var err error - - if publicKey, err = w.C.GenerateKeyPair(); err != nil { - return err - } - - acceptHeader := ctx.Request().Header.Get("Accept") - - if ct, _, _ := mime.ParseMediaType(acceptHeader); ct == "application/json" { - var j jwk.Key - var err error - if j, err = jwk.New(publicKey); err != nil { - return err - } - - return ctx.JSON(http.StatusOK, j) - } - - // backwards compatible PEM format is the default - pub, err := util.PublicKeyToPem(publicKey) - if err != nil { - return err - } - - return ctx.String(http.StatusOK, pub) -} - // SignJwt handles api calls for signing a Jwt func (w *Wrapper) SignJwt(ctx echo.Context) error { - buf, err := readBody(ctx) - if err != nil { - return err - } - var signRequest = &SignJwtRequest{} - err = json.Unmarshal(buf, signRequest) - + err := ctx.Bind(signRequest) if err != nil { log.Logger().Error(err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) @@ -139,21 +98,3 @@ func (w *Wrapper) PublicKey(ctx echo.Context, kid string) error { return ctx.String(http.StatusOK, pub) } - -func readBody(ctx echo.Context) ([]byte, error) { - req := ctx.Request() - if req.Body == nil { - msg := "missing body in request" - log.Logger().Error(msg) - return nil, echo.NewHTTPError(http.StatusBadRequest, msg) - } - - buf, err := ioutil.ReadAll(req.Body) - if err != nil { - msg := fmt.Sprintf("error reading request: %v", err) - log.Logger().Error(msg) - return nil, echo.NewHTTPError(http.StatusBadRequest, msg) - } - - return buf, nil -} diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go index 3dc910db3e..ffc1657245 100644 --- a/crypto/api/v1/api_test.go +++ b/crypto/api/v1/api_test.go @@ -19,110 +19,107 @@ package v1 import ( - "bytes" "encoding/json" - "io/ioutil" + "errors" "net/http" - "os" "testing" "github.com/golang/mock/gomock" - "github.com/nuts-foundation/nuts-node/core" - "github.com/nuts-foundation/nuts-node/crypto" - "github.com/nuts-foundation/nuts-node/crypto/util" + "github.com/nuts-foundation/nuts-node/crypto/storage" + "github.com/nuts-foundation/nuts-node/crypto/test" "github.com/nuts-foundation/nuts-node/mock" - "github.com/nuts-foundation/nuts-node/test/io" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestWrapper_SignJwt(t *testing.T) { - os.Setenv("NUTS_MODE", "server") - defer os.Unsetenv("NUTS_MODE") - core.NutsConfig().Load(&cobra.Command{}) - - client := apiWrapper(t) - - publicKey, _ := client.C.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) - t.Run("Missing claims returns 400", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + ctx := newMockContext(t) + defer ctx.ctrl.Finish() jsonRequest := SignJwtRequest{ - Kid: kid, + Kid: "kid", } + jsonData, _ := json.Marshal(jsonRequest) - json, _ := json.Marshal(jsonRequest) - request := &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(json)), - } - - echo.EXPECT().Request().Return(request) + ctx.echo.EXPECT().Bind(gomock.Any()).Do(func(f interface{}) { + _ = json.Unmarshal(jsonData, f) + }) - err := client.SignJwt(echo) + err := ctx.client.SignJwt(ctx.echo) assert.NotNil(t, err) assert.Contains(t, err.Error(), "code=400, message=missing claims") }) t.Run("Missing kid returns 400", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + ctx := newMockContext(t) + defer ctx.ctrl.Finish() jsonRequest := SignJwtRequest{ Claims: map[string]interface{}{"iss": "nuts"}, } + jsonData, _ := json.Marshal(jsonRequest) + + ctx.echo.EXPECT().Bind(gomock.Any()).Do(func(f interface{}) { + _ = json.Unmarshal(jsonData, f) + }) + + err := ctx.client.SignJwt(ctx.echo) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "code=400, message=missing kid") + }) - json, _ := json.Marshal(jsonRequest) - request := &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(json)), + t.Run("Sign error returns 400", func(t *testing.T) { + ctx := newMockContext(t) + defer ctx.ctrl.Finish() + + jsonRequest := SignJwtRequest{ + Kid: "unknown", + Claims: map[string]interface{}{"iss": "nuts"}, } + jsonData, _ := json.Marshal(jsonRequest) - echo.EXPECT().Request().Return(request) + ctx.echo.EXPECT().Bind(gomock.Any()).Do(func(f interface{}) { + _ = json.Unmarshal(jsonData, f) + }) + ctx.keyStore.EXPECT().SignJWT(gomock.Any(), "unknown").Return("", errors.New("b00m!")) - err := client.SignJwt(echo) + err := ctx.client.SignJwt(ctx.echo) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "code=400, message=missing kid") + assert.Contains(t, err.Error(), "code=400, message=b00m!") }) t.Run("All OK returns 200", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + ctx := newMockContext(t) + defer ctx.ctrl.Finish() jsonRequest := SignJwtRequest{ - Kid: kid, + Kid: "kid", Claims: map[string]interface{}{"iss": "nuts"}, } - json, _ := json.Marshal(jsonRequest) - request := &http.Request{ - Body: ioutil.NopCloser(bytes.NewReader(json)), - } + jsonData, _ := json.Marshal(jsonRequest) - echo.EXPECT().Request().Return(request) - echo.EXPECT().String(http.StatusOK, gomock.Any()) + ctx.echo.EXPECT().Bind(gomock.Any()).Do(func(f interface{}) { + _ = json.Unmarshal(jsonData, f) + }) + ctx.keyStore.EXPECT().SignJWT(gomock.Any(), "kid").Return("token", nil) + ctx.echo.EXPECT().String(http.StatusOK, "token") - err := client.SignJwt(echo) + err := ctx.client.SignJwt(ctx.echo) assert.Nil(t, err) }) t.Run("Missing body gives 400", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) - - request := &http.Request{} + ctx := newMockContext(t) + defer ctx.ctrl.Finish() - echo.EXPECT().Request().Return(request) + ctx.echo.EXPECT().Bind(gomock.Any()).Return(errors.New("missing body in request")) - err := client.SignJwt(echo) + err := ctx.client.SignJwt(ctx.echo) assert.NotNil(t, err) assert.Contains(t, err.Error(), "code=400, message=missing body in request") @@ -130,58 +127,71 @@ func TestWrapper_SignJwt(t *testing.T) { } func TestWrapper_PublicKey(t *testing.T) { - client := apiWrapper(t) - - publicKey, _ := client.C.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) - t.Run("PublicKey API call returns 200", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + ctx := newMockContext(t) + defer ctx.ctrl.Finish() + + key := test.GenerateECKey() - echo.EXPECT().Request().Return(&http.Request{}) - echo.EXPECT().String(http.StatusOK, gomock.Any()) + ctx.echo.EXPECT().Request().Return(&http.Request{}) + ctx.keyStore.EXPECT().GetPublicKey("kid").Return(key.Public(), nil) + ctx.echo.EXPECT().String(http.StatusOK, gomock.Any()) - _ = client.PublicKey(echo, kid) + _ = ctx.client.PublicKey(ctx.echo, "kid") }) t.Run("PublicKey API call returns JWK", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + ctx := newMockContext(t) + defer ctx.ctrl.Finish() - echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) - echo.EXPECT().JSON(http.StatusOK, gomock.Any()) + key := test.GenerateECKey() - _ = client.PublicKey(echo, kid) + ctx.echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) + ctx.keyStore.EXPECT().GetPublicKey("kid").Return(key.Public(), nil) + ctx.echo.EXPECT().JSON(http.StatusOK, gomock.Any()) + + _ = ctx.client.PublicKey(ctx.echo, "kid") }) t.Run("PublicKey API call returns 404 for unknown", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + ctx := newMockContext(t) + defer ctx.ctrl.Finish() - echo.EXPECT().Request().Return(&http.Request{}) - echo.EXPECT().NoContent(http.StatusNotFound) + ctx.echo.EXPECT().Request().Return(&http.Request{}) + ctx.keyStore.EXPECT().GetPublicKey("kid").Return(nil, storage.ErrNotFound) + ctx.echo.EXPECT().NoContent(http.StatusNotFound) - _ = client.PublicKey(echo, "not") + _ = ctx.client.PublicKey(ctx.echo, "kid") }) - t.Run("PublicKey API call returns 404 for unknown, JWK requested", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - echo := mock.NewMockContext(ctrl) + t.Run("PublicKey API call returns 500 for other error", func(t *testing.T) { + ctx := newMockContext(t) + defer ctx.ctrl.Finish() - echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) - echo.EXPECT().NoContent(http.StatusNotFound) + ctx.echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}}) + ctx.keyStore.EXPECT().GetPublicKey("kid").Return(nil, errors.New("b00m!")) - _ = client.PublicKey(echo, "not") + err := ctx.client.PublicKey(ctx.echo, "kid") + assert.Error(t, err) }) } -func apiWrapper(t *testing.T) *Wrapper { - crypto := crypto.NewTestCryptoInstance(io.TestDirectory(t)) +type mockContext struct { + ctrl *gomock.Controller + echo *mock.MockContext + keyStore *mock.MockKeyStore + client *Wrapper +} - return &Wrapper{C: crypto} +func newMockContext(t *testing.T) mockContext { + ctrl := gomock.NewController(t) + keyStore := mock.NewMockKeyStore(ctrl) + client := &Wrapper{C: keyStore} + + return mockContext{ + ctrl: ctrl, + echo: mock.NewMockContext(ctrl), + keyStore: keyStore, + client: client, + } } From 4f04b8bd0a4248c84a2e455996397aea60572c90 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:27:43 +0100 Subject: [PATCH 12/27] some more coverage --- crypto/crypto_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 34398ae5d2..f7755299b0 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -94,6 +94,20 @@ func TestCrypto_GetPrivateKey(t *testing.T) { _, ok = pk.(*ecdsa.PrivateKey) assert.False(t, ok) }) + + t.Run("get private key, assert parts", func(t *testing.T) { + publicKey, _ := client.GenerateKeyPair() + kid, _ := util.Fingerprint(publicKey) + + pk, _ := client.GetPrivateKey(kid) + if !assert.NotNil(t, pk) { + return + } + + ok := pk.(opaquePrivateKey) + assert.NotNil(t, ok.publicKey) + assert.NotNil(t, ok.signFn) + }) } func TestCrypto_KeyExistsFor(t *testing.T) { From 600f4324187cd5d298bdd9d71b5949a6ebcdc259 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:44:48 +0100 Subject: [PATCH 13/27] added coverage, added NamingFunc on key generation --- crypto/api/v1/api_test.go | 17 ++++++++------- crypto/crypto.go | 20 +++--------------- crypto/crypto_test.go | 29 ++++++++++++++++---------- crypto/interface.go | 8 +++++-- crypto/jwx.go | 2 +- crypto/jwx_test.go | 5 ++--- {mock => crypto/mock}/mock_crypto.go | 31 +++++++++++++++++++++------- docs/pages/development.rst | 2 +- 8 files changed, 64 insertions(+), 50 deletions(-) rename {mock => crypto/mock}/mock_crypto.go (73%) diff --git a/crypto/api/v1/api_test.go b/crypto/api/v1/api_test.go index ffc1657245..c8fe0ab8e9 100644 --- a/crypto/api/v1/api_test.go +++ b/crypto/api/v1/api_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/golang/mock/gomock" + mock2 "github.com/nuts-foundation/nuts-node/crypto/mock" "github.com/nuts-foundation/nuts-node/crypto/storage" "github.com/nuts-foundation/nuts-node/crypto/test" "github.com/nuts-foundation/nuts-node/mock" @@ -177,21 +178,21 @@ func TestWrapper_PublicKey(t *testing.T) { } type mockContext struct { - ctrl *gomock.Controller - echo *mock.MockContext - keyStore *mock.MockKeyStore - client *Wrapper + ctrl *gomock.Controller + echo *mock.MockContext + keyStore *mock2.MockKeyStore + client *Wrapper } func newMockContext(t *testing.T) mockContext { ctrl := gomock.NewController(t) - keyStore := mock.NewMockKeyStore(ctrl) + keyStore := mock2.NewMockKeyStore(ctrl) client := &Wrapper{C: keyStore} return mockContext{ - ctrl: ctrl, - echo: mock.NewMockContext(ctrl), + ctrl: ctrl, + echo: mock.NewMockContext(ctrl), keyStore: keyStore, - client: client, + client: client, } } diff --git a/crypto/crypto.go b/crypto/crypto.go index 2c451380ad..ea48fa3d8d 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -30,7 +30,6 @@ import ( "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto/storage" - "github.com/nuts-foundation/nuts-node/crypto/util" ) // Config holds the values for the crypto engine @@ -135,31 +134,18 @@ func (client *Crypto) doConfigure() error { } // GenerateKeyPair generates a new key pair. If a key pair with the same identifier already exists, it is overwritten. -func (client *Crypto) GenerateKeyPair() (crypto.PublicKey, error) { - privateKey, err := client.generateAndStoreKeyPair() - if err != nil { - return nil, err - } - - return jwk.PublicKeyOf(privateKey) -} - -func (client *Crypto) generateAndStoreKeyPair() (crypto.PrivateKey, error) { +func (client *Crypto) New(namingFunc KidNamingFunc) (crypto.PublicKey, error) { keyPair, err := generateECKeyPair() if err != nil { return nil, err } - kid, err := util.Fingerprint(keyPair.PublicKey) - if err != nil { - return nil, err - } - + kid := namingFunc(keyPair.Public()) if err = client.Storage.SavePrivateKey(kid, keyPair); err != nil { return nil, err } - return keyPair, nil + return jwk.PublicKeyOf(keyPair) } func generateECKeyPair() (*ecdsa.PrivateKey, error) { diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index f7755299b0..5762263330 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -19,13 +19,13 @@ package crypto import ( + "crypto" "crypto/ecdsa" "crypto/rsa" "errors" "reflect" "testing" - "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/nuts-foundation/nuts-node/test/io" "github.com/nuts-foundation/nuts-node/core" @@ -49,8 +49,8 @@ func TestCryptoBackend(t *testing.T) { func TestCrypto_PublicKey(t *testing.T) { client := createCrypto(t) - publicKey, _ := client.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) + kid := "kid" + client.New(stringNamingFunc(kid)) t.Run("Public key is returned from storage", func(t *testing.T) { pub, err := client.GetPublicKey(kid) @@ -77,8 +77,8 @@ func TestCrypto_GetPrivateKey(t *testing.T) { assert.Error(t, err) }) t.Run("get private key, assert non-exportable", func(t *testing.T) { - publicKey, _ := client.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) + kid := "kid" + client.New(stringNamingFunc(kid)) pk, err := client.GetPrivateKey(kid) if !assert.NoError(t, err) { @@ -96,8 +96,8 @@ func TestCrypto_GetPrivateKey(t *testing.T) { }) t.Run("get private key, assert parts", func(t *testing.T) { - publicKey, _ := client.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) + kid := "kid2" + client.New(stringNamingFunc(kid)) pk, _ := client.GetPrivateKey(kid) if !assert.NotNil(t, pk) { @@ -113,8 +113,8 @@ func TestCrypto_GetPrivateKey(t *testing.T) { func TestCrypto_KeyExistsFor(t *testing.T) { client := createCrypto(t) - pub, _ := client.GenerateKeyPair() - kid, _ := util.Fingerprint(pub) + kid := "kid" + client.New(stringNamingFunc(kid)) t.Run("returns true for existing key", func(t *testing.T) { assert.True(t, client.PrivateKeyExists(kid)) @@ -125,11 +125,12 @@ func TestCrypto_KeyExistsFor(t *testing.T) { }) } -func TestCrypto_GenerateKeyPair(t *testing.T) { +func TestCrypto_New(t *testing.T) { client := createCrypto(t) t.Run("ok", func(t *testing.T) { - publicKey, err := client.GenerateKeyPair() + kid := "kid" + publicKey, err := client.New(stringNamingFunc(kid)) assert.NoError(t, err) assert.NotNil(t, publicKey) }) @@ -177,6 +178,12 @@ func TestCrypto_Configure(t *testing.T) { }) } +func stringNamingFunc(name string) KidNamingFunc { + return func(key crypto.PublicKey) string { + return name + } +} + func createCrypto(t *testing.T) *Crypto { if err := core.NutsConfig().Load(&cobra.Command{}); err != nil { panic(err) diff --git a/crypto/interface.go b/crypto/interface.go index faeb6b70f0..6e4d37545b 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -22,10 +22,14 @@ import ( "crypto" ) +// KidNamingFunc is a function passed to New() which generates the kid for the pub/priv key +type KidNamingFunc func(key crypto.PublicKey) string + // KeyStore defines the functions than can be called by a Cmd, Direct or via rest call. type KeyStore interface { - // GenerateKeyPair generates a keypair, stores the private key and returns the public key. - GenerateKeyPair() (crypto.PublicKey, error) + // New generates a keypair and returns the public key. + // the KidNamingFunc will provide the kid. priv/pub keys are appended with a postfix and stored + New(namingFunc KidNamingFunc) (crypto.PublicKey, error) // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. GetPrivateKey(kid string) (crypto.Signer, error) // GetPublicKey returns the PublicKey diff --git a/crypto/jwx.go b/crypto/jwx.go index 2fd312b636..2b021bf3db 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -45,7 +45,7 @@ func (client *Crypto) SignJWT(claims map[string]interface{}, kid string) (token return "", err } - if err = jwk.AssignKeyID(key); err != nil { + if err = key.Set(jwk.KeyIDKey, kid); err != nil { return "", err } diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 994e887984..78c6c3b08f 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -31,7 +31,6 @@ import ( "github.com/lestrrat-go/jwx/jws" "github.com/nuts-foundation/nuts-node/crypto/storage" "github.com/nuts-foundation/nuts-node/crypto/test" - "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -105,8 +104,8 @@ func TestSignJWT(t *testing.T) { func TestCrypto_SignJWT(t *testing.T) { client := createCrypto(t) - publicKey, _ := client.GenerateKeyPair() - kid, _ := util.Fingerprint(publicKey) + kid := "kid" + client.New(stringNamingFunc(kid)) t.Run("creates valid JWT", func(t *testing.T) { tokenString, err := client.SignJWT(map[string]interface{}{"iss": "nuts"}, kid) diff --git a/mock/mock_crypto.go b/crypto/mock/mock_crypto.go similarity index 73% rename from mock/mock_crypto.go rename to crypto/mock/mock_crypto.go index 72f54b210c..c6b8dbb86f 100644 --- a/mock/mock_crypto.go +++ b/crypto/mock/mock_crypto.go @@ -1,3 +1,18 @@ +/* + * Nuts node + * Copyright (C) 2021 Nuts community + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + // Code generated by MockGen. DO NOT EDIT. // Source: crypto/interface.go @@ -6,8 +21,10 @@ package mock import ( crypto "crypto" - gomock "github.com/golang/mock/gomock" reflect "reflect" + + gomock "github.com/golang/mock/gomock" + crypto0 "github.com/nuts-foundation/nuts-node/crypto" ) // MockKeyStore is a mock of KeyStore interface @@ -33,19 +50,19 @@ func (m *MockKeyStore) EXPECT() *MockKeyStoreMockRecorder { return m.recorder } -// GenerateKeyPair mocks base method -func (m *MockKeyStore) GenerateKeyPair() (crypto.PublicKey, error) { +// New mocks base method +func (m *MockKeyStore) New(namingFunc crypto0.KidNamingFunc) (crypto.PublicKey, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateKeyPair") + ret := m.ctrl.Call(m, "New", namingFunc) ret0, _ := ret[0].(crypto.PublicKey) ret1, _ := ret[1].(error) return ret0, ret1 } -// GenerateKeyPair indicates an expected call of GenerateKeyPair -func (mr *MockKeyStoreMockRecorder) GenerateKeyPair() *gomock.Call { +// New indicates an expected call of New +func (mr *MockKeyStoreMockRecorder) New(namingFunc interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKeyPair", reflect.TypeOf((*MockKeyStore)(nil).GenerateKeyPair)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockKeyStore)(nil).New), namingFunc) } // GetPrivateKey mocks base method diff --git a/docs/pages/development.rst b/docs/pages/development.rst index 4a7f961d09..2bdbdc5fc8 100644 --- a/docs/pages/development.rst +++ b/docs/pages/development.rst @@ -37,7 +37,7 @@ These mocks are used by other modules .. code-block:: shell - mockgen -destination=mock/mock_crypto.go -package=mock -source=crypto/interface.go KeyStore + mockgen -destination=crypto/mock/mock_crypto.go -package=mock -source=crypto/interface.go KeyStore README ****** From 86989f50e7926a3304ef5d4634566de412f46c0d Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:47:33 +0100 Subject: [PATCH 14/27] removed kid fingwerprint --- crypto/storage/fs_test.go | 10 ++++------ crypto/util/kid.go | 40 --------------------------------------- 2 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 crypto/util/kid.go diff --git a/crypto/storage/fs_test.go b/crypto/storage/fs_test.go index 8543c38905..62f1e3f35f 100644 --- a/crypto/storage/fs_test.go +++ b/crypto/storage/fs_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/nuts-foundation/nuts-node/crypto/test" - "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/nuts-foundation/nuts-node/test/io" "github.com/stretchr/testify/assert" @@ -29,7 +28,7 @@ func Test_fs_GetPublicKey(t *testing.T) { t.Run("ok", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid, _ := util.Fingerprint(pk) + kid := "kid" err := storage.SavePublicKey(kid, pk) @@ -56,8 +55,7 @@ func Test_fs_GetPrivateKey(t *testing.T) { }) t.Run("private key invalid", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) - pk := test.GenerateECKey() - kid, _ := util.Fingerprint(pk.PublicKey) + kid := "kid" path := storage.getEntryPath(kid, privateKeyEntry) file, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) _, err := file.WriteString("hello world") @@ -73,7 +71,7 @@ func Test_fs_GetPrivateKey(t *testing.T) { t.Run("ok", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid, _ := util.Fingerprint(pk) + kid := "kid" err := storage.SavePrivateKey(kid, pk) if !assert.NoError(t, err) { @@ -98,7 +96,7 @@ func Test_fs_KeyExistsFor(t *testing.T) { t.Run("existing entry", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) pk := test.GenerateECKey() - kid, _ := util.Fingerprint(pk) + kid := "kid" storage.SavePrivateKey(kid, pk) assert.True(t, storage.PrivateKeyExists(kid)) }) diff --git a/crypto/util/kid.go b/crypto/util/kid.go deleted file mode 100644 index 5c39cdac8e..0000000000 --- a/crypto/util/kid.go +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Nuts node - * Copyright (C) 2021 Nuts community - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package util - -import ( - "crypto" - "encoding/base64" - - "github.com/lestrrat-go/jwx/jwk" -) - -// Fingerprint returns the JWK thumbprint using the indicated -// hashing algorithm, according to RFC 7638 -func Fingerprint(key interface{}) (string, error) { - k, err := jwk.New(key) - if err != nil { - return "", err - } - - tp, err := k.Thumbprint(crypto.SHA256) - if err != nil { - return "", err - } - - // trailing '=' not allowed in kid - return base64.RawURLEncoding.EncodeToString(tp), nil -} From 11bd968a2cdccd5aa3fd99c166c24a023c12ff78 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:49:28 +0100 Subject: [PATCH 15/27] ignore mock in codeclimate --- .codeclimate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index e736d509f7..c9ff01d3eb 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -11,7 +11,7 @@ exclude_patterns: - "**/*_test.go" - '**/test.go' - "test/*" - - "**/mock.go" + - "**/*_mock.go" - "**/generated.go" - "mock/*" From 9f3783fbf5898058d356714e00d0fc142a5ca350 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:51:31 +0100 Subject: [PATCH 16/27] cleanup --- crypto/jwx.go | 2 +- crypto/storage/fs.go | 2 +- crypto/storage/fs_test.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crypto/jwx.go b/crypto/jwx.go index 2b021bf3db..92a67317b5 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -33,7 +33,7 @@ import ( // ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported") -// SignJwtFor creates a signed JWT given a legalEntity and map of claims +// SignJWT creates a signed JWT given a legalEntity and map of claims func (client *Crypto) SignJWT(claims map[string]interface{}, kid string) (token string, err error) { privateKey, err := client.Storage.GetPrivateKey(kid) diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go index 82f2ec22ba..9174482978 100644 --- a/crypto/storage/fs.go +++ b/crypto/storage/fs.go @@ -62,7 +62,7 @@ type fileSystemBackend struct { // NewFileSystemBackend creates a new filesystem backend, all directories will be created for the given path // Using a filesystem backend in production is not recommended! -func NewFileSystemBackend(fspath string) (*fileSystemBackend, error) { +func NewFileSystemBackend(fspath string) (Storage, error) { if fspath == "" { return nil, errors.New("filesystem path is empty") } diff --git a/crypto/storage/fs_test.go b/crypto/storage/fs_test.go index 62f1e3f35f..93c344d7d7 100644 --- a/crypto/storage/fs_test.go +++ b/crypto/storage/fs_test.go @@ -30,7 +30,7 @@ func Test_fs_GetPublicKey(t *testing.T) { pk := test.GenerateECKey() kid := "kid" - err := storage.SavePublicKey(kid, pk) + err := storage.(*fileSystemBackend).SavePublicKey(kid, pk) if !assert.NoError(t, err) { return @@ -56,7 +56,7 @@ func Test_fs_GetPrivateKey(t *testing.T) { t.Run("private key invalid", func(t *testing.T) { storage, _ := NewFileSystemBackend(io.TestDirectory(t)) kid := "kid" - path := storage.getEntryPath(kid, privateKeyEntry) + path := storage.(*fileSystemBackend).getEntryPath(kid, privateKeyEntry) file, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) _, err := file.WriteString("hello world") if !assert.NoError(t, err) { From c76cddf0dec7a8283b8a5cffec12d587b9cab9cf Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:52:43 +0100 Subject: [PATCH 17/27] codeclimate conf --- .codeclimate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index c9ff01d3eb..1d4ffdd5d1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -11,9 +11,9 @@ exclude_patterns: - "**/*_test.go" - '**/test.go' - "test/*" - - "**/*_mock.go" + - "**/mock.go" - "**/generated.go" - - "mock/*" + - "**/mock/*" plugins: gofmt: From b9788f186c045844e0a1ee3a763d15a9c06131ba Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:58:00 +0100 Subject: [PATCH 18/27] more cleanup --- crypto/crypto.go | 4 ++-- crypto/engine/engine.go | 11 ++++++++--- crypto/types/types.go | 26 -------------------------- 3 files changed, 10 insertions(+), 31 deletions(-) delete mode 100644 crypto/types/types.go diff --git a/crypto/crypto.go b/crypto/crypto.go index ea48fa3d8d..b90ceb5ab3 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -54,7 +54,7 @@ func DefaultCryptoConfig() Config { } } -// default implementation for Instance +// Crypto holds references to storage and needed config type Crypto struct { Storage storage.Storage Config Config @@ -133,7 +133,7 @@ func (client *Crypto) doConfigure() error { return nil } -// GenerateKeyPair generates a new key pair. If a key pair with the same identifier already exists, it is overwritten. +// New generates a new key pair. If a key pair with the same identifier already exists, it is overwritten. func (client *Crypto) New(namingFunc KidNamingFunc) (crypto.PublicKey, error) { keyPair, err := generateECKeyPair() if err != nil { diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go index a5e3934086..18f63d398e 100644 --- a/crypto/engine/engine.go +++ b/crypto/engine/engine.go @@ -30,13 +30,18 @@ import ( "github.com/nuts-foundation/nuts-node/core" crypto2 "github.com/nuts-foundation/nuts-node/crypto" api "github.com/nuts-foundation/nuts-node/crypto/api/v1" - "github.com/nuts-foundation/nuts-node/crypto/types" "github.com/nuts-foundation/nuts-node/crypto/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" ) +// ConfigStorage is used as --storage config flag +const ConfigStorage string = "storage" + +// ConfigFSPath is used as --fspath config flagclient.getStoragePath() +const ConfigFSPath string = "fspath" + // NewCryptoEngine the engine configuration for nuts-go. func NewCryptoEngine() *core.Engine { cb := crypto2.Instance() @@ -60,8 +65,8 @@ func flagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("crypto", pflag.ContinueOnError) defs := crypto2.DefaultCryptoConfig() - flags.String(types.ConfigStorage, defs.Storage, fmt.Sprintf("Storage to use, 'fs' for file system, default: %s", defs.Storage)) - flags.String(types.ConfigFSPath, defs.Fspath, fmt.Sprintf("When file system is used as storage, this configures the path where key material and the truststore are persisted, default: %v", defs.Fspath)) + flags.String(ConfigStorage, defs.Storage, fmt.Sprintf("Storage to use, 'fs' for file system, default: %s", defs.Storage)) + flags.String(ConfigFSPath, defs.Fspath, fmt.Sprintf("When file system is used as storage, this configures the path where key material and the truststore are persisted, default: %v", defs.Fspath)) return flags } diff --git a/crypto/types/types.go b/crypto/types/types.go deleted file mode 100644 index 45aa2b0f74..0000000000 --- a/crypto/types/types.go +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Nuts crypto - * Copyright (C) 2019. Nuts community - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -// types and interfaces used by all other packages -package types - -// ConfigStorage is used as --storage config flag -const ConfigStorage string = "storage" - -// ConfigFSPath is used as --fspath config flagclient.getStoragePath() -const ConfigFSPath string = "fspath" From d544c980e84942a63a40a9fca9dc1be29fcf4943 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 11:58:57 +0100 Subject: [PATCH 19/27] remove storage package comment --- crypto/storage/storage.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/crypto/storage/storage.go b/crypto/storage/storage.go index 3aa0600e0b..09bbb86916 100644 --- a/crypto/storage/storage.go +++ b/crypto/storage/storage.go @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -// The backend package contains the various options for storing the actual private keys. -// Currently only a file backend is supported package storage import ( From 6ec07c734731926762f02adec9b852b3aa7d478f Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 13:15:02 +0100 Subject: [PATCH 20/27] extra test for crypto engine --- crypto/api/v1/client.go | 3 +-- crypto/crypto_test.go | 17 +++++----------- crypto/engine/engine.go | 33 ++++++++++-------------------- crypto/engine/engine_test.go | 39 ++++++++++++++++++++++++++++++++++++ crypto/jwx_test.go | 2 +- crypto/test.go | 8 ++++++++ 6 files changed, 65 insertions(+), 37 deletions(-) diff --git a/crypto/api/v1/client.go b/crypto/api/v1/client.go index 2397d2b25e..c4590570a7 100644 --- a/crypto/api/v1/client.go +++ b/crypto/api/v1/client.go @@ -21,7 +21,6 @@ package v1 import ( "context" - "crypto" "errors" "fmt" "io/ioutil" @@ -59,7 +58,7 @@ func (hb HTTPClient) client() ClientInterface { } // GetPublicKey returns a PrivateKey from the server given a kid -func (hb HTTPClient) GetPublicKey(kid string) (crypto.PublicKey, error) { +func (hb HTTPClient) GetPublicKey(kid string) (jwk.Key, error) { ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) defer cancel() httpClient := hb.clientWithRequestEditor(func(ctx context.Context, req *http.Request) error { diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 5762263330..5bb6afab26 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -19,7 +19,6 @@ package crypto import ( - "crypto" "crypto/ecdsa" "crypto/rsa" "errors" @@ -50,7 +49,7 @@ func TestCrypto_PublicKey(t *testing.T) { client := createCrypto(t) kid := "kid" - client.New(stringNamingFunc(kid)) + client.New(StringNamingFunc(kid)) t.Run("Public key is returned from storage", func(t *testing.T) { pub, err := client.GetPublicKey(kid) @@ -78,7 +77,7 @@ func TestCrypto_GetPrivateKey(t *testing.T) { }) t.Run("get private key, assert non-exportable", func(t *testing.T) { kid := "kid" - client.New(stringNamingFunc(kid)) + client.New(StringNamingFunc(kid)) pk, err := client.GetPrivateKey(kid) if !assert.NoError(t, err) { @@ -97,7 +96,7 @@ func TestCrypto_GetPrivateKey(t *testing.T) { t.Run("get private key, assert parts", func(t *testing.T) { kid := "kid2" - client.New(stringNamingFunc(kid)) + client.New(StringNamingFunc(kid)) pk, _ := client.GetPrivateKey(kid) if !assert.NotNil(t, pk) { @@ -114,7 +113,7 @@ func TestCrypto_KeyExistsFor(t *testing.T) { client := createCrypto(t) kid := "kid" - client.New(stringNamingFunc(kid)) + client.New(StringNamingFunc(kid)) t.Run("returns true for existing key", func(t *testing.T) { assert.True(t, client.PrivateKeyExists(kid)) @@ -130,7 +129,7 @@ func TestCrypto_New(t *testing.T) { t.Run("ok", func(t *testing.T) { kid := "kid" - publicKey, err := client.New(stringNamingFunc(kid)) + publicKey, err := client.New(StringNamingFunc(kid)) assert.NoError(t, err) assert.NotNil(t, publicKey) }) @@ -178,12 +177,6 @@ func TestCrypto_Configure(t *testing.T) { }) } -func stringNamingFunc(name string) KidNamingFunc { - return func(key crypto.PublicKey) string { - return name - } -} - func createCrypto(t *testing.T) *Crypto { if err := core.NutsConfig().Load(&cobra.Command{}); err != nil { panic(err) diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go index 18f63d398e..e7c54010c7 100644 --- a/crypto/engine/engine.go +++ b/crypto/engine/engine.go @@ -19,19 +19,16 @@ package engine import ( + "crypto" "encoding/json" "errors" "fmt" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/core" crypto2 "github.com/nuts-foundation/nuts-node/crypto" api "github.com/nuts-foundation/nuts-node/crypto/api/v1" "github.com/nuts-foundation/nuts-node/crypto/util" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -79,20 +76,6 @@ func cmd() *cobra.Command { Use: "crypto", Short: "crypto commands", } - - cmd.AddCommand(&cobra.Command{ - Use: "server", - Short: "Run standalone crypto server", - Run: func(cmd *cobra.Command, args []string) { - cryptoEngine := crypto2.Instance() - echoServer := echo.New() - echoServer.HideBanner = true - echoServer.Use(middleware.Logger()) - api.RegisterHandlers(echoServer, &api.Wrapper{C: cryptoEngine}) - logrus.Fatal(echoServer.Start(":1324")) - }, - }) - cmd.AddCommand(&cobra.Command{ Use: "publicKey [kid]", Short: "views the publicKey for a given kid", @@ -109,19 +92,18 @@ func cmd() *cobra.Command { cc := newCryptoClient(cmd) kid := args[0] - pubKey, err := cc.GetPublicKey(kid) + jwkKey, err := cc.GetPublicKey(kid) if err != nil { cmd.Printf("Error printing publicKey: %v", err) return } // printout in JWK - jwk, err := jwk.New(pubKey) if err != nil { cmd.Printf("Error printing publicKey: %v", err) return } - asJSON, err := json.MarshalIndent(jwk, "", " ") + asJSON, err := json.MarshalIndent(jwkKey, "", " ") if err != nil { cmd.Printf("Error printing publicKey: %v\n", err) return @@ -131,7 +113,14 @@ func cmd() *cobra.Command { cmd.Println("") // printout in PEM - publicKeyAsPEM, err := util.PublicKeyToPem(pubKey) + var target interface{} + err = jwkKey.Raw(&target) + if err != nil { + cmd.Printf("Error printing publicKey: %v\n", err) + return + } + + publicKeyAsPEM, err := util.PublicKeyToPem(target.(crypto.PublicKey)) if err != nil { cmd.Printf("Error printing publicKey: %v\n", err) return diff --git a/crypto/engine/engine_test.go b/crypto/engine/engine_test.go index 146162a5cd..f27db3e97f 100644 --- a/crypto/engine/engine_test.go +++ b/crypto/engine/engine_test.go @@ -21,6 +21,9 @@ package engine import ( "bytes" "io/ioutil" + "net/http" + "net/http/httptest" + "os" "strings" "testing" @@ -61,6 +64,23 @@ func TestNewCryptoEngine_Routes(t *testing.T) { }) } +type handler struct { + statusCode int + responseData []byte +} + +func (h handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(h.statusCode) + writer.Write(h.responseData) +} +var jwkAsString = ` +{ + "kty" : "RSA", + "n" : "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w", + "e" : "AQAB" +}` +var jwkAsBytes = []byte(jwkAsString) + func TestNewCryptoEngine_Cmd(t *testing.T) { core.NutsConfig().Load(&cobra.Command{}) @@ -92,6 +112,25 @@ func TestNewCryptoEngine_Cmd(t *testing.T) { return } }) + + t.Run("ok - write to stdout", func(t *testing.T) { + cmd, _ := createCmd(t) + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: jwkAsBytes}) + os.Setenv("NUTS_ADDRESS", s.URL) + core.NutsConfig().Load(cmd) + defer s.Close() + + buf := new(bytes.Buffer) + cmd.SetArgs([]string{"publicKey", "kid"}) + cmd.SetOut(buf) + err := cmd.Execute() + + if !assert.NoError(t, err) { + return + } + assert.Contains(t, buf.String(), "Public key in JWK") + assert.Contains(t, buf.String(), "Public key in PEM") + }) }) } diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 78c6c3b08f..b0c80932a9 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -105,7 +105,7 @@ func TestCrypto_SignJWT(t *testing.T) { client := createCrypto(t) kid := "kid" - client.New(stringNamingFunc(kid)) + client.New(StringNamingFunc(kid)) t.Run("creates valid JWT", func(t *testing.T) { tokenString, err := client.SignJWT(map[string]interface{}{"iss": "nuts"}, kid) diff --git a/crypto/test.go b/crypto/test.go index 2a8d39d387..4a9ccbe7d6 100644 --- a/crypto/test.go +++ b/crypto/test.go @@ -1,6 +1,7 @@ package crypto import ( + "crypto" "path" "github.com/sirupsen/logrus" @@ -26,3 +27,10 @@ func TestCryptoConfig(testDirectory string) Config { config.Fspath = path.Join(testDirectory, "crypto") return config } + +// StringNamingFunc can be used to give a key a simple string name +func StringNamingFunc(name string) KidNamingFunc { + return func(key crypto.PublicKey) string { + return name + } +} From 9b6c54e35cf0ac9ecfd2d289bf26a85d7d01ca33 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 13:42:18 +0100 Subject: [PATCH 21/27] one more test --- crypto/jwx_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index b0c80932a9..76fbacac03 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -132,3 +132,13 @@ func TestCrypto_SignJWT(t *testing.T) { assert.True(t, errors.Is(err, storage.ErrNotFound)) }) } + +func TestCrypto_convertHeaders(t *testing.T) { + rawHeaders := map[string]interface{} { + "key": "value", + } + + jwtHeader := convertHeaders(rawHeaders) + v, _ := jwtHeader.Get("key") + assert.Equal(t, "value", v) +} From 893cf56c83e3f75f3de9ad03887477748af854c1 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 13:50:53 +0100 Subject: [PATCH 22/27] one more test --- crypto/crypto_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 5bb6afab26..3c863d9b6e 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -177,6 +177,15 @@ func TestCrypto_Configure(t *testing.T) { }) } +func TestCryptoConfig_getFsPath(t *testing.T) { + t.Run("no path configured returns defaultPath", func(t *testing.T) { + c := Config{ + Fspath: "", + } + assert.Equal(t, "./", c.getFSPath()) + }) +} + func createCrypto(t *testing.T) *Crypto { if err := core.NutsConfig().Load(&cobra.Command{}); err != nil { panic(err) From 8e121544d141729eb2be64ec1e4cb7453bc09742 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 14:08:32 +0100 Subject: [PATCH 23/27] return kid from keyStore.New --- crypto/crypto.go | 12 ++++++++---- crypto/crypto_test.go | 3 ++- crypto/interface.go | 2 +- crypto/mock/mock_crypto.go | 25 +++++-------------------- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/crypto/crypto.go b/crypto/crypto.go index b90ceb5ab3..9dd5b32561 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -134,18 +134,22 @@ func (client *Crypto) doConfigure() error { } // New generates a new key pair. If a key pair with the same identifier already exists, it is overwritten. -func (client *Crypto) New(namingFunc KidNamingFunc) (crypto.PublicKey, error) { +func (client *Crypto) New(namingFunc KidNamingFunc) (crypto.PublicKey, string, error) { keyPair, err := generateECKeyPair() if err != nil { - return nil, err + return nil, "", err } kid := namingFunc(keyPair.Public()) if err = client.Storage.SavePrivateKey(kid, keyPair); err != nil { - return nil, err + return nil, "", err } - return jwk.PublicKeyOf(keyPair) + pkey, err := jwk.PublicKeyOf(keyPair) + if err != nil { + return nil, "", err + } + return pkey, kid, nil } func generateECKeyPair() (*ecdsa.PrivateKey, error) { diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 3c863d9b6e..031bd5b194 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -129,9 +129,10 @@ func TestCrypto_New(t *testing.T) { t.Run("ok", func(t *testing.T) { kid := "kid" - publicKey, err := client.New(StringNamingFunc(kid)) + publicKey, returnKid, err := client.New(StringNamingFunc(kid)) assert.NoError(t, err) assert.NotNil(t, publicKey) + assert.Equal(t, kid, returnKid) }) } diff --git a/crypto/interface.go b/crypto/interface.go index 6e4d37545b..3bd1844cd2 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -29,7 +29,7 @@ type KidNamingFunc func(key crypto.PublicKey) string type KeyStore interface { // New generates a keypair and returns the public key. // the KidNamingFunc will provide the kid. priv/pub keys are appended with a postfix and stored - New(namingFunc KidNamingFunc) (crypto.PublicKey, error) + New(namingFunc KidNamingFunc) (crypto.PublicKey, string, error) // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. GetPrivateKey(kid string) (crypto.Signer, error) // GetPublicKey returns the PublicKey diff --git a/crypto/mock/mock_crypto.go b/crypto/mock/mock_crypto.go index c6b8dbb86f..2d0a43db0e 100644 --- a/crypto/mock/mock_crypto.go +++ b/crypto/mock/mock_crypto.go @@ -1,18 +1,3 @@ -/* - * Nuts node - * Copyright (C) 2021 Nuts community - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - // Code generated by MockGen. DO NOT EDIT. // Source: crypto/interface.go @@ -21,10 +6,9 @@ package mock import ( crypto "crypto" - reflect "reflect" - gomock "github.com/golang/mock/gomock" crypto0 "github.com/nuts-foundation/nuts-node/crypto" + reflect "reflect" ) // MockKeyStore is a mock of KeyStore interface @@ -51,12 +35,13 @@ func (m *MockKeyStore) EXPECT() *MockKeyStoreMockRecorder { } // New mocks base method -func (m *MockKeyStore) New(namingFunc crypto0.KidNamingFunc) (crypto.PublicKey, error) { +func (m *MockKeyStore) New(namingFunc crypto0.KidNamingFunc) (crypto.PublicKey, string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "New", namingFunc) ret0, _ := ret[0].(crypto.PublicKey) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // New indicates an expected call of New From 6920e6308c27de4093fb99af175003d2a14ffa30 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 14:13:49 +0100 Subject: [PATCH 24/27] one more test --- crypto/jwx_test.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 76fbacac03..b511801ed3 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -134,11 +134,18 @@ func TestCrypto_SignJWT(t *testing.T) { } func TestCrypto_convertHeaders(t *testing.T) { - rawHeaders := map[string]interface{} { - "key": "value", - } + t.Run("nil headers", func(t *testing.T) { + jwtHeader := convertHeaders(nil) + assert.Len(t, jwtHeader.PrivateParams(), 0) + }) + + t.Run("ok", func(t *testing.T) { + rawHeaders := map[string]interface{} { + "key": "value", + } - jwtHeader := convertHeaders(rawHeaders) - v, _ := jwtHeader.Get("key") - assert.Equal(t, "value", v) + jwtHeader := convertHeaders(rawHeaders) + v, _ := jwtHeader.Get("key") + assert.Equal(t, "value", v) + }) } From 0a10ec3d3665b4ebadf23cb15fc503678cc8d3d3 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 14:18:58 +0100 Subject: [PATCH 25/27] one more test --- crypto/crypto_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 031bd5b194..58d9508131 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -19,7 +19,9 @@ package crypto import ( + "crypto" "crypto/ecdsa" + "crypto/rand" "crypto/rsa" "errors" "reflect" @@ -104,8 +106,10 @@ func TestCrypto_GetPrivateKey(t *testing.T) { } ok := pk.(opaquePrivateKey) - assert.NotNil(t, ok.publicKey) - assert.NotNil(t, ok.signFn) + assert.NotNil(t, ok.Public()) + + _, err := ok.Sign(rand.Reader, []byte("hi"), crypto.SHA256) + assert.NoError(t, err) }) } From 43be5e57d56da46bcb55eecbae5cfa05018570aa Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 14:55:49 +0100 Subject: [PATCH 26/27] PR review --- crypto/api/v1/api.go | 22 +++++++++++++++------- crypto/api/v1/client.go | 2 +- crypto/crypto.go | 5 +++-- crypto/interface.go | 5 ++++- crypto/storage/fs.go | 3 --- crypto/storage/storage.go | 4 ++++ crypto/util/pem.go | 15 +++++++++------ crypto/util/pem_test.go | 21 ++++++++++++++------- go.mod | 1 + 9 files changed, 51 insertions(+), 27 deletions(-) diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index 719e95baad..0ccc2714ee 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -37,6 +37,18 @@ type Wrapper struct { C crypto.KeyStore } +func (signRequest SignJwtRequest) validate() error { + if len(signRequest.Kid) == 0 { + return errors.New("missing kid") + } + + if len(signRequest.Claims) == 0 { + return errors.New("missing claims") + } + + return nil +} + // SignJwt handles api calls for signing a Jwt func (w *Wrapper) SignJwt(ctx echo.Context) error { var signRequest = &SignJwtRequest{} @@ -46,12 +58,8 @@ func (w *Wrapper) SignJwt(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - if len(signRequest.Kid) == 0 { - return echo.NewHTTPError(http.StatusBadRequest, "missing kid") - } - - if len(signRequest.Claims) == 0 { - return echo.NewHTTPError(http.StatusBadRequest, "missing claims") + if err := signRequest.validate(); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) } sig, err := w.C.SignJWT(signRequest.Claims, signRequest.Kid) @@ -64,7 +72,7 @@ func (w *Wrapper) SignJwt(ctx echo.Context) error { return ctx.String(http.StatusOK, sig) } -// PublicKey returns a public key for the given urn. The urn represents a legal entity. The api returns the public key either in PEM or JWK format. +// PublicKey returns a public key for the given kid. The urn represents a legal entity. The api returns the public key either in PEM or JWK format. // It uses the accept header to determine this. Default is PEM (text/plain), only when application/json is requested will it return JWK. func (w *Wrapper) PublicKey(ctx echo.Context, kid string) error { acceptHeader := ctx.Request().Header.Get("Accept") diff --git a/crypto/api/v1/client.go b/crypto/api/v1/client.go index c4590570a7..f711838c91 100644 --- a/crypto/api/v1/client.go +++ b/crypto/api/v1/client.go @@ -57,7 +57,7 @@ func (hb HTTPClient) client() ClientInterface { return hb.clientWithRequestEditor(nil) } -// GetPublicKey returns a PrivateKey from the server given a kid +// GetPublicKey returns a PublicKey from the server given a kid func (hb HTTPClient) GetPublicKey(kid string) (jwk.Key, error) { ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) defer cancel() diff --git a/crypto/crypto.go b/crypto/crypto.go index 9dd5b32561..580843174c 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -133,7 +133,8 @@ func (client *Crypto) doConfigure() error { return nil } -// New generates a new key pair. If a key pair with the same identifier already exists, it is overwritten. +// New generates a new key pair. If a key is overwritten is handled by the storage implementation. +// it's considered bad practise to reuse a kid for different keys. func (client *Crypto) New(namingFunc KidNamingFunc) (crypto.PublicKey, string, error) { keyPair, err := generateECKeyPair() if err != nil { @@ -161,7 +162,7 @@ func (client *Crypto) PrivateKeyExists(kid string) bool { return client.Storage.PrivateKeyExists(kid) } -// GetPublicKey loads the key from storage and returns it as PEM encoded. Only supports RSA style keys +// GetPublicKey loads the key from storage func (client *Crypto) GetPublicKey(kid string) (crypto.PublicKey, error) { return client.Storage.GetPublicKey(kid) } diff --git a/crypto/interface.go b/crypto/interface.go index 3bd1844cd2..993d152ba6 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -31,11 +31,14 @@ type KeyStore interface { // the KidNamingFunc will provide the kid. priv/pub keys are appended with a postfix and stored New(namingFunc KidNamingFunc) (crypto.PublicKey, string, error) // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. + // If a key is missing, a Storage.ErrNotFound is returned GetPrivateKey(kid string) (crypto.Signer, error) // GetPublicKey returns the PublicKey + // If a key is missing, a Storage.ErrNotFound is returned GetPublicKey(kid string) (crypto.PublicKey, error) // SignJWT creates a signed JWT using the given key and map of claims (private key must be present). SignJWT(claims map[string]interface{}, kid string) (string, error) // PrivateKeyExists returns if the specified private key exists. - PrivateKeyExists(key string) bool + // If an error occurs, false is also returned + PrivateKeyExists(kid string) bool } diff --git a/crypto/storage/fs.go b/crypto/storage/fs.go index 9174482978..5b509b6426 100644 --- a/crypto/storage/fs.go +++ b/crypto/storage/fs.go @@ -43,9 +43,6 @@ type fileOpenError struct { err error } -// ErrNotFound indicates that the specified crypto storage entry couldn't be found. -var ErrNotFound = errors.New("entry not found") - // Error returns the string representation func (f *fileOpenError) Error() string { return fmt.Sprintf("could not open entry %s with filename %s: %v", f.kid, f.filePath, f.err) diff --git a/crypto/storage/storage.go b/crypto/storage/storage.go index 09bbb86916..9f12a7b960 100644 --- a/crypto/storage/storage.go +++ b/crypto/storage/storage.go @@ -20,8 +20,12 @@ package storage import ( "crypto" + "errors" ) +// ErrNotFound indicates that the specified crypto storage entry couldn't be found. +var ErrNotFound = errors.New("entry not found") + // Storage interface containing functions for storing and retrieving keys type Storage interface { GetPrivateKey(kid string) (crypto.Signer, error) diff --git a/crypto/util/pem.go b/crypto/util/pem.go index 6a3b905a89..1d44657541 100644 --- a/crypto/util/pem.go +++ b/crypto/util/pem.go @@ -26,17 +26,20 @@ import ( // PemToPublicKey converts a PEM encoded public key to a crypto.PublicKey func PemToPublicKey(pub []byte) (crypto.PublicKey, error) { + block, _ := pem.Decode(pub) - if block == nil || block.Type != "PUBLIC KEY" { + if block == nil { return nil, ErrWrongPublicKey } - b := block.Bytes - key, err := x509.ParsePKIXPublicKey(b) - if err != nil { - return nil, err + switch block.Type { + case "PUBLIC KEY": + return x509.ParsePKIXPublicKey(block.Bytes) + case "RSA PUBLIC KEY": + return x509.ParsePKCS1PublicKey(block.Bytes) + default: + return nil, ErrWrongPublicKey } - return key, nil } // PublicKeyToPem converts an public key to PEM encoding diff --git a/crypto/util/pem_test.go b/crypto/util/pem_test.go index b7d8159d03..e592f8f1a9 100644 --- a/crypto/util/pem_test.go +++ b/crypto/util/pem_test.go @@ -54,18 +54,14 @@ func TestCrypto_pemToPublicKey(t *testing.T) { t.Run("wrong PEM block gives error", func(t *testing.T) { _, err := PemToPublicKey([]byte{}) - if err == nil { - t.Errorf("Expected error, Got nothing") + if !assert.Error(t, err) { return } - expected := "failed to decode PEM block containing public key, key is of the wrong type" - if err.Error() != expected { - t.Errorf("Expected error [%s], got [%s]", expected, err.Error()) - } + assert.Equal(t, ErrWrongPublicKey, err) }) - t.Run("converts EC public key", func(t *testing.T) { + t.Run("converts public key", func(t *testing.T) { pem := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEny33KMxU+mtPxSBMIztm69lehhNo\nCQD632dFAYSzDGh2LqemmYx9EKFzuzvCqbw87BD3spzbakjj5R315qV0gw==\n-----END PUBLIC KEY-----" pk, err := PemToPublicKey([]byte(pem)) @@ -75,6 +71,17 @@ func TestCrypto_pemToPublicKey(t *testing.T) { assert.IsType(t, &ecdsa.PublicKey{}, pk) }) + + t.Run("converts RSA public key", func(t *testing.T) { + pem := "-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBAMXC7V5p/LRULNuRWNBBcizjCrMPIV57LjNG6RCpkJsFtTfw5Ra+aGFJ\nmoEjlSrOsJ1aRO2krR4UTCijOrv1JNFjCvv81urSK9xSUAXzQcPdogf051ZDt1Ct\nEv4ETZQkXDMibzlbmgXq1V+oib4FXDCk0Emu6SAfOGmov/V9eShNAgMBAAE=\n-----END RSA PUBLIC KEY-----" + + pk, err := PemToPublicKey([]byte(pem)) + if !assert.NoError(t, err) { + return + } + + assert.IsType(t, &rsa.PublicKey{}, pk) + }) } func TestPemToSigner(t *testing.T) { diff --git a/go.mod b/go.mod index 066a483e19..08d9de0b13 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/deepmap/oapi-codegen v1.4.2 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/golang/mock v1.4.4 github.com/labstack/echo/v4 v4.1.17 github.com/lestrrat-go/jwx v1.0.7 From 91c82731fd65be899b03e8161f31692440a642f8 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 15 Jan 2021 14:56:38 +0100 Subject: [PATCH 27/27] one more PR thingy --- crypto/jwx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/jwx.go b/crypto/jwx.go index 92a67317b5..9d0e790cf1 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -97,7 +97,7 @@ func JWTKidAlg(tokenString string) (string, jwa.SignatureAlgorithm, error) { } if len(j.Signatures()) != 1 { - return "", "", errors.New("incorrect amount of signatures in JWT") + return "", "", errors.New("incorrect number of signatures in JWT") } sig := j.Signatures()[0]