xref: /aosp_15_r20/system/security/mls/mls-rs-crypto-boringssl/src/kdf.rs (revision e1997b9af69e3155ead6e072d106a0077849ffba)
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