diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 3310ad2ba9..17354201fe 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -74,6 +74,10 @@ func (m *mockAuthClient) AuthorizationEndpointEnabled() bool { return true } +func (m *mockAuthClient) OpenID4VCIEnabled() bool { + return true +} + func (m *mockAuthClient) AuthzServer() oauth.AuthorizationServer { return m.authzServer } diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index c3affbcf97..1ad04fd1eb 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -29,7 +29,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/nuts-foundation/nuts-node/core/to" "html/template" "net/http" "net/url" @@ -37,6 +36,8 @@ import ( "strings" "time" + "github.com/nuts-foundation/nuts-node/core/to" + "github.com/labstack/echo/v4" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" @@ -252,8 +253,9 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ } func (r Wrapper) Callback(ctx context.Context, request CallbackRequestObject) (CallbackResponseObject, error) { - if !r.auth.AuthorizationEndpointEnabled() { - // Callback endpoint is only used by flows initiated through the authorization endpoint. + // Callback endpoint is only used by flows initiated through the authorization endpoint. + // It's used by both OpenID4VP and OpenID4VCI flows + if !r.auth.OpenID4VPEnabled() && !r.auth.OpenID4VCIEnabled() { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, Description: "callback endpoint is disabled", @@ -297,8 +299,14 @@ func (r Wrapper) Callback(ctx context.Context, request CallbackRequestObject) (C // continue flow switch oauthSession.ClientFlow { case credentialRequestClientFlow: + if !r.auth.OpenID4VCIEnabled() { + return nil, withCallbackURI(oauthError(oauth.InvalidRequest, "openid4vci is disabled"), oauthSession.redirectURI()) + } return r.handleOpenID4VCICallback(ctx, *request.Params.Code, oauthSession) case accessTokenRequestClientFlow: + if !r.auth.OpenID4VPEnabled() { + return nil, withCallbackURI(oauthError(oauth.InvalidRequest, "openid4vp is disabled"), oauthSession.redirectURI()) + } return r.handleCallback(ctx, *request.Params.Code, oauthSession) default: // programming error, should never happen @@ -444,7 +452,8 @@ func (r Wrapper) introspectAccessToken(input string) (*ExtendedTokenIntrospectio // HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow. func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) { - if !r.auth.AuthorizationEndpointEnabled() { + // Check if either OpenID4VP or OpenID4VCI authorization endpoint is enabled + if !r.auth.OpenID4VPEnabled() && !r.auth.OpenID4VCIEnabled() { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, Description: "authorization endpoint is disabled", @@ -482,6 +491,18 @@ func (r Wrapper) handleAuthorizeRequest(ctx context.Context, subject string, own // - Regular authorization code flow for EHR data access through access token, authentication of end-user using OpenID4VP. // - OpenID4VCI; authorization code flow for credential issuance to (end-user) wallet + // Check if authorization endpoint is enabled for code flow. + // Since we can't distinguish between the two use cases at this point (see TODO below), + // we allow the request if either OpenID4VP or OpenID4VCI is enabled. + if !r.auth.AuthorizationEndpointEnabled() && !r.auth.OpenID4VCIEnabled() { + redirectURI, _ := url.Parse(requestObject.get(oauth.RedirectURIParam)) + return nil, oauth.OAuth2Error{ + Code: oauth.InvalidRequest, + Description: "authorization endpoint is disabled", + RedirectURI: redirectURI, + } + } + // TODO: officially flow switching has to be determined by the client_id // registered client_ids should list which flow they support // client registration could be done via rfc7591.... @@ -490,6 +511,16 @@ func (r Wrapper) handleAuthorizeRequest(ctx context.Context, subject string, own case oauth.VPTokenResponseType: // Options: // - OpenID4VP flow, vp_token is sent in Authorization Response + // Check if OpenID4VP authorization endpoint is enabled for vp_token flow + if !r.auth.AuthorizationEndpointEnabled() { + redirectURI, _ := url.Parse(requestObject.get(oauth.RedirectURIParam)) + return nil, oauth.OAuth2Error{ + Code: oauth.InvalidRequest, + Description: "authorization endpoint for OpenID4VP is disabled", + RedirectURI: redirectURI, + } + } + // non-spec: if the scheme is openid4vp (URL starts with openid4vp:), the OpenID4VP request should be handled by a user wallet, rather than an organization wallet. // Requests to user wallets can then be rendered as QR-code (or use a cloud wallet). // Note that it can't be called from the outside, but only by internal dispatch (since Echo doesn't handle openid4vp:, obviously). @@ -616,7 +647,8 @@ func (r Wrapper) OAuthAuthorizationServerMetadata(_ context.Context, request OAu func (r Wrapper) oauthAuthorizationServerMetadata(clientID url.URL) (*oauth.AuthorizationServerMetadata, error) { md := authorizationServerMetadata(&clientID, r.auth.SupportedDIDMethods()) - if !r.auth.AuthorizationEndpointEnabled() { + // Remove authorization endpoint from metadata if both OpenID4VP and OpenID4VCI authorization endpoints are disabled + if !r.auth.AuthorizationEndpointEnabled() && !r.auth.OpenID4VCIEnabled() { md.AuthorizationEndpoint = "" } return &md, nil diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index ed973f375f..034ece69dc 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -90,7 +90,7 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { assert.NotEmpty(t, res.(OAuthAuthorizationServerMetadata200JSONResponse).AuthorizationEndpoint) }) t.Run("authorization endpoint disabled", func(t *testing.T) { - ctx := newCustomTestClient(t, verifierURL, false) + ctx := newCustomTestClient(t, verifierURL, false, false) res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{SubjectID: verifierSubject}) @@ -101,7 +101,7 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { t.Run("base URL (prepended before /iam)", func(t *testing.T) { // 200 baseURL := test.MustParseURL("https://example.com/base") - ctx := newCustomTestClient(t, baseURL, false) + ctx := newCustomTestClient(t, baseURL, false, false) res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{SubjectID: verifierSubject}) @@ -250,13 +250,39 @@ func TestWrapper_PresentationDefinition(t *testing.T) { func TestWrapper_HandleAuthorizeRequest(t *testing.T) { t.Run("disabled", func(t *testing.T) { - ctx := newCustomTestClient(t, verifierURL, false) + ctx := newCustomTestClient(t, verifierURL, false, false) response, err := ctx.client.HandleAuthorizeRequest(nil, HandleAuthorizeRequestRequestObject{SubjectID: verifierSubject}) requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint is disabled") assert.Nil(t, response) }) + t.Run("OpenID4VCI and OpenID4VP disabled, response_type=code - should fail", func(t *testing.T) { + ctx := newCustomTestClient(t, verifierURL, false, false) // Both disabled + + // This should fail at the outer check (before parsing) since both configs are disabled + res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{"key": "test_value"}), + HandleAuthorizeRequestRequestObject{SubjectID: verifierSubject}) + + requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint is disabled") + assert.Nil(t, res) + }) + t.Run("OpenID4VP disabled, response_type=vp_token - should fail", func(t *testing.T) { + ctx := newCustomTestClient(t, verifierURL, false, true) // Only OpenID4VCI enabled + + // HandleAuthorizeRequest + requestParams := oauthParameters{ + oauth.RedirectURIParam: "https://example.com", + oauth.ResponseTypeParam: oauth.VPTokenResponseType, + } + ctx.jar.EXPECT().Parse(gomock.Any(), gomock.Any(), url.Values{"key": []string{"test_value"}}).Return(requestParams, nil) + + res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{"key": "test_value"}), + HandleAuthorizeRequestRequestObject{SubjectID: verifierSubject}) + + requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint for OpenID4VP is disabled") + assert.Nil(t, res) + }) t.Run("ok - response_type=code", func(t *testing.T) { ctx := newTestClient(t) @@ -428,7 +454,7 @@ func TestWrapper_Callback(t *testing.T) { TokenEndpoint: "https://example.com/token", } t.Run("disabled", func(t *testing.T) { - ctx := newCustomTestClient(t, verifierURL, false) + ctx := newCustomTestClient(t, verifierURL, false, false) response, err := ctx.client.Callback(nil, CallbackRequestObject{SubjectID: holderSubjectID}) @@ -1579,10 +1605,10 @@ type testCtx struct { func newTestClient(t testing.TB) *testCtx { publicURL, _ := url.Parse("https://example.com") - return newCustomTestClient(t, publicURL, true) + return newCustomTestClient(t, publicURL, true, true) } -func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled bool) *testCtx { +func newCustomTestClient(t testing.TB, publicURL *url.URL, openid4vpEnabled bool, openid4vciEnabled bool) *testCtx { ctrl := gomock.NewController(t) storageEngine := storage.NewTestStorageEngine(t) authnServices := auth.NewMockAuthenticationServices(ctrl) @@ -1607,7 +1633,8 @@ func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled b mockVCR.EXPECT().Verifier().Return(vcVerifier).AnyTimes() mockVCR.EXPECT().Wallet().Return(mockWallet).AnyTimes() authnServices.EXPECT().IAMClient().Return(iamClient).AnyTimes() - authnServices.EXPECT().AuthorizationEndpointEnabled().Return(authEndpointEnabled).AnyTimes() + authnServices.EXPECT().AuthorizationEndpointEnabled().Return(openid4vpEnabled).AnyTimes() + authnServices.EXPECT().OpenID4VCIEnabled().Return(openid4vciEnabled).AnyTimes() subjectManager.EXPECT().ListDIDs(gomock.Any(), holderSubjectID).Return([]did.DID{holderDID}, nil).AnyTimes() subjectManager.EXPECT().ListDIDs(gomock.Any(), unknownSubjectID).Return(nil, didsubject.ErrSubjectNotFound).AnyTimes() diff --git a/auth/auth.go b/auth/auth.go index f135335c01..17eb08af69 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -88,11 +88,16 @@ func (auth *Auth) PublicURL() *url.URL { return auth.publicURL } -// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled. +// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled for OpenID4VP flows. func (auth *Auth) AuthorizationEndpointEnabled() bool { return auth.config.AuthorizationEndpoint.Enabled } +// OpenID4VCIEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled for OpenID4VCI flows. +func (auth *Auth) OpenID4VCIEnabled() bool { + return auth.config.OpenID4VCI.Enabled +} + // ContractNotary returns an implementation of the ContractNotary interface. func (auth *Auth) ContractNotary() services.ContractNotary { return auth.contractNotary diff --git a/auth/cmd/cmd.go b/auth/cmd/cmd.go index 5a2abf01e5..9e7a632bde 100644 --- a/auth/cmd/cmd.go +++ b/auth/cmd/cmd.go @@ -44,9 +44,15 @@ const ConfHTTPTimeout = "auth.http.timeout" // ConfAccessTokenLifeSpan defines how long (in seconds) an access token is valid const ConfAccessTokenLifeSpan = "auth.accesstokenlifespan" -// ConfAuthEndpointEnabled is the config key for enabling the Auth v2 API's Authorization Endpoint +// ConfAuthEndpointEnabled is the config key for enabling the Auth v2 API's Authorization Endpoint for OpenID4VP flows const ConfAuthEndpointEnabled = "auth.authorizationendpoint.enabled" +// ConfOpenID4VCIEnabled is the config key for enabling OpenID4VCI. +const ConfOpenID4VCIEnabled = "auth.openid4vci.enabled" + +// ConfOpenID4VPEnabled is the config key for enabling OpenID4VP. +const ConfOpenID4VPEnabled = "auth.openid4vp.enabled" + // FlagSet returns the configuration flags supported by this module. func FlagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("auth", pflag.ContinueOnError) @@ -59,8 +65,8 @@ func FlagSet() *pflag.FlagSet { flags.Int(ConfClockSkew, defs.ClockSkew, "allowed JWT Clock skew in milliseconds") flags.Int(ConfAccessTokenLifeSpan, defs.AccessTokenLifeSpan, "defines how long (in seconds) an access token is valid. Uses default in strict mode.") flags.StringSlice(ConfContractValidators, defs.ContractValidators, "sets the different contract validators to use") - flags.Bool(ConfAuthEndpointEnabled, defs.AuthorizationEndpoint.Enabled, "enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. "+ - "This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature.") + flags.Bool(ConfOpenID4VCIEnabled, defs.OpenID4VCI.Enabled, "enables OpenID4VCI (experimental) support, allowing credential issuance be requested for local wallets through OpenID4VCI.") + flags.Bool(ConfOpenID4VPEnabled, defs.OpenID4VP.Enabled, "enables OpenID4VCI (experimental) support, allowing; authentication of clients using OpenID4VP (as verifier) and responding to OpenID4VP requests from OpenID4VP verifiers (as wallet).") _ = flags.MarkDeprecated("auth.http.timeout", "use httpclient.timeout instead") return flags diff --git a/auth/cmd/cmd_test.go b/auth/cmd/cmd_test.go index 5fe44f4c19..fc64509187 100644 --- a/auth/cmd/cmd_test.go +++ b/auth/cmd/cmd_test.go @@ -50,6 +50,7 @@ func TestFlagSet(t *testing.T) { ConfAutoUpdateIrmaSchemas, ConfIrmaCorsOrigin, ConfIrmaSchemeManager, + ConfOpenID4VCIEnabled, }, keys) } diff --git a/auth/config.go b/auth/config.go index 0f30bd3c95..648a5605f6 100644 --- a/auth/config.go +++ b/auth/config.go @@ -26,20 +26,25 @@ import ( // Config holds all the configuration params type Config struct { - Irma IrmaConfig `koanf:"irma"` - HTTPTimeout int `koanf:"http.timeout"` - ClockSkew int `koanf:"clockskew"` - ContractValidators []string `koanf:"contractvalidators"` - AccessTokenLifeSpan int `koanf:"accesstokenlifespan"` - AuthorizationEndpoint AuthorizationEndpointConfig `koanf:"authorizationendpoint"` + Irma IrmaConfig `koanf:"irma"` + HTTPTimeout int `koanf:"http.timeout"` + ClockSkew int `koanf:"clockskew"` + ContractValidators []string `koanf:"contractvalidators"` + AccessTokenLifeSpan int `koanf:"accesstokenlifespan"` + OpenID4VCI OpenID4VCIConfig `koanf:"openid4vci"` + OpenID4VP OpenID4VPConfig `koanf:"openid4vp"` } -type AuthorizationEndpointConfig struct { - // Enabled is a flag to enable or disable the v2 API's Authorization Endpoint (/authorize), used for: - // - As OpenID4VP verifier: to authenticate clients (that initiate the Authorized Code flow) using OpenID4VP - // - As OpenID4VP wallet: to authenticate verifiers using OpenID4VP - // - As OpenID4VCI wallet: to support dynamic credential requests (currently not supported) - // Disabling the authorization endpoint will also disable to callback endpoint and removes the endpoint from the metadata. +type OpenID4VPConfig struct { + // Enabled is a flag to enable or disable OpenID4VP support: + // - As OpenID4VP verifier: to authenticate clients using OpenID4VP + // - As OpenID4VP wallet: to respond to OpenID4VP requests to from OpenID4VP verifiers. + Enabled bool `koanf:"enabled"` +} + +type OpenID4VCIConfig struct { + // Enabled is a flag to enable OpenID4VCI support. + // If enabled, credential issuance can be requested for local wallets through OpenID4VCI. Enabled bool `koanf:"enabled"` } diff --git a/auth/interface.go b/auth/interface.go index 6a0cd7eecb..4767ed7a4b 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -19,10 +19,11 @@ package auth import ( + "net/url" + "github.com/nuts-foundation/nuts-node/auth/client/iam" "github.com/nuts-foundation/nuts-node/auth/services" "github.com/nuts-foundation/nuts-node/auth/services/oauth" - "net/url" ) // ModuleName contains the name of this module @@ -40,8 +41,10 @@ type AuthenticationServices interface { ContractNotary() services.ContractNotary // PublicURL returns the public URL of the node. PublicURL() *url.URL - // AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled. - AuthorizationEndpointEnabled() bool + // OpenID4VCIEnabled returns whether OpenID4VCI is enabled. + OpenID4VCIEnabled() bool + // OpenID4VPEnabled returns whether OpenID4VP is enabled. + OpenID4VPEnabled() bool // SupportedDIDMethods lists the DID methods the Nuts node can resolve. SupportedDIDMethods() []string } diff --git a/auth/mock.go b/auth/mock.go index e92db3f34a..27bf8dcde6 100644 --- a/auth/mock.go +++ b/auth/mock.go @@ -99,6 +99,20 @@ func (mr *MockAuthenticationServicesMockRecorder) IAMClient() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IAMClient", reflect.TypeOf((*MockAuthenticationServices)(nil).IAMClient)) } +// OpenID4VCIEnabled mocks base method. +func (m *MockAuthenticationServices) OpenID4VCIEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OpenID4VCIEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// OpenID4VCIEnabled indicates an expected call of OpenID4VCIEnabled. +func (mr *MockAuthenticationServicesMockRecorder) OpenID4VCIEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenID4VCIEnabled", reflect.TypeOf((*MockAuthenticationServices)(nil).OpenID4VCIEnabled)) +} + // PublicURL mocks base method. func (m *MockAuthenticationServices) PublicURL() *url.URL { m.ctrl.T.Helper() diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index a93ea21e31..bb98e665f6 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -16,7 +16,8 @@ verbosity info Log level (trace, debug, info, warn, error) httpclient.timeout 30s Request time-out for HTTP clients, such as '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax. **Auth** - auth.authorizationendpoint.enabled false enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature. + auth.authorizationendpoint.enabled false enables the v2 API's OAuth2 Authorization Endpoint for OpenID4VP flows. This flag might be removed in a future version (or its default become 'true') as the implementation of OpenID4VP matures. + auth.openid4vci.enabled false enables the v2 API's OAuth2 Authorization Endpoint for OpenID4VCI flows. This flag might be removed in a future version (or its default become 'true') as the implementation of OpenID4VCI matures. **Crypto** crypto.storage Storage to use, 'fs' for file system (for development purposes), 'vaultkv' for HashiCorp Vault KV store, 'azure-keyvault' for Azure Key Vault, 'external' for an external backend (deprecated). crypto.azurekv.hsm false Whether to store the key in a hardware security module (HSM). If true, the Azure Key Vault must be configured for HSM usage. Default: false diff --git a/vcr/pe/schema/gen/go.mod b/vcr/pe/schema/gen/go.mod index b872a8ae8e..05c918dc65 100644 --- a/vcr/pe/schema/gen/go.mod +++ b/vcr/pe/schema/gen/go.mod @@ -2,6 +2,4 @@ module github.com/nuts-foundation/nuts-node/vcr/pe/gen/schema go 1.21 -require ( - github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 // indirect -) +require github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 diff --git a/vcr/pe/schema/gen/go.sum b/vcr/pe/schema/gen/go.sum index 1f994a18a1..d5af305fbb 100644 --- a/vcr/pe/schema/gen/go.sum +++ b/vcr/pe/schema/gen/go.sum @@ -1,16 +1,2 @@ -github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= -github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= -github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= -github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= -github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= -github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= -github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= -github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 h1:XIsQOSBJi/9Bexr+rjUpuYi0IkQ+YqNKKlE7Yt/sw9Q= -github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97/go.mod h1:zTyVtYhYjcHpfCtqnCMxejgp0pEEwb/xJzhn05NrkJk= github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 h1:/rNdG6EuzjwcR1KRFpF+9qWmWh2xIcz84QOeMGr/2L8= github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60/go.mod h1:traiLYQ0YD7qUMCdjo6/jSaJRPHXniX4HVs+PhEhYpc= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=