forked from jbowes/httpsig
-
Notifications
You must be signed in to change notification settings - Fork 0
/
canonicalize.go
410 lines (332 loc) · 13 KB
/
canonicalize.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
// Copyright (c) 2021 James Bowes. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package httpsig
import (
"bytes"
"errors"
"fmt"
"net/http"
nurl "net/url"
"strconv"
"strings"
"time"
)
// Section 2.1 covers canonicalizing headers.
// Section 2.4 step 2 covers using them as input.
func canonicalizeHeader(name string, hdr http.Header) ([]byte, string, error) {
// XXX: Structured headers are not considered, and they should be :)
headerValues := hdr.Values(name)
// 1. Create an ordered list of the field values of each instance of
// the field in the message, in the order that they occur (or will
// occur) in the message.
vc := make([]string, len(headerValues))
for i, val := range headerValues {
// 2. Strip leading and trailing whitespace from each item in the list.
// Note that since HTTP field values are not allowed to contain
// leading and trailing whitespace, this will be a no-op in a
// compliant implementation.
val = strings.TrimSpace(val)
// 3. Remove any obsolete line-folding within the line and replace it
// with a single space (" "), as discussed in Section 5.2 of
// [HTTP1]. Note that this behavior is specific to [HTTP1] and does
// not apply to other versions of the HTTP specification.
vc[i] = strings.ReplaceAll(val, "\n", " ")
}
// 4. Concatenate the list of values together with a single comma (",")
// and a single space (" ") between each item.
name = strings.ToLower(name)
return []byte(fmt.Sprintf("\""+name+"\": %s\n", strings.Join(vc, ", "))), name, nil
}
// The @method derived component refers to the HTTP method of a request
// message. The component value is canonicalized by taking the value of
// the method as a string. Note that the method name is case-sensitive
// as per [HTTP], Section 9.1, and conventionally standardized method
// names are uppercase US-ASCII. If used, the @method component
// identifier MUST occur only once in the covered components.
//
// For example, the following request message:
// POST /path?param=value HTTP/1.1
// Host: www.example.com
// Would result in the following @method component value: "@method": POST
func canonicalizeMethod(method string) []byte {
// Section 2.3.2 covers canonicalization of the method.
// Section 2.4 step 2 covers using it as input.
// Method should always be caps.
return []byte(fmt.Sprintf("\"@method\": %s\n", strings.ToUpper(method)))
}
func canonicalizeAuthority(authority string) []byte {
// Section 2.3.4 covers canonicalization of the authority.
// Section 2.4 step 2 covers using it as input.
// _, err := fmt.Fprintf(out, "\"@authority\": %s\n", authority)
return []byte(fmt.Sprintf("\"@authority\": %s\n", authority))
}
func canonicalizePath(url nurl.URL) []byte {
// Section 2.3.7 covers canonicalization of the path.
// Section 2.4 step 2 covers using it as input.
return []byte(fmt.Sprintf("\"@path\": %s\n", url.Path))
}
// The @query derived component refers to the query component of the
// HTTP request message. The component value is the entire normalized
// query string defined by [RFC3986], including the leading ? character.
// The value is normalized according to the rules in [HTTP],
// Section 4.2.3. Namely, percent-encoded octets are decoded. If used,
// the @query component identifier MUST occur only once in the covered
// components.
//
// For example, the following request message:
//
// POST /path?param=value&foo=bar&baz=batman HTTP/1.1
// Host: www.example.com
//
// Would result in the following @query component value:
// ?param=value&foo=bar&baz=batman
//
// And the following signature base line:
//
// "@query": ?param=value&foo=bar&baz=batman
//
// Section 2.3.8 covers canonicalization of the query.
// Section 2.4 step 2 covers using it as input.
func canonicalizeQuery(url nurl.URL) ([]byte, error) {
// Get query strings without the ?s
rawQuery := url.RawQuery
// If the query string is absent from the request message, the value is
// the leading ? character alone: ?
// Resulting in the following signature base line:
// "@query": ?
rawQuery = "?" + url.RawQuery
// The value is normalized according to the rules in [HTTP], Section 4.2.3.
// Namely, percent-encoded octets are decoded.
query, err := nurl.QueryUnescape(rawQuery)
if err != nil {
return []byte{}, fmt.Errorf("unable to canonicalize query component of the http request. %w", err)
}
return []byte(fmt.Sprintf("\"@query\": %s\n", query)), nil
}
func canonicalizeSignatureParams(sp *signatureParams) []byte {
// Section 2.3.1 covers canonicalization of the signature parameters
return []byte(fmt.Sprintf("\"@signature-params\": %s", sp.normalizeValues()))
}
// HTTP Message Signatures have metadata properties that provide
// information regarding the signature's generation and verification,
// such as the set of covered components, a timestamp, identifiers for
// verification key material, and other utilities.
// The signature parameters component name is @signature-params. This
// message component's value is REQUIRED as part of the signature base
// (Section 2.4) but the component identifier MUST NOT be enumerated
// within the set of covered components itself.
// The signature parameters component value is the serialization of the
// signature parameters for this signature, including the covered
// components set with all associated parameters. These parameters
// include any of the following:
// * created: Creation time as an Integer UNIX timestamp value. Sub-
// second precision is not supported. Inclusion of this parameter is
// RECOMMENDED.
// * expires: Expiration time as an Integer UNIX timestamp value. Sub-
// second precision is not supported.
// * nonce: A random unique value generated for this signature as a
// String value.
// * alg: The HTTP message signature algorithm from the HTTP Message
// Signature Algorithm Registry, as a String value.
// * keyid: The identifier for the key material as a String value.
// Additional parameters can be defined in the HTTP Signature Parameters
// Registry (Section 6.2.2).
type signatureParams struct {
id string
coveredComponents []string
// The identifier for the key material as a String value.
keyID *string
// The HTTP message signature algorithm from the HTTP Message Signature Algorithm Registry, as a String value.
alg *string
//Creation time as an Integer UNIX timestamp value.
//Sub-second precision is not supported. Inclusion of this parameter is RECOMMENDED.
created *time.Time
// Expiration time as an Integer UNIX timestamp value. Sub-second precision is not supported.
expires *time.Time
// A random unique value generated for this signature as a String value.
nonce *string
}
// Cannonicalized the values but not the entire thing
// This allows this func to be used to build the header
func (sp signatureParams) normalizeValues() []byte {
components := make([]string, len(sp.coveredComponents))
// Transform all coverend components to lowercase and
// wrap each component with ""
// Do not separate with a comma
for i, component := range sp.coveredComponents {
components[i] = fmt.Sprintf("\"%s\"", strings.ToLower(component))
}
// Each CC must be separated by a single white space
// EX: ("@target-uri" "@authority" "date" "cache-control")
sigParams := fmt.Sprintf("(%s)", strings.Join(components, " "))
if sp.created != nil {
sigParams += fmt.Sprintf(";created=%d", time.Now().Unix())
}
if sp.expires != nil {
sigParams += fmt.Sprintf(";expires=%d", sp.expires.Unix())
}
if sp.keyID != nil {
sigParams += fmt.Sprintf(";keyid=%s", *sp.keyID)
}
if sp.alg != nil {
sigParams += fmt.Sprintf(";alg=%s", *sp.alg)
}
if sp.nonce != nil {
sigParams += fmt.Sprintf(";nonce=%s", *sp.nonce)
}
return []byte(sigParams)
}
var errMalformedSignatureInput = errors.New("malformed signature-input header")
func parseStringComponent(component string) (string, error) {
n := len(component)
if n < 2 {
return component, errMalformedSignatureInput
}
if component[0] != '"' && component[n - 1] != '"' {
return component, errMalformedSignatureInput
}
return strings.Trim(component, `"`), nil
}
func parseSignatureParams(in string) (signatureParams, error) {
sp := signatureParams{}
// Seperate components from associated parameters
// Components will be strings delimeted by a single whitespace inside ()
// Right now there are only ["created", "expires", "nonce", "alg", "keyid"]
// The associated parameters will be appended to the end of the string delimited by ';'
//
// EX: ["("@method" "@path" "@query" "authorization" "content-type" "content-digest")", "created=1657133676", 'nonce="foo"']
parts := strings.Split(in, ";")
if len(parts) < 1 {
// Associated parameters are optional at the bare minimum the string will consist of
// No covered components: ()
return sp, errMalformedSignatureInput
}
componentStr := parts[0]
// The Covered Components MUST be encapsulated by ()
if componentStr[0] != '(' || componentStr[len(parts[0])-1] != ')' {
return sp, errMalformedSignatureInput
}
// Strip leading and trailing parenthesis:
// '"@method" "@path" "@query" "authorization" "content-type" "content-digest"'
componentStr = componentStr[1:len(componentStr)-1]
// Components will be delimeted by string
// ["@method", "@path", "@query", "authorization", "content-type", "content-digest"]
components := strings.Split(parts[0][1:len(parts[0])-1], " ")
// Now that we have an approximate length
sp.coveredComponents = make([]string, len(components))
// Components are not required, it is acceptible to have none
// Otherwise validate and normalize component
for i, component := range components {
n := len(component)
if n < 2 {
return sp, errMalformedSignatureInput
}
if component[0] != '"' && component[n - 1] != '"' {
return sp, errMalformedSignatureInput
}
sp.coveredComponents[i] = strings.Trim(component, `"`)
}
// Index 1 through n will be the associated parameters
// They are not required so it is acceptable to have none
// EX: ["created=1657133676", 'nonce="foo"']]
for _, param := range parts[1:] {
// Parse key and value
// ["created", "1657133676"]
paramParts := strings.Split(param, "=")
if len(paramParts) != 2 {
return sp, errMalformedSignatureInput
}
var covParam string
var err error
// TODO: error when not wrapped in quotes
switch paramParts[0] {
case "alg":
*sp.alg, err = parseStringComponent(paramParts[1])
if err != nil {
return sp, err
}
case "keyid":
covParam = strings.Trim(paramParts[1], `"`)
sp.keyID = &covParam
case "nonce":
covParam = strings.Trim(paramParts[1], `"`)
sp.nonce = &covParam
case "created":
i, err := strconv.ParseInt(paramParts[1], 10, 64)
if err != nil {
return sp, errMalformedSignatureInput
}
t := time.Unix(i, 0)
sp.created = &t
case "expires":
i, err := strconv.ParseInt(paramParts[1], 10, 64)
if err != nil {
return sp, errMalformedSignatureInput
}
t := time.Unix(i, 0)
sp.expires = &t
default:
// TODO: unknown params could be kept? hard to say.
return sp, errMalformedSignatureInput
}
}
return sp, nil
}
func canonicalizeDerivedComponent(component string, msg message) ([]byte, error) {
var err error
var value []byte
switch component {
case "@method":
value = canonicalizeMethod(msg.Method)
case "@path":
value = canonicalizePath(*msg.URL)
case "@query":
value, err = canonicalizeQuery(*msg.URL)
case "@authority":
value = canonicalizeAuthority(msg.Authority)
default:
return value, fmt.Errorf("unsupported derived component %v", component)
}
if err != nil {
return value, fmt.Errorf("issue deriving component %s, %w", component, err)
}
return value, nil
}
func cannonicalizeDictionary(name string, headers http.Header) ([]byte, string, error) {
builder := bytes.Buffer{}
values := headers.Values(name)
headerName := strings.ToLower(name)
if len(values) != 1 {
return nil, builder.String(), fmt.Errorf("")
}
// Dictionaries will be in the form:
// Example-Dict: a=1, b=2;x=1;y=2, c=(a b c), d
rawDict := values[0]
// Split the dictionary by key/value pair
// Each kv pair is delimited by comma
// Resulting: ["a=1", "b=2;x=1;y=2", "c=(a b c)", "d"]
members := strings.Split(rawDict, ",")
// Cannonicalize each memeber of the dictionary into the buff
for _, member := range members {
// Parse key/value pair
// EX: " a=1 "
// Remove excess white space: "a=1"
// Parse key and value into array ["a", "1"]
pair := strings.Split(strings.TrimSpace(member), "=")
if len(pair) < 2 {
return builder.Bytes(), headerName, fmt.Errorf(
"invalid dictionary member %s",
member,
)
}
key := strings.TrimSpace(pair[0])
// TODO: Remove all excess whitespace
// spec references "strict member_value algorithm."
// However, I am not able to find any ref to it
value := strings.TrimSpace(pair[1])
// Write to the buffer
fmt.Fprintf(&builder, "\"%s\";key=\"%s\": %s\n", headerName, key, value)
}
return builder.Bytes(), headerName, nil
}