Skip to content

Commit e8005fe

Browse files
committed
Structured fields
1 parent 525e333 commit e8005fe

File tree

4 files changed

+75
-26
lines changed

4 files changed

+75
-26
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
HTTP Message Signatures, implementing [draft-ietf-httpbis-message-signatures-07](https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-07.html).
1+
HTTP Message Signatures, implementing [draft-ietf-httpbis-message-signatures-08](https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-08.html).
22

3-
This is a nearly feature-complete implementation of draft -07, including all test vectors.
3+
This is a nearly feature-complete implementation of draft -08, including all test vectors.
44

55
### Notes and Missing Features
66
* The `Accept-Signature` header.
77
* Inclusion of `Signature` and `Signature-Input` as trailers is optional and is not yet implemented.
8-
* Extracting specialty components from the "related request". See [related issue](https://github.com/httpwg/http-extensions/issues/1905).
8+
* Extracting derived components from the "related request". See [related issue](https://github.com/httpwg/http-extensions/issues/1905).
99
* In responses, when using the "wrapped handler" feature, the `Content-Type` header is only signed if set explicitly by the server. This is different, but arguably more secure, than the normal `net.http` behavior.
10-
* The `sf` parameter, and in particular behavior when it is *not* given.
10+
* Test vectors are still verified to -07, update to -08 is pending.
1111

1212
[![Go Reference](https://pkg.go.dev/badge/github.com/yaronf/httpsign.svg)](https://pkg.go.dev/github.com/yaronf/httpsign)
1313
[![Test](https://github.com/yaronf/httpsign/actions/workflows/test.yml/badge.svg)](https://github.com/yaronf/httpsign/actions/workflows/test.yml)

fields.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
type Fields []field
1212

1313
// The SFV representation of a field is name;flagName="flagValue"
14+
// Note that this is a subset of SFV, we only support string-valued params, and only one param
15+
// per field for now.
1416
type field struct {
1517
name string
1618
flagName, flagValue string
@@ -83,16 +85,35 @@ func (fs *Fields) AddDictHeader(hdr, key string) *Fields {
8385
return fs
8486
}
8587

86-
func (f field) asSignatureInput() (string, error) {
88+
func fromStructuredField(hdr string) *field {
89+
h := strings.ToLower(hdr)
90+
f := field{h, "sf", ""}
91+
return &f
92+
}
93+
94+
// AddStructuredField indicates that a header should be interpreted as a structured field, per RFC 8941
95+
func (fs *Fields) AddStructuredField(hdr string) *Fields {
96+
f := fromStructuredField(hdr)
97+
*fs = append(*fs, *f)
98+
return fs
99+
}
100+
101+
func (f field) toItem() httpsfv.Item {
87102
p := httpsfv.NewParams()
88-
if f.flagName != "" {
103+
if f.flagName == "sf" { //special case
104+
p.Add(f.flagName, true)
105+
} else if f.flagName != "" {
89106
p.Add(f.flagName, f.flagValue)
90107
}
91108
i := httpsfv.Item{
92109
Value: f.name,
93110
Params: p,
94111
}
95-
s, err := httpsfv.Marshal(i)
112+
return i
113+
}
114+
115+
func (f field) asSignatureInput() (string, error) {
116+
s, err := httpsfv.Marshal(f.toItem())
96117
return s, err
97118
}
98119

@@ -102,19 +123,7 @@ func (fs *Fields) asSignatureInput(p *httpsfv.Params) (string, error) {
102123
Params: httpsfv.NewParams(),
103124
}
104125
for _, f := range *fs {
105-
if f.flagName == "" {
106-
il.Items = append(il.Items, httpsfv.Item{
107-
Value: f.name,
108-
Params: httpsfv.NewParams(),
109-
})
110-
} else {
111-
p := httpsfv.NewParams()
112-
p.Add(f.flagName, f.flagValue)
113-
il.Items = append(il.Items, httpsfv.Item{
114-
Value: f.name,
115-
Params: p,
116-
})
117-
}
126+
il.Items = append(il.Items, f.toItem())
118127
}
119128
il.Params = p
120129
s, err := httpsfv.Marshal(il)

signatures.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,15 @@ func generateSignatureInput(message parsedMessage, fields Fields, params string)
6767
}
6868

6969
func generateFieldValues(f field, message parsedMessage) ([]string, error) {
70-
if f.flagName == "" {
70+
if f.flagName == "" || f.flagName == "sf" {
7171
if strings.HasPrefix(f.name, "@") { // derived component
7272
vv, found := message.derived[f.name]
7373
if !found {
7474
return nil, fmt.Errorf("derived header %s not found", f.name)
7575
}
7676
return []string{vv}, nil
7777
}
78-
vv, found := message.headers[f.name] // normal header, cannot use "Values" on lowercased header name
79-
if !found {
80-
return nil, fmt.Errorf("header %s not found", f.name)
81-
}
82-
return []string{foldFields(vv)}, nil
78+
return message.getHeader(f.name, f.flagName == "sf")
8379
}
8480
if f.name == "@query-params" && f.flagName == "name" {
8581
vals, found := message.qParams[f.flagValue]
@@ -94,6 +90,25 @@ func generateFieldValues(f field, message parsedMessage) ([]string, error) {
9490
return nil, fmt.Errorf("unrecognized field %s", f)
9591
}
9692

93+
func (message *parsedMessage) getHeader(hdr string, structured bool) ([]string, error) {
94+
vv, found := message.headers[hdr] // normal header, cannot use "Values" on lowercased header name
95+
if !found {
96+
return nil, fmt.Errorf("header %s not found", hdr)
97+
}
98+
if !structured {
99+
return []string{foldFields(vv)}, nil
100+
}
101+
sfv, err := httpsfv.UnmarshalDictionary(vv)
102+
if err != nil {
103+
return nil, fmt.Errorf("could not unmarshal %s, possibly not a structured field: %w", hdr, err)
104+
}
105+
s, err := httpsfv.Marshal(sfv)
106+
if err != nil {
107+
return nil, fmt.Errorf("could not re-marshal %s", hdr)
108+
}
109+
return []string{s}, nil
110+
}
111+
97112
func (message *parsedMessage) getDictHeader(hdr, member string) ([]string, error) {
98113
vals, found := message.headers[hdr]
99114
if !found {

signatures_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,18 @@ func Test_signRequestDebug(t *testing.T) {
12571257
wantSignatureInput: "\"example-dictionary\": a=1, b=2;x=1;y=2, c=(a b c)\n\"@signature-params\": (\"example-dictionary\");alg=\"hmac-sha256\";keyid=\"test-key-hmac\"",
12581258
wantErr: false,
12591259
},
1260+
{
1261+
name: "normal header as SFV, sec. 2.1.1",
1262+
args: args{
1263+
signatureName: "sig1",
1264+
signer: makeHMACSigner(*NewSignConfig().SignCreated(false), *NewFields().AddStructuredField("example-dictionary")),
1265+
req: readRequest(dict1),
1266+
},
1267+
wantSignatureInputHeader: "sig1=(\"example-dictionary\";sf);alg=\"hmac-sha256\";keyid=\"test-key-hmac\"",
1268+
wantSignature: "sig1=:Ts9x90TxYoMYiGHgSysuJdOI26mL7Tzg310l9FdYt7I=:",
1269+
wantSignatureInput: "\"example-dictionary\";sf: a=1, b=2;x=1;y=2, c=(a b c)\n\"@signature-params\": (\"example-dictionary\";sf);alg=\"hmac-sha256\";keyid=\"test-key-hmac\"",
1270+
wantErr: false,
1271+
},
12601272
{
12611273
name: "cross-line header, trim",
12621274
args: args{
@@ -1282,6 +1294,19 @@ func Test_signRequestDebug(t *testing.T) {
12821294
wantSignatureInput: "\"x-ows-header\": Leading and trailing whitespace.\n\"x-obs-fold-header\": Obsolete line folding.\n\"cache-control\": max-age=60, must-revalidate\n\"example-dictionary\": a=1, b=2;x=1;y=2, c=(a b c)\n\"@signature-params\": (\"x-ows-header\" \"x-obs-fold-header\" \"cache-control\" \"example-dictionary\");alg=\"hmac-sha256\";keyid=\"test-key-hmac\"",
12831295
wantErr: false,
12841296
},
1297+
{
1298+
name: "reserialized dictionary headers, Sec. 2.1.2",
1299+
args: args{
1300+
signatureName: "sig1",
1301+
signer: makeHMACSigner(*NewSignConfig().SignCreated(false),
1302+
*NewFields().AddHeaders("Cache-Control").AddDictHeader("Example-Dictionary", "a").AddDictHeader("Example-Dictionary", "b").AddDictHeader("Example-Dictionary", "c")),
1303+
req: readRequest(httpreq4),
1304+
},
1305+
wantSignatureInputHeader: "sig1=(\"cache-control\" \"example-dictionary\";key=\"a\" \"example-dictionary\";key=\"b\" \"example-dictionary\";key=\"c\");alg=\"hmac-sha256\";keyid=\"test-key-hmac\"",
1306+
wantSignature: "sig1=:8ahOKeLf89H5O4bjk1PU6Hl/X48KUO7fxyG8l/sxOJE=:",
1307+
wantSignatureInput: "\"cache-control\": max-age=60, must-revalidate\n\"example-dictionary\";key=\"a\": 1\n\"example-dictionary\";key=\"b\": 2;x=1;y=2\n\"example-dictionary\";key=\"c\": (a b c)\n\"@signature-params\": (\"cache-control\" \"example-dictionary\";key=\"a\" \"example-dictionary\";key=\"b\" \"example-dictionary\";key=\"c\");alg=\"hmac-sha256\";keyid=\"test-key-hmac\"",
1308+
wantErr: false,
1309+
},
12851310
}
12861311
for _, tt := range tests {
12871312
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)