xref: /openbmc/libbej/test/bej_decoder_test.cpp (revision 7c1be8ddd11a0ae7936cd5afc2c58c11735f88f3)
1 #include "bej_common_test.hpp"
2 #include "bej_decoder_json.hpp"
3 #include "bej_encoder_json.hpp"
4 
5 #include <memory>
6 #include <string_view>
7 #include <vector>
8 
9 #include <gmock/gmock-matchers.h>
10 #include <gmock/gmock.h>
11 #include <gtest/gtest.h>
12 
13 namespace libbej
14 {
15 
16 struct BejDecoderTestParams
17 {
18     const std::string testName;
19     const BejTestInputFiles inputFiles;
20 };
21 
PrintTo(const BejDecoderTestParams & params,std::ostream * os)22 void PrintTo(const BejDecoderTestParams& params, std::ostream* os)
23 {
24     *os << params.testName;
25 }
26 
27 using BejDecoderTest = testing::TestWithParam<BejDecoderTestParams>;
28 
29 const BejTestInputFiles driveOemTestFiles = {
30     .jsonFile = "../test/json/drive_oem.json",
31     .schemaDictionaryFile = "../test/dictionaries/drive_oem_dict.bin",
32     .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
33     .errorDictionaryFile = "",
34     .encodedStreamFile = "../test/encoded/drive_oem_enc.bin",
35 };
36 
37 const BejTestInputFiles circuitTestFiles = {
38     .jsonFile = "../test/json/circuit.json",
39     .schemaDictionaryFile = "../test/dictionaries/circuit_dict.bin",
40     .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
41     .errorDictionaryFile = "",
42     .encodedStreamFile = "../test/encoded/circuit_enc.bin",
43 };
44 
45 const BejTestInputFiles storageTestFiles = {
46     .jsonFile = "../test/json/storage.json",
47     .schemaDictionaryFile = "../test/dictionaries/storage_dict.bin",
48     .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
49     .errorDictionaryFile = "",
50     .encodedStreamFile = "../test/encoded/storage_enc.bin",
51 };
52 
53 const BejTestInputFiles dummySimpleTestFiles = {
54     .jsonFile = "../test/json/dummysimple.json",
55     .schemaDictionaryFile = "../test/dictionaries/dummy_simple_dict.bin",
56     .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
57     .errorDictionaryFile = "",
58     .encodedStreamFile = "../test/encoded/dummy_simple_enc.bin",
59 };
60 
TEST_P(BejDecoderTest,Decode)61 TEST_P(BejDecoderTest, Decode)
62 {
63     const BejDecoderTestParams& test_case = GetParam();
64     auto inputsOrErr = loadInputs(test_case.inputFiles);
65     EXPECT_TRUE(inputsOrErr);
66 
67     BejDictionaries dictionaries = {
68         .schemaDictionary = inputsOrErr->schemaDictionary,
69         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
70         .annotationDictionary = inputsOrErr->annotationDictionary,
71         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
72         .errorDictionary = inputsOrErr->errorDictionary,
73         .errorDictionarySize = inputsOrErr->errorDictionarySize,
74     };
75 
76     BejDecoderJson decoder;
77     EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
78     std::string decoded = decoder.getOutput();
79     nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
80 
81     // Just comparing nlohmann::json types could lead to errors. It compares the
82     // byte values. So int64 and unit64 comparisons might be incorrect. Eg:
83     // bytes values for -5 and 18446744073709551611 are the same. So compare the
84     // string values.
85     EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
86 }
87 
88 /**
89  * TODO: Add more test cases.
90  * - Test Enums inside array elemets
91  * - Array inside an array: is this a valid case?
92  * - Real numbers with exponent part
93  * - Every type inside an array.
94  */
95 INSTANTIATE_TEST_SUITE_P(
96     , BejDecoderTest,
97     testing::ValuesIn<BejDecoderTestParams>({
98         {"DriveOEM", driveOemTestFiles},
99         {"Circuit", circuitTestFiles},
100         {"Storage", storageTestFiles},
101         {"DummySimple", dummySimpleTestFiles},
102     }),
__anon72dd28ce0102(const testing::TestParamInfo<BejDecoderTest::ParamType>& info) 103     [](const testing::TestParamInfo<BejDecoderTest::ParamType>& info) {
104         return info.param.testName;
105     });
106 
TEST(BejDecoderSecurityTest,MaxOperationsLimit)107 TEST(BejDecoderSecurityTest, MaxOperationsLimit)
108 {
109     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
110     ASSERT_TRUE(inputsOrErr);
111 
112     BejDictionaries dictionaries = {
113         .schemaDictionary = inputsOrErr->schemaDictionary,
114         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
115         .annotationDictionary = inputsOrErr->annotationDictionary,
116         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
117         .errorDictionary = inputsOrErr->errorDictionary,
118         .errorDictionarySize = inputsOrErr->errorDictionarySize,
119     };
120 
121     // Each array element below consists of a set and two properties, resulting
122     // in 3 operations. 400,000 elements will result in 1,200,000 operations,
123     // which should exceed the limit of 1,000,000.
124     constexpr int numElements = 400000;
125 
126     auto root = std::make_unique<RedfishPropertyParent>();
127     bejTreeInitSet(root.get(), "DummySimple");
128 
129     auto childArray = std::make_unique<RedfishPropertyParent>();
130     bejTreeInitArray(childArray.get(), "ChildArrayProperty");
131     bejTreeLinkChildToParent(root.get(), childArray.get());
132 
133     std::vector<std::unique_ptr<RedfishPropertyParent>> sets;
134     std::vector<std::unique_ptr<RedfishPropertyLeafBool>> bools;
135     std::vector<std::unique_ptr<RedfishPropertyLeafEnum>> enums;
136     sets.reserve(numElements);
137     bools.reserve(numElements);
138     enums.reserve(numElements);
139 
140     for (int i = 0; i < numElements; ++i)
141     {
142         auto chArraySet = std::make_unique<RedfishPropertyParent>();
143         bejTreeInitSet(chArraySet.get(), nullptr);
144 
145         auto chArraySetBool = std::make_unique<RedfishPropertyLeafBool>();
146         bejTreeAddBool(chArraySet.get(), chArraySetBool.get(), "AnotherBoolean",
147                        true);
148 
149         auto chArraySetLs = std::make_unique<RedfishPropertyLeafEnum>();
150         bejTreeAddEnum(chArraySet.get(), chArraySetLs.get(), "LinkStatus",
151                        "NoLink");
152 
153         bejTreeLinkChildToParent(childArray.get(), chArraySet.get());
154 
155         sets.push_back(std::move(chArraySet));
156         bools.push_back(std::move(chArraySetBool));
157         enums.push_back(std::move(chArraySetLs));
158     }
159 
160     libbej::BejEncoderJson encoder;
161     encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
162     std::vector<uint8_t> outputBuffer = encoder.getOutput();
163 
164     BejDecoderJson decoder;
165     EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
166                 bejErrorNotSupported);
167 }
168 
TEST(BejDecoderSecurityTest,RealWithTooManyLeadingZeros)169 TEST(BejDecoderSecurityTest, RealWithTooManyLeadingZeros)
170 {
171     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
172     ASSERT_TRUE(inputsOrErr);
173 
174     BejDictionaries dictionaries = {
175         .schemaDictionary = inputsOrErr->schemaDictionary,
176         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
177         .annotationDictionary = inputsOrErr->annotationDictionary,
178         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
179         .errorDictionary = inputsOrErr->errorDictionary,
180         .errorDictionarySize = inputsOrErr->errorDictionarySize,
181     };
182 
183     auto root = std::make_unique<RedfishPropertyParent>();
184     bejTreeInitSet(root.get(), "DummySimple");
185 
186     // 1.003 was randomely chosen
187     auto real = std::make_unique<RedfishPropertyLeafReal>();
188     bejTreeAddReal(root.get(), real.get(), "SampleRealProperty", 1.003);
189 
190     libbej::BejEncoderJson encoder;
191     encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
192     std::vector<uint8_t> outputBuffer = encoder.getOutput();
193 
194     // Manually tamper with the encoded stream to create the attack vector.
195     // We will find the `bejReal` property and overwrite its `zeroCount`.
196     // The property "SampleRealProperty" has sequence number 4. The encoded
197     // sequence number is `(4 << 1) | 0 = 8`. The nnint for 8 is `0x01, 0x08`.
198     const std::vector<uint8_t> realPropSeqNum = {0x01, 0x08};
199     auto it = std::search(outputBuffer.begin(), outputBuffer.end(),
200                           realPropSeqNum.begin(), realPropSeqNum.end());
201     ASSERT_NE(it, outputBuffer.end()) << "Could not find bejReal property";
202 
203     // The structure of a bejReal SFLV is: S(nnint) F(u8) L(nnint) V(...)
204     // The structure of V is: nnint(len(whole)), int(whole), nnint(zeroCount)...
205     // Find the start of the value (V) by skipping S, F, and L.
206     size_t sflvOffset = std::distance(outputBuffer.begin(), it);
207     const uint8_t* streamPtr = outputBuffer.data() + sflvOffset;
208     // Skip S
209     streamPtr += bejGetNnintSize(streamPtr);
210     // Skip F
211     streamPtr++;
212     // Skip L
213     const uint8_t* valuePtr = streamPtr + bejGetNnintSize(streamPtr);
214 
215     // Find the start of zeroCount within V.
216     const uint8_t* zeroCountPtr = valuePtr + bejGetNnintSize(valuePtr);
217     zeroCountPtr += bejGetNnint(valuePtr); // Skip int(whole)
218 
219     // The original zeroCount for 1.003 is 2. nnint(2) is `0x01, 0x02`.
220     // We replace it with a zeroCount of 101, which exceeds the limit.
221     // nnint(101) is `0x01, 101`. The size is the same (2 bytes), so we don't
222     // need to update the SFLV length field (L).
223     ASSERT_EQ(bejGetNnint(zeroCountPtr), 2);
224     size_t zeroCountOffset = zeroCountPtr - outputBuffer.data();
225     // nnint value for 101
226     outputBuffer[zeroCountOffset + 1] = 101;
227 
228     BejDecoderJson decoder;
229     EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
230                 bejErrorInvalidSize);
231 }
232 
TEST(BejDecoderSecurityTest,StringTooLong)233 TEST(BejDecoderSecurityTest, StringTooLong)
234 {
235     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
236     ASSERT_TRUE(inputsOrErr);
237 
238     BejDictionaries dictionaries = {
239         .schemaDictionary = inputsOrErr->schemaDictionary,
240         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
241         .annotationDictionary = inputsOrErr->annotationDictionary,
242         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
243         .errorDictionary = inputsOrErr->errorDictionary,
244         .errorDictionarySize = inputsOrErr->errorDictionarySize,
245     };
246 
247     auto root = std::make_unique<RedfishPropertyParent>();
248     bejTreeInitSet(root.get(), "DummySimple");
249 
250     // Create a string with a length greater than MAX_BEJ_STRING_LEN (65536).
251     std::string longString(65537, 'A');
252 
253     auto stringProp = std::make_unique<RedfishPropertyLeafString>();
254     bejTreeAddString(root.get(), stringProp.get(), "Id", longString.c_str());
255 
256     libbej::BejEncoderJson encoder;
257     encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
258     std::vector<uint8_t> outputBuffer = encoder.getOutput();
259 
260     // The decoder should return an error because the string is too long.
261     BejDecoderJson decoder;
262     EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
263                 bejErrorInvalidSize);
264 }
265 
TEST(BejDecoderSecurityTest,ValueBeyondStreamLength)266 TEST(BejDecoderSecurityTest, ValueBeyondStreamLength)
267 {
268     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
269     ASSERT_TRUE(inputsOrErr);
270 
271     BejDictionaries dictionaries = {
272         .schemaDictionary = inputsOrErr->schemaDictionary,
273         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
274         .annotationDictionary = inputsOrErr->annotationDictionary,
275         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
276         .errorDictionary = inputsOrErr->errorDictionary,
277         .errorDictionarySize = inputsOrErr->errorDictionarySize,
278     };
279 
280     auto root = std::make_unique<RedfishPropertyParent>();
281     bejTreeInitSet(root.get(), "DummySimple");
282 
283     auto intProp = std::make_unique<RedfishPropertyLeafInt>();
284     bejTreeAddInteger(root.get(), intProp.get(), "SampleIntegerProperty", 123);
285 
286     libbej::BejEncoderJson encoder;
287     encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
288     std::vector<uint8_t> outputBuffer = encoder.getOutput();
289 
290     // Tamper with the encoded stream to simulate a value extending beyond the
291     // stream length. This stream only has an integer 0x7b. The value before it
292     // is the length tuple.
293     outputBuffer[outputBuffer.size() - 2] = 0x05;
294 
295     BejDecoderJson decoder;
296     EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
297                 bejErrorInvalidSize);
298 }
299 
TEST(BejDecoderSecurityTest,InvalidSchemaDictionarySize)300 TEST(BejDecoderSecurityTest, InvalidSchemaDictionarySize)
301 {
302     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
303     ASSERT_TRUE(inputsOrErr);
304 
305     BejDictionaries dictionaries = {
306         .schemaDictionary = inputsOrErr->schemaDictionary,
307         .schemaDictionarySize = 10,
308         .annotationDictionary = inputsOrErr->annotationDictionary,
309         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
310         .errorDictionary = inputsOrErr->errorDictionary,
311         .errorDictionarySize = inputsOrErr->errorDictionarySize,
312     };
313 
314     BejDecoderJson decoder;
315     EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
316                 bejErrorInvalidSize);
317 }
318 
TEST(BejDecoderSecurityTest,InvalidAnnotationDictionarySize)319 TEST(BejDecoderSecurityTest, InvalidAnnotationDictionarySize)
320 {
321     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
322     ASSERT_TRUE(inputsOrErr);
323 
324     BejDictionaries dictionaries = {
325         .schemaDictionary = inputsOrErr->schemaDictionary,
326         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
327         .annotationDictionary = inputsOrErr->annotationDictionary,
328         .annotationDictionarySize = 10,
329         .errorDictionary = inputsOrErr->errorDictionary,
330         .errorDictionarySize = inputsOrErr->errorDictionarySize,
331     };
332 
333     BejDecoderJson decoder;
334     EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
335                 bejErrorInvalidSize);
336 }
337 
TEST(BejDecoderSecurityTest,InvalidErrorDictionarySize)338 TEST(BejDecoderSecurityTest, InvalidErrorDictionarySize)
339 {
340     auto inputsOrErr = loadInputs(dummySimpleTestFiles);
341     ASSERT_TRUE(inputsOrErr);
342 
343     BejDictionaries dictionaries = {
344         .schemaDictionary = inputsOrErr->schemaDictionary,
345         .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
346         .annotationDictionary = inputsOrErr->annotationDictionary,
347         .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
348         .errorDictionary = inputsOrErr->errorDictionary,
349         .errorDictionarySize = 10,
350     };
351 
352     BejDecoderJson decoder;
353     EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
354                 bejErrorInvalidSize);
355 }
356 
357 } // namespace libbej
358