From 2c77c5df21f49195aa015fbd8b957d8ab0598714 Mon Sep 17 00:00:00 2001 From: Filip Burlacu Date: Wed, 18 May 2022 10:50:23 -0400 Subject: [PATCH] feat: httpsig GNAP request proof algorithm Signed-off-by: Filip Burlacu --- cmd/auth-rest/go.mod | 1 + cmd/auth-rest/go.sum | 1 + component/gnap/as/client.go | 67 +++--- component/gnap/as/client_test.go | 114 +++++++--- component/gnap/rs/client.go | 33 ++- component/gnap/rs/client_test.go | 64 ++++-- go.mod | 1 + go.sum | 1 + pkg/restapi/gnap/operations.go | 49 ++++- pkg/restapi/gnap/operations_test.go | 118 +++++++++-- spi/gnap/api.go | 12 +- spi/gnap/clientverifier/httpsig/verifier.go | 29 --- spi/gnap/go.mod | 3 +- spi/gnap/go.sum | 6 + spi/gnap/internal/digest/digest.go | 42 ++++ spi/gnap/internal/jwksignature/ecdsa.go | 114 ++++++++++ spi/gnap/internal/jwksignature/ecdsa_test.go | 133 ++++++++++++ .../internal/jwksignature/jwksignature.go | 67 ++++++ .../jwksignature/jwksignature_test.go | 196 ++++++++++++++++++ spi/gnap/proof/httpsig/sign_verify_test.go | 62 ++++++ spi/gnap/proof/httpsig/signer.go | 88 ++++++++ spi/gnap/proof/httpsig/signer_test.go | 155 ++++++++++++++ spi/gnap/proof/httpsig/verifier.go | 115 ++++++++++ spi/gnap/proof/httpsig/verifier_test.go | 173 ++++++++++++++++ test/bdd/go.mod | 1 + test/bdd/go.sum | 2 +- test/bdd/pkg/gnap/signer.go | 16 -- test/bdd/pkg/gnap/steps.go | 116 +++++++---- 28 files changed, 1578 insertions(+), 201 deletions(-) delete mode 100644 spi/gnap/clientverifier/httpsig/verifier.go create mode 100644 spi/gnap/internal/digest/digest.go create mode 100644 spi/gnap/internal/jwksignature/ecdsa.go create mode 100644 spi/gnap/internal/jwksignature/ecdsa_test.go create mode 100644 spi/gnap/internal/jwksignature/jwksignature.go create mode 100644 spi/gnap/internal/jwksignature/jwksignature_test.go create mode 100644 spi/gnap/proof/httpsig/sign_verify_test.go create mode 100644 spi/gnap/proof/httpsig/signer.go create mode 100644 spi/gnap/proof/httpsig/signer_test.go create mode 100644 spi/gnap/proof/httpsig/verifier.go create mode 100644 spi/gnap/proof/httpsig/verifier_test.go delete mode 100644 test/bdd/pkg/gnap/signer.go diff --git a/cmd/auth-rest/go.mod b/cmd/auth-rest/go.mod index fe68521..3f6e232 100644 --- a/cmd/auth-rest/go.mod +++ b/cmd/auth-rest/go.mod @@ -56,6 +56,7 @@ require ( github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect github.com/hyperledger/aries-framework-go v0.1.8 // indirect + github.com/igor-pavlenko/httpsignatures-go v0.0.23 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/cmd/auth-rest/go.sum b/cmd/auth-rest/go.sum index ae0b4aa..81dae53 100644 --- a/cmd/auth-rest/go.sum +++ b/cmd/auth-rest/go.sum @@ -539,6 +539,7 @@ github.com/hyperledger/aries-framework-go/test/component v0.0.0-20220330140627-0 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/igor-pavlenko/httpsignatures-go v0.0.23 h1:b+bo2vox5fwHKGiGfnu9/L5gM6RwSBdKQMPi48scAj4= github.com/igor-pavlenko/httpsignatures-go v0.0.23/go.mod h1:3LVsCi3evlfQSNDKMTg3uElxEP8SjK3/Q5N9I8GU9W0= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= diff --git a/component/gnap/as/client.go b/component/gnap/as/client.go index dc24b63..f625059 100644 --- a/component/gnap/as/client.go +++ b/component/gnap/as/client.go @@ -10,7 +10,6 @@ package as import ( "bytes" - "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -38,15 +37,15 @@ type Client struct { // and a base URL of the authorization server. func NewClient(signer gnap.Signer, httpClient *http.Client, gnapAuthServerURL string) (*Client, error) { if signer == nil { - return nil, fmt.Errorf("gnap auth client: missing signer") + return nil, fmt.Errorf("missing signer") } if httpClient == nil { - return nil, fmt.Errorf("gnap auth client: missing http client") + return nil, fmt.Errorf("missing http client") } if gnapAuthServerURL == "" { - return nil, fmt.Errorf("gnap auth client: missing Authorization Server URL") + return nil, fmt.Errorf("missing Authorization Server URL") } return &Client{ @@ -58,21 +57,18 @@ func NewClient(signer gnap.Signer, httpClient *http.Client, gnapAuthServerURL st // RequestAccess creates a GNAP grant access req then submit it to the server to receive a response with an // interact_ref value. -func (c *Client) RequestAccess(req *gnap.AuthRequest) (*gnap.AuthResponse, error) { +func (c *Client) RequestAccess(req *gnap.AuthRequest) (*gnap.AuthResponse, error) { // nolint:gocyclo if req == nil { - return nil, fmt.Errorf("requestAccess: empty request") + return nil, fmt.Errorf("empty request") } - mReq, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("requestAccess: marshal access token error: %w", err) + if req.Client != nil && !req.Client.IsReference && req.Client.Key != nil { + req.Client.Key.Proof = c.signer.ProofType() } - var sig []byte - - sig, err = c.signer.Sign(mReq) + mReq, err := json.Marshal(req) if err != nil { - return nil, fmt.Errorf("requestAccess: signature error: %w", err) + return nil, fmt.Errorf("marshal access token error: %w", err) } requestReader := bytes.NewReader(mReq) @@ -81,16 +77,19 @@ func (c *Client) RequestAccess(req *gnap.AuthRequest) (*gnap.AuthResponse, error httpReq, err := http.NewRequest(http.MethodPost, url, requestReader) // nolint:noctx if err != nil { - return nil, fmt.Errorf("requestAccess: failed to build http request: %w", err) + return nil, fmt.Errorf("failed to build http request: %w", err) } httpReq.Header.Add("Content-Type", contentType) - // httpReq.Header.Add("Signature-Input", "TODO") // TODO update signature input - httpReq.Header.Add("Signature", base64.URLEncoding.EncodeToString(sig)) + + httpReq, err = c.signer.Sign(httpReq, mReq) + if err != nil { + return nil, fmt.Errorf("signature error: %w", err) + } r, err := c.httpClient.Do(httpReq) if err != nil { - return nil, fmt.Errorf("requestAccess: failed to post HTTP request to [%s]: %w", gnaprest.AuthRequestPath, err) + return nil, fmt.Errorf("failed to post HTTP request to [%s]: %w", gnaprest.AuthRequestPath, err) } defer func() { @@ -101,20 +100,20 @@ func (c *Client) RequestAccess(req *gnap.AuthRequest) (*gnap.AuthResponse, error }() if r.StatusCode != http.StatusOK { - return nil, fmt.Errorf("requestAccess: Auth server replied with invalid Status [%s]: %v", + return nil, fmt.Errorf("auth server replied with invalid status [%s]: %v", gnaprest.AuthRequestPath, r.Status) } respBody, err := ioutil.ReadAll(r.Body) if err != nil { - return nil, fmt.Errorf("requestAccess: read response failed [%s]: %w", gnaprest.AuthRequestPath, err) + return nil, fmt.Errorf("read response failed [%s]: %w", gnaprest.AuthRequestPath, err) } gnapResp := &gnap.AuthResponse{} err = json.Unmarshal(respBody, gnapResp) if err != nil { - return nil, fmt.Errorf("requestAccess: read response not properly formatted [%s, %w]", + return nil, fmt.Errorf("read response not properly formatted [%s, %w]", gnaprest.AuthRequestPath, err) } @@ -124,19 +123,12 @@ func (c *Client) RequestAccess(req *gnap.AuthRequest) (*gnap.AuthResponse, error // Continue gnap auth request containing interact_ref. func (c *Client) Continue(req *gnap.ContinueRequest, token string) (*gnap.AuthResponse, error) { if req == nil { - return nil, fmt.Errorf("continue: empty request") + return nil, fmt.Errorf("empty request") } mReq, err := json.Marshal(req) if err != nil { - return nil, fmt.Errorf("continue: marshal access token error: %w", err) - } - - var sig []byte - - sig, err = c.signer.Sign(mReq) - if err != nil { - return nil, fmt.Errorf("continue: signature error: %w", err) + return nil, fmt.Errorf("marshal access token error: %w", err) } requestReader := bytes.NewReader(mReq) @@ -144,17 +136,20 @@ func (c *Client) Continue(req *gnap.ContinueRequest, token string) (*gnap.AuthRe //nolint:noctx // TODO add context if needed. httpReq, err := http.NewRequest(http.MethodPost, c.gnapAuthServerURL+gnaprest.AuthContinuePath, requestReader) if err != nil { - return nil, fmt.Errorf("continue: failed to build http request: %w", err) + return nil, fmt.Errorf("failed to build http request: %w", err) } httpReq.Header.Add("Content-Type", contentType) - // httpReq.Header.Add("Signature-Input", "TODO") // TODO update signature input - httpReq.Header.Add("Signature", base64.URLEncoding.EncodeToString(sig)) httpReq.Header.Add("Authorization", "GNAP "+token) + httpReq, err = c.signer.Sign(httpReq, mReq) + if err != nil { + return nil, fmt.Errorf("signature error: %w", err) + } + r, err := c.httpClient.Do(httpReq) if err != nil { - return nil, fmt.Errorf("continue: failed to post HTTP request to [%s]: %w", gnaprest.AuthContinuePath, err) + return nil, fmt.Errorf("failed to post HTTP request to [%s]: %w", gnaprest.AuthContinuePath, err) } defer func() { @@ -165,20 +160,20 @@ func (c *Client) Continue(req *gnap.ContinueRequest, token string) (*gnap.AuthRe }() if r.StatusCode != http.StatusOK { - return nil, fmt.Errorf("continue: Auth server replied with invalid Status [%s]: %v", + return nil, fmt.Errorf("auth server replied with invalid status [%s]: %v", gnaprest.AuthContinuePath, r.Status) } respBody, err := ioutil.ReadAll(r.Body) if err != nil { - return nil, fmt.Errorf("continue: read response failed [%s, %w]", gnaprest.AuthContinuePath, err) + return nil, fmt.Errorf("read response failed [%s, %w]", gnaprest.AuthContinuePath, err) } gnapResp := &gnap.AuthResponse{} err = json.Unmarshal(respBody, gnapResp) if err != nil { - return nil, fmt.Errorf("continue: read response not properly formatted [%s, %w]", + return nil, fmt.Errorf("read response not properly formatted [%s, %w]", gnaprest.AuthContinuePath, err) } diff --git a/component/gnap/as/client_test.go b/component/gnap/as/client_test.go index bac358f..ccb0d96 100644 --- a/component/gnap/as/client_test.go +++ b/component/gnap/as/client_test.go @@ -7,6 +7,9 @@ SPDX-License-Identifier: Apache-2.0 package as import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/tls" "crypto/x509" "encoding/json" @@ -22,6 +25,8 @@ import ( "time" "github.com/google/uuid" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" gnaprest "github.com/trustbloc/auth/pkg/restapi/gnap" @@ -35,15 +40,15 @@ const ( func TestGNAPAuthClient(t *testing.T) { c, err := NewClient(nil, nil, "") - require.EqualError(t, err, "gnap auth client: missing signer") + require.EqualError(t, err, "missing signer") require.Empty(t, c) c, err = NewClient(&mockSigner{}, nil, "") - require.EqualError(t, err, "gnap auth client: missing http client") + require.EqualError(t, err, "missing http client") require.Empty(t, c) c, err = NewClient(&mockSigner{}, &http.Client{}, "") - require.EqualError(t, err, "gnap auth client: missing Authorization Server URL") + require.EqualError(t, err, "missing Authorization Server URL") require.Empty(t, c) c, err = NewClient(&mockSigner{}, &http.Client{}, "https://auth/server/url") @@ -55,6 +60,7 @@ func TestRequestAccess(t *testing.T) { tests := []struct { name string signer gnap.Signer + privKey *jwk.JWK tokenVal string grantReq *gnap.AuthRequest grantResp *gnap.AuthResponse @@ -63,74 +69,83 @@ func TestRequestAccess(t *testing.T) { { name: "success requesting gnap access", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: &gnap.AuthRequest{ AccessToken: []*gnap.TokenRequest{}, - Client: &gnap.RequestClient{}, - Interact: &gnap.RequestInteract{}, + Client: &gnap.RequestClient{ + Key: clientKey(t), + }, + Interact: &gnap.RequestInteract{}, }, grantResp: &gnap.AuthResponse{AccessToken: []gnap.AccessToken{{Value: "test Success Value"}}}, }, { name: "error requesting gnap access with invalid server URL", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: &gnap.AuthRequest{ AccessToken: []*gnap.TokenRequest{}, Client: &gnap.RequestClient{}, Interact: &gnap.RequestInteract{}, }, - errMsg: "requestAccess: failed to build http request: parse \"\\u007fbad url/gnap/auth\": net/url: " + + errMsg: "failed to build http request: parse \"\\u007fbad url/gnap/auth\": net/url: " + "invalid control character in URL", }, { name: "error requesting gnap access with empty request", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: nil, - errMsg: "requestAccess: empty request", + errMsg: "empty request", }, { name: "error requesting gnap access with invalid signer", signer: &mockSigner{SignatureErr: fmt.Errorf("signing error")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: &gnap.AuthRequest{ AccessToken: []*gnap.TokenRequest{}, Client: &gnap.RequestClient{}, Interact: &gnap.RequestInteract{}, }, - errMsg: "requestAccess: signature error: signing error", + errMsg: "signature error: signing error", }, { - name: "error requesting gnap access with http server returning 501 error", - signer: &mockSigner{SignatureVal: []byte("signature")}, + name: "error requesting gnap access with http server returning 501 error", + signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.AuthRequest{ AccessToken: []*gnap.TokenRequest{}, Client: &gnap.RequestClient{}, Interact: &gnap.RequestInteract{}, }, - errMsg: "requestAccess: Auth server replied with invalid Status [/gnap/auth]: 501 Not Implemented", + errMsg: "auth server replied with invalid status [/gnap/auth]: 501 Not Implemented", }, { - name: "error requesting gnap access with bad http client error", - signer: &mockSigner{SignatureVal: []byte("signature")}, + name: "error requesting gnap access with bad http client error", + signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.AuthRequest{ AccessToken: []*gnap.TokenRequest{}, Client: &gnap.RequestClient{}, Interact: &gnap.RequestInteract{}, }, - errMsg: "requestAccess: failed to post HTTP request to [/gnap/auth]: Post \"%s\": x509:" + + errMsg: "failed to post HTTP request to [/gnap/auth]: Post \"%s\": x509:" + " certificate signed by unknown authority", }, { - name: "error requesting gnap access with bad response unmarshall", - signer: &mockSigner{SignatureVal: []byte("signature")}, + name: "error requesting gnap access with bad response unmarshall", + signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.AuthRequest{ AccessToken: []*gnap.TokenRequest{}, Client: &gnap.RequestClient{}, Interact: &gnap.RequestInteract{}, }, - errMsg: "requestAccess: read response not properly formatted [/gnap/auth, unexpected end of JSON input]", + errMsg: "read response not properly formatted [/gnap/auth, unexpected end of JSON input]", grantResp: &gnap.AuthResponse{ InstanceID: "mocking empty response", }, @@ -187,14 +202,17 @@ func TestContinue(t *testing.T) { tests := []struct { name string signer gnap.Signer + privKey *jwk.JWK tokenVal string grantReq *gnap.ContinueRequest + client *gnap.RequestClient grantResp *gnap.AuthResponse errMsg string }{ { name: "success continuing gnap access", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: &gnap.ContinueRequest{ InteractRef: "", @@ -204,45 +222,51 @@ func TestContinue(t *testing.T) { { name: "error continuing gnap access with invalid server URL", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: &gnap.ContinueRequest{ InteractRef: "", }, - errMsg: "continue: failed to build http request: parse \"\\u007fbad url/gnap/continue\": net/url: " + + errMsg: "failed to build http request: parse \"\\u007fbad url/gnap/continue\": net/url: " + "invalid control character in URL", }, { name: "error continuing gnap access with empty request", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: nil, - errMsg: "continue: empty request", + errMsg: "empty request", }, { name: "error requesting gnap access with invalid signer", signer: &mockSigner{SignatureErr: fmt.Errorf("signing error")}, + privKey: privKey(t), tokenVal: "test Success Value", grantReq: &gnap.ContinueRequest{InteractRef: ""}, - errMsg: "continue: signature error: signing error", + errMsg: "signature error: signing error", }, { name: "error continuing gnap access with http server returning 501 error", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.ContinueRequest{InteractRef: ""}, - errMsg: "continue: Auth server replied with invalid Status [/gnap/continue]: 501 Not Implemented", + errMsg: "auth server replied with invalid status [/gnap/continue]: 501 Not Implemented", }, { name: "error continuing gnap access with bad http client error", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.ContinueRequest{InteractRef: ""}, - errMsg: "continue: failed to post HTTP request to [/gnap/continue]: Post \"%s\": x509:" + + errMsg: "failed to post HTTP request to [/gnap/continue]: Post \"%s\": x509:" + " certificate signed by unknown authority", }, { name: "error continuing gnap access with bad response unmarshall", signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.ContinueRequest{InteractRef: ""}, - errMsg: "continue: read response not properly formatted [/gnap/continue, unexpected end of JSON input]", + errMsg: "read response not properly formatted [/gnap/continue, unexpected end of JSON input]", grantResp: &gnap.AuthResponse{ InstanceID: "mocking empty response", }, @@ -544,6 +568,46 @@ type mockSigner struct { SignatureErr error } -func (s *mockSigner) Sign(_ []byte) ([]byte, error) { - return s.SignatureVal, s.SignatureErr +func (s *mockSigner) ProofType() string { + return "mock" +} + +func (s *mockSigner) Sign(request *http.Request, requestBody []byte) (*http.Request, error) { + return request, s.SignatureErr +} + +func privKey(t *testing.T) *jwk.JWK { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + return &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } +} + +func clientKey(t *testing.T) *gnap.ClientKey { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + return &gnap.ClientKey{ + JWK: jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + }, + } } diff --git a/component/gnap/rs/client.go b/component/gnap/rs/client.go index 558c3a2..8224d62 100644 --- a/component/gnap/rs/client.go +++ b/component/gnap/rs/client.go @@ -10,7 +10,6 @@ package rs import ( "bytes" - "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -57,21 +56,18 @@ func NewClient(signer gnap.Signer, httpClient *http.Client, gnapResourceServerUR } // Introspect verifies a GNAP auth grant request. -func (c *Client) Introspect(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { +func (c *Client) Introspect(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { // nolint:gocyclo if req == nil { - return nil, fmt.Errorf("introspect: empty request") + return nil, fmt.Errorf("empty request") } - mReq, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("introspect: signature error: %w", err) + if req.ResourceServer != nil && !req.ResourceServer.IsReference && req.ResourceServer.Key != nil { + req.ResourceServer.Key.Proof = c.signer.ProofType() } - var sig []byte - - sig, err = c.signer.Sign(mReq) + mReq, err := json.Marshal(req) if err != nil { - return nil, fmt.Errorf("introspect: signature error: %w", err) + return nil, fmt.Errorf("marshal error: %w", err) } requestReader := bytes.NewReader(mReq) @@ -79,16 +75,19 @@ func (c *Client) Introspect(req *gnap.IntrospectRequest) (*gnap.IntrospectRespon //nolint:noctx // TODO add context if needed. httpReq, err := http.NewRequest(http.MethodPost, c.gnapResourceServerURL+gnaprest.AuthIntrospectPath, requestReader) if err != nil { - return nil, fmt.Errorf("introspect: failed to build http request: %w", err) + return nil, fmt.Errorf("failed to build http request: %w", err) } httpReq.Header.Add("Content-Type", contentType) - // httpReq.Header.Add("Signature-Input", "TODO") // TODO update signature input - httpReq.Header.Add("Signature", base64.URLEncoding.EncodeToString(sig)) + + httpReq, err = c.signer.Sign(httpReq, mReq) + if err != nil { + return nil, fmt.Errorf("signature error: %w", err) + } r, err := c.httpClient.Do(httpReq) if err != nil { - return nil, fmt.Errorf("introspect: failed to post HTTP request to [%s]: %w", gnaprest.AuthIntrospectPath, + return nil, fmt.Errorf("failed to post HTTP request to [%s]: %w", gnaprest.AuthIntrospectPath, err) } @@ -100,20 +99,20 @@ func (c *Client) Introspect(req *gnap.IntrospectRequest) (*gnap.IntrospectRespon }() if r.StatusCode != http.StatusOK { - return nil, fmt.Errorf("introspect: Resource server replied with invalid Status [%s]: %v", + return nil, fmt.Errorf("auth server replied with invalid status [%s]: %v", gnaprest.AuthIntrospectPath, r.Status) } respBody, err := ioutil.ReadAll(r.Body) if err != nil { - return nil, fmt.Errorf("introspect: read response failed [%s, %w]", gnaprest.AuthIntrospectPath, err) + return nil, fmt.Errorf("read response failed [%s, %w]", gnaprest.AuthIntrospectPath, err) } gnapResp := &gnap.IntrospectResponse{} err = json.Unmarshal(respBody, gnapResp) if err != nil { - return nil, fmt.Errorf("introspect: read response not properly formatted [%s, %w]", + return nil, fmt.Errorf("read response not properly formatted [%s, %w]", gnaprest.AuthIntrospectPath, err) } diff --git a/component/gnap/rs/client_test.go b/component/gnap/rs/client_test.go index 16359c0..b97a6e2 100644 --- a/component/gnap/rs/client_test.go +++ b/component/gnap/rs/client_test.go @@ -7,7 +7,9 @@ SPDX-License-Identifier: Apache-2.0 package rs import ( + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" @@ -23,7 +25,9 @@ import ( "testing" "time" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" + "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" gnaprest "github.com/trustbloc/auth/pkg/restapi/gnap" @@ -58,6 +62,7 @@ func TestRequestAccess(t *testing.T) { name string signer gnap.Signer tokenRef string + privKey *jwk.JWK grantReq *gnap.IntrospectRequest grantResp *gnap.IntrospectResponse errMsg string @@ -66,8 +71,12 @@ func TestRequestAccess(t *testing.T) { name: "success gnap introspecting access", signer: &mockSigner{SignatureVal: []byte("signature")}, tokenRef: "test Success Value", + privKey: privKey(t), grantReq: &gnap.IntrospectRequest{ AccessToken: "", + ResourceServer: &gnap.RequestClient{ + Key: clientKey(t), + }, }, grantResp: &gnap.IntrospectResponse{ Active: true, @@ -85,64 +94,70 @@ func TestRequestAccess(t *testing.T) { name: "error gnap introspecting access with invalid server URL", signer: &mockSigner{SignatureVal: []byte("signature")}, tokenRef: "test Success Value", + privKey: privKey(t), grantReq: &gnap.IntrospectRequest{ AccessToken: "", }, - errMsg: "introspect: failed to build http request: parse \"\\u007fbad url/gnap/introspect\": net/url: " + + errMsg: "failed to build http request: parse \"\\u007fbad url/gnap/introspect\": net/url: " + "invalid control character in URL", }, { name: "error gnap introspecting with empty request", signer: &mockSigner{SignatureVal: []byte("signature")}, tokenRef: "test Success Value", + privKey: privKey(t), grantReq: nil, - errMsg: "introspect: empty request", + errMsg: "empty request", }, { name: "error gnap introspecting with invalid signer", signer: &mockSigner{SignatureErr: fmt.Errorf("signing error")}, tokenRef: "test Success Value", + privKey: privKey(t), grantReq: &gnap.IntrospectRequest{ AccessToken: "", Proof: "", Access: nil, ResourceServer: nil, }, - errMsg: "introspect: signature error: signing error", + errMsg: "signature error: signing error", }, { - name: "error gnap introspecting with http server returning 501 error", - signer: &mockSigner{SignatureVal: []byte("signature")}, + name: "error gnap introspecting with http server returning 501 error", + signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.IntrospectRequest{ AccessToken: "", Proof: "", Access: nil, ResourceServer: nil, }, - errMsg: "introspect: Resource server replied with invalid Status [/gnap/introspect]: 501 Not Implemented", + errMsg: "auth server replied with invalid status [/gnap/introspect]: 501 Not Implemented", }, { - name: "error gnap introspecting access with bad http client error", - signer: &mockSigner{SignatureVal: []byte("signature")}, + name: "error gnap introspecting access with bad http client error", + signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.IntrospectRequest{ AccessToken: "", Proof: "", Access: nil, ResourceServer: nil, }, - errMsg: "introspect: failed to post HTTP request to [/gnap/introspect]: Post \"%s\": x509:" + + errMsg: "failed to post HTTP request to [/gnap/introspect]: Post \"%s\": x509:" + " certificate signed by unknown authority", }, { - name: "error gnap introspecting with bad response unmarshall", - signer: &mockSigner{SignatureVal: []byte("signature")}, + name: "error gnap introspecting with bad response unmarshall", + signer: &mockSigner{SignatureVal: []byte("signature")}, + privKey: privKey(t), grantReq: &gnap.IntrospectRequest{ AccessToken: "", Proof: "", Access: nil, ResourceServer: nil, }, - errMsg: "introspect: read response not properly formatted [/gnap/introspect, unexpected end of JSON input]", + errMsg: "read response not properly formatted [/gnap/introspect, unexpected end of JSON input]", grantResp: &gnap.IntrospectResponse{ Active: false, Access: nil, @@ -399,8 +414,12 @@ type mockSigner struct { SignatureErr error } -func (s *mockSigner) Sign(_ []byte) ([]byte, error) { - return s.SignatureVal, s.SignatureErr +func (s *mockSigner) ProofType() string { + return "mock" +} + +func (s *mockSigner) Sign(request *http.Request, requestBody []byte) (*http.Request, error) { + return request, s.SignatureErr } func clientKey(t *testing.T) *gnap.ClientKey { @@ -419,3 +438,20 @@ func clientKey(t *testing.T) *gnap.ClientKey { return &ck } + +func privKey(t *testing.T) *jwk.JWK { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + return &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } +} diff --git a/go.mod b/go.mod index 4be727d..088e211 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/gorilla/securecookie v1.1.1 // indirect + github.com/igor-pavlenko/httpsignatures-go v0.0.23 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kilic/bls12-381 v0.1.1-0.20210503002446-7b7597926c69 // indirect github.com/mailru/easyjson v0.7.6 // indirect diff --git a/go.sum b/go.sum index 6eac072..fe3ba8a 100644 --- a/go.sum +++ b/go.sum @@ -476,6 +476,7 @@ github.com/hyperledger/aries-framework-go/test/component v0.0.0-20220322085443-5 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/igor-pavlenko/httpsignatures-go v0.0.23 h1:b+bo2vox5fwHKGiGfnu9/L5gM6RwSBdKQMPi48scAj4= github.com/igor-pavlenko/httpsignatures-go v0.0.23/go.mod h1:3LVsCi3evlfQSNDKMTg3uElxEP8SjK3/Q5N9I8GU9W0= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= diff --git a/pkg/restapi/gnap/operations.go b/pkg/restapi/gnap/operations.go index f6b01b7..2a9d2df 100644 --- a/pkg/restapi/gnap/operations.go +++ b/pkg/restapi/gnap/operations.go @@ -7,10 +7,12 @@ SPDX-License-Identifier: Apache-2.0 package gnap import ( + "bytes" "context" "crypto/tls" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "strings" @@ -31,7 +33,7 @@ import ( "github.com/trustbloc/auth/pkg/restapi/common" oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" "github.com/trustbloc/auth/spi/gnap" - "github.com/trustbloc/auth/spi/gnap/clientverifier/httpsig" + "github.com/trustbloc/auth/spi/gnap/proof/httpsig" ) var logger = log.New("auth-restapi") //nolint:gochecknoglobals @@ -155,7 +157,20 @@ func (o *Operation) GetRESTHandlers() []common.Handler { func (o *Operation) authRequestHandler(w http.ResponseWriter, req *http.Request) { // nolint: dupl authRequest := &gnap.AuthRequest{} - if err := json.NewDecoder(req.Body).Decode(authRequest); err != nil { + bodyBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + logger.Errorf("error reading request body: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) + + return + } + + req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) + + if err = json.Unmarshal(bodyBytes, authRequest); err != nil { logger.Errorf("failed to parse gnap auth request: %s", err.Error()) w.WriteHeader(http.StatusBadRequest) o.writeResponse(w, &gnap.ErrorResponse{ @@ -391,7 +406,20 @@ func (o *Operation) authContinueHandler(w http.ResponseWriter, req *http.Request continueRequest := &gnap.ContinueRequest{} - if err := json.NewDecoder(req.Body).Decode(continueRequest); err != nil { + bodyBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + logger.Errorf("error reading request body: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) + + return + } + + req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) + + if err = json.Unmarshal(bodyBytes, continueRequest); err != nil { logger.Errorf("failed to parse gnap continue request: %s", err.Error()) w.WriteHeader(http.StatusBadRequest) o.writeResponse(w, &gnap.ErrorResponse{ @@ -420,7 +448,20 @@ func (o *Operation) authContinueHandler(w http.ResponseWriter, req *http.Request func (o *Operation) introspectHandler(w http.ResponseWriter, req *http.Request) { // nolint: dupl introspectRequest := &gnap.IntrospectRequest{} - if err := json.NewDecoder(req.Body).Decode(introspectRequest); err != nil { + bodyBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + logger.Errorf("error reading request body: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) + + return + } + + req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) + + if err = json.Unmarshal(bodyBytes, introspectRequest); err != nil { logger.Errorf("failed to parse gnap introspection request: %s", err.Error()) w.WriteHeader(http.StatusBadRequest) o.writeResponse(w, &gnap.ErrorResponse{ diff --git a/pkg/restapi/gnap/operations_test.go b/pkg/restapi/gnap/operations_test.go index 4a2f084..99e36fa 100644 --- a/pkg/restapi/gnap/operations_test.go +++ b/pkg/restapi/gnap/operations_test.go @@ -9,7 +9,8 @@ package gnap import ( "bytes" "context" - "crypto/ed25519" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "encoding/json" "errors" @@ -21,8 +22,9 @@ import ( "github.com/google/uuid" "github.com/hyperledger/aries-framework-go/component/storageutil/mem" - "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" mockstore "github.com/hyperledger/aries-framework-go/pkg/mock/storage" + "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -34,6 +36,7 @@ import ( "github.com/trustbloc/auth/pkg/internal/common/mockstorage" oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" "github.com/trustbloc/auth/spi/gnap" + "github.com/trustbloc/auth/spi/gnap/proof/httpsig" ) func TestNew(t *testing.T) { @@ -92,6 +95,20 @@ func TestOperation_AuthProvidersHandler(t *testing.T) { } func TestOperation_authRequestHandler(t *testing.T) { + t.Run("fail to read body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, &errorReader{err: expectErr}) + + o.authRequestHandler(rw, req) + + require.Equal(t, http.StatusInternalServerError, rw.Code) + }) + t.Run("fail to parse empty request body", func(t *testing.T) { o := &Operation{} @@ -120,10 +137,12 @@ func TestOperation_authRequestHandler(t *testing.T) { o, err := New(config(t)) require.NoError(t, err) + priv, client := clientKey(t) + authReq := &gnap.AuthRequest{ Client: &gnap.RequestClient{ IsReference: false, - Key: clientKey(t), + Key: client, }, } @@ -134,6 +153,9 @@ func TestOperation_authRequestHandler(t *testing.T) { req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader(authReqBytes)) + req, err = httpsig.Sign(req, authReqBytes, priv, "sha-256") + require.NoError(t, err) + o.authRequestHandler(rw, req) require.Equal(t, http.StatusOK, rw.Code) @@ -188,6 +210,25 @@ func TestOperation_authContinueHandler(t *testing.T) { require.Equal(t, errRequestDenied, resp.Error) }) + t.Run("fail to read request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, AuthContinuePath, &errorReader{err: expectErr}) + req.Header.Add("Authorization", "GNAP mock-token") + + o.authContinueHandler(rw, req) + + require.Equal(t, http.StatusInternalServerError, rw.Code) + + resp := &gnap.ErrorResponse{} + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) + require.Equal(t, errRequestDenied, resp.Error) + }) + t.Run("fail to parse empty request body", func(t *testing.T) { o := &Operation{} @@ -225,6 +266,20 @@ func TestOperation_authContinueHandler(t *testing.T) { } func TestOperation_introspectHandler(t *testing.T) { + t.Run("fail to read request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, &errorReader{err: expectErr}) + + o.introspectHandler(rw, req) + + require.Equal(t, http.StatusInternalServerError, rw.Code) + }) + t.Run("fail to parse empty request body", func(t *testing.T) { o := &Operation{} @@ -253,11 +308,13 @@ func TestOperation_introspectHandler(t *testing.T) { o, err := New(config(t)) require.NoError(t, err) + priv, client := clientKey(t) + intReq := &gnap.IntrospectRequest{ AccessToken: "invalid token", Proof: "httpsig", ResourceServer: &gnap.RequestClient{ - Key: clientKey(t), + Key: client, }, } @@ -268,6 +325,9 @@ func TestOperation_introspectHandler(t *testing.T) { req := httptest.NewRequest(http.MethodPost, AuthIntrospectPath, bytes.NewReader(intReqBytes)) + req, err = httpsig.Sign(req, intReqBytes, priv, "sha-256") + require.NoError(t, err) + o.introspectHandler(rw, req) require.Equal(t, http.StatusOK, rw.Code) @@ -795,11 +855,13 @@ func Test_Full_Flow(t *testing.T) { state string ) + userPriv, userClient := clientKey(t) + { authReq := &gnap.AuthRequest{ Client: &gnap.RequestClient{ IsReference: false, - Key: clientKey(t), + Key: userClient, }, AccessToken: []*gnap.TokenRequest{ { @@ -827,6 +889,9 @@ func Test_Full_Flow(t *testing.T) { req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader(authReqBytes)) + req, err = httpsig.Sign(req, authReqBytes, userPriv, "sha-256") + require.NoError(t, err) + o.authRequestHandler(rw, req) require.Equal(t, http.StatusOK, rw.Code) @@ -909,6 +974,9 @@ func Test_Full_Flow(t *testing.T) { req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader(contReqBytes)) req.Header.Add("Authorization", "GNAP "+authResp.Continue.AccessToken.Value) + req, err = httpsig.Sign(req, contReqBytes, userPriv, "sha-256") + require.NoError(t, err) + o.authContinueHandler(rw, req) require.Equal(t, http.StatusOK, rw.Code) @@ -918,12 +986,14 @@ func Test_Full_Flow(t *testing.T) { require.Len(t, contResp.AccessToken, 1) + rsPriv, rsClient := clientKey(t) + { intReq := &gnap.IntrospectRequest{ AccessToken: contResp.AccessToken[0].Value, Proof: "httpsig", ResourceServer: &gnap.RequestClient{ - Key: clientKey(t), + Key: rsClient, }, } @@ -934,6 +1004,9 @@ func Test_Full_Flow(t *testing.T) { req := httptest.NewRequest(http.MethodPost, AuthIntrospectPath, bytes.NewReader(intReqBytes)) + req, err = httpsig.Sign(req, intReqBytes, rsPriv, "sha-256") + require.NoError(t, err) + o.introspectHandler(rw, req) require.Equal(t, http.StatusOK, rw.Code) @@ -1077,21 +1150,42 @@ func config(t *testing.T) *Config { } } -func clientKey(t *testing.T) *gnap.ClientKey { +type errorReader struct { + err error +} + +func (e *errorReader) Read([]byte) (int, error) { + return 0, e.err +} + +func clientKey(t *testing.T) (*jwk.JWK, *gnap.ClientKey) { t.Helper() - pub, _, err := ed25519.GenerateKey(rand.Reader) + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - k, err := jwksupport.JWKFromKey(pub) - require.NoError(t, err) + privJWK := jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + pubJWK := jwk.JWK{ + JSONWebKey: privJWK.Public(), + Kty: "EC", + Crv: "P-256", + } ck := gnap.ClientKey{ Proof: "httpsig", - JWK: *k, + JWK: pubJWK, } - return &ck + return &privJWK, &ck } const ( diff --git a/spi/gnap/api.go b/spi/gnap/api.go index b3233c0..2711af4 100644 --- a/spi/gnap/api.go +++ b/spi/gnap/api.go @@ -6,12 +6,12 @@ SPDX-License-Identifier: Apache-2.0 package gnap +import ( + "net/http" +) + // Signer api for GNAP http signatures. type Signer interface { - Sign(msg []byte) ([]byte, error) -} - -// Verifier api for GNAP http signatures verification. -type Verifier interface { - Verify(msg, sig []byte) error + ProofType() string + Sign(request *http.Request, requestBody []byte) (*http.Request, error) } diff --git a/spi/gnap/clientverifier/httpsig/verifier.go b/spi/gnap/clientverifier/httpsig/verifier.go deleted file mode 100644 index 8958554..0000000 --- a/spi/gnap/clientverifier/httpsig/verifier.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package httpsig - -import ( - "net/http" - - "github.com/trustbloc/auth/spi/gnap" -) - -// Verifier verifies that the client request is signed by the client key, using http-signature verification. -type Verifier struct { - req *http.Request -} - -// NewVerifier initializes an http-signature Verifier on the given client request. -func NewVerifier(req *http.Request) *Verifier { - return &Verifier{req: req} -} - -// Verify verifies that the Verifier's client request is signed by the client key, using http-signature verification. -func (v *Verifier) Verify(key *gnap.ClientKey) error { - // TODO https://github.com/trustbloc/auth/issues/185 - return nil -} diff --git a/spi/gnap/go.mod b/spi/gnap/go.mod index f307654..922ebe6 100644 --- a/spi/gnap/go.mod +++ b/spi/gnap/go.mod @@ -8,6 +8,8 @@ go 1.17 require ( github.com/hyperledger/aries-framework-go v0.1.8 + github.com/igor-pavlenko/httpsignatures-go v0.0.23 + github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 github.com/stretchr/testify v1.7.1 ) @@ -19,7 +21,6 @@ require ( github.com/kilic/bls12-381 v0.1.1-0.20210503002446-7b7597926c69 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 // indirect github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect diff --git a/spi/gnap/go.sum b/spi/gnap/go.sum index 169f35c..c9baabc 100644 --- a/spi/gnap/go.sum +++ b/spi/gnap/go.sum @@ -42,6 +42,7 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.35.1/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.35.7/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go v1.36.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -85,7 +86,9 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= @@ -209,6 +212,8 @@ github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210807121559-b github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210820153043-8b6f36d10ab9/go.mod h1:7jEZdg455syX4f+ozLgwhYfIuiEQ/TgdIoOyALMwPG0= github.com/hyperledger/aries-framework-go/test/component v0.0.0-20220217153004-1622c70e5767/go.mod h1:HojN6OAh8ZtXBe5X2arcSOe1SLo5Dsjqto8ICjSLQ2g= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/igor-pavlenko/httpsignatures-go v0.0.23 h1:b+bo2vox5fwHKGiGfnu9/L5gM6RwSBdKQMPi48scAj4= +github.com/igor-pavlenko/httpsignatures-go v0.0.23/go.mod h1:3LVsCi3evlfQSNDKMTg3uElxEP8SjK3/Q5N9I8GU9W0= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= @@ -244,6 +249,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f/go.mod h1:414R+qZrt4f9S2TO/s6YVQMNAXR2KdwqQ7pW+O4oYzU= github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= diff --git a/spi/gnap/internal/digest/digest.go b/spi/gnap/internal/digest/digest.go new file mode 100644 index 0000000..c7684a7 --- /dev/null +++ b/spi/gnap/internal/digest/digest.go @@ -0,0 +1,42 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package digest + +import ( + "crypto" + _ "crypto/sha256" + "errors" +) + +const ( + SHA256 = "sha-256" +) + +// Digest computes the content-digest of the given message. +type Digest func([]byte) ([]byte, error) + +// GetDigest returns the Digest matching the given name. +func GetDigest(name string) (Digest, error) { + switch name { + case SHA256: + return hashToDigest(crypto.SHA256), nil + default: + return nil, errors.New("unsupported digest") + } +} + +func hashToDigest(hash crypto.Hash) Digest { + return func(msg []byte) ([]byte, error) { + h := hash.New() + _, err := h.Write(msg) + if err != nil { + return nil, err + } + + return h.Sum(nil), nil + } +} diff --git a/spi/gnap/internal/jwksignature/ecdsa.go b/spi/gnap/internal/jwksignature/ecdsa.go new file mode 100644 index 0000000..d2c840b --- /dev/null +++ b/spi/gnap/internal/jwksignature/ecdsa.go @@ -0,0 +1,114 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package jwksignature + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "encoding/asn1" + "errors" + "fmt" + "math/big" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" +) + +// copied from afgo:pkg/doc/util/signature/internal/signer/ecdsa.go +//nolint:gomnd +func ecdsaSign(msg []byte, privateKey *ecdsa.PrivateKey, alg string) ([]byte, error) { + var hash crypto.Hash + + switch alg { + case "ES256": + hash = crypto.SHA256 + default: + return nil, errors.New("alg not supported") + } + + hasher := hash.New() + _, err := hasher.Write(msg) + if err != nil { + return nil, fmt.Errorf("ecdsa hash error: %w", err) + } + + hashed := hasher.Sum(nil) + + r, s, err := ecdsa.Sign(rand.Reader, privateKey, hashed) + if err != nil { + return nil, fmt.Errorf("error signing with ecdsa: %w", err) + } + + curveBits := privateKey.Curve.Params().BitSize + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes++ + } + + copyPadded := func(source []byte, size int) []byte { + dest := make([]byte, size) + copy(dest[size-len(source):], source) + + return dest + } + + return append(copyPadded(r.Bytes(), keyBytes), copyPadded(s.Bytes(), keyBytes)...), nil +} + +func ecdsaVerifier(pubKeyJWK *jwk.JWK, msg, signature []byte) error { + switch pubKeyJWK.Algorithm { + case "ES256": + return ecdsaVerify(pubKeyJWK, msg, signature, 32, crypto.SHA256) + } + + return errors.New("ecdsa alg not supported") +} + +// copied (with amendments) from afgo:pkg/doc/signature/verifier/public_key_verifier.go +func ecdsaVerify(pubKeyJWK *jwk.JWK, msg, signature []byte, keySize int, hash crypto.Hash) error { + ecdsaPubKey, ok := pubKeyJWK.Key.(*ecdsa.PublicKey) + if !ok { + return errors.New("invalid public key type") + } + + if len(signature) < 2*keySize { + return errors.New("invalid signature size") + } + + hasher := hash.New() + + _, err := hasher.Write(msg) + if err != nil { + return errors.New("hash error") + } + + hashValue := hasher.Sum(nil) + + r := big.NewInt(0).SetBytes(signature[:keySize]) + s := big.NewInt(0).SetBytes(signature[keySize:]) + + if len(signature) > 2*keySize { + var esig struct { + R, S *big.Int + } + + if _, err := asn1.Unmarshal(signature, &esig); err != nil { + return fmt.Errorf("asn.1 unmarshal: %w", err) + } + + r = esig.R + s = esig.S + } + + verified := ecdsa.Verify(ecdsaPubKey, hashValue, r, s) + if !verified { + return errors.New("invalid signature") + } + + return nil +} diff --git a/spi/gnap/internal/jwksignature/ecdsa_test.go b/spi/gnap/internal/jwksignature/ecdsa_test.go new file mode 100644 index 0000000..fd1d9f6 --- /dev/null +++ b/spi/gnap/internal/jwksignature/ecdsa_test.go @@ -0,0 +1,133 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package jwksignature + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "math/big" + "testing" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/square/go-jose/v3" + "github.com/stretchr/testify/require" +) + +func Test_ecdsaSign(t *testing.T) { + t.Run("success", func(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + msg := []byte("the quick brown fox jumps over the lazy dog") + + sig, err := ecdsaSign(msg, ecKey, es256Alg) + require.NoError(t, err) + require.NotEmpty(t, sig) + }) + + t.Run("alg not supported", func(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + sig, err := ecdsaSign(nil, ecKey, "blah blah") + require.Error(t, err) + require.Nil(t, sig) + require.Contains(t, err.Error(), "alg not supported") + }) + + t.Run("signing error", func(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + ecKey.PublicKey.Curve = &elliptic.CurveParams{ + N: &big.Int{}, // make key invalid so ecdsa.Sign returns an error + } + + sig, err := ecdsaSign(nil, ecKey, es256Alg) + require.Error(t, err) + require.Nil(t, sig) + require.Contains(t, err.Error(), "error signing with ecdsa") + }) +} + +func Test_ecdsaVerify(t *testing.T) { + publicKey := `{ + "kty": "EC", + "kid": "key1", + "crv": "P-256", + "alg": "ES256", + "x": "igkN3pcl8OZ9bfzrLCRbflZ9cVmQVKfwXSHDbgN3G6U", + "y": "0qhuWhPxLeXgEWZnfUXObCZBb-n_wckAE-M5_4tGhWk" +}` + + publicKeyJWK := &jwk.JWK{} + + err := json.Unmarshal([]byte(publicKey), publicKeyJWK) + require.NoError(t, err) + + sigString := `z4few6IW83cySeJa+JUsyAOpP3hfpL7BkXMiGyN7RS9kMzLDMIJ8PMULomGu3X3iMsQOqFH+B7EdUQdY7IDixA==` + + sig, err := base64.StdEncoding.DecodeString(sigString) + require.NoError(t, err) + + msg := []byte("the quick brown fox jumps over the lazy dog") + + t.Run("success", func(t *testing.T) { + err = ecdsaVerifier(publicKeyJWK, msg, sig) + require.NoError(t, err) + }) + + t.Run("alg not supported", func(t *testing.T) { + pk := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Algorithm: "blah blah", + }, + } + + err := ecdsaVerifier(pk, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "alg not supported") + }) + + t.Run("invalid key type", func(t *testing.T) { + pk := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: []byte{}, + Algorithm: "ES256", + }, + } + + err := ecdsaVerifier(pk, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid public key type") + }) + + t.Run("invalid signature size", func(t *testing.T) { + err := ecdsaVerifier(publicKeyJWK, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid signature size") + }) + + t.Run("asn.1 unmarshal error", func(t *testing.T) { + badSig := make([]byte, len(sig)*2) + + err := ecdsaVerifier(publicKeyJWK, nil, badSig) + require.Error(t, err) + require.Contains(t, err.Error(), "asn.1 unmarshal") + }) + + t.Run("invalid signature", func(t *testing.T) { + badSig := make([]byte, len(sig)) + + err := ecdsaVerifier(publicKeyJWK, nil, badSig) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid signature") + }) +} diff --git a/spi/gnap/internal/jwksignature/jwksignature.go b/spi/gnap/internal/jwksignature/jwksignature.go new file mode 100644 index 0000000..154bcf5 --- /dev/null +++ b/spi/gnap/internal/jwksignature/jwksignature.go @@ -0,0 +1,67 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package jwksignature + +import ( + "crypto/ecdsa" + "errors" + "fmt" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/igor-pavlenko/httpsignatures-go" +) + +// SignatureAlgorithm provides http-signature JWK signatures. +type SignatureAlgorithm struct { + alg string +} + +// NewJWKAlgorithm +func NewJWKAlgorithm(alg string) *SignatureAlgorithm { + return &SignatureAlgorithm{ + alg: alg, + } +} + +// Algorithm returns the SignatureAlgorithm's algorithm. +func (s *SignatureAlgorithm) Algorithm() string { + return s.alg +} + +// Create implements http-signatures' Signer API. +func (s *SignatureAlgorithm) Create(secret httpsignatures.Secret, data []byte) ([]byte, error) { + priv := &jwk.JWK{} + + err := priv.UnmarshalJSON([]byte(secret.PrivateKey)) + if err != nil { + return nil, fmt.Errorf("parsing secret into JWK: %w", err) + } + + switch k := priv.Key.(type) { + case *ecdsa.PrivateKey: + return ecdsaSign(data, k, priv.Algorithm) + default: + return nil, errors.New("key type not supported") + } +} + +func (s *SignatureAlgorithm) Verify(secret httpsignatures.Secret, data []byte, signature []byte) error { + pub := &jwk.JWK{} + + // Note: httpsignatures-go uses PrivateKey value to store public key too. + err := pub.UnmarshalJSON([]byte(secret.PrivateKey)) + if err != nil { + return fmt.Errorf("parsing public key into JWK: %w", err) + } + + switch pub.Key.(type) { + case *ecdsa.PublicKey, *ecdsa.PrivateKey: + return ecdsaVerifier(pub, data, signature) + default: + return errors.New("key type not supported") + } +} diff --git a/spi/gnap/internal/jwksignature/jwksignature_test.go b/spi/gnap/internal/jwksignature/jwksignature_test.go new file mode 100644 index 0000000..f27252d --- /dev/null +++ b/spi/gnap/internal/jwksignature/jwksignature_test.go @@ -0,0 +1,196 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package jwksignature + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "testing" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/igor-pavlenko/httpsignatures-go" + "github.com/square/go-jose/v3" + "github.com/stretchr/testify/require" +) + +const ( + es256Alg = "ES256" +) + +func TestSignatureAlgorithm_Algorithm(t *testing.T) { + mockAlg := "mock-alg" + + alg := NewJWKAlgorithm(mockAlg) + + require.Equal(t, mockAlg, alg.Algorithm()) +} + +func TestSignatureAlgorithm_Create(t *testing.T) { + t.Run("success", func(t *testing.T) { + _, privData := secretPair(t) + + msg := []byte("the quick brown fox jumps over the lazy dog") + + alg := NewJWKAlgorithm(es256Alg) + + sig, err := alg.Create(privData, msg) + require.NoError(t, err) + require.NotEmpty(t, sig) + }) + + t.Run("unmarshal error", func(t *testing.T) { + privData := httpsignatures.Secret{ + PrivateKey: "foo bar baz", + } + + alg := NewJWKAlgorithm(es256Alg) + + sig, err := alg.Create(privData, nil) + require.Nil(t, sig) + require.Error(t, err) + require.Contains(t, err.Error(), "parsing secret") + }) + + t.Run("unsupported key type", func(t *testing.T) { + privData := httpsignatures.Secret{ + PrivateKey: `{ + "kty": "OKP", + "use": "enc", + "crv": "Ed25519", + "kid": "sample@sample.id", + "x": "sEHL6KXs8bUz9Ss2qSWWjhhRMHVjrog0lzFENM132R8", + "alg": "EdDSA" + }`, + } + + alg := NewJWKAlgorithm(es256Alg) + + sig, err := alg.Create(privData, nil) + require.Nil(t, sig) + require.Error(t, err) + require.Contains(t, err.Error(), "key type not supported") + }) +} + +func TestSignatureAlgorithm_Verify(t *testing.T) { + t.Run("success", func(t *testing.T) { + publicKey := `{ + "kty": "EC", + "kid": "key1", + "crv": "P-256", + "alg": "ES256", + "x": "igkN3pcl8OZ9bfzrLCRbflZ9cVmQVKfwXSHDbgN3G6U", + "y": "0qhuWhPxLeXgEWZnfUXObCZBb-n_wckAE-M5_4tGhWk" +}` + + pubData := httpsignatures.Secret{ + KeyID: "key1", + PrivateKey: publicKey, + Algorithm: es256Alg, + } + + sigString := `z4few6IW83cySeJa+JUsyAOpP3hfpL7BkXMiGyN7RS9kMzLDMIJ8PMULomGu3X3iMsQOqFH+B7EdUQdY7IDixA==` + + sig, err := base64.StdEncoding.DecodeString(sigString) + require.NoError(t, err) + + msg := []byte("the quick brown fox jumps over the lazy dog") + + alg := NewJWKAlgorithm(es256Alg) + + err = alg.Verify(pubData, msg, sig) + require.NoError(t, err) + + }) + + t.Run("unmarshal error", func(t *testing.T) { + privData := httpsignatures.Secret{ + PrivateKey: "foo bar baz", + } + + alg := NewJWKAlgorithm(es256Alg) + + err := alg.Verify(privData, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "parsing public key") + }) + + t.Run("unsupported key type", func(t *testing.T) { + privData := httpsignatures.Secret{ + PrivateKey: `{ + "kty": "OKP", + "use": "enc", + "crv": "Ed25519", + "kid": "sample@sample.id", + "x": "sEHL6KXs8bUz9Ss2qSWWjhhRMHVjrog0lzFENM132R8", + "alg": "EdDSA" + }`, + } + + alg := NewJWKAlgorithm(es256Alg) + + err := alg.Verify(privData, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "key type not supported") + }) +} + +func Test_SignVerify(t *testing.T) { + t.Run("success", func(t *testing.T) { + pubData, privData := secretPair(t) + + msg := []byte("the quick brown fox jumps over the lazy dog") + + alg := NewJWKAlgorithm(es256Alg) + + sig, err := alg.Create(privData, msg) + require.NoError(t, err) + + err = alg.Verify(pubData, msg, sig) + require.NoError(t, err) + }) +} + +func secretPair(t *testing.T) (pub, priv httpsignatures.Secret) { + t.Helper() + + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + kid := "key1" + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: ecKey, + KeyID: kid, + Algorithm: es256Alg, + }, + Kty: "EC", + Crv: "P-256", + } + + privBytes, err := json.Marshal(privJWK) + require.NoError(t, err) + + pubJWK := privJWK.Public() + + pubBytes, err := json.Marshal(&pubJWK) + require.NoError(t, err) + + return httpsignatures.Secret{ + KeyID: kid, + PrivateKey: string(pubBytes), + Algorithm: es256Alg, + }, httpsignatures.Secret{ + KeyID: kid, + PrivateKey: string(privBytes), + Algorithm: es256Alg, + } +} diff --git a/spi/gnap/proof/httpsig/sign_verify_test.go b/spi/gnap/proof/httpsig/sign_verify_test.go new file mode 100644 index 0000000..f047ac0 --- /dev/null +++ b/spi/gnap/proof/httpsig/sign_verify_test.go @@ -0,0 +1,62 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package httpsig + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/square/go-jose/v3" + "github.com/stretchr/testify/require" + "github.com/trustbloc/auth/spi/gnap" +) + +func TestSignVerify(t *testing.T) { + t.Run("success", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Add("Authorization", "Bearer OPEN-SESAME") + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + pubJWK := jwk.JWK{ + JSONWebKey: privJWK.Public(), + Kty: "EC", + Crv: "P-256", + } + + req, err = Sign(req, body, privJWK, "sha-256") + require.NoError(t, err) + + v := NewVerifier(req) + + err = v.Verify(&gnap.ClientKey{ + Proof: "httpsig", + JWK: pubJWK, + }) + require.NoError(t, err) + }) +} diff --git a/spi/gnap/proof/httpsig/signer.go b/spi/gnap/proof/httpsig/signer.go new file mode 100644 index 0000000..4e2d0a7 --- /dev/null +++ b/spi/gnap/proof/httpsig/signer.go @@ -0,0 +1,88 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package httpsig + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/igor-pavlenko/httpsignatures-go" + "github.com/trustbloc/auth/spi/gnap/internal/digest" + "github.com/trustbloc/auth/spi/gnap/internal/jwksignature" +) + +// Signer signs GNAP http requests using http-signature. +type Signer struct { + SigningKey *jwk.JWK +} + +// ProofType returns "httpsig", the GNAP proof type of the http-signature proof method. +func (s *Signer) ProofType() string { + return "httpsig" +} + +// Sign signs the given request using sha-256 for a content digest, and http-signature to sign headers. +func (s *Signer) Sign(request *http.Request, requestBody []byte) (*http.Request, error) { + return Sign(request, requestBody, s.SigningKey, digest.SHA256) +} + +// Sign signs a GNAP http request, adding http-signature headers. +func Sign(req *http.Request, bodyBytes []byte, signingKey *jwk.JWK, digestName string) (*http.Request, error) { + keyBytes, err := json.Marshal(signingKey) + if err != nil { + return nil, fmt.Errorf("marshalling signing key: %w", err) + } + + ss := httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{ + signingKey.KeyID: { + KeyID: signingKey.KeyID, + PublicKey: "", + PrivateKey: string(keyBytes), + Algorithm: signingKey.Algorithm, + }, + }) + hs := httpsignatures.NewHTTPSignatures(ss) + hs.SetSignatureHashAlgorithm(jwksignature.NewJWKAlgorithm(signingKey.Algorithm)) + + coveredComponents := []string{ + "(request-target)", // in this implementation, this string is the code for method + target-uri + } + + if len(bodyBytes) != 0 { + digestAlgorithm, err := digest.GetDigest(digestName) + if err != nil { + return nil, err + } + + contentDigest, err := digestAlgorithm(bodyBytes) + if err != nil { + return nil, fmt.Errorf("creating content-digest: %w", err) + } + + digestValue := digestName + "=:" + base64.StdEncoding.EncodeToString(contentDigest) + ":" + + req.Header.Add("Content-Digest", digestValue) + + coveredComponents = append(coveredComponents, "content-digest") + } + + if req.Header.Get("Authorization") != "" { + coveredComponents = append(coveredComponents, "authorization") + } + + hs.SetDefaultSignatureHeaders(coveredComponents) + + err = hs.Sign(signingKey.KeyID, req) + if err != nil { + return nil, fmt.Errorf("failed to sign: %w", err) + } + + return req, nil +} diff --git a/spi/gnap/proof/httpsig/signer_test.go b/spi/gnap/proof/httpsig/signer_test.go new file mode 100644 index 0000000..eb70253 --- /dev/null +++ b/spi/gnap/proof/httpsig/signer_test.go @@ -0,0 +1,155 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package httpsig + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/square/go-jose/v3" + "github.com/stretchr/testify/require" + "github.com/trustbloc/auth/spi/gnap/internal/digest" +) + +func TestSigner(t *testing.T) { + t.Run("ProofType", func(t *testing.T) { + require.Equal(t, "httpsig", (&Signer{}).ProofType()) + }) + + t.Run("Sign", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Add("Authorization", "Bearer OPEN-SESAME") + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + signer := &Signer{ + SigningKey: privJWK, + } + + req, err = signer.Sign(req, body) + require.NoError(t, err) + }) + +} + +func TestSign(t *testing.T) { + t.Run("success", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Add("Authorization", "Bearer OPEN-SESAME") + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + req, err = Sign(req, body, privJWK, digest.SHA256) + require.NoError(t, err) + }) + + t.Run("jwk marshal error", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Add("Authorization", "Bearer OPEN-SESAME") + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: []byte{}, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "OKP", // incorrect type data, to force a marshalling error + Crv: "X25519", + } + + _, err := Sign(req, body, privJWK, digest.SHA256) + require.Error(t, err) + require.Contains(t, err.Error(), "marshalling signing key") + }) + + t.Run("unsupported digest", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Add("Authorization", "Bearer OPEN-SESAME") + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + req, err = Sign(req, body, privJWK, "unknown digest") + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported digest") + }) + + t.Run("fail to sign", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Add("Authorization", "Bearer OPEN-SESAME") + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: &priv.PublicKey, // jwk algorithm will fail to sign given a public key + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + req, err = Sign(req, body, privJWK, digest.SHA256) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to sign") + }) +} diff --git a/spi/gnap/proof/httpsig/verifier.go b/spi/gnap/proof/httpsig/verifier.go new file mode 100644 index 0000000..5d2e54b --- /dev/null +++ b/spi/gnap/proof/httpsig/verifier.go @@ -0,0 +1,115 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package httpsig + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/igor-pavlenko/httpsignatures-go" + + "github.com/trustbloc/auth/spi/gnap" + "github.com/trustbloc/auth/spi/gnap/internal/digest" + "github.com/trustbloc/auth/spi/gnap/internal/jwksignature" +) + +// Verifier verifies that the client request is signed by the client key, using http-signature verification. +type Verifier struct { + req *http.Request +} + +// NewVerifier initializes an http-signature Verifier on the given client request. +func NewVerifier(req *http.Request) *Verifier { + return &Verifier{req: req} +} + +// Verify verifies that the Verifier's client request is signed by the client key, using http-signature verification. +func (v *Verifier) Verify(key *gnap.ClientKey) error { + keyBytes, err := json.Marshal(&key.JWK) + if err != nil { + return fmt.Errorf("marshalling verification key: %w", err) + } + + ss := httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{ + key.JWK.KeyID: { + KeyID: key.JWK.KeyID, + PublicKey: "", + PrivateKey: string(keyBytes), + Algorithm: key.JWK.Algorithm, + }, + }) + hs := httpsignatures.NewHTTPSignatures(ss) + hs.SetSignatureHashAlgorithm(jwksignature.NewJWKAlgorithm(key.JWK.Algorithm)) + + var bodyBytes []byte + + if v.req.Body != nil { + bodyBytes, err = ioutil.ReadAll(v.req.Body) + if err != nil { + return err + } + + v.req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) + + if len(bodyBytes) > 0 { + err = verifyDigest(v.req, bodyBytes) + if err != nil { + return err + } + + // TODO: confirm that the content-digest header is included in the http-signature input. + } + } + + err = hs.Verify(v.req) + if err != nil { + return fmt.Errorf("failed verification: %w", err) + } + + return nil +} + +func verifyDigest(req *http.Request, bodyBytes []byte) error { + contentDigest := req.Header.Get("Content-Digest") + if len(contentDigest) == 0 { + return errors.New("request with body should have a content-digest header") + } + + digestParts := strings.Split(contentDigest, ":") + if len(digestParts) < 2 { + return errors.New("content-digest header should have name and value") + } + + digestName := strings.Trim(digestParts[0], "=") + + digestAlg, err := digest.GetDigest(digestName) + if err != nil { + return err + } + + digestValue, err := base64.StdEncoding.DecodeString(digestParts[1]) + if err != nil { + return fmt.Errorf("decoding digest value: %w", err) + } + + computedDigest, err := digestAlg(bodyBytes) + if err != nil { + return fmt.Errorf("computing expected digest: %w", err) + } + + if !bytes.Equal(computedDigest, digestValue) { + return errors.New("content-digest header does not match digest of request body") + } + + return nil +} diff --git a/spi/gnap/proof/httpsig/verifier_test.go b/spi/gnap/proof/httpsig/verifier_test.go new file mode 100644 index 0000000..62e421b --- /dev/null +++ b/spi/gnap/proof/httpsig/verifier_test.go @@ -0,0 +1,173 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package httpsig + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/square/go-jose/v3" + "github.com/stretchr/testify/require" + "github.com/trustbloc/auth/spi/gnap" + "github.com/trustbloc/auth/spi/gnap/internal/digest" +) + +func TestNewVerifier(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/example/path", nil) + + v := NewVerifier(req) + + require.NotNil(t, v) + require.Equal(t, req, v.req) +} + +func TestVerifier_Verify(t *testing.T) { + t.Run("jwk marshal error", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/example/path", nil) + + v := NewVerifier(req) + + err := v.Verify(&gnap.ClientKey{ + JWK: jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: []byte{}, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "OKP", // incorrect type data, to force a marshalling error + Crv: "X25519", + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "marshalling verification key") + }) + + t.Run("fail to read body", func(t *testing.T) { + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", &errorReader{err: expectErr}) + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.ErrorIs(t, err, expectErr) + }) + + t.Run("has body but no content-digest", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.Contains(t, err.Error(), "should have a content-digest") + }) + + t.Run("content-digest header has invalid format", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Set("content-digest", "foo") + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.Contains(t, err.Error(), "content-digest header should have name and value") + }) + + t.Run("unsupported content-digest algorithm", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Set("content-digest", "foo:bar:") + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported digest") + }) + + t.Run("invalid digest value", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Set("content-digest", digest.SHA256+"=:#&^^%##$:") + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.Contains(t, err.Error(), "decoding digest value") + }) + + t.Run("digest does not match", func(t *testing.T) { + body := []byte("foo bar baz") + + req := httptest.NewRequest(http.MethodPost, "http://foo.bar/baz", bytes.NewReader(body)) + + req.Header.Set("content-digest", digest.SHA256+"=:eyJk:") + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.Contains(t, err.Error(), "content-digest header does not match digest") + }) + + t.Run("signature verification error", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://foo.bar/baz", nil) + + v := NewVerifier(req) + + err := v.Verify(clientKey(t)) + require.Error(t, err) + require.Contains(t, err.Error(), "failed verification") + }) + +} + +func clientKey(t *testing.T) *gnap.ClientKey { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + return &gnap.ClientKey{ + JWK: jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: &(priv.PublicKey), + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + }, + } +} + +type errorReader struct { + err error +} + +func (e *errorReader) Read([]byte) (int, error) { + return 0, e.err +} diff --git a/test/bdd/go.mod b/test/bdd/go.mod index 0b923ac..d53ff0a 100644 --- a/test/bdd/go.mod +++ b/test/bdd/go.mod @@ -59,6 +59,7 @@ require ( github.com/hashicorp/go-memdb v1.3.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hyperledger/aries-framework-go/spi v0.0.0-20220330140627-07042d78580c // indirect + github.com/igor-pavlenko/httpsignatures-go v0.0.23 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kilic/bls12-381 v0.1.1-0.20210503002446-7b7597926c69 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect diff --git a/test/bdd/go.sum b/test/bdd/go.sum index f0b926d..d558bcb 100644 --- a/test/bdd/go.sum +++ b/test/bdd/go.sum @@ -557,6 +557,7 @@ github.com/hyperledger/aries-framework-go/test/component v0.0.0-20220322085443-5 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/igor-pavlenko/httpsignatures-go v0.0.23 h1:b+bo2vox5fwHKGiGfnu9/L5gM6RwSBdKQMPi48scAj4= github.com/igor-pavlenko/httpsignatures-go v0.0.23/go.mod h1:3LVsCi3evlfQSNDKMTg3uElxEP8SjK3/Q5N9I8GU9W0= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -809,7 +810,6 @@ github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/sjson v1.1.4/go.mod h1:wXpKXu8CtDjKAZ+3DrKY5ROCorDFahq8l0tey/Lx1fg= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/trustbloc/auth/test/bdd v0.0.0-20220420191458-a538ca915d5d/go.mod h1:oZyBz03lDvTpNR+T4HBNdiOcEHbYVHWdh8BCLUM67vQ= github.com/trustbloc/edge-core v0.1.8 h1:m4X5XNDwiHJjGf8gHnpo6aLkBYuqDyNRq+npjxLc5cY= github.com/trustbloc/edge-core v0.1.8/go.mod h1:gfoyG/xquRXyHkww0ldM2jwOTuKKZpHYn+87f+TBQ8M= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= diff --git a/test/bdd/pkg/gnap/signer.go b/test/bdd/pkg/gnap/signer.go deleted file mode 100644 index 77430ba..0000000 --- a/test/bdd/pkg/gnap/signer.go +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package gnap - -type Signer struct { - PrivateKey []byte -} - -func (m *Signer) Sign(msg []byte) ([]byte, error) { - // TODO add signature - return msg, nil -} diff --git a/test/bdd/pkg/gnap/steps.go b/test/bdd/pkg/gnap/steps.go index e85f062..bf6cba9 100644 --- a/test/bdd/pkg/gnap/steps.go +++ b/test/bdd/pkg/gnap/steps.go @@ -7,7 +7,8 @@ SPDX-License-Identifier: Apache-2.0 package gnap import ( - "crypto/ed25519" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "fmt" "net/http" @@ -17,10 +18,11 @@ import ( "github.com/cucumber/godog" "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" - "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" + "github.com/square/go-jose/v3" "github.com/trustbloc/auth/component/gnap/as" "github.com/trustbloc/auth/component/gnap/rs" "github.com/trustbloc/auth/spi/gnap" + "github.com/trustbloc/auth/spi/gnap/proof/httpsig" bddctx "github.com/trustbloc/auth/test/bdd/pkg/context" ) @@ -40,7 +42,9 @@ type Steps struct { ctx *bddctx.BDDContext gnapClient *as.Client gnapRSClient *rs.Client - pubKeyJWK jwk.JWK + + clientPubKey *jwk.JWK + rsPubKey *jwk.JWK authResp *gnap.AuthResponse interactRef string browser *http.Client @@ -66,44 +70,77 @@ func (s *Steps) createGNAPClient() error { Transport: &http.Transport{TLSClientConfig: s.ctx.TLSConfig()}, } - // create key-pair - pub, private, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return fmt.Errorf("failed to create ed25519 key-pair: %w", err) - } + { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } - pubKeyJWK, err := jwksupport.JWKFromKey(pub) - if err != nil { - return fmt.Errorf("failed to create jwk from key: %w", err) - } + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } - // create gnap as client - gnapClient, err := as.NewClient( - &Signer{ - PrivateKey: private, - }, - httpClient, - authServerURL, - ) - if err != nil { - return fmt.Errorf("failed to create gnap as go-client: %w", err) - } + pubJWK := &jwk.JWK{ + JSONWebKey: privJWK.Public(), + Kty: "EC", + Crv: "P-256", + } - // create gnap rs client - gnapRSClient, err := rs.NewClient( - &Signer{ - PrivateKey: private, - }, - httpClient, - authServerURL, - ) - if err != nil { - return fmt.Errorf("failed to create gnap rs go-client: %w", err) + // create gnap as client + gnapClient, err := as.NewClient( + &httpsig.Signer{SigningKey: privJWK}, + httpClient, + authServerURL, + ) + if err != nil { + return fmt.Errorf("failed to create gnap as go-client: %w", err) + } + + s.gnapClient = gnapClient + s.clientPubKey = pubJWK } - s.gnapClient = gnapClient - s.gnapRSClient = gnapRSClient - s.pubKeyJWK = *pubKeyJWK + { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + privJWK := &jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + } + + pubJWK := &jwk.JWK{ + JSONWebKey: privJWK.Public(), + Kty: "EC", + Crv: "P-256", + } + + // create gnap rs client + gnapRSClient, err := rs.NewClient( + &httpsig.Signer{SigningKey: privJWK}, + httpClient, + authServerURL, + ) + if err != nil { + return fmt.Errorf("failed to create gnap rs go-client: %w", err) + } + + s.gnapRSClient = gnapRSClient + s.rsPubKey = pubJWK + } return nil } @@ -115,7 +152,7 @@ func (s *Steps) txnRequest() error { Client: &gnap.RequestClient{ Key: &gnap.ClientKey{ Proof: "httpsig", - JWK: s.pubKeyJWK, + JWK: *s.clientPubKey, }, }, AccessToken: []*gnap.TokenRequest{ @@ -298,15 +335,14 @@ func (s *Steps) introspection() error { req := &gnap.IntrospectRequest{ ResourceServer: &gnap.RequestClient{ Key: &gnap.ClientKey{ - JWK: s.pubKeyJWK, + JWK: *s.rsPubKey, + Proof: "httpsig", }, }, Proof: "httpsig", AccessToken: tok.Value, } - fmt.Printf("token: %#v\n", tok) - resp, err := s.gnapRSClient.Introspect(req) if err != nil { return fmt.Errorf("failed to call introspect request: %w", err)