-
Notifications
You must be signed in to change notification settings - Fork 7
/
signer.go
217 lines (184 loc) · 5.28 KB
/
signer.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
package surl
import (
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"hash"
"net/url"
"path"
"strings"
"sync"
"time"
"golang.org/x/crypto/blake2b"
)
var (
// ErrInvalidSignature is returned when the signature is invalid.
ErrInvalidSignature = errors.New("invalid signature")
// ErrInvalidFormat is returned when the format of the signed URL is
// invalid.
ErrInvalidFormat = errors.New("invalid format")
// ErrExpired is returned when a signed URL has expired.
ErrExpired = errors.New("URL has expired")
// Default formatter is the query formatter.
DefaultFormatter = WithQueryFormatter()
// Default expiry encoding is base10 (decimal)
DefaultExpiryFormatter = WithDecimalExpiry()
)
// Signer is capable of signing and verifying signed URLs with an expiry.
type Signer struct {
mu sync.Mutex
hash hash.Hash
dirty bool
prefix string
payloadOptions
formatter
intEncoding
}
// New constructs a new signer, performing the one-off task of generating a
// secure hash from the key. The key must be between 0 and 64 bytes long;
// anything longer is truncated. Options alter the default format and behaviour
// of signed URLs.
func New(key []byte, opts ...Option) *Signer {
hash, err := blake2b.New256(key)
if err != nil {
// Safely ignore one and only error regarding keys longer than 64 bytes.
hash, _ = blake2b.New256(key[0:64])
}
s := &Signer{
hash: hash,
}
DefaultFormatter(s)
DefaultExpiryFormatter(s)
// Leave caller options til last so that they override defaults.
for _, o := range opts {
o(s)
}
return s
}
// Option permits customising the construction of a Signer
type Option func(*Signer)
// SkipQuery instructs Signer to skip the query string when computing the
// signature. This is useful, say, if you have pagination query parameters but
// you want to use the same signed URL regardless of their value.
func SkipQuery() Option {
return func(s *Signer) {
s.skipQuery = true
}
}
// SkipScheme instructs Signer to skip the scheme when computing the signature.
// This is useful, say, if you generate signed URLs in production where you use
// https but you want to use these URLs in development too where you use http.
func SkipScheme() Option {
return func(s *Signer) {
s.skipScheme = true
}
}
// PrefixPath prefixes the signed URL's path with a string. This can make it easier for a server
// to differentiate between signed and non-signed URLs. Note: the prefix is not
// part of the signature computation.
func PrefixPath(prefix string) Option {
return func(s *Signer) {
s.prefix = prefix
}
}
// WithQueryFormatter instructs Signer to use query parameters to store the signature
// and expiry in a signed URL.
func WithQueryFormatter() Option {
return func(s *Signer) {
s.formatter = &queryFormatter{}
}
}
// WithPathFormatter instructs Signer to store the signature and expiry in the
// path of a signed URL.
func WithPathFormatter() Option {
return func(s *Signer) {
s.formatter = &pathFormatter{}
}
}
// WithDecimalExpiry instructs Signer to use base10 to encode the expiry
func WithDecimalExpiry() Option {
return func(s *Signer) {
s.intEncoding = stdIntEncoding(10)
}
}
// WithBase58Expiry instructs Signer to use base58 to encode the expiry
func WithBase58Expiry() Option {
return func(s *Signer) {
s.intEncoding = &base58Encoding{}
}
}
// Sign generates a signed URL with the given lifespan.
func (s *Signer) Sign(unsigned string, lifespan time.Duration) (string, error) {
u, err := url.ParseRequestURI(unsigned)
if err != nil {
return "", err
}
// Add expiry to unsigned URL
expiry := time.Now().Add(lifespan)
encodedExpiry := s.Encode(expiry.Unix())
s.addExpiry(u, encodedExpiry)
// Build payload for signature computation
payload := s.buildPayload(*u, s.payloadOptions)
// Sign payload creating a signature
sig := s.sign([]byte(payload))
// Add signature to url
encodedSig := base64.RawURLEncoding.EncodeToString(sig)
s.addSignature(u, encodedSig)
if s.prefix != "" {
u.Path = path.Join(s.prefix, u.Path)
}
// return signed URL
return u.String(), nil
}
// Verify verifies a signed URL, validating its signature and ensuring it is
// unexpired.
func (s *Signer) Verify(signed string) error {
u, err := url.ParseRequestURI(signed)
if err != nil {
return err
}
if !strings.HasPrefix(u.Path, s.prefix) {
return ErrInvalidFormat
}
u.Path = u.Path[len(s.prefix):]
encodedSig, err := s.extractSignature(u)
if err != nil {
return err
}
sig, err := base64.RawURLEncoding.DecodeString(encodedSig)
if err != nil {
return fmt.Errorf("%w: invalid base64: %s", ErrInvalidSignature, encodedSig)
}
// build the payload for signature computation
payload := s.buildPayload(*u, s.payloadOptions)
// create another signature for comparison and compare
compare := s.sign([]byte(payload))
if subtle.ConstantTimeCompare(sig, compare) != 1 {
return ErrInvalidSignature
}
// get expiry from signed URL
encodedExpiry, err := s.extractExpiry(u)
if err != nil {
return err
}
expiry, err := s.Decode(encodedExpiry)
if err != nil {
return err
}
if time.Now().After(time.Unix(expiry, 0)) {
return ErrExpired
}
// valid, unexpired, signature
return nil
}
func (s *Signer) sign(data []byte) []byte {
s.mu.Lock()
defer s.mu.Unlock()
if s.dirty {
s.hash.Reset()
}
s.dirty = true
s.hash.Write(data)
return s.hash.Sum(nil)
}