Skip to content

Commit 6f4e295

Browse files
yaronfcursoragent
andcommitted
test: pin wimsey v0.5.0 / v3 WIMSE conformance vectors
Record tag+commit in testdata/wimse/SOURCE, refresh sign-eddsa/es256 to wimse-conformance/v3, and cover the new accepted/authority rewrite cases. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f9866ca commit 6f4e295

4 files changed

Lines changed: 174 additions & 7 deletions

File tree

testdata/wimse/SOURCE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Vendored from https://github.com/kanywst/wimsey
2+
#
3+
# Tag: v0.5.0
4+
# Commit: 2a7569a8e89ffcfd63ebb1dd454f2a425ca877a4
5+
# Tree: https://github.com/kanywst/wimsey/tree/v0.5.0/conformance/httpsig
6+
# Path: conformance/httpsig/
7+
# Spec: draft-ietf-wimse-http-signature-06
8+
# Format: wimse-conformance/v3
9+
#
10+
# Files: sign-eddsa.json, sign-es256.json

testdata/wimse/sign-eddsa.json

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"format": "wimse-conformance/v2",
2+
"format": "wimse-conformance/v3",
33
"suite": "httpsig",
44
"id": "sign-eddsa",
55
"spec": "draft-ietf-wimse-http-signature-06",
@@ -145,6 +145,33 @@
145145
]
146146
}
147147
},
148+
{
149+
"id": "authority-rewritten-inside-the-covered-set",
150+
"description": "this signature does cover `@authority`, so the same rewritten host that the accepted case tolerates now breaks the signature",
151+
"expect": "invalid_signature",
152+
"request": {
153+
"method": "POST",
154+
"authority": "attacker.example.net",
155+
"path": "/transfer",
156+
"query": null,
157+
"headers": [
158+
[
159+
"Content-Type",
160+
"application/json"
161+
],
162+
[
163+
"Content-Digest",
164+
"sha-256=:TUu+Wcaq0iRCzeGZpqil8DRAX814+1qBwk7ySd4cRfE=:"
165+
],
166+
[
167+
"Workload-Identity-Token",
168+
"eyJ0eXAiOiJ3aXQrand0IiwiYWxnIjoiRWREU0EiLCJraWQiOiJpc3N1ZXIta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwic3ViIjoic3BpZmZlOi8vZXhhbXBsZS5vcmcvd29ya2xvYWQvYXBpIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDAsImp0aSI6ImExYjJjMyIsImNuZiI6eyJqd2siOnsiYWxnIjoiRWREU0EiLCJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Il9SY2tPRnFneDF0ay0zak5ZQy1oMlpIOTZfZHJFOFdPMXdMcXlEWHA5aGcifX19.gEOIwRVXNwJTSXRGBDPV4CQLRUFeKYx-oK_nwSxaxegepaEEcKbn1TIPacMI0rWR2M8kRikyzB4jkcudkUtXCA"
169+
]
170+
]
171+
},
172+
"signature_input": "wimse=(\"@method\" \"@request-target\" \"content-type\" \"content-digest\" \"workload-identity-token\" \"@authority\");created=1700000000;expires=1700000300;nonce=\"abcd1111\";tag=\"wimse-workload-to-workload\";wimse-aud=\"https://service.example/transfer\";wimse-sign-response",
173+
"signature": "wimse=:NfKo1GMN/+ykiCORV/NelHjfc/80QlByEKGhTsG3HrVOxi6NkW0jAd9wnry2PNxokuZi7A8RIJ189XpPf/myAA==:"
174+
},
148175
{
149176
"id": "created-in-future",
150177
"description": "the signature's `created` is ahead of the verifier's clock",
@@ -173,6 +200,32 @@
173200
]
174201
}
175202
],
203+
"accepted": [
204+
{
205+
"id": "authority-rewritten-outside-the-covered-set",
206+
"description": "`@authority` is not covered, so a rewritten host leaves the signature valid: a signature protects the components it covers and no others",
207+
"request": {
208+
"method": "POST",
209+
"authority": "attacker.example.net",
210+
"path": "/transfer",
211+
"query": null,
212+
"headers": [
213+
[
214+
"Content-Type",
215+
"application/json"
216+
],
217+
[
218+
"Content-Digest",
219+
"sha-256=:TUu+Wcaq0iRCzeGZpqil8DRAX814+1qBwk7ySd4cRfE=:"
220+
],
221+
[
222+
"Workload-Identity-Token",
223+
"eyJ0eXAiOiJ3aXQrand0IiwiYWxnIjoiRWREU0EiLCJraWQiOiJpc3N1ZXIta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwic3ViIjoic3BpZmZlOi8vZXhhbXBsZS5vcmcvd29ya2xvYWQvYXBpIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDAsImp0aSI6ImExYjJjMyIsImNuZiI6eyJqd2siOnsiYWxnIjoiRWREU0EiLCJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Il9SY2tPRnFneDF0ay0zak5ZQy1oMlpIOTZfZHJFOFdPMXdMcXlEWHA5aGcifX19.gEOIwRVXNwJTSXRGBDPV4CQLRUFeKYx-oK_nwSxaxegepaEEcKbn1TIPacMI0rWR2M8kRikyzB4jkcudkUtXCA"
224+
]
225+
]
226+
}
227+
}
228+
],
176229
"response": {
177230
"status": 200,
178231
"headers": [

testdata/wimse/sign-es256.json

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"format": "wimse-conformance/v2",
2+
"format": "wimse-conformance/v3",
33
"suite": "httpsig",
44
"id": "sign-es256",
55
"spec": "draft-ietf-wimse-http-signature-06",
@@ -147,6 +147,33 @@
147147
]
148148
}
149149
},
150+
{
151+
"id": "authority-rewritten-inside-the-covered-set",
152+
"description": "this signature does cover `@authority`, so the same rewritten host that the accepted case tolerates now breaks the signature",
153+
"expect": "invalid_signature",
154+
"request": {
155+
"method": "POST",
156+
"authority": "attacker.example.net",
157+
"path": "/transfer",
158+
"query": null,
159+
"headers": [
160+
[
161+
"Content-Type",
162+
"application/json"
163+
],
164+
[
165+
"Content-Digest",
166+
"sha-256=:TUu+Wcaq0iRCzeGZpqil8DRAX814+1qBwk7ySd4cRfE=:"
167+
],
168+
[
169+
"Workload-Identity-Token",
170+
"eyJ0eXAiOiJ3aXQrand0IiwiYWxnIjoiRVMyNTYiLCJraWQiOiJpc3N1ZXIta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwic3ViIjoic3BpZmZlOi8vZXhhbXBsZS5vcmcvd29ya2xvYWQvYXBpIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDAsImp0aSI6ImExYjJjMyIsImNuZiI6eyJqd2siOnsiYWxnIjoiRVMyNTYiLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImNUWDZUOWs2Q2R6cGk3OW9HMHY4OVE1OERXTlU1aXI3Q184cU5DbGhlR1UiLCJ5IjoiN1V3ZkF0MjVBajdsYWxWLVVWMXFuY1pzRWZJZ2xnM2xsRE5OOVloM1p5USJ9fX0.5hgKPe0utuP-jgVBlaXzEfWSWyU_cCmKUVGEO6q159Hs-cH-XogDwFVyl7qgKXf6uFTEmPtO2nQrY3G1jxyAiw"
171+
]
172+
]
173+
},
174+
"signature_input": "wimse=(\"@method\" \"@request-target\" \"content-type\" \"content-digest\" \"workload-identity-token\" \"@authority\");created=1700000000;expires=1700000300;nonce=\"abcd1111\";tag=\"wimse-workload-to-workload\";wimse-aud=\"https://service.example/transfer\";wimse-sign-response",
175+
"signature": "wimse=:ti6V+DuBZCyZQXqdvCSChh1l61qLVyu4liFfr7aGdgyypuL0nsShqC3SSe6xBiNc8FiNzwyNhErQU/MrcCdQEQ==:"
176+
},
150177
{
151178
"id": "created-in-future",
152179
"description": "the signature's `created` is ahead of the verifier's clock",
@@ -175,6 +202,32 @@
175202
]
176203
}
177204
],
205+
"accepted": [
206+
{
207+
"id": "authority-rewritten-outside-the-covered-set",
208+
"description": "`@authority` is not covered, so a rewritten host leaves the signature valid: a signature protects the components it covers and no others",
209+
"request": {
210+
"method": "POST",
211+
"authority": "attacker.example.net",
212+
"path": "/transfer",
213+
"query": null,
214+
"headers": [
215+
[
216+
"Content-Type",
217+
"application/json"
218+
],
219+
[
220+
"Content-Digest",
221+
"sha-256=:TUu+Wcaq0iRCzeGZpqil8DRAX814+1qBwk7ySd4cRfE=:"
222+
],
223+
[
224+
"Workload-Identity-Token",
225+
"eyJ0eXAiOiJ3aXQrand0IiwiYWxnIjoiRVMyNTYiLCJraWQiOiJpc3N1ZXIta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwic3ViIjoic3BpZmZlOi8vZXhhbXBsZS5vcmcvd29ya2xvYWQvYXBpIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDAsImp0aSI6ImExYjJjMyIsImNuZiI6eyJqd2siOnsiYWxnIjoiRVMyNTYiLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImNUWDZUOWs2Q2R6cGk3OW9HMHY4OVE1OERXTlU1aXI3Q184cU5DbGhlR1UiLCJ5IjoiN1V3ZkF0MjVBajdsYWxWLVVWMXFuY1pzRWZJZ2xnM2xsRE5OOVloM1p5USJ9fX0.5hgKPe0utuP-jgVBlaXzEfWSWyU_cCmKUVGEO6q159Hs-cH-XogDwFVyl7qgKXf6uFTEmPtO2nQrY3G1jxyAiw"
226+
]
227+
]
228+
}
229+
}
230+
],
178231
"response": {
179232
"status": 200,
180233
"headers": [

wimse_conformance_test.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
)
2222

2323
// WIMSE HTTP Message Signature profile (draft-ietf-wimse-http-signature-06).
24-
// Vectors: https://github.com/kanywst/wimsey/tree/main/conformance/httpsig
24+
// Vectors: https://github.com/kanywst/wimsey (see testdata/wimse/SOURCE for tag/commit).
2525
//
2626
// httpsign implements RFC 9421, not the WIMSE profile. This harness:
2727
// 1. checks we can mint/verify the golden HTTP signatures
@@ -67,6 +67,16 @@ type wimseNegative struct {
6767
ExpectedReqNonce string `json:"expected_req_nonce"`
6868
}
6969

70+
// wimseAccepted is a positive case that must still verify (e.g. uncovered component mutated).
71+
type wimseAccepted struct {
72+
ID string `json:"id"`
73+
Description string `json:"description"`
74+
SignatureInput string `json:"signature_input"`
75+
Signature string `json:"signature"`
76+
Body string `json:"body"`
77+
Request *wimseHTTPRequest `json:"request"`
78+
}
79+
7080
type wimseResponse struct {
7181
Status int `json:"status"`
7282
Headers [][2]string `json:"headers"`
@@ -91,10 +101,11 @@ type wimseSuite struct {
91101
Params map[string]any `json:"params"`
92102
Request wimseHTTPRequest `json:"request"`
93103
Body string `json:"body"`
94-
SignatureInput string `json:"signature_input"`
95-
Signature string `json:"signature"`
96-
Negative []wimseNegative `json:"negative"`
97-
Response *wimseResponse `json:"response"`
104+
SignatureInput string `json:"signature_input"`
105+
Signature string `json:"signature"`
106+
Negative []wimseNegative `json:"negative"`
107+
Accepted []wimseAccepted `json:"accepted"`
108+
Response *wimseResponse `json:"response"`
98109
}
99110

100111
func loadWimseSuite(t *testing.T, name string) wimseSuite {
@@ -386,6 +397,14 @@ func TestWimseConformance(t *testing.T) {
386397
}
387398
})
388399

400+
t.Run("Accepted", func(t *testing.T) {
401+
for _, a := range s.Accepted {
402+
t.Run(a.ID, func(t *testing.T) {
403+
runWimseRequestAccepted(t, s, keys, reqFields, a)
404+
})
405+
}
406+
})
407+
389408
if s.Response == nil {
390409
return
391410
}
@@ -434,6 +453,38 @@ func TestWimseConformance(t *testing.T) {
434453
}
435454
}
436455

456+
func runWimseRequestAccepted(t *testing.T, s wimseSuite, keys wimseKeyPair, reqFields Fields, a wimseAccepted) {
457+
t.Helper()
458+
body := s.Body
459+
if a.Body != "" {
460+
body = a.Body
461+
}
462+
httpReq := s.Request
463+
if a.Request != nil {
464+
httpReq = *a.Request
465+
}
466+
sigIn := s.SignatureInput
467+
if a.SignatureInput != "" {
468+
sigIn = a.SignatureInput
469+
}
470+
sig := s.Signature
471+
if a.Signature != "" {
472+
sig = a.Signature
473+
}
474+
475+
req := wimseHTTPReq(t, httpReq, body)
476+
req.Header.Set("Signature-Input", sigIn)
477+
req.Header.Set("Signature", sig)
478+
479+
details, err := RequestDetails(s.Label, req)
480+
require.NoError(t, err, a.Description)
481+
got := wimseRequestProfileCheck(t, details, req, time.Unix(s.VerifyNow, 0), 0, "https://service.example/transfer", s.Components)
482+
require.Empty(t, got, a.Description)
483+
484+
err = VerifyRequest(s.Label, *wimseRFCVerifier(t, keys, reqFields), req)
485+
require.NoError(t, err, a.Description)
486+
}
487+
437488
func runWimseRequestNegative(t *testing.T, s wimseSuite, keys wimseKeyPair, reqFields Fields, n wimseNegative) {
438489
t.Helper()
439490
body := s.Body

0 commit comments

Comments
 (0)