diff --git a/authentication.go b/authentication.go index e7705714..e165e8e7 100644 --- a/authentication.go +++ b/authentication.go @@ -1,282 +1,17 @@ package bux import ( - "bytes" "context" - "fmt" - "io" - "net/http" - "strconv" - "strings" - "time" - - "github.com/BuxOrg/bux/utils" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/libsv/go-bk/bip32" - "github.com/libsv/go-bt/v2/bscript" ) -// AuthenticateRequest will parse the incoming request for the associated authentication header, -// and it will check the Key/Signature -// -// Sets req.Context(xpub) and req.Context(xpub_hash) -func (c *Client) AuthenticateRequest(ctx context.Context, req *http.Request, adminXPubs []string, - adminRequired, requireSigning, signingDisabled bool, -) (*http.Request, error) { - // Get the xPub/Access Key from the header - xPub := strings.TrimSpace(req.Header.Get(AuthHeader)) - authAccessKey := strings.TrimSpace(req.Header.Get(AuthAccessKey)) - if len(xPub) == 0 && len(authAccessKey) == 0 { // No value found - return req, ErrMissingAuthHeader - } - - // Check for admin key - if adminRequired { - if !utils.StringInSlice(xPub, adminXPubs) { - return req, ErrNotAdminKey - } - } - - xPubID := utils.Hash(xPub) - xPubOrAccessKey := xPub - if xPub != "" { - // Validate that the xPub is an HD key (length, validation) - if _, err := utils.ValidateXPub(xPubOrAccessKey); err != nil { - return req, err - } - } else if authAccessKey != "" { - xPubOrAccessKey = authAccessKey - - accessKey, err := getAccessKey(ctx, utils.Hash(authAccessKey), c.DefaultModelOptions()...) - if err != nil { - return req, err - } - if accessKey == nil || accessKey.RevokedAt.Valid { - return req, ErrAuthAccessKeyNotFound - } - - xPubID = accessKey.XpubID - } - - if req.Body == nil { - return req, ErrMissingBody - } - defer func() { - _ = req.Body.Close() - }() - b, err := io.ReadAll(req.Body) - if err != nil { - return req, err - } - - req.Body = io.NopCloser(bytes.NewReader(b)) - - authTime, _ := strconv.Atoi(req.Header.Get(AuthHeaderTime)) - authData := &AuthPayload{ - AuthHash: req.Header.Get(AuthHeaderHash), - AuthNonce: req.Header.Get(AuthHeaderNonce), - AuthTime: int64(authTime), - BodyContents: string(b), - Signature: req.Header.Get(AuthSignature), - } - - // adminRequired will always force checking of a signature - if (requireSigning || adminRequired) && !signingDisabled { - if err = c.checkSignature(ctx, xPubOrAccessKey, authData); err != nil { - return req, err - } - req = setOnRequest(req, ParamAuthSigned, true) - } else { - // check the signature and add to request, but do not fail if incorrect - err = c.checkSignature(ctx, xPubOrAccessKey, authData) - req = setOnRequest(req, ParamAuthSigned, err == nil) - - // NOTE: you can not use an access key if signing is invalid - ever - if xPubOrAccessKey == authAccessKey && err != nil { - return req, err - } - } - - req = setOnRequest(req, ParamAdminRequest, adminRequired) - - // Set the data back onto the request - return setOnRequest(setOnRequest(req, ParamXPubKey, xPub), ParamXPubHashKey, xPubID), nil -} - -// checkSignature check the signature for the provided auth payload -func (c *Client) checkSignature(ctx context.Context, xPubOrAccessKey string, auth *AuthPayload) error { - // Check that we have the basic signature components - if err := checkSignatureRequirements(auth); err != nil { - return err - } - - // Check xPub vs Access Key - if strings.Contains(xPubOrAccessKey, "xpub") && len(xPubOrAccessKey) > 64 { - return verifyKeyXPub(xPubOrAccessKey, auth) - } - return verifyAccessKey(ctx, xPubOrAccessKey, auth, c.DefaultModelOptions()...) -} - -// checkSignatureRequirements will check the payload for basic signature requirements -func checkSignatureRequirements(auth *AuthPayload) error { - // Check that we have a signature - if auth == nil || auth.Signature == "" { - return ErrMissingSignature - } - - // Check the auth hash vs the body hash - bodyHash := createBodyHash(auth.BodyContents) - if auth.AuthHash != bodyHash { - return ErrAuhHashMismatch - } - - // Check the auth timestamp - if time.Now().UTC().After(time.UnixMilli(auth.AuthTime).Add(AuthSignatureTTL)) { - return ErrSignatureExpired - } - return nil -} - -// verifyKeyXPub will verify the xPub key and the signature payload -func verifyKeyXPub(xPub string, auth *AuthPayload) error { - // Validate that the xPub is an HD key (length, validation) - if _, err := utils.ValidateXPub(xPub); err != nil { - return err - } - - // Cannot be nil - if auth == nil { - return ErrMissingSignature - } - - // Get the key from xPub - key, err := bitcoin.GetHDKeyFromExtendedPublicKey(xPub) - if err != nil { - return err - } - - // Derive the address for signing - if key, err = utils.DeriveChildKeyFromHex(key, auth.AuthNonce); err != nil { - return err - } - - var address *bscript.Address - if address, err = bitcoin.GetAddressFromHDKey(key); err != nil { - return err // Should never error - } - - // Return the error if verification fails - message := getSigningMessage(xPub, auth) - if err = bitcoin.VerifyMessage( - address.AddressString, - auth.Signature, - message, - ); err != nil { - return ErrSignatureInvalid - } - return nil -} - -// verifyAccessKey will verify the access key and the signature payload -func verifyAccessKey(ctx context.Context, key string, auth *AuthPayload, opts ...ModelOps) error { - // Get access key from DB - // todo: add caching in the future, faster than DB - accessKey, err := getAccessKey(ctx, utils.Hash(key), opts...) +func (c *Client) AuthenticateAccessKey(ctx context.Context, pubAccessKey string) (*AccessKey, error) { + accessKey, err := getAccessKey(ctx, pubAccessKey, c.DefaultModelOptions()...) if err != nil { - return err + return nil, err } else if accessKey == nil { - return ErrUnknownAccessKey + return nil, ErrUnknownAccessKey } else if accessKey.RevokedAt.Valid { - return ErrAccessKeyRevoked - } - - var address *bscript.Address - if address, err = bitcoin.GetAddressFromPubKeyString( - key, true, - ); err != nil { - return err - } - - // Return the error if verification fails - if err = bitcoin.VerifyMessage( - address.AddressString, - auth.Signature, - getSigningMessage(key, auth), - ); err != nil { - return ErrSignatureInvalid - } - return nil -} - -// SetSignature will set the signature on the header for the request -func SetSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) error { - // Create the signature - authData, err := createSignature(xPriv, bodyString) - if err != nil { - return err - } - - // Set the auth header - header.Set(AuthHeader, authData.xPub) - - return setSignatureHeaders(header, authData) -} - -// SetSignatureFromAccessKey will set the signature on the header for the request from an access key -func SetSignatureFromAccessKey(header *http.Header, privateKeyHex, bodyString string) error { - // Create the signature - authData, err := createSignatureAccessKey(privateKeyHex, bodyString) - if err != nil { - return err + return nil, ErrAccessKeyRevoked } - - // Set the auth header - header.Set(AuthAccessKey, authData.accessKey) - - return setSignatureHeaders(header, authData) -} - -func setSignatureHeaders(header *http.Header, authData *AuthPayload) error { - // Create the auth header hash - header.Set(AuthHeaderHash, authData.AuthHash) - - // Set the nonce - header.Set(AuthHeaderNonce, authData.AuthNonce) - - // Set the time - header.Set(AuthHeaderTime, fmt.Sprintf("%d", authData.AuthTime)) - - // Set the signature - header.Set(AuthSignature, authData.Signature) - - return nil -} - -// CreateSignature will create a signature for the given key & body contents -func CreateSignature(xPriv *bip32.ExtendedKey, bodyString string) (string, error) { - authData, err := createSignature(xPriv, bodyString) - if err != nil { - return "", err - } - return authData.Signature, nil -} - -// getSigningMessage will build the signing message string -func getSigningMessage(xPub string, auth *AuthPayload) string { - return fmt.Sprintf("%s%s%s%d", xPub, auth.AuthHash, auth.AuthNonce, auth.AuthTime) -} - -// GetXpubFromRequest gets the stored xPub from the request if found -func GetXpubFromRequest(req *http.Request) (string, bool) { - return getFromRequest(req, ParamXPubKey) -} - -// GetXpubIDFromRequest gets the stored xPubID from the request if found -func GetXpubIDFromRequest(req *http.Request) (string, bool) { - return getFromRequest(req, ParamXPubHashKey) -} - -// IsAdminRequest gets the stored xPub from the request if found -func IsAdminRequest(req *http.Request) (bool, bool) { - return getBoolFromRequest(req, ParamAdminRequest) + return accessKey, nil } diff --git a/authentication_internal.go b/authentication_internal.go deleted file mode 100644 index d5fd7fc2..00000000 --- a/authentication_internal.go +++ /dev/null @@ -1,184 +0,0 @@ -package bux - -import ( - "context" - "encoding/hex" - "net/http" - "strings" - "time" - - "github.com/BuxOrg/bux/utils" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" -) - -const ( - // AuthHeader is the header to use for authentication (raw xPub) - AuthHeader = "bux-auth-xpub" - - // AuthAccessKey is the header to use for access key authentication (access public key) - AuthAccessKey = "bux-auth-key" - - // AuthSignature is the given signature (body + timestamp) - AuthSignature = "bux-auth-signature" - - // AuthHeaderHash hash of the body coming from the request - AuthHeaderHash = "bux-auth-hash" - - // AuthHeaderNonce random nonce for the request - AuthHeaderNonce = "bux-auth-nonce" - - // AuthHeaderTime the time of the request, only valid for 30 seconds - AuthHeaderTime = "bux-auth-time" - - // AuthSignatureTTL is the max TTL for a signature to be valid - AuthSignatureTTL = 20 * time.Second -) - -// AuthPayload is the authentication payload for checking or creating a signature -type AuthPayload struct { - AuthHash string `json:"auth_hash"` - AuthNonce string `json:"auth_nonce"` - AuthTime int64 `json:"auth_time"` - BodyContents string `json:"body_contents"` - Signature string `json:"signature"` - xPub string - accessKey string -} - -// ParamRequestKey for context key -type ParamRequestKey string - -const ( - // ParamXPubKey the request parameter for the xpub string - ParamXPubKey ParamRequestKey = "xpub" - - // ParamXPubHashKey the request parameter for the xpub ID - ParamXPubHashKey ParamRequestKey = "xpub_hash" - - // ParamAdminRequest the request parameter whether this is an admin request - ParamAdminRequest ParamRequestKey = "auth_admin" - - // ParamAuthSigned the request parameter that says whether the request was signed - ParamAuthSigned ParamRequestKey = "auth_signed" -) - -// createBodyHash will create the hash of the body, removing any carriage returns -func createBodyHash(bodyContents string) string { - return utils.Hash(strings.TrimSuffix(bodyContents, "\n")) -} - -// createSignature will create a signature for the given key & body contents -func createSignature(xPriv *bip32.ExtendedKey, bodyString string) (payload *AuthPayload, err error) { - - // No key? - if xPriv == nil { - err = ErrMissingXPriv - return - } - - // Get the xPub - payload = new(AuthPayload) - if payload.xPub, err = bitcoin.GetExtendedPublicKey( - xPriv, - ); err != nil { // Should never error if key is correct - return - } - - // auth_nonce is a random unique string to seed the signing message - // this can be checked server side to make sure the request is not being replayed - if payload.AuthNonce, err = utils.RandomHex(32); err != nil { // Should never error if key is correct - return - } - - // Derive the address for signing - var key *bip32.ExtendedKey - if key, err = utils.DeriveChildKeyFromHex( - xPriv, payload.AuthNonce, - ); err != nil { - return - } - - var privateKey *bec.PrivateKey - if privateKey, err = bitcoin.GetPrivateKeyFromHDKey(key); err != nil { - return // Should never error if key is correct - } - - return createSignatureCommon(payload, bodyString, privateKey) -} - -// createSignatureAccessKey will create a signature for the given access key & body contents -func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *AuthPayload, err error) { - - // No key? - if privateKeyHex == "" { - err = ErrMissingAccessKey - return - } - - var privateKey *bec.PrivateKey - if privateKey, err = bitcoin.PrivateKeyFromString( - privateKeyHex, - ); err != nil { - return - } - publicKey := privateKey.PubKey() - - // Get the xPub - payload = new(AuthPayload) - payload.accessKey = hex.EncodeToString(publicKey.SerialiseCompressed()) - - // auth_nonce is a random unique string to seed the signing message - // this can be checked server side to make sure the request is not being replayed - payload.AuthNonce, err = utils.RandomHex(32) - if err != nil { - return nil, err - } - - return createSignatureCommon(payload, bodyString, privateKey) -} - -// createSignatureCommon will create a signature -func createSignatureCommon(payload *AuthPayload, bodyString string, privateKey *bec.PrivateKey) (*AuthPayload, error) { - - // Create the auth header hash - payload.AuthHash = utils.Hash(bodyString) - - // auth_time is the current time and makes sure a request can not be sent after 30 secs - payload.AuthTime = time.Now().UnixMilli() - - key := payload.xPub - if key == "" && payload.accessKey != "" { - key = payload.accessKey - } - - // Signature, using bitcoin signMessage - var err error - if payload.Signature, err = bitcoin.SignMessage( - hex.EncodeToString(privateKey.Serialise()), - getSigningMessage(key, payload), - true, - ); err != nil { - return nil, err - } - - return payload, nil -} - -// setOnRequest will set the value on the request with the given key -func setOnRequest(req *http.Request, keyName ParamRequestKey, value interface{}) *http.Request { - return req.WithContext(context.WithValue(req.Context(), keyName, value)) -} - -// getFromRequest gets the stored value from the request if found -func getFromRequest(req *http.Request, key ParamRequestKey) (v string, ok bool) { - v, ok = req.Context().Value(key).(string) - return -} - -// getBoolFromRequest gets the stored bool value from the request if found -func getBoolFromRequest(req *http.Request, key ParamRequestKey) (v bool, ok bool) { - v, ok = req.Context().Value(key).(bool) - return -} diff --git a/authentication_test.go b/authentication_test.go deleted file mode 100644 index 76e2058a..00000000 --- a/authentication_test.go +++ /dev/null @@ -1,686 +0,0 @@ -package bux - -import ( - "bytes" - "context" - "fmt" - "net/http" - "strconv" - "testing" - "time" - - "github.com/BuxOrg/bux/utils" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - // testAccessKey = "9b2a4421edd88782a193ea8195cce1fe9b632df575c88d70f20a1fdf6835b764" - // testAccessKeyAddress = "1HuoHijPa7BqQNiV953pd3taqnmyhgDXFt" - // testAccessKeyID = "b7b91e8aca22b4ee33f3f0e48c00cd4631dc2dbba1f773829883eaae42fa2234" - // testAccessKeyPublic = "02719a5e3623bee13f8116f1db4ee54603c993e020087960f31d2e0b4cbd97d175" - // testSignatureAuthNonce = `dec0535f13b7ed61c2b188b7fe8fd5f578d6931aa90b6063c653ce0f8eefacf1` - // testSignatureAuthTime = "1643828414038" - // testSignatureXpub = `xpub661MyMwAqRbcFnj7dmEoX4ULYMJ2vxFBkH3oGrpuQMHTMpxUEGND1UXwskzgtUj6R7i9dRNGYj6NYuXWKVM5yAJYjSGuvBJfDTpqjsh8a3T` - testAccessKeyPKH = "b97e4834a13d188ab0588dc2aaff11a6658771cd" - testBodyContents = `{"test_field":"test_value"}` - testEncryption = "35dbe09a941a90a5f59e57020face68860d7b284b7b2973a58de8b4242ec5a925a40ac2933b7e45e78a0b3a13123520e46f9566815589ba2d345577dadee0d5e" - testSignature = `HxNguR72c6BV7tKNn5BQ3/mS2+RX3BGyQHFfVfQ3v4mVdAuh+w32QsFYxsB13KiXuRJ7ZnN7C8RhkAtLi/qvH88=` - testSignatureAuthHash = `5858adf09a0cc01f6d3a4d377f010408313031bb96b40d98e6edccf18c26464e` - testXpubAuth = "xpub661MyMwAqRbcH3WGvLjupmr43L1GVH3MP2WQWvdreDraBeFJy64Xxv4LLX9ZVWWz3ZjZkMuZtSsc9qH9JZR74bR4PWkmtEvP423r6DJR8kA" - testXpubAuthHash = "d8c2bed524071d72d859caf90da5f448b5861cd4d4fd47697f94166c13c5a987" -) - -// TestClient_AuthenticateRequest will test the method AuthenticateRequest() -func TestClient_AuthenticateRequest(t *testing.T) { - t.Parallel() - - t.Run("valid xpub", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthHeader, testXpubAuth) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{""}, false, false, false, - ) - require.NoError(t, err) - require.NotNil(t, req) - - // Test the request - x, ok := GetXpubFromRequest(req) - assert.Equal(t, testXpubAuth, x) - assert.Equal(t, true, ok) - - x, ok = GetXpubIDFromRequest(req) - assert.Equal(t, testXpubAuthHash, x) - assert.Equal(t, true, ok) - }) - - t.Run("xpub - valid signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var req *http.Request - req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - err = SetSignature(&req.Header, key, `{}`) - require.NoError(t, err) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{}, false, true, false, - ) - require.NoError(t, err) - require.NotNil(t, req) - assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) - }) - - t.Run("xpub - valid signature - not required", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var req *http.Request - req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - var authData *AuthPayload - authData, err = createSignature(key, `{}`) - require.NoError(t, err) - require.NotNil(t, authData) - - err = SetSignature(&req.Header, key, `{}`) - require.NoError(t, err) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{authData.xPub}, false, false, false, - ) - require.NoError(t, err) - require.NotNil(t, req) - assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) - }) - - t.Run("error - admin required", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthHeader, testXpubAuth) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{""}, true, false, false, - ) - require.Error(t, err) - require.NotNil(t, req) - assert.ErrorIs(t, err, ErrNotAdminKey) - }) - - t.Run("error - admin key - missing body", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthHeader, testXpubAuth) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{testXpubAuth}, true, false, false, - ) - require.Error(t, err) - require.NotNil(t, req) - assert.ErrorIs(t, err, ErrMissingBody) - }) - - t.Run("error - admin key - missing signature", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthHeader, testXpubAuth) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{testXpubAuth}, true, false, false, - ) - require.Error(t, err) - require.NotNil(t, req) - assert.ErrorIs(t, err, ErrMissingSignature) - }) - - t.Run("admin key - valid signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var req *http.Request - req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - var authData *AuthPayload - authData, err = createSignature(key, `{}`) - require.NoError(t, err) - require.NotNil(t, authData) - - err = SetSignature(&req.Header, key, `{}`) - require.NoError(t, err) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{authData.xPub}, true, false, false, - ) - require.NoError(t, err) - require.NotNil(t, req) - }) - - t.Run("admin key - signing disabled", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthHeader, testXpubAuth) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{testXpubAuth}, true, false, true, - ) - require.NoError(t, err) - require.NotNil(t, req) - }) - - t.Run("no authentication header set", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{""}, false, false, false, - ) - require.Error(t, err) - require.NotNil(t, req) - - // Test the request - x, ok := GetXpubFromRequest(req) - assert.Equal(t, "", x) - assert.Equal(t, false, ok) - - x, ok = GetXpubIDFromRequest(req) - assert.Equal(t, "", x) - assert.Equal(t, false, ok) - }) - - t.Run("invalid xpub length", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthHeader, "invalid-length") - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{""}, false, false, false, - ) - require.Error(t, err) - require.NotNil(t, req) - - // Test the request - x, ok := GetXpubFromRequest(req) - assert.Equal(t, "", x) - assert.Equal(t, false, ok) - - x, ok = GetXpubIDFromRequest(req) - assert.Equal(t, "", x) - assert.Equal(t, false, ok) - }) - - t.Run("access key - not signed", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - req.Header.Set(AuthAccessKey, "020202") - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - _, err = client.AuthenticateRequest( - context.Background(), req, []string{""}, false, false, false, - ) - require.ErrorIs(t, err, ErrAuthAccessKeyNotFound) - }) - - t.Run("access key - key not found", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - _, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - var authData *AuthPayload - // AuthAccessKey - authData, err = createSignatureAccessKey(testAccessKeyPKH, `{}`) - require.NoError(t, err) - require.NotNil(t, authData) - - err = SetSignatureFromAccessKey(&req.Header, testAccessKeyPKH, `{}`) - require.NoError(t, err) - - _, err = client.AuthenticateRequest( - context.Background(), req, []string{authData.xPub}, false, true, false, - ) - require.ErrorIs(t, err, ErrAuthAccessKeyNotFound) - }) - - t.Run("access key - valid signature", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - ctx, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - accessKey := newAccessKey(testXPubID, append(client.DefaultModelOptions(), New())...) - err = accessKey.Save(ctx) - require.NoError(t, err) - - var authData *AuthPayload - // AuthAccessKey - authData, err = createSignatureAccessKey(accessKey.Key, `{}`) - require.NoError(t, err) - require.NotNil(t, authData) - - err = SetSignatureFromAccessKey(&req.Header, accessKey.Key, `{}`) - require.NoError(t, err) - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{authData.xPub}, false, true, false, - ) - require.NoError(t, err) - require.NotNil(t, req) - assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) - }) - - t.Run("access key - valid signature - not required", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) - require.NoError(t, err) - require.NotNil(t, req) - - ctx, client, deferMe := CreateTestSQLiteClient(t, false, false) - defer deferMe() - - accessKey := newAccessKey(testXPubID, append(client.DefaultModelOptions(), New())...) - err = accessKey.Save(ctx) - require.NoError(t, err) - - var authData *AuthPayload - // AuthAccessKey - authData, err = createSignatureAccessKey(accessKey.Key, `{}`) - require.NoError(t, err) - require.NotNil(t, authData) - - err = SetSignatureFromAccessKey(&req.Header, accessKey.Key, `{}`) - require.NoError(t, err) - - req, err = client.AuthenticateRequest( - context.Background(), req, []string{authData.xPub}, false, false, false, - ) - require.NoError(t, err) - require.NotNil(t, req) - assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) - }) -} - -// Test_verifyKeyXPub will test the method verifyKeyXPub() -func Test_verifyKeyXPub(t *testing.T) { - t.Parallel() - - t.Run("error - missing auth data", func(t *testing.T) { - err := verifyKeyXPub(testXpubAuth, nil) - require.Error(t, err) - assert.ErrorIs(t, err, ErrMissingSignature) - }) - - t.Run("error - missing auth signature", func(t *testing.T) { - err := checkSignatureRequirements(&AuthPayload{}) - require.Error(t, err) - assert.ErrorIs(t, err, ErrMissingSignature) - }) - - t.Run("error - auth hash mismatch", func(t *testing.T) { - err := checkSignatureRequirements(&AuthPayload{ - AuthHash: "bad-hash", - BodyContents: testBodyContents, - Signature: testSignature, - }) - require.Error(t, err) - assert.ErrorIs(t, err, ErrAuhHashMismatch) - }) - - t.Run("error - signature expired", func(t *testing.T) { - err := checkSignatureRequirements(&AuthPayload{ - AuthHash: testSignatureAuthHash, - BodyContents: testBodyContents, - Signature: testSignature, - AuthTime: 1643828414038, - }) - require.Error(t, err) - assert.ErrorIs(t, err, ErrSignatureExpired) - }) - - t.Run("error - bad xpub", func(t *testing.T) { - err := verifyKeyXPub("invalid-key", &AuthPayload{ - AuthHash: testSignatureAuthHash, - BodyContents: testBodyContents, - Signature: testSignature, - AuthTime: time.Now().UnixMilli(), - }) - require.Error(t, err) - }) - - t.Run("error - invalid signature - time is wrong", func(t *testing.T) { - err := checkSignatureRequirements(&AuthPayload{ - AuthHash: testSignatureAuthHash, - BodyContents: testBodyContents, - Signature: testSignature, - AuthTime: 0, - }) - require.Error(t, err) - assert.ErrorIs(t, err, ErrSignatureExpired) - }) - - t.Run("valid signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - authData, err2 := createSignature(key, testBodyContents) - require.NoError(t, err2) - require.NotNil(t, authData) - - err = verifyKeyXPub(authData.xPub, &AuthPayload{ - AuthHash: authData.AuthHash, - AuthNonce: authData.AuthNonce, - AuthTime: authData.AuthTime, - BodyContents: testBodyContents, - Signature: authData.Signature, - }) - require.NoError(t, err) - }) -} - -// TestCreateSignature will test the method CreateSignature() -func TestCreateSignature(t *testing.T) { - t.Parallel() - - t.Run("valid signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var sig string - sig, err = CreateSignature(key, testBodyContents) - require.NoError(t, err) - require.NotNil(t, sig) - assert.Greater(t, len(sig), 40) - }) - - t.Run("missing key", func(t *testing.T) { - sig, err := CreateSignature(nil, testBodyContents) - require.Error(t, err) - assert.Equal(t, "", sig) - }) - - t.Run("missing body contents - still has signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var sig string - sig, err = CreateSignature(key, "") - require.NoError(t, err) - require.NotNil(t, sig) - assert.Greater(t, len(sig), 40) - }) -} - -// Test_createSignature will test the method createSignature() -func Test_createSignature(t *testing.T) { - t.Parallel() - - t.Run("valid signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var authData *AuthPayload - authData, err = createSignature(key, testBodyContents) - require.NoError(t, err) - require.NotNil(t, authData) - - assert.Equal(t, utils.XpubKeyLength, len(authData.xPub)) - assert.Equal(t, 64, len(authData.AuthHash)) - assert.Equal(t, 64, len(authData.AuthNonce)) - assert.Greater(t, authData.AuthTime, time.Now().Add(-1*time.Second).UnixMilli()) - - err = verifyKeyXPub(authData.xPub, &AuthPayload{ - AuthHash: authData.AuthHash, - AuthNonce: authData.AuthNonce, - AuthTime: authData.AuthTime, - BodyContents: testBodyContents, - Signature: authData.Signature, - }) - require.NoError(t, err) - }) - - t.Run("error - missing key", func(t *testing.T) { - authData, err := createSignature(nil, testBodyContents) - require.Error(t, err) - require.Nil(t, authData) - }) - - t.Run("error - empty body - valid signature", func(t *testing.T) { - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var authData *AuthPayload - authData, err = createSignature(key, "") - require.NoError(t, err) - require.NotNil(t, authData) - - assert.Equal(t, utils.XpubKeyLength, len(authData.xPub)) - assert.Equal(t, 64, len(authData.AuthHash)) - assert.Equal(t, 64, len(authData.AuthNonce)) - assert.Greater(t, authData.AuthTime, time.Now().Add(-1*time.Second).UnixMilli()) - - err = verifyKeyXPub(authData.xPub, &AuthPayload{ - AuthHash: authData.AuthHash, - AuthNonce: authData.AuthNonce, - AuthTime: authData.AuthTime, - BodyContents: "", - Signature: authData.Signature, - }) - require.NoError(t, err) - }) -} - -// TestSetSignature will test the method SetSignature() -func TestSetSignature(t *testing.T) { - t.Parallel() - - t.Run("error - bad signature", func(t *testing.T) { - err := SetSignature(nil, nil, testBodyContents) - require.Error(t, err) - }) - - t.Run("valid set headers", func(t *testing.T) { - emptyHeaders := &http.Header{} - - key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) - require.NoError(t, err) - require.NotNil(t, key) - - var xPub string - xPub, err = bitcoin.GetExtendedPublicKey(key) - require.NoError(t, err) - require.NotEmpty(t, xPub) - - err = SetSignature(emptyHeaders, key, testBodyContents) - require.NoError(t, err) - - assert.NotEmpty(t, emptyHeaders.Get(AuthHeader)) - assert.NotEmpty(t, emptyHeaders.Get(AuthHeaderHash)) - assert.NotEmpty(t, emptyHeaders.Get(AuthHeaderNonce)) - assert.NotEmpty(t, emptyHeaders.Get(AuthHeaderTime)) - assert.NotEmpty(t, emptyHeaders.Get(AuthSignature)) - - authTime, _ := strconv.Atoi(emptyHeaders.Get(AuthHeaderTime)) - err = verifyKeyXPub(xPub, &AuthPayload{ - AuthHash: emptyHeaders.Get(AuthHeaderHash), - AuthNonce: emptyHeaders.Get(AuthHeaderNonce), - AuthTime: int64(authTime), - BodyContents: testBodyContents, - Signature: emptyHeaders.Get(AuthSignature), - }) - require.NoError(t, err) - }) -} - -// Test_getSigningMessage will test the method Test_getSigningMessage() -func Test_getSigningMessage(t *testing.T) { - t.Parallel() - - t.Run("valid format", func(t *testing.T) { - message := getSigningMessage(testXpubAuth, &AuthPayload{ - AuthHash: testXpubAuthHash, - AuthNonce: "auth-nonce", - AuthTime: 12345678, - }) - assert.Equal(t, fmt.Sprintf("%s%s%s%d", testXpubAuth, testXpubAuthHash, "auth-nonce", 12345678), message) - }) -} - -// TestGetXpubFromRequest will test the method GetXpubFromRequest() -func TestGetXpubFromRequest(t *testing.T) { - t.Parallel() - - t.Run("valid value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req = setOnRequest(req, ParamXPubKey, testXpubAuth) - - xPub, success := GetXpubFromRequest(req) - assert.Equal(t, testXpubAuth, xPub) - assert.Equal(t, true, success) - }) - - t.Run("no value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - xPub, success := GetXpubFromRequest(req) - assert.Equal(t, "", xPub) - assert.Equal(t, false, success) - }) -} - -// TestIsAdminRequest will test the method IsAdminRequest() -func TestIsAdminRequest(t *testing.T) { - t.Parallel() - - t.Run("no value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - isAdmin, ok := IsAdminRequest(req) - assert.Equal(t, false, ok) - assert.Equal(t, false, isAdmin) - }) - - t.Run("false value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req = setOnRequest(req, ParamAdminRequest, false) - - isAdmin, ok := IsAdminRequest(req) - assert.Equal(t, true, ok) - assert.Equal(t, false, isAdmin) - }) - - t.Run("valid value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req = setOnRequest(req, ParamAdminRequest, true) - - isAdmin, ok := IsAdminRequest(req) - assert.Equal(t, true, ok) - assert.Equal(t, true, isAdmin) - }) -} - -// TestGetXpubHashFromRequest will test the method GetXpubIDFromRequest() -func TestGetXpubIDFromRequest(t *testing.T) { - t.Parallel() - - t.Run("valid value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - req = setOnRequest(req, ParamXPubHashKey, testXpubAuthHash) - - xPubHash, success := GetXpubIDFromRequest(req) - assert.Equal(t, testXpubAuthHash, xPubHash) - assert.Equal(t, true, success) - }) - - t.Run("no value", func(t *testing.T) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) - require.NoError(t, err) - require.NotNil(t, req) - - xPubHash, success := GetXpubIDFromRequest(req) - assert.Equal(t, "", xPubHash) - assert.Equal(t, false, success) - }) -} diff --git a/interface.go b/interface.go index f93509ea..31290779 100644 --- a/interface.go +++ b/interface.go @@ -172,8 +172,7 @@ type ClientInterface interface { TransactionService UTXOService XPubService - AuthenticateRequest(ctx context.Context, req *http.Request, adminXPubs []string, - adminRequired, requireSigning, signingDisabled bool) (*http.Request, error) + AuthenticateAccessKey(ctx context.Context, pubAccessKey string) (*AccessKey, error) Close(ctx context.Context) error Debug(on bool) DefaultSyncConfig() *SyncConfig diff --git a/model_options_test.go b/model_options_test.go index e86dba3f..99d5f63a 100644 --- a/model_options_test.go +++ b/model_options_test.go @@ -6,6 +6,10 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + testEncryption = "35dbe09a941a90a5f59e57020face68860d7b284b7b2973a58de8b4242ec5a925a40ac2933b7e45e78a0b3a13123520e46f9566815589ba2d345577dadee0d5e" +) + // TestNew will test the method New() func TestNew(t *testing.T) { t.Parallel() diff --git a/model_paymail_addresses_test.go b/model_paymail_addresses_test.go index 2fa8529f..1144792d 100644 --- a/model_paymail_addresses_test.go +++ b/model_paymail_addresses_test.go @@ -10,6 +10,10 @@ import ( "github.com/stretchr/testify/require" ) +const ( + testXpubAuth = "xpub661MyMwAqRbcH3WGvLjupmr43L1GVH3MP2WQWvdreDraBeFJy64Xxv4LLX9ZVWWz3ZjZkMuZtSsc9qH9JZR74bR4PWkmtEvP423r6DJR8kA" +) + // TestNewPaymail will test the method newPaymail() func TestNewPaymail(t *testing.T) {