Skip to content

Commit 99f24f5

Browse files
committed
Stop advertising the unimplementable AES-GCM suites
The AES-GCM symmetric suites were advertised but could never be used. bitcoin-hpke removed its AES-GCM schemes in 0.13.0, and `dispatch_hpkes_new!` only ever mapped ChaCha20Poly1305, so a peer that honoured the advertisement and selected AES-GCM got `Err(InvalidKeyType)`. `Config::supported()` returned true for it anyway, and the test constants listed it first, which is why 11 of this crate's own tests failed. Correcting `supported()` makes `strip_unsupported` prune the advertised KeyConfig automatically. The `Aead` enum keeps its GCM variants so other peers' configs still parse. `decode` built its probe config with `Aes128Gcm` on the grounds that "the KDF and AEAD doesn't matter here"; it does now, since the probe is checked against `supported()`, so it uses ChaCha20Poly1305. `decode` also rejects a config whose suite list is empty after pruning, with `Error::Unsupported`. Without that, a client handed a GCM-only config reached `ClientRequest::from_config`, which selects `symmetric[0]`, and panicked on the empty list where `main` returned `InvalidKeyType`. `decode_list` already skips `Unsupported` entries, so a list containing such a config decodes to the usable remainder. A test covers both paths. The example server no longer requests AES-128-GCM, which it would only have had stripped. Two tests were stale from before the secp256k1 port and never passed: `derive_key_pair`'s expected config encoded a 32-byte X25519 key under KEM 0x0020, regenerated here for KEM 0x0016 with a 65-byte key; and `truncate_kdf_aead_list` hard-coded an offset that assumed the X25519 key size, so it now derives the offset from the encoding. Also fixes two lints current clippy rejects under this crate's `deny(warnings, clippy::pedantic)`: a redundant `continue` and non-inlined format args.
1 parent ba508f8 commit 99f24f5

4 files changed

Lines changed: 59 additions & 32 deletions

File tree

ohttp-server/src/main.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,7 @@ async fn main() -> Res<()> {
108108
let config = KeyConfig::new(
109109
0,
110110
Kem::K256Sha256,
111-
vec![
112-
SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm),
113-
SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305),
114-
],
111+
vec![SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)],
115112
)?;
116113
let ohttp = OhttpServer::new(config)?;
117114
println!(

ohttp/src/config.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,9 @@ impl KeyConfig {
181181
let key_id = r.read_u8()?;
182182
let kem = Kem::try_from(r.read_u16::<NetworkEndian>()?)?;
183183

184-
// Note that the KDF and AEAD doesn't matter here.
185-
let kem_config = HpkeConfig::new(kem, Kdf::HkdfSha256, AeadId::Aes128Gcm);
184+
// Only the KEM is being validated here. The KDF and AEAD are filled with a
185+
// pair this backend implements so the check cannot fail on them.
186+
let kem_config = HpkeConfig::new(kem, Kdf::HkdfSha256, AeadId::ChaCha20Poly1305);
186187
if !kem_config.supported() {
187188
return Err(Error::Unsupported);
188189
}
@@ -210,6 +211,12 @@ impl KeyConfig {
210211
}
211212

212213
Self::strip_unsupported(&mut symmetric, kem);
214+
// A config whose every suite is one this backend cannot perform is unusable.
215+
// Report it like an unsupported KEM, so `decode_list` skips it and a client
216+
// never selects from an empty suite list.
217+
if symmetric.is_empty() {
218+
return Err(Error::Unsupported);
219+
}
213220
let pk = HpkeR::decode_public_key(kem_config.kem(), &pk_buf)?;
214221

215222
Ok(Self {
@@ -241,7 +248,7 @@ impl KeyConfig {
241248
r.consume(len);
242249
match res {
243250
Ok(config) => configs.push(config),
244-
Err(Error::Unsupported) => continue,
251+
Err(Error::Unsupported) => {}
245252
Err(e) => return Err(e),
246253
}
247254
}
@@ -278,10 +285,8 @@ mod test {
278285

279286
const KEY_ID: KeyId = 1;
280287
const KEM: Kem = Kem::K256Sha256;
281-
const SYMMETRIC: &[SymmetricSuite] = &[
282-
SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm),
283-
SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305),
284-
];
288+
const SYMMETRIC: &[SymmetricSuite] =
289+
&[SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
285290

286291
#[test]
287292
fn encode_decode_config_list() {
@@ -364,13 +369,41 @@ mod test {
364369
fn truncate_kdf_aead_list() {
365370
init();
366371

367-
let mut x25519 = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))
372+
let mut encoded = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))
368373
.unwrap()
369374
.encode()
370375
.unwrap();
371-
x25519.truncate(38);
372-
assert_eq!(usize::from(x25519[36]), SYMMETRIC.len() * 4);
373-
x25519[36] = 1;
374-
assert!(matches!(KeyConfig::decode(&x25519), Err(Error::Format)));
376+
// The suite list sits at the end, preceded by its u16 length; derive the offset
377+
// rather than hard-coding one, since the public key size depends on the KEM.
378+
let len_lo = encoded.len() - SYMMETRIC.len() * 4 - 1;
379+
assert_eq!(usize::from(encoded[len_lo]), SYMMETRIC.len() * 4);
380+
// A length that isn't a whole number of suites must be rejected.
381+
encoded[len_lo] = 1;
382+
assert!(matches!(KeyConfig::decode(&encoded), Err(Error::Format)));
383+
}
384+
385+
/// A config that offers only suites this backend cannot perform is unsupported,
386+
/// not a config with nothing in it.
387+
#[test]
388+
fn decode_rejects_config_with_no_supported_suite() {
389+
init();
390+
391+
let mut encoded = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))
392+
.unwrap()
393+
.encode()
394+
.unwrap();
395+
// The AEAD id is the last field of the last suite; swap it for AES-128-GCM.
396+
let aead_at = encoded.len() - 2;
397+
encoded[aead_at..].copy_from_slice(&u16::from(Aead::Aes128Gcm).to_be_bytes());
398+
assert!(matches!(
399+
KeyConfig::decode(&encoded),
400+
Err(Error::Unsupported)
401+
));
402+
403+
// In a list, such a config is skipped rather than failing the whole list.
404+
let mut list = Vec::new();
405+
list.extend_from_slice(&u16::try_from(encoded.len()).unwrap().to_be_bytes());
406+
list.extend_from_slice(&encoded);
407+
assert!(KeyConfig::decode_list(&list).unwrap().is_empty());
375408
}
376409
}

ohttp/src/lib.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,8 @@ mod test {
323323

324324
const KEY_ID: KeyId = 1;
325325
const KEM: Kem = Kem::K256Sha256;
326-
const SYMMETRIC: &[SymmetricSuite] = &[
327-
SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm),
328-
SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305),
329-
];
326+
const SYMMETRIC: &[SymmetricSuite] =
327+
&[SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
330328

331329
const REQUEST: &[u8] = &[
332330
0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x0b, 0x65, 0x78, 0x61,
@@ -473,11 +471,15 @@ mod test {
473471
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
474472
0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
475473
];
474+
// key_id 0x01, KEM 0x0016 (DHKEM secp256k1, HKDF-SHA256), a 65-byte uncompressed
475+
// public key, then a 4-byte suite list of HKDF-SHA256 + ChaCha20Poly1305.
476476
const EXPECTED_CONFIG: &[u8] = &[
477-
0x01, 0x00, 0x20, 0xfc, 0x01, 0x38, 0x93, 0x64, 0x10, 0x31, 0x1a, 0x0c, 0x64, 0x1a,
478-
0x5c, 0xa0, 0x86, 0x39, 0x1d, 0xe8, 0xe7, 0x03, 0x82, 0x33, 0x3f, 0x6d, 0x64, 0x49,
479-
0x25, 0x21, 0xad, 0x7d, 0xc7, 0x8a, 0x5d, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01, 0x00,
480-
0x01, 0x00, 0x03,
477+
0x01, 0x00, 0x16, 0x04, 0x43, 0xa4, 0xf2, 0x16, 0x79, 0xd7, 0x31, 0x3b, 0x32, 0xbf,
478+
0xc9, 0x8c, 0xb7, 0x75, 0xbd, 0xa9, 0xf8, 0x76, 0xb6, 0x0c, 0xe8, 0x92, 0xd1, 0xaf,
479+
0xc9, 0xf6, 0xcf, 0x74, 0xb8, 0x15, 0xd3, 0x42, 0x74, 0xe3, 0xce, 0x9d, 0x68, 0x24,
480+
0xa8, 0xc8, 0xa5, 0xf5, 0x45, 0x1c, 0x2a, 0x1c, 0xda, 0xda, 0x5d, 0x2d, 0x87, 0x48,
481+
0xd5, 0x40, 0xdd, 0xb6, 0xa8, 0x37, 0x70, 0xca, 0x47, 0x13, 0x62, 0x27, 0x00, 0x04,
482+
0x00, 0x01, 0x00, 0x03,
481483
];
482484

483485
init();

ohttp/src/rh/hpke.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ impl Config {
4242

4343
pub fn supported(self) -> bool {
4444
// TODO support more options
45-
self.kdf == Kdf::HkdfSha256 && matches!(self.aead, Aead::Aes128Gcm | Aead::ChaCha20Poly1305)
45+
self.kdf == Kdf::HkdfSha256 && self.aead == Aead::ChaCha20Poly1305
4646
}
4747
}
4848

@@ -401,7 +401,7 @@ pub fn generate_key_pair(kem: Kem) -> Res<(PrivateKey, PublicKey)> {
401401
(PrivateKey::K256(sk), PublicKey::K256(pk))
402402
}
403403
};
404-
trace!("Generated key pair: sk={:?} pk={:?}", sk, pk);
404+
trace!("Generated key pair: sk={sk:?} pk={pk:?}");
405405
Ok((sk, pk))
406406
}
407407

@@ -413,7 +413,7 @@ pub fn derive_key_pair(kem: Kem, ikm: &[u8]) -> Res<(PrivateKey, PublicKey)> {
413413
(PrivateKey::K256(sk), PublicKey::K256(pk))
414414
}
415415
};
416-
trace!("Derived key pair: sk={:?} pk={:?}", sk, pk);
416+
trace!("Derived key pair: sk={sk:?} pk={pk:?}");
417417
Ok((sk, pk))
418418
}
419419

@@ -462,11 +462,6 @@ mod test {
462462
assert_eq!(&pt[..], PT);
463463
}
464464

465-
#[test]
466-
fn seal_open_gcm() {
467-
seal_open(Aead::Aes128Gcm, Kem::K256Sha256);
468-
}
469-
470465
#[test]
471466
fn seal_open_chacha() {
472467
seal_open(Aead::ChaCha20Poly1305, Kem::K256Sha256);

0 commit comments

Comments
 (0)