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
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
517 lines
40 KiB
Rust
517 lines
40 KiB
Rust
//! 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";
|