Skip to content

feat: Add alternative verification API and expand MessageDetails #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package httpsign

import (
"encoding/base64"
"github.com/stretchr/testify/assert"
"net/http"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
)

var httpreq1pssNoSig = `POST /foo?param=Value&Pet=dog HTTP/1.1
Expand Down Expand Up @@ -83,3 +86,96 @@ func FuzzSignAndVerifyHMAC(f *testing.F) {
}
})
}

func FuzzVerify(f *testing.F) {
f.Add("GET", "https://example.com/path", "example.com", "https", 0, "", "", "", "", true, false)
f.Add("POST", "https://api.example.com", "api.example.com", "https", 0, "", "", "", "", false, true)
f.Add("", "", "", "", 200, "GET", "https://example.com", "example.com", "https", true, false)
f.Add("PUT", "", "", "http", 0, "", "", "", "", false, false)
f.Add("", "", "", "", 404, "", "", "", "", false, false)
f.Add("0", "%", "0", "0", 0, "", "", "", "", true, false)

f.Fuzz(func(t *testing.T, method, urlStr, authority, scheme string, statusCode int,
assocMethod, assocURLStr, assocAuthority, assocScheme string,
hasHeaders, hasTrailers bool) {

config := NewMessageConfig()

if method != "" {
config = config.WithMethod(method)
}
if urlStr != "" {
u, err := url.Parse(urlStr)
if err == nil {
config = config.WithURL(u)
}
}
if authority != "" {
config = config.WithAuthority(authority)
}
if scheme != "" {
config = config.WithScheme(scheme)
}

if statusCode > 0 {
config = config.WithStatusCode(statusCode)
}

if hasHeaders {
headers := http.Header{
"Content-Type": []string{"application/json"},
"X-Test": []string{"fuzz"},
}
config = config.WithHeaders(headers)
}
if hasTrailers {
trailers := http.Header{
"X-Trailer": []string{"test"},
}
config = config.WithTrailers(trailers)
}

if statusCode > 0 && assocMethod != "" {
var assocURL *url.URL
if assocURLStr != "" {
assocURL, _ = url.Parse(assocURLStr)
}
assocHeaders := http.Header{"X-Assoc": []string{"test"}}
config = config.WithAssociatedRequest(assocMethod, assocURL, assocHeaders, assocAuthority, assocScheme)
}

msg, err := NewMessage(config)

if err == nil {
if msg.headers == nil && msg.method != "" {
t.Errorf("Request message created without headers")
}
if msg.headers == nil && msg.statusCode != nil {
t.Errorf("Response message created without headers")
}

key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
verifier, _ := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false), Fields{})

if msg.headers != nil {
msg.headers.Set("Signature-Input", `sig1=("@method");created=1618884473;keyid="test-key"`)
msg.headers.Set("Signature", `sig1=:test:`)
}

_, _ = msg.Verify("sig1", *verifier)
}

if err != nil {
hasRequest := method != ""
hasResponse := statusCode > 0

if !hasRequest && !hasResponse {
assert.Contains(t, err.Error(), "must have either method")
} else if hasRequest && hasResponse {
assert.Contains(t, err.Error(), "cannot have both request and response")
} else if (hasRequest || hasResponse) && !hasHeaders {
assert.Contains(t, err.Error(), "must have headers")
}
}
})
}
9 changes: 7 additions & 2 deletions http2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"bufio"
"bytes"
"crypto/tls"
"github.com/andreyvit/diff"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"text/template"

"github.com/andreyvit/diff"
)

var wantFields = `"kuku": my awesome header
Expand Down Expand Up @@ -81,7 +82,11 @@ func testHTTP(t *testing.T, proto string) {
if err != nil {
t.Errorf("could not create verifier")
}
sigInput, err := verifyRequestDebug("sig1", *verifier, r)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change the old test instead of creating a new one?

message, err := NewMessage(NewMessageConfig().WithRequest(r))
if err != nil {
t.Errorf("failed to create message: %v", err)
}
sigInput, _, err := verifyDebug("sig1", *verifier, message)
if err != nil {
t.Errorf("failed to verify request: sig input: %s\nerr: %v", sigInput, err)
}
Expand Down
173 changes: 102 additions & 71 deletions httpparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,23 @@ func parseRequest(req *http.Request, withTrailers bool) (*parsedMessage, error)
if req == nil {
return nil, nil
}
err := validateMessageHeaders(req.Header)
if err != nil {
return nil, err
}
if withTrailers {
_, err = duplicateBody(&req.Body) // read the entire body to populate the trailers
if err != nil {
return nil, fmt.Errorf("cannot duplicate request body: %w", err)
}
err = validateMessageHeaders(req.Trailer)
if err != nil {
return nil, fmt.Errorf("could not validate trailers: %w", err)
}

scheme := "http"
if req.TLS != nil {
scheme = "https"
}
// Query params are only obtained from the URL (i.e. not from the message body, when using application/x-www-form-urlencoded)
// So we are not vulnerable to the issue described in Sec. "Ambiguous Handling of Query Elements" of the draft.
values, err := url.ParseQuery(req.URL.RawQuery)
if err != nil {
return nil, fmt.Errorf("cannot parse query: %s", req.URL.RawQuery)
}
escaped := reEncodeQPs(values)
u := req.URL
if u.Host == "" {
u.Host = req.Host
}
if u.Scheme == "" {
if req.TLS == nil {
u.Scheme = "http"
} else {
u.Scheme = "https"
}

msg := &Message{
method: req.Method,
url: req.URL,
headers: req.Header,
trailers: req.Trailer,
body: &req.Body,
authority: req.Host,
scheme: scheme,
}
return &parsedMessage{derived: generateReqDerivedComponents(req), url: u, headers: normalizeHeaderNames(req.Header),
trailers: normalizeHeaderNames(req.Trailer), qParams: escaped}, nil

return parseMessage(msg, withTrailers)
}

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

func parseResponse(res *http.Response, withTrailers bool) (*parsedMessage, error) {
err := validateMessageHeaders(res.Header)
if err != nil {
return nil, err
}
if withTrailers {
_, err = duplicateBody(&res.Body) // read the entire body to populate the trailers
if err != nil {
return nil, fmt.Errorf("cannot duplicate request body: %w", err)
}
err = validateMessageHeaders(res.Trailer)
if err != nil {
return nil, fmt.Errorf("could not validate trailers: %w", err)
}
msg := &Message{
statusCode: &res.StatusCode,
headers: res.Header,
trailers: res.Trailer,
body: &res.Body,
}

return &parsedMessage{derived: generateResDerivedComponents(res), url: nil,
headers: normalizeHeaderNames(res.Header)}, nil
return parseMessage(msg, withTrailers)
}

func validateMessageHeaders(header http.Header) error {
Expand All @@ -112,6 +86,9 @@ func validateMessageHeaders(header http.Header) error {
}

func foldFields(fields []string) string {
if len(fields) == 0 {
return ""
}
ff := strings.TrimSpace(fields[0])
for i := 1; i < len(fields); i++ {
ff += ", " + strings.TrimSpace(fields[i])
Expand All @@ -123,17 +100,14 @@ func derivedComponent(name, v string, components components) {
components[name] = v
}

func generateReqDerivedComponents(req *http.Request) components {
components := components{}
derivedComponent("@method", scMethod(req), components)
theURL := req.URL
derivedComponent("@target-uri", scTargetURI(theURL), components)
derivedComponent("@path", scPath(theURL), components)
derivedComponent("@authority", scAuthority(req), components)
derivedComponent("@scheme", scScheme(theURL), components)
derivedComponent("@request-target", scRequestTarget(theURL), components)
derivedComponent("@query", scQuery(theURL), components)
return components
func generateReqDerivedComponents(method string, u *url.URL, authority string, components components) {
derivedComponent("@method", method, components)
derivedComponent("@target-uri", scTargetURI(u), components)
derivedComponent("@path", scPath(u), components)
derivedComponent("@authority", authority, components)
derivedComponent("@scheme", scScheme(u), components)
derivedComponent("@request-target", scRequestTarget(u), components)
derivedComponent("@query", scQuery(u), components)
}

func scPath(theURL *url.URL) string {
Expand Down Expand Up @@ -162,24 +136,81 @@ func scScheme(url *url.URL) string {
return url.Scheme
}

func scAuthority(req *http.Request) string {
return req.Host
}

func scTargetURI(url *url.URL) string {
return url.String()
}

func scMethod(req *http.Request) string {
return req.Method
func scStatus(statusCode int) string {
return strconv.Itoa(statusCode)
}

func generateResDerivedComponents(res *http.Response) components {
components := components{}
derivedComponent("@status", scStatus(res), components)
return components
}
func parseMessage(msg *Message, withTrailers bool) (*parsedMessage, error) {
if msg == nil {
return nil, nil
}

err := validateMessageHeaders(msg.headers)
if err != nil {
return nil, err
}

if withTrailers {
if msg.body != nil {
_, err = duplicateBody(msg.body)
if err != nil {
return nil, fmt.Errorf("cannot duplicate message body: %w", err)
}
}
err = validateMessageHeaders(msg.trailers)
if err != nil {
return nil, fmt.Errorf("could not validate trailers: %w", err)
}
}

derived := components{}
var u *url.URL
var qParams url.Values

if msg.method != "" || msg.url != nil {
if msg.method == "" || msg.url == nil {
return nil, fmt.Errorf("invalid state: method or url without the other")
}

u = msg.url
if u == nil {
u = &url.URL{Path: "/"}
}
if u.Host == "" && msg.authority != "" {
u.Host = msg.authority
}
if u.Scheme == "" {
if msg.scheme != "" {
u.Scheme = msg.scheme
} else {
u.Scheme = "http"
}
}

if u.RawQuery != "" {
values, err := url.ParseQuery(u.RawQuery)
if err != nil {
return nil, fmt.Errorf("cannot parse query: %s", u.RawQuery)
}
qParams = reEncodeQPs(values)
}

generateReqDerivedComponents(msg.method, u, msg.authority, derived)
} else if msg.statusCode != nil {
derivedComponent("@status", scStatus(*msg.statusCode), derived)
} else {
return nil, fmt.Errorf("invalid state: method and url, or status required")
}

func scStatus(res *http.Response) string {
return strconv.Itoa(res.StatusCode)
return &parsedMessage{
derived: derived,
url: u,
headers: normalizeHeaderNames(msg.headers),
trailers: normalizeHeaderNames(msg.trailers),
qParams: qParams,
}, nil
}
Loading