Skip to content

Commit 201ebbb

Browse files
committed
test: add fuzz test for metadata.go
Signed-off-by: Marvin Drees <[email protected]>
1 parent b6305fc commit 201ebbb

File tree

2 files changed

+782
-0
lines changed

2 files changed

+782
-0
lines changed

internal/testutils/helpers/fuzz.go

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
// Copyright 2024 The Update Framework Authors
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+
// SPDX-License-Identifier: Apache-2.0
16+
//
17+
18+
package helpers
19+
20+
import (
21+
"encoding/json"
22+
"fmt"
23+
"maps"
24+
"math/rand"
25+
"strings"
26+
"testing"
27+
"time"
28+
)
29+
30+
// FuzzDataGenerator provides utilities for generating fuzz test data
31+
type FuzzDataGenerator struct {
32+
rand *rand.Rand
33+
}
34+
35+
// NewFuzzDataGenerator creates a new fuzz data generator
36+
func NewFuzzDataGenerator(seed int64) *FuzzDataGenerator {
37+
return &FuzzDataGenerator{
38+
rand: rand.New(rand.NewSource(seed)),
39+
}
40+
}
41+
42+
// GenerateRandomString generates a random string of specified length
43+
func (f *FuzzDataGenerator) GenerateRandomString(length int) string {
44+
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
45+
b := make([]byte, length)
46+
for i := range b {
47+
b[i] = charset[f.rand.Intn(len(charset))]
48+
}
49+
return string(b)
50+
}
51+
52+
// GenerateRandomBytes generates random bytes of specified length
53+
func (f *FuzzDataGenerator) GenerateRandomBytes(length int) []byte {
54+
b := make([]byte, length)
55+
f.rand.Read(b)
56+
return b
57+
}
58+
59+
// GenerateRandomInt generates a random integer up to max
60+
func (f *FuzzDataGenerator) GenerateRandomInt(max int) int {
61+
return f.rand.Intn(max)
62+
}
63+
64+
// GenerateRandomJSON generates random JSON-like data for fuzzing
65+
func (f *FuzzDataGenerator) GenerateRandomJSON() []byte {
66+
data := map[string]any{
67+
"signed": map[string]any{
68+
"_type": f.GenerateRandomString(f.rand.Intn(20) + 1),
69+
"version": f.rand.Intn(1000),
70+
"spec_version": f.GenerateRandomString(10),
71+
"expires": time.Now().Add(time.Duration(f.rand.Intn(365*24)) * time.Hour).Format(time.RFC3339),
72+
},
73+
"signatures": []map[string]any{
74+
{
75+
"keyid": f.GenerateRandomString(64),
76+
"sig": f.GenerateRandomString(128),
77+
},
78+
},
79+
}
80+
81+
jsonData, _ := json.Marshal(data)
82+
return jsonData
83+
}
84+
85+
// GenerateCorruptedJSON generates various types of corrupted JSON for fuzzing
86+
func (f *FuzzDataGenerator) GenerateCorruptedJSON() []byte {
87+
corruptionTypes := []func() []byte{
88+
// Truncated JSON
89+
func() []byte {
90+
validJSON := f.GenerateRandomJSON()
91+
if len(validJSON) > 10 {
92+
return validJSON[:len(validJSON)/2]
93+
}
94+
return validJSON
95+
},
96+
// Invalid characters
97+
func() []byte {
98+
return []byte(strings.ReplaceAll(string(f.GenerateRandomJSON()), ":", f.GenerateRandomString(5)))
99+
},
100+
// Nested objects with random depths
101+
func() []byte {
102+
depth := f.rand.Intn(100) + 1
103+
json := "{"
104+
for i := range depth {
105+
json += fmt.Sprintf(`"level%d": {`, i)
106+
}
107+
json += `"value": "test"`
108+
for range depth {
109+
json += "}"
110+
}
111+
json += "}"
112+
return []byte(json)
113+
},
114+
// Very long strings
115+
func() []byte {
116+
longString := f.GenerateRandomString(f.rand.Intn(10000) + 1000)
117+
return fmt.Appendf([]byte{}, `{"long_string": "%s"}`, longString)
118+
},
119+
// Invalid Unicode
120+
func() []byte {
121+
return append([]byte(`{"test": "`), append(f.GenerateRandomBytes(50), []byte(`"}`)...)...)
122+
},
123+
}
124+
125+
corruptionFunc := corruptionTypes[f.rand.Intn(len(corruptionTypes))]
126+
return corruptionFunc()
127+
}
128+
129+
// FuzzMetadataOperations provides fuzz testing for metadata operations
130+
func FuzzMetadataOperations(f *testing.F, operation func(data []byte) error) {
131+
f.Helper()
132+
133+
// Add seed data
134+
generator := NewFuzzDataGenerator(time.Now().UnixNano())
135+
136+
// Add valid metadata as seeds
137+
f.Add(CreateTestRootJSON(&testing.T{}))
138+
f.Add(CreateTestTargetsJSON(&testing.T{}))
139+
f.Add(CreateTestSnapshotJSON(&testing.T{}))
140+
f.Add(CreateTestTimestampJSON(&testing.T{}))
141+
142+
// Add some corrupted data as seeds
143+
for range 10 {
144+
f.Add(generator.GenerateCorruptedJSON())
145+
}
146+
147+
// Add edge cases
148+
f.Add([]byte(""))
149+
f.Add([]byte("{}"))
150+
f.Add([]byte("null"))
151+
f.Add([]byte("[]"))
152+
153+
f.Fuzz(func(t *testing.T, data []byte) {
154+
// The operation should never panic, even with invalid input
155+
defer func() {
156+
if r := recover(); r != nil {
157+
t.Errorf("operation panicked with input %q: %v", string(data), r)
158+
}
159+
}()
160+
161+
// Execute the operation - errors are expected and acceptable
162+
_ = operation(data)
163+
})
164+
}
165+
166+
// FuzzJSONMarshaling tests JSON marshaling/unmarshaling with random data
167+
func FuzzJSONMarshaling(f *testing.F) {
168+
f.Helper()
169+
170+
// Add seed data
171+
f.Add(CreateTestRootJSON(&testing.T{}))
172+
f.Add([]byte(`{"test": "value"}`))
173+
f.Add([]byte(`{}`))
174+
175+
f.Fuzz(func(t *testing.T, data []byte) {
176+
defer func() {
177+
if r := recover(); r != nil {
178+
t.Errorf("JSON marshaling panicked with input %q: %v", string(data), r)
179+
}
180+
}()
181+
182+
var v any
183+
err := json.Unmarshal(data, &v)
184+
if err != nil {
185+
// Invalid JSON is expected, not an error
186+
return
187+
}
188+
189+
// If we can unmarshal, we should be able to marshal back
190+
_, err = json.Marshal(v)
191+
if err != nil {
192+
t.Errorf("failed to marshal back after unmarshal: %v", err)
193+
}
194+
})
195+
}
196+
197+
// FuzzStringOperations tests string operations with random input
198+
func FuzzStringOperations(f *testing.F, operation func(s string) error) {
199+
f.Helper()
200+
201+
generator := NewFuzzDataGenerator(time.Now().UnixNano())
202+
203+
// Add seed data
204+
f.Add("")
205+
f.Add("test")
206+
f.Add("1234567890")
207+
f.Add("special!@#$%^&*()chars")
208+
f.Add(strings.Repeat("a", 1000))
209+
210+
// Add random strings
211+
for range 5 {
212+
f.Add(generator.GenerateRandomString(100))
213+
}
214+
215+
f.Fuzz(func(t *testing.T, s string) {
216+
defer func() {
217+
if r := recover(); r != nil {
218+
t.Errorf("string operation panicked with input %q: %v", s, r)
219+
}
220+
}()
221+
222+
_ = operation(s)
223+
})
224+
}
225+
226+
// FuzzBytesOperations tests byte operations with random input
227+
func FuzzBytesOperations(f *testing.F, operation func(data []byte) error) {
228+
f.Helper()
229+
230+
generator := NewFuzzDataGenerator(time.Now().UnixNano())
231+
232+
// Add seed data
233+
f.Add([]byte(""))
234+
f.Add([]byte("test"))
235+
f.Add([]byte{0, 1, 2, 3, 255})
236+
237+
// Add random bytes
238+
for range 5 {
239+
f.Add(generator.GenerateRandomBytes(100))
240+
}
241+
242+
f.Fuzz(func(t *testing.T, data []byte) {
243+
defer func() {
244+
if r := recover(); r != nil {
245+
t.Errorf("bytes operation panicked with input %v: %v", data, r)
246+
}
247+
}()
248+
249+
_ = operation(data)
250+
})
251+
}
252+
253+
// GenerateRandomMetadataFields generates random values for metadata fields
254+
func (f *FuzzDataGenerator) GenerateRandomMetadataFields() map[string]any {
255+
return map[string]any{
256+
"version": f.rand.Intn(1000000),
257+
"spec_version": f.GenerateRandomString(f.rand.Intn(20) + 1),
258+
"expires": f.GenerateRandomTime().Format(time.RFC3339),
259+
"type": f.GenerateRandomString(f.rand.Intn(20) + 1),
260+
"length": f.rand.Intn(1000000),
261+
"hashes": map[string]string{
262+
"sha256": f.GenerateRandomString(64),
263+
"sha512": f.GenerateRandomString(128),
264+
},
265+
"keyids": []string{f.GenerateRandomString(64), f.GenerateRandomString(64)},
266+
"threshold": f.rand.Intn(10) + 1,
267+
"custom": map[string]any{
268+
"random_field": f.GenerateRandomString(100),
269+
"number": f.rand.Intn(1000),
270+
},
271+
}
272+
}
273+
274+
// GenerateRandomTime generates a random time within a reasonable range
275+
func (f *FuzzDataGenerator) GenerateRandomTime() time.Time {
276+
// Generate time between 2020 and 2030
277+
start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
278+
end := time.Date(2030, 12, 31, 23, 59, 59, 0, time.UTC)
279+
280+
duration := end.Sub(start)
281+
randomDuration := time.Duration(f.rand.Int63n(int64(duration)))
282+
283+
return start.Add(randomDuration)
284+
}
285+
286+
// GenerateRandomSignature generates a random signature structure
287+
func (f *FuzzDataGenerator) GenerateRandomSignature() map[string]any {
288+
return map[string]any{
289+
"keyid": f.GenerateRandomString(64),
290+
"sig": f.GenerateRandomString(f.rand.Intn(200) + 50),
291+
}
292+
}
293+
294+
// GenerateRandomKey generates a random key structure
295+
func (f *FuzzDataGenerator) GenerateRandomKey() map[string]any {
296+
keyTypes := []string{"ed25519", "rsa", "ecdsa", "unknown"}
297+
schemes := []string{"ed25519", "rsa-pss-sha256", "ecdsa-sha2-nistp256", "unknown"}
298+
299+
return map[string]any{
300+
"keytype": keyTypes[f.rand.Intn(len(keyTypes))],
301+
"scheme": schemes[f.rand.Intn(len(schemes))],
302+
"keyval": map[string]any{
303+
"public": f.GenerateRandomString(f.rand.Intn(500) + 50),
304+
},
305+
}
306+
}
307+
308+
// CreateFuzzTestMetadata creates various metadata structures for fuzz testing
309+
func (f *FuzzDataGenerator) CreateFuzzTestMetadata(metadataType string) []byte {
310+
base := map[string]any{
311+
"signed": map[string]any{
312+
"_type": metadataType,
313+
},
314+
"signatures": []any{
315+
f.GenerateRandomSignature(),
316+
},
317+
}
318+
319+
// Add type-specific fields
320+
signed := base["signed"].(map[string]any)
321+
fields := f.GenerateRandomMetadataFields()
322+
maps.Copy(signed, fields)
323+
324+
// Add type-specific structures
325+
switch metadataType {
326+
case "root":
327+
signed["keys"] = map[string]any{
328+
f.GenerateRandomString(64): f.GenerateRandomKey(),
329+
}
330+
signed["roles"] = map[string]any{
331+
"root": map[string]any{
332+
"keyids": []string{f.GenerateRandomString(64)},
333+
"threshold": f.rand.Intn(5) + 1,
334+
},
335+
}
336+
signed["consistent_snapshot"] = f.rand.Intn(2) == 1
337+
338+
case "targets":
339+
signed["targets"] = map[string]any{
340+
f.GenerateRandomString(20): fields["hashes"],
341+
}
342+
343+
case "snapshot":
344+
signed["meta"] = map[string]any{
345+
"targets.json": map[string]any{
346+
"version": f.rand.Intn(1000),
347+
"hashes": fields["hashes"],
348+
"length": f.rand.Intn(10000),
349+
},
350+
}
351+
352+
case "timestamp":
353+
signed["meta"] = map[string]any{
354+
"snapshot.json": map[string]any{
355+
"version": f.rand.Intn(1000),
356+
"hashes": fields["hashes"],
357+
"length": f.rand.Intn(10000),
358+
},
359+
}
360+
}
361+
362+
jsonData, _ := json.Marshal(base)
363+
return jsonData
364+
}

0 commit comments

Comments
 (0)