libsoliton 0.1.3

Core cryptographic library for the LO protocol — hybrid post-quantum key exchange, signatures, ratchet, and storage encryption
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
//! 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";