Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
42 changes: 39 additions & 3 deletions crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
176 changes: 175 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 @@ -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
Expand Down Expand Up @@ -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")
}
}
})
}
60 changes: 59 additions & 1 deletion 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 @@ -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 {
Expand Down Expand Up @@ -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")
}
Loading