blob: 6b88d3744a3b73d8923a2756c1ced7eaf1b2660d [file] [log] [blame]
Cindy Lin6ec3c2b2024-05-16 07:39:23 +00001// 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
17use bssl_crypto::digest;
18use bssl_crypto::hkdf::{HkdfSha256, HkdfSha512, Prk, Salt};
19use mls_rs_core::crypto::CipherSuite;
20use mls_rs_core::error::IntoAnyError;
21use mls_rs_crypto_traits::{KdfId, KdfType};
22use thiserror::Error;
23
24/// Errors returned from KDF.
25#[derive(Debug, Error)]
26pub 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
56impl 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)]
64pub struct Kdf(KdfId);
65
66impl 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)]
76impl 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))]
141mod 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}