libsoliton/soliton/tests/compute_vectors.rs
Kamal Tufekcic 1d99048c95
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
initial commit
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-02 23:48:10 +03:00

517 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Compute and verify Specification.md Appendix F test vectors F.25-F.29.
//!
//! Run with: cargo test -p libsoliton compute_vectors -- --nocapture
//!
//! On first run the tests print the computed hex values; those values are then
//! hardcoded in Specification.md. Subsequent runs verify the implementation still
//! matches.
use soliton::identity::IdentitySecretKey;
use soliton::primitives::sha3_256;
use soliton::primitives::{aead, argon2, hkdf};
// ── F.25: Standalone HKDF-SHA3-256 ───────────────────────────────────────────
#[test]
fn f25_hkdf_sha3_256() {
let salt = [0x00u8; 32];
let ikm = [0x01u8; 64];
let info = b"lo-test-hkdf-v1";
let mut out = [0u8; 64];
hkdf::hkdf_sha3_256(&salt, &ikm, info, &mut out).unwrap();
let hex = hex::encode(out);
println!("F.25 HKDF output (64 bytes):\n {hex}");
// Assert against the expected value computed from the reference implementation.
assert_eq!(
hex, EXPECTED_F25,
"F.25 HKDF vector mismatch — implementation changed or wrong HMAC block size"
);
}
const EXPECTED_F25: &str = "4a694c255636bd5a472c807cf1400a05f78a4a3e93b7f663dd6825c9d496904c6224e025169b8c67e62ed3b10129da39c546d6e84c84920f69232fd8e76e7cf0";
// ── F.26: Standalone XChaCha20-Poly1305 ──────────────────────────────────────
#[test]
fn f26_xchacha20_poly1305() {
let key = [0x02u8; 32];
let nonce = [0x03u8; 24];
let plaintext = b"hello world";
let aad_bytes = b"lo-test-aead-v1";
let ct = aead::aead_encrypt(&key, &nonce, plaintext, aad_bytes).unwrap();
assert_eq!(
ct.len(),
11 + 16,
"expected 11-byte ciphertext + 16-byte tag"
);
let hex = hex::encode(&ct);
println!("F.26 XChaCha20-Poly1305 ciphertext||tag (27 bytes):\n {hex}");
println!(" ciphertext: {}", hex::encode(&ct[..11]));
println!(" tag: {}", hex::encode(&ct[11..]));
assert_eq!(hex, EXPECTED_F26, "F.26 XChaCha20-Poly1305 vector mismatch");
}
const EXPECTED_F26: &str = "356c4d3352734de8f25fe391c8f97e537cf5c7d3f07d2b03388f77";
// ── F.27: HybridSign / HybridVerify ──────────────────────────────────────────
//
// Uses a synthetic identity key with known sub-key seeds and ML-DSA rnd pinned
// to [0x00; 32] for determinism. This is NOT production signing — hedged mode
// with getrandom entropy is required outside of test vectors.
#[test]
fn f27_hybrid_sign_verify() {
use ml_dsa::{B32, MlDsa65, SigningKey};
// Synthetic identity secret key:
// bytes 0..2432 — X-Wing sk (not used for signing, all 0x01)
// bytes 2432..2464 — Ed25519 seed = [0x02; 32]
// bytes 2464..2496 — ML-DSA-65 seed = [0x03; 32]
let mut sk_bytes = vec![0x01u8; 2496];
sk_bytes[2432..2464].fill(0x02); // Ed25519 seed
sk_bytes[2464..2496].fill(0x03); // ML-DSA-65 seed
let _sk = IdentitySecretKey::from_bytes(sk_bytes).unwrap();
let message = b"lo-test-sign-v1";
// Ed25519 component — deterministic (RFC 8032 §5.1.6 uses SHA-512(seed||msg)).
let ed_seed: [u8; 32] = [0x02u8; 32];
let ed_signing_key = ed25519_dalek::SigningKey::from_bytes(&ed_seed);
use ed25519_dalek::Signer;
let ed_sig = ed_signing_key.sign(message);
let ed_sig_bytes = ed_sig.to_bytes(); // 64 bytes
// ML-DSA-65 component — pinned rnd = [0x00; 32] for determinism.
// Seed is type Seed = B32 (no generic parameter).
let mldsa_seed: ml_dsa::Seed = [0x03u8; 32].into();
let mldsa_sk = SigningKey::<MlDsa65>::from_seed(&mldsa_seed);
let rnd: B32 = [0x00u8; 32].into();
// sign_internal returns Signature<MlDsa65> directly (not encoded).
let mldsa_sig = mldsa_sk.sign_internal(&[message.as_ref()], &rnd);
let mldsa_sig_bytes: Vec<u8> = mldsa_sig.encode().to_vec();
assert_eq!(mldsa_sig_bytes.len(), 3309, "ML-DSA-65 signature size");
// Composite: Ed25519 (64) || ML-DSA-65 (3309) = 3373 bytes.
let mut composite = Vec::with_capacity(3373);
composite.extend_from_slice(&ed_sig_bytes);
composite.extend_from_slice(&mldsa_sig_bytes);
assert_eq!(composite.len(), 3373);
// Verify both components independently to confirm the vector is self-consistent.
let ed_verifying = ed_signing_key.verifying_key();
ed_verifying
.verify_strict(message, &ed_sig)
.expect("Ed25519 verify failed");
// verify_internal expects &Signature<MlDsa65>, not encoded bytes.
let mldsa_vk = mldsa_sk.verifying_key();
assert!(
mldsa_vk.verify_internal(message, &mldsa_sig),
"ML-DSA verify_internal failed"
);
// Also verify via soliton's hybrid_verify to confirm the composite layout matches.
let mut pk_bytes = vec![0x00u8; 3200];
// Ed25519 public key at bytes 1216..1248
pk_bytes[1216..1248].copy_from_slice(ed_verifying.as_bytes());
// ML-DSA public key at bytes 1248..3200
let mldsa_pk_bytes = mldsa_vk.encode().to_vec();
assert_eq!(mldsa_pk_bytes.len(), 1952, "ML-DSA-65 pk size");
pk_bytes[1248..3200].copy_from_slice(&mldsa_pk_bytes);
let pk = soliton::identity::IdentityPublicKey::from_bytes(pk_bytes).unwrap();
let hybrid_sig = soliton::identity::HybridSignature::from_bytes(composite.clone()).unwrap();
soliton::identity::hybrid_verify(&pk, message, &hybrid_sig).expect("hybrid_verify failed");
let hex = hex::encode(&composite);
assert_eq!(hex, EXPECTED_F27, "F.27 HybridSign vector mismatch");
}
const EXPECTED_F27: &str = "21aafa2d66a4774e163064717412a2694527c84cdc57e93370ba05738940bdd0facc5cb6330088ce849635ac41a0099842a40ef82cb0046f6978eeb7196be00f1b47e0e18a96f465b42396b24a77f72f9a8bace68a607bb155842ebed1a2994434432a73def2d5a0d5de41edb1be4a53d8586434e498ed880e01008cf4607b14de2b9164e11f7ca202888d3ff5fb8caeb8b3dc56d911391ec94854679c97352f0fbb893df675f6da9b1105515957b766fdc08b80be1b10c913d5a03526234cb3eb92b2953775442f3927f8b8e2faf18aeb13818983a72b2334c295f20a4099687d12b90e4df62e3a70645798ce3816d8327e68a22e995a707f8a2189782ee515db898ac8ee8f722d6c7a6a526d418b03ca429ca996c7bc8f5f15d4d9831a2a15d46a8534eda607bf0def6fb703565d1ac1abe7df371c5a72567998459f33ee83c7d2cb2afe7c683568859c7d560c841182b92b63f5427dfb12c457c05b5a41c19d4d9b38953d5403814e8640902eb6992775aefa30a764c5b7167bef27c92159dbe62d9be2b680ef16c7cc8e31afa2712212e50ea2053cf02fe8e2d8a7a754a90df89c1c07e79240205c0154f0f937c188c1053fde54f0d4976be155f0c43e659d5510ca8d8dfa49a6aaf0537c0d653799cd3b9f8a82bc2b889ddd7e2a0085709ea642e23ab79e5bbbbd351bf6a062880b3d824057e1d4425d45de8815d2ee5d3ec611566b021027883548d8ef1c9449ba137ecc62fdcdd4aeb52d81e9aca67dd6a592cca85a0d7c5fb5cbe9e0722c77bfab6aaa105c5b9920f2cd0e19a04cc902a3f6ec020a5db8cc167ced093a75531feb1a6ffc415d0487459a58a3844e03db2d50498f92bec6f6ffbdbd898b4bc650ae066d6f2d57c3045b34de041c0c820b24e62554208770e65d0f87d0cdcb105214972d8fbb7e1dae88a367e638305193079e9525af08dfd6a4aea91b6cb80e0240b40a2eb2601b054d1f0769d7392d5a6f04571468c9303952b24f0058bc5d1b1d257367d81a33693945b77c98e188a977962e8dfced1a1ef3f096cce98ddfd199a8f96df6981d572e23fcc2277e2d4af5b5d4ca5bafaf94ebbb4cdd7f4575e0a81ee7b00c1121ca9d54ddda71aae4ad0cd36dda568f23ec1c30fa34a4a221ab86d2bb9d24456bb2382eb748c052a2ec6c943ab884e833b992d4a6d9f1209d778634eabb4611525eac931b1360647b3bc0868e5b62a723cfcb7214052c9ea89d867fb85288f47b8e26a41fdb08a4d2dc26f5e247624eaea5df2270ab2d6e46e3efe0e6e4e3bcf586c287967d3bfaab24b4eb37fdf63ac3cbdff44dfcf1b26d5c127c5991d85d36ba67d9a30a963e53f53279d49b21d21e4f7f3fb520bb3dbf3ecac4abb052278c25da4fb5a662df85e133705026b82705cb8f43c4666efe7159a7067bcfdb433d373afe35087d5ac5267372ac69c867b3f22a2d1489f46853af370ce38d87cf12039faccd6bd22935029d8b41ee0ccef60a02a0986851a65e6920b91a04edd1214cd33e33c83f1788d5d59775ef4d3938aac737a40497f595b4e6e249fd6192d7ad18cc818835c16f29d58fd16dcabcce70cef7c5ce8d280da7f1a525e02b6d32b549819dc4571fe898f03f95ae34d644dd473a9d8832115293876b7bd26779fde43968f4aba8f00fa452ed6b67d89ec571651b61cbc811b6f914147bf3ef51449dc3ee50b56b5d9eff689d2a345b3cf30c6e8e6d1cf540bbda68d657087ba73896db0ade6fe8359a4b1b696131c3de72449ac22feea1b3475b94f378bdcd979f8c5441b96bfa92eaab76ef0e2ff5f3292ba9bd614af635dd0f5eb727b9fe916a4d476661637d47c2212deec52a76a05e7b7bd6363ed50f70c622299dbc29afdebc5af25a099c1e239e626b1f589acdc2a0aedead5f9d0a8efa81aff8eef6aed2bd19d0a20ee1f71b7730a483dcab7e0ecd7d09a01f301c41eb16b5ecd847edcaf880bb3246e5f856a26af60e7b04d8019edb3d24def94926fea520d2f6e03ec5ec2d59a7d45969f576f44ca80db6a0083a705d6bbd92ee2e616f9289e6d6fcf42356d2762dae019177d5ec6b1fb7effc30aed5396d09cb756efb36ee2d07b891dab607ced2440f7e2dabc84c556ab138fe48f510add050b9c5ae6014eade2474c4b5fd80fa8708a357a5b4c7b5230b3e02f930dd29e0d4cce88ddac945898296a774b785cf52c3fdc3abc394f7c361bc4b43f399ebf48b3f75a29ba47a508cb5450943bdd3bfb2ddc4b07c6dab3fb0f799278ffce9e98eca310d00c39656f55747173f60e9dbd852190ba5b0d65520129b6d6d113e66510962ebffd262dd5342caf00950bc621069ca8867b9bf7695d1e0dd034484a70fc63de5dedc10f4f912ffb046c2bcfd911bd96eda77aef252736c2cb0da9d5f5368cd668b22378f4e0e292523072a6385007a1f70c3b95e7d1034d175dbb944fe4f8c13ca7c3cbf42f0858466434d8f69dec7230eaa10db1f20da50c38caa1bc9f530352043c0104c97f4154feafd2a68b5a42926c6f6a4bcc48d4921a63cef4310b068b4695e3c2507db2a3681db2ed3c2e036220c776795f5e2d68a5834b66f8e430b42d6e7a8b56e55e38fd0bbb2e4d58e15f4eebeb8edec497fe48494ed5db4688b9ef93ef0c0763f48e3faf1726e25668d86a298cf135418a7492ea6c3edea8f31d1ec14e2a6989292652eb2202de05d09fd12255e1fbb961912197965c4dc4d5badfd879fd8b519a280fba01fefc374edb50fec0bc00df21ec002d80a36ff33826781d3311190797e498613a3136a70e247b2a407887f47697086e8b449d980e7204dff1e74a47d975f6daefea7f2b6f4ee4f00c637c4f12cd5daa892f607e76feea893cd3bd1844ef82c5dd6f7b5c047a48dd1963e943b6417331e12e6d037f9a1a6e525c16c6a1999df58309eddea68a63c6569abd8c808f688dcfb6cd4f65fda5fb70fb7b67942bb48446cf43d7828d2618ae435d8c1da3ef3a292d0fba4632488ec3a6f78b4f9c53380c0f6ee65298992ad4862211492ef4751c4e4821e16a47c2aab6541a93843a3d194648e9e6d830e7599bb3309d611a253e04e55a38200caf722a4a2d3b988fafdac9927938bd20c98c1d207a4527d5a4778e10d889b6067866ed22ad85cd21214c2adad42fa1ca0ee6e74683fc549c373bea4d74ccf4260cfc1f2da694bea658987d8c0669c24a964bbde4ce0f467ca49d9c61246adbc19560ea5a04dd57b21bd33405d8312dc735c4b594d08fb428502a85fee960f67d56562a076f1de0777d91f4d038a3092fb32f40ca9814b5b42a53ebbca72f82a1764ea5cae36b9b5892377416debce72194ebf569edd36e55bd4cadc769343b7f5539137be5480a0c83bc3d9e6a15cc6ac0e6441794672d598f867d59b613c9baed10e6d29ae823625392c7ad1170606aa0e267c0409cbf6fb2f75ffa454973f2aa1d35e1f11249eb531d5ee7a0e3278398195e194d251d8d9a4c9174e2f8e89fd10944269bda20e11b12f81b39c9cd3ff9590951ce54cc19a439dc2f422a035eb02f6137b938d572fb11e7aa64e10ec075683f178b7a4f014611a3964b7b958786915aa7f33f8ce4fb3e15dbd39e111b43230d87daf59aebaa7c8137010388e29a952607ea9dae677ac3280f6b10930e8ef02ddd72a25b059864c8e49f7737e6d086af33ee309180e586fed5601d750a055c62c10ec63daf44299ccefea74b30b950b2f5d0552ed372350511c6a4ea2497285b90400d82fbdac3f50b37aab60325f1620268121f2432d153163549d0202e0b63e71c3f265155179effa0f00c34bacf4f10f6ea3524c7a47e7be236c82839beb9e3aa16aafa2e4fd56e62aade089fc81fd2f9c59d9b67d0357768d0e691415671b0b28972e9f847ee1142f14b1486e28b8c0d1d7731a040a97cf375f9f0d3f5ca6a32275dadb13c888d640908de73f151841b387d202315b1f4e217554ba153f75a8ae187c805129aea6a577da9102f024199809770cca1170c89f05d6dc5ee876c98f0ee31b8acfc98015b271341427b960015c0560f627a5f8378f683eb2936748557a2ee32168bc1faed7b31b498bb86c780e7e7d86b3a56c7fee30587cd284562e3aada483fa8b176992d47c6feafaaf2af07fd5d7ee644c84e1bef83206640dec170c5c7b4811e7622a70ca42a33ea0fd2c316cf83d34e5b040e48df54b7897f65c8a375ddeaae5a508cac31f68ffb2c693479f64f05066661396e8f5dd2d69e8daa58c54242262695a5e52c789d54b6604589efde1b254f44e9a60f0187b1a7c5bf15a57b407de5f84e44c8bb48af8597691e27dd467082c1eb15c470be5d576ac16232b878c81c32504701584bd5a0f1452dace5403afb81c4b40efdd4179391f464c933ad7945157c8562dab7570869b01618046db94b701f1e8a7c59204a8b68d9df8f326f03f62261d066bd19dbfde48377fdaf94a61bb8f22ba3b00c5aea6815a7512658db8b574f20bca5b592e12d5970f0a27912727fc8a13d7008c73e3b9e4ddbac1837cac0caea204c00aa14d8fa4f4234370f9d12f38a21d13e929cf70d11f00f2728e8d85c145399d8a3ad3f12ca8c04dce5534c13efc4ba7d03df3e333bf5cfa8201beb1c8bb289762a0789eb8a0d9368c98a13c1d6a307e9ce690de24d263fa8f1015398188a1bcdee7e9466b6c88a1afb3bbd03756ad2a35464c932a5194000000000000000000000000000000000000000000000000000a1316161b1e";
// ── F.28: Streaming AEAD multi-chunk wire vector ──────────────────────────────
//
// Computed manually using the same nonce/AAD construction as StreamEncryptor,
// bypassing stream_encrypt_init (which draws a random base_nonce).
#[test]
fn f28_streaming_aead_wire() {
// Fixed inputs per the planned vector spec.
let key = [0x04u8; 32];
let base_nonce = [0x05u8; 24];
let version: u8 = 0x01;
let flags: u8 = 0x00;
let caller_aad: &[u8] = b"";
// Header: version(1) || flags(1) || base_nonce(24)
let mut header = [0u8; 26];
header[0] = version;
header[1] = flags;
header[2..].copy_from_slice(&base_nonce);
// Chunk 0: plaintext = [0x41; 16], non-final (tag_byte = 0x00)
let chunk0 = encrypt_streaming_chunk(
&key,
&base_nonce,
version,
flags,
caller_aad,
&[0x41u8; 16],
0,
false,
);
assert_eq!(chunk0.len(), 33); // 1 + 16 + 16
// Chunk 1: plaintext = [0x42; 8], final (tag_byte = 0x01)
let chunk1 = encrypt_streaming_chunk(
&key,
&base_nonce,
version,
flags,
caller_aad,
&[0x42u8; 8],
1,
true,
);
assert_eq!(chunk1.len(), 25); // 1 + 8 + 16
println!("F.28 Streaming AEAD wire bytes:");
println!(" header (26 bytes): {}", hex::encode(header));
println!(" chunk0 (33 bytes): {}", hex::encode(&chunk0));
println!(" chunk1 (25 bytes): {}", hex::encode(&chunk1));
println!(
" full stream: {}",
hex::encode([&header[..], &chunk0, &chunk1].concat())
);
let full = [&header[..], &chunk0, &chunk1].concat();
assert_eq!(
hex::encode(&full),
EXPECTED_F28,
"F.28 streaming wire vector mismatch"
);
}
/// Compute one streaming chunk using the same nonce/AAD derivation as StreamEncryptor.
#[allow(clippy::too_many_arguments)]
fn encrypt_streaming_chunk(
key: &[u8; 32],
base_nonce: &[u8; 24],
version: u8,
flags: u8,
caller_aad: &[u8],
plaintext: &[u8],
index: u64,
is_last: bool,
) -> Vec<u8> {
let tag_byte: u8 = if is_last { 0x01 } else { 0x00 };
// Nonce = base_nonce XOR (index_be || tag_byte || 0×15)
let mut mask = [0u8; 24];
mask[..8].copy_from_slice(&index.to_be_bytes());
mask[8] = tag_byte;
let mut nonce = *base_nonce;
for i in 0..24 {
nonce[i] ^= mask[i];
}
// AAD = "lo-stream-v1" || version || flags || base_nonce || index_be || tag_byte || caller_aad
let mut chunk_aad = Vec::new();
chunk_aad.extend_from_slice(b"lo-stream-v1");
chunk_aad.push(version);
chunk_aad.push(flags);
chunk_aad.extend_from_slice(base_nonce);
chunk_aad.extend_from_slice(&index.to_be_bytes());
chunk_aad.push(tag_byte);
chunk_aad.extend_from_slice(caller_aad);
let ciphertext = aead::aead_encrypt(key, &nonce, plaintext, &chunk_aad).unwrap();
let mut out = Vec::with_capacity(1 + ciphertext.len());
out.push(tag_byte);
out.extend_from_slice(&ciphertext);
out
}
const EXPECTED_F28: &str = "010005050505050505050505050505050505050505050505050500d5425e7085cc776bc8c608ad84c41cc37eefb10d2b859ebddf8c1187c616c0c401aac61cb7b722895cb246433e7ebc081e92150081150d345d";
// ── F.29: Argon2id + XChaCha20-Poly1305 passphrase-protected key blob ────────
#[test]
fn f29_passphrase_key_blob() {
let password = b"lo-test-passphrase";
let argon2_salt = [0x06u8; 16];
let aead_nonce = [0x07u8; 24];
// Use OWASP_MIN for test speed; real blobs use RECOMMENDED.
// OWASP_MIN: m=19456 KiB, t=2, p=1
let params = argon2::Argon2Params::OWASP_MIN;
// Synthetic identity public key — all zeros for known fingerprint.
let synth_pk = [0x00u8; 3200];
let fingerprint = sha3_256::hash(&synth_pk); // 32 bytes, used as AAD
// Derive 32-byte key from passphrase.
let mut derived_key = [0u8; 32];
argon2::argon2id(password, &argon2_salt, params, &mut derived_key).unwrap();
// Encrypt a small synthetic key payload.
let plaintext = b"test-key-material";
let ct = aead::aead_encrypt(&derived_key, &aead_nonce, plaintext, &fingerprint).unwrap();
assert_eq!(ct.len(), 17 + 16); // plaintext + tag
// Serialize blob: salt(16) || nonce(24) || ciphertext
let mut blob = Vec::new();
blob.extend_from_slice(&argon2_salt);
blob.extend_from_slice(&aead_nonce);
blob.extend_from_slice(&ct);
assert_eq!(blob.len(), 16 + 24 + 33); // = 73 bytes
println!("F.29 Argon2id + XChaCha20-Poly1305 passphrase blob:");
println!(" derived_key (32 bytes): {}", hex::encode(derived_key));
println!(" fingerprint/AAD (32): {}", hex::encode(fingerprint));
println!(" ciphertext+tag (33): {}", hex::encode(&ct));
println!(" full blob (73 bytes): {}", hex::encode(&blob));
assert_eq!(
hex::encode(&blob),
EXPECTED_F29,
"F.29 passphrase blob vector mismatch"
);
}
const EXPECTED_F29: &str = "06060606060606060606060606060606070707070707070707070707070707070707070707070707f90394fa7144500a63da86ca3ff6d900f855314f4c9030ab88b060a0ab41b9eede";
// ── F.33: HMAC-SHA3-256 with 100-byte key ─────────────────────────────────────
//
// Discriminates SHA-2 and SHA3-256 HMAC implementations: 100 bytes is above
// SHA-2's 64-byte block size (which would force key hashing) but below SHA3-256's
// 136-byte block size (which pads without hashing). Any reimplementer using a
// SHA-2-configured HMAC library hashes the 100-byte key to 32 bytes first,
// producing a different MAC — all existing vectors use 32-byte keys, which lie
// below both block sizes and therefore do not expose this bug.
#[test]
fn f33_hmac_sha3_256_long_key() {
use soliton::primitives::hmac::hmac_sha3_256;
// 100-byte key: above SHA-2 block (64 B) but below SHA3-256 block (136 B).
let key = [0xABu8; 100];
let data = b"lo-hmac-v1";
let mac = hmac_sha3_256(&key, data);
let hex = hex::encode(mac);
println!("F.33 HMAC-SHA3-256(key=[0xAB]×100, data=\"lo-hmac-v1\"):\n {hex}");
assert_eq!(
hex, EXPECTED_F33,
"F.33 HMAC-SHA3-256 long-key mismatch — SHA3-256 block size is 136 B, not 64 B"
);
}
const EXPECTED_F33: &str = "aa5575019f7aade135d379d92699d13d62cded9208869f9c9898d687d93ae293";
// ── F.34: SHA3-256 of first-message AAD with OPK (§5.4 Step 7) ───────────────
//
// Hashing the AAD provides a fixed-size discriminator for a 4,669-byte structure.
// The OPK path adds 1,126 bytes (1 flag + 2 len + 1120 ct + 4 id) vs the no-OPK
// path (4,669 vs 3,543 bytes). A reimplementer who omits the OPK ciphertext from
// the AAD, or who reverses the ct_opk/opk_id order, produces a different hash.
#[test]
fn f34_first_message_aad_with_opk() {
use soliton::kex::{SessionInit, build_first_message_aad};
use soliton::primitives::{sha3_256, xwing};
let sender_fp = [0xAAu8; 32];
let recipient_fp = [0xBBu8; 32];
// Synthetic SessionInit with all fields fixed for reproducibility.
let si = SessionInit {
crypto_version: "lo-crypto-v1".to_string(),
sender_ik_fingerprint: sender_fp,
recipient_ik_fingerprint: recipient_fp,
// sender_ek: 1216-byte X-Wing public key.
sender_ek: xwing::PublicKey::from_bytes(vec![0xCCu8; 1216]).unwrap(),
// ct_ik, ct_spk, ct_opk: 1120-byte X-Wing ciphertexts.
ct_ik: xwing::Ciphertext::from_bytes(vec![0xDDu8; 1120]).unwrap(),
ct_spk: xwing::Ciphertext::from_bytes(vec![0xEEu8; 1120]).unwrap(),
spk_id: 42u32,
ct_opk: Some(xwing::Ciphertext::from_bytes(vec![0xFFu8; 1120]).unwrap()),
opk_id: Some(7u32),
};
let aad = build_first_message_aad(&sender_fp, &recipient_fp, &si).unwrap();
println!("F.34 first-message AAD length: {} bytes", aad.len());
let hash = sha3_256::hash(&aad);
let hex = hex::encode(hash);
println!("F.34 SHA3-256(first_message_aad with OPK) (32 bytes):\n {hex}");
assert_eq!(
hex, EXPECTED_F34,
"F.34 first-message AAD with OPK hash mismatch"
);
}
const EXPECTED_F34: &str = "ba8e4c4ffb1330f47e5ca95a63671970036a1f3d07934836548efa0403e84815";
// ── F.35: HybridSign over SPK message (§5.3) ──────────────────────────────────
//
// "lo-spk-sig-v1" || SPK_pub is the message Alice signs when publishing a
// pre-key bundle. T1: verifies the domain label and that SPK_pub bytes are
// covered verbatim (no further encoding). Uses the same synthetic identity key
// and pinned ML-DSA rnd as F.27 for reproducibility.
#[test]
fn f35_hybrid_sign_spk() {
use ml_dsa::{B32, MlDsa65, SigningKey};
// Synthetic identity key: same seeds as F.27.
// Ed25519 seed = [0x02; 32], ML-DSA-65 seed = [0x03; 32].
let ed_seed: [u8; 32] = [0x02u8; 32];
let ed_signing_key = ed25519_dalek::SigningKey::from_bytes(&ed_seed);
use ed25519_dalek::Signer;
let mldsa_seed: ml_dsa::Seed = [0x03u8; 32].into();
let mldsa_sk = SigningKey::<MlDsa65>::from_seed(&mldsa_seed);
// SPK message: "lo-spk-sig-v1" || [0xCC; 1216].
// The domain label prevents cross-context reuse of SPK signatures as
// session-init signatures (which use "lo-kex-init-sig-v1").
let mut message = Vec::new();
message.extend_from_slice(b"lo-spk-sig-v1");
message.extend_from_slice(&[0xCCu8; 1216]);
assert_eq!(message.len(), 13 + 1216);
// Ed25519: deterministic per RFC 8032.
let ed_sig = ed_signing_key.sign(&message);
let ed_sig_bytes = ed_sig.to_bytes(); // 64 bytes
// ML-DSA-65: rnd pinned to [0x00; 32] for test-vector reproducibility.
let rnd: B32 = [0x00u8; 32].into();
let mldsa_sig = mldsa_sk.sign_internal(&[message.as_ref()], &rnd);
let mldsa_sig_bytes: Vec<u8> = mldsa_sig.encode().to_vec();
assert_eq!(mldsa_sig_bytes.len(), 3309, "ML-DSA-65 signature size");
// Composite: Ed25519(64) || ML-DSA-65(3309) = 3373 bytes.
let mut composite = Vec::with_capacity(3373);
composite.extend_from_slice(&ed_sig_bytes);
composite.extend_from_slice(&mldsa_sig_bytes);
assert_eq!(composite.len(), 3373);
let hex = hex::encode(&composite);
println!("F.35 HybridSign(SPK) (3373 bytes):\n {}...", &hex[..64]);
assert_eq!(
hex, EXPECTED_F35,
"F.35 HybridSign over SPK message mismatch"
);
}
const EXPECTED_F35: &str = "2856bb008aa260e6b541ead779730ad350d97feb39db4829cb4ef5520979f3c3820bda50d51fec0e16ae1b7bb2cba8016ab389222c51b46af1fa223914ad8a01393759b7f59dd4439f2935d3b1e56bbbf1a67d3a2cdc6878bf08e5f365fa2c33b287fbc4b7583bbb7468efdda971eb94c1e5b5daa2558f1a6fd6edae1619220bc9f96777bc2124ed4020e6e43d140c454af790618f44b2b5e7740e487dedc10a42b98abf7743b7ab16152fb6599701be1b5ee8acb3252901f66f146d046c9af982f142c5b096dea56fc96575c326e3a3d3e8e759a8514e42d87ddc80dcd0c4875c06658a2c3532b6307d98843839f9b694dab1c55bc77b79260fdf77a886c9e49e14c050f027b6373182a3c334aa963a96807e43c23a4e62d5276784990290ff5cda45afccf70193dbd64ba683a0033a950a47176344c29a36f38c2480fc91545e29382ba89a32fa54487e1561e0256bc5a1371de480a13d48f1ca68e89b582b108cff6481edd82dacfbbaeca248165ccf628c5499531d3d9f1c6160b8efb4de25635366cca4f4fa274866dd950346a95641ea2f0343db9d657ee83576ea29c951842ef8f250a55ca7e2f4981b3ad8493e8b886277336a091d60d78f5c5e9d0f876eb936646b628c96f93ea30e6b86735b22e5dc84927aefae6234ab172e2e3c207875b65f2639b62585326e17afe6aa722d494b0df688c7de68fa187252abd5b3228b9c9761d7aa027d74d5b4183c462fbca53f2021d2d30c8fa5813ec0849814f05e3cbb7280982fc4d27478a97b455e40bb0e366582e329008838506f63efa0f772e016f3ba280d94b5a8ddf98588b830968031ec63b5a6f08be415a7ea44879004167ccac1be5b4959ffb13b8f61c1b16d3ebd35b56cac46b6e82966ed696b3d25d7ccf35fbe8922316335b1033f9aae80f881c63dfd84fb7456e76a9bddac2b9b08aa10ae74cab9f468d4d89f66e25052f0eba1acda0223c81e2fa5fbde49313cedf7519e8fcef2f85fbc2d307c88f507c346346609b1cdbc9ebb0c227994fc2b39af14dc5bfe08b1256c9a3e265c6b655305a921ff6bc551c085a0e15ec1746f3cde28c90b2d0e3674946b0d2c58e28a059b65d6815225695bfdcb8963e91e5763a3d72fc6ce00f97f71ff7be99cf19f42bef4cbb22a2c74cd52febe79e4d10e44c590a6a0eae5a8299f70cc7de38c623b3d9f628a9c780d0c782a66d71bb6d4354ee622511adf7553aab3962e622e8e73684bf53ef4c71c2fd713c51f972d9ce06c426f9eaf9db9695acd17a11ec57a1113660a293e8c5a5d21bc25ae97c940e5fb66baaf40fb35aa10fb28f6ab7fc523c849bdfa5745e6b9f1bd62f59884692769da238b09d7ebde878da6b5e2661aa995e48519fb0e280dfde557ac9a0a167e76afc2a7337b44511e3eca05cd09e005b2b4bd91f592271cf40a4784bd9c6e30411a70f8e7d9dc3475c16aa3caa6eb1e5af288e608829a60bb27acdc596541a6e88cabf23ef952217f7d5ac7d38a45ea660fe1b8672c92fd49b625074aac7a0c3d4559a1d902f54c1b85f6ff25219ff0c5b59bc09d6f964166b56e33449767c8e5ea1d3e0bbae641fa35999f94b5a2679e26d23475cac464a9723361458864f22ea09db6452e1ed3a776062b0e7ae36f57f76ef4d02f56ec83fdb43bc3cf1dfbb70fc11a94f63c1bcd82737e8ccb0f03c9f38f4453521489df6e2bd119edc6d8a780590f296a0de079d93a18515e06a9992d726320f7ba375962f92f671e60689be40c48b2714f9f51a1de76e5c104e852b7145445196a2bccd92d3c468340fc567107ce7b7f29b062cebeb9dc69c803dbb501819fa0e38f0d763d681dc57281d59dca755d0988b691ec22e1d6990124bb7b132cd6a4faa560ff17f2da58a6f79ddc8967676849e4d817d030742b29dab1ee551fa2adc5f57386e94dbc7eb34d3112bf61671470f50fe58c524b9669f41537c3241cbf8000e48089dcefec1c1914b804a00543754b555b97fd5c49e521f4d4ca23b5298ca62c6cfa8b2453abb96203de2f08d26319c725064d00fca131476ea7f448e7ad03a0d8000252c0d46709d4a9cb67336911c5133d151d09894e84c3e22704676d1ea031d8ed72901e0336eacae13de957a8200a165e9ac80e544f9c1890901a4a4da374c42259c134fa75b99ccffe3d4bb2f5810e730f87223f8c9beb9921b24c32b7cc5a650dcb4c9e31a0ff521031eb99cae31fdab851e5076bdfae671be7c4899ebadcbd6245640410c7bdaec8b7c8bb816f2c37f21347eb285fd5f0486e67110a779c3c463bb675c7436ca16ec4ed0a1f2a387034af50db1d8024fcbc9dd8c9aa9835c1117735628cf2f1f464767e15f2aa58cb3ead455f5c83c5f6a6ae2f7f22793054fcf5edb85cced4820dd2803368b0509f8712875ca8bfd5bffdc0d5ec916739af7abe963efca015706378dfe9a3e203ab371be7314c8343d6ca7f781d257a9c0154a454c46e0c9add97acf61956e064e0db7c049c538e8062f27389322a071dd40ea01af6bdfa5c5cb45911019fa01540ad44f719e49f4c8e8c24b9e03b90d5900ddb91a4959805e4829611bec1d378bb65cfa9f456ba27b424d1846b8b4b81d5299fc40aa3c8ef1526c5ed2d707ed6926acb79faa2144eb43971cfa5f2880ba4428d8909943d4684dab2a3979e6788e50e2e8cefcdaae4ca06a4e0ccd43f7a2e52a913809e5d44808519b8289ebfc26ffec03f297fcc5219435dad23098abf0ec6b562a7c86422a40c900123b80ebbf93e243a7c2c196b0ce73fec11ce653250513c01b579964034d9c66543ee01f03da01d3bc7a31fa7f941738bfab96c085d9728c43d787272a589aebc8462845bae76d22b063c246beee66f29c8874f401fc1ef697e4d71cfa984a1786de99102a50df2e02ef0acc06c4415686ea5627ac3a418ad86565023bb3a3852ae6478879e10231aafd3536acf78a3c37e14db200d9123258497fe58813ab5bf8d5a5a2cd9757049ca9b03edc83729c85154c9ac22359d154d8c88dc60f07801a83623841ab97ef1c139d8c2e776ce8294991d8ebe33c7ebed7d686799fbc341d95407e05482f9bb06cc20ced85fc29930cf4e206aea752dfaba9354aa86ea0f5ebfbeafc36b2b3b4da0ee76963d120e28970ae7f30e449ead63e01b0d413126e50081225f5ae662e08834ec25d59f8cdc3841e3d27f4fba3315d98a4acbadaf51e5bbb842ce65117a1d6e66be6d0ba2155b53a27dac6bcc8d1f4866ee8525daf85838d0c871e0cb18bbfa38b0a7dce6756910c5891efe61710c51d94dfbf49638470082fa27fcd8ab92dbd6cb839cc00c260d2ca974258567ccde4475d391f3da30330c8baed4b67b8697a7c347a92419ece1a07ef6b94589a68bcd2ebebf073b67cd6bec169d2acf16e2a4f83433eb310dd70f6a3ab756f6fa6f8136151e1f979dbac20ec1c09defcfba23147bf857bcfb067bd9a1fec0edc7f6042bbe1a5976b90e983a3042c3464e66583db0799cf81cf8eff8fe89a1c92f308085190b6e97a4f90a559aaa9b31fd588b4c756357f4d1636c3e950040270397d5bb73e3a5d5f95de29816ac2b2c983de18b54a5133c40ae50539ae1ab0229781dfb90e0aa5c613cda5c80067b466a7a521a8643715fa29dad8059ae6924e998920ab9e61a2655c35d27912a748af89f33da1945f83e0837111acc9df72e56add233896d00e49b8edc2b33024a05b2d88fce4108b9d789fde47d041f75e51baaaa1b1a153ac013939e7857f9c155ff8e3965b2e7ac63bd32a939bb0e47df449d90e021c9cbede9024e7f9976cc21f0d328c4f29974cf8256d4836dfef9de62c9a4a0d7519ab9ad27dd8b3efdebfee0ab948c2d17cee744f9f9e7ae64f5af308c94effbe644b893c2fdd75d47bcea8868fda88d303db15f0d2f3fbaf74a1508dc8a39926dd78d731d0be6d6e86bb2b8db2628a3d78a1248820277437c2b08a50947180d223467e24bc7a2eaec87df74b58dab8461b83546265d9e45982b691fd3d57b203892a6ab4d930a15f24262cfd0c68c1c331f6a75f6f0df0223fe08766d5bf67aa3f93fffdf1ebbf3edaf8a91ce83408c8de30a8b078f473026d8e309672d58edfd072b7916f11944b9f75b0710496f6003ebb406a4546b2c7f6a36d7b3de10ccd3cf602b67f2a33d31bdc513ef89ab9cb552144dd3d4c6e110fdf0aa408b2a93ce387607556605314e1a0841203b5c384b513715d25477b399a2ae352bf8c707e5485906c54f50e8f3499f4388710540584264efecb42c4691bbe4392e7f9b8b17d3b967962c741533a1da11d2ae3d9b3e1d2b5c5d04ef08ba15d55380191db248fb8e8d74ee2aad3c56228222968fe8ce99503f26bb7ec229a6eb254ddb548aeddfc6e7e6caa6abb27b6a749aa381605978fda7e574e37df42298fd35400a744edce1e6a8d8b56da6c828a00796e4ec9c21bdc61fa6192e9dab3927e089387e03cd1a1cb4e70f6ef53c56650caeac7e642a9c6dbe34759abc96caca8dcd969476009af31839ac7b739f6e62014f56439b0f73b250931ae35f802c885bc2d2660c4947d517186f6cf6358d10686b0485e312a7ff2f937292bac7d74ce2ffbc2774ce2ceaf5385034133833ad2a841ea2bc091fa8b32bb2ba7a81c091084921f29435706b4f5b1605143f4f5c95b6bc3d697898acb5e59ef5121d35676b85aa3d5694e9eaef63eb0000000000000000000000000000000000000000000000080f11181e20";
// ── F.36: HybridSign over encode_session_init (§5.4 Step 6) ──────────────────
//
// "lo-kex-init-sig-v1" || encode_session_init(si) is the message Alice signs
// to prove she initiated the session. This vector uses a synthetic SessionInit
// (no OPK) with the same fixed keys as F.34. A reimplementer who signs the
// SessionInit fields directly (instead of the encoded form) or who omits the
// domain label produces a signature that hybrid_verify will reject.
#[test]
fn f36_hybrid_sign_session_init() {
use ml_dsa::{B32, MlDsa65, SigningKey};
use soliton::kex::{SessionInit, encode_session_init};
use soliton::primitives::xwing;
// Synthetic SessionInit without OPK (same base fields as F.34).
let si = SessionInit {
crypto_version: "lo-crypto-v1".to_string(),
sender_ik_fingerprint: [0xAAu8; 32],
recipient_ik_fingerprint: [0xBBu8; 32],
sender_ek: xwing::PublicKey::from_bytes(vec![0xCCu8; 1216]).unwrap(),
ct_ik: xwing::Ciphertext::from_bytes(vec![0xDDu8; 1120]).unwrap(),
ct_spk: xwing::Ciphertext::from_bytes(vec![0xEEu8; 1120]).unwrap(),
spk_id: 42u32,
ct_opk: None,
opk_id: None,
};
let si_bytes = encode_session_init(&si).unwrap();
println!("F.36 encode_session_init length: {} bytes", si_bytes.len());
// Signed message: domain label || encoded session init.
let mut message = Vec::new();
message.extend_from_slice(b"lo-kex-init-sig-v1");
message.extend_from_slice(&si_bytes);
// Same synthetic identity key as F.27 and F.35.
let ed_seed: [u8; 32] = [0x02u8; 32];
let ed_signing_key = ed25519_dalek::SigningKey::from_bytes(&ed_seed);
use ed25519_dalek::Signer;
let ed_sig = ed_signing_key.sign(&message);
let ed_sig_bytes = ed_sig.to_bytes(); // 64 bytes
let mldsa_seed: ml_dsa::Seed = [0x03u8; 32].into();
let mldsa_sk = SigningKey::<MlDsa65>::from_seed(&mldsa_seed);
let rnd: B32 = [0x00u8; 32].into();
let mldsa_sig = mldsa_sk.sign_internal(&[message.as_ref()], &rnd);
let mldsa_sig_bytes: Vec<u8> = mldsa_sig.encode().to_vec();
assert_eq!(mldsa_sig_bytes.len(), 3309, "ML-DSA-65 signature size");
let mut composite = Vec::with_capacity(3373);
composite.extend_from_slice(&ed_sig_bytes);
composite.extend_from_slice(&mldsa_sig_bytes);
assert_eq!(composite.len(), 3373);
let hex = hex::encode(&composite);
println!(
"F.36 HybridSign(session-init) (3373 bytes):\n {}...",
&hex[..64]
);
assert_eq!(
hex, EXPECTED_F36,
"F.36 HybridSign over encode_session_init mismatch"
);
}
const EXPECTED_F36: &str = "c53f65e56414c595257a2e7233b91b5c52f2da83edc9c6245c63091dc83815c4c72fc53db16e5bd658826641c15e5dc33397e85b4447bff11213eb4273376c0352ce76735c536f7b4b70a268bfd7ed5cfa7f7fe0d05f9410b9821fc616bb38579fd8ba735acd29709f9aafcb787cfbdcb44399d29f8eab7ff3b8ed0886fa28aab56d0c7da575fb4e1fb5f8722fe6c1d47a2cc254f6616a62740eb60d3dfa44aee1062d7ec246f65b20fe180294da28efdb5b598c254affa49703f599d4460074e4d51b73d9a1634824b4f10a69a078d9f128b379b634d625ace4dd89e62e17f124d8a6537fd374bdfc99be1124175c7a9796c909c828e1ce97a3e1279b1635f1cdfd560166437af83f92598c25cf06b238e6e0b6a9697570a5a9dde2e1d18bb8abb6a60e09cbbb039eb772479498b0743cb93f28c4677ad7d69be7664d948c3c9d7b1ff269fb6e523a1716d35f83722e18491fce8e0e072c37d1f3429c0ef7a9f2aaa6215594a18a4a00d8c9896d503bc8fdad08f4a1981d5f07c16980771a517249ad47d6acbaa34acbac1891c3d2767630dba95da8b13b47a924660c795f4926d3f943ebfaeedaa7022a8a6559d15fa0cced0d2be58578cae4c5ec4afe14cdc2568e73200fd88b66046db02df6aa6797c6df5661d945a010342bf0d8226cd1050317ca965d7be29beb2788a33c6324eedb32e20ab29349de73419e0bf68005577a26f398562644a6bfac78ed8cdd5a0e46347891f07053d0914df352a5047bc624a24bda149e5fb1e3ca6bb2d921552eb86aa8302e864c43d69cdc4e97779671715729f12720ef297d0069bb5a67d7755677b0ea9f563f8991fe00d324d003daa70113c664f1ca60759dc81ab19c041eed6752b85462cfa7fecf73889b20f658ce7a849b03f41643f2179b7e1a8df179bbde46131467f375c3969229b7f0b461446ad6d4d8fc5fda068e622fac165c7f407506ac4acea833bf048ff755e3648f2f802c8e3b58a5d00fc89801709514b140bf55a9f73e161b50f25e32c93fa186868f35e7989e91a737ba25a00ff64aa2136887ca3dbb976fa738a3bf422229d1c2437c79b8933d297d8510ec3aca6ef389b649e0a99de83970abc0f2b360fa12b48a5ac82dadd4eeb4628ba4a620457d9da1aaa32d08339ce1fe779d12438f3dcfcd02bb3373d2f26ae13f615712334c5032bc2fde1923826dffe84d8b254f666341ce3730438dba7920528194293c918465459e9c8b4c44d44d97afe7983129c28bb0c6ba30cd1128780bce333fbda438c35e81689d864f1b7a9bf3a892ec244cd98a8bdd2bb65a558e99ddf122ce4066a62c27bb862e416fe3077600b57f8edffb93dc69b1875f6536f61a4c80db6e3c50c03ddd9b9734a747b1bcb2e7ff5dfbc56dafd8942f7a75fd8cad56125124ede7fb9140fe9faf0e3bc9f9bfe9d74aaa50a8359371db1a54d6ab75bdd18c5e32b84c788d3f7352b5d29d1dda7896988657897b85f872a2072b3b080c15978a3c08636b490b73299f8f6d26b698743d6723283dccf66515f7c8d2104acd355e3b458d6b88d79cffc553f1bdc8c58fce790e08e7f3f51e0e1b0b82b5066a82b1de2a06126cf239cdef892520a250adedce215cbb5a556b932316f65a02182db0e559c006c006073c9659e6367535f779d5dad9c8af09954fdd729a6984d15fe82aa2332c16592831e83047ba02c5abe169e7d3c7505452ff560faa6f9d5e9c6ed78c5ce77df05a45b8938778ca588dc1bc58c7ad15cfb8ff455e361d69ae2129a7fcaab7bd07d0203803bd134fba8111e2511848b71c906ec30d265d254a18f90d8c4dc5a05db72f62d907734bafcbebf60be2fe25bc28f1ed3689eb81608d950847ff165202c407ba11325899e3b9e41180bbe3701b4b46a980a605f1fd275e229516d1bc01fe7bf80b4d35bede0b8622aaaedced839b53c5abb971b66782d3ad655f85b0b7eb40531372f6142eaf5e70786bf22cab863de6982b248959ba6c46f6d6897ef9ac3636945e3c980002ac9b8934785376a7b576fca5d34d4a332355c3d55dade6602f585b8a9caa70497788d0b216966f79bd6c5432232d6aff2a741f39ee8f0ceffa93412d18ba227a66482d9267f3e4e48ff14a4ca565462b2b1580f7b71c1871bcaed36686953cf303b0588b2a51f41a3efb45a8c46b2a6b37ad107d4ca2a8042c65e2d0bc42308882f1aea159dd214fb9fd9d35249f8a2adbb3c7e4d748eb1ae73df7e61127335dfb18afbc9154b328bd0dbbbaa7236fd5c2b74d3a11a5c6c57525440b15d817d7ed9264bc94ef0b2b01c038f71639d19f6c5f037dcff98e356df68e9740b130c900dedf7aa4823a51c635959e9d253c8755ed345c06eb91158626395bd07cf88b9c5ada63bdf2e65ccf341276cd40fb426d2d2b8065f5fcf124c83a28f02ae2b65c9178e26941de2c332b2effeb77cc7a673e79fc696b1205694809a3c8beb5d488cb70e94a5a299502e998ec77b9e437de80afc718e94b7f9d07c3288c61dedd66cc87101da26127f25b00a2283f43ed6c598af9fde0cc06d4fed7b6291af144e1767a488a0d0f2f9c3684d372598133870ff31a66a367bc4bfd832f3cf645cbe08dcd265d4033e4b3f649188e1733357e6fbcb32234e4aa529e4edb1c066589bb237fccbcbb294120150365d24be4d94f7d87bd031a27876ddd2c7f7a24b88d90c391f5663bc1c14b4af45ea625b6929f6683f720ecb40931b0d884102d2bcffc8a7821dcdeaaec9d3a894313d390180b1160614db8224e25bdd3df82ee329acf398b0881c52e6eac1da229359389fc68749b1ae5f13f8c74234181ac92542a2df67a5ab88485945966f845e87629d1466e93da12c4996e5fc5d2e49584570864dbacc5c381e74e54235669b164cc18e62c1d5b78b68c2dd9ba639db4e371a53b8e095750d3bd81ab7ac1500bf70dac29926980d0002042c42c0478de2923b82df58e5b71b70dd5521d2dfc81a2eda5569bd72ff87ec29eac363479091e29804e43c304afc56183c7f107c04fe51544aa644ef837db79d2e0508a84377b8669df1445b4589736c239c11216dd3aee886d2ec8b80312c5e211cbbabf51bebc08f9f26475ed94f93f89dca33de029a38519c45031b79eb00806b4430fd149293aec257a943132fe02555b51d5847cf021a4922b4df29c4cce0c00fabf9fa7cfe07e31dc28af07c0873785a414bb5b7b88fcb5bf4cf8f40532dd344480a030863531ab15203b288856a2ded7e1eab9bfb964b9ede65bfc20307d42995505a240fccbd3ea5460bcda0c4376b928d01d3577d62d3036363a501d9f49300337b576d5776353dbc9f51d79c754a018f00748a1a2be515799b071a9dd8acd735e35e54dcd4cc2c6b7676145a3152b347af66528bad836eaef9da8e78877701a3022fb293ff86094cfad7eebdaa7a66b742eb92acac26a90248abc99c44a346dce8c17b7108aa2e1ca1db1f3a349b71517b9ac0c0ff575543044434f90fb0a165c2bbb187b8bf2e632232df83ca33146fe3f988fce47c28e0c04f450909e18d837846c2247563c9c28816048a28f0cf00bb6926d9cbfd93c13aeb6fe57ebbdb9bb4e4089713373d7bf73ace0c0dfb2ae0eae9c6e22cec69b62aabadf7e76bc98af9b92dae27b25cf87c8347cee30aa532e68fec0eeda9d563c444613d75daa0f2c13a5bbe64bc3713c290df6508a8218b0b90c3d846b68dbc609cfda9eb89ef014ae503f201209aa4aeef768c9cd0398017ac74016482f022cd07176d51147c7287fcec651de1b67a71bff6e75eac83453db00ff05ecc3db9d3c63e26e1ac6887aaf8818bc48ca3554b391f76b871fb4d0e9ca2fdb238ac99894f51be6fcb150e2bbb08cfc8a5b6614736aa11b0ff717d2640adb5d946cf4dc6e0d32f90f23a720454435042d1d42bf84110e58883e6d877ebfdc8b3bb79b88f09d0708d550f45edd96a36f6a9323a406372994731248dbbe385bc5c6d475b2248100a07c26d944809028a70cf78513698eac5801ce22c15883f286f8f150f460cf54b7070cf6dabfd2cd11fd21af5e60c0d52ba904288c6b156fbaf8e5919f94d376ee610876b27a80acf15b3fe83c9f30624d2d8a3c2e8d0667004bdd533cb06a0ecbfc134900910c7d80c9fa6da0af92ad4fde5d7bc35a998b161807e18b105edab05618eb76631ced7d5c43b2742f6fab046f46cd3b69abe600326dbaccf5adeb244f908bd092de4bac461e8a2facbf7831356a4936e55def2f16d3c59cb29bc7283c71ddbaaccfa488dbf4efb92f7c46f1aee3c166060d94b42652bcad6c40a860ac52511680a2cae51e892ab845211358e0526700cb7a0ebd048b8fdbb076da0a8c2fe4e74738cd3909fb4f49be84c6fea1b5b00296bca5cb112496cb26ddd65853175676e08d18c8cb8da98e8a4b79f7bfe1ed446903200df6040c5652be0188e20e40bc96d93ba4a653f5b9bd3afc4fc74b544346baa63a3e88a4ea33bce63e79e9bcc24b23adfa5f905ee1bbed995873432c1080b3398d7d2a5728727c0fa5b8c994030a90112491b09d01f809bdf4ba96067d5b41ad6bbb7236c03e9f66a4966f3c9cc22ba80b372b7207ebeceda178ac46c33641b8f7bf5b9065f71a8983fd7a527314f23a7b0e8bf6a8280c47a9f52e0716c446882e3d62b4c62d365fb5c8004d7f82b0b8c1e4e8f73c3f4b686f858f96a6f72498a7de131a468fc0e0000000000000000000000000000000050a141e2228";
// ── F.37: LO-Auth HMAC derivation (§4) ────────────────────────────────────────
//
// The LO-Auth proof token = HMAC-SHA3-256(shared_secret, "lo-auth-v1").
// This vector uses a synthetic shared secret to isolate the HMAC construction
// from the KEM. The KEM round-trip is covered by the xwing tests. The critical
// reimplementation risk here is using the wrong label or computing the HMAC in
// the wrong direction (key vs. data).
#[test]
fn f37_lo_auth_hmac() {
use soliton::primitives::hmac::hmac_sha3_256;
// Synthetic shared secret.
let ss = [0x08u8; 32];
// The static HMAC label from §4 / constants.rs.
let label = b"lo-auth-v1";
let token = hmac_sha3_256(&ss, label);
let hex = hex::encode(token);
println!("F.37 LO-Auth token = HMAC-SHA3-256(ss=[0x08]×32, \"lo-auth-v1\"):\n {hex}");
// Cross-check: auth_respond produces the same value if given matching sk/ct.
// That path is covered by auth module unit tests; here we isolate the HMAC step.
assert_eq!(
hex, EXPECTED_F37,
"F.37 LO-Auth HMAC mismatch — verify label is \"lo-auth-v1\" and key/data order"
);
}
const EXPECTED_F37: &str = "4e14e7ab92b70dd587a558e208cbcd98fd933048a2b2bf90e188e1d9b04f6e2a";