mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
216 lines
6.2 KiB
Go
216 lines
6.2 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mlkem
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"testing"
|
|
)
|
|
|
|
// FuzzMLKEMEncapsDecaps tests that encapsulate/decapsulate roundtrips correctly,
|
|
// producing the same shared secret on both sides for arbitrary randomness.
|
|
func FuzzMLKEMEncapsDecaps(f *testing.F) {
|
|
f.Add(uint8(0))
|
|
f.Add(uint8(1))
|
|
f.Add(uint8(2))
|
|
|
|
f.Fuzz(func(t *testing.T, modeIdx uint8) {
|
|
mode := Mode(modeIdx % 3)
|
|
|
|
pk, sk, err := GenerateKey(mode)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey(%d): %v", mode, err)
|
|
}
|
|
|
|
ct, sharedKey1, err := pk.Encapsulate()
|
|
if err != nil {
|
|
t.Fatalf("Encapsulate: %v", err)
|
|
}
|
|
|
|
expectedCTSize := GetCiphertextSize(mode)
|
|
if len(ct) != expectedCTSize {
|
|
t.Fatalf("ciphertext size = %d, want %d", len(ct), expectedCTSize)
|
|
}
|
|
|
|
sharedKey2, err := sk.Decapsulate(ct)
|
|
if err != nil {
|
|
t.Fatalf("Decapsulate: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(sharedKey1, sharedKey2) {
|
|
t.Fatal("shared secrets do not match")
|
|
}
|
|
|
|
// Verify key serialization roundtrip
|
|
pkBytes := pk.Bytes()
|
|
pk2, err := PublicKeyFromBytes(pkBytes, mode)
|
|
if err != nil {
|
|
t.Fatalf("PublicKeyFromBytes: %v", err)
|
|
}
|
|
|
|
skBytes := sk.Bytes()
|
|
sk2, err := PrivateKeyFromBytes(skBytes, mode)
|
|
if err != nil {
|
|
t.Fatalf("PrivateKeyFromBytes: %v", err)
|
|
}
|
|
|
|
// Encapsulate with deserialized public key
|
|
ct2, sharedKey3, err := pk2.Encapsulate()
|
|
if err != nil {
|
|
t.Fatalf("Encapsulate with deserialized pk: %v", err)
|
|
}
|
|
|
|
// Decapsulate with deserialized private key
|
|
sharedKey4, err := sk2.Decapsulate(ct2)
|
|
if err != nil {
|
|
t.Fatalf("Decapsulate with deserialized sk: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(sharedKey3, sharedKey4) {
|
|
t.Fatal("shared secrets do not match after key deserialization")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzMLKEMMalformedCiphertext tests that Decapsulate doesn't panic on garbage ciphertext.
|
|
// ML-KEM is designed to be implicitly rejection-safe: decapsulating an invalid
|
|
// ciphertext produces a pseudorandom shared secret rather than an error (FO transform).
|
|
// We verify no panics occur regardless of ciphertext content.
|
|
func FuzzMLKEMMalformedCiphertext(f *testing.F) {
|
|
f.Add([]byte{}, uint8(0))
|
|
f.Add([]byte{0x00}, uint8(0))
|
|
f.Add(bytes.Repeat([]byte{0xff}, 768), uint8(0)) // MLKEM512 ct size
|
|
f.Add(bytes.Repeat([]byte{0xaa}, 1088), uint8(1)) // MLKEM768 ct size
|
|
f.Add(bytes.Repeat([]byte{0xbb}, 1568), uint8(2)) // MLKEM1024 ct size
|
|
f.Add(bytes.Repeat([]byte{0x00}, 1088), uint8(1))
|
|
f.Add(bytes.Repeat([]byte{0xde, 0xad}, 800), uint8(2))
|
|
|
|
f.Fuzz(func(t *testing.T, garbageCT []byte, modeIdx uint8) {
|
|
mode := Mode(modeIdx % 3)
|
|
|
|
_, sk, err := GenerateKey(mode)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey(%d): %v", mode, err)
|
|
}
|
|
|
|
// Must not panic. May return error for wrong-size ciphertext.
|
|
sharedKey, err := sk.Decapsulate(garbageCT)
|
|
|
|
expectedCTSize := GetCiphertextSize(mode)
|
|
if len(garbageCT) != expectedCTSize {
|
|
// Wrong-size ciphertext must be rejected.
|
|
if err == nil {
|
|
t.Fatal("Decapsulate accepted wrong-size ciphertext")
|
|
}
|
|
return
|
|
}
|
|
|
|
// For correct-size garbage ciphertext, ML-KEM's implicit rejection
|
|
// means decaps should succeed (returning a pseudorandom key) or
|
|
// return an error. Either is acceptable; panic is not.
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// If decapsulation succeeded, shared key should be non-empty.
|
|
if len(sharedKey) == 0 {
|
|
t.Fatal("Decapsulate returned empty shared key without error")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzMLKEMMalformedPublicKey tests that Encapsulate doesn't panic on garbage public keys.
|
|
// Exercises both PublicKeyFromBytes deserialization and Encapsulate with the result.
|
|
func FuzzMLKEMMalformedPublicKey(f *testing.F) {
|
|
f.Add([]byte{}, uint8(0))
|
|
f.Add(bytes.Repeat([]byte{0xff}, 48), uint8(0))
|
|
f.Add(bytes.Repeat([]byte{0x00}, 800), uint8(0)) // MLKEM512 pk size
|
|
f.Add(bytes.Repeat([]byte{0xaa}, 1184), uint8(1)) // MLKEM768 pk size
|
|
f.Add(bytes.Repeat([]byte{0xbb}, 1568), uint8(2)) // MLKEM1024 pk size
|
|
f.Add(bytes.Repeat([]byte{0xde, 0xad, 0xbe, 0xef}, 300), uint8(1))
|
|
|
|
f.Fuzz(func(t *testing.T, garbagePK []byte, modeIdx uint8) {
|
|
mode := Mode(modeIdx % 3)
|
|
|
|
// PublicKeyFromBytes must not panic.
|
|
pk, err := PublicKeyFromBytes(garbagePK, mode)
|
|
if err != nil {
|
|
// Expected for most garbage inputs (wrong size, invalid encoding).
|
|
return
|
|
}
|
|
|
|
// If deserialization succeeded, Encapsulate must not panic.
|
|
ct, sharedKey, err := pk.Encapsulate()
|
|
if err != nil {
|
|
// Some garbage keys may parse but fail during encapsulation.
|
|
return
|
|
}
|
|
|
|
// If encapsulation succeeded, outputs should be non-empty.
|
|
if len(ct) == 0 {
|
|
t.Fatal("Encapsulate returned empty ciphertext without error")
|
|
}
|
|
if len(sharedKey) == 0 {
|
|
t.Fatal("Encapsulate returned empty shared key without error")
|
|
}
|
|
|
|
// Verify ciphertext size matches expected for the mode.
|
|
expectedCTSize := GetCiphertextSize(mode)
|
|
if len(ct) != expectedCTSize {
|
|
t.Fatalf("ciphertext size = %d, want %d", len(ct), expectedCTSize)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzMLKEMKeySerializationRoundtrip tests that key serialization/deserialization
|
|
// roundtrips without losing data.
|
|
func FuzzMLKEMKeySerializationRoundtrip(f *testing.F) {
|
|
f.Add(uint8(0))
|
|
f.Add(uint8(1))
|
|
f.Add(uint8(2))
|
|
|
|
f.Fuzz(func(t *testing.T, modeIdx uint8) {
|
|
mode := Mode(modeIdx % 3)
|
|
|
|
pk, sk, err := GenerateKey(mode)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey(%d): %v", mode, err)
|
|
}
|
|
|
|
// Public key roundtrip
|
|
pkBytes := pk.Bytes()
|
|
pk2, err := PublicKeyFromBytes(pkBytes, mode)
|
|
if err != nil {
|
|
t.Fatalf("PublicKeyFromBytes: %v", err)
|
|
}
|
|
if !bytes.Equal(pk.Bytes(), pk2.Bytes()) {
|
|
t.Fatal("public key bytes changed after roundtrip")
|
|
}
|
|
|
|
// Private key roundtrip
|
|
skBytes := sk.Bytes()
|
|
sk2, err := PrivateKeyFromBytes(skBytes, mode)
|
|
if err != nil {
|
|
t.Fatalf("PrivateKeyFromBytes: %v", err)
|
|
}
|
|
if !bytes.Equal(sk.Bytes(), sk2.Bytes()) {
|
|
t.Fatal("private key bytes changed after roundtrip")
|
|
}
|
|
|
|
// Cross-key compatibility: encaps with pk, decaps with deserialized sk
|
|
ct, ss1, err := pk.Encapsulate(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Encapsulate: %v", err)
|
|
}
|
|
ss2, err := sk2.Decapsulate(ct)
|
|
if err != nil {
|
|
t.Fatalf("Decapsulate with deserialized sk: %v", err)
|
|
}
|
|
if !bytes.Equal(ss1, ss2) {
|
|
t.Fatal("shared secrets differ after private key deserialization")
|
|
}
|
|
})
|
|
}
|