mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
179 lines
5.9 KiB
Go
179 lines
5.9 KiB
Go
// Algorithm-substitution defense tests for verifyJWS (Red audit F3 /
|
|
// CVE-2015-9235 family). The eight cases below pin down the alg ↔
|
|
// key-type matrix at the dispatch layer:
|
|
//
|
|
// 1. EdDSA + ed25519.PublicKey → PASS
|
|
// 2. ES256 + *ecdsa.PublicKey on P-256 → PASS
|
|
// 3. PS256 + *rsa.PublicKey → PASS
|
|
// 4. EdDSA paired with RSA pub → ErrAlgKeyTypeMismatch
|
|
// 5. ES256 paired with ed25519 pub → ErrAlgKeyTypeMismatch
|
|
// 6. ES256 paired with ecdsa P-384 → ErrAlgKeyTypeMismatch
|
|
// 7. alg=none → ErrAlgNoneRefused
|
|
// 8. alg=Unknown → ErrUnknownAlg
|
|
//
|
|
// Cases 4-6 are the substitution attack: a forged JWS header that
|
|
// names a different alg than the trust-root's key actually supports.
|
|
// Pre-fix, dispatch ran by alg but verification ran by key type, so
|
|
// an attacker who could choose the alg got to pick which primitive
|
|
// ran against which key. The test for case 4 is the canonical
|
|
// reproducer in the audit narrative.
|
|
package nvidia
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"errors"
|
|
"math/big"
|
|
"testing"
|
|
)
|
|
|
|
func TestVerifyJWS_EdDSA_Valid(t *testing.T) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
signedInput := []byte("eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ0ZXN0In0")
|
|
sig := ed25519.Sign(priv, signedInput)
|
|
if err := verifyJWS(pub, "EdDSA", signedInput, sig); err != nil {
|
|
t.Fatalf("expected PASS, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifyJWS_ES256_Valid(t *testing.T) {
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
signedInput := []byte("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0")
|
|
digest := sha256.Sum256(signedInput)
|
|
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
sig := joseConcat(r, s, 32) // P-256 coord size = 32 bytes
|
|
|
|
if err := verifyJWS(&priv.PublicKey, "ES256", signedInput, sig); err != nil {
|
|
t.Fatalf("expected PASS, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifyJWS_PS256_Valid(t *testing.T) {
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
signedInput := []byte("eyJhbGciOiJQUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0")
|
|
digest := sha256.Sum256(signedInput)
|
|
sig, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, digest[:], &rsa.PSSOptions{
|
|
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
|
Hash: crypto.SHA256,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
if err := verifyJWS(&priv.PublicKey, "PS256", signedInput, sig); err != nil {
|
|
t.Fatalf("expected PASS, got %v", err)
|
|
}
|
|
}
|
|
|
|
// === Algorithm-substitution attacks ===
|
|
|
|
// alg=EdDSA paired with an RSA trust-root for the same kid. The forged
|
|
// header tells the verifier "this is an Ed25519 signature"; the trust
|
|
// root holds an *rsa.PublicKey. Pre-fix, verifySignature dispatched by
|
|
// key type and ran RSA-PSS on the attacker-chosen signedInput. Post-fix,
|
|
// the alg=EdDSA branch type-asserts ed25519.PublicKey and refuses.
|
|
func TestVerifyJWS_EdDSA_AgainstRSAPub_Rejected(t *testing.T) {
|
|
rsaPriv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
signedInput := []byte("attacker-chosen")
|
|
// Attacker can produce *anything* for sig — verifier must reject
|
|
// before looking at sig at all.
|
|
sig := []byte{0xAA, 0xBB, 0xCC}
|
|
err = verifyJWS(&rsaPriv.PublicKey, "EdDSA", signedInput, sig)
|
|
if !errors.Is(err, ErrAlgKeyTypeMismatch) {
|
|
t.Fatalf("expected ErrAlgKeyTypeMismatch, got %v", err)
|
|
}
|
|
}
|
|
|
|
// alg=ES256 paired with ed25519 pub.
|
|
func TestVerifyJWS_ES256_AgainstEd25519Pub_Rejected(t *testing.T) {
|
|
pub, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
err = verifyJWS(pub, "ES256", []byte("input"), []byte{0x00})
|
|
if !errors.Is(err, ErrAlgKeyTypeMismatch) {
|
|
t.Fatalf("expected ErrAlgKeyTypeMismatch, got %v", err)
|
|
}
|
|
}
|
|
|
|
// alg=ES256 paired with ecdsa P-384 pub. Curve mismatch — even though
|
|
// both are *ecdsa.PublicKey, ES256 is bound to P-256 only.
|
|
func TestVerifyJWS_ES256_AgainstP384Pub_Rejected(t *testing.T) {
|
|
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
// Construct a real ES384-shaped signature so the failure is
|
|
// proven to come from the curve check, not from a malformed sig.
|
|
signedInput := []byte("input")
|
|
digest := sha512.Sum384(signedInput)
|
|
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
sig := joseConcat(r, s, 48) // P-384 coord size = 48 bytes
|
|
|
|
err = verifyJWS(&priv.PublicKey, "ES256", signedInput, sig)
|
|
if !errors.Is(err, ErrAlgKeyTypeMismatch) {
|
|
t.Fatalf("expected ErrAlgKeyTypeMismatch (curve), got %v", err)
|
|
}
|
|
}
|
|
|
|
// alg=none must be refused unconditionally regardless of trust-root key.
|
|
func TestVerifyJWS_AlgNone_Refused(t *testing.T) {
|
|
pub, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
for _, alg := range []string{"none", "None", "NONE", ""} {
|
|
err := verifyJWS(pub, alg, []byte("input"), []byte("sig"))
|
|
if !errors.Is(err, ErrAlgNoneRefused) {
|
|
t.Fatalf("alg=%q: expected ErrAlgNoneRefused, got %v", alg, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unknown alg must be refused.
|
|
func TestVerifyJWS_UnknownAlg_Refused(t *testing.T) {
|
|
pub, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("genkey: %v", err)
|
|
}
|
|
err = verifyJWS(pub, "HS256", []byte("input"), []byte("sig"))
|
|
if !errors.Is(err, ErrUnknownAlg) {
|
|
t.Fatalf("expected ErrUnknownAlg, got %v", err)
|
|
}
|
|
}
|
|
|
|
// === helpers ===
|
|
|
|
// joseConcat encodes (r, s) as fixed-width big-endian r||s (RFC 7515 §3.4).
|
|
func joseConcat(r, s *big.Int, size int) []byte {
|
|
out := make([]byte, size*2)
|
|
rB := r.Bytes()
|
|
sB := s.Bytes()
|
|
copy(out[size-len(rB):size], rB)
|
|
copy(out[size*2-len(sB):], sB)
|
|
return out
|
|
}
|