mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-22 21:08:38 +00:00

We've been maintaining temporary dev forks of golang.org/x/crypto/{acme,ssh} in https://github.com/tailscale/golang-x-crypto instead of using this repo's tempfork directory as we do with other packages. The reason we were doing that was because x/crypto/ssh depended on x/crypto/ssh/internal/poly1305 and I hadn't noticed there are forwarding wrappers already available in x/crypto/poly1305. It also depended internal/bcrypt_pbkdf but we don't use that so it's easy to just delete that calling code in our tempfork/ssh. Now that our SSH changes have been upstreamed, we can soon unfork from SSH. That leaves ACME remaining. This change copies our tailscale/golang-x-crypto/acme code to tempfork/acme but adds a test that our vendored copied still matches our tailscale/golang-x-crypto repo, where we can continue to do development work and rebases with upstream. A comment on the new test describes the expected workflow. While we could continue to just import & use tailscale/golang-x-crypto/acme, it seems a bit nicer to not have that entire-fork-of-x-crypto visible at all in our transitive deps and the questions that invites. Showing just a fork of an ACME client is much less scary. It does add a step to the process of hacking on the ACME client code, but we do that approximately never anyway, and the extra step is very incremental compared to the existing tedious steps. Updates #8593 Updates #10238 Change-Id: I8af4378c04c1f82e63d31bf4d16dba9f510f9199 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
551 lines
19 KiB
Go
551 lines
19 KiB
Go
// Copyright 2015 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package acme
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"testing"
|
|
)
|
|
|
|
// The following shell command alias is used in the comments
|
|
// throughout this file:
|
|
// alias b64raw="base64 -w0 | tr -d '=' | tr '/+' '_-'"
|
|
|
|
const (
|
|
// Modulus in raw base64:
|
|
// 4xgZ3eRPkwoRvy7qeRUbmMDe0V-xH9eWLdu0iheeLlrmD2mqWXfP9IeSKApbn34
|
|
// g8TuAS9g5zhq8ELQ3kmjr-KV86GAMgI6VAcGlq3QrzpTCf_30Ab7-zawrfRaFON
|
|
// a1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosqEXeaIkVYBEhbh
|
|
// Nu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZfoyFyek380mHg
|
|
// JAumQ_I2fjj98_97mk3ihOY4AgVdCDj1z_GCoZkG5Rq7nbCGyosyKWyDX00Zs-n
|
|
// NqVhoLeIvXC4nnWdJMZ6rogxyQQ
|
|
testKeyPEM = `
|
|
-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq
|
|
WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30
|
|
Ab7+zawrfRaFONa1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosq
|
|
EXeaIkVYBEhbhNu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZf
|
|
oyFyek380mHgJAumQ/I2fjj98/97mk3ihOY4AgVdCDj1z/GCoZkG5Rq7nbCGyosy
|
|
KWyDX00Zs+nNqVhoLeIvXC4nnWdJMZ6rogxyQQIDAQABAoIBACIEZTOI1Kao9nmV
|
|
9IeIsuaR1Y61b9neOF/MLmIVIZu+AAJFCMB4Iw11FV6sFodwpEyeZhx2WkpWVN+H
|
|
r19eGiLX3zsL0DOdqBJoSIHDWCCMxgnYJ6nvS0nRxX3qVrBp8R2g12Ub+gNPbmFm
|
|
ecf/eeERIVxfifd9VsyRu34eDEvcmKFuLYbElFcPh62xE3x12UZvV/sN7gXbawpP
|
|
G+w255vbE5MoaKdnnO83cTFlcHvhn24M/78qP7Te5OAeelr1R89kYxQLpuGe4fbS
|
|
zc6E3ym5Td6urDetGGrSY1Eu10/8sMusX+KNWkm+RsBRbkyKq72ks/qKpOxOa+c6
|
|
9gm+Y8ECgYEA/iNUyg1ubRdH11p82l8KHtFC1DPE0V1gSZsX29TpM5jS4qv46K+s
|
|
8Ym1zmrORM8x+cynfPx1VQZQ34EYeCMIX212ryJ+zDATl4NE0I4muMvSiH9vx6Xc
|
|
7FmhNnaYzPsBL5Tm9nmtQuP09YEn8poiOJFiDs/4olnD5ogA5O4THGkCgYEA5MIL
|
|
qWYBUuqbEWLRtMruUtpASclrBqNNsJEsMGbeqBJmoMxdHeSZckbLOrqm7GlMyNRJ
|
|
Ne/5uWRGSzaMYuGmwsPpERzqEvYFnSrpjW5YtXZ+JtxFXNVfm9Z1gLLgvGpOUCIU
|
|
RbpoDckDe1vgUuk3y5+DjZihs+rqIJ45XzXTzBkCgYBWuf3segruJZy5rEKhTv+o
|
|
JqeUvRn0jNYYKFpLBeyTVBrbie6GkbUGNIWbrK05pC+c3K9nosvzuRUOQQL1tJbd
|
|
4gA3oiD9U4bMFNr+BRTHyZ7OQBcIXdz3t1qhuHVKtnngIAN1p25uPlbRFUNpshnt
|
|
jgeVoHlsBhApcs5DUc+pyQKBgDzeHPg/+g4z+nrPznjKnktRY1W+0El93kgi+J0Q
|
|
YiJacxBKEGTJ1MKBb8X6sDurcRDm22wMpGfd9I5Cv2v4GsUsF7HD/cx5xdih+G73
|
|
c4clNj/k0Ff5Nm1izPUno4C+0IOl7br39IPmfpSuR6wH/h6iHQDqIeybjxyKvT1G
|
|
N0rRAoGBAKGD+4ZI/E1MoJ5CXB8cDDMHagbE3cq/DtmYzE2v1DFpQYu5I4PCm5c7
|
|
EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO
|
|
9XWsXpbSTsRPj0sv1rB+UzBJ0PgjK4q2zOF0sNo7b1+6nlM3BWPx
|
|
-----END RSA PRIVATE KEY-----
|
|
`
|
|
|
|
// This thumbprint is for the testKey defined above.
|
|
testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ"
|
|
|
|
// openssl ecparam -name secp256k1 -genkey -noout
|
|
testKeyECPEM = `
|
|
-----BEGIN EC PRIVATE KEY-----
|
|
MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49
|
|
AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5
|
|
QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ==
|
|
-----END EC PRIVATE KEY-----
|
|
`
|
|
// openssl ecparam -name secp384r1 -genkey -noout
|
|
testKeyEC384PEM = `
|
|
-----BEGIN EC PRIVATE KEY-----
|
|
MIGkAgEBBDAQ4lNtXRORWr1bgKR1CGysr9AJ9SyEk4jiVnlUWWUChmSNL+i9SLSD
|
|
Oe/naPqXJ6CgBwYFK4EEACKhZANiAAQzKtj+Ms0vHoTX5dzv3/L5YMXOWuI5UKRj
|
|
JigpahYCqXD2BA1j0E/2xt5vlPf+gm0PL+UHSQsCokGnIGuaHCsJAp3ry0gHQEke
|
|
WYXapUUFdvaK1R2/2hn5O+eiQM8YzCg=
|
|
-----END EC PRIVATE KEY-----
|
|
`
|
|
// openssl ecparam -name secp521r1 -genkey -noout
|
|
testKeyEC512PEM = `
|
|
-----BEGIN EC PRIVATE KEY-----
|
|
MIHcAgEBBEIBSNZKFcWzXzB/aJClAb305ibalKgtDA7+70eEkdPt28/3LZMM935Z
|
|
KqYHh/COcxuu3Kt8azRAUz3gyr4zZKhlKUSgBwYFK4EEACOhgYkDgYYABAHUNKbx
|
|
7JwC7H6pa2sV0tERWhHhB3JmW+OP6SUgMWryvIKajlx73eS24dy4QPGrWO9/ABsD
|
|
FqcRSkNVTXnIv6+0mAF25knqIBIg5Q8M9BnOu9GGAchcwt3O7RDHmqewnJJDrbjd
|
|
GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ==
|
|
-----END EC PRIVATE KEY-----
|
|
`
|
|
// 1. openssl ec -in key.pem -noout -text
|
|
// 2. remove first byte, 04 (the header); the rest is X and Y
|
|
// 3. convert each with: echo <val> | xxd -r -p | b64raw
|
|
testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ"
|
|
testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk"
|
|
testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt"
|
|
testKeyEC384PubY = "Dy_lB0kLAqJBpyBrmhwrCQKd68tIB0BJHlmF2qVFBXb2itUdv9oZ-TvnokDPGMwo"
|
|
testKeyEC512PubX = "AdQ0pvHsnALsfqlraxXS0RFaEeEHcmZb44_pJSAxavK8gpqOXHvd5Lbh3LhA8atY738AGwMWpxFKQ1VNeci_r7SY"
|
|
testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax"
|
|
|
|
// echo -n '{"crv":"P-256","kty":"EC","x":"<testKeyECPubX>","y":"<testKeyECPubY>"}' | \
|
|
// openssl dgst -binary -sha256 | b64raw
|
|
testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU"
|
|
)
|
|
|
|
var (
|
|
testKey *rsa.PrivateKey
|
|
testKeyEC *ecdsa.PrivateKey
|
|
testKeyEC384 *ecdsa.PrivateKey
|
|
testKeyEC512 *ecdsa.PrivateKey
|
|
)
|
|
|
|
func init() {
|
|
testKey = parseRSA(testKeyPEM, "testKeyPEM")
|
|
testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM")
|
|
testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM")
|
|
testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM")
|
|
}
|
|
|
|
func decodePEM(s, name string) []byte {
|
|
d, _ := pem.Decode([]byte(s))
|
|
if d == nil {
|
|
panic("no block found in " + name)
|
|
}
|
|
return d.Bytes
|
|
}
|
|
|
|
func parseRSA(s, name string) *rsa.PrivateKey {
|
|
b := decodePEM(s, name)
|
|
k, err := x509.ParsePKCS1PrivateKey(b)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("%s: %v", name, err))
|
|
}
|
|
return k
|
|
}
|
|
|
|
func parseEC(s, name string) *ecdsa.PrivateKey {
|
|
b := decodePEM(s, name)
|
|
k, err := x509.ParseECPrivateKey(b)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("%s: %v", name, err))
|
|
}
|
|
return k
|
|
}
|
|
|
|
func TestJWSEncodeJSON(t *testing.T) {
|
|
claims := struct{ Msg string }{"Hello JWS"}
|
|
// JWS signed with testKey and "nonce" as the nonce value
|
|
// JSON-serialized JWS fields are split for easier testing
|
|
const (
|
|
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"}
|
|
protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
|
|
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
|
|
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
|
|
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
|
|
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
|
|
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
|
|
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
|
|
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
|
|
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
|
|
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
|
|
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
|
|
// {"Msg":"Hello JWS"}
|
|
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
|
|
// printf '<protected>.<payload>' | openssl dgst -binary -sha256 -sign testKey | b64raw
|
|
signature = "YFyl_xz1E7TR-3E1bIuASTr424EgCvBHjt25WUFC2VaDjXYV0Rj_" +
|
|
"Hd3dJ_2IRqBrXDZZ2n4ZeA_4mm3QFwmwyeDwe2sWElhb82lCZ8iX" +
|
|
"uFnjeOmSOjx-nWwPa5ibCXzLq13zZ-OBV1Z4oN_TuailQeRoSfA3" +
|
|
"nO8gG52mv1x2OMQ5MAFtt8jcngBLzts4AyhI6mBJ2w7Yaj3ZCriq" +
|
|
"DWA3GLFvvHdW1Ba9Z01wtGT2CuZI7DUk_6Qj1b3BkBGcoKur5C9i" +
|
|
"bUJtCkABwBMvBQNyD3MmXsrRFRTgvVlyU_yMaucYm7nmzEr_2PaQ" +
|
|
"50rFt_9qOfJ4sfbLtG1Wwae57BQx1g"
|
|
)
|
|
|
|
b, err := jwsEncodeJSON(claims, testKey, noKeyID, "nonce", "url")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var jws struct{ Protected, Payload, Signature string }
|
|
if err := json.Unmarshal(b, &jws); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if jws.Protected != protected {
|
|
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
|
|
}
|
|
if jws.Payload != payload {
|
|
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
|
|
}
|
|
if jws.Signature != signature {
|
|
t.Errorf("signature:\n%s\nwant:\n%s", jws.Signature, signature)
|
|
}
|
|
}
|
|
|
|
func TestJWSEncodeNoNonce(t *testing.T) {
|
|
kid := KeyID("https://example.org/account/1")
|
|
claims := "RawString"
|
|
const (
|
|
// {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"}
|
|
protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYWNjb3VudC8xIiwidXJsIjoidXJsIn0"
|
|
// "Raw String"
|
|
payload = "RawString"
|
|
)
|
|
|
|
b, err := jwsEncodeJSON(claims, testKeyEC, kid, "", "url")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var jws struct{ Protected, Payload, Signature string }
|
|
if err := json.Unmarshal(b, &jws); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if jws.Protected != protected {
|
|
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
|
|
}
|
|
if jws.Payload != payload {
|
|
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
|
|
}
|
|
|
|
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
|
|
if err != nil {
|
|
t.Fatalf("jws.Signature: %v", err)
|
|
}
|
|
r, s := big.NewInt(0), big.NewInt(0)
|
|
r.SetBytes(sig[:len(sig)/2])
|
|
s.SetBytes(sig[len(sig)/2:])
|
|
h := sha256.Sum256([]byte(protected + "." + payload))
|
|
if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) {
|
|
t.Error("invalid signature")
|
|
}
|
|
}
|
|
|
|
func TestJWSEncodeKID(t *testing.T) {
|
|
kid := KeyID("https://example.org/account/1")
|
|
claims := struct{ Msg string }{"Hello JWS"}
|
|
// JWS signed with testKeyEC
|
|
const (
|
|
// {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"}
|
|
protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5" +
|
|
"vcmcvYWNjb3VudC8xIiwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
|
|
// {"Msg":"Hello JWS"}
|
|
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
|
|
)
|
|
|
|
b, err := jwsEncodeJSON(claims, testKeyEC, kid, "nonce", "url")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var jws struct{ Protected, Payload, Signature string }
|
|
if err := json.Unmarshal(b, &jws); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if jws.Protected != protected {
|
|
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
|
|
}
|
|
if jws.Payload != payload {
|
|
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
|
|
}
|
|
|
|
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
|
|
if err != nil {
|
|
t.Fatalf("jws.Signature: %v", err)
|
|
}
|
|
r, s := big.NewInt(0), big.NewInt(0)
|
|
r.SetBytes(sig[:len(sig)/2])
|
|
s.SetBytes(sig[len(sig)/2:])
|
|
h := sha256.Sum256([]byte(protected + "." + payload))
|
|
if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) {
|
|
t.Error("invalid signature")
|
|
}
|
|
}
|
|
|
|
func TestJWSEncodeJSONEC(t *testing.T) {
|
|
tt := []struct {
|
|
key *ecdsa.PrivateKey
|
|
x, y string
|
|
alg, crv string
|
|
}{
|
|
{testKeyEC, testKeyECPubX, testKeyECPubY, "ES256", "P-256"},
|
|
{testKeyEC384, testKeyEC384PubX, testKeyEC384PubY, "ES384", "P-384"},
|
|
{testKeyEC512, testKeyEC512PubX, testKeyEC512PubY, "ES512", "P-521"},
|
|
}
|
|
for i, test := range tt {
|
|
claims := struct{ Msg string }{"Hello JWS"}
|
|
b, err := jwsEncodeJSON(claims, test.key, noKeyID, "nonce", "url")
|
|
if err != nil {
|
|
t.Errorf("%d: %v", i, err)
|
|
continue
|
|
}
|
|
var jws struct{ Protected, Payload, Signature string }
|
|
if err := json.Unmarshal(b, &jws); err != nil {
|
|
t.Errorf("%d: %v", i, err)
|
|
continue
|
|
}
|
|
|
|
b, err = base64.RawURLEncoding.DecodeString(jws.Protected)
|
|
if err != nil {
|
|
t.Errorf("%d: jws.Protected: %v", i, err)
|
|
}
|
|
var head struct {
|
|
Alg string
|
|
Nonce string
|
|
URL string `json:"url"`
|
|
KID string `json:"kid"`
|
|
JWK struct {
|
|
Crv string
|
|
Kty string
|
|
X string
|
|
Y string
|
|
} `json:"jwk"`
|
|
}
|
|
if err := json.Unmarshal(b, &head); err != nil {
|
|
t.Errorf("%d: jws.Protected: %v", i, err)
|
|
}
|
|
if head.Alg != test.alg {
|
|
t.Errorf("%d: head.Alg = %q; want %q", i, head.Alg, test.alg)
|
|
}
|
|
if head.Nonce != "nonce" {
|
|
t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce)
|
|
}
|
|
if head.URL != "url" {
|
|
t.Errorf("%d: head.URL = %q; want 'url'", i, head.URL)
|
|
}
|
|
if head.KID != "" {
|
|
// We used noKeyID in jwsEncodeJSON: expect no kid value.
|
|
t.Errorf("%d: head.KID = %q; want empty", i, head.KID)
|
|
}
|
|
if head.JWK.Crv != test.crv {
|
|
t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv)
|
|
}
|
|
if head.JWK.Kty != "EC" {
|
|
t.Errorf("%d: head.JWK.Kty = %q; want EC", i, head.JWK.Kty)
|
|
}
|
|
if head.JWK.X != test.x {
|
|
t.Errorf("%d: head.JWK.X = %q; want %q", i, head.JWK.X, test.x)
|
|
}
|
|
if head.JWK.Y != test.y {
|
|
t.Errorf("%d: head.JWK.Y = %q; want %q", i, head.JWK.Y, test.y)
|
|
}
|
|
}
|
|
}
|
|
|
|
type customTestSigner struct {
|
|
sig []byte
|
|
pub crypto.PublicKey
|
|
}
|
|
|
|
func (s *customTestSigner) Public() crypto.PublicKey { return s.pub }
|
|
func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {
|
|
return s.sig, nil
|
|
}
|
|
|
|
func TestJWSEncodeJSONCustom(t *testing.T) {
|
|
claims := struct{ Msg string }{"hello"}
|
|
const (
|
|
// printf '{"Msg":"hello"}' | b64raw
|
|
payload = "eyJNc2ciOiJoZWxsbyJ9"
|
|
// printf 'testsig' | b64raw
|
|
testsig = "dGVzdHNpZw"
|
|
|
|
// the example P256 curve point from https://tools.ietf.org/html/rfc7515#appendix-A.3.1
|
|
// encoded as ASN.1…
|
|
es256stdsig = "MEUCIA7RIVN5Y2xIPC9/FVgH1AKjsigDOvl8fheBmsMWnqZlAiEA" +
|
|
"xQoH04w8cOXY8S2vCEpUgKZlkMXyk1Cajz9/ioOjVNU"
|
|
// …and RFC7518 (https://tools.ietf.org/html/rfc7518#section-3.4)
|
|
es256jwsig = "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw" +
|
|
"5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
|
|
|
|
// printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":<testKeyECPubY>,"y":<testKeyECPubY>},"nonce":"nonce","url":"url"}' | b64raw
|
|
es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0" +
|
|
"eSI6IkVDIiwieCI6IjVsaEV1ZzV4SzR4QkRaMm5BYmF4THRhTGl2" +
|
|
"ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFHa3Yw" +
|
|
"VGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6" +
|
|
"Im5vbmNlIiwidXJsIjoidXJsIn0"
|
|
|
|
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"}
|
|
rs256phead = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
|
|
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
|
|
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
|
|
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
|
|
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
|
|
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
|
|
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
|
|
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
|
|
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
|
|
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
|
|
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9"
|
|
)
|
|
|
|
tt := []struct {
|
|
alg, phead string
|
|
pub crypto.PublicKey
|
|
stdsig, jwsig string
|
|
}{
|
|
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig},
|
|
{"RS256", rs256phead, testKey.Public(), testsig, testsig},
|
|
}
|
|
for _, tc := range tt {
|
|
tc := tc
|
|
t.Run(tc.alg, func(t *testing.T) {
|
|
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
|
|
if err != nil {
|
|
t.Errorf("couldn't decode test vector: %v", err)
|
|
}
|
|
signer := &customTestSigner{
|
|
sig: stdsig,
|
|
pub: tc.pub,
|
|
}
|
|
|
|
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var j jsonWebSignature
|
|
if err := json.Unmarshal(b, &j); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if j.Protected != tc.phead {
|
|
t.Errorf("j.Protected = %q\nwant %q", j.Protected, tc.phead)
|
|
}
|
|
if j.Payload != payload {
|
|
t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload)
|
|
}
|
|
if j.Sig != tc.jwsig {
|
|
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJWSWithMAC(t *testing.T) {
|
|
// Example from RFC 7520 Section 4.4.3.
|
|
// https://tools.ietf.org/html/rfc7520#section-4.4.3
|
|
b64Key := "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
|
|
rawPayload := []byte("It\xe2\x80\x99s a dangerous business, Frodo, going out your " +
|
|
"door. You step onto the road, and if you don't keep your feet, " +
|
|
"there\xe2\x80\x99s no knowing where you might be swept off " +
|
|
"to.")
|
|
protected := "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" +
|
|
"VlZjMxNGJjNzAzNyJ9"
|
|
payload := "SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywg" +
|
|
"Z29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9h" +
|
|
"ZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXi" +
|
|
"gJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9m" +
|
|
"ZiB0by4"
|
|
sig := "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
|
|
|
|
key, err := base64.RawURLEncoding.DecodeString(b64Key)
|
|
if err != nil {
|
|
t.Fatalf("unable to decode key: %q", b64Key)
|
|
}
|
|
got, err := jwsWithMAC(key, "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "", rawPayload)
|
|
if err != nil {
|
|
t.Fatalf("jwsWithMAC() = %q", err)
|
|
}
|
|
if got.Protected != protected {
|
|
t.Errorf("got.Protected = %q\nwant %q", got.Protected, protected)
|
|
}
|
|
if got.Payload != payload {
|
|
t.Errorf("got.Payload = %q\nwant %q", got.Payload, payload)
|
|
}
|
|
if got.Sig != sig {
|
|
t.Errorf("got.Signature = %q\nwant %q", got.Sig, sig)
|
|
}
|
|
}
|
|
|
|
func TestJWSWithMACError(t *testing.T) {
|
|
p := "{}"
|
|
if _, err := jwsWithMAC(nil, "", "", []byte(p)); err == nil {
|
|
t.Errorf("jwsWithMAC(nil, ...) = success; want err")
|
|
}
|
|
}
|
|
|
|
func TestJWKThumbprintRSA(t *testing.T) {
|
|
// Key example from RFC 7638
|
|
const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +
|
|
"VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" +
|
|
"4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" +
|
|
"W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" +
|
|
"1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" +
|
|
"aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
|
|
const base64E = "AQAB"
|
|
const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
|
|
|
|
b, err := base64.RawURLEncoding.DecodeString(base64N)
|
|
if err != nil {
|
|
t.Fatalf("Error parsing example key N: %v", err)
|
|
}
|
|
n := new(big.Int).SetBytes(b)
|
|
|
|
b, err = base64.RawURLEncoding.DecodeString(base64E)
|
|
if err != nil {
|
|
t.Fatalf("Error parsing example key E: %v", err)
|
|
}
|
|
e := new(big.Int).SetBytes(b)
|
|
|
|
pub := &rsa.PublicKey{N: n, E: int(e.Uint64())}
|
|
th, err := JWKThumbprint(pub)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if th != expected {
|
|
t.Errorf("thumbprint = %q; want %q", th, expected)
|
|
}
|
|
}
|
|
|
|
func TestJWKThumbprintEC(t *testing.T) {
|
|
// Key example from RFC 7520
|
|
// expected was computed with
|
|
// printf '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
|
|
// openssl dgst -binary -sha256 | b64raw
|
|
const (
|
|
base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" +
|
|
"KqjqvjyekWF-7ytDyRXYgCF5cj0Kt"
|
|
base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" +
|
|
"QkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
|
|
expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M"
|
|
)
|
|
|
|
b, err := base64.RawURLEncoding.DecodeString(base64X)
|
|
if err != nil {
|
|
t.Fatalf("Error parsing example key X: %v", err)
|
|
}
|
|
x := new(big.Int).SetBytes(b)
|
|
|
|
b, err = base64.RawURLEncoding.DecodeString(base64Y)
|
|
if err != nil {
|
|
t.Fatalf("Error parsing example key Y: %v", err)
|
|
}
|
|
y := new(big.Int).SetBytes(b)
|
|
|
|
pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}
|
|
th, err := JWKThumbprint(pub)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if th != expected {
|
|
t.Errorf("thumbprint = %q; want %q", th, expected)
|
|
}
|
|
}
|
|
|
|
func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
|
|
_, err := JWKThumbprint(struct{}{})
|
|
if err != ErrUnsupportedKey {
|
|
t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
|
|
}
|
|
}
|