Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
18726b1
added baseline funcs from old repo
woutslakhorst Jan 14, 2021
e19fe9e
added todo
woutslakhorst Jan 14, 2021
1885c56
some cleanup in crypto
woutslakhorst Jan 14, 2021
7d93bed
removed generateKeys from API, removed Client interface
woutslakhorst Jan 15, 2021
25ba805
renamed crypto interface to KeyStore
woutslakhorst Jan 15, 2021
9e7ce43
removed nuts-go-test dep
woutslakhorst Jan 15, 2021
1fb6380
removed obsolete jwt lib
woutslakhorst Jan 15, 2021
69abcf2
gofmt
woutslakhorst Jan 15, 2021
75d02de
cleanup
woutslakhorst Jan 15, 2021
a2c2494
cleanup
woutslakhorst Jan 15, 2021
784ae99
api test cleanup
woutslakhorst Jan 15, 2021
4f04b8b
some more coverage
woutslakhorst Jan 15, 2021
600f432
added coverage, added NamingFunc on key generation
woutslakhorst Jan 15, 2021
86989f5
removed kid fingwerprint
woutslakhorst Jan 15, 2021
11bd968
ignore mock in codeclimate
woutslakhorst Jan 15, 2021
9f3783f
cleanup
woutslakhorst Jan 15, 2021
c76cddf
codeclimate conf
woutslakhorst Jan 15, 2021
b9788f1
more cleanup
woutslakhorst Jan 15, 2021
d544c98
remove storage package comment
woutslakhorst Jan 15, 2021
6ec07c7
extra test for crypto engine
woutslakhorst Jan 15, 2021
9b6c54e
one more test
woutslakhorst Jan 15, 2021
893cf56
one more test
woutslakhorst Jan 15, 2021
8e12154
return kid from keyStore.New
woutslakhorst Jan 15, 2021
6920e63
one more test
woutslakhorst Jan 15, 2021
0a10ec3
one more test
woutslakhorst Jan 15, 2021
43be5e5
PR review
woutslakhorst Jan 15, 2021
91c8273
one more PR thingy
woutslakhorst Jan 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exclude_patterns:
- "test/*"
- "**/mock.go"
- "**/generated.go"
- "mock/*"
- "**/mock/*"

plugins:
gofmt:
Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ func Test_rootCmd(t *testing.T) {
assert.NoError(t, CreateCommand().Execute())
assert.False(t, routesCalled, "engine.Routes was called")
})
}
}
1 change: 0 additions & 1 deletion core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
const NutsVendorOID = NutsOID + ".4"
108 changes: 108 additions & 0 deletions crypto/api/v1/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

package v1

import (
"errors"
"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.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{}
err := ctx.Bind(signRequest)
if err != nil {
log.Logger().Error(err.Error())
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

if err := signRequest.validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}

sig, err := w.C.SignJWT(signRequest.Claims, signRequest.Kid)

if err != nil {
log.Logger().Error(err.Error())
Comment thread
woutslakhorst marked this conversation as resolved.
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

return ctx.String(http.StatusOK, sig)
}

// 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")

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 := jwk.New(pubKey)
if err != nil {
log.Logger().Error(err.Error())
return err
}

return ctx.JSON(http.StatusOK, jwk)
}

// backwards compatible PEM format is the default
pub, err := util.PublicKeyToPem(pubKey)
if err != nil {
log.Logger().Error(err.Error())
return err
}

return ctx.String(http.StatusOK, pub)
}
198 changes: 198 additions & 0 deletions crypto/api/v1/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

package v1

import (
"encoding/json"
"errors"
"net/http"
"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"
"github.com/stretchr/testify/assert"
)

func TestWrapper_SignJwt(t *testing.T) {
t.Run("Missing claims returns 400", func(t *testing.T) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

jsonRequest := SignJwtRequest{
Kid: "kid",
}
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 claims")
})

t.Run("Missing kid returns 400", func(t *testing.T) {
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")
})

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)

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 := ctx.client.SignJwt(ctx.echo)

assert.NotNil(t, err)
assert.Contains(t, err.Error(), "code=400, message=b00m!")
})

t.Run("All OK returns 200", func(t *testing.T) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

jsonRequest := SignJwtRequest{
Kid: "kid",
Claims: map[string]interface{}{"iss": "nuts"},
}

jsonData, _ := json.Marshal(jsonRequest)

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 := ctx.client.SignJwt(ctx.echo)

assert.Nil(t, err)
})

t.Run("Missing body gives 400", func(t *testing.T) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

ctx.echo.EXPECT().Bind(gomock.Any()).Return(errors.New("missing body in request"))

err := ctx.client.SignJwt(ctx.echo)

assert.NotNil(t, err)
assert.Contains(t, err.Error(), "code=400, message=missing body in request")
})
}

func TestWrapper_PublicKey(t *testing.T) {
t.Run("PublicKey API call returns 200", func(t *testing.T) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

key := test.GenerateECKey()

ctx.echo.EXPECT().Request().Return(&http.Request{})
ctx.keyStore.EXPECT().GetPublicKey("kid").Return(key.Public(), nil)
ctx.echo.EXPECT().String(http.StatusOK, gomock.Any())

_ = ctx.client.PublicKey(ctx.echo, "kid")
})

t.Run("PublicKey API call returns JWK", func(t *testing.T) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

key := test.GenerateECKey()

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) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

ctx.echo.EXPECT().Request().Return(&http.Request{})
ctx.keyStore.EXPECT().GetPublicKey("kid").Return(nil, storage.ErrNotFound)
ctx.echo.EXPECT().NoContent(http.StatusNotFound)

_ = ctx.client.PublicKey(ctx.echo, "kid")
})

t.Run("PublicKey API call returns 500 for other error", func(t *testing.T) {
ctx := newMockContext(t)
defer ctx.ctrl.Finish()

ctx.echo.EXPECT().Request().Return(&http.Request{Header: http.Header{"Accept": []string{"application/json"}}})
ctx.keyStore.EXPECT().GetPublicKey("kid").Return(nil, errors.New("b00m!"))

err := ctx.client.PublicKey(ctx.echo, "kid")
assert.Error(t, err)
})
}

type mockContext struct {
ctrl *gomock.Controller
echo *mock.MockContext
keyStore *mock2.MockKeyStore
client *Wrapper
}

func newMockContext(t *testing.T) mockContext {
ctrl := gomock.NewController(t)
keyStore := mock2.NewMockKeyStore(ctrl)
client := &Wrapper{C: keyStore}

return mockContext{
ctrl: ctrl,
echo: mock.NewMockContext(ctrl),
keyStore: keyStore,
client: client,
}
}
Loading