diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a492c2e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# See https://editorconfig.org + +# top-most EditorConfig file +root = true + +# Go files +[*._test.go] +# Preserve trailing whitespace in tests since some depend on it +trim_trailing_whitespace = false diff --git a/crypto_test.go b/crypto_test.go index cb15049..a06ac0b 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -4,12 +4,13 @@ import ( "crypto/ed25519" "crypto/rand" "crypto/rsa" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/stretchr/testify/assert" "reflect" "strings" "testing" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/stretchr/testify/assert" ) func TestNewHMACSHA256Signer(t *testing.T) { @@ -162,6 +163,41 @@ func TestForeignSigner(t *testing.T) { } } +// Same as TestForeignSigner but using Message +func TestMessageForeignSigner(t *testing.T) { + priv, pub, err := genP256KeyPair() + if err != nil { + t.Errorf("Failed to generate keypair: %v", err) + } + + config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false) + signatureName := "sig1" + fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet") + signer, err := NewJWSSigner(jwa.ES256, priv, config.SetKeyID("key1"), fields) + if err != nil { + t.Errorf("Failed to create JWS signer") + } + req := readRequest(httpreq2) + sigInput, sig, err := SignRequest(signatureName, *signer, req) + if err != nil { + t.Errorf("signature failed: %v", err) + } + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + verifier, err := NewJWSVerifier(jwa.ES256, pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + if err != nil { + t.Errorf("Failed to create Message") + } + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + func makeRSAPrivateKey() *rsa.PrivateKey { priv, _ := rsa.GenerateKey(rand.Reader, 2048) return priv diff --git a/doc.go b/doc.go index 3fc0fea..3b8f14c 100644 --- a/doc.go +++ b/doc.go @@ -6,4 +6,5 @@ // WrapHandler installs a wrapper around a normal HTTP message handler. // Digest functionality (creation and validation of the Content-Digest header) is available automatically // through the Client and WrapHandler interfaces, otherwise it is available separately. +// Use Message and its Verify method if you need more flexibility such as in a non-HTTP context. package httpsign diff --git a/fuzz_test.go b/fuzz_test.go index 708ba51..738edfb 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -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 @@ -55,6 +58,50 @@ func FuzzVerifyRequest(f *testing.F) { }) } +// Same as FuzzVerifyRequest but using Message +func FuzzMessageVerifyRequest(f *testing.F) { + type inputs struct { + req, sigInput, sig string + } + testcases := []inputs{ + {httpreq1pssNoSig, + "sig-b21=();created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"b3k2pp5k7z-50gnwp.yemd\"", + "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:", + }, + {httpreq1pssNoSig, + "sig-b21=(date);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"", + "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:", + }, + {httpreq1pssNoSig, + "sig-b21=(some-field;tr);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"", + "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:", + }, + {httpreq1pssNoSig, + "sig-b22=(some-field;tr;bs);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"", + "sig-b22=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:", + }, + } + for _, tc := range testcases { + f.Add(tc.req, tc.sigInput, tc.sig) // Use f.Add to provide a seed corpus + } + f.Fuzz(func(t *testing.T, reqString, sigInput, sig string) { + req := readRequest(reqString) + if req != nil { + req.Header.Set("Signature-Input", sigInput) + req.Header.Set("Signature", sig) + } + + sigName := "sig-b21" + verifier := makeRSAVerifier(f, "key1", *NewFields()) + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + if err != nil { + t.Errorf("Failed to create Message") + } + _, _ = msg.Verify(sigName, verifier) + // only report panics + }) +} + func FuzzSignAndVerifyHMAC(f *testing.F) { type inputs struct { req string @@ -83,3 +130,130 @@ func FuzzSignAndVerifyHMAC(f *testing.F) { } }) } + +// Same as FuzzSignAndVerifyHMAC but using Message +func FuzzMessageSignAndVerifyHMAC(f *testing.F) { + type inputs struct { + req string + } + testcases := []inputs{ + {httpreq1}, + } + for _, tc := range testcases { + f.Add(tc.req) + } + f.Fuzz(func(t *testing.T, reqString string) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475) + fields := Headers("@authority", "date", "content-type") + signatureName := "sig1" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + signer, _ := NewHMACSHA256Signer(key, config.SetKeyID("test-shared-secret"), fields) + req := readRequest(reqString) + sigInput, sig, err := SignRequest(signatureName, *signer, req) + if err == nil { + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + if err != nil { + t.Errorf("Failed to create Message") + } + _, err = msg.Verify(signatureName, *verifier) + assert.NoError(t, err, "verification error") + } + }) +} + +func FuzzMessageVerify(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") + } + } + }) +} diff --git a/http2_test.go b/http2_test.go index 8984c0f..9371a35 100644 --- a/http2_test.go +++ b/http2_test.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "crypto/tls" - "github.com/andreyvit/diff" "io" "net/http" "net/http/httptest" @@ -12,6 +11,8 @@ import ( "strings" "testing" "text/template" + + "github.com/andreyvit/diff" ) var wantFields = `"kuku": my awesome header @@ -96,6 +97,61 @@ func testHTTP(t *testing.T, proto string) { simpleClient(t, proto, simpleHandler) } +func testMessageHTTP(t *testing.T, proto string) { + simpleHandler := func(w http.ResponseWriter, r *http.Request) { + reqProto := r.Proto + if reqProto != proto { + t.Errorf("expected %s, got %s", proto, reqProto) + } + var scheme string + if ts.TLS == nil { + scheme = "http" + } else { + scheme = "https" + } + sp := bytes.Split([]byte(ts.URL), []byte(":")) + portval, err := strconv.Atoi(string(sp[2])) + if err != nil { + t.Errorf("cannot parse server port number") + } + tpl, err := template.New("fields").Parse(wantFields) + if err != nil { + t.Errorf("could not parse template") + } + type inputs struct { + Port int + Scheme string + } + // Use the Template facility to create the list of expected signed fields + wf, err := execTemplate(*tpl, "fields", inputs{Port: portval, Scheme: scheme}) + if err != nil { + t.Errorf("execTemplate failed") + } + verifier, err := NewHMACSHA256Verifier(bytes.Repeat([]byte{0x03}, 64), + NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), + Headers("@query")) + if err != nil { + t.Errorf("could not create verifier") + } + msg, err := NewMessage(NewMessageConfig().WithRequest(r)) + if err != nil { + t.Errorf("could not create message") + } + sigInput, _, err := verifyDebug("sig1", *verifier, msg) + if err != nil { + t.Errorf("failed to verify request: sig input: %s\nerr: %v", sigInput, err) + } + + if sigInput != wf { + t.Errorf("unexpected fields: %s\n", diff.CharacterDiff(sigInput, wantFields)) + } + w.WriteHeader(200) + } + + // And run the client code... + simpleClient(t, proto, simpleHandler) +} + func simpleClient(t *testing.T, proto string, simpleHandler func(w http.ResponseWriter, r *http.Request)) { // Client code switch proto { @@ -153,8 +209,10 @@ func simpleClient(t *testing.T, proto string, simpleHandler func(w http.Response func TestHTTP11(t *testing.T) { testHTTP(t, "HTTP/1.1") + testMessageHTTP(t, "HTTP/1.1") } func TestHTTP20(t *testing.T) { testHTTP(t, "HTTP/2.0") + testMessageHTTP(t, "HTTP/2.0") } diff --git a/httpparse.go b/httpparse.go index c7e1232..36ff12c 100644 --- a/httpparse.go +++ b/httpparse.go @@ -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 { @@ -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 { @@ -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]) @@ -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 { @@ -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 } diff --git a/message.go b/message.go new file mode 100644 index 0000000..3923fc8 --- /dev/null +++ b/message.go @@ -0,0 +1,217 @@ +package httpsign + +import ( + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +// MessageDetails aggregates the details of a signed message, for a given signature +type MessageDetails struct { + KeyID string + Alg string + Fields Fields + Created *time.Time + Expires *time.Time + Nonce *string + Tag *string +} + +// Message represents a parsed HTTP message ready for signature verification. +type Message struct { + headers http.Header + trailers http.Header + body *io.ReadCloser + + method string + url *url.URL + authority string + scheme string + statusCode *int + assocReq *Message +} + +// NewMessage constructs a new Message from the provided config. +func NewMessage(config *MessageConfig) (*Message, error) { + if config == nil { + config = NewMessageConfig() + } + + hasRequest := config.method != "" + hasResponse := config.statusCode != nil + + if !hasRequest && !hasResponse { + return nil, fmt.Errorf("message config must have either method (for request) or status code (for response)") + } + + if hasRequest && hasResponse { + return nil, fmt.Errorf("message config cannot have both request and response fields set") + } + + if hasRequest { + if config.headers == nil { + return nil, fmt.Errorf("request message must have headers") + } + } + + if hasResponse { + if config.headers == nil { + return nil, fmt.Errorf("response message must have headers") + } + } + + var assocReq *Message + if config.assocReq != nil { + method := config.assocReq.method + u := config.assocReq.url + headers := config.assocReq.headers + authority := config.assocReq.authority + scheme := config.assocReq.scheme + if method == "" || u == nil || headers == nil || authority == "" || scheme == "" { + return nil, fmt.Errorf("invalid associated request") + } + assocReq = &Message{ + method: method, + url: u, + headers: headers, + authority: authority, + scheme: scheme, + } + } + + return &Message{ + headers: config.headers, + trailers: config.trailers, + body: config.body, + method: config.method, + url: config.url, + authority: config.authority, + scheme: config.scheme, + statusCode: config.statusCode, + assocReq: assocReq, + }, nil +} + +// MessageConfig configures a Message for signature verification. +type MessageConfig struct { + method string + url *url.URL + headers http.Header + trailers http.Header + body *io.ReadCloser + authority string + scheme string + + statusCode *int + + assocReq *MessageConfig +} + +// NewMessageConfig returns a new MessageConfig. +func NewMessageConfig() *MessageConfig { + return &MessageConfig{} +} + +func (b *MessageConfig) WithMethod(method string) *MessageConfig { + b.method = method + return b +} + +func (b *MessageConfig) WithURL(u *url.URL) *MessageConfig { + b.url = u + return b +} + +func (b *MessageConfig) WithHeaders(headers http.Header) *MessageConfig { + b.headers = headers + return b +} + +func (b *MessageConfig) WithTrailers(trailers http.Header) *MessageConfig { + b.trailers = trailers + return b +} + +func (b *MessageConfig) WithBody(body *io.ReadCloser) *MessageConfig { + b.body = body + return b +} + +func (b *MessageConfig) WithAuthority(authority string) *MessageConfig { + b.authority = authority + return b +} + +func (b *MessageConfig) WithScheme(scheme string) *MessageConfig { + b.scheme = scheme + return b +} + +func (b *MessageConfig) WithStatusCode(statusCode int) *MessageConfig { + b.statusCode = &statusCode + return b +} + +func (b *MessageConfig) WithAssociatedRequest(method string, u *url.URL, headers http.Header, authority, scheme string) *MessageConfig { + b.assocReq = &MessageConfig{ + method: method, + url: u, + headers: headers, + authority: authority, + scheme: scheme, + } + return b +} + +func (b *MessageConfig) WithRequest(req *http.Request) *MessageConfig { + if req == nil { + return b + } + + scheme := "http" + if req.TLS != nil { + scheme = "https" + } + + return b. + WithMethod(req.Method). + WithURL(req.URL). + WithHeaders(req.Header). + WithTrailers(req.Trailer). + WithBody(&req.Body). + WithAuthority(req.Host). + WithScheme(scheme) +} + +func (b *MessageConfig) WithResponse(res *http.Response, req *http.Request) *MessageConfig { + if res == nil { + return b + } + + b = b. + WithStatusCode(res.StatusCode). + WithHeaders(res.Header). + WithTrailers(res.Trailer). + WithBody(&res.Body) + + if req != nil { + scheme := "http" + if req.TLS != nil { + scheme = "https" + } + b = b.WithAssociatedRequest(req.Method, req.URL, req.Header, req.Host, scheme) + } + + return b +} + +// Verify verifies a signature on this message. +func (m *Message) Verify(signatureName string, verifier Verifier) (*MessageDetails, error) { + _, psiSig, err := verifyDebug(signatureName, verifier, m) + if err != nil { + return nil, err + } + return signatureDetails(psiSig) +} diff --git a/message_test.go b/message_test.go new file mode 100644 index 0000000..b586179 --- /dev/null +++ b/message_test.go @@ -0,0 +1,48 @@ +package httpsign_test + +import ( + "bufio" + "bytes" + "fmt" + "net/http" + "strings" + + "github.com/yaronf/httpsign" +) + +func ExampleMessage_Verify() { + config := httpsign.NewVerifyConfig().SetKeyID("my-shared-secret").SetVerifyCreated(false) // for testing only + verifier, _ := httpsign.NewHMACSHA256Verifier(bytes.Repeat([]byte{0x77}, 64), config, + httpsign.Headers("@authority", "Date", "@method")) + reqStr := `GET /foo HTTP/1.1 +Host: example.org +Date: Tue, 20 Apr 2021 02:07:55 GMT +Cache-Control: max-age=60 +Signature-Input: sig77=("@authority" "date" "@method");alg="hmac-sha256";keyid="my-shared-secret" +Signature: sig77=:3e9KqLP62NHfHY5OMG4036+U6tvBowZF35ALzTjpsf0=: + +` + req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr))) + + // Using WithRequest + msgWithRequest, _ := httpsign.NewMessage(httpsign.NewMessageConfig().WithRequest(req)) + _, err1 := msgWithRequest.Verify("sig77", *verifier) + + // Using constituent parts + msgWithConstituents, _ := httpsign.NewMessage(httpsign.NewMessageConfig(). + WithMethod(req.Method). + WithURL(req.URL). + WithHeaders(req.Header). + WithTrailers(req.Trailer). + WithBody(&req.Body). + WithAuthority(req.Host). + WithScheme(req.URL.Scheme)) + + _, err2 := msgWithConstituents.Verify("sig77", *verifier) + + fmt.Printf("WithRequest: %t\n", err1 == nil) + fmt.Printf("Constituents: %t", err2 == nil) + // Output: + // WithRequest: true + // Constituents: true +} diff --git a/signatures.go b/signatures.go index 4f96d08..2e54f57 100644 --- a/signatures.go +++ b/signatures.go @@ -3,11 +3,12 @@ package httpsign import ( "errors" "fmt" - "github.com/dunglas/httpsfv" "io" "net/http" "strings" "time" + + "github.com/dunglas/httpsfv" ) func signMessage(config SignConfig, signatureName string, signer Signer, parsedMessage, parsedAssocMessage *parsedMessage, @@ -358,28 +359,12 @@ func VerifyRequest(signatureName string, verifier Verifier, req *http.Request) e } func verifyRequestDebug(signatureName string, verifier Verifier, req *http.Request) (signatureBase string, err error) { - if req == nil { - return "", fmt.Errorf("nil request") - } - if signatureName == "" { - return "", fmt.Errorf("empty signature name") - } - withTrailers, wantSigRaw, psiSig, err := extractSignatureFields(signatureName, &verifier, req.Header, req.Trailer, &req.Body) + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) if err != nil { return "", err } - parsedMessage, err := parseRequest(req, withTrailers) - if err != nil { - return "", err - } - return verifyMessage(*verifier.config, verifier, parsedMessage, nil, verifier.fields, - wantSigRaw, psiSig) -} - -// MessageDetails aggregates the details of a signed message, for a given signature -type MessageDetails struct { - KeyID, Alg string - Fields Fields + signatureBase, _, err = verifyDebug(signatureName, verifier, msg) + return } // RequestDetails parses a signed request and returns the key ID and optionally the algorithm used in the given signature. @@ -397,6 +382,46 @@ func RequestDetails(signatureName string, req *http.Request) (details *MessageDe return signatureDetails(psiSig) } +func verifyDebug(signatureName string, verifier Verifier, message *Message) (string, *psiSignature, error) { + if message == nil { + return "", nil, fmt.Errorf("nil message") + } + if signatureName == "" { + return "", nil, fmt.Errorf("empty signature name") + } + + withTrailers, wantSigRaw, psiSig, err := extractSignatureFields( + signatureName, &verifier, message.headers, message.trailers, message.body) + if err != nil { + return "", nil, err + } + + var parsedMsg *parsedMessage + var parsedAssoc *parsedMessage + + // Parse the main message + parsedMsg, err = parseMessage(message, withTrailers) + if err != nil { + return "", nil, err + } + + // If there's an associated request, parse that too + if assocMsg := message.assocReq; assocMsg != nil { + parsedAssoc, err = parseMessage(assocMsg, false) + if err != nil { + return "", nil, err + } + } + + signatureBase, err := verifyMessage(*verifier.config, verifier, parsedMsg, parsedAssoc, + verifier.fields, wantSigRaw, psiSig) + if err != nil { + return "", nil, err + } + + return signatureBase, psiSig, nil +} + // ResponseDetails parses a signed response and returns the key ID and optionally the algorithm used in the given signature. func ResponseDetails(signatureName string, res *http.Response) (details *MessageDetails, err error) { if res == nil { @@ -475,11 +500,28 @@ func signatureDetails(signature *psiSignature) (details *MessageDetails, err err return nil, fmt.Errorf("malformed \"alg\" parameter") } } - return &MessageDetails{ + details = &MessageDetails{ KeyID: keyID, Alg: alg, Fields: signature.fields, - }, nil + } + + if created, ok := signature.params["created"].(int64); ok { + t := time.Unix(created, 0) + details.Created = &t + } + if expires, ok := signature.params["expires"].(int64); ok { + t := time.Unix(expires, 0) + details.Expires = &t + } + if nonce, ok := signature.params["nonce"].(string); ok { + details.Nonce = &nonce + } + if tag, ok := signature.params["tag"].(string); ok { + details.Tag = &tag + } + + return details, nil } // VerifyResponse verifies a signed HTTP response. Returns an error if verification failed for any reason, otherwise nil. @@ -489,30 +531,12 @@ func VerifyResponse(signatureName string, verifier Verifier, res *http.Response, } func verifyResponseDebug(signatureName string, verifier Verifier, res *http.Response, req *http.Request) (signatureBase string, err error) { - if res == nil { - return "", fmt.Errorf("nil response") - } - if signatureName == "" { - return "", fmt.Errorf("empty signature name") - } - resWithTrailers, wantSigRaw, psiSig, err := extractSignatureFields(signatureName, &verifier, res.Header, res.Trailer, &res.Body) + msg, err := NewMessage(NewMessageConfig().WithResponse(res, req)) if err != nil { return "", err } - parsedMessage, err := parseResponse(res, resWithTrailers) - if err != nil { - return "", err - } - // Read the associated request with trailers if the verifier requests its trailers, or there are signed trailer - // covered in the signature - reqWithTrailers := verifier.fields.hasTrailerFields(true) || psiSig.fields.hasTrailerFields(true) - parsedAssocMessage, err := parseRequest(req, reqWithTrailers) - if err != nil { - return "", err - } - signatureBase, err = verifyMessage(*verifier.config, verifier, parsedMessage, parsedAssocMessage, - verifier.fields, wantSigRaw, psiSig) - return signatureBase, err + signatureBase, _, err = verifyDebug(signatureName, verifier, msg) + return } func extractSignatureFields(signatureName string, verifier *Verifier, diff --git a/signatures_test.go b/signatures_test.go index 663476a..5f4c4f9 100644 --- a/signatures_test.go +++ b/signatures_test.go @@ -14,11 +14,12 @@ import ( "encoding/base64" "encoding/pem" "fmt" - "github.com/stretchr/testify/assert" "net/http" "strings" "testing" "time" + + "github.com/stretchr/testify/assert" ) var httpreq1 = `POST /foo?param=value&pet=dog HTTP/1.1 @@ -685,6 +686,25 @@ func TestSignAndVerifyHMAC(t *testing.T) { assert.NoError(t, err, "verification error") } +// Same as TestSignAndVerifyHMAC but using Message +func TestMessageSignAndVerifyHMAC(t *testing.T) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret") + fields := Headers("@authority", "date", "content-type") + signatureName := "sig1" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + signer, _ := NewHMACSHA256Signer(key, config, fields) + req := readRequest(httpreq1) + sigInput, sig, _ := SignRequest(signatureName, *signer, req) + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + assert.NoError(t, err, "verification error") +} + func TestSignAndVerifyHMACNoHeader(t *testing.T) { config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret") fields := Headers("@authority", "content-type") @@ -710,6 +730,34 @@ func TestSignAndVerifyHMACNoHeader(t *testing.T) { assert.Error(t, err, "verification should fail, header not found") } +// Same as TestSignAndVerifyHMACNoHeader but using Message +func TestMessageSignAndVerifyHMACNoHeader(t *testing.T) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret") + fields := Headers("@authority", "content-type") + signatureName := "sig1" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + signer, _ := NewHMACSHA256Signer(key, config, fields) + req := readRequest(longReq1) + _, sig, err := SignRequest(signatureName, *signer, req) + assert.NoError(t, err, "failed to sign") + req.Header.Add("Signature", sig) + verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + err = VerifyRequest(signatureName, *verifier, req) + assert.Error(t, err, "verification should fail, header not found") + + req = readRequest(longReq1) + sigInput, _, err := SignRequest(signatureName, *signer, req) + assert.NoError(t, err, "failed to sign") + req.Header.Add("Signature-Input", sigInput) + verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + assert.Error(t, err, "verification should fail, header not found") +} + func TestSignAndVerifyHMACBad(t *testing.T) { config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret") fields := Headers("@authority", "date", "content-type") @@ -727,6 +775,26 @@ func TestSignAndVerifyHMACBad(t *testing.T) { assert.Error(t, err, "verification should have failed") } +// Same as TestSignAndVerifyHMACBad but using Message +func TestMessageSignAndVerifyHMACBad(t *testing.T) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret") + fields := Headers("@authority", "date", "content-type") + signatureName := "sig1" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + signer, _ := NewHMACSHA256Signer(key, config, fields) + req := readRequest(httpreq1) + sigInput, sig, _ := SignRequest(signatureName, *signer, req) + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + badkey := append(key, byte(0x77)) + verifier, err := NewHMACSHA256Verifier(badkey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + assert.Error(t, err, "verification should have failed") +} + func TestCreated(t *testing.T) { testOnceWithConfig := func(t *testing.T, createdTime int64, verifyConfig *VerifyConfig, wantSuccess bool) { fields := Headers("@status", "date", "content-type") @@ -792,6 +860,76 @@ func TestCreated(t *testing.T) { t.Run("verify logic requires to verify Created", testDateFail) } +// Same as TestCreated but using Message +func TestMessageCreated(t *testing.T) { + testOnceWithConfig := func(t *testing.T, createdTime int64, verifyConfig *VerifyConfig, wantSuccess bool) { + fields := Headers("@status", "date", "content-type") + signatureName := "sigres" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + signConfig := NewSignConfig().SignCreated(true).setFakeCreated(createdTime).SetKeyID("test-shared-secret") + signer, _ := NewHMACSHA256Signer(key, signConfig, fields) + res := readResponse(httpres2) + nowStr := time.Now().UTC().Format(http.TimeFormat) + res.Header.Set("Date", nowStr) + sigInput, sig, _ := SignResponse(signatureName, *signer, res, nil) + + res2 := readResponse(httpres2) + res2.Header.Set("Date", nowStr) + res2.Header.Add("Signature", sig) + res2.Header.Add("Signature-Input", sigInput) + verifier, err := NewHMACSHA256Verifier(key, verifyConfig, fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithResponse(res2, nil)) + if err != nil { + t.Errorf("could not create Message: %s", err) + } + _, err = msg.Verify(signatureName, *verifier) + + if wantSuccess && err != nil { + t.Errorf("verification error: %s", err) + } + if !wantSuccess && err == nil { + t.Errorf("expected verification to fail") + } + } + testOnce := func(t *testing.T, createdTime int64, wantSuccess bool) { + testOnceWithConfig(t, createdTime, nil, wantSuccess) + } + now := time.Now().Unix() // the window is in ms, but "created" granularity is in sec! + testInWindow := func(t *testing.T) { testOnce(t, now, true) } + testOlder := func(t *testing.T) { testOnce(t, now-20_000, false) } + testNewer := func(t *testing.T) { testOnce(t, now+3_000, false) } + testOldWindow1 := func(t *testing.T) { + testOnceWithConfig(t, now-20_000, NewVerifyConfig().SetNotOlderThan(19_000*time.Second), false) + } + testOldWindow2 := func(t *testing.T) { + testOnceWithConfig(t, now-20_000, NewVerifyConfig().SetNotOlderThan(21_000*time.Second), true) + } + testNewWindow1 := func(t *testing.T) { + testOnceWithConfig(t, now+15_000, NewVerifyConfig().SetNotNewerThan(16_000*time.Second), true) + } + testNewWindow2 := func(t *testing.T) { + testOnceWithConfig(t, now+15_000, NewVerifyConfig().SetNotNewerThan(14_000*time.Second), false) + } + testDate := func(t *testing.T) { + testOnceWithConfig(t, now, NewVerifyConfig().SetVerifyDateWithin(100*time.Millisecond), true) + } + testDateFail := func(t *testing.T) { + testOnceWithConfig(t, now, NewVerifyConfig().SetVerifyCreated(false).SetVerifyDateWithin(100*time.Millisecond), false) + } + t.Run("in window", testInWindow) + t.Run("older", testOlder) + t.Run("newer", testNewer) + t.Run("older, smaller than window", testOldWindow1) + t.Run("older, larger than window", testOldWindow2) + t.Run("newer, smaller than window", testNewWindow1) + t.Run("newer, larger than window", testNewWindow2) + t.Run("verify Date header within window", testDate) + t.Run("verify logic requires to verify Created", testDateFail) +} + func TestSignAndVerifyResponseHMAC(t *testing.T) { fields := Headers("@status", "date", "content-type") signatureName := "sigres" @@ -814,6 +952,33 @@ func TestSignAndVerifyResponseHMAC(t *testing.T) { } } +// Same as TestSignAndVerifyResponseHMAC but using Message +func TestMessageSignAndVerifyResponseHMAC(t *testing.T) { + fields := Headers("@status", "date", "content-type") + signatureName := "sigres" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + config := NewSignConfig().SetExpires(999).SetKeyID("test-shared-secret") // should have expired long ago (but will be ignored by verifier) + signer, _ := NewHMACSHA256Signer(key, config, fields) // default config + res := readResponse(httpres2) + sigInput, sig, _ := SignResponse(signatureName, *signer, res, nil) + + res2 := readResponse(httpres2) + res2.Header.Add("Signature", sig) + res2.Header.Add("Signature-Input", sigInput) + verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetRejectExpired(false).SetKeyID("test-shared-secret"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithResponse(res2, nil)) + if err != nil { + t.Errorf("could not create Message: %s", err) + } + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + func TestSignAndVerifyRSAPSS(t *testing.T) { config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-key-rsa-pss") fields := Headers("@authority", "date", "content-type") @@ -841,6 +1006,36 @@ func TestSignAndVerifyRSAPSS(t *testing.T) { } } +// Same as TestSignAndVerifyRSAPSS but using Message +func TestMessageSignAndVerifyRSAPSS(t *testing.T) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-key-rsa-pss") + fields := Headers("@authority", "date", "content-type") + signatureName := "sig1" + prvKey, err := loadRSAPSSPrivateKey(rsaPSSPrvKey) + if err != nil { + t.Errorf("cannot read private key") + } + signer, _ := NewRSAPSSSigner(*prvKey, config, fields) + req := readRequest(httpreq1) + sigInput, sig, _ := SignRequest(signatureName, *signer, req) + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey) + if err != nil { + t.Errorf("cannot read public key: %v", err) + } + verifier, err := NewRSAPSSVerifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-rsa-pss"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + func TestSignAndVerifyRSA(t *testing.T) { config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-key-rsa") fields := Headers("@authority", "date", "content-type") @@ -868,6 +1063,36 @@ func TestSignAndVerifyRSA(t *testing.T) { } } +// Same as TestSignAndVerifyRSA but using Message +func TestMessageSignAndVerifyRSA(t *testing.T) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-key-rsa") + fields := Headers("@authority", "date", "content-type") + signatureName := "sig1" + prvKey, err := parseRsaPrivateKeyFromPemStr(rsaPrvKey2) + if err != nil { + t.Errorf("cannot read private key") + } + signer, _ := NewRSASigner(*prvKey, config, fields) + req := readRequest(httpreq1) + sigInput, sig, _ := SignRequest(signatureName, *signer, req) + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + pubKey, err := parseRsaPublicKeyFromPemStr(rsaPubKey2) + if err != nil { + t.Errorf("cannot read public key: %v", err) + } + verifier, err := NewRSAVerifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-rsa"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + func TestSignAndVerifyP256(t *testing.T) { config := NewSignConfig().setFakeCreated(1618884475).SetKeyID("test-key-p256") signatureName := "sig1" @@ -894,6 +1119,35 @@ func TestSignAndVerifyP256(t *testing.T) { } } +// Same as TestMessageSignAndVerifyP256 but using Message +func TestMessageSignAndVerifyP256(t *testing.T) { + config := NewSignConfig().setFakeCreated(1618884475).SetKeyID("test-key-p256") + signatureName := "sig1" + prvKey, pubKey, err := genP256KeyPair() + if err != nil { + t.Errorf("cannot generate P-256 keypair") + } + fields := *NewFields().AddHeader("@method").AddHeader("Date").AddHeader("Content-Type").AddQueryParam("pet") + signer, _ := NewP256Signer(*prvKey, config, fields) + req := readRequest(httpreq2) + sigInput, sig, err := SignRequest(signatureName, *signer, req) + if err != nil { + t.Errorf("signature failed: %v", err) + } + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + verifier, err := NewP256Verifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-p256"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + func TestSignAndVerifyP384(t *testing.T) { config := NewSignConfig().setFakeCreated(1618884475).SetKeyID("test-key-p384") signatureName := "sig1" @@ -920,6 +1174,35 @@ func TestSignAndVerifyP384(t *testing.T) { } } +// Same as TestSignAndVerifyP384 but using Message +func TestMessageSignAndVerifyP384(t *testing.T) { + config := NewSignConfig().setFakeCreated(1618884475).SetKeyID("test-key-p384") + signatureName := "sig1" + prvKey, pubKey, err := genP384KeyPair() + if err != nil { + t.Errorf("cannot generate P-384 keypair") + } + fields := *NewFields().AddHeader("@method").AddHeader("Date").AddHeader("Content-Type").AddQueryParam("pet") + signer, _ := NewP384Signer(*prvKey, config, fields) + req := readRequest(httpreq2) + sigInput, sig, err := SignRequest(signatureName, *signer, req) + if err != nil { + t.Errorf("signature failed: %v", err) + } + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + verifier, err := NewP384Verifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-p384"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + func TestSignAndVerifyEdDSA(t *testing.T) { pubKey1, prvKey1, err := ed25519.GenerateKey(nil) // Need some tweaking for RFC 8032 keys, see package doc if err != nil { @@ -964,20 +1247,67 @@ func signAndVerifyEdDSA(t *testing.T, signer *Signer, pubKey ed25519.PublicKey, } } -func TestSignResponse(t *testing.T) { - type args struct { - signatureName string - signer Signer - res *http.Response +// Same as TestSignAndVerifyEdDSA but using Message +func TestMessageSignAndVerifyEdDSA(t *testing.T) { + pubKey1, prvKey1, err := ed25519.GenerateKey(nil) // Need some tweaking for RFC 8032 keys, see package doc + if err != nil { + t.Errorf("cannot generate keypair: %s", err) } - tests := []struct { - name string - args args - want string - want1 string - wantErr bool - }{ - { + config := NewSignConfig().setFakeCreated(1618884475).SetKeyID("test-key-ed25519") + fields := *NewFields().AddHeader("@method").AddHeader("Date").AddHeader("Content-Type").AddQueryParam("pet") + signer1, _ := NewEd25519Signer(prvKey1, config, fields) + + messageSignAndVerifyEdDSA(t, signer1, pubKey1, fields) + + seed2 := make([]byte, ed25519.SeedSize) + _, err = rand.Read(seed2) + if err != nil { + t.Errorf("rand failed?") + } + config.SetKeyID("test-key-ed25519") + prvKey2 := ed25519.NewKeyFromSeed(seed2) + pubKey2 := prvKey2.Public().(ed25519.PublicKey) + + signer2, _ := NewEd25519SignerFromSeed(seed2, config, fields) + + messageSignAndVerifyEdDSA(t, signer2, pubKey2, fields) +} + +func messageSignAndVerifyEdDSA(t *testing.T, signer *Signer, pubKey ed25519.PublicKey, fields Fields) { + signatureName := "sig1" + req := readRequest(httpreq2) + sigInput, sig, err := SignRequest(signatureName, *signer, req) + if err != nil { + t.Errorf("signature failed: %v", err) + } + req.Header.Add("Signature", sig) + req.Header.Add("Signature-Input", sigInput) + verifier, err := NewEd25519Verifier(pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ed25519"), fields) + if err != nil { + t.Errorf("could not generate Verifier: %s", err) + } + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + if err != nil { + t.Errorf("verification error: %s", err) + } +} + +func TestSignResponse(t *testing.T) { + type args struct { + signatureName string + signer Signer + res *http.Response + } + tests := []struct { + name string + args args + want string + want1 string + wantErr bool + }{ + { name: "test response with HMAC", args: args{ signatureName: "sig1", @@ -1184,6 +1514,164 @@ func TestVerifyRequest(t *testing.T) { } } +// Same as TestVerifyRequest but using Message +func TestMessageVerifyRequest(t *testing.T) { + type args struct { + signatureName string + verifier Verifier + req *http.Request + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "test case B.2.1", + args: args{ + signatureName: "sig-b21", + verifier: makeRSAVerifier(t, "test-key-rsa-pss", *NewFields()), + req: readRequest(httpreq1pssMinimal), + }, + want: true, + wantErr: false, + }, + { + name: "test case B.2.2", + args: args{ + signatureName: "sig-b22", + verifier: (func() Verifier { + pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + verifier, _ := NewRSAPSSVerifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-rsa-pss"), *NewFields()) + return *verifier + })(), + req: readRequest(httpreq1pssSelective), + }, + want: true, + wantErr: false, + }, + { + name: "test case B.2.3", + args: args{ + signatureName: "sig-b23", + verifier: (func() Verifier { + pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + verifier, _ := NewRSAPSSVerifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-rsa-pss"), *NewFields()) + return *verifier + })(), + req: readRequest(httpreq1pssFull), + }, + want: true, + wantErr: false, + }, + { + name: "test case B.2.6", + args: args{ + signatureName: "sig-b26", + verifier: (func() Verifier { + prvKey, err := parseEdDSAPrivateKeyFromPemStr(ed25519PrvKey) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + pubKey := prvKey.Public().(ed25519.PublicKey) + verifier, _ := NewEd25519Verifier(pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ed25519"), *NewFields()) + return *verifier + })(), + req: readRequest(httpreq1ed25519), + }, + want: true, + wantErr: false, + }, + { + name: "test case B.3", // TLS-terminating proxy + args: args{ + signatureName: "ttrp", + verifier: (func() Verifier { + pubKey, _ := parseECPublicKeyFromPemStr(p256PubKey2) + verifier, _ := NewP256Verifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), *NewFields()) + return *verifier + })(), + req: readRequest(httpreqtlsproxy), + }, + want: true, + wantErr: false, + }, + { + name: "verify bad sig (not base64)", + args: args{ + signatureName: "sig1", + verifier: makeRSAVerifier(t, "test-key-rsa-pss", *NewFields()), + req: readRequest(httpreq1pssSelectiveBad), + }, + want: false, + wantErr: true, + }, + { + name: "missing fields", + args: args{ + signatureName: "sig1", + verifier: makeRSAVerifier(t, "test-key-rsa-pss", *NewFields().AddQueryParam("missing")), + req: readRequest(httpreq1pssMinimal), + }, + want: false, + wantErr: true, + }, + { + name: "bad keyID", + args: args{ + signatureName: "sig-b22", + verifier: (func() Verifier { + pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + verifier, _ := NewRSAPSSVerifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("bad-key-id"), *NewFields()) + return *verifier + })(), + req: readRequest(httpreq1pssSelective), + }, + want: false, + wantErr: true, + }, + { + name: "keyID not verified", // this is NOT a failure + args: args{ + signatureName: "sig-b22", + verifier: (func() Verifier { + pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + verifier, _ := NewRSAPSSVerifier(*pubKey, NewVerifyConfig(). + SetVerifyCreated(false), *NewFields()) + return *verifier + })(), + req: readRequest(httpreq1pssSelective), + }, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg, err := NewMessage(NewMessageConfig().WithRequest(tt.args.req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(tt.args.signatureName, tt.args.verifier) + if (err != nil) != tt.wantErr { + t.Errorf("VerifyRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + type failer interface { Errorf(format string, args ...any) } @@ -1345,6 +1833,42 @@ func TestDictionary(t *testing.T) { } } +// Same as TestDictionary but using Message +func TestMessageDictionary(t *testing.T) { + priv, pub, err := genP256KeyPair() + if err != nil { + t.Errorf("failed to generate key") + } + res := readResponse(httpres2) + res.Header.Set("X-Dictionary", "a=1, b=2;x=1;y=2, c=(a b c)") + signer2, err := NewP256Signer(*priv, NewSignConfig().SetKeyID("key10"), + *NewFields().AddHeader("@status").AddDictHeader("x-dictionary", "a")) + if err != nil { + t.Errorf("Could not create signer") + } + sigInput2, sig2, err := SignResponse("sig2", *signer2, res, nil) + if err != nil { + t.Errorf("Could not sign response: %v", err) + } + res.Header.Add("Signature-Input", sigInput2) + res.Header.Add("Signature", sig2) + + // Client verifies response + verifier2, err := NewP256Verifier(*pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key10"), + *NewFields().AddHeader("@status").AddDictHeader("x-dictionary", "a")) + if err != nil { + t.Errorf("Could not create verifier: %v", err) + } + msg, err := NewMessage(NewMessageConfig().WithResponse(res, nil)) + if err != nil { + t.Errorf("could not create Message: %s", err) + } + _, err = msg.Verify("sig2", *verifier2) + if err != nil { + t.Errorf("Could not verify response: %v", err) + } +} + func TestMultipleSignaturesOld(t *testing.T) { priv1, _, err := genP256KeyPair() // no pub, no verify if err != nil { @@ -1418,6 +1942,28 @@ func TestMultipleSignatures(t *testing.T) { assert.NoError(t, err, "proxy signature not verified") } +// Same as TestMultipleSignatures but using Message +func TestMessageMultipleSignatures(t *testing.T) { + msg, err := NewMessage(NewMessageConfig().WithRequest(readRequest(httpreq9))) + assert.NoError(t, err, "cannot create message") + pubKey1, err := parseECPublicKeyFromPemStr(p256PubKey2) + assert.NoError(t, err, "cannot parse ECC public key") + verifier1, err := NewP256Verifier(*pubKey1, NewVerifyConfig(). + SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), Headers("@method", "@authority", "@path", "content-digest", + "content-type", "content-length")) + assert.NoError(t, err, "cannot create verifier1") + _, _, err = verifyDebug("sig1", *verifier1, msg) + assert.Error(t, err, "sig1 cannot be verified, because the proxy modified the authority field") + + pubKey2, err := parseRsaPublicKey(rsaPubKey) + assert.NoError(t, err, "cannot parse RSA public key") + verifier2, err := NewRSAVerifier(*pubKey2, NewVerifyConfig(). + SetVerifyCreated(false).SetRejectExpired(false).SetKeyID("test-key-rsa"), *NewFields().AddDictHeader("Signature", "sig1").AddHeaders("@authority", "forwarded")) + assert.NoError(t, err, "cannot create verifier2") + _, _, err = verifyDebug("proxy_sig", *verifier2, msg) + assert.NoError(t, err, "proxy signature not verified") +} + func fold(vs []string) string { return strings.Join(vs, ",") } @@ -1631,13 +2177,55 @@ func TestVerifyResponse(t *testing.T) { } } -func TestOptionalSign(t *testing.T) { - req := readRequest(httpreq2) - f1 := NewFields().AddHeader("date").AddHeaderOptional("x-optional") - key1 := bytes.Repeat([]byte{0x55}, 64) - signer1, err := NewHMACSHA256Signer(key1, NewSignConfig().setFakeCreated(9999).SetKeyID("key1"), *f1) - assert.NoError(t, err, "Could not create signer") - signatureInput, _, signatureBase, err := signRequestDebug("sig1", *signer1, req) +// Same as TestVerifyResponse but using Message +func TestMessageVerifyResponse(t *testing.T) { + type args struct { + signatureName string + verifier Verifier + res *http.Response + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "test case B.2.4", + args: args{ + signatureName: "sig-b24", + verifier: (func() Verifier { + pubKey, err := parseECPublicKeyFromPemStr(p256PubKey2) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + verifier, _ := NewP256Verifier(*pubKey, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), *NewFields()) + return *verifier + })(), + res: readResponse(httpres4), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg, err := NewMessage(NewMessageConfig().WithResponse(tt.args.res, nil)) + if err != nil { + t.Errorf("could not create Message: %s", err) + } + if _, err = msg.Verify(tt.args.signatureName, tt.args.verifier); (err != nil) != tt.wantErr { + t.Errorf("VerifyResponse() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestOptionalSign(t *testing.T) { + req := readRequest(httpreq2) + f1 := NewFields().AddHeader("date").AddHeaderOptional("x-optional") + key1 := bytes.Repeat([]byte{0x55}, 64) + signer1, err := NewHMACSHA256Signer(key1, NewSignConfig().setFakeCreated(9999).SetKeyID("key1"), *f1) + assert.NoError(t, err, "Could not create signer") + signatureInput, _, signatureBase, err := signRequestDebug("sig1", *signer1, req) assert.NoError(t, err, "Should not fail with optional header absent") assert.Equal(t, "sig1=(\"date\");created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", signatureInput) assert.Equal(t, "\"date\": Tue, 20 Apr 2021 02:07:55 GMT\n\"@signature-params\": (\"date\");created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", signatureBase) @@ -1699,6 +2287,31 @@ func TestAssocMessage(t *testing.T) { assert.NoError(t, err, "Verification should succeed") } +// Same as TestAssocMessage but using Message +func TestMessageAssocRequest(t *testing.T) { + key1 := bytes.Repeat([]byte{0x66}, 64) + assocReq := readRequest(httpreq2) + res1 := readResponse(httpres2) + res1.Header.Set("X-Dictionary", "a=1, b=2;x=1;y=2, c=(a b c)") + f3 := NewFields().AddDictHeaderExt("x-dictionary", "a", true, false, false).AddDictHeaderExt("x-dictionary", "zz", true, false, false). + AddQueryParamExt("pet", false, true, false) + signer3, err := NewHMACSHA256Signer(key1, NewSignConfig().setFakeCreated(9999).SetKeyID("key1"), *f3) + assert.NoError(t, err, "Could not create signer") + signatureInput, signature, signatureBase, err := signResponseDebug("sig1", *signer3, res1, assocReq) + assert.NoError(t, err, "Should not fail with dict headers") + assert.Equal(t, "sig1=(\"x-dictionary\";key=\"a\" \"@query-param\";name=\"pet\";req);created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", signatureInput) + assert.Equal(t, "\"x-dictionary\";key=\"a\": 1\n\"@query-param\";name=\"pet\";req: dog\n\"@query-param\";name=\"pet\";req: snake\n\"@signature-params\": (\"x-dictionary\";key=\"a\" \"@query-param\";name=\"pet\";req);created=9999;alg=\"hmac-sha256\";keyid=\"key1\"", signatureBase) + res1.Header.Add("Signature-Input", signatureInput) + res1.Header.Add("Signature", signature) + + verifier, err := NewHMACSHA256Verifier(key1, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), *f3) + assert.NoError(t, err, "Should create verifier") + msg, err := NewMessage(NewMessageConfig().WithResponse(res1, assocReq)) + assert.NoError(t, err, "Should create message") + _, err = msg.Verify("sig1", *verifier) + assert.NoError(t, err, "Verification should succeed") +} + var httpreq6 = `POST /foo?param=Value&Pet=dog HTTP/1.1 Host: example.com Date: Tue, 20 Apr 2021 02:07:55 GMT @@ -1774,6 +2387,27 @@ func TestRequestBinding(t *testing.T) { assert.NoError(t, err, "verify response") } +// Same as TestRequestBinding but using Message +func TestMessageRequestBinding(t *testing.T) { + req := readRequest(httpreq6) + contentDigest := req.Header.Values("Content-Digest") + err := ValidateContentDigestHeader(contentDigest, &req.Body, []string{DigestSha512}) + assert.NoError(t, err, "validate digest") + + res := readResponse(httpres6) + pubKey2, err := parseECPublicKeyFromPemStr(p256PubKey2) + assert.NoError(t, err, "read pub key") + fields2 := *NewFields() + verifier2, err := NewP256Verifier(*pubKey2, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), fields2) + assert.NoError(t, err, "create verifier") + msg, err := NewMessage(NewMessageConfig().WithResponse(res, req)) + if err != nil { + t.Errorf("could not create Message: %s", err) + } + _, err = msg.Verify("reqres", *verifier2) + assert.NoError(t, err, "verify response") +} + func TestOptionalVerify(t *testing.T) { req := readRequest(httpreq2) req.Header.Add("X-Opt1", "val1") @@ -1813,6 +2447,54 @@ func TestOptionalVerify(t *testing.T) { assert.NoError(t, err, "Should not fail: absent and not signed") } +// Same as TestOptionalVerify but using Message +func TestMessageOptionalVerify(t *testing.T) { + req := readRequest(httpreq2) + req.Header.Add("X-Opt1", "val1") + f1 := NewFields().AddHeader("date").AddHeaderOptional("x-opt1") + key1 := bytes.Repeat([]byte{0x66}, 64) + signer, err := NewHMACSHA256Signer(key1, NewSignConfig().setFakeCreated(8888).SetKeyID("key1"), *f1) + assert.NoError(t, err, "Could not create signer") + sigInput, signature, err := SignRequest("sig1", *signer, req) + assert.NoError(t, err, "Should not fail with optional header present") + req.Header.Add("Signature-Input", sigInput) + req.Header.Add("Signature", signature) + + verifier, err := NewHMACSHA256Verifier(key1, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), *f1) + assert.NoError(t, err, "Could not create verifier") + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify("sig1", *verifier) + assert.NoError(t, err, "Should not fail: present and signed") + + req.Header.Del("X-Opt1") // header absent but included in covered components + msg, err = NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify("sig1", *verifier) + assert.Error(t, err, "Should fail: absent and signed") + + req = readRequest(httpreq2) // header present but not signed + req.Header.Add("X-Opt1", "val1") + f2 := NewFields().AddHeader("date") // without the optional header + signer, err = NewHMACSHA256Signer(key1, NewSignConfig().setFakeCreated(2222).SetKeyID("key1"), *f2) + assert.NoError(t, err, "Should not fail to create Signer") + sigInput, signature, err = SignRequest("sig1", *signer, req) + assert.NoError(t, err, "Should not fail with redundant header present") + req.Header.Add("Signature-Input", sigInput) + req.Header.Add("Signature", signature) + + msg, err = NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify("sig1", *verifier) + assert.Error(t, err, "Should fail: present and not signed") + + req.Header.Del("X-Opt1") + msg, err = NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify("sig1", *verifier) + assert.NoError(t, err, "Should not fail: absent and not signed") +} + func TestBinarySequence(t *testing.T) { priv, pub, err := genP256KeyPair() assert.NoError(t, err, "failed to generate key") @@ -1851,6 +2533,49 @@ func TestBinarySequence(t *testing.T) { assert.NoError(t, err, "could not verify response") } +// Same as TestBinarySequence but using Message +func TestMessageBinarySequence(t *testing.T) { + priv, pub, err := genP256KeyPair() + assert.NoError(t, err, "failed to generate key") + res := readResponse(httpres2) + res.Header.Add("Set-Cookie", "a=1, b=2;x=1;y=2, c=(a b c)") + res.Header.Add("Set-Cookie", "d=5, eee") + + // First signature try fails + signer1, err := NewP256Signer(*priv, NewSignConfig().SetKeyID("key20"), + *NewFields().AddHeader("@status").AddHeaderExt("set-cookie", false, false, false, false)) + assert.NoError(t, err, "could not create signer") + _, _, err = SignResponse("sig2", *signer1, res, nil) + assert.Error(t, err, "signature should have failed") + + signer2, err := NewP256Signer(*priv, NewSignConfig().setFakeCreated(1659563420).SetKeyID("key20"), + *NewFields().AddHeader("@status").AddHeaderExt("set-cookie", false, true, false, false)) + assert.NoError(t, err, "could not create signer") + sigInput, sig, sigBase, err := signResponseDebug("sig2", *signer2, res, nil) + assert.NoError(t, err, "could not sign response") + assert.Equal(t, "\"@status\": 200\n\"set-cookie\";bs: :YT0xLCBiPTI7eD0xO3k9MiwgYz0oYSBiIGMp:, :ZD01LCBlZWU=:\n\"@signature-params\": (\"@status\" \"set-cookie\";bs);created=1659563420;alg=\"ecdsa-p256-sha256\";keyid=\"key20\"", sigBase, "unexpected signature base") + res.Header.Add("Signature-Input", sigInput) + res.Header.Add("Signature", sig) + + // Client verifies response - should fail + verifier1, err := NewP256Verifier(*pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key20"), + *NewFields().AddHeader("@status").AddHeaderExt("set-cookie", false, false, false, false)) + assert.NoError(t, err, "could not create verifier") + msg, err := NewMessage(NewMessageConfig().WithResponse(res, nil)) + assert.NoError(t, err, "could not create message") + _, err = msg.Verify("sig2", *verifier1) + assert.Error(t, err, "binary sequence verified as non-bs") + + // Client verifies response - should succeed + verifier2, err := NewP256Verifier(*pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key20"), + *NewFields().AddHeader("@status").AddHeaderExt("set-cookie", false, true, false, false)) + assert.NoError(t, err, "could not create verifier") + msg, err = NewMessage(NewMessageConfig().WithResponse(res, nil)) + assert.NoError(t, err, "could not create message") + _, err = msg.Verify("sig2", *verifier2) + assert.NoError(t, err, "could not verify response") +} + func TestSignatureTag(t *testing.T) { priv, pub, err := genP256KeyPair() assert.NoError(t, err, "failed to generate key") @@ -1901,6 +2626,63 @@ func TestSignatureTag(t *testing.T) { assert.Error(t, err, "should have failed to verify response") } +// Same as TestSignatureTag but using Message +func TestMessageSignatureTag(t *testing.T) { + priv, pub, err := genP256KeyPair() + assert.NoError(t, err, "failed to generate key") + res := readResponse(httpres2) + + signer1, err := NewP256Signer(*priv, NewSignConfig().SetTag("ctx1").setFakeCreated(1660755826).SetKeyID("key21"), + *NewFields().AddHeader("@status")) + assert.NoError(t, err, "could not create signer") + sigInput, sig, sigBase, err := signResponseDebug("sig2", *signer1, res, nil) + assert.NoError(t, err, "signature failed") + assert.Equal(t, "\"@status\": 200\n\"@signature-params\": (\"@status\");created=1660755826;alg=\"ecdsa-p256-sha256\";tag=\"ctx1\";keyid=\"key21\"", sigBase, "unexpected signature base") + res.Header.Add("Signature-Input", sigInput) + res.Header.Add("Signature", sig) + + // Signature should fail with malformed tag + signer2, err := NewP256Signer(*priv, NewSignConfig().SetTag("ctx1\x00").SetKeyID("key21"), + *NewFields().AddHeader("@status")) + assert.NoError(t, err, "could not create signer") + _, _, _, err = signResponseDebug("sig2", *signer2, res, nil) + assert.Error(t, err, "signature should fail") + + // Signature should fail when the key ID is an empty string + signer3, err := NewP256Signer(*priv, NewSignConfig().SetKeyID(""), + *NewFields().AddHeader("@status")) + assert.NoError(t, err, "could not create signer") + _, _, _, err = signResponseDebug("sig2", *signer3, res, nil) + assert.Error(t, err, "signature should fail") + + // Client verifies response - should succeed, no tag constraint + verifier1, err := NewP256Verifier(*pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key21"), + *NewFields().AddHeader("@status")) + assert.NoError(t, err, "could not create verifier") + msg, err := NewMessage(NewMessageConfig().WithResponse(res, nil)) + assert.NoError(t, err, "could not create message") + _, err = msg.Verify("sig2", *verifier1) + assert.NoError(t, err, "failed to verify response") + + // Client verifies response - should succeed, correct tag + verifier2, err := NewP256Verifier(*pub, NewVerifyConfig().SetVerifyCreated(false).SetAllowedTags([]string{"ctx3", "ctx2", "ctx1"}).SetKeyID("key21"), + *NewFields().AddHeader("@status")) + assert.NoError(t, err, "could not create verifier") + msg, err = NewMessage(NewMessageConfig().WithResponse(res, nil)) + assert.NoError(t, err, "could not create message") + _, err = msg.Verify("sig2", *verifier2) + assert.NoError(t, err, "failed to verify response") + + // Client verifies response - should fail, incorrect tags + verifier3, err := NewP256Verifier(*pub, NewVerifyConfig().SetVerifyCreated(false).SetAllowedTags([]string{"ctx5", "ctx6", "ctx7"}).SetKeyID("key21"), + *NewFields().AddHeader("@status")) + assert.NoError(t, err, "could not create verifier") + msg, err = NewMessage(NewMessageConfig().WithResponse(res, nil)) + assert.NoError(t, err, "could not create message") + _, err = msg.Verify("sig2", *verifier3) + assert.Error(t, err, "should have failed to verify response") +} + var httpTransform1 = `GET /demo?name1=Value1&Name2=value2 HTTP/1.1 Host: example.org Date: Fri, 15 Jul 2022 14:24:55 GMT @@ -1988,6 +2770,36 @@ func TestTransformations(t *testing.T) { testOneTransformation(t, httpTransform6, false) } +func testMessageOneTransformation(t *testing.T, reqStr string, verifies bool) { + // Initial verification successful + prvKey, err := parseEdDSAPrivateKeyFromPemStr(ed25519PrvKey) + if err != nil { + t.Errorf("cannot parse public key: %v", err) + } + pubKey := prvKey.Public().(ed25519.PublicKey) + verifier, err := NewEd25519Verifier(pubKey, NewVerifyConfig().SetVerifyCreated(false), *NewFields()) + assert.NoError(t, err, "could not create verifier") + req := readRequest(reqStr) + msg, err := NewMessage(NewMessageConfig().WithRequest(req)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify("transform", *verifier) + if verifies { + assert.NoError(t, err, "failed to verify request") + } else { + assert.Error(t, err, "should fail to verify request") + } +} + +// Same as TestTransformations but using Message +func TestMessageTransformations(t *testing.T) { + testMessageOneTransformation(t, httpTransform1, true) + testMessageOneTransformation(t, httpTransform2, true) + testMessageOneTransformation(t, httpTransform3, true) + testMessageOneTransformation(t, httpTransform4, true) + testMessageOneTransformation(t, httpTransform5, false) + testMessageOneTransformation(t, httpTransform6, false) +} + var httpreq10 = `GET /parameters?var=this%20is%20a%20big%0Amultiline%20value&bar=with+plus+whitespace&fa%C3%A7ade%22%3A%20=something HTTP/1.1 Host: www.example.com Date: Tue, 20 Apr 2021 02:07:56 GMT @@ -2064,6 +2876,42 @@ func TestRequestBinding17(t *testing.T) { assert.NoError(t, err, "validate response digest") } +// Same as TestRequestBinding17 but using Message +func TestMessageRequestBinding17(t *testing.T) { + req := readRequest(httpreq11) + reqContentDigest := req.Header.Values("Content-Digest") + err := ValidateContentDigestHeader(reqContentDigest, &req.Body, []string{DigestSha512}) + assert.NoError(t, err, "validate request digest") + + res := readResponse(httpres9) + msg, err := NewMessage(NewMessageConfig().WithResponse(res, req)) + assert.NoError(t, err, "cannot create message") + pubKey2, err := parseECPublicKeyFromPemStr(p256PubKey2) + assert.NoError(t, err, "read pub key") + fields2 := *NewFields().AddHeaders("@status", "content-digest", "content-type"). + AddHeaderExt("@authority", false, false, true, false). + AddHeaderExt("@method", false, false, true, false). + AddHeaderExt("@path", false, false, true, false). + AddHeaderExt("content-digest", false, false, true, false) + verifier2, err := NewP256Verifier(*pubKey2, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), fields2) + assert.NoError(t, err, "create verifier") + sigBase, _, err := verifyDebug("reqres", *verifier2, msg) + expected := `"@status": 503 +"content-digest": sha-512=:0Y6iCBzGg5rZtoXS95Ijz03mslf6KAMCloESHObfwnHJDbkkWWQz6PhhU9kxsTbARtY2PTBOzq24uJFpHsMuAg==: +"content-type": application/json +"@authority";req: example.com +"@method";req: POST +"@path";req: /foo +"content-digest";req: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==: +"@signature-params": ("@status" "content-digest" "content-type" "@authority";req "@method";req "@path";req "content-digest";req);created=1618884479;keyid="test-key-ecc-p256"` + assert.NoError(t, err, "verify response") + assert.Equal(t, expected, sigBase, "Incorrect signature base for response") + + responseContentDigest := res.Header.Values("Content-Digest") + err = ValidateContentDigestHeader(responseContentDigest, &res.Body, []string{DigestSha512}) + assert.NoError(t, err, "validate response digest") +} + var httpreq12 = `POST /foo?param=Value&Pet=dog HTTP/1.1 Host: origin.host.internal.example Date: Tue, 20 Apr 2021 02:07:56 GMT @@ -2115,6 +2963,34 @@ func TestMultipleSignatures17(t *testing.T) { assert.NoError(t, err, "sig1 should verify for the original message that the proxy received") } +// Same as TestMultipleSignatures17 but using Message +func TestMessageMultipleSignatures17(t *testing.T) { + msg, err := NewMessage(NewMessageConfig().WithRequest(readRequest(httpreq12))) + assert.NoError(t, err, "cannot create message") + pubKey1, err := parseECPublicKeyFromPemStr(p256PubKey2) + assert.NoError(t, err, "cannot parse ECC public key") + verifier1, err := NewP256Verifier(*pubKey1, NewVerifyConfig(). + SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), Headers("@method", "@authority", "@path", "content-digest", + "content-type", "content-length")) + assert.NoError(t, err, "cannot create verifier1") + _, _, err = verifyDebug("sig1", *verifier1, msg) + assert.Error(t, err, "sig1 cannot be verified, because the proxy modified the authority field") + + pubKey2, err := parseRsaPublicKey(rsaPubKey) + assert.NoError(t, err, "cannot parse RSA public key") + verifier2, err := NewRSAVerifier(*pubKey2, NewVerifyConfig(). + SetVerifyCreated(false).SetRejectExpired(false), *NewFields().AddHeaders("@authority", "forwarded")) + assert.NoError(t, err, "cannot create verifier2") + _, _, err = verifyDebug("proxy_sig", *verifier2, msg) + assert.NoError(t, err, "proxy signature not verified") + + msg, err = NewMessage(NewMessageConfig().WithRequest(readRequest(httpreq13))) + assert.NoError(t, err, "cannot create message") + sigBase, _, err := verifyDebug("sig1", *verifier1, msg) + assert.NotEmpty(t, sigBase) + assert.NoError(t, err, "sig1 should verify for the original message that the proxy received") +} + var httpreq14 = `POST /foo?param=Value&Pet=dog HTTP/1.1 Host: example.com Date: Tue, 20 Apr 2021 02:07:55 GMT @@ -2176,3 +3052,43 @@ func TestRequestBindingSignedResponse17(t *testing.T) { err = ValidateContentDigestHeader(responseContentDigest, &res.Body, []string{DigestSha512}) assert.NoError(t, err, "validate response digest") } + +// Same as TestRequestBindingSignedResponse17 but using Message +func TestMessageRequestBindingSignedResponse17(t *testing.T) { + req := readRequest(httpreq14) + reqContentDigest := req.Header.Values("Content-Digest") + err := ValidateContentDigestHeader(reqContentDigest, &req.Body, []string{DigestSha512}) + assert.NoError(t, err, "validate request digest") + + res := readResponse(httpres10) + msg, err := NewMessage(NewMessageConfig().WithResponse(res, req)) + pubKey2, err := parseECPublicKeyFromPemStr(p256PubKey2) + assert.NoError(t, err, "read pub key") + fields2 := *NewFields().AddHeaders("@status", "content-digest", "content-type"). + AddHeaderExt("@authority", false, false, true, false). + AddHeaderExt("@method", false, false, true, false). + AddHeaderExt("@path", false, false, true, false). + AddHeaderExt("@query", false, false, true, false). + AddHeaderExt("content-digest", false, false, true, false) + verifier2, err := NewP256Verifier(*pubKey2, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-key-ecc-p256"), fields2) + assert.NoError(t, err, "create verifier") + sigBase, _, err := verifyDebug("reqres", *verifier2, msg) + expected := `"@status": 503 +"content-digest": sha-512=:0Y6iCBzGg5rZtoXS95Ijz03mslf6KAMCloESHObfwnHJDbkkWWQz6PhhU9kxsTbARtY2PTBOzq24uJFpHsMuAg==: +"content-type": application/json +"@authority";req: example.com +"@method";req: POST +"@path";req: /foo +"@query";req: ?param=Value&Pet=dog +"content-digest";req: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==: +"content-type";req: application/json +"content-length";req: 18 +"@signature-params": ("@status" "content-digest" "content-type" "@authority";req "@method";req "@path";req "@query";req "content-digest";req "content-type";req "content-length";req);created=1618884479;keyid="test-key-ecc-p256"` + + assert.NoError(t, err, "verify response") + assert.Equal(t, expected, sigBase, "Incorrect signature base for response") + + responseContentDigest := res.Header.Values("Content-Digest") + err = ValidateContentDigestHeader(responseContentDigest, &res.Body, []string{DigestSha512}) + assert.NoError(t, err, "validate response digest") +} diff --git a/signaturesex_test.go b/signaturesex_test.go index ab6df42..869d65f 100644 --- a/signaturesex_test.go +++ b/signaturesex_test.go @@ -4,9 +4,10 @@ import ( "bufio" "bytes" "fmt" - "github.com/yaronf/httpsign" "net/http" "strings" + + "github.com/yaronf/httpsign" ) func ExampleSignRequest() { @@ -44,3 +45,23 @@ Signature: sig77=:3e9KqLP62NHfHY5OMG4036+U6tvBowZF35ALzTjpsf0=: fmt.Printf("verified: %t", err == nil) // Output: verified: true } + +// Same as ExampleVerifyRequest but using Message +func ExampleMessageVerifyRequest() { + config := httpsign.NewVerifyConfig().SetKeyID("my-shared-secret").SetVerifyCreated(false) // for testing only + verifier, _ := httpsign.NewHMACSHA256Verifier(bytes.Repeat([]byte{0x77}, 64), config, + httpsign.Headers("@authority", "Date", "@method")) + reqStr := `GET /foo HTTP/1.1 +Host: example.org +Date: Tue, 20 Apr 2021 02:07:55 GMT +Cache-Control: max-age=60 +Signature-Input: sig77=("@authority" "date" "@method");alg="hmac-sha256";keyid="my-shared-secret" +Signature: sig77=:3e9KqLP62NHfHY5OMG4036+U6tvBowZF35ALzTjpsf0=: + +` + req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr))) + msg, _ := httpsign.NewMessage(httpsign.NewMessageConfig().WithRequest(req)) + _, err := msg.Verify("sig77", *verifier) + fmt.Printf("verified: %t", err == nil) + // Output: verified: true +} diff --git a/trailer_test.go b/trailer_test.go index efbc100..2945333 100644 --- a/trailer_test.go +++ b/trailer_test.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/base64" "fmt" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "net/url" "strings" "testing" + + "github.com/stretchr/testify/assert" ) var rawPost1 = `POST /foo HTTP/1.1 @@ -173,3 +174,50 @@ func TestTrailer_SigFields(t *testing.T) { err = VerifyRequest(signatureName, *verifier, req2) assert.Error(t, err, "verification error") } + +// Same as TestTrailer_SigFields but using Message +func TestMessageTrailer_SigFields(t *testing.T) { + config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret") + fields := Headers("@authority", "@method", "content-type") + signatureName := "sig1" + key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==") + signer, _ := NewHMACSHA256Signer(key, config, fields) + req := readRequest(rawPost2) + sigInput, sig, err := SignRequest(signatureName, *signer, req) + assert.NoError(t, err, "signature failed") + // Add signature correctly + signedMessage := rawPost2 + "Signature: " + sig + "\n" + "Signature-Input: " + sigInput + "\n\n" + signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input", + 1) + req2 := readRequestChunked(signedMessage) + verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err := NewMessage(NewMessageConfig().WithRequest(req2)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + assert.NoError(t, err, "verification error") + + // Missing Signature-Input + signedMessage = rawPost2 + "Signature: " + sig + "\n\n" + signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input", + 1) + req2 = readRequestChunked(signedMessage) + verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err = NewMessage(NewMessageConfig().WithRequest(req2)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + assert.Error(t, err, "verification error") + + // Missing Signature + signedMessage = rawPost2 + "Signature-Input: " + sigInput + "\n\n" + signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input", + 1) + req2 = readRequestChunked(signedMessage) + verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields) + assert.NoError(t, err, "could not generate Verifier") + msg, err = NewMessage(NewMessageConfig().WithRequest(req2)) + assert.NoError(t, err, "could not create Message") + _, err = msg.Verify(signatureName, *verifier) + assert.Error(t, err, "verification error") +}