-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsignedurls.go
More file actions
225 lines (188 loc) · 5.56 KB
/
signedurls.go
File metadata and controls
225 lines (188 loc) · 5.56 KB
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
package signed
import (
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"fmt"
"hash"
"net/http"
"strconv"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(SignedUrl{})
}
// SignedUrl is a Caddy request matcher that validates signed URLs using HMAC signatures.
//
// The signature is expected to be provided as a query parameter named "signature"
// or as an "X-Signature" header. The URL is considered valid if the signature
// matches the expected value computed using the secret key and the canonical
// URL (path + query string without the signature).
//
// Optionally, an "expires" query parameter can be included to specify a Unix
// timestamp after which the URL is no longer valid.
//
// The signature should be encoded using base64 URL encoding without padding.
//
// Example Caddyfile usage:
// ```
// handle /static/thumbnails/* {
// @signed {
// signed_url {$SIGNED_URL_SECRET}
// }
// handle @signed {
// root /data/files/thumbnails
// uri strip_prefix /static/thumbnails
// file_server
// }
// handle {
// respond "Unauthorized" 401
// }
// }
//
// ```
type SignedUrl struct {
// The secret key used to sign URLs. This should be a strong, random string.
Secret string `json:"secret,omitempty"`
// The hash algorithm to use for signing. Supported values: "sha256" (default), "sha384", "sha512".
Algorithm string `json:"algorithm,omitempty"`
hashFunc func() hash.Hash
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (SignedUrl) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.matchers.signed_url",
New: func() caddy.Module { return new(SignedUrl) },
}
}
func (s *SignedUrl) Provision(ctx caddy.Context) error {
s.logger = ctx.Logger()
// Set hash function
switch s.Algorithm {
case "", "sha256":
s.hashFunc = sha256.New
case "sha384":
s.hashFunc = sha512.New384
case "sha512":
s.hashFunc = sha512.New
default:
return fmt.Errorf("unsupported algorithm: %s", s.Algorithm)
}
return nil
}
func (s *SignedUrl) Validate() error {
if s.Secret == "" {
return fmt.Errorf("secret is required")
}
s.logger.Debug("settings", zap.String("secret", s.Secret), zap.String("alg", s.Algorithm))
return nil
}
func (s *SignedUrl) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
// --- handle single-line shorthand: signed_url "secret" ---
args := d.RemainingArgs()
if len(args) == 1 {
s.Secret = args[0]
} else if len(args) > 1 {
return d.ArgErr()
}
// --- handle block options ---
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "secret":
if !d.NextArg() {
return d.ArgErr()
}
if s.Secret != "" {
return d.Err("secret already configured")
}
s.Secret = d.Val()
case "algorithm":
if !d.NextArg() {
return d.ArgErr()
}
s.Algorithm = d.Val()
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}
}
return nil
}
func (s *SignedUrl) Match(r *http.Request) bool {
match, err := s.MatchWithError(r)
if err != nil {
s.logger.Error("failed to validate signed URL", zap.Error(err))
}
return match
}
func (s *SignedUrl) MatchWithError(r *http.Request) (bool, error) {
query := r.URL.Query()
s.logger.Info("MatchWithError called",
zap.String("path", r.URL.Path),
zap.Any("query", query),
)
sigStr := r.URL.Query().Get("signature")
if sigStr == "" {
sigStr = r.Header.Get("X-Signature")
}
if sigStr == "" {
s.logger.Warn("Missing signature", zap.String("path", r.URL.Path))
return false, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("missing signature"))
}
s.logger.Debug("Signature", zap.String("sigStr", sigStr))
sig, err := base64.RawURLEncoding.DecodeString(sigStr)
if err != nil {
s.logger.Debug("signature decode failed", zap.Error(err))
return false, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("invalid signature encoding"))
}
// Construct canonical URL for signing
// Build canonical path+query string
q := r.URL.Query()
q.Del("signature")
canonical := r.URL.Path
encoded := q.Encode()
// Check expiration
now := time.Now().Unix()
expStr := r.URL.Query().Get("expires")
if expStr != "" {
exp, err := strconv.ParseInt(expStr, 10, 64)
if err != nil {
return false, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("invalid expires param"))
}
if now > exp {
s.logger.Warn("URL expired", zap.String("url", canonical), zap.Int64("expires", exp))
return false, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("URL expired"))
}
}
if encoded != "" {
canonical += "?" + encoded
}
if !s.verifySignature(canonical, sig) {
s.logger.Debug("signature mismatch", zap.String("url", canonical))
return false, caddyhttp.Error(http.StatusForbidden, fmt.Errorf("signature"))
}
return true, nil
}
func (s *SignedUrl) verifySignature(input string, sig []byte) bool {
h := hmac.New(s.hashFunc, []byte(s.Secret))
h.Write([]byte(input))
expectedSig := h.Sum(nil)
s.logger.Debug("verifying sig on input expected signature",
zap.String("input", input),
zap.String("signature", base64.RawURLEncoding.EncodeToString(expectedSig)),
)
return hmac.Equal(sig, expectedSig)
}
var (
_ caddy.Provisioner = (*SignedUrl)(nil)
_ caddy.Module = (*SignedUrl)(nil)
_ caddyhttp.RequestMatcher = (*SignedUrl)(nil)
_ caddyhttp.RequestMatcherWithError = (*SignedUrl)(nil)
_ caddyfile.Unmarshaler = (*SignedUrl)(nil)
)