Coverage Report

Created: 2026-11-12 07:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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