web engine - experimental web browser

Implement HKDF: HMAC-based key derivation function (RFC 5869)

Add pure Rust HKDF implementation in the crypto crate, generic over
hash function via the existing HashFunction trait:

- hkdf_extract(salt, IKM) -> PRK
- hkdf_expand(PRK, info, L) -> OKM with length validation
- hkdf() combined convenience function
- HKDF-SHA-256, HKDF-SHA-384, HKDF-SHA-512 via generics
- Empty salt defaults to HashLen zeros per RFC 5869 §2.2
- Output length validated: L <= 255 * HashLen
- 14 tests including all RFC 5869 SHA-256 test vectors (cases 1-3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

authored by pierrelf.com

Claude Opus 4.6 and committed by tangled.org eaec46bf 10288a77

+296
+295
crates/crypto/src/hkdf.rs
··· 1 + //! HKDF: HMAC-based Extract-and-Expand Key Derivation Function (RFC 5869). 2 + 3 + use crate::hmac::{HashFunction, Hmac}; 4 + 5 + /// HKDF-Extract: derive a pseudorandom key (PRK) from input keying material. 6 + /// 7 + /// `salt` is an optional non-secret random value; if empty, a string of 8 + /// `H::OUTPUT_SIZE` zeros is used (per RFC 5869 §2.2). 9 + pub fn hkdf_extract<H: HashFunction>(salt: &[u8], ikm: &[u8]) -> Vec<u8> { 10 + let effective_salt: Vec<u8>; 11 + let salt = if salt.is_empty() { 12 + effective_salt = vec![0u8; H::OUTPUT_SIZE]; 13 + &effective_salt 14 + } else { 15 + salt 16 + }; 17 + let mut hmac = Hmac::<H>::new(salt); 18 + hmac.update(ikm); 19 + hmac.finalize() 20 + } 21 + 22 + /// HKDF-Expand: expand a PRK into output keying material of length `len`. 23 + /// 24 + /// `len` must be <= 255 * `H::OUTPUT_SIZE`. 25 + /// Returns `None` if `len` exceeds the maximum. 26 + pub fn hkdf_expand<H: HashFunction>(prk: &[u8], info: &[u8], len: usize) -> Option<Vec<u8>> { 27 + let hash_len = H::OUTPUT_SIZE; 28 + if len > 255 * hash_len { 29 + return None; 30 + } 31 + 32 + let n = len.div_ceil(hash_len); 33 + let mut okm = Vec::with_capacity(n * hash_len); 34 + let mut t_prev: Vec<u8> = Vec::new(); 35 + 36 + for i in 1..=n { 37 + let mut hmac = Hmac::<H>::new(prk); 38 + hmac.update(&t_prev); 39 + hmac.update(info); 40 + hmac.update(&[i as u8]); 41 + t_prev = hmac.finalize(); 42 + okm.extend_from_slice(&t_prev); 43 + } 44 + 45 + okm.truncate(len); 46 + Some(okm) 47 + } 48 + 49 + /// Combined HKDF: extract then expand in one call. 50 + /// 51 + /// Returns `None` if `len` exceeds 255 * `H::OUTPUT_SIZE`. 52 + pub fn hkdf<H: HashFunction>(salt: &[u8], ikm: &[u8], info: &[u8], len: usize) -> Option<Vec<u8>> { 53 + let prk = hkdf_extract::<H>(salt, ikm); 54 + hkdf_expand::<H>(&prk, info, len) 55 + } 56 + 57 + // --------------------------------------------------------------------------- 58 + // Tests — RFC 5869 test vectors 59 + // --------------------------------------------------------------------------- 60 + 61 + #[cfg(test)] 62 + mod tests { 63 + use super::*; 64 + use crate::sha2::Sha256; 65 + 66 + fn hex(bytes: &[u8]) -> String { 67 + bytes.iter().map(|b| format!("{b:02x}")).collect() 68 + } 69 + 70 + fn from_hex(s: &str) -> Vec<u8> { 71 + (0..s.len()) 72 + .step_by(2) 73 + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) 74 + .collect() 75 + } 76 + 77 + // ----------------------------------------------------------------------- 78 + // Test Case 1: Basic test case with SHA-256 79 + // ----------------------------------------------------------------------- 80 + 81 + #[test] 82 + fn rfc5869_case1_extract() { 83 + let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 84 + let salt = from_hex("000102030405060708090a0b0c"); 85 + let prk = hkdf_extract::<Sha256>(&salt, &ikm); 86 + assert_eq!( 87 + hex(&prk), 88 + "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5" 89 + ); 90 + } 91 + 92 + #[test] 93 + fn rfc5869_case1_expand() { 94 + let prk = from_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"); 95 + let info = from_hex("f0f1f2f3f4f5f6f7f8f9"); 96 + let okm = hkdf_expand::<Sha256>(&prk, &info, 42).unwrap(); 97 + assert_eq!( 98 + hex(&okm), 99 + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" 100 + ); 101 + } 102 + 103 + #[test] 104 + fn rfc5869_case1_combined() { 105 + let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 106 + let salt = from_hex("000102030405060708090a0b0c"); 107 + let info = from_hex("f0f1f2f3f4f5f6f7f8f9"); 108 + let okm = hkdf::<Sha256>(&salt, &ikm, &info, 42).unwrap(); 109 + assert_eq!( 110 + hex(&okm), 111 + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" 112 + ); 113 + } 114 + 115 + // ----------------------------------------------------------------------- 116 + // Test Case 2: Longer inputs/outputs with SHA-256 117 + // ----------------------------------------------------------------------- 118 + 119 + #[test] 120 + fn rfc5869_case2_extract() { 121 + let ikm = from_hex( 122 + "000102030405060708090a0b0c0d0e0f\ 123 + 101112131415161718191a1b1c1d1e1f\ 124 + 202122232425262728292a2b2c2d2e2f\ 125 + 303132333435363738393a3b3c3d3e3f\ 126 + 404142434445464748494a4b4c4d4e4f", 127 + ); 128 + let salt = from_hex( 129 + "606162636465666768696a6b6c6d6e6f\ 130 + 707172737475767778797a7b7c7d7e7f\ 131 + 808182838485868788898a8b8c8d8e8f\ 132 + 909192939495969798999a9b9c9d9e9f\ 133 + a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", 134 + ); 135 + let prk = hkdf_extract::<Sha256>(&salt, &ikm); 136 + assert_eq!( 137 + hex(&prk), 138 + "06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244" 139 + ); 140 + } 141 + 142 + #[test] 143 + fn rfc5869_case2_expand() { 144 + let prk = from_hex("06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244"); 145 + let info = from_hex( 146 + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\ 147 + c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\ 148 + d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\ 149 + e0e1e2e3e4e5e6e7e8e9eaebecedeeef\ 150 + f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", 151 + ); 152 + let okm = hkdf_expand::<Sha256>(&prk, &info, 82).unwrap(); 153 + assert_eq!( 154 + hex(&okm), 155 + "b11e398dc80327a1c8e7f78c596a4934\ 156 + 4f012eda2d4efad8a050cc4c19afa97c\ 157 + 59045a99cac7827271cb41c65e590e09\ 158 + da3275600c2f09b8367793a9aca3db71\ 159 + cc30c58179ec3e87c14c01d5c1f3434f\ 160 + 1d87" 161 + ); 162 + } 163 + 164 + #[test] 165 + fn rfc5869_case2_combined() { 166 + let ikm = from_hex( 167 + "000102030405060708090a0b0c0d0e0f\ 168 + 101112131415161718191a1b1c1d1e1f\ 169 + 202122232425262728292a2b2c2d2e2f\ 170 + 303132333435363738393a3b3c3d3e3f\ 171 + 404142434445464748494a4b4c4d4e4f", 172 + ); 173 + let salt = from_hex( 174 + "606162636465666768696a6b6c6d6e6f\ 175 + 707172737475767778797a7b7c7d7e7f\ 176 + 808182838485868788898a8b8c8d8e8f\ 177 + 909192939495969798999a9b9c9d9e9f\ 178 + a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", 179 + ); 180 + let info = from_hex( 181 + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\ 182 + c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\ 183 + d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\ 184 + e0e1e2e3e4e5e6e7e8e9eaebecedeeef\ 185 + f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", 186 + ); 187 + let okm = hkdf::<Sha256>(&salt, &ikm, &info, 82).unwrap(); 188 + assert_eq!( 189 + hex(&okm), 190 + "b11e398dc80327a1c8e7f78c596a4934\ 191 + 4f012eda2d4efad8a050cc4c19afa97c\ 192 + 59045a99cac7827271cb41c65e590e09\ 193 + da3275600c2f09b8367793a9aca3db71\ 194 + cc30c58179ec3e87c14c01d5c1f3434f\ 195 + 1d87" 196 + ); 197 + } 198 + 199 + // ----------------------------------------------------------------------- 200 + // Test Case 3: SHA-256, zero-length salt and info 201 + // ----------------------------------------------------------------------- 202 + 203 + #[test] 204 + fn rfc5869_case3_extract() { 205 + let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 206 + let prk = hkdf_extract::<Sha256>(&[], &ikm); 207 + assert_eq!( 208 + hex(&prk), 209 + "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04" 210 + ); 211 + } 212 + 213 + #[test] 214 + fn rfc5869_case3_expand() { 215 + let prk = from_hex("19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04"); 216 + let okm = hkdf_expand::<Sha256>(&prk, &[], 42).unwrap(); 217 + assert_eq!( 218 + hex(&okm), 219 + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8" 220 + ); 221 + } 222 + 223 + #[test] 224 + fn rfc5869_case3_combined() { 225 + let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 226 + let okm = hkdf::<Sha256>(&[], &ikm, &[], 42).unwrap(); 227 + assert_eq!( 228 + hex(&okm), 229 + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8" 230 + ); 231 + } 232 + 233 + // ----------------------------------------------------------------------- 234 + // Output length validation 235 + // ----------------------------------------------------------------------- 236 + 237 + #[test] 238 + fn expand_rejects_oversized_output() { 239 + let prk = [0x42u8; 32]; 240 + // 255 * 32 = 8160 is the max for SHA-256 241 + assert!(hkdf_expand::<Sha256>(&prk, &[], 8160).is_some()); 242 + assert!(hkdf_expand::<Sha256>(&prk, &[], 8161).is_none()); 243 + } 244 + 245 + #[test] 246 + fn hkdf_rejects_oversized_output() { 247 + let ikm = [0x0bu8; 22]; 248 + assert!(hkdf::<Sha256>(&[], &ikm, &[], 8161).is_none()); 249 + } 250 + 251 + // ----------------------------------------------------------------------- 252 + // Edge cases 253 + // ----------------------------------------------------------------------- 254 + 255 + #[test] 256 + fn expand_zero_length_output() { 257 + let prk = [0x42u8; 32]; 258 + let okm = hkdf_expand::<Sha256>(&prk, &[], 0).unwrap(); 259 + assert!(okm.is_empty()); 260 + } 261 + 262 + #[test] 263 + fn expand_exact_hash_length() { 264 + let prk = [0x42u8; 32]; 265 + let okm = hkdf_expand::<Sha256>(&prk, b"info", 32).unwrap(); 266 + assert_eq!(okm.len(), 32); 267 + } 268 + 269 + #[test] 270 + fn extract_expand_with_sha512() { 271 + use crate::sha2::Sha512; 272 + 273 + // Use Test Case 1 inputs but with SHA-512 — verify it produces 274 + // the correct output length and doesn't panic. 275 + let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 276 + let salt = from_hex("000102030405060708090a0b0c"); 277 + let prk = hkdf_extract::<Sha512>(&salt, &ikm); 278 + assert_eq!(prk.len(), 64); 279 + 280 + let okm = hkdf_expand::<Sha512>(&prk, b"test info", 100).unwrap(); 281 + assert_eq!(okm.len(), 100); 282 + } 283 + 284 + #[test] 285 + fn extract_expand_with_sha384() { 286 + use crate::sha2::Sha384; 287 + 288 + let ikm = [0xaau8; 80]; 289 + let prk = hkdf_extract::<Sha384>(&[], &ikm); 290 + assert_eq!(prk.len(), 48); 291 + 292 + let okm = hkdf_expand::<Sha384>(&prk, b"context", 60).unwrap(); 293 + assert_eq!(okm.len(), 60); 294 + } 295 + }
+1
crates/crypto/src/lib.rs
··· 1 1 //! Pure Rust cryptography — AES-GCM, ChaCha20-Poly1305, SHA-2, X25519, RSA, X.509, ASN.1. 2 2 3 + pub mod hkdf; 3 4 pub mod hmac; 4 5 pub mod sha2;