/src/connectedhomeip/src/crypto/tests/FuzzCryptoPrimitivesPW.cpp
Line | Count | Source |
1 | | /* |
2 | | * |
3 | | * Copyright (c) 2026 Project CHIP Authors |
4 | | * All rights reserved. |
5 | | * |
6 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
7 | | * you may not use this file except in compliance with the License. |
8 | | * You may obtain a copy of the License at |
9 | | * |
10 | | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | | * |
12 | | * Unless required by applicable law or agreed to in writing, software |
13 | | * distributed under the License is distributed on an "AS IS" BASIS, |
14 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15 | | * See the License for the specific language governing permissions and |
16 | | * limitations under the License. |
17 | | */ |
18 | | |
19 | | /** |
20 | | * @file |
21 | | * FuzzTest harness for the AEAD and KDF primitives that sit underneath |
22 | | * every encrypted Matter session: CASE and PASE derive keys with |
23 | | * HKDF-SHA256, and every secured message is AES-CCM. |
24 | | * |
25 | | * Each parameter has its own typed domain, seeded from this directory's |
26 | | * AES-CCM and HKDF test vectors, so the checks are functional invariants |
27 | | * rather than only "does not crash": |
28 | | * |
29 | | * - AES-CCM round-trip: decrypt(encrypt(pt)) recovers pt. |
30 | | * - AES-CCM authenticity: a tampered tag or ciphertext must not decrypt. |
31 | | * - HKDF determinism: identical inputs produce identical output. |
32 | | * |
33 | | * Seeds are attached to the individual domains rather than to the |
34 | | * FUZZ_TEST registration, because only domain-level seeds are injected |
35 | | * when the suite runs under the libFuzzer compatibility engine. |
36 | | */ |
37 | | |
38 | | #include <array> |
39 | | #include <cstdint> |
40 | | #include <cstring> |
41 | | #include <iterator> |
42 | | #include <vector> |
43 | | |
44 | | #include <fuzztest/fuzztest_core.h> |
45 | | #include <gtest/gtest.h> |
46 | | |
47 | | #include <crypto/CHIPCryptoPAL.h> |
48 | | #include <crypto/DefaultSessionKeystore.h> |
49 | | #include <lib/support/CHIPMem.h> |
50 | | #include <lib/support/CodeUtils.h> |
51 | | |
52 | | #if CHIP_CRYPTO_PSA |
53 | | #include <psa/crypto.h> |
54 | | #endif |
55 | | |
56 | | #include "AES_CCM_128_test_vectors.h" |
57 | | #include "DerSigConversion_test_vectors.h" |
58 | | #include "HKDF_SHA256_test_vectors.h" |
59 | | |
60 | | namespace { |
61 | | |
62 | | using namespace chip; |
63 | | using namespace chip::Crypto; |
64 | | using namespace fuzztest; |
65 | | |
66 | | using KeyArray = std::array<uint8_t, kAES_CCM128_Key_Length>; |
67 | | using Bytes = std::vector<uint8_t>; |
68 | | |
69 | | // The test vectors carry nonces of 8 and 12 bytes; Matter itself mandates 13. |
70 | | // Allow a little headroom either side so nonce-length handling is exercised. |
71 | | constexpr size_t kMaxNonceLen = kAES_CCM128_Nonce_Length + 3; |
72 | | constexpr size_t kMaxAadLen = 1024; |
73 | | constexpr size_t kMaxTextLen = 2048; |
74 | | |
75 | | void EnsureInitialized() |
76 | 1.12k | { |
77 | 1.12k | static const bool sInitialized = [] { |
78 | 11 | VerifyOrDie(Platform::MemoryInit() == CHIP_NO_ERROR); |
79 | | #if CHIP_CRYPTO_PSA |
80 | | // The PSA backend rejects every key operation until the driver is |
81 | | // initialized, which would otherwise turn each property below into a |
82 | | // vacuous pass. |
83 | | VerifyOrDie(psa_crypto_init() == PSA_SUCCESS); |
84 | | #endif |
85 | 11 | return true; |
86 | 11 | }(); |
87 | 1.12k | (void) sInitialized; |
88 | 1.12k | } |
89 | | |
90 | | Bytes ToBytes(const uint8_t * data, size_t len) |
91 | 13.5k | { |
92 | 13.5k | return (data == nullptr) ? Bytes{} : Bytes(data, data + len); |
93 | 13.5k | } |
94 | | |
95 | | // Pull one field out of every AES-CCM test vector to use as domain seeds. |
96 | | template <typename Selector> |
97 | | std::vector<Bytes> CcmSeeds(Selector select, size_t maxLen) |
98 | 242 | { |
99 | 242 | std::vector<Bytes> seeds; |
100 | 242 | for (const auto * vector : ccm_128_test_vectors) |
101 | 12.8k | { |
102 | 12.8k | Bytes value = select(vector); |
103 | 12.8k | if (value.size() <= maxLen) |
104 | 12.8k | { |
105 | 12.8k | seeds.push_back(std::move(value)); |
106 | 12.8k | } |
107 | 12.8k | } |
108 | 242 | return seeds; |
109 | 242 | } FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::CcmSeeds<(anonymous namespace)::AnyNonce()::$_0>((anonymous namespace)::AnyNonce()::$_0, unsigned long) Line | Count | Source | 98 | 88 | { | 99 | 88 | std::vector<Bytes> seeds; | 100 | 88 | for (const auto * vector : ccm_128_test_vectors) | 101 | 4.66k | { | 102 | 4.66k | Bytes value = select(vector); | 103 | 4.66k | if (value.size() <= maxLen) | 104 | 4.66k | { | 105 | 4.66k | seeds.push_back(std::move(value)); | 106 | 4.66k | } | 107 | 4.66k | } | 108 | 88 | return seeds; | 109 | 88 | } |
FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::CcmSeeds<(anonymous namespace)::AnyAad()::$_0>((anonymous namespace)::AnyAad()::$_0, unsigned long) Line | Count | Source | 98 | 44 | { | 99 | 44 | std::vector<Bytes> seeds; | 100 | 44 | for (const auto * vector : ccm_128_test_vectors) | 101 | 2.33k | { | 102 | 2.33k | Bytes value = select(vector); | 103 | 2.33k | if (value.size() <= maxLen) | 104 | 2.33k | { | 105 | 2.33k | seeds.push_back(std::move(value)); | 106 | 2.33k | } | 107 | 2.33k | } | 108 | 44 | return seeds; | 109 | 44 | } |
FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::CcmSeeds<(anonymous namespace)::AnyCiphertext()::$_0>((anonymous namespace)::AnyCiphertext()::$_0, unsigned long) Line | Count | Source | 98 | 44 | { | 99 | 44 | std::vector<Bytes> seeds; | 100 | 44 | for (const auto * vector : ccm_128_test_vectors) | 101 | 2.33k | { | 102 | 2.33k | Bytes value = select(vector); | 103 | 2.33k | if (value.size() <= maxLen) | 104 | 2.33k | { | 105 | 2.33k | seeds.push_back(std::move(value)); | 106 | 2.33k | } | 107 | 2.33k | } | 108 | 44 | return seeds; | 109 | 44 | } |
FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::CcmSeeds<(anonymous namespace)::AnyTag()::$_0>((anonymous namespace)::AnyTag()::$_0, unsigned long) Line | Count | Source | 98 | 22 | { | 99 | 22 | std::vector<Bytes> seeds; | 100 | 22 | for (const auto * vector : ccm_128_test_vectors) | 101 | 1.16k | { | 102 | 1.16k | Bytes value = select(vector); | 103 | 1.16k | if (value.size() <= maxLen) | 104 | 1.16k | { | 105 | 1.16k | seeds.push_back(std::move(value)); | 106 | 1.16k | } | 107 | 1.16k | } | 108 | 22 | return seeds; | 109 | 22 | } |
FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::CcmSeeds<(anonymous namespace)::AnyPlaintext()::$_0>((anonymous namespace)::AnyPlaintext()::$_0, unsigned long) Line | Count | Source | 98 | 44 | { | 99 | 44 | std::vector<Bytes> seeds; | 100 | 44 | for (const auto * vector : ccm_128_test_vectors) | 101 | 2.33k | { | 102 | 2.33k | Bytes value = select(vector); | 103 | 2.33k | if (value.size() <= maxLen) | 104 | 2.33k | { | 105 | 2.33k | seeds.push_back(std::move(value)); | 106 | 2.33k | } | 107 | 2.33k | } | 108 | 44 | return seeds; | 109 | 44 | } |
|
110 | | |
111 | | std::vector<KeyArray> CcmKeySeeds() |
112 | 88 | { |
113 | 88 | std::vector<KeyArray> seeds; |
114 | 88 | for (const auto * vector : ccm_128_test_vectors) |
115 | 4.66k | { |
116 | 4.66k | if (vector->key_len != kAES_CCM128_Key_Length) |
117 | 0 | { |
118 | 0 | continue; |
119 | 0 | } |
120 | 4.66k | KeyArray key{}; |
121 | 4.66k | memcpy(key.data(), vector->key, key.size()); |
122 | 4.66k | seeds.push_back(key); |
123 | 4.66k | } |
124 | 88 | return seeds; |
125 | 88 | } |
126 | | |
127 | | auto AnyKey() |
128 | 88 | { |
129 | 88 | return Arbitrary<KeyArray>().WithSeeds(CcmKeySeeds()); |
130 | 88 | } |
131 | | |
132 | | auto AnyNonce() |
133 | 88 | { |
134 | 88 | return Arbitrary<Bytes>() |
135 | 88 | .WithMaxSize(kMaxNonceLen) |
136 | 4.66k | .WithSeeds(CcmSeeds([](const ccm_128_test_vector * v) { return ToBytes(v->nonce, v->nonce_len); }, kMaxNonceLen)); |
137 | 88 | } |
138 | | |
139 | | auto AnyAad() |
140 | 44 | { |
141 | 44 | return Arbitrary<Bytes>() |
142 | 44 | .WithMaxSize(kMaxAadLen) |
143 | 2.33k | .WithSeeds(CcmSeeds([](const ccm_128_test_vector * v) { return ToBytes(v->aad, v->aad_len); }, kMaxAadLen)); |
144 | 44 | } |
145 | | |
146 | | auto AnyPlaintext() |
147 | 44 | { |
148 | 44 | return Arbitrary<Bytes>() |
149 | 44 | .WithMaxSize(kMaxTextLen) |
150 | 2.33k | .WithSeeds(CcmSeeds([](const ccm_128_test_vector * v) { return ToBytes(v->pt, v->pt_len); }, kMaxTextLen)); |
151 | 44 | } |
152 | | |
153 | | auto AnyCiphertext() |
154 | 44 | { |
155 | 44 | return Arbitrary<Bytes>() |
156 | 44 | .WithMaxSize(kMaxTextLen) |
157 | 2.33k | .WithSeeds(CcmSeeds([](const ccm_128_test_vector * v) { return ToBytes(v->ct, v->ct_len); }, kMaxTextLen)); |
158 | 44 | } |
159 | | |
160 | | auto AnyTag() |
161 | 22 | { |
162 | 22 | return Arbitrary<Bytes>() |
163 | 22 | .WithMaxSize(kAES_CCM128_Tag_Length) |
164 | 1.16k | .WithSeeds(CcmSeeds([](const ccm_128_test_vector * v) { return ToBytes(v->tag, v->tag_len); }, kAES_CCM128_Tag_Length)); |
165 | 22 | } |
166 | | |
167 | | // AES-CCM permits these MIC/tag lengths. |
168 | | auto AnyTagLen() |
169 | 44 | { |
170 | 44 | return ElementOf<size_t>({ 4, 6, 8, 10, 12, 14, 16 }); |
171 | 44 | } |
172 | | |
173 | | bool MakeKey(const KeyArray & raw, DefaultSessionKeystore & keystore, Aes128KeyHandle & out) |
174 | 339 | { |
175 | 339 | Symmetric128BitsKeyByteArray material; |
176 | 339 | memcpy(material, raw.data(), raw.size()); |
177 | 339 | return keystore.CreateKey(material, out) == CHIP_NO_ERROR; |
178 | 339 | } |
179 | | |
180 | | // Drive the decryptor with fully fuzzer-controlled inputs. The authenticity |
181 | | // check is expected to reject them; the contract under test is that it does so |
182 | | // without reading or writing out of bounds. |
183 | | void AesCcmDecryptNoCrash(const KeyArray & key, const Bytes & nonce, const Bytes & aad, const Bytes & ciphertext, |
184 | | const Bytes & tagBytes, size_t tagLen) |
185 | 95 | { |
186 | 95 | EnsureInitialized(); |
187 | | |
188 | 95 | DefaultSessionKeystore keystore; |
189 | 95 | Aes128KeyHandle handle; |
190 | 95 | if (!MakeKey(key, keystore, handle)) |
191 | 0 | { |
192 | 0 | return; |
193 | 0 | } |
194 | | |
195 | 95 | Bytes tag(tagLen, 0); |
196 | 281 | for (size_t i = 0; i < tagLen && i < tagBytes.size(); ++i) |
197 | 186 | { |
198 | 186 | tag[i] = tagBytes[i]; |
199 | 186 | } |
200 | | |
201 | 95 | Bytes plaintext(ciphertext.size()); |
202 | 95 | RETURN_SAFELY_IGNORED AES_CCM_decrypt(ciphertext.data(), ciphertext.size(), aad.data(), aad.size(), tag.data(), tagLen, handle, |
203 | 95 | nonce.data(), nonce.size(), plaintext.data()); |
204 | 95 | keystore.DestroyKey(handle); |
205 | 95 | } |
206 | | |
207 | | FUZZ_TEST(CryptoPrimitives, AesCcmDecryptNoCrash) |
208 | | .WithDomains(AnyKey(), AnyNonce(), AnyAad(), AnyCiphertext(), AnyTag(), AnyTagLen()); |
209 | | |
210 | | // Round-trip plus authenticity: an honest decrypt must recover the plaintext, |
211 | | // and any single-bit change to the tag or the ciphertext must be rejected. |
212 | | void AesCcmRoundtrip(const KeyArray & key, const Bytes & nonce, const Bytes & aad, const Bytes & plaintext, size_t tagLen) |
213 | 101 | { |
214 | 101 | EnsureInitialized(); |
215 | | |
216 | 101 | DefaultSessionKeystore keystore; |
217 | 101 | Aes128KeyHandle handle; |
218 | 101 | if (!MakeKey(key, keystore, handle)) |
219 | 0 | { |
220 | 0 | return; |
221 | 0 | } |
222 | | |
223 | 101 | Bytes ciphertext(plaintext.size()); |
224 | 101 | Bytes tag(tagLen); |
225 | 101 | CHIP_ERROR encryptError = AES_CCM_encrypt(plaintext.data(), plaintext.size(), aad.data(), aad.size(), handle, nonce.data(), |
226 | 101 | nonce.size(), ciphertext.data(), tag.data(), tagLen); |
227 | 101 | if (encryptError != CHIP_NO_ERROR) |
228 | 68 | { |
229 | 68 | keystore.DestroyKey(handle); |
230 | 68 | return; |
231 | 68 | } |
232 | | |
233 | 33 | Bytes recovered(plaintext.size()); |
234 | 33 | ASSERT_EQ(AES_CCM_decrypt(ciphertext.data(), ciphertext.size(), aad.data(), aad.size(), tag.data(), tagLen, handle, |
235 | 33 | nonce.data(), nonce.size(), recovered.data()), |
236 | 33 | CHIP_NO_ERROR); |
237 | 33 | ASSERT_EQ(recovered, plaintext); |
238 | | |
239 | 33 | Bytes sink(plaintext.size()); |
240 | | |
241 | 33 | Bytes tamperedTag = tag; |
242 | 33 | tamperedTag[0] ^= 0x01; |
243 | 33 | ASSERT_NE(AES_CCM_decrypt(ciphertext.data(), ciphertext.size(), aad.data(), aad.size(), tamperedTag.data(), tagLen, handle, |
244 | 33 | nonce.data(), nonce.size(), sink.data()), |
245 | 33 | CHIP_NO_ERROR); |
246 | | |
247 | 33 | if (!ciphertext.empty()) |
248 | 18 | { |
249 | 18 | Bytes tamperedCiphertext = ciphertext; |
250 | 18 | tamperedCiphertext[0] ^= 0x01; |
251 | 18 | ASSERT_NE(AES_CCM_decrypt(tamperedCiphertext.data(), tamperedCiphertext.size(), aad.data(), aad.size(), tag.data(), tagLen, |
252 | 18 | handle, nonce.data(), nonce.size(), sink.data()), |
253 | 18 | CHIP_NO_ERROR); |
254 | 18 | } |
255 | | |
256 | 33 | keystore.DestroyKey(handle); |
257 | 33 | } |
258 | | |
259 | | FUZZ_TEST(CryptoPrimitives, AesCcmRoundtrip).WithDomains(AnyKey(), AnyNonce(), AnyAad(), AnyPlaintext(), AnyTagLen()); |
260 | | |
261 | | // AES-CTR carries no authentication tag: it is the keystream XOR used to |
262 | | // obfuscate the privacy header of a group message, and on receive it runs over |
263 | | // the datagram before the AES-CCM tag is checked. |
264 | | void AesCtrCryptNoCrash(const KeyArray & key, const Bytes & nonce, const Bytes & input) |
265 | 72 | { |
266 | 72 | EnsureInitialized(); |
267 | | |
268 | 72 | DefaultSessionKeystore keystore; |
269 | 72 | Aes128KeyHandle handle; |
270 | 72 | if (!MakeKey(key, keystore, handle)) |
271 | 0 | { |
272 | 0 | return; |
273 | 0 | } |
274 | | |
275 | 72 | Bytes output(input.size()); |
276 | 72 | RETURN_SAFELY_IGNORED AES_CTR_crypt(input.data(), input.size(), handle, nonce.data(), nonce.size(), output.data()); |
277 | 72 | keystore.DestroyKey(handle); |
278 | 72 | } |
279 | | |
280 | | FUZZ_TEST(CryptoPrimitives, AesCtrCryptNoCrash).WithDomains(AnyKey(), AnyNonce(), AnyCiphertext()); |
281 | | |
282 | | // CTR mode is its own inverse, so applying it twice must return the input. |
283 | | void AesCtrRoundtrip(const KeyArray & key, const Bytes & nonce, const Bytes & plaintext) |
284 | 71 | { |
285 | 71 | EnsureInitialized(); |
286 | | |
287 | 71 | DefaultSessionKeystore keystore; |
288 | 71 | Aes128KeyHandle handle; |
289 | 71 | if (!MakeKey(key, keystore, handle)) |
290 | 0 | { |
291 | 0 | return; |
292 | 0 | } |
293 | | |
294 | 71 | Bytes ciphertext(plaintext.size()); |
295 | 71 | if (AES_CTR_crypt(plaintext.data(), plaintext.size(), handle, nonce.data(), nonce.size(), ciphertext.data()) == CHIP_NO_ERROR) |
296 | 26 | { |
297 | 26 | Bytes recovered(plaintext.size()); |
298 | 26 | ASSERT_EQ(AES_CTR_crypt(ciphertext.data(), ciphertext.size(), handle, nonce.data(), nonce.size(), recovered.data()), |
299 | 26 | CHIP_NO_ERROR); |
300 | 26 | ASSERT_EQ(recovered, plaintext); |
301 | 26 | } |
302 | 71 | keystore.DestroyKey(handle); |
303 | 71 | } |
304 | | |
305 | | FUZZ_TEST(CryptoPrimitives, AesCtrRoundtrip).WithDomains(AnyKey(), AnyNonce(), AnyPlaintext()); |
306 | | |
307 | | // Epoch keys and compressed fabric ids from the group key derivation vectors in |
308 | | // TestChipCryptoPAL.cpp. |
309 | | const uint8_t kEpochKey1[] = { 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf }; |
310 | | const uint8_t kEpochKey2[] = { 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf }; |
311 | | const uint8_t kEpochKey3[] = { 0x23, 0x5b, 0xf7, 0xe6, 0x28, 0x23, 0xd3, 0x58, 0xdc, 0xa4, 0xba, 0x50, 0xb1, 0x53, 0x5f, 0x4b }; |
312 | | const uint8_t kCompressedFabricId1[] = { 0x29, 0x06, 0xc9, 0x08, 0xd1, 0x15, 0xd3, 0x62 }; |
313 | | const uint8_t kCompressedFabricId2[] = { 0x87, 0xe1, 0xb0, 0x04, 0xe2, 0x35, 0xa1, 0x30 }; |
314 | | |
315 | | auto AnyEpochKey() |
316 | 44 | { |
317 | 44 | return Arbitrary<Bytes>() |
318 | 44 | .WithMaxSize(CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES * 2) |
319 | 44 | .WithSeeds({ Bytes(std::begin(kEpochKey1), std::end(kEpochKey1)), Bytes(std::begin(kEpochKey2), std::end(kEpochKey2)), |
320 | 44 | Bytes(std::begin(kEpochKey3), std::end(kEpochKey3)) }); |
321 | 44 | } |
322 | | |
323 | | auto AnyCompressedFabricId() |
324 | 44 | { |
325 | 44 | return Arbitrary<Bytes>() |
326 | 44 | .WithMaxSize(kCompressedFabricIdentifierSize * 2) |
327 | 44 | .WithSeeds({ Bytes(std::begin(kCompressedFabricId1), std::end(kCompressedFabricId1)), |
328 | 44 | Bytes(std::begin(kCompressedFabricId2), std::end(kCompressedFabricId2)) }); |
329 | 44 | } |
330 | | |
331 | | // The group key schedule: an epoch key plus the compressed fabric id yield the |
332 | | // encryption key, the privacy key and the session id used for group messaging. |
333 | | void GroupKeyDerivation(const Bytes & epochKey, const Bytes & compressedFabricId) |
334 | 39 | { |
335 | 39 | EnsureInitialized(); |
336 | | |
337 | 39 | const ByteSpan epochKeySpan(epochKey.data(), epochKey.size()); |
338 | 39 | const ByteSpan fabricIdSpan(compressedFabricId.data(), compressedFabricId.size()); |
339 | | |
340 | 39 | uint8_t operationalKeyBuf[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] = { 0 }; |
341 | 39 | MutableByteSpan operationalKey(operationalKeyBuf); |
342 | 39 | if (DeriveGroupOperationalKey(epochKeySpan, fabricIdSpan, operationalKey) == CHIP_NO_ERROR) |
343 | 7 | { |
344 | | // The session id is derived from the operational key, so it is only |
345 | | // meaningful once that derivation has succeeded. |
346 | 7 | uint16_t sessionId = 0; |
347 | 7 | RETURN_SAFELY_IGNORED DeriveGroupSessionId(ByteSpan(operationalKey.data(), operationalKey.size()), sessionId); |
348 | 7 | } |
349 | | |
350 | | // Fed the raw fuzzer bytes rather than the derived operational key that |
351 | | // production uses, so that the length-rejection path is exercised too. |
352 | 39 | uint8_t privacyKeyBuf[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] = { 0 }; |
353 | 39 | MutableByteSpan privacyKey(privacyKeyBuf); |
354 | 39 | RETURN_SAFELY_IGNORED DeriveGroupPrivacyKey(epochKeySpan, privacyKey); |
355 | | |
356 | 39 | GroupOperationalCredentials credentials; |
357 | 39 | RETURN_SAFELY_IGNORED DeriveGroupOperationalCredentials(epochKeySpan, fabricIdSpan, credentials); |
358 | 39 | } |
359 | | |
360 | | FUZZ_TEST(CryptoPrimitives, GroupKeyDerivation).WithDomains(AnyEpochKey(), AnyCompressedFabricId()); |
361 | | |
362 | | // The combined credentials call must agree with running the same schedule by |
363 | | // hand. Note the chaining: the privacy key is derived from the *operational* |
364 | | // key, not from the epoch key -- DeriveGroupPrivacyKey names its parameter |
365 | | // encryption_key, and both keys are 16 bytes, so feeding it the epoch key is |
366 | | // accepted and silently yields a different key. |
367 | | void GroupKeyDerivationAgrees(const Bytes & epochKey, const Bytes & compressedFabricId) |
368 | 40 | { |
369 | 40 | EnsureInitialized(); |
370 | | |
371 | 40 | const ByteSpan epochKeySpan(epochKey.data(), epochKey.size()); |
372 | 40 | const ByteSpan fabricIdSpan(compressedFabricId.data(), compressedFabricId.size()); |
373 | | |
374 | 40 | GroupOperationalCredentials credentials; |
375 | 40 | if (DeriveGroupOperationalCredentials(epochKeySpan, fabricIdSpan, credentials) != CHIP_NO_ERROR) |
376 | 35 | { |
377 | 35 | return; |
378 | 35 | } |
379 | | |
380 | 5 | uint8_t operationalKeyBuf[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] = { 0 }; |
381 | 5 | MutableByteSpan operationalKey(operationalKeyBuf); |
382 | 5 | ASSERT_EQ(DeriveGroupOperationalKey(epochKeySpan, fabricIdSpan, operationalKey), CHIP_NO_ERROR); |
383 | 5 | ASSERT_EQ(memcmp(credentials.encryption_key, operationalKey.data(), operationalKey.size()), 0); |
384 | | |
385 | 5 | uint8_t privacyKeyBuf[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] = { 0 }; |
386 | 5 | MutableByteSpan privacyKey(privacyKeyBuf); |
387 | 5 | ASSERT_EQ(DeriveGroupPrivacyKey(ByteSpan(operationalKey.data(), operationalKey.size()), privacyKey), CHIP_NO_ERROR); |
388 | 5 | ASSERT_EQ(memcmp(credentials.privacy_key, privacyKey.data(), privacyKey.size()), 0); |
389 | | |
390 | 5 | uint16_t sessionId = 0; |
391 | 5 | ASSERT_EQ(DeriveGroupSessionId(ByteSpan(operationalKey.data(), operationalKey.size()), sessionId), CHIP_NO_ERROR); |
392 | 5 | ASSERT_EQ(sessionId, credentials.hash); |
393 | 5 | } |
394 | | |
395 | | FUZZ_TEST(CryptoPrimitives, GroupKeyDerivationAgrees).WithDomains(AnyEpochKey(), AnyCompressedFabricId()); |
396 | | |
397 | | // ECDSA signatures cross this boundary in both directions: raw fixed-width r||s |
398 | | // on the Matter wire, X9.62 DER inside certificates. |
399 | | std::vector<Bytes> DerSigSeeds(bool wantDer) |
400 | 66 | { |
401 | 66 | std::vector<Bytes> seeds; |
402 | 66 | for (const auto & vector : kDerSigConvTestVectors) |
403 | 330 | { |
404 | 330 | seeds.push_back(wantDer ? ToBytes(vector.der_version, vector.der_version_length) |
405 | 330 | : ToBytes(vector.raw_version, vector.raw_version_length)); |
406 | 330 | } |
407 | 66 | return seeds; |
408 | 66 | } |
409 | | |
410 | | // The vectors use 32- and 64-byte field elements, so the buffers here are sized |
411 | | // past P-256's kMax_ECDSA_Signature_Length rather than from it. Fuzzing the |
412 | | // width a little beyond the largest vector exercises the length validation. |
413 | | constexpr size_t kMaxFeLen = 80; |
414 | | constexpr size_t kMaxRawSigLen = 2 * kMaxFeLen; |
415 | | constexpr size_t kMaxDerSigLen = kMaxRawSigLen + kMax_ECDSA_X9Dot62_Asn1_Overhead; |
416 | | |
417 | | auto AnyFeLength() |
418 | 44 | { |
419 | 44 | return InRange<size_t>(0, kMaxFeLen); |
420 | 44 | } |
421 | | |
422 | | void EcdsaSignatureRawToDer(size_t feLengthBytes, const Bytes & rawSig) |
423 | 105 | { |
424 | 105 | EnsureInitialized(); |
425 | | |
426 | 105 | uint8_t derBuf[kMaxDerSigLen]; |
427 | 105 | MutableByteSpan derSig(derBuf); |
428 | 105 | RETURN_SAFELY_IGNORED EcdsaRawSignatureToAsn1(feLengthBytes, ByteSpan(rawSig.data(), rawSig.size()), derSig); |
429 | 105 | } |
430 | | |
431 | | FUZZ_TEST(CryptoPrimitives, EcdsaSignatureRawToDer) |
432 | | .WithDomains(AnyFeLength(), Arbitrary<Bytes>().WithMaxSize(kMaxRawSigLen).WithSeeds(DerSigSeeds(false))); |
433 | | |
434 | | void EcdsaSignatureDerToRaw(size_t feLengthBytes, const Bytes & derSig) |
435 | 303 | { |
436 | 303 | EnsureInitialized(); |
437 | | |
438 | 303 | uint8_t rawBuf[kMaxRawSigLen]; |
439 | 303 | MutableByteSpan rawSig(rawBuf); |
440 | 303 | RETURN_SAFELY_IGNORED EcdsaAsn1SignatureToRaw(feLengthBytes, ByteSpan(derSig.data(), derSig.size()), rawSig); |
441 | 303 | } |
442 | | |
443 | | FUZZ_TEST(CryptoPrimitives, EcdsaSignatureDerToRaw) |
444 | | .WithDomains(AnyFeLength(), Arbitrary<Bytes>().WithMaxSize(kMaxDerSigLen).WithSeeds(DerSigSeeds(true))); |
445 | | |
446 | | // The two converters are inverses for a well-formed raw signature, so a |
447 | | // successful round-trip must reproduce the input exactly. |
448 | | void EcdsaSignatureConversionRoundtrip(const Bytes & rawSig) |
449 | 121 | { |
450 | 121 | EnsureInitialized(); |
451 | | |
452 | | // Both directions take the field-element width, which is half of r||s. |
453 | 121 | if (rawSig.empty() || (rawSig.size() % 2) != 0) |
454 | 33 | { |
455 | 33 | return; |
456 | 33 | } |
457 | 88 | const size_t feLengthBytes = rawSig.size() / 2; |
458 | | |
459 | 88 | uint8_t derBuf[kMaxDerSigLen]; |
460 | 88 | MutableByteSpan derSig(derBuf); |
461 | 88 | if (EcdsaRawSignatureToAsn1(feLengthBytes, ByteSpan(rawSig.data(), rawSig.size()), derSig) != CHIP_NO_ERROR) |
462 | 0 | { |
463 | 0 | return; |
464 | 0 | } |
465 | | |
466 | 88 | uint8_t rawBuf[kMaxRawSigLen]; |
467 | 88 | MutableByteSpan recovered(rawBuf); |
468 | 88 | ASSERT_EQ(EcdsaAsn1SignatureToRaw(feLengthBytes, ByteSpan(derSig.data(), derSig.size()), recovered), CHIP_NO_ERROR); |
469 | 88 | ASSERT_EQ(recovered.size(), rawSig.size()); |
470 | 88 | ASSERT_EQ(memcmp(recovered.data(), rawSig.data(), rawSig.size()), 0); |
471 | 88 | } |
472 | | |
473 | | FUZZ_TEST(CryptoPrimitives, EcdsaSignatureConversionRoundtrip) |
474 | | .WithDomains(Arbitrary<Bytes>().WithMaxSize(kMaxRawSigLen).WithSeeds(DerSigSeeds(false))); |
475 | | |
476 | | template <typename Selector> |
477 | | std::vector<Bytes> HkdfSeeds(Selector select, size_t maxLen) |
478 | 132 | { |
479 | 132 | std::vector<Bytes> seeds; |
480 | 132 | for (const auto & vector : hkdf_sha256_test_vectors) |
481 | 396 | { |
482 | 396 | Bytes value = select(vector); |
483 | 396 | if (value.size() <= maxLen) |
484 | 396 | { |
485 | 396 | seeds.push_back(std::move(value)); |
486 | 396 | } |
487 | 396 | } |
488 | 132 | return seeds; |
489 | 132 | } FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::HkdfSeeds<(anonymous namespace)::AnyHkdfSecret()::$_0>((anonymous namespace)::AnyHkdfSecret()::$_0, unsigned long) Line | Count | Source | 478 | 44 | { | 479 | 44 | std::vector<Bytes> seeds; | 480 | 44 | for (const auto & vector : hkdf_sha256_test_vectors) | 481 | 132 | { | 482 | 132 | Bytes value = select(vector); | 483 | 132 | if (value.size() <= maxLen) | 484 | 132 | { | 485 | 132 | seeds.push_back(std::move(value)); | 486 | 132 | } | 487 | 132 | } | 488 | 44 | return seeds; | 489 | 44 | } |
FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::HkdfSeeds<(anonymous namespace)::AnyHkdfSalt()::$_0>((anonymous namespace)::AnyHkdfSalt()::$_0, unsigned long) Line | Count | Source | 478 | 44 | { | 479 | 44 | std::vector<Bytes> seeds; | 480 | 44 | for (const auto & vector : hkdf_sha256_test_vectors) | 481 | 132 | { | 482 | 132 | Bytes value = select(vector); | 483 | 132 | if (value.size() <= maxLen) | 484 | 132 | { | 485 | 132 | seeds.push_back(std::move(value)); | 486 | 132 | } | 487 | 132 | } | 488 | 44 | return seeds; | 489 | 44 | } |
FuzzCryptoPrimitivesPW.cpp:std::__1::vector<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > > > (anonymous namespace)::HkdfSeeds<(anonymous namespace)::AnyHkdfInfo()::$_0>((anonymous namespace)::AnyHkdfInfo()::$_0, unsigned long) Line | Count | Source | 478 | 44 | { | 479 | 44 | std::vector<Bytes> seeds; | 480 | 44 | for (const auto & vector : hkdf_sha256_test_vectors) | 481 | 132 | { | 482 | 132 | Bytes value = select(vector); | 483 | 132 | if (value.size() <= maxLen) | 484 | 132 | { | 485 | 132 | seeds.push_back(std::move(value)); | 486 | 132 | } | 487 | 132 | } | 488 | 44 | return seeds; | 489 | 44 | } |
|
490 | | |
491 | | constexpr size_t kMaxHkdfInputLen = 512; |
492 | | |
493 | | auto AnyHkdfSecret() |
494 | 44 | { |
495 | 44 | return Arbitrary<Bytes>() |
496 | 44 | .WithMaxSize(kMaxHkdfInputLen) |
497 | 44 | .WithSeeds( |
498 | 132 | HkdfSeeds([](const hkdf_sha256_vector & v) { return ToBytes(v.initial_key_material, v.initial_key_material_length); }, |
499 | 44 | kMaxHkdfInputLen)); |
500 | 44 | } |
501 | | |
502 | | auto AnyHkdfSalt() |
503 | 44 | { |
504 | 44 | return Arbitrary<Bytes>() |
505 | 44 | .WithMaxSize(kMaxHkdfInputLen) |
506 | 132 | .WithSeeds(HkdfSeeds([](const hkdf_sha256_vector & v) { return ToBytes(v.salt, v.salt_length); }, kMaxHkdfInputLen)); |
507 | 44 | } |
508 | | |
509 | | auto AnyHkdfInfo() |
510 | 44 | { |
511 | 44 | return Arbitrary<Bytes>() |
512 | 44 | .WithMaxSize(kMaxHkdfInputLen) |
513 | 132 | .WithSeeds(HkdfSeeds([](const hkdf_sha256_vector & v) { return ToBytes(v.info, v.info_length); }, kMaxHkdfInputLen)); |
514 | 44 | } |
515 | | |
516 | | void HkdfSha256NoCrash(const Bytes & secret, const Bytes & salt, const Bytes & info, size_t outLen) |
517 | 93 | { |
518 | 93 | EnsureInitialized(); |
519 | | |
520 | 93 | Bytes out(outLen); |
521 | 93 | HKDF_sha hkdf; |
522 | 93 | RETURN_SAFELY_IGNORED hkdf.HKDF_SHA256(secret.data(), secret.size(), salt.data(), salt.size(), info.data(), info.size(), |
523 | 93 | out.data(), outLen); |
524 | 93 | } |
525 | | |
526 | | FUZZ_TEST(CryptoPrimitives, HkdfSha256NoCrash).WithDomains(AnyHkdfSecret(), AnyHkdfSalt(), AnyHkdfInfo(), InRange<size_t>(1, 4096)); |
527 | | |
528 | | // HKDF is a pure function: the same inputs must always produce the same output. |
529 | | void HkdfSha256Deterministic(const Bytes & secret, const Bytes & salt, const Bytes & info, size_t outLen) |
530 | 83 | { |
531 | 83 | EnsureInitialized(); |
532 | | |
533 | 83 | HKDF_sha hkdf; |
534 | 83 | Bytes first(outLen); |
535 | 83 | Bytes second(outLen); |
536 | 83 | CHIP_ERROR firstError = |
537 | 83 | hkdf.HKDF_SHA256(secret.data(), secret.size(), salt.data(), salt.size(), info.data(), info.size(), first.data(), outLen); |
538 | 83 | CHIP_ERROR secondError = |
539 | 83 | hkdf.HKDF_SHA256(secret.data(), secret.size(), salt.data(), salt.size(), info.data(), info.size(), second.data(), outLen); |
540 | 83 | ASSERT_EQ(firstError, secondError); |
541 | 83 | if (firstError == CHIP_NO_ERROR) |
542 | 40 | { |
543 | | ASSERT_EQ(first, second); |
544 | 40 | } |
545 | 83 | } |
546 | | |
547 | | FUZZ_TEST(CryptoPrimitives, HkdfSha256Deterministic) |
548 | | .WithDomains(AnyHkdfSecret(), AnyHkdfSalt(), AnyHkdfInfo(), InRange<size_t>(1, 1024)); |
549 | | |
550 | | } // namespace |