This repository has been archived by the owner on Apr 2, 2024. It is now read-only.
generated from mrz1836/go-template
-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
authentication.go
282 lines (238 loc) · 7.82 KB
/
authentication.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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...)
if err != nil {
return err
} else if accessKey == nil {
return 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
}
// 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)
}