Skip to content

Commit c5bd7b1

Browse files
Merge pull request #4 from danieldia-dev/main
Expand test coverage for auth KEM paths and context sequence number boundaries
2 parents 35a7e1c + 342c97d commit c5bd7b1

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

src/context.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ mod tests {
219219
assert_eq!(r, Err(HpkeError::MessageLimitReached));
220220
}
221221

222+
#[test]
223+
fn seal_succeeds_before_message_limit_then_fails() {
224+
let mut ctx: Ctx = Context::new(vec![0x42u8; 32], vec![0x77u8; 12], vec![0u8; 32]).unwrap();
225+
// Last valid sequence number --> seal must succeed.
226+
ctx.set_seq_for_test(u64::MAX - 1);
227+
assert!(ctx.seal(b"aad", b"hello").is_ok());
228+
// seq is now u64::MAX --> next seal must be rejected.
229+
assert_eq!(
230+
ctx.seal(b"aad", b"hello"),
231+
Err(HpkeError::MessageLimitReached)
232+
);
233+
}
234+
222235
#[test]
223236
fn open_rejects_at_message_limit() {
224237
let mut ctx: Ctx = Context::new(vec![0x42u8; 32], vec![0x77u8; 12], vec![0u8; 32]).unwrap();
@@ -227,6 +240,6 @@ mod tests {
227240
let ct = sibling.seal(b"aad", b"hello").unwrap();
228241
ctx.set_seq_for_test(u64::MAX);
229242
let r = ctx.open(b"aad", &ct);
230-
assert!(r.is_err());
243+
assert_eq!(r, Err(HpkeError::MessageLimitReached));
231244
}
232245
}

src/kem/dh.rs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ fn suite_id<D: DiffieHellman>() -> [u8; 5] {
184184
s
185185
}
186186

187+
#[inline]
187188
fn extract_and_expand<D: DiffieHellman, H: Kdf>(
188189
dh: &[u8],
189190
kem_context: &[&[u8]],
@@ -200,6 +201,7 @@ fn extract_and_expand<D: DiffieHellman, H: Kdf>(
200201
)
201202
}
202203

204+
#[inline]
203205
fn extract_and_expand_pieces<D: DiffieHellman, H: Kdf>(
204206
dh_pieces: &[&[u8]],
205207
kem_context: &[&[u8]],
@@ -429,14 +431,14 @@ impl<D: DiffieHellman, H: Kdf> Kem for DhKem<D, H> {
429431
enc: &Self::EncappedKey,
430432
sk_r: &Self::PrivateKey,
431433
) -> Result<Self::SharedSecret, HpkeError> {
432-
let pk_e = D::pk_from_bytes(&enc.0)?;
434+
let pk_e = D::pk_from_bytes(enc.as_ref())?;
433435
let dh = Zeroizing::new(D::dh(&sk_r.sk, &pk_e)?);
434436
// `sk_r.pk_bytes` was cached at construction time; using it here
435437
// saves the base-point scalar mult that `sk_to_pk` would otherwise
436438
// perform on every decap.
437439
Ok(DhSharedSecret(extract_and_expand::<D, H>(
438440
&dh,
439-
&[&enc.0, &sk_r.pk_bytes],
441+
&[enc.as_ref(), &sk_r.pk_bytes],
440442
)?))
441443
}
442444

@@ -474,28 +476,28 @@ impl<D: DiffieHellman, H: Kdf> AuthKem for DhKem<D, H> {
474476
sk_r: &Self::PrivateKey,
475477
pk_s: &Self::PublicKey,
476478
) -> Result<Self::SharedSecret, HpkeError> {
477-
let pk_e = D::pk_from_bytes(&enc.0)?;
479+
let pk_e = D::pk_from_bytes(enc.as_ref())?;
478480
let dh1 = Zeroizing::new(D::dh(&sk_r.sk, &pk_e)?);
479481
let dh2 = Zeroizing::new(D::dh(&sk_r.sk, &pk_s.0)?);
480482
let pk_sender = D::pk_to_bytes(&pk_s.0);
481483
Ok(DhSharedSecret(extract_and_expand_pieces::<D, H>(
482484
&[&dh1, &dh2],
483-
&[&enc.0, &sk_r.pk_bytes, &pk_sender],
485+
&[enc.as_ref(), &sk_r.pk_bytes, &pk_sender],
484486
)?))
485487
}
486488
}
487489

488490
fn encap_with<D: DiffieHellman, H: Kdf, R: CryptoRng + RngCore>(
489491
rng: &mut R,
490492
pk_r: &DhPublicKey<D>,
491-
sk_s_authed: Option<&DhPrivateKey<D>>,
493+
sk_sender: Option<&DhPrivateKey<D>>,
492494
) -> Result<(DhSharedSecret, DhEncappedKey), HpkeError> {
493495
let (sk_e, pk_e) = D::generate(rng);
494496
let dh1 = Zeroizing::new(D::dh(&sk_e, &pk_r.0)?);
495497
let enc = D::pk_to_bytes(&pk_e);
496498
let pk_recipient = D::pk_to_bytes(&pk_r.0);
497499

498-
let ss = match sk_s_authed {
500+
let ss = match sk_sender {
499501
None => extract_and_expand::<D, H>(&dh1, &[&enc, &pk_recipient])?,
500502
Some(sk_s) => {
501503
let dh2 = Zeroizing::new(D::dh(&sk_s.sk, &pk_r.0)?);
@@ -996,6 +998,48 @@ mod tests {
996998
);
997999
}
9981000

1001+
/// Verify that `extract_and_expand_pieces` produces the same output
1002+
/// as `extract_and_expand` when given two DH slices that together
1003+
/// equal the single concatenated input.
1004+
#[test]
1005+
fn extract_and_expand_pieces_matches_concat() {
1006+
let dh1 = [0x01u8; 32];
1007+
let dh2 = [0x02u8; 32];
1008+
let mut dh_concat = Vec::new();
1009+
dh_concat.extend_from_slice(&dh1);
1010+
dh_concat.extend_from_slice(&dh2);
1011+
1012+
let kem_context: &[&[u8]] = &[b"enc_bytes", b"pk_recipient"];
1013+
1014+
let single =
1015+
extract_and_expand::<X25519, crate::HkdfSha256>(&dh_concat, kem_context).unwrap();
1016+
1017+
let pieces =
1018+
extract_and_expand_pieces::<X25519, crate::HkdfSha256>(&[&dh1, &dh2], kem_context)
1019+
.unwrap();
1020+
1021+
assert_eq!(single, pieces);
1022+
}
1023+
1024+
// Unit test that specifically calls `auth_encap_with_ikm` and `auth_decap`
1025+
// directly (bypassing the full HPKE stack), so that failures point exactly
1026+
// at the KEM layer (rather than somewhere in the key schedule or AEAD on top of it).
1027+
#[test]
1028+
fn x25519_auth_encap_decap_roundtrip() {
1029+
type Suite = DhKem<X25519, crate::HkdfSha256>;
1030+
1031+
let (sk_r, pk_r) = Suite::derive_key_pair(b"auth-test-recipient-ikm").unwrap();
1032+
let (sk_s, pk_s) = Suite::derive_key_pair(b"auth-test-sender-ikm").unwrap();
1033+
1034+
let ikm_e = b"auth-test-ephemeral-ikm";
1035+
let (ss_enc, enc) =
1036+
auth_encap_with_ikm::<X25519, crate::HkdfSha256>(&pk_r, &sk_s, ikm_e).unwrap();
1037+
1038+
let ss_dec = Suite::auth_decap(&enc, &sk_r, &pk_s).unwrap();
1039+
1040+
assert_eq!(ss_enc.as_ref(), ss_dec.as_ref());
1041+
}
1042+
9991043
/// All public `DhKem` aliases satisfy both `Kem` and `AuthKem`. Compile-only.
10001044
#[test]
10011045
fn aliases_implement_kem_and_authkem() {

0 commit comments

Comments
 (0)