Skip to content

Commit

Permalink
Merge pull request #4 from nyonson/refactor/drop-sha2-dependency
Browse files Browse the repository at this point in the history
BIP324 HKDF implementation
  • Loading branch information
rustaceanrob authored Mar 4, 2024
2 parents 046cf93 + 27f0d37 commit 2c8adcd
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 47 deletions.
58 changes: 15 additions & 43 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ rust-version = "1.56.1"
[dependencies]
secp256k1 = { version="0.28.2" }
rand = "0.8.4"
hkdf = "0.12.4"
sha2 = "0.10.8"
bitcoin_hashes = "0.13.0"
chacha20poly1305 = "0.10.1"
chacha20 = "0.9.1"

Expand Down
145 changes: 145 additions & 0 deletions src/hkdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
//!
//! The interface is limited to the BIP324 use case for now. This
//! includes hardcoding to the SHA256 hash implementation, as well
//! as requiring an extract step.
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
use core::fmt;

// Hardcoded hash length for SHA256 backed implementation.
const HASH_LENGTH_BYTES: usize = sha256::Hash::LEN;
// Output keying material max length multiple.
const MAX_OUTPUT_BLOCKS: usize = 255;

#[derive(Copy, Clone, Debug)]
pub struct InvalidLength;

impl fmt::Display for InvalidLength {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "too large output")
}
}

impl std::error::Error for InvalidLength {}

/// HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
pub struct Hkdf {
/// Pseudorandom key based on the extract step.
prk: [u8; HASH_LENGTH_BYTES],
}

impl Hkdf {
/// Initialize a HKDF by performing the extract step.
pub fn extract(salt: &[u8], ikm: &[u8]) -> Self {
// Hardcoding SHA256 for now, might be worth parameterizing hash function.
let mut hmac_engine: HmacEngine<sha256::Hash> = HmacEngine::new(salt);
hmac_engine.input(ikm);
Self {
prk: Hmac::from_engine(hmac_engine)
.to_byte_array()
.try_into()
.expect("32 byte hash"),
}
}

/// Expand the key to generate output key material in okm.
pub fn expand(&self, info: &[u8], okm: &mut [u8]) -> Result<(), InvalidLength> {
// Length of output keying material must be less than 255 * hash length.
if okm.len() > (MAX_OUTPUT_BLOCKS * HASH_LENGTH_BYTES) {
return Err(InvalidLength);
}

// Counter starts at "1" based on RFC5869 spec and is committed to in the hash.
let mut counter = 1u8;
// Ceiling calculation for the total number of blocks (iterations) required for the expand.
let total_blocks = (okm.len() + HASH_LENGTH_BYTES - 1) / HASH_LENGTH_BYTES;

while counter <= total_blocks as u8 {
let mut hmac_engine: HmacEngine<sha256::Hash> = HmacEngine::new(&self.prk);

// First block does not have a previous block,
// all other blocks include last block in the HMAC input.
if counter != 1u8 {
let previous_start_index = (counter as usize - 2) * HASH_LENGTH_BYTES;
let previous_end_index = (counter as usize - 1) * HASH_LENGTH_BYTES;
hmac_engine.input(&okm[previous_start_index..previous_end_index]);
}
hmac_engine.input(info);
hmac_engine.input(&[counter]);

let t = Hmac::from_engine(hmac_engine);
let start_index = (counter as usize - 1) * HASH_LENGTH_BYTES;
// Last block might not take full hash length.
let end_index = if counter == (total_blocks as u8) {
okm.len()
} else {
counter as usize * HASH_LENGTH_BYTES
};

okm[start_index..end_index]
.copy_from_slice(&t.to_byte_array()[0..(end_index - start_index)]);

counter = counter + 1;
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use hex;

#[test]
fn test_rfc5869_basic() {
let salt = hex::decode("000102030405060708090a0b0c").unwrap();
let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
let info = hex::decode("f0f1f2f3f4f5f6f7f8f9").unwrap();

let hkdf = Hkdf::extract(&salt, &ikm);
let mut okm = [0u8; 42];
hkdf.expand(&info, &mut okm).unwrap();

assert_eq!(
hex::encode(okm),
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
);
}

#[test]
fn test_rfc5869_longer_inputs_outputs() {
let salt = hex::decode(
"606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
).unwrap();
let ikm = hex::decode(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f"
).unwrap();
let info = hex::decode(
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
).unwrap();

let hkdf = Hkdf::extract(&salt, &ikm);
let mut okm = [0u8; 82];
hkdf.expand(&info, &mut okm).unwrap();

assert_eq!(
hex::encode(okm),
"b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87"
);
}

#[test]
fn test_too_long_okm() {
let salt = hex::decode("000102030405060708090a0b0c").unwrap();
let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
let info = hex::decode("f0f1f2f3f4f5f6f7f8f9").unwrap();

let hkdf = Hkdf::extract(&salt, &ikm);
let mut okm = [0u8; 256 * 32];
let e = hkdf.expand(&info, &mut okm);

assert!(e.is_err());
}
}
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
//! ```
mod error;
mod hkdf;
mod types;

use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
use chacha20::ChaCha20;
use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, KeyInit, Nonce};
Expand All @@ -49,7 +51,6 @@ use secp256k1::{
ellswift::{ElligatorSwift, ElligatorSwiftParty},
PublicKey, Secp256k1, SecretKey,
};
use sha2::Sha256;
pub use types::SessionKeyMaterial;
pub use types::{
CompleteHandshake, EcdhPoint, HandshakeRole, InitiatorHandshake, ReceivedMessage,
Expand Down Expand Up @@ -373,7 +374,7 @@ fn initialize_session_key_material(ikm: &[u8]) -> SessionKeyMaterial {
let ikm_salt = "bitcoin_v2_shared_secret".as_bytes();
let magic = NETWORK_MAGIC.as_slice();
let salt = [ikm_salt, magic].concat();
let (_, hk) = Hkdf::<Sha256>::extract(Some(salt.as_slice()), ikm);
let hk = Hkdf::extract(salt.as_slice(), ikm);
let mut session_id = [0u8; 32];
let session_info = "session_id".as_bytes();
hk.expand(session_info, &mut session_id)
Expand Down

0 comments on commit 2c8adcd

Please sign in to comment.