Skip to content

Commit d569067

Browse files
authored
Merge pull request #14 from jvatic/verify
feat: Add alternative verification API and expand MessageDetails
2 parents 412ad6f + d6f8d15 commit d569067

File tree

12 files changed

+1725
-142
lines changed

12 files changed

+1725
-142
lines changed

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# See https://editorconfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
# Go files
7+
[*._test.go]
8+
# Preserve trailing whitespace in tests since some depend on it
9+
trim_trailing_whitespace = false

crypto_test.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"crypto/ed25519"
55
"crypto/rand"
66
"crypto/rsa"
7-
"github.com/lestrrat-go/jwx/v2/jwa"
8-
"github.com/lestrrat-go/jwx/v2/jws"
9-
"github.com/stretchr/testify/assert"
107
"reflect"
118
"strings"
129
"testing"
10+
11+
"github.com/lestrrat-go/jwx/v2/jwa"
12+
"github.com/lestrrat-go/jwx/v2/jws"
13+
"github.com/stretchr/testify/assert"
1314
)
1415

1516
func TestNewHMACSHA256Signer(t *testing.T) {
@@ -162,6 +163,41 @@ func TestForeignSigner(t *testing.T) {
162163
}
163164
}
164165

166+
// Same as TestForeignSigner but using Message
167+
func TestMessageForeignSigner(t *testing.T) {
168+
priv, pub, err := genP256KeyPair()
169+
if err != nil {
170+
t.Errorf("Failed to generate keypair: %v", err)
171+
}
172+
173+
config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false)
174+
signatureName := "sig1"
175+
fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet")
176+
signer, err := NewJWSSigner(jwa.ES256, priv, config.SetKeyID("key1"), fields)
177+
if err != nil {
178+
t.Errorf("Failed to create JWS signer")
179+
}
180+
req := readRequest(httpreq2)
181+
sigInput, sig, err := SignRequest(signatureName, *signer, req)
182+
if err != nil {
183+
t.Errorf("signature failed: %v", err)
184+
}
185+
req.Header.Add("Signature", sig)
186+
req.Header.Add("Signature-Input", sigInput)
187+
verifier, err := NewJWSVerifier(jwa.ES256, pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
188+
if err != nil {
189+
t.Errorf("could not generate Verifier: %s", err)
190+
}
191+
msg, err := NewMessage(NewMessageConfig().WithRequest(req))
192+
if err != nil {
193+
t.Errorf("Failed to create Message")
194+
}
195+
_, err = msg.Verify(signatureName, *verifier)
196+
if err != nil {
197+
t.Errorf("verification error: %s", err)
198+
}
199+
}
200+
165201
func makeRSAPrivateKey() *rsa.PrivateKey {
166202
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
167203
return priv

doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
// WrapHandler installs a wrapper around a normal HTTP message handler.
77
// Digest functionality (creation and validation of the Content-Digest header) is available automatically
88
// through the Client and WrapHandler interfaces, otherwise it is available separately.
9+
// Use Message and its Verify method if you need more flexibility such as in a non-HTTP context.
910
package httpsign

fuzz_test.go

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

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

912
var httpreq1pssNoSig = `POST /foo?param=Value&Pet=dog HTTP/1.1
@@ -55,6 +58,50 @@ func FuzzVerifyRequest(f *testing.F) {
5558
})
5659
}
5760

61+
// Same as FuzzVerifyRequest but using Message
62+
func FuzzMessageVerifyRequest(f *testing.F) {
63+
type inputs struct {
64+
req, sigInput, sig string
65+
}
66+
testcases := []inputs{
67+
{httpreq1pssNoSig,
68+
"sig-b21=();created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"b3k2pp5k7z-50gnwp.yemd\"",
69+
"sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
70+
},
71+
{httpreq1pssNoSig,
72+
"sig-b21=(date);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
73+
"sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
74+
},
75+
{httpreq1pssNoSig,
76+
"sig-b21=(some-field;tr);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
77+
"sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
78+
},
79+
{httpreq1pssNoSig,
80+
"sig-b22=(some-field;tr;bs);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
81+
"sig-b22=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
82+
},
83+
}
84+
for _, tc := range testcases {
85+
f.Add(tc.req, tc.sigInput, tc.sig) // Use f.Add to provide a seed corpus
86+
}
87+
f.Fuzz(func(t *testing.T, reqString, sigInput, sig string) {
88+
req := readRequest(reqString)
89+
if req != nil {
90+
req.Header.Set("Signature-Input", sigInput)
91+
req.Header.Set("Signature", sig)
92+
}
93+
94+
sigName := "sig-b21"
95+
verifier := makeRSAVerifier(f, "key1", *NewFields())
96+
msg, err := NewMessage(NewMessageConfig().WithRequest(req))
97+
if err != nil {
98+
t.Errorf("Failed to create Message")
99+
}
100+
_, _ = msg.Verify(sigName, verifier)
101+
// only report panics
102+
})
103+
}
104+
58105
func FuzzSignAndVerifyHMAC(f *testing.F) {
59106
type inputs struct {
60107
req string
@@ -83,3 +130,130 @@ func FuzzSignAndVerifyHMAC(f *testing.F) {
83130
}
84131
})
85132
}
133+
134+
// Same as FuzzSignAndVerifyHMAC but using Message
135+
func FuzzMessageSignAndVerifyHMAC(f *testing.F) {
136+
type inputs struct {
137+
req string
138+
}
139+
testcases := []inputs{
140+
{httpreq1},
141+
}
142+
for _, tc := range testcases {
143+
f.Add(tc.req)
144+
}
145+
f.Fuzz(func(t *testing.T, reqString string) {
146+
config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475)
147+
fields := Headers("@authority", "date", "content-type")
148+
signatureName := "sig1"
149+
key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
150+
signer, _ := NewHMACSHA256Signer(key, config.SetKeyID("test-shared-secret"), fields)
151+
req := readRequest(reqString)
152+
sigInput, sig, err := SignRequest(signatureName, *signer, req)
153+
if err == nil {
154+
req.Header.Add("Signature", sig)
155+
req.Header.Add("Signature-Input", sigInput)
156+
verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
157+
assert.NoError(t, err, "could not generate Verifier")
158+
msg, err := NewMessage(NewMessageConfig().WithRequest(req))
159+
if err != nil {
160+
t.Errorf("Failed to create Message")
161+
}
162+
_, err = msg.Verify(signatureName, *verifier)
163+
assert.NoError(t, err, "verification error")
164+
}
165+
})
166+
}
167+
168+
func FuzzMessageVerify(f *testing.F) {
169+
f.Add("GET", "https://example.com/path", "example.com", "https", 0, "", "", "", "", true, false)
170+
f.Add("POST", "https://api.example.com", "api.example.com", "https", 0, "", "", "", "", false, true)
171+
f.Add("", "", "", "", 200, "GET", "https://example.com", "example.com", "https", true, false)
172+
f.Add("PUT", "", "", "http", 0, "", "", "", "", false, false)
173+
f.Add("", "", "", "", 404, "", "", "", "", false, false)
174+
f.Add("0", "%", "0", "0", 0, "", "", "", "", true, false)
175+
176+
f.Fuzz(func(t *testing.T, method, urlStr, authority, scheme string, statusCode int,
177+
assocMethod, assocURLStr, assocAuthority, assocScheme string,
178+
hasHeaders, hasTrailers bool) {
179+
180+
config := NewMessageConfig()
181+
182+
if method != "" {
183+
config = config.WithMethod(method)
184+
}
185+
if urlStr != "" {
186+
u, err := url.Parse(urlStr)
187+
if err == nil {
188+
config = config.WithURL(u)
189+
}
190+
}
191+
if authority != "" {
192+
config = config.WithAuthority(authority)
193+
}
194+
if scheme != "" {
195+
config = config.WithScheme(scheme)
196+
}
197+
198+
if statusCode > 0 {
199+
config = config.WithStatusCode(statusCode)
200+
}
201+
202+
if hasHeaders {
203+
headers := http.Header{
204+
"Content-Type": []string{"application/json"},
205+
"X-Test": []string{"fuzz"},
206+
}
207+
config = config.WithHeaders(headers)
208+
}
209+
if hasTrailers {
210+
trailers := http.Header{
211+
"X-Trailer": []string{"test"},
212+
}
213+
config = config.WithTrailers(trailers)
214+
}
215+
216+
if statusCode > 0 && assocMethod != "" {
217+
var assocURL *url.URL
218+
if assocURLStr != "" {
219+
assocURL, _ = url.Parse(assocURLStr)
220+
}
221+
assocHeaders := http.Header{"X-Assoc": []string{"test"}}
222+
config = config.WithAssociatedRequest(assocMethod, assocURL, assocHeaders, assocAuthority, assocScheme)
223+
}
224+
225+
msg, err := NewMessage(config)
226+
227+
if err == nil {
228+
if msg.headers == nil && msg.method != "" {
229+
t.Errorf("Request message created without headers")
230+
}
231+
if msg.headers == nil && msg.statusCode != nil {
232+
t.Errorf("Response message created without headers")
233+
}
234+
235+
key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
236+
verifier, _ := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false), Fields{})
237+
238+
if msg.headers != nil {
239+
msg.headers.Set("Signature-Input", `sig1=("@method");created=1618884473;keyid="test-key"`)
240+
msg.headers.Set("Signature", `sig1=:test:`)
241+
}
242+
243+
_, _ = msg.Verify("sig1", *verifier)
244+
}
245+
246+
if err != nil {
247+
hasRequest := method != ""
248+
hasResponse := statusCode > 0
249+
250+
if !hasRequest && !hasResponse {
251+
assert.Contains(t, err.Error(), "must have either method")
252+
} else if hasRequest && hasResponse {
253+
assert.Contains(t, err.Error(), "cannot have both request and response")
254+
} else if (hasRequest || hasResponse) && !hasHeaders {
255+
assert.Contains(t, err.Error(), "must have headers")
256+
}
257+
}
258+
})
259+
}

http2_test.go

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

1718
var wantFields = `"kuku": my awesome header
@@ -96,6 +97,61 @@ func testHTTP(t *testing.T, proto string) {
9697
simpleClient(t, proto, simpleHandler)
9798
}
9899

100+
func testMessageHTTP(t *testing.T, proto string) {
101+
simpleHandler := func(w http.ResponseWriter, r *http.Request) {
102+
reqProto := r.Proto
103+
if reqProto != proto {
104+
t.Errorf("expected %s, got %s", proto, reqProto)
105+
}
106+
var scheme string
107+
if ts.TLS == nil {
108+
scheme = "http"
109+
} else {
110+
scheme = "https"
111+
}
112+
sp := bytes.Split([]byte(ts.URL), []byte(":"))
113+
portval, err := strconv.Atoi(string(sp[2]))
114+
if err != nil {
115+
t.Errorf("cannot parse server port number")
116+
}
117+
tpl, err := template.New("fields").Parse(wantFields)
118+
if err != nil {
119+
t.Errorf("could not parse template")
120+
}
121+
type inputs struct {
122+
Port int
123+
Scheme string
124+
}
125+
// Use the Template facility to create the list of expected signed fields
126+
wf, err := execTemplate(*tpl, "fields", inputs{Port: portval, Scheme: scheme})
127+
if err != nil {
128+
t.Errorf("execTemplate failed")
129+
}
130+
verifier, err := NewHMACSHA256Verifier(bytes.Repeat([]byte{0x03}, 64),
131+
NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"),
132+
Headers("@query"))
133+
if err != nil {
134+
t.Errorf("could not create verifier")
135+
}
136+
msg, err := NewMessage(NewMessageConfig().WithRequest(r))
137+
if err != nil {
138+
t.Errorf("could not create message")
139+
}
140+
sigInput, _, err := verifyDebug("sig1", *verifier, msg)
141+
if err != nil {
142+
t.Errorf("failed to verify request: sig input: %s\nerr: %v", sigInput, err)
143+
}
144+
145+
if sigInput != wf {
146+
t.Errorf("unexpected fields: %s\n", diff.CharacterDiff(sigInput, wantFields))
147+
}
148+
w.WriteHeader(200)
149+
}
150+
151+
// And run the client code...
152+
simpleClient(t, proto, simpleHandler)
153+
}
154+
99155
func simpleClient(t *testing.T, proto string, simpleHandler func(w http.ResponseWriter, r *http.Request)) {
100156
// Client code
101157
switch proto {
@@ -153,8 +209,10 @@ func simpleClient(t *testing.T, proto string, simpleHandler func(w http.Response
153209

154210
func TestHTTP11(t *testing.T) {
155211
testHTTP(t, "HTTP/1.1")
212+
testMessageHTTP(t, "HTTP/1.1")
156213
}
157214

158215
func TestHTTP20(t *testing.T) {
159216
testHTTP(t, "HTTP/2.0")
217+
testMessageHTTP(t, "HTTP/2.0")
160218
}

0 commit comments

Comments
 (0)