key-cape/src/internal/jose/jose_test.go

169 lines
5.3 KiB
Go
Raw Normal View History

package jose_test
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"strings"
"testing"
"keycape/internal/jose"
)
// internal/jose is the single signature verifier behind both the caller CLI and
// upstream provider verification, so it is tested directly rather than only
// through its callers.
var testKey = func() *rsa.PrivateKey {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
return key
}()
func jwks(kid string, key *rsa.PrivateKey) []byte {
return []byte(fmt.Sprintf(`{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":%q,"n":%q,"e":%q}]}`,
kid,
base64.RawURLEncoding.EncodeToString(key.PublicKey.N.Bytes()),
base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.PublicKey.E)).Bytes())))
}
func sign(t *testing.T, header, claims string, key *rsa.PrivateKey) string {
t.Helper()
input := base64.RawURLEncoding.EncodeToString([]byte(header)) + "." +
base64.RawURLEncoding.EncodeToString([]byte(claims))
digest := sha256.Sum256([]byte(input))
signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, digest[:])
if err != nil {
t.Fatal(err)
}
return input + "." + base64.RawURLEncoding.EncodeToString(signature)
}
func TestVerifyAcceptsGenuineTokenAndReturnsClaims(t *testing.T) {
keys, err := jose.ParseJWKS(jwks("k1", testKey))
if err != nil {
t.Fatal(err)
}
token := sign(t, `{"alg":"RS256","kid":"k1"}`, `{"sub":"alice","n":1}`, testKey)
claims, err := jose.Verify(token, keys)
if err != nil {
t.Fatalf("genuine token rejected: %v", err)
}
if claims["sub"] != "alice" {
t.Fatalf("claims not returned: %v", claims)
}
}
func TestVerifyRejections(t *testing.T) {
keys, err := jose.ParseJWKS(jwks("k1", testKey))
if err != nil {
t.Fatal(err)
}
other, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
genuine := sign(t, `{"alg":"RS256","kid":"k1"}`, `{"sub":"alice"}`, testKey)
parts := strings.Split(genuine, ".")
cases := map[string]string{
"wrong signing key": sign(t, `{"alg":"RS256","kid":"k1"}`, `{"sub":"alice"}`, other),
"unknown key id": sign(t, `{"alg":"RS256","kid":"k2"}`, `{"sub":"alice"}`, testKey),
"missing key id": sign(t, `{"alg":"RS256"}`, `{"sub":"alice"}`, testKey),
"alg none": sign(t, `{"alg":"none","kid":"k1"}`, `{"sub":"alice"}`, testKey),
"alg HS256": sign(t, `{"alg":"HS256","kid":"k1"}`, `{"sub":"alice"}`, testKey),
// A critical extension we do not understand must not be ignored.
"critical extension": sign(t, `{"alg":"RS256","kid":"k1","crit":["exp"]}`, `{"sub":"alice"}`, testKey),
"tampered payload": parts[0] + "." + base64.RawURLEncoding.EncodeToString([]byte(`{"sub":"mallory"}`)) + "." + parts[2],
"two segments": parts[0] + "." + parts[1],
"payload not json": sign(t, `{"alg":"RS256","kid":"k1"}`, `not-json`, testKey),
"empty": "",
}
for name, token := range cases {
t.Run(name, func(t *testing.T) {
if _, err := jose.Verify(token, keys); err == nil {
t.Fatal("accepted")
}
})
}
}
func TestParseJWKSRejectsUnusableKeySets(t *testing.T) {
small, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
t.Fatal(err)
}
cases := map[string][]byte{
"undersized modulus": jwks("k1", small),
"empty set": []byte(`{"keys":[]}`),
"no rsa signing key": []byte(`{"keys":[{"kty":"EC","kid":"k1","crv":"P-256"}]}`),
"missing key id": []byte(`{"keys":[{"kty":"RSA","n":"AQAB","e":"AQAB"}]}`),
"bad base64": []byte(`{"keys":[{"kty":"RSA","kid":"k1","n":"!!!","e":"AQAB"}]}`),
"even exponent": []byte(fmt.Sprintf(`{"keys":[{"kty":"RSA","kid":"k1","n":%q,"e":"AQAA"}]}`, base64.RawURLEncoding.EncodeToString(testKey.PublicKey.N.Bytes()))),
"not json": []byte(`nonsense`),
}
for name, raw := range cases {
t.Run(name, func(t *testing.T) {
if _, err := jose.ParseJWKS(raw); err == nil {
t.Fatal("accepted")
}
})
}
}
// Two entries under one key id make key selection ambiguous, so the set is
// refused rather than resolved by order.
func TestParseJWKSRejectsDuplicateKeyIDs(t *testing.T) {
var first, second map[string]interface{}
if err := json.Unmarshal(jwks("k1", testKey), &first); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(jwks("k1", testKey), &second); err != nil {
t.Fatal(err)
}
merged, err := json.Marshal(map[string]interface{}{
"keys": []interface{}{
first["keys"].([]interface{})[0],
second["keys"].([]interface{})[0],
},
})
if err != nil {
t.Fatal(err)
}
if _, err := jose.ParseJWKS(merged); err == nil {
t.Fatal("duplicate key id accepted")
}
}
// Keys for other algorithms alongside a usable RS256 key are ignored, not fatal.
func TestParseJWKSIgnoresIrrelevantKeys(t *testing.T) {
var usable map[string]interface{}
if err := json.Unmarshal(jwks("k1", testKey), &usable); err != nil {
t.Fatal(err)
}
mixed, err := json.Marshal(map[string]interface{}{
"keys": []interface{}{
map[string]interface{}{"kty": "EC", "kid": "ec", "crv": "P-256"},
usable["keys"].([]interface{})[0],
},
})
if err != nil {
t.Fatal(err)
}
keys, err := jose.ParseJWKS(mixed)
if err != nil {
t.Fatalf("usable key set rejected: %v", err)
}
if _, ok := keys["k1"]; !ok || len(keys) != 1 {
t.Fatalf("unexpected key set: %v", keys)
}
}