Cindy Lin | 6ec3c2b | 2024-05-16 07:39:23 +0000 | [diff] [blame^] | 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 { |
| 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. |
| 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 | |
| 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 | |
| 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 | |
| 131 | fn extract_size(&self) -> usize { |
| 132 | self.0.extract_size() |
| 133 | } |
| 134 | |
| 135 | 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] |
| 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] |
| 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] |
| 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] |
| 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] |
| 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] |
| 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] |
| 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] |
| 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] |
| 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 | } |