Skip to content

Commit 0dc2f97

Browse files
committed
feat: canonical format fixes and test coverage
1 parent 9d5fb50 commit 0dc2f97

File tree

6 files changed

+2434
-804
lines changed

6 files changed

+2434
-804
lines changed

base.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import (
4343

4444
type signatureItem struct {
4545
key httpsfv.Item
46-
value httpsfv.StructuredFieldValue
46+
value []string
4747
}
4848

4949
func createSigningParameters(config *SignConfig) *httpsfv.Params {
@@ -228,7 +228,7 @@ func createSignatureBase(fields []string, msg *Message) ([]signatureItem, error)
228228
lcName := strings.ToLower(field.Value.(string))
229229

230230
if lcName != "@signature-params" {
231-
var value httpsfv.StructuredFieldValue
231+
var value []string
232232
if strings.HasPrefix(lcName, "@") {
233233
value, err = canonicaliseComponent(lcName, params, msg)
234234
} else {

canonicalise.go

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ package httpsig
3333

3434
import (
3535
"context"
36-
"encoding/base64"
3736
"errors"
3837
"fmt"
38+
"net"
3939
"net/http"
4040
"net/url"
41+
"regexp"
42+
"strconv"
4143
"strings"
4244

4345
"github.com/dunglas/httpsfv"
@@ -100,7 +102,7 @@ func parseHeader(values []string) (httpsfv.StructuredFieldValue, error) {
100102
return nil, errors.New("unable to parse structured header")
101103
}
102104

103-
func canonicaliseComponent(component string, params *httpsfv.Params, message *Message) (httpsfv.StructuredFieldValue, error) {
105+
func canonicaliseComponent(component string, params *httpsfv.Params, message *Message) ([]string, error) {
104106
_, isReq := params.Get("req")
105107
switch component {
106108
case "@method":
@@ -110,56 +112,71 @@ func canonicaliseComponent(component string, params *httpsfv.Params, message *Me
110112
if !message.IsRequest && !isReq {
111113
return nil, errors.New("method component not valid for responses")
112114
}
113-
return httpsfv.NewItem(strings.ToUpper(message.Method)), nil
115+
return []string{strings.ToUpper(message.Method)}, nil
114116
case "@target-uri":
115117
// Section 2.2.2 covers canonicalisation of the target-uri.
116118
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-target-uri
117119
if !message.IsRequest && !isReq {
118120
return nil, errors.New("target-uri component not valid for responses")
119121
}
120-
return httpsfv.NewItem(message.URL.String()), nil
122+
return []string{message.URL.String()}, nil
121123
case "@authority":
122124
// Section 2.2.3 covers canonicalisation of the target-uri.
123125
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-authority
124126
if !message.IsRequest && !isReq {
125127
return nil, errors.New("authority component not valid for responses")
126128
}
127-
return httpsfv.NewItem(message.Authority), nil
129+
host, port, err := net.SplitHostPort(message.Authority)
130+
if err != nil {
131+
// no port, just use the whole thing
132+
return []string{strings.ToLower(message.Authority)}, nil
133+
}
134+
switch strings.ToLower(message.URL.Scheme) {
135+
case "http":
136+
if port == "80" {
137+
return []string{strings.ToLower(host)}, nil
138+
}
139+
case "https":
140+
if port == "443" {
141+
return []string{strings.ToLower(host)}, nil
142+
}
143+
}
144+
return []string{strings.ToLower(message.Authority)}, nil
128145
case "@scheme":
129146
// Section 2.2.4 covers canonicalisation of the scheme.
130147
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-scheme
131148
// Scheme should always be lowercase.
132149
if !message.IsRequest && !isReq {
133150
return nil, errors.New("scheme component not valid for responses")
134151
}
135-
return httpsfv.NewItem(strings.ToLower(message.URL.Scheme)), nil
152+
return []string{strings.ToLower(message.URL.Scheme)}, nil
136153
case "@request-target":
137154
// Section 2.2.5 covers canonicalisation of the request-target.
138155
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-request-target
139156
if !message.IsRequest && !isReq {
140157
return nil, errors.New("request-target component not valid for responses")
141158
}
142-
return httpsfv.NewItem(message.URL.RequestURI()), nil
159+
return []string{message.URL.RequestURI()}, nil
143160
case "@path":
144161
// Section 2.2.6 covers canonicalisation of the path.
145162
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-path
146163
if !message.IsRequest && !isReq {
147164
return nil, errors.New("path component not valid for responses")
148165
}
149166
// empty path means use `/`
150-
path := message.URL.Path
167+
path := message.URL.EscapedPath()
151168
if path == "" || path[0] != '/' {
152169
path = "/" + path
153170
}
154-
return httpsfv.NewItem(path), nil
171+
return []string{path}, nil
155172
case "@query":
156173
// Section 2.2.7 covers canonicalisation of the query.
157174
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-query
158175
if !message.IsRequest && !isReq {
159176
return nil, errors.New("query component not valid for responses")
160177
}
161178
// absent query params means use `?`
162-
return httpsfv.NewItem("?" + message.URL.RawQuery), nil
179+
return []string{"?" + message.URL.RawQuery}, nil
163180
case "@query-param":
164181
// Section 2.2.8 covers canonicalisation of the query-param.
165182
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-query-parameters
@@ -173,37 +190,44 @@ func canonicaliseComponent(component string, params *httpsfv.Params, message *Me
173190
if !ok {
174191
return nil, errors.New("query-param must have a named parameter")
175192
}
176-
decodedName, err := url.QueryUnescape(name.(string))
193+
decodedName, err := url.PathUnescape(name.(string))
177194
if err != nil {
178195
return nil, fmt.Errorf("unable to decode query parameter name: %w", err)
179196
}
180197
query := message.URL.Query()
181198
if !query.Has(decodedName) {
182199
return nil, fmt.Errorf("expected query parameter \"%s\" not found", name)
183200
}
184-
decodedValue, err := url.QueryUnescape(query.Get(decodedName))
185-
if err != nil {
186-
return nil, fmt.Errorf("unable to decode query parameter value: %w", err)
201+
var values []string
202+
for _, v := range query[decodedName] {
203+
decodedValue, err := url.PathUnescape(v)
204+
if err != nil {
205+
return nil, fmt.Errorf("unable to decode query parameter value: %w", err)
206+
}
207+
values = append(values, url.PathEscape(decodedValue))
187208
}
188-
return httpsfv.NewItem(url.QueryEscape(decodedValue)), nil
209+
return values, nil
189210
case "@status":
190211
// Section 2.2.9 covers canonicalisation of the status.
191212
// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-status-code
192-
if message.IsRequest && isReq {
213+
if message.IsRequest || (!message.IsRequest && isReq) {
193214
return nil, errors.New("status component not valid for requests")
194215
}
195-
return httpsfv.NewItem(message.StatusCode), nil
216+
return []string{strconv.Itoa(message.StatusCode)}, nil
196217
default:
197218
return nil, fmt.Errorf("unknown component: %s", component)
198219
}
199220
}
200221

201-
func canonicaliseHeader(header string, params *httpsfv.Params, message *Message) (httpsfv.StructuredFieldValue, error) {
222+
func canonicaliseHeader(header string, params *httpsfv.Params, message *Message) ([]string, error) {
202223
var v []string
203224
if _, isReq := params.Get("req"); isReq {
204225
if message.IsRequest {
205226
return nil, errors.New("req parameter not valid for requests")
206227
}
228+
if message.RequestHeader == nil {
229+
return nil, errors.New("req parameter requires a request header")
230+
}
207231
v = message.RequestHeader.Values(header)
208232
} else {
209233
v = message.Header.Values(header)
@@ -242,29 +266,51 @@ func canonicaliseHeader(header string, params *httpsfv.Params, message *Message)
242266
if !ok {
243267
return nil, fmt.Errorf("unable to find key \"%s\" in structured field", key)
244268
}
245-
return val, nil
269+
270+
marshalled, err := httpsfv.Marshal(val)
271+
if err != nil {
272+
return nil, err
273+
}
274+
275+
return []string{marshalled}, nil
246276
}
247-
return parsed, nil
277+
278+
marshalled, err := httpsfv.Marshal(parsed)
279+
if err != nil {
280+
return nil, err
281+
}
282+
283+
return []string{marshalled}, nil
248284
}
249285

250286
if isBs {
251-
encoded := httpsfv.List{}
252-
for _, sv := range v {
253-
decoded, err := base64.StdEncoding.DecodeString(sv)
287+
encoded := make([]string, len(v))
288+
for i, sv := range v {
289+
regex := regexp.MustCompile(`\s+`)
290+
values := strings.Split(sv, ",")
291+
for j, v := range values {
292+
values[j] = regex.ReplaceAllString(strings.TrimSpace(v), " ")
293+
}
294+
item := httpsfv.NewItem([]byte(strings.Join(values, ", ")))
295+
marshalled, err := httpsfv.Marshal(item)
254296
if err != nil {
255-
return nil, fmt.Errorf("unable to decode base64 value %s: %w", sv, err)
297+
return nil, err
256298
}
257-
enc := base64.StdEncoding.EncodeToString([]byte(strings.TrimSpace(string(decoded))))
258-
item := httpsfv.NewItem([]byte(enc))
259-
encoded = append(encoded, item)
299+
encoded[i] = marshalled
260300
}
301+
261302
return encoded, nil
262303
}
263304

264305
// raw encoding
265-
encoded := httpsfv.List{}
266-
for _, sv := range v {
267-
encoded = append(encoded, httpsfv.NewItem(strings.TrimSpace(sv)))
306+
encoded := make([]string, len(v))
307+
regex := regexp.MustCompile(`\s+`)
308+
for i, sv := range v {
309+
values := strings.Split(sv, ",")
310+
for j, v := range values {
311+
values[j] = regex.ReplaceAllString(strings.TrimSpace(v), " ")
312+
}
313+
encoded[i] = strings.Join(values, ", ")
268314
}
269315
return encoded, nil
270316
}
@@ -284,15 +330,6 @@ func quoteString(input string) string {
284330
return input
285331
}
286332

287-
func unquoteString(input string) string {
288-
// if it's quoted, attempt to unquote
289-
bytes := []byte(input)
290-
if len(bytes) > 2 && bytes[0] == '"' && bytes[len(bytes)-1] == '"' {
291-
bytes = bytes[1 : len(bytes)-1]
292-
}
293-
return string(bytes)
294-
}
295-
296333
func formatSignatureBase(items []signatureItem) (string, error) {
297334
var b strings.Builder
298335

@@ -302,16 +339,13 @@ func formatSignatureBase(items []signatureItem) (string, error) {
302339
return "", err
303340
}
304341

305-
marshalledValue, err := httpsfv.Marshal(item.value)
306-
if err != nil {
307-
return "", err
308-
}
342+
value := strings.Join(item.value, ", ")
309343

310-
_, err = b.WriteString(fmt.Sprintf("%s: %s\n", marshalledKey, unquoteString(marshalledValue)))
344+
_, err = b.WriteString(fmt.Sprintf("%s: %s\n", marshalledKey, value))
311345
if err != nil {
312346
return "", err
313347
}
314348
}
315349

316-
return strings.TrimSpace(b.String()), nil
350+
return strings.TrimRight(b.String(), "\n"), nil
317351
}

signer.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,12 @@ func (s *signer) Sign(msg *Message) (http.Header, error) {
230230
}
231231
input.Params = signingParameters
232232

233-
signatureBase = append(signatureBase, signatureItem{httpsfv.NewItem("@signature-params"), input})
233+
marshalledInput, err := httpsfv.Marshal(input)
234+
if err != nil {
235+
return nil, err
236+
}
237+
238+
signatureBase = append(signatureBase, signatureItem{httpsfv.NewItem("@signature-params"), []string{marshalledInput}})
234239

235240
base, err := formatSignatureBase(signatureBase)
236241
if err != nil {

0 commit comments

Comments
 (0)