Skip to content

Commit ba4014a

Browse files
authored
Add PSK bundle validation (#72)
* Made PskBundle require a constructor that now validates inputs * Updated CHANGELOG * Oops forgot to update KAT and example * Fixed typo
1 parent 1f06ba1 commit ba4014a

File tree

7 files changed

+52
-24
lines changed

7 files changed

+52
-24
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
9+
* Made `PskBundle` require an explicit constructor that performs validation on inputs
10+
711
## [0.12.0] - 2024-07-03
812

913
### Additions

examples/agility.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -702,10 +702,7 @@ fn main() {
702702
let psk_id = b"preshared key attempt #5, take 2. action";
703703
let psk_bundle = {
704704
csprng.fill_bytes(&mut psk_bytes);
705-
AgilePskBundle(PskBundle {
706-
psk: &psk_bytes,
707-
psk_id,
708-
})
705+
AgilePskBundle(PskBundle::new(&psk_bytes, psk_id).unwrap())
709706
};
710707

711708
// Make two agreeing OpModes (AuthPsk is the most complicated, so we're just using

src/kat_tests.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,7 @@ fn make_op_mode_r<'a, Kem: KemTrait>(
222222
psk_id: Option<&'a [u8]>,
223223
) -> OpModeR<'a, Kem> {
224224
// Deserialize the optional bundle
225-
let bundle = psk.map(|bytes| PskBundle {
226-
psk: bytes,
227-
psk_id: psk_id.unwrap(),
228-
});
225+
let bundle = psk.map(|bytes| PskBundle::new(bytes, psk_id.unwrap()).unwrap());
229226

230227
// These better be set if the mode ID calls for them
231228
match mode_id {

src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ pub enum HpkeError {
164164
/// An input isn't the right length. First value is the expected length, second is the given
165165
/// length.
166166
IncorrectInputLength(usize, usize),
167+
/// A preshared key bundle was constructed incorrectly
168+
InvalidPskBundle,
167169
}
168170

169171
impl core::fmt::Display for HpkeError {
@@ -181,6 +183,9 @@ impl core::fmt::Display for HpkeError {
181183
"Incorrect input length. Expected {} bytes. Got {}.",
182184
expected, given
183185
),
186+
HpkeError::InvalidPskBundle => {
187+
write!(f, "Preshared key bundle is missing a key or key ID")
188+
}
184189
}
185190
}
186191
}

src/op_mode.rs

+39-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1-
use crate::kem::Kem as KemTrait;
1+
use crate::{kem::Kem as KemTrait, HpkeError};
22

33
/// Contains preshared key bytes and an identifier. This is intended to go inside an `OpModeR` or
44
/// `OpModeS` struct.
5-
///
6-
/// Requirements
7-
/// ============
8-
/// `psk` MUST contain at least 32 bytes of entropy. Further, `psk.len()` SHOULD be at least as
9-
/// long as an extracted key from the KDF you use with `setup_sender`/`setup_receiver`, i.e., at
10-
/// least `Kdf::extracted_key_size()`.
115
#[derive(Clone, Copy)]
126
pub struct PskBundle<'a> {
137
/// The preshared key
14-
pub psk: &'a [u8],
8+
psk: &'a [u8],
159
/// A bytestring that uniquely identifies this PSK
16-
pub psk_id: &'a [u8],
10+
psk_id: &'a [u8],
11+
}
12+
13+
impl<'a> PskBundle<'a> {
14+
/// Creates a new preshared key bundle from the given preshared key and its ID
15+
///
16+
/// Errors
17+
/// ======
18+
/// `psk` and `psk_id` must either both be empty or both be nonempty. If one is empty while
19+
/// the other is not, then this returns [`HpkeError::InvalidPskBundle`].
20+
///
21+
/// Other requirements
22+
/// ==================
23+
/// Other requirements from the HPKE spec: `psk` MUST contain at least 32 bytes of entropy.
24+
/// Further, `psk.len()` SHOULD be at least as long as an extracted key from the KDF you use
25+
/// with `setup_sender`/`setup_receiver`, i.e., at least `Kdf::extracted_key_size()`.
26+
pub fn new(psk: &'a [u8], psk_id: &'a [u8]) -> Result<Self, HpkeError> {
27+
// RFC 9180 §5.1: The psk and psk_id fields MUST appear together or not at all
28+
if (psk.is_empty() && psk_id.is_empty()) || (!psk.is_empty() && !psk_id.is_empty()) {
29+
Ok(PskBundle { psk, psk_id })
30+
} else {
31+
Err(HpkeError::InvalidPskBundle)
32+
}
33+
}
1734
}
1835

1936
/// The operation mode of the HPKE session (receiver's view). This is how the sender authenticates
@@ -23,7 +40,8 @@ pub struct PskBundle<'a> {
2340
pub enum OpModeR<'a, Kem: KemTrait> {
2441
/// No extra information included
2542
Base,
26-
/// A preshared key known to the sender and receiver
43+
/// A preshared key known to the sender and receiver. If the bundle contents is empty strings,
44+
/// then this is equivalent to `Base`.
2745
Psk(PskBundle<'a>),
2846
/// The identity public key of the sender
2947
Auth(Kem::PublicKey),
@@ -50,7 +68,8 @@ impl<Kem: KemTrait> OpModeR<'_, Kem> {
5068
pub enum OpModeS<'a, Kem: KemTrait> {
5169
/// No extra information included
5270
Base,
53-
/// A preshared key known to the sender and receiver
71+
/// A preshared key known to the sender and receiver. If the bundle contents is empty strings,
72+
/// then this is equivalent to `Base`.
5473
Psk(PskBundle<'a>),
5574
/// The identity keypair of the sender
5675
Auth((Kem::PrivateKey, Kem::PublicKey)),
@@ -148,3 +167,12 @@ impl<Kem: KemTrait> OpMode<Kem> for OpModeS<'_, Kem> {
148167
}
149168
}
150169
}
170+
171+
// Test that you can only make a PskBundle if both fields are empty or both fields are nonempty
172+
#[test]
173+
fn psk_bundle_validation() {
174+
assert!(PskBundle::new(b"hello", b"world").is_ok());
175+
assert!(PskBundle::new(b"", b"").is_ok());
176+
assert!(PskBundle::new(b"hello", b"").is_err());
177+
assert!(PskBundle::new(b"", b"world").is_err());
178+
}

src/single_shot.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,7 @@ mod test {
177177
// Set up an arbitrary info string, a random PSK, and an arbitrary PSK ID
178178
let info = b"why would you think in a million years that that would actually work";
179179
let (psk, psk_id) = (gen_rand_buf(), gen_rand_buf());
180-
let psk_bundle = PskBundle {
181-
psk: &psk,
182-
psk_id: &psk_id,
183-
};
180+
let psk_bundle = PskBundle::new(&psk, &psk_id).unwrap();
184181

185182
// Generate the sender's and receiver's long-term keypairs
186183
let (sk_sender_id, pk_sender_id) = Kem::gen_keypair(&mut csprng);

src/test_util.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub(crate) fn new_op_mode_pair<'a, Kdf: KdfTrait, Kem: KemTrait>(
8080
) -> (OpModeS<'a, Kem>, OpModeR<'a, Kem>) {
8181
let mut csprng = StdRng::from_os_rng();
8282
let (sk_sender, pk_sender) = Kem::gen_keypair(&mut csprng);
83-
let psk_bundle = PskBundle { psk, psk_id };
83+
let psk_bundle = PskBundle::new(psk, psk_id).unwrap();
8484

8585
match kind {
8686
OpModeKind::Base => {

0 commit comments

Comments
 (0)