Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ would imply a correspondence that no longer exists.

### Changes

* SHA-256/384/512 are now backed by rust-bitcoin's [`bitcoin_hashes`](https://crates.io/crates/bitcoin_hashes) instead of [`sha2`](https://crates.io/crates/sha2)
* SHA-256 is now backed by rust-bitcoin's [`bitcoin_hashes`](https://crates.io/crates/bitcoin_hashes) instead of [`sha2`](https://crates.io/crates/sha2)
* ChaCha20-Poly1305 is now backed by rust-bitcoin's [`chacha20-poly1305`](https://crates.io/crates/chacha20-poly1305) instead of [`chacha20poly1305`](https://crates.io/crates/chacha20poly1305)
* HKDF is now computed over `bitcoin_hashes`' HMAC instead of the [`hkdf`](https://crates.io/crates/hkdf) and [`hmac`](https://crates.io/crates/hmac) crates. `bitcoin_hashes` 0.14 ships HMAC but not HKDF, so RFC 5869 Extract and Expand are written out in `kdf.rs`
* Removed `HkdfSha384` and `HkdfSha512`. Nothing in the Bitcoin ecosystem uses them with HPKE; `HkdfSha256` is the only KDF left, the one rust-payjoin instantiates. `bitcoin_hashes` still ships both hashes, so restoring them is a `Kdf` impl each, and `MAX_DIGEST_SIZE` stays at the RFC 9180 bound of 64 so wider digests keep fitting the key-schedule buffers
* `Kdf::HashImpl` is now a `bitcoin_hashes::Hash` type, and `Kdf` gained an `OutputSize` associated type. The `digest` trait tower and the hidden `LabeledExpand` trait are gone
* Dropped the `sha2`, `chacha20poly1305`, `hkdf`, `hmac` and `digest` dependencies. Every cryptographic primitive now comes from rust-bitcoin; the remaining non-rust-bitcoin dependencies (`aead`, `generic-array`, `subtle`, `zeroize`, `rand_core`) are trait and utility crates
* Bumped MSRV from 1.63.0 to 1.85, matching [rust-payjoin](https://github.com/payjoin/rust-payjoin)

### Notes

* Wire compatibility is unchanged: the RFC 9180 known-answer tests pass unmodified. The in-crate HKDF is also checked against RFC 5869 Appendix A and the Wycheproof HKDF-SHA-256/384/512 suites.
* Wire compatibility for DHKEM(secp256k1, HKDF-SHA256) with HKDF-SHA256 is unchanged: the secp256k1 known-answer vectors in `test-vectors-k256.json` pass unmodified. The in-crate HKDF is also checked against RFC 5869 Appendix A, RFC 4231 and the Wycheproof HKDF-SHA-256 suite.
* Both new dependencies are CC0-1.0 licensed; the crates they replace were MIT/Apache-2.0.
* `chacha20-poly1305` does not zeroize per-operation key copies. Its key types are `Copy`
by design, following the rust-bitcoin position in
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ Here are all the primitives listed in the spec. The primitives with checked boxe
- [X] DHKEM(secp256k1, HKDF-SHA256)
* KDFs
- [X] HKDF-SHA256
- [X] HKDF-SHA384
- [X] HKDF-SHA512
* AEADs
- [X] ChaCha20Poly1305

Expand Down Expand Up @@ -73,7 +71,7 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes made throughout past vers
Tests
-----

To run all tests, execute `cargo test --all-features`. This includes known-answer tests, which test against `test-vector-COMMIT_ID.json`,where `COMMIT_ID` is the short commit of the version of the [spec](https://github.com/cfrg/draft-irtf-cfrg-hpke) that the test vectors came from. The finalized spec uses commit 5f503c5. See the [reference implementation](https://github.com/cisco/go-hpke) for information on how to generate a test vector. The HKDF implementation is also tested against the [Wycheproof](https://github.com/C2SP/wycheproof) HKDF vectors in `test-vectors-wycheproof-hkdf-*.json`, which are Apache-2.0 licensed.
To run all tests, execute `cargo test --all-features`. This includes known-answer tests, which test against `test-vector-COMMIT_ID.json`,where `COMMIT_ID` is the short commit of the version of the [spec](https://github.com/cfrg/draft-irtf-cfrg-hpke) that the test vectors came from. The finalized spec uses commit 5f503c5. See the [reference implementation](https://github.com/cisco/go-hpke) for information on how to generate a test vector. The HKDF implementation is also tested against the [Wycheproof](https://github.com/C2SP/wycheproof) HKDF vectors in `test-vectors-wycheproof-hkdf-sha256.json`, which are Apache-2.0 licensed.

Benchmarks
----------
Expand Down
4 changes: 2 additions & 2 deletions examples/client_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

use bitcoin_hpke::{
aead::{AeadTag, ChaCha20Poly1305},
kdf::HkdfSha384,
kdf::HkdfSha256,
kem::SecpK256HkdfSha256,
Deserializable, Kem as KemTrait, OpModeR, OpModeS, Serializable,
};
Expand All @@ -29,7 +29,7 @@ const INFO_STR: &[u8] = b"example session";
// These are the only algorithms we're gonna use for this example
type Kem = SecpK256HkdfSha256;
type Aead = ChaCha20Poly1305;
type Kdf = HkdfSha384;
type Kdf = HkdfSha256;

// Initializes the server with a fresh keypair
fn server_init() -> (<Kem as KemTrait>::PrivateKey, <Kem as KemTrait>::PublicKey) {
Expand Down
6 changes: 3 additions & 3 deletions src/kat_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
aead::{Aead, AesGcm128, AesGcm256, ChaCha20Poly1305, ExportOnlyAead},
kdf::{HkdfSha256, HkdfSha384, HkdfSha512, Kdf as KdfTrait},
kdf::{HkdfSha256, Kdf as KdfTrait},
kem::{self, Kem as KemTrait, SecpK256HkdfSha256, SharedSecret},
op_mode::{OpModeR, PskBundle},
setup::setup_receiver,
Expand Down Expand Up @@ -870,11 +870,11 @@ fn kat_test() {
continue;
}

// This unrolls into 36 `if let` statements
// This unrolls into 4 `if let` statements
dispatch_testcase!(
tv,
(AesGcm128, AesGcm256, ChaCha20Poly1305, ExportOnlyAead),
(HkdfSha256, HkdfSha384, HkdfSha512),
(HkdfSha256),
(SecpK256HkdfSha256)
);

Expand Down
114 changes: 43 additions & 71 deletions src/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

use crate::{util::write_u16_be, HpkeError};

use bitcoin_hashes::{sha256, sha384, sha512, Hash, HashEngine, Hmac, HmacEngine};
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
use generic_array::{
typenum::{Unsigned, U32, U48, U64},
typenum::{Unsigned, U32},
ArrayLength, GenericArray,
};
use zeroize::Zeroize;

const VERSION_LABEL: &[u8] = b"HPKE-v1";

// This is the maximum value of Nh. It is achieved by HKDF-SHA512 in RFC 9180 §7.2.
// This is the maximum value of Nh in RFC 9180 §7.2, achieved by HKDF-SHA512. It sizes the fixed
// key-schedule buffers in `setup`, and `Kdf` is a public trait, so it stays at the spec-wide bound
// rather than the 32 bytes of the one KDF implemented here. `assert_output_size` enforces it.
pub(crate) const MAX_DIGEST_SIZE: usize = 64;

/// Represents key derivation functionality
Expand Down Expand Up @@ -47,37 +49,13 @@ impl KdfTrait for HkdfSha256 {
const KDF_ID: u16 = 0x0001;
}

/// The implementation of HKDF-SHA384
pub struct HkdfSha384 {}

impl KdfTrait for HkdfSha384 {
#[doc(hidden)]
type HashImpl = sha384::Hash;
#[doc(hidden)]
type OutputSize = U48;

// RFC 9180 §7.2: HKDF-SHA384
const KDF_ID: u16 = 0x0002;
}

/// The implementation of HKDF-SHA512
pub struct HkdfSha512 {}

impl KdfTrait for HkdfSha512 {
#[doc(hidden)]
type HashImpl = sha512::Hash;
#[doc(hidden)]
type OutputSize = U64;

// RFC 9180 §7.2: HKDF-SHA512
const KDF_ID: u16 = 0x0003;
}

// `Kdf::OutputSize` and `HashImpl::LEN` name the same length but nothing ties them together at
// the type level, and the copies into `DigestArray` below would panic on a mismatch. Checking at
// monomorphization turns a wrong `Kdf` impl into a compile error instead.
// the type level, and the copies into `DigestArray` below would panic on a mismatch. A digest
// wider than `MAX_DIGEST_SIZE` would likewise overflow the key-schedule buffers in `setup`.
// Checking at monomorphization turns a wrong `Kdf` impl into a compile error instead.
const fn assert_output_size<Kdf: KdfTrait>() {
assert!(<Kdf::OutputSize as Unsigned>::USIZE == <Kdf::HashImpl as Hash>::LEN);
assert!(<Kdf::OutputSize as Unsigned>::USIZE <= MAX_DIGEST_SIZE);
}

// RFC 5869 §2.2
Expand Down Expand Up @@ -197,6 +175,19 @@ pub fn labeled_expand<Kdf: KdfTrait>(
mod tests {
use super::*;

use bitcoin_hashes::sha512;
use generic_array::typenum::U64;

// Test-only HKDF-SHA512. Not exported, since no Bitcoin HPKE deployment uses it, but it runs
// the generic Extract and Expand code at a second block and digest size.
struct HkdfSha512 {}

impl KdfTrait for HkdfSha512 {
type HashImpl = sha512::Hash;
type OutputSize = U64;
const KDF_ID: u16 = 0x0003;
}

use hex_literal::hex;

// RFC 5869 Appendix A, cases 1-3 (SHA-256). Case 2 needs three output blocks, case 3 has an
Expand Down Expand Up @@ -257,9 +248,10 @@ mod tests {
);
}

// HKDF-Extract is one HMAC, so RFC 4231 cases 2 and 6 pin it for SHA-384 and SHA-512 too,
// which RFC 5869 and the RFC 9180 known-answer tests never reach. The data is fed in two
// pieces, as `labeled_extract` does.
// HKDF-Extract is one HMAC, so RFC 4231 cases 2 and 6 pin it directly. The data is fed in
// two pieces, as `labeled_extract` does, which neither RFC 5869 nor Wycheproof exercise.
// `HkdfSha512` keeps the generic Extract and Expand code honest at a second block and
// digest size, since every shipped `Kdf` is 32 bytes wide.
#[test]
fn extract_is_hmac_rfc4231() {
fn check<Kdf: KdfTrait>(key: &[u8], data: &[u8], expected: &[u8]) {
Expand All @@ -275,14 +267,7 @@ mod tests {
data,
&hex!("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"),
);
check::<HkdfSha384>(
key,
data,
&hex!(
"af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47"
"e42ec3736322445e8e2240ca5e69e2c78b3239ecfab21649"
),
);

check::<HkdfSha512>(
key,
data,
Expand All @@ -292,22 +277,14 @@ mod tests {
),
);

// Key longer than every block size, so HMAC hashes it first
// RFC 4231 case 6: the key exceeds SHA-512's 128-byte block, so HMAC hashes it first
let key = [0xaa_u8; 131];
let data = b"Test Using Larger Than Block-Size Key - Hash Key First";
check::<HkdfSha256>(
&key,
data,
&hex!("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"),
);
check::<HkdfSha384>(
&key,
data,
&hex!(
"4ece084485813e9088d2c63a041bc5b44f9ef1012a2b588f"
"3cd11f05033ac4c60c2ef6ab4030fe8296248df163f44952"
),
);
check::<HkdfSha512>(
&key,
data,
Expand All @@ -320,18 +297,23 @@ mod tests {

#[test]
fn expand_rejects_more_than_255_blocks() {
let prk = [0u8; 32];
let mut okm = [0u8; 255 * 32 + 1];
assert!(hkdf_expand::<HkdfSha256>(&prk, &[b"info"], &mut okm[..255 * 32]).is_ok());
assert_eq!(
hkdf_expand::<HkdfSha256>(&prk, &[b"info"], &mut okm),
Err(HpkeError::KdfOutputTooLong)
);
fn check<Kdf: KdfTrait>() {
let hash_len = <Kdf::HashImpl as Hash>::LEN;
let prk = [0u8; MAX_DIGEST_SIZE];
let mut okm = [0u8; 255 * MAX_DIGEST_SIZE + 1];
let (prk, okm) = (&prk[..hash_len], &mut okm[..255 * hash_len + 1]);
assert!(hkdf_expand::<Kdf>(prk, &[b"info"], &mut okm[..255 * hash_len]).is_ok());
assert_eq!(
hkdf_expand::<Kdf>(prk, &[b"info"], okm),
Err(HpkeError::KdfOutputTooLong)
);
}
check::<HkdfSha256>();
check::<HkdfSha512>();
}

// Wycheproof's HKDF suites, from https://github.com/C2SP/wycheproof (testvectors_v1). They
// cover empty salts and infos, the 255-block maximum, and over-long requests for all three
// hash functions.
// Wycheproof's HKDF-SHA-256 suite, from https://github.com/C2SP/wycheproof (testvectors_v1).
// It covers empty salts and infos, the 255-block maximum, and over-long requests.
#[cfg(feature = "std")]
mod wycheproof {
use super::*;
Expand Down Expand Up @@ -396,15 +378,5 @@ mod tests {
fn hkdf_sha256() {
run::<HkdfSha256>("test-vectors-wycheproof-hkdf-sha256.json");
}

#[test]
fn hkdf_sha384() {
run::<HkdfSha384>("test-vectors-wycheproof-hkdf-sha384.json");
}

#[test]
fn hkdf_sha512() {
run::<HkdfSha512>("test-vectors-wycheproof-hkdf-sha512.json");
}
}
}
4 changes: 2 additions & 2 deletions src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pub(crate) fn dhkex_gen_keypair<Kex: DhKeyExchange, R: CryptoRng + RngCore>(
GenericArray::default();
// Fill it with randomness
csprng.fill_bytes(&mut ikm);
// Run derive_keypair with a nonsense ciphersuite. We use SHA-512 to satisfy any security level
Kex::derive_keypair::<crate::kdf::HkdfSha512>(b"31337", &ikm)
// Any suite id and KDF yield a valid keypair, so the choice here is arbitrary
Kex::derive_keypair::<crate::kdf::HkdfSha256>(b"31337", &ikm)
}

/// Creates a pair of `AeadCtx`s without doing a key exchange
Expand Down
Loading
Loading