Skip to content

Commit fa1a291

Browse files
x1ddosAlex Vaghin
authored andcommitted
acme: add KID variant to jwsEncodeJSON
RFC8555 requires that most requests contain "kid" field in the protected header. The JWK version is still used for new account creation and certificate revocation requests. Previously, in earlier drafts JWK variant was used exclusively. While JWK is computed based off the account public key, the new "kid" field takes literal value of the Account URL provided by the CA during a new registration. The actual support for KID-based JWS requests in Client will be added in a follow up CL. For what concerns the existing behaviour of JWS requests, a new field "url" is added to the protected header. Before: {"alg":"...", "jwk":"...", "nonce":"..."} After: {"alg":"...", "jwk":"...", "nonce":"...", "url":"..."} where the new field takes a value of the target request URL. This still works for CAs supporting pre-RFC protocol versions. Updates golang/go#21081 Change-Id: I460cfcd3dfdfe7fe3009a92a0a8a709fa07d0e7a Reviewed-on: https://go-review.googlesource.com/c/crypto/+/191601 Run-TryBot: Alex Vaghin <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 2682ddc commit fa1a291

File tree

3 files changed

+117
-37
lines changed

3 files changed

+117
-37
lines changed

acme/http.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string,
200200
if err != nil {
201201
return nil, nil, err
202202
}
203-
b, err := jwsEncodeJSON(body, key, nonce)
203+
b, err := jwsEncodeJSON(body, key, noKeyID, nonce, url)
204204
if err != nil {
205205
return nil, nil, err
206206
}

acme/jws.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,38 @@ import (
1717
"math/big"
1818
)
1919

20+
// keyID is the account identity provided by a CA during registration.
21+
type keyID string
22+
23+
// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
24+
// See jwsEncodeJSON for details.
25+
const noKeyID = keyID("")
26+
2027
// jwsEncodeJSON signs claimset using provided key and a nonce.
21-
// The result is serialized in JSON format.
28+
// The result is serialized in JSON format containing either kid or jwk
29+
// fields based on the provided keyID value.
30+
//
31+
// If kid is non-empty, its quoted value is inserted in the protected head
32+
// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
33+
// as "jwk" field value.
34+
//
2235
// See https://tools.ietf.org/html/rfc7515#section-7.
23-
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byte, error) {
24-
jwk, err := jwkEncode(key.Public())
25-
if err != nil {
26-
return nil, err
27-
}
36+
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
2837
alg, sha := jwsHasher(key.Public())
2938
if alg == "" || !sha.Available() {
3039
return nil, ErrUnsupportedKey
3140
}
32-
phead := fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q}`, alg, jwk, nonce)
41+
var phead string
42+
switch kid {
43+
case noKeyID:
44+
jwk, err := jwkEncode(key.Public())
45+
if err != nil {
46+
return nil, err
47+
}
48+
phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, url)
49+
default:
50+
phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, kid, nonce, url)
51+
}
3352
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
3453
cs, err := json.Marshal(claimset)
3554
if err != nil {

acme/jws_test.go

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"crypto/ecdsa"
1010
"crypto/elliptic"
1111
"crypto/rsa"
12+
"crypto/sha256"
1213
"crypto/x509"
1314
"encoding/base64"
1415
"encoding/json"
@@ -19,7 +20,18 @@ import (
1920
"testing"
2021
)
2122

23+
// The following shell command alias is used in the comments
24+
// throughout this file:
25+
// alias b64raw="base64 -w0 | tr -d '=' | tr '/+' '_-'"
26+
2227
const (
28+
// Modulus in raw base64:
29+
// 4xgZ3eRPkwoRvy7qeRUbmMDe0V-xH9eWLdu0iheeLlrmD2mqWXfP9IeSKApbn34
30+
// g8TuAS9g5zhq8ELQ3kmjr-KV86GAMgI6VAcGlq3QrzpTCf_30Ab7-zawrfRaFON
31+
// a1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosqEXeaIkVYBEhbh
32+
// Nu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZfoyFyek380mHg
33+
// JAumQ_I2fjj98_97mk3ihOY4AgVdCDj1z_GCoZkG5Rq7nbCGyosyKWyDX00Zs-n
34+
// NqVhoLeIvXC4nnWdJMZ6rogxyQQ
2335
testKeyPEM = `
2436
-----BEGIN RSA PRIVATE KEY-----
2537
MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq
@@ -82,7 +94,7 @@ GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ==
8294
`
8395
// 1. openssl ec -in key.pem -noout -text
8496
// 2. remove first byte, 04 (the header); the rest is X and Y
85-
// 3. convert each with: echo <val> | xxd -r -p | base64 -w 100 | tr -d '=' | tr '/+' '_-'
97+
// 3. convert each with: echo <val> | xxd -r -p | b64raw
8698
testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ"
8799
testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk"
88100
testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt"
@@ -91,7 +103,7 @@ GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ==
91103
testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax"
92104

93105
// echo -n '{"crv":"P-256","kty":"EC","x":"<testKeyECPubX>","y":"<testKeyECPubY>"}' | \
94-
// openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '/+' '_-'
106+
// openssl dgst -binary -sha256 | b64raw
95107
testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU"
96108
)
97109

@@ -140,7 +152,7 @@ func TestJWSEncodeJSON(t *testing.T) {
140152
// JWS signed with testKey and "nonce" as the nonce value
141153
// JSON-serialized JWS fields are split for easier testing
142154
const (
143-
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce"}
155+
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"}
144156
protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
145157
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
146158
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
@@ -151,19 +163,20 @@ func TestJWSEncodeJSON(t *testing.T) {
151163
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
152164
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
153165
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
154-
"UVEifSwibm9uY2UiOiJub25jZSJ9"
166+
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
155167
// {"Msg":"Hello JWS"}
156-
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
157-
signature = "eAGUikStX_UxyiFhxSLMyuyBcIB80GeBkFROCpap2sW3EmkU_ggF" +
158-
"knaQzxrTfItICSAXsCLIquZ5BbrSWA_4vdEYrwWtdUj7NqFKjHRa" +
159-
"zpLHcoR7r1rEHvkoP1xj49lS5fc3Wjjq8JUhffkhGbWZ8ZVkgPdC" +
160-
"4tMBWiQDoth-x8jELP_3LYOB_ScUXi2mETBawLgOT2K8rA0Vbbmx" +
161-
"hWNlOWuUf-8hL5YX4IOEwsS8JK_TrTq5Zc9My0zHJmaieqDV0UlP" +
162-
"k0onFjPFkGm7MrPSgd0MqRG-4vSAg2O4hDo7rKv4n8POjjXlNQvM" +
163-
"9IPLr8qZ7usYBKhEGwX3yq_eicAwBw"
168+
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
169+
// printf '<protected>.<payload>' | openssl dgst -binary -sha256 -sign testKey | b64raw
170+
signature = "YFyl_xz1E7TR-3E1bIuASTr424EgCvBHjt25WUFC2VaDjXYV0Rj_" +
171+
"Hd3dJ_2IRqBrXDZZ2n4ZeA_4mm3QFwmwyeDwe2sWElhb82lCZ8iX" +
172+
"uFnjeOmSOjx-nWwPa5ibCXzLq13zZ-OBV1Z4oN_TuailQeRoSfA3" +
173+
"nO8gG52mv1x2OMQ5MAFtt8jcngBLzts4AyhI6mBJ2w7Yaj3ZCriq" +
174+
"DWA3GLFvvHdW1Ba9Z01wtGT2CuZI7DUk_6Qj1b3BkBGcoKur5C9i" +
175+
"bUJtCkABwBMvBQNyD3MmXsrRFRTgvVlyU_yMaucYm7nmzEr_2PaQ" +
176+
"50rFt_9qOfJ4sfbLtG1Wwae57BQx1g"
164177
)
165178

166-
b, err := jwsEncodeJSON(claims, testKey, "nonce")
179+
b, err := jwsEncodeJSON(claims, testKey, noKeyID, "nonce", "url")
167180
if err != nil {
168181
t.Fatal(err)
169182
}
@@ -182,6 +195,46 @@ func TestJWSEncodeJSON(t *testing.T) {
182195
}
183196
}
184197

198+
func TestJWSEncodeKID(t *testing.T) {
199+
kid := keyID("https://example.org/account/1")
200+
claims := struct{ Msg string }{"Hello JWS"}
201+
// JWS signed with testKeyEC
202+
const (
203+
// {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"}
204+
protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5" +
205+
"vcmcvYWNjb3VudC8xIiwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
206+
// {"Msg":"Hello JWS"}
207+
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
208+
)
209+
210+
b, err := jwsEncodeJSON(claims, testKeyEC, kid, "nonce", "url")
211+
if err != nil {
212+
t.Fatal(err)
213+
}
214+
var jws struct{ Protected, Payload, Signature string }
215+
if err := json.Unmarshal(b, &jws); err != nil {
216+
t.Fatal(err)
217+
}
218+
if jws.Protected != protected {
219+
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
220+
}
221+
if jws.Payload != payload {
222+
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
223+
}
224+
225+
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
226+
if err != nil {
227+
t.Fatalf("jws.Signature: %v", err)
228+
}
229+
r, s := big.NewInt(0), big.NewInt(0)
230+
r.SetBytes(sig[:len(sig)/2])
231+
s.SetBytes(sig[len(sig)/2:])
232+
h := sha256.Sum256([]byte(protected + "." + payload))
233+
if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) {
234+
t.Error("invalid signature")
235+
}
236+
}
237+
185238
func TestJWSEncodeJSONEC(t *testing.T) {
186239
tt := []struct {
187240
key *ecdsa.PrivateKey
@@ -194,7 +247,7 @@ func TestJWSEncodeJSONEC(t *testing.T) {
194247
}
195248
for i, test := range tt {
196249
claims := struct{ Msg string }{"Hello JWS"}
197-
b, err := jwsEncodeJSON(claims, test.key, "nonce")
250+
b, err := jwsEncodeJSON(claims, test.key, noKeyID, "nonce", "url")
198251
if err != nil {
199252
t.Errorf("%d: %v", i, err)
200253
continue
@@ -212,6 +265,8 @@ func TestJWSEncodeJSONEC(t *testing.T) {
212265
var head struct {
213266
Alg string
214267
Nonce string
268+
URL string `json:"url"`
269+
KID string `json:"kid"`
215270
JWK struct {
216271
Crv string
217272
Kty string
@@ -228,6 +283,13 @@ func TestJWSEncodeJSONEC(t *testing.T) {
228283
if head.Nonce != "nonce" {
229284
t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce)
230285
}
286+
if head.URL != "url" {
287+
t.Errorf("%d: head.URL = %q; want 'url'", i, head.URL)
288+
}
289+
if head.KID != "" {
290+
// We used noKeyID in jwsEncodeJSON: expect no kid value.
291+
t.Errorf("%d: head.KID = %q; want empty", i, head.KID)
292+
}
231293
if head.JWK.Crv != test.crv {
232294
t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv)
233295
}
@@ -256,18 +318,19 @@ func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, e
256318
func TestJWSEncodeJSONCustom(t *testing.T) {
257319
claims := struct{ Msg string }{"hello"}
258320
const (
259-
// printf '{"Msg":"hello"}' | base64 | tr -d '=' | tr '/+' '_-'
321+
// printf '{"Msg":"hello"}' | b64raw
260322
payload = "eyJNc2ciOiJoZWxsbyJ9"
261-
// printf 'testsig' | base64 | tr -d '='
323+
// printf 'testsig' | b64raw
262324
testsig = "dGVzdHNpZw"
263325

264-
// printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":<testKeyECPubY>,"y":<testKeyECPubY>,"nonce":"nonce"}' | \
265-
// base64 | tr -d '=' | tr '/+' '_-'
266-
es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IjVsaEV1" +
267-
"ZzV4SzR4QkRaMm5BYmF4THRhTGl2ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFH" +
268-
"a3YwVGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6Im5vbmNlIn0"
326+
// printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":<testKeyECPubY>,"y":<testKeyECPubY>},"nonce":"nonce","url":"url"}' | b64raw
327+
es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0" +
328+
"eSI6IkVDIiwieCI6IjVsaEV1ZzV4SzR4QkRaMm5BYmF4THRhTGl2" +
329+
"ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFHa3Yw" +
330+
"VGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6" +
331+
"Im5vbmNlIiwidXJsIjoidXJsIn0"
269332

270-
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce"}
333+
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"}
271334
rs256phead = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
272335
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
273336
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
@@ -278,15 +341,15 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
278341
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
279342
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
280343
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
281-
"UVEifSwibm9uY2UiOiJub25jZSJ9"
344+
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
282345
)
283346

284347
tt := []struct {
285348
alg, phead string
286349
pub crypto.PublicKey
287350
}{
288-
{"RS256", rs256phead, testKey.Public()},
289351
{"ES256", es256phead, testKeyEC.Public()},
352+
{"RS256", rs256phead, testKey.Public()},
290353
}
291354
for _, tc := range tt {
292355
tc := tc
@@ -295,7 +358,7 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
295358
sig: []byte("testsig"),
296359
pub: tc.pub,
297360
}
298-
b, err := jwsEncodeJSON(claims, signer, "nonce")
361+
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
299362
if err != nil {
300363
t.Fatal(err)
301364
}
@@ -352,10 +415,8 @@ func TestJWKThumbprintRSA(t *testing.T) {
352415
func TestJWKThumbprintEC(t *testing.T) {
353416
// Key example from RFC 7520
354417
// expected was computed with
355-
// echo -n '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
356-
// openssl dgst -binary -sha256 | \
357-
// base64 | \
358-
// tr -d '=' | tr '/+' '_-'
418+
// printf '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
419+
// openssl dgst -binary -sha256 | b64raw
359420
const (
360421
base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" +
361422
"KqjqvjyekWF-7ytDyRXYgCF5cj0Kt"

0 commit comments

Comments
 (0)