This repository has been archived by the owner on Dec 31, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27
/
hawk.go
691 lines (608 loc) · 18.8 KB
/
hawk.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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
// Package hawk implements the Hawk HTTP authentication scheme.
package hawk
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"encoding/base64"
"fmt"
"hash"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
// Now is a func() time.Time that is used by the package to get the current time.
var Now = time.Now
// MaxTimestampSkew is the maximum ±skew that a request timestamp can have without returning ErrTimestampSkew.
var MaxTimestampSkew = time.Minute
var (
ErrNoAuth = AuthError("no Authorization header or bewit parameter found")
ErrReplay = AuthError("request nonce is being replayed")
ErrInvalidMAC = AuthError("invalid MAC")
ErrBewitExpired = AuthError("bewit expired")
ErrTimestampSkew = AuthError("timestamp skew too high")
ErrMissingServerAuth = AuthError("missing Server-Authentication header")
ErrInvalidBewitMethod = AuthError("bewit only allows HEAD and GET requests")
)
type AuthError string
func (e AuthError) Error() string { return "hawk: " + string(e) }
type CredentialErrorType int
const (
UnknownID CredentialErrorType = iota
UnknownApp
IDAppMismatch
)
func (t CredentialErrorType) String() string {
switch t {
case UnknownApp:
return "unknown app"
case IDAppMismatch:
return "id/app mismatch"
}
return "unknown id"
}
// CredentialError is returned by a CredentialsLookupFunc when the provided credentials
// ID is invalid.
type CredentialError struct {
Type CredentialErrorType
Credentials *Credentials
}
func (e *CredentialError) Error() string {
return fmt.Sprintf("hawk: credential error with id %s and app %s: %s", e.Credentials.ID, e.Credentials.App, e.Type)
}
type Credentials struct {
ID string
Key string
Hash func() hash.Hash
// Data may be set in a CredentialsLookupFunc to correlate the credentials
// with an internal data record.
Data interface{}
App string
Delegate string
}
func (creds *Credentials) MAC() hash.Hash { return hmac.New(creds.Hash, []byte(creds.Key)) }
type AuthType int
const (
AuthHeader AuthType = iota
AuthResponse
AuthBewit
)
func (a AuthType) String() string {
switch a {
case AuthResponse:
return "response"
case AuthBewit:
return "bewit"
default:
return "header"
}
}
// A CredentialsLookupFunc is called by NewAuthFromRequest after parsing the
// request auth. The Credentials will never be nil and ID will always be set.
// App and Delegate will be set if provided in the request. This function must
// look up the corresponding Key and Hash and set them on the provided
// Credentials. If the Key/Hash are found and the App/Delegate are valid (if
// provided) the error should be nil. If the Key or App could not be found or
// the App does not match the ID, then a CredentialError must be returned.
// Errors will propagate to the caller of NewAuthFromRequest, so internal errors
// may be returned.
type CredentialsLookupFunc func(*Credentials) error
// A NonceCheckFunc is called by NewAuthFromRequest and should make sure that
// the provided nonce is unique within the context of the provided time.Time and
// Credentials. It should return false if the nonce is being replayed.
type NonceCheckFunc func(string, time.Time, *Credentials) bool
type AuthFormatError struct {
Field string
Err string
}
func (e AuthFormatError) Error() string { return "hawk: invalid " + e.Field + ", " + e.Err }
// ParseRequestHeader parses a Hawk header (provided in the Authorization
// HTTP header) and populates an Auth. If an error is returned it will always be
// of type AuthFormatError.
func ParseRequestHeader(header string) (*Auth, error) {
auth := &Auth{ActualTimestamp: Now()}
err := auth.ParseHeader(header, AuthHeader)
if err != nil {
return nil, err
}
if auth.Credentials.ID == "" {
return nil, AuthFormatError{"id", "missing or empty"}
}
if auth.Timestamp.IsZero() {
return nil, AuthFormatError{"ts", "missing, empty, or zero"}
}
if auth.Nonce == "" {
return nil, AuthFormatError{"nonce", "missing or empty"}
}
auth.ReqHash = true
return auth, nil
}
// ParseBewit parses a bewit token provided in a URL parameter and populates an
// Auth. If an error is returned it will always be of type AuthFormatError.
func ParseBewit(bewit string) (*Auth, error) {
if len(bewit)%4 != 0 {
bewit += strings.Repeat("=", 4-len(bewit)%4)
}
decoded, err := base64.URLEncoding.DecodeString(bewit)
if err != nil {
return nil, AuthFormatError{"bewit", "malformed base64 encoding"}
}
components := bytes.SplitN(decoded, []byte(`\`), 4)
if len(components) != 4 {
return nil, AuthFormatError{"bewit", "missing components"}
}
auth := &Auth{
Credentials: Credentials{ID: string(components[0])},
Ext: string(components[3]),
Method: "GET",
ActualTimestamp: Now(),
IsBewit: true,
}
ts, err := strconv.ParseInt(string(components[1]), 10, 64)
if err != nil {
return nil, AuthFormatError{"ts", "not an integer"}
}
auth.Timestamp = time.Unix(ts, 0)
auth.MAC = make([]byte, base64.StdEncoding.DecodedLen(len(components[2])))
n, err := base64.StdEncoding.Decode(auth.MAC, components[2])
if err != nil {
return nil, AuthFormatError{"mac", "malformed base64 encoding"}
}
auth.MAC = auth.MAC[:n]
return auth, nil
}
// NewAuthFromRequest parses a request containing an Authorization header or
// bewit parameter and populates an Auth. If creds is not nil it will be called
// to look up the associated credentials. If nonce is not nil it will be called
// to make sure the nonce is not replayed.
//
// If the request does not contain a bewit or Authorization header, ErrNoAuth is
// returned. If the request contains a bewit and it is not a GET or HEAD
// request, ErrInvalidBewitMethod is returned. If there is an error parsing the
// provided auth details, an AuthFormatError will be returned. If creds returns
// an error, it will be returned. If nonce returns false, ErrReplay will be
// returned.
func NewAuthFromRequest(req *http.Request, creds CredentialsLookupFunc, nonce NonceCheckFunc) (*Auth, error) {
header := req.Header.Get("Authorization")
bewit := req.URL.Query().Get("bewit")
var auth *Auth
var err error
if header != "" {
auth, err = ParseRequestHeader(header)
if err != nil {
return nil, err
}
}
if auth == nil && bewit != "" {
if req.Method != "GET" && req.Method != "HEAD" {
return nil, ErrInvalidBewitMethod
}
auth, err = ParseBewit(bewit)
if err != nil {
return nil, err
}
}
if auth == nil {
return nil, ErrNoAuth
}
auth.Method = req.Method
auth.RequestURI = req.URL.Path
if req.URL.RawQuery != "" {
auth.RequestURI += "?" + req.URL.RawQuery
}
if bewit != "" {
auth.Method = "GET"
bewitPattern, _ := regexp.Compile(`\?bewit=` + bewit + `\z|bewit=` + bewit + `&|&bewit=` + bewit + `\z`)
auth.RequestURI = bewitPattern.ReplaceAllString(auth.RequestURI, "")
}
auth.Host, auth.Port = extractReqHostPort(req)
if creds != nil {
err = creds(&auth.Credentials)
if err != nil {
return nil, err
}
}
if nonce != nil && !auth.IsBewit && !nonce(auth.Nonce, auth.Timestamp, &auth.Credentials) {
return nil, ErrReplay
}
return auth, nil
}
func extractReqHostPort(req *http.Request) (host string, port string) {
if idx := strings.Index(req.Host, ":"); idx != -1 {
host, port, _ = net.SplitHostPort(req.Host)
} else {
host = req.Host
}
if req.URL.Host != "" {
if idx := strings.Index(req.Host, ":"); idx != -1 {
host, port, _ = net.SplitHostPort(req.Host)
} else {
host = req.URL.Host
}
}
if port == "" {
if req.URL.Scheme == "http" {
port = "80"
} else {
port = "443"
}
}
return
}
// NewRequestAuth builds a client Auth based on req and creds. tsOffset will be
// applied to Now when setting the timestamp.
func NewRequestAuth(req *http.Request, creds *Credentials, tsOffset time.Duration) *Auth {
auth := &Auth{
Method: req.Method,
Credentials: *creds,
Timestamp: Now().Add(tsOffset),
Nonce: nonce(),
RequestURI: req.URL.RequestURI(),
}
auth.Host, auth.Port = extractReqHostPort(req)
return auth
}
// NewRequestAuth builds a client Auth based on uri and creds. tsOffset will be
// applied to Now when setting the timestamp.
func NewURLAuth(uri string, creds *Credentials, tsOffset time.Duration) (*Auth, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
auth := &Auth{
Method: "GET",
Credentials: *creds,
Timestamp: Now().Add(tsOffset),
}
if u.Path != "" {
// url.Parse unescapes the path, which is unexpected
auth.RequestURI = "/" + strings.SplitN(uri[8:], "/", 2)[1]
} else {
auth.RequestURI = "/"
}
auth.Host, auth.Port = extractURLHostPort(u)
return auth, nil
}
func extractURLHostPort(u *url.URL) (host string, port string) {
if idx := strings.Index(u.Host, ":"); idx != -1 {
host, port, _ = net.SplitHostPort(u.Host)
} else {
host = u.Host
}
if port == "" {
if u.Scheme == "http" {
port = "80"
} else {
port = "443"
}
}
return
}
func nonce() string {
b := make([]byte, 8)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(b)[:8]
}
const headerVersion = "1"
type Auth struct {
Credentials Credentials
Method string
RequestURI string
Host string
Port string
MAC []byte
Nonce string
Ext string
Hash []byte
// ReqHash is true if the request contained a hash
ReqHash bool
IsBewit bool
Timestamp time.Time
// ActualTimestamp is when the request was received
ActualTimestamp time.Time
}
// field is of form: key="value"
func lexField(r *strings.Reader) (string, string, error) {
key := make([]byte, 0, 5)
val := make([]byte, 0, 32)
// read the key
for {
ch, _ := r.ReadByte()
if ch == '=' {
break
}
if ch < 'a' || ch > 'z' { // fail if not ASCII lowercase letter
return "", "", AuthFormatError{"header", "cannot parse header field"}
}
key = append(key, ch)
}
if ch, _ := r.ReadByte(); ch != '"' {
return "", "", AuthFormatError{string(key), "cannot parse value"}
}
// read the value
for {
ch, _ := r.ReadByte()
if ch == '"' {
break
}
// character class is ASCII printable [\x20-\x7E] without \ and "
if ch < 0x20 || ch > 0x7E || ch == '\\' {
return "", "", AuthFormatError{string(key), "cannot parse value"}
}
val = append(val, ch)
}
return string(key), string(val), nil
}
func lexHeader(header string) (map[string]string, error) {
params := make(map[string]string, 8)
r := strings.NewReader(header)
for {
ch, eof := r.ReadByte()
if eof != nil {
break
}
switch {
case ch == ' ' || ch == '\t' || ch == ',': //ignore spaces and commas
case ch >= 'a' && ch <= 'z': //beginning of key/value pair like 'id="abcdefg"'
r.UnreadByte()
key, val, err := lexField(r)
if err != nil {
return params, err
}
params[key] = val
default: //invalid character encountered
return params, AuthFormatError{"header", "cannot parse header"}
}
}
return params, nil
}
// ParseHeader parses a Hawk request or response header and populates auth.
// t must be AuthHeader if the header is an Authorization header from a request
// or AuthResponse if the header is a Server-Authorization header from
// a response.
func (auth *Auth) ParseHeader(header string, t AuthType) error {
if len(header) < 4 || strings.ToLower(header[:4]) != "hawk" {
return AuthFormatError{"scheme", "must be Hawk"}
}
fields, err := lexHeader(header[4:])
if err != nil {
return err
}
if hash, ok := fields["hash"]; ok {
auth.Hash, err = base64.StdEncoding.DecodeString(hash)
if err != nil {
return AuthFormatError{"hash", "malformed base64 encoding"}
}
}
auth.Ext = fields["ext"]
mac := fields["mac"]
if mac == "" {
return AuthFormatError{"mac", "missing or empty"}
}
auth.MAC, err = base64.StdEncoding.DecodeString(mac)
if err != nil {
return AuthFormatError{"mac", "malformed base64 encoding"}
}
if t == AuthHeader {
auth.Credentials.App = fields["app"]
auth.Credentials.Delegate = fields["dlg"]
auth.Credentials.ID = fields["id"]
if ts, ok := fields["ts"]; ok {
tsint, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return AuthFormatError{"ts", "not an integer"}
}
auth.Timestamp = time.Unix(tsint, 0)
}
auth.Nonce = fields["nonce"]
}
return nil
}
// Valid confirms that the timestamp is within skew and verifies the MAC.
//
// If the request is valid, nil will be returned. If auth is a bewit and the
// method is not GET or HEAD, ErrInvalidBewitMethod will be returned. If auth is
// a bewit and the timestamp is after the the specified expiry, ErrBewitExpired
// will be returned. If auth is from a request header and the timestamp is
// outside the maximum skew, ErrTimestampSkew will be returned. If the MAC is
// not the expected value, ErrInvalidMAC will be returned.
func (auth *Auth) Valid() error {
t := AuthHeader
if auth.IsBewit {
t = AuthBewit
if auth.Method != "GET" && auth.Method != "HEAD" {
return ErrInvalidBewitMethod
}
if auth.ActualTimestamp.After(auth.Timestamp) {
return ErrBewitExpired
}
} else {
skew := auth.ActualTimestamp.Sub(auth.Timestamp)
if abs(skew) > MaxTimestampSkew {
return ErrTimestampSkew
}
}
if !hmac.Equal(auth.mac(t), auth.MAC) {
if auth.IsBewit && strings.HasPrefix(auth.RequestURI, "http") && len(auth.RequestURI) > 9 {
// try just the path
uri := auth.RequestURI
auth.RequestURI = "/" + strings.SplitN(auth.RequestURI[8:], "/", 2)[1]
if auth.Valid() == nil {
return nil
}
auth.RequestURI = uri
}
return ErrInvalidMAC
}
return nil
}
func abs(d time.Duration) time.Duration {
if d < 0 {
return -d
}
return d
}
// ValidResponse checks that a response Server-Authorization header is correct.
//
// ErrMissingServerAuth is returned if header is an empty string. ErrInvalidMAC
// is returned if the MAC is not the expected value.
func (auth *Auth) ValidResponse(header string) error {
if header == "" {
return ErrMissingServerAuth
}
err := auth.ParseHeader(header, AuthResponse)
if err != nil {
return err
}
if !hmac.Equal(auth.mac(AuthResponse), auth.MAC) {
return ErrInvalidMAC
}
return nil
}
// PayloadHash initializes a hash for body validation. To validate a request or
// response body, call PayloadHash with contentType set to the body Content-Type
// with all parameters and prefix/suffix whitespace stripped, write the entire
// body to the returned hash, and then validate the hash with ValidHash.
func (auth *Auth) PayloadHash(contentType string) hash.Hash {
h := auth.Credentials.Hash()
h.Write([]byte("hawk." + headerVersion + ".payload\n" + contentType + "\n"))
return h
}
// ValidHash writes the final newline to h and checks if it matches auth.Hash.
func (auth *Auth) ValidHash(h hash.Hash) bool {
h.Write([]byte("\n"))
return bytes.Equal(h.Sum(nil), auth.Hash)
}
// SetHash writes the final newline to h and sets auth.Hash to the sum. This is
// used to specify a response payload hash.
func (auth *Auth) SetHash(h hash.Hash) {
h.Write([]byte("\n"))
auth.Hash = h.Sum(nil)
auth.ReqHash = false
}
// ResponseHeader builds a response header based on the auth and provided ext,
// which may be an empty string. Use PayloadHash and SetHash before
// ResponseHeader to include a hash of the response payload.
func (auth *Auth) ResponseHeader(ext string) string {
auth.Ext = ext
if auth.ReqHash {
auth.Hash = nil
}
h := `Hawk mac="` + base64.StdEncoding.EncodeToString(auth.mac(AuthResponse)) + `"`
if auth.Ext != "" {
h += `, ext="` + auth.Ext + `"`
}
if auth.Hash != nil {
h += `, hash="` + base64.StdEncoding.EncodeToString(auth.Hash) + `"`
}
return h
}
// RequestHeader builds a request header based on the auth.
func (auth *Auth) RequestHeader() string {
auth.MAC = auth.mac(AuthHeader)
h := `Hawk id="` + auth.Credentials.ID +
`", mac="` + base64.StdEncoding.EncodeToString(auth.MAC) +
`", ts="` + strconv.FormatInt(auth.Timestamp.Unix(), 10) +
`", nonce="` + auth.Nonce + `"`
if len(auth.Hash) > 0 {
h += `, hash="` + base64.StdEncoding.EncodeToString(auth.Hash) + `"`
}
if auth.Ext != "" {
h += `, ext="` + auth.Ext + `"`
}
if auth.Credentials.App != "" {
h += `, app="` + auth.Credentials.App + `"`
}
if auth.Credentials.Delegate != "" {
h += `, dlg="` + auth.Credentials.Delegate + `"`
}
return h
}
// Bewit creates and encoded request bewit parameter based on the auth.
func (auth *Auth) Bewit() string {
auth.Method = "GET"
auth.Nonce = ""
return strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(auth.Credentials.ID+`\`+
strconv.FormatInt(auth.Timestamp.Unix(), 10)+`\`+
base64.StdEncoding.EncodeToString(auth.mac(AuthBewit))+`\`+
auth.Ext)), "=")
}
// NormalizedString builds the string that will be HMACed to create a request
// MAC.
func (auth *Auth) NormalizedString(t AuthType) string {
str := "hawk." + headerVersion + "." + t.String() + "\n" +
strconv.FormatInt(auth.Timestamp.Unix(), 10) + "\n" +
auth.Nonce + "\n" +
auth.Method + "\n" +
auth.RequestURI + "\n" +
auth.Host + "\n" +
auth.Port + "\n" +
base64.StdEncoding.EncodeToString(auth.Hash) + "\n" +
auth.Ext + "\n"
if auth.Credentials.App != "" {
str += auth.Credentials.App + "\n"
str += auth.Credentials.Delegate + "\n"
}
return str
}
func (auth *Auth) mac(t AuthType) []byte {
mac := auth.Credentials.MAC()
mac.Write([]byte(auth.NormalizedString(t)))
return mac.Sum(nil)
}
func (auth *Auth) tsMac(ts string) []byte {
mac := auth.Credentials.MAC()
mac.Write([]byte("hawk." + headerVersion + ".ts\n" + ts + "\n"))
return mac.Sum(nil)
}
// StaleTimestampHeader builds a signed WWW-Authenticate response header for use
// when Valid returns ErrTimestampSkew.
func (auth *Auth) StaleTimestampHeader() string {
ts := strconv.FormatInt(Now().Unix(), 10)
return `Hawk ts="` + ts +
`", tsm="` + base64.StdEncoding.EncodeToString(auth.tsMac(ts)) +
`", error="Stale timestamp"`
}
var tsHeaderRegex = regexp.MustCompile(`(ts|tsm|error)="([ !#-\[\]-~]+)"`) // character class is ASCII printable [\x20-\x7E] without \ and "
// UpdateOffset parses a signed WWW-Authenticate response header containing
// a stale timestamp error and updates auth.Timestamp with an adjusted
// timestamp.
func (auth *Auth) UpdateOffset(header string) (time.Duration, error) {
if len(header) < 4 || strings.ToLower(header[:4]) != "hawk" {
return 0, AuthFormatError{"scheme", "must be Hawk"}
}
matches := tsHeaderRegex.FindAllStringSubmatch(header, 3)
var err error
var ts time.Time
var tsm []byte
for _, match := range matches {
switch match[1] {
case "ts":
t, err := strconv.ParseInt(match[2], 10, 64)
if err != nil {
return 0, AuthFormatError{"ts", "not an integer"}
}
ts = time.Unix(t, 0)
case "tsm":
tsm, err = base64.StdEncoding.DecodeString(match[2])
if err != nil {
return 0, AuthFormatError{"tsm", "malformed base64 encoding"}
}
}
}
if !hmac.Equal(tsm, auth.tsMac(strconv.FormatInt(ts.Unix(), 10))) {
return 0, ErrInvalidMAC
}
offset := ts.Sub(Now())
auth.Timestamp = ts
auth.Nonce = nonce()
return offset, nil
}