Skip to content

Commit d025787

Browse files
committed
feat: Add alternative verification API and expand MessageDetails
- Add `Message` struct with a `Verify` method that can be used in place of `VerifyRequest` + `RequestDetails`, and `VerifyResponse` + `ResponseDetails`. - `MessageDetails` now contains created, expires, nonce, and tag params.
1 parent a15a359 commit d025787

File tree

5 files changed

+527
-126
lines changed

5 files changed

+527
-126
lines changed

fuzz_test.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package httpsign
22

33
import (
44
"encoding/base64"
5-
"github.com/stretchr/testify/assert"
5+
"net/http"
6+
"net/url"
67
"testing"
8+
9+
"github.com/stretchr/testify/assert"
710
)
811

912
var httpreq1pssNoSig = `POST /foo?param=Value&Pet=dog HTTP/1.1
@@ -83,3 +86,96 @@ func FuzzSignAndVerifyHMAC(f *testing.F) {
8386
}
8487
})
8588
}
89+
90+
func FuzzVerify(f *testing.F) {
91+
f.Add("GET", "https://example.com/path", "example.com", "https", 0, "", "", "", "", true, false)
92+
f.Add("POST", "https://api.example.com", "api.example.com", "https", 0, "", "", "", "", false, true)
93+
f.Add("", "", "", "", 200, "GET", "https://example.com", "example.com", "https", true, false)
94+
f.Add("PUT", "", "", "http", 0, "", "", "", "", false, false)
95+
f.Add("", "", "", "", 404, "", "", "", "", false, false)
96+
f.Add("0", "%", "0", "0", 0, "", "", "", "", true, false)
97+
98+
f.Fuzz(func(t *testing.T, method, urlStr, authority, scheme string, statusCode int,
99+
assocMethod, assocURLStr, assocAuthority, assocScheme string,
100+
hasHeaders, hasTrailers bool) {
101+
102+
config := NewMessageConfig()
103+
104+
if method != "" {
105+
config = config.WithMethod(method)
106+
}
107+
if urlStr != "" {
108+
u, err := url.Parse(urlStr)
109+
if err == nil {
110+
config = config.WithURL(u)
111+
}
112+
}
113+
if authority != "" {
114+
config = config.WithAuthority(authority)
115+
}
116+
if scheme != "" {
117+
config = config.WithScheme(scheme)
118+
}
119+
120+
if statusCode > 0 {
121+
config = config.WithStatusCode(statusCode)
122+
}
123+
124+
if hasHeaders {
125+
headers := http.Header{
126+
"Content-Type": []string{"application/json"},
127+
"X-Test": []string{"fuzz"},
128+
}
129+
config = config.WithHeaders(headers)
130+
}
131+
if hasTrailers {
132+
trailers := http.Header{
133+
"X-Trailer": []string{"test"},
134+
}
135+
config = config.WithTrailers(trailers)
136+
}
137+
138+
if statusCode > 0 && assocMethod != "" {
139+
var assocURL *url.URL
140+
if assocURLStr != "" {
141+
assocURL, _ = url.Parse(assocURLStr)
142+
}
143+
assocHeaders := http.Header{"X-Assoc": []string{"test"}}
144+
config = config.WithAssociatedRequest(assocMethod, assocURL, assocHeaders, assocAuthority, assocScheme)
145+
}
146+
147+
msg, err := NewMessage(config)
148+
149+
if err == nil {
150+
if msg.headers == nil && msg.method != "" {
151+
t.Errorf("Request message created without headers")
152+
}
153+
if msg.headers == nil && msg.statusCode != nil {
154+
t.Errorf("Response message created without headers")
155+
}
156+
157+
key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
158+
verifier, _ := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false), Fields{})
159+
160+
if msg.headers != nil {
161+
msg.headers.Set("Signature-Input", `sig1=("@method");created=1618884473;keyid="test-key"`)
162+
msg.headers.Set("Signature", `sig1=:test:`)
163+
}
164+
165+
_, _ = msg.Verify("sig1", *verifier)
166+
}
167+
168+
if err != nil {
169+
hasRequest := method != ""
170+
hasResponse := statusCode > 0
171+
172+
if !hasRequest && !hasResponse {
173+
assert.Contains(t, err.Error(), "must have either method")
174+
} else if hasRequest && hasResponse {
175+
assert.Contains(t, err.Error(), "cannot have both request and response")
176+
} else if (hasRequest || hasResponse) && !hasHeaders {
177+
assert.Contains(t, err.Error(), "must have headers")
178+
}
179+
}
180+
})
181+
}

http2_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import (
44
"bufio"
55
"bytes"
66
"crypto/tls"
7-
"github.com/andreyvit/diff"
87
"io"
98
"net/http"
109
"net/http/httptest"
1110
"strconv"
1211
"strings"
1312
"testing"
1413
"text/template"
14+
15+
"github.com/andreyvit/diff"
1516
)
1617

1718
var wantFields = `"kuku": my awesome header
@@ -81,7 +82,11 @@ func testHTTP(t *testing.T, proto string) {
8182
if err != nil {
8283
t.Errorf("could not create verifier")
8384
}
84-
sigInput, err := verifyRequestDebug("sig1", *verifier, r)
85+
message, err := NewMessage(NewMessageConfig().WithRequest(r))
86+
if err != nil {
87+
t.Errorf("failed to create message: %v", err)
88+
}
89+
sigInput, _, err := verifyDebug("sig1", *verifier, message)
8590
if err != nil {
8691
t.Errorf("failed to verify request: sig input: %s\nerr: %v", sigInput, err)
8792
}

httpparse.go

Lines changed: 102 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,23 @@ func parseRequest(req *http.Request, withTrailers bool) (*parsedMessage, error)
2222
if req == nil {
2323
return nil, nil
2424
}
25-
err := validateMessageHeaders(req.Header)
26-
if err != nil {
27-
return nil, err
28-
}
29-
if withTrailers {
30-
_, err = duplicateBody(&req.Body) // read the entire body to populate the trailers
31-
if err != nil {
32-
return nil, fmt.Errorf("cannot duplicate request body: %w", err)
33-
}
34-
err = validateMessageHeaders(req.Trailer)
35-
if err != nil {
36-
return nil, fmt.Errorf("could not validate trailers: %w", err)
37-
}
25+
26+
scheme := "http"
27+
if req.TLS != nil {
28+
scheme = "https"
3829
}
39-
// Query params are only obtained from the URL (i.e. not from the message body, when using application/x-www-form-urlencoded)
40-
// So we are not vulnerable to the issue described in Sec. "Ambiguous Handling of Query Elements" of the draft.
41-
values, err := url.ParseQuery(req.URL.RawQuery)
42-
if err != nil {
43-
return nil, fmt.Errorf("cannot parse query: %s", req.URL.RawQuery)
44-
}
45-
escaped := reEncodeQPs(values)
46-
u := req.URL
47-
if u.Host == "" {
48-
u.Host = req.Host
49-
}
50-
if u.Scheme == "" {
51-
if req.TLS == nil {
52-
u.Scheme = "http"
53-
} else {
54-
u.Scheme = "https"
55-
}
30+
31+
msg := &Message{
32+
method: req.Method,
33+
url: req.URL,
34+
headers: req.Header,
35+
trailers: req.Trailer,
36+
body: &req.Body,
37+
authority: req.Host,
38+
scheme: scheme,
5639
}
57-
return &parsedMessage{derived: generateReqDerivedComponents(req), url: u, headers: normalizeHeaderNames(req.Header),
58-
trailers: normalizeHeaderNames(req.Trailer), qParams: escaped}, nil
40+
41+
return parseMessage(msg, withTrailers)
5942
}
6043

6144
func reEncodeQPs(values url.Values) url.Values {
@@ -82,23 +65,14 @@ func normalizeHeaderNames(header http.Header) http.Header {
8265
}
8366

8467
func parseResponse(res *http.Response, withTrailers bool) (*parsedMessage, error) {
85-
err := validateMessageHeaders(res.Header)
86-
if err != nil {
87-
return nil, err
88-
}
89-
if withTrailers {
90-
_, err = duplicateBody(&res.Body) // read the entire body to populate the trailers
91-
if err != nil {
92-
return nil, fmt.Errorf("cannot duplicate request body: %w", err)
93-
}
94-
err = validateMessageHeaders(res.Trailer)
95-
if err != nil {
96-
return nil, fmt.Errorf("could not validate trailers: %w", err)
97-
}
68+
msg := &Message{
69+
statusCode: &res.StatusCode,
70+
headers: res.Header,
71+
trailers: res.Trailer,
72+
body: &res.Body,
9873
}
9974

100-
return &parsedMessage{derived: generateResDerivedComponents(res), url: nil,
101-
headers: normalizeHeaderNames(res.Header)}, nil
75+
return parseMessage(msg, withTrailers)
10276
}
10377

10478
func validateMessageHeaders(header http.Header) error {
@@ -112,6 +86,9 @@ func validateMessageHeaders(header http.Header) error {
11286
}
11387

11488
func foldFields(fields []string) string {
89+
if len(fields) == 0 {
90+
return ""
91+
}
11592
ff := strings.TrimSpace(fields[0])
11693
for i := 1; i < len(fields); i++ {
11794
ff += ", " + strings.TrimSpace(fields[i])
@@ -123,17 +100,14 @@ func derivedComponent(name, v string, components components) {
123100
components[name] = v
124101
}
125102

126-
func generateReqDerivedComponents(req *http.Request) components {
127-
components := components{}
128-
derivedComponent("@method", scMethod(req), components)
129-
theURL := req.URL
130-
derivedComponent("@target-uri", scTargetURI(theURL), components)
131-
derivedComponent("@path", scPath(theURL), components)
132-
derivedComponent("@authority", scAuthority(req), components)
133-
derivedComponent("@scheme", scScheme(theURL), components)
134-
derivedComponent("@request-target", scRequestTarget(theURL), components)
135-
derivedComponent("@query", scQuery(theURL), components)
136-
return components
103+
func generateReqDerivedComponents(method string, u *url.URL, authority string, components components) {
104+
derivedComponent("@method", method, components)
105+
derivedComponent("@target-uri", scTargetURI(u), components)
106+
derivedComponent("@path", scPath(u), components)
107+
derivedComponent("@authority", authority, components)
108+
derivedComponent("@scheme", scScheme(u), components)
109+
derivedComponent("@request-target", scRequestTarget(u), components)
110+
derivedComponent("@query", scQuery(u), components)
137111
}
138112

139113
func scPath(theURL *url.URL) string {
@@ -162,24 +136,81 @@ func scScheme(url *url.URL) string {
162136
return url.Scheme
163137
}
164138

165-
func scAuthority(req *http.Request) string {
166-
return req.Host
167-
}
168-
169139
func scTargetURI(url *url.URL) string {
170140
return url.String()
171141
}
172142

173-
func scMethod(req *http.Request) string {
174-
return req.Method
143+
func scStatus(statusCode int) string {
144+
return strconv.Itoa(statusCode)
175145
}
176146

177-
func generateResDerivedComponents(res *http.Response) components {
178-
components := components{}
179-
derivedComponent("@status", scStatus(res), components)
180-
return components
181-
}
147+
func parseMessage(msg *Message, withTrailers bool) (*parsedMessage, error) {
148+
if msg == nil {
149+
return nil, nil
150+
}
151+
152+
err := validateMessageHeaders(msg.headers)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
if withTrailers {
158+
if msg.body != nil {
159+
_, err = duplicateBody(msg.body)
160+
if err != nil {
161+
return nil, fmt.Errorf("cannot duplicate message body: %w", err)
162+
}
163+
}
164+
err = validateMessageHeaders(msg.trailers)
165+
if err != nil {
166+
return nil, fmt.Errorf("could not validate trailers: %w", err)
167+
}
168+
}
169+
170+
derived := components{}
171+
var u *url.URL
172+
var qParams url.Values
173+
174+
if msg.method != "" || msg.url != nil {
175+
if msg.method == "" || msg.url == nil {
176+
return nil, fmt.Errorf("invalid state: method or url without the other")
177+
}
178+
179+
u = msg.url
180+
if u == nil {
181+
u = &url.URL{Path: "/"}
182+
}
183+
if u.Host == "" && msg.authority != "" {
184+
u.Host = msg.authority
185+
}
186+
if u.Scheme == "" {
187+
if msg.scheme != "" {
188+
u.Scheme = msg.scheme
189+
} else {
190+
u.Scheme = "http"
191+
}
192+
}
193+
194+
if u.RawQuery != "" {
195+
values, err := url.ParseQuery(u.RawQuery)
196+
if err != nil {
197+
return nil, fmt.Errorf("cannot parse query: %s", u.RawQuery)
198+
}
199+
qParams = reEncodeQPs(values)
200+
}
201+
202+
generateReqDerivedComponents(msg.method, u, msg.authority, derived)
203+
} else if msg.statusCode != nil {
204+
derivedComponent("@status", scStatus(*msg.statusCode), derived)
205+
} else {
206+
return nil, fmt.Errorf("invalid state: method and url, or status required")
207+
}
182208

183-
func scStatus(res *http.Response) string {
184-
return strconv.Itoa(res.StatusCode)
209+
return &parsedMessage{
210+
derived: derived,
211+
url: u,
212+
headers: normalizeHeaderNames(msg.headers),
213+
trailers: normalizeHeaderNames(msg.trailers),
214+
qParams: qParams,
215+
}, nil
185216
}

0 commit comments

Comments
 (0)