Skip to content

Commit 64d083b

Browse files
committed
Sign and verify optional headers
1 parent 7131121 commit 64d083b

File tree

3 files changed

+165
-3
lines changed

3 files changed

+165
-3
lines changed

fields.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import (
88

99
// Fields is a list of fields to be signed or verified. To initialize, use Headers or for more complex
1010
// cases, NewFields followed by a chain of Add... methods.
11+
//
12+
// Several component types may be marked as optional. When signing a message, an optional component (e.g., header)
13+
// is signed if it exists in the message to be signed, otherwise it is not included in the signature input.
14+
// Upon verification, a field marked optional must be included in the signed components if it appears at all.
15+
// This allows for intuitive handling of application components (headers, query parameters) whose presence in
16+
// the message depends on application logic. Please do NOT use this functionality for headers that may legitimately be
17+
// added by a proxy, such as X-Forwarded-For.
1118
type Fields struct {
1219
f []field
1320
}
@@ -94,6 +101,15 @@ func (fs *Fields) AddHeader(hdr string) *Fields {
94101
return fs
95102
}
96103

104+
// AddOptionalHeader appends a bare header name, e.g. "cache-control". The field is optional, see type documentation
105+
// for details.
106+
func (fs *Fields) AddOptionalHeader(hdr string) *Fields {
107+
f := fromHeaderName(hdr)
108+
f.markOptional()
109+
fs.f = append(fs.f, *f)
110+
return fs
111+
}
112+
97113
func fromQueryParam(qp string) *field {
98114
q := strings.ToLower(qp)
99115
i := httpsfv.NewItem("@query-params")
@@ -120,6 +136,15 @@ func (fs *Fields) AddQueryParam(qp string) *Fields {
120136
return fs
121137
}
122138

139+
// AddOptionalQueryParam indicates a request for a specific query parameter to be signed. The field is optional,
140+
// see type documentation for details.
141+
func (fs *Fields) AddOptionalQueryParam(qp string) *Fields {
142+
f := fromQueryParam(qp)
143+
f.markOptional()
144+
fs.f = append(fs.f, *f)
145+
return fs
146+
}
147+
123148
func fromDictHeader(hdr, key string) *field {
124149
h := strings.ToLower(hdr)
125150
i := httpsfv.NewItem(h)
@@ -143,6 +168,15 @@ func (fs *Fields) AddDictHeader(hdr, key string) *Fields {
143168
return fs
144169
}
145170

171+
// AddOptionalDictHeader indicates that out of a header structured as a dictionary, a specific key value is signed/verified.
172+
// The field is optional, see type documentation for details.
173+
func (fs *Fields) AddOptionalDictHeader(hdr, key string) *Fields {
174+
f := fromDictHeader(hdr, key)
175+
f.markOptional()
176+
fs.f = append(fs.f, *f)
177+
return fs
178+
}
179+
146180
func fromStructuredField(hdr string) *field {
147181
h := strings.ToLower(hdr)
148182
i := httpsfv.NewItem(h)
@@ -163,6 +197,15 @@ func (fs *Fields) AddStructuredField(hdr string) *Fields {
163197
return fs
164198
}
165199

200+
// AddOptionalStructuredField indicates that a header should be interpreted as a structured field, per RFC 8941.
201+
// The field is optional, see type documentation for details.
202+
func (fs *Fields) AddOptionalStructuredField(hdr string) *Fields {
203+
f := fromStructuredField(hdr)
204+
f.markOptional()
205+
fs.f = append(fs.f, *f)
206+
return fs
207+
}
208+
166209
func fromRequestResponse(sigName string) *field {
167210
i := httpsfv.NewItem("@request-response")
168211
i.Params.Add("key", sigName)
@@ -179,6 +222,50 @@ func (f field) asSignatureInput() (string, error) {
179222
return s, err
180223
}
181224

225+
func (f field) markOptional() {
226+
if f.Params == nil {
227+
f.Params = httpsfv.NewParams()
228+
}
229+
f.Params.Add("optional", true)
230+
}
231+
232+
func (f field) unmarkOptional() {
233+
if f.Params == nil {
234+
f.Params = httpsfv.NewParams()
235+
}
236+
f.Params.Del("optional")
237+
}
238+
239+
func (f field) isOptional() bool {
240+
if f.Params != nil {
241+
v, ok := f.Params.Get("optional")
242+
if ok {
243+
vv, ok2 := v.(bool)
244+
if ok2 {
245+
return vv
246+
}
247+
}
248+
}
249+
return false
250+
}
251+
252+
// Not a full deep copy, but good enough for mutating params
253+
func (f field) copy() field {
254+
ff := field{
255+
Value: f.Value,
256+
}
257+
if f.Params == nil {
258+
ff.Params = nil
259+
} else {
260+
ff.Params = httpsfv.NewParams()
261+
for _, n := range f.Params.Names() {
262+
v, _ := f.Params.Get(n)
263+
ff.Params.Add(n, v)
264+
}
265+
}
266+
return ff
267+
}
268+
182269
func (fs *Fields) asSignatureInput(p *httpsfv.Params) (string, error) {
183270
il := httpsfv.InnerList{
184271
Items: []httpsfv.Item{},

signatures.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import (
1818

1919
func signMessage(config SignConfig, signatureName string, signer Signer, parsedMessage parsedMessage,
2020
fields Fields) (signatureInputHeader, signature, signatureInput string, err error) {
21-
sigParams, err := generateSigParams(&config, signer.keyID, signer.alg, signer.foreignSigner, fields)
21+
filtered := filterOptionalFields(fields, parsedMessage)
22+
sigParams, err := generateSigParams(&config, signer.keyID, signer.alg, signer.foreignSigner, filtered)
2223
if err != nil {
2324
return "", "", "", err
2425
}
2526
signatureInputHeader = fmt.Sprintf("%s=%s", signatureName, sigParams)
26-
signatureInput, err = generateSignatureInput(parsedMessage, fields, sigParams)
27+
signatureInput, err = generateSignatureInput(parsedMessage, filtered, sigParams)
2728
if err != nil {
2829
return "", "", "", err
2930
}
@@ -34,6 +35,23 @@ func signMessage(config SignConfig, signatureName string, signer Signer, parsedM
3435
return signatureInputHeader, signature, signatureInput, nil
3536
}
3637

38+
func filterOptionalFields(fields Fields, message parsedMessage) Fields {
39+
filtered := *NewFields()
40+
for _, f := range fields.f {
41+
if !f.isOptional() {
42+
filtered.f = append(filtered.f, f)
43+
} else {
44+
_, err := generateFieldValues(f, message)
45+
if err == nil { // value was found
46+
ff := f.copy()
47+
ff.unmarkOptional()
48+
filtered.f = append(filtered.f, ff)
49+
}
50+
}
51+
}
52+
return filtered
53+
}
54+
3755
func generateSignature(name string, signer Signer, input string) (string, error) {
3856
raw, err := signer.sign([]byte(input))
3957
if err != nil {
@@ -385,7 +403,8 @@ func verifyMessage(config VerifyConfig, name string, verifier Verifier, message
385403
if err != nil {
386404
return "", err
387405
}
388-
if !(psiSig.fields.contains(&fields)) {
406+
filtered := filterOptionalFields(fields, message)
407+
if !(psiSig.fields.contains(&filtered)) {
389408
return "", fmt.Errorf("actual signature does not cover all required fields")
390409
}
391410
err = applyVerificationPolicy(verifier, message, psiSig, config)

signatures_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,3 +1489,59 @@ func TestVerifyResponse(t *testing.T) {
14891489
})
14901490
}
14911491
}
1492+
1493+
func TestOptionalSign(t *testing.T) {
1494+
req := readRequest(httpreq2)
1495+
f := NewFields().AddHeader("date").AddOptionalHeader("x-optional")
1496+
key1 := bytes.Repeat([]byte{0x55}, 64)
1497+
signer, err := NewHMACSHA256Signer("key1", key1, NewSignConfig().setFakeCreated(9999), *f)
1498+
assert.NoError(t, err, "Could not create signer")
1499+
sigInputHeader, _, sigInput, err := signRequestDebug("sig1", *signer, req)
1500+
assert.NoError(t, err, "Should not fail with optional header absent")
1501+
assert.Equal(t, "sig1=(\"date\");created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", sigInputHeader)
1502+
assert.Equal(t, "\"date\": Tue, 20 Apr 2021 02:07:55 GMT\n\"@signature-params\": (\"date\");created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", sigInput)
1503+
1504+
req.Header.Add("X-Optional", "value")
1505+
sigInputHeader, _, sigInput, err = signRequestDebug("sig1", *signer, req)
1506+
assert.NoError(t, err, "Should not fail with optional header present")
1507+
assert.Equal(t, "sig1=(\"date\" \"x-optional\");created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", sigInputHeader)
1508+
assert.Equal(t, "\"date\": Tue, 20 Apr 2021 02:07:55 GMT\n\"x-optional\": value\n\"@signature-params\": (\"date\" \"x-optional\");created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", sigInput)
1509+
}
1510+
1511+
func TestOptionalVerify(t *testing.T) {
1512+
req := readRequest(httpreq2)
1513+
req.Header.Add("X-Opt1", "val1")
1514+
f1 := NewFields().AddHeader("date").AddOptionalHeader("x-opt1")
1515+
key1 := bytes.Repeat([]byte{0x66}, 64)
1516+
signer, err := NewHMACSHA256Signer("key1", key1, NewSignConfig().setFakeCreated(8888), *f1)
1517+
assert.NoError(t, err, "Could not create signer")
1518+
sigInputHeader, signature, err := SignRequest("sig1", *signer, req)
1519+
assert.NoError(t, err, "Should not fail with optional header present")
1520+
req.Header.Add("Signature-Input", sigInputHeader)
1521+
req.Header.Add("Signature", signature)
1522+
1523+
verifier, err := NewHMACSHA256Verifier("key1", key1, NewVerifyConfig().SetVerifyCreated(false), *f1)
1524+
assert.NoError(t, err, "Could not create verifier")
1525+
err = VerifyRequest("sig1", *verifier, req)
1526+
assert.NoError(t, err, "Should not fail: present and signed")
1527+
1528+
req.Header.Del("X-Opt1") // header absent but included in covered components
1529+
err = VerifyRequest("sig1", *verifier, req)
1530+
assert.Error(t, err, "Should fail: absent and signed")
1531+
1532+
req = readRequest(httpreq2) // header present but not signed
1533+
req.Header.Add("X-Opt1", "val1")
1534+
f2 := NewFields().AddHeader("date") // without the optional header
1535+
signer, err = NewHMACSHA256Signer("key1", key1, NewSignConfig().setFakeCreated(2222), *f2)
1536+
sigInputHeader, signature, err = SignRequest("sig1", *signer, req)
1537+
assert.NoError(t, err, "Should not fail with redundant header present")
1538+
req.Header.Add("Signature-Input", sigInputHeader)
1539+
req.Header.Add("Signature", signature)
1540+
1541+
err = VerifyRequest("sig1", *verifier, req)
1542+
assert.Error(t, err, "Should fail: present and not signed")
1543+
1544+
req.Header.Del("X-Opt1")
1545+
err = VerifyRequest("sig1", *verifier, req)
1546+
assert.NoError(t, err, "Should not fail: absent and not signed")
1547+
}

0 commit comments

Comments
 (0)