1 // Copyright 2024, The Android Open Source Project 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //! Key derivation function. 16 17 use bssl_crypto::digest; 18 use bssl_crypto::hkdf::{HkdfSha256, HkdfSha512, Prk, Salt}; 19 use mls_rs_core::crypto::CipherSuite; 20 use mls_rs_core::error::IntoAnyError; 21 use mls_rs_crypto_traits::{KdfId, KdfType}; 22 use thiserror::Error; 23 24 /// Errors returned from KDF. 25 #[derive(Debug, Error)] 26 pub enum KdfError { 27 /// Error returned when the input key material (IKM) is too short. 28 #[error("KDF IKM of length {len}, expected length at least {min_len}")] 29 TooShortIkm { 30 /// Invalid IKM length. 31 len: usize, 32 /// Minimum IKM length. 33 min_len: usize, 34 }, 35 /// Error returned when the pseudorandom key (PRK) is too short. 36 #[error("KDF PRK of length {len}, expected length at least {min_len}")] 37 TooShortPrk { 38 /// Invalid PRK length. 39 len: usize, 40 /// Minimum PRK length. 41 min_len: usize, 42 }, 43 /// Error returned when the output key material (OKM) requested it too long. 44 #[error("KDF OKM of length {len} requested, expected length at most {max_len}")] 45 TooLongOkm { 46 /// Invalid OKM length. 47 len: usize, 48 /// Maximum OKM length. 49 max_len: usize, 50 }, 51 /// Error returned when unsupported cipher suite is requested. 52 #[error("unsupported cipher suite")] 53 UnsupportedCipherSuite, 54 } 55 56 impl IntoAnyError for KdfError { into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self>57 fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> { 58 Ok(self.into()) 59 } 60 } 61 62 /// KdfType implementation backed by BoringSSL. 63 #[derive(Clone)] 64 pub struct Kdf(KdfId); 65 66 impl Kdf { 67 /// Creates a new Kdf. new(cipher_suite: CipherSuite) -> Option<Self>68 pub fn new(cipher_suite: CipherSuite) -> Option<Self> { 69 KdfId::new(cipher_suite).map(Self) 70 } 71 } 72 73 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] 74 #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))] 75 #[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)] 76 impl KdfType for Kdf { 77 type Error = KdfError; 78 extract(&self, salt: &[u8], ikm: &[u8]) -> Result<Vec<u8>, KdfError>79 async fn extract(&self, salt: &[u8], ikm: &[u8]) -> Result<Vec<u8>, KdfError> { 80 if ikm.is_empty() { 81 return Err(KdfError::TooShortIkm { len: 0, min_len: 1 }); 82 } 83 84 let salt = if salt.is_empty() { Salt::None } else { Salt::NonEmpty(salt) }; 85 86 match self.0 { 87 KdfId::HkdfSha256 => { 88 Ok(HkdfSha256::extract(ikm, salt).as_bytes()[..self.extract_size()].to_vec()) 89 } 90 KdfId::HkdfSha512 => { 91 Ok(HkdfSha512::extract(ikm, salt).as_bytes()[..self.extract_size()].to_vec()) 92 } 93 _ => Err(KdfError::UnsupportedCipherSuite), 94 } 95 } 96 expand(&self, prk: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>, KdfError>97 async fn expand(&self, prk: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>, KdfError> { 98 if prk.len() < self.extract_size() { 99 return Err(KdfError::TooShortPrk { len: prk.len(), min_len: self.extract_size() }); 100 } 101 102 match self.0 { 103 KdfId::HkdfSha256 => match Prk::new::<digest::Sha256>(prk) { 104 Some(hkdf) => { 105 let mut out = vec![0; len]; 106 match hkdf.expand_into(info, &mut out) { 107 Ok(_) => Ok(out), 108 Err(_) => { 109 Err(KdfError::TooLongOkm { len, max_len: HkdfSha256::MAX_OUTPUT_LEN }) 110 } 111 } 112 } 113 None => Err(KdfError::TooShortPrk { len: prk.len(), min_len: self.extract_size() }), 114 }, 115 KdfId::HkdfSha512 => match Prk::new::<digest::Sha512>(prk) { 116 Some(hkdf) => { 117 let mut out = vec![0; len]; 118 match hkdf.expand_into(info, &mut out) { 119 Ok(_) => Ok(out), 120 Err(_) => { 121 Err(KdfError::TooLongOkm { len, max_len: HkdfSha512::MAX_OUTPUT_LEN }) 122 } 123 } 124 } 125 None => Err(KdfError::TooShortPrk { len: prk.len(), min_len: self.extract_size() }), 126 }, 127 _ => Err(KdfError::UnsupportedCipherSuite), 128 } 129 } 130 extract_size(&self) -> usize131 fn extract_size(&self) -> usize { 132 self.0.extract_size() 133 } 134 kdf_id(&self) -> u16135 fn kdf_id(&self) -> u16 { 136 self.0 as u16 137 } 138 } 139 140 #[cfg(all(not(mls_build_async), test))] 141 mod test { 142 use super::{Kdf, KdfError, KdfType}; 143 use crate::test_helpers::decode_hex; 144 use assert_matches::assert_matches; 145 use bssl_crypto::hkdf::{HkdfSha256, HkdfSha512}; 146 use mls_rs_core::crypto::CipherSuite; 147 148 #[test] sha256()149 fn sha256() { 150 // https://www.rfc-editor.org/rfc/rfc5869.html#appendix-A.1 151 let salt: [u8; 13] = decode_hex("000102030405060708090a0b0c"); 152 let ikm: [u8; 22] = decode_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 153 let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9"); 154 let expected_prk: [u8; 32] = 155 decode_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"); 156 let expected_okm: [u8; 42] = decode_hex( 157 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865", 158 ); 159 160 let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap(); 161 let prk = kdf.extract(&salt, &ikm).unwrap(); 162 assert_eq!(prk, expected_prk); 163 assert_eq!(kdf.expand(&prk, &info, 42).unwrap(), expected_okm); 164 } 165 166 #[test] sha512()167 fn sha512() { 168 // https://github.com/C2SP/wycheproof/blob/cd27d6419bedd83cbd24611ec54b6d4bfdb0cdca/testvectors/hkdf_sha512_test.json#L141 169 let salt: [u8; 16] = decode_hex("1d6f3b38a1e607b5e6bcd4af1800a9d3"); 170 let ikm: [u8; 16] = decode_hex("5d3db20e8238a90b62a600fa57fdb318"); 171 let info: [u8; 20] = decode_hex("2bc5f39032b6fc87da69ba8711ce735b169646fd"); 172 let expected_okm: [u8; 42] = decode_hex( 173 "8c3cf7122dcb5eb7efaf02718f1faf70bca20dcb75070e9d0871a413a6c05fc195a75aa9ffc349d70aae", 174 ); 175 176 let kdf = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap(); 177 let prk = kdf.extract(&salt, &ikm).unwrap(); 178 assert_eq!(kdf.expand(&prk, &info, 42).unwrap(), expected_okm); 179 } 180 181 #[test] sha256_extract_short_ikm()182 fn sha256_extract_short_ikm() { 183 let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap(); 184 assert_matches!(kdf.extract(b"salty", b""), Err(KdfError::TooShortIkm { .. })); 185 } 186 187 #[test] sha256_expand_short_prk()188 fn sha256_expand_short_prk() { 189 let prk_short: [u8; 16] = decode_hex("077709362c2e32df0ddc3f0dc47bba63"); 190 let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9"); 191 192 let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap(); 193 assert_matches!(kdf.expand(&prk_short, &info, 42), Err(KdfError::TooShortPrk { .. })); 194 } 195 196 #[test] sha256_expand_long_okm()197 fn sha256_expand_long_okm() { 198 // https://www.rfc-editor.org/rfc/rfc5869.html#appendix-A.1 199 let prk: [u8; 32] = 200 decode_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"); 201 let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9"); 202 203 let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap(); 204 assert_matches!( 205 kdf.expand(&prk, &info, HkdfSha256::MAX_OUTPUT_LEN + 1), 206 Err(KdfError::TooLongOkm { .. }) 207 ); 208 } 209 210 #[test] sha512_extract_short_ikm()211 fn sha512_extract_short_ikm() { 212 let kdf = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap(); 213 assert_matches!(kdf.extract(b"salty", b""), Err(KdfError::TooShortIkm { .. })); 214 } 215 216 #[test] sha512_expand_short_prk()217 fn sha512_expand_short_prk() { 218 let prk_short: [u8; 16] = decode_hex("077709362c2e32df0ddc3f0dc47bba63"); 219 let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9"); 220 221 let kdf = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap(); 222 assert_matches!(kdf.expand(&prk_short, &info, 42), Err(KdfError::TooShortPrk { .. })); 223 } 224 225 #[test] sha512_expand_long_okm()226 fn sha512_expand_long_okm() { 227 // https://github.com/C2SP/wycheproof/blob/cd27d6419bedd83cbd24611ec54b6d4bfdb0cdca/testvectors/hkdf_sha512_test.json#L141 228 let salt: [u8; 16] = decode_hex("1d6f3b38a1e607b5e6bcd4af1800a9d3"); 229 let ikm: [u8; 16] = decode_hex("5d3db20e8238a90b62a600fa57fdb318"); 230 let info: [u8; 20] = decode_hex("2bc5f39032b6fc87da69ba8711ce735b169646fd"); 231 232 let kdf_sha512 = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap(); 233 let prk = kdf_sha512.extract(&salt, &ikm).unwrap(); 234 assert_matches!( 235 kdf_sha512.expand(&prk, &info, HkdfSha512::MAX_OUTPUT_LEN + 1), 236 Err(KdfError::TooLongOkm { .. }) 237 ); 238 } 239 240 #[test] unsupported_cipher_suites()241 fn unsupported_cipher_suites() { 242 let ikm: [u8; 22] = decode_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); 243 let salt: [u8; 13] = decode_hex("000102030405060708090a0b0c"); 244 245 assert_matches!( 246 Kdf::new(CipherSuite::P384_AES256).unwrap().extract(&salt, &ikm), 247 Err(KdfError::UnsupportedCipherSuite) 248 ); 249 } 250 } 251