-
Notifications
You must be signed in to change notification settings - Fork 283
/
signed.go
129 lines (110 loc) · 3.12 KB
/
signed.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
package urlutil
import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"time"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
// SignedURL is a shared-key HMAC wrapped URL.
type SignedURL struct {
uri url.URL
key []byte
signed bool
// mockable time for testing
timeNow func() time.Time
}
// NewSignedURL creates a new copy of a URL that can be signed with a shared key.
//
// N.B. It is the user's responsibility to make sure the key is 256 bits and the url is not nil.
func NewSignedURL(key []byte, uri *url.URL) *SignedURL {
return &SignedURL{uri: *uri, key: key, timeNow: time.Now} // uri is copied
}
// Sign creates a shared-key HMAC signed URL.
func (su *SignedURL) Sign() *url.URL {
now := su.timeNow()
issued := newNumericDate(now)
expiry := newNumericDate(now.Add(5 * time.Minute))
params := su.uri.Query()
params.Set(QueryHmacIssued, fmt.Sprint(issued))
params.Set(QueryHmacExpiry, fmt.Sprint(expiry))
su.uri.RawQuery = params.Encode()
params.Set(QueryHmacSignature, hmacURL(su.key, su.uri.String(), issued, expiry))
su.uri.RawQuery = params.Encode()
su.signed = true
return &su.uri
}
// String implements the stringer interface and returns a signed URL string.
func (su *SignedURL) String() string {
if !su.signed {
su.Sign()
su.signed = true
}
return su.uri.String()
}
// Validate checks to see if a signed URL is valid.
func (su *SignedURL) Validate() error {
now := su.timeNow()
params := su.uri.Query()
sig, err := base64.URLEncoding.DecodeString(params.Get(QueryHmacSignature))
if err != nil {
return fmt.Errorf("internal/urlutil: malformed signature %w", err)
}
params.Del(QueryHmacSignature)
su.uri.RawQuery = params.Encode()
issued, err := newNumericDateFromString(params.Get(QueryHmacIssued))
if err != nil {
return err
}
expiry, err := newNumericDateFromString(params.Get(QueryHmacExpiry))
if err != nil {
return err
}
if expiry != nil && now.Add(-DefaultLeeway).After(expiry.Time()) {
return ErrExpired
}
if issued != nil && now.Add(DefaultLeeway).Before(issued.Time()) {
return ErrIssuedInTheFuture
}
validHMAC := cryptutil.CheckHMAC(
[]byte(fmt.Sprint(su.uri.String(), issued, expiry)),
sig,
su.key)
if !validHMAC {
return fmt.Errorf("internal/urlutil: hmac failed")
}
return nil
}
// hmacURL takes a redirect url string and timestamp and returns the base64
// encoded HMAC result.
func hmacURL(key []byte, data ...interface{}) string {
h := cryptutil.GenerateHMAC([]byte(fmt.Sprint(data...)), key)
return base64.URLEncoding.EncodeToString(h)
}
// numericDate used because we don't need the precision of a typical time.Time.
type numericDate int64
func newNumericDate(t time.Time) *numericDate {
if t.IsZero() {
return nil
}
out := numericDate(t.Unix())
return &out
}
func newNumericDateFromString(s string) (*numericDate, error) {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, ErrNumericDateMalformed
}
out := numericDate(i)
return &out, nil
}
func (n *numericDate) Time() time.Time {
if n == nil {
return time.Time{}
}
return time.Unix(int64(*n), 0)
}
func (n *numericDate) String() string {
return strconv.FormatInt(int64(*n), 10)
}