-
Notifications
You must be signed in to change notification settings - Fork 12
test: add go fuzz tests for key internal functions #868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shaoboon
wants to merge
1
commit into
main
Choose a base branch
from
sb_go_fuzz
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # Fuzz Testing | ||
|
|
||
| ## Overview | ||
|
|
||
| Console employs two complementary fuzzing strategies: | ||
|
|
||
| 1. **Internal function fuzzing** — Go's built-in `go test -fuzz` framework targets parsing, transformation, validation, and cryptographic functions at the unit level. | ||
| 2. **API fuzzing** — *(planned)* HTTP-level fuzzing of Console's REST API endpoints. | ||
|
|
||
| --- | ||
|
|
||
| ## Internal Function Fuzzing | ||
|
|
||
| Uses Go's native fuzzing to test internal functions for crash resistance, determinism, and input-safety. There are **17 fuzz targets** across **11 test files** covering four categories: JSON/DTO validation, use-case DTO↔entity transforms, cryptographic parsing, and string parsing. | ||
|
|
||
| ### Coverage Summary | ||
|
|
||
| #### 1. JSON Deserialization & DTO Validation (7 targets) | ||
|
|
||
| All targets live in a single file and share a generic helper `fuzzJSONAndValidate[T]` that verifies `json.Unmarshal` determinism, `reflect.DeepEqual` consistency, and struct-level validator stability. | ||
|
|
||
| | Target | DTO Type | Validators Exercised | File | | ||
| |--------|----------|----------------------|------| | ||
| | `FuzzDeviceJSONProcessing` | `Device` | default | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
| | `FuzzProfileJSONProcessing` | `Profile` | `ValidateAMTPassOrGenRan`, `ValidateCIRAOrTLS`, `ValidateWiFiDHCP` | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
| | `FuzzDomainJSONProcessing` | `Domain` | `ValidateAlphaNumHyphenUnderscore` | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
| | `FuzzCIRAConfigJSONProcessing` | `CIRAConfig` | default | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
| | `FuzzWirelessConfigJSONProcessing` | `WirelessConfig` | `ValidateAuthandIEEE` | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
| | `FuzzIEEE8021xJSONProcessing` | `IEEE8021xConfig` | `AuthProtocolValidator` | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
| | `FuzzProfileWiFiJSONProcessing` | `ProfileWiFiConfigs` | default | `internal/entity/dto/v1/json_fuzz_test.go` | | ||
|
|
||
| **What is tested:** arbitrary JSON payloads (valid, malformed, deeply nested, oversized, unicode/null-byte) are deserialized twice and validated twice. Failures in determinism, panics, or mismatched validation results are caught. | ||
|
|
||
| #### 2. Use-Case DTO↔Entity Transform Round-Trips (7 targets) | ||
|
|
||
| Each target fuzzes both `dtoToEntity` and `entityToDTO` in its respective use-case package. Cryptographic dependencies are satisfied with mock implementations. All targets assert determinism via dual invocation and `reflect.DeepEqual`. | ||
|
|
||
| | Target | Package | Functions Under Test | File | | ||
| |--------|---------|----------------------|------| | ||
| | `FuzzCIRAConfigTransforms` | `ciraconfigs` | `dtoToEntity`, `entityToDTO` | `internal/usecase/ciraconfigs/transform_fuzz_test.go` | | ||
| | `FuzzDeviceTransforms` | `devices` | `dtoToEntity`, `entityToDTO` + GUID lowercasing, cert-hash nil check | `internal/usecase/devices/transform_fuzz_test.go` | | ||
| | `FuzzDomainTransforms` | `domains` | `dtoToEntity`, `entityToDTO` + RFC3339 expiration parsing, password encryption | `internal/usecase/domains/transform_fuzz_test.go` | | ||
| | `FuzzIEEE8021xConfigTransforms` | `ieee8021xconfigs` | `dtoToEntity`, `entityToDTO` | `internal/usecase/ieee8021xconfigs/transform_fuzz_test.go` | | ||
| | `FuzzProfileTransforms` | `profiles` | `dtoToEntity`, `entityToDTO` + tag join consistency | `internal/usecase/profiles/transform_fuzz_test.go` | | ||
| | `FuzzProfileWiFiConfigTransforms` | `profilewificonfigs` | `dtoToEntity`, `entityToDTO` | `internal/usecase/profilewificonfigs/transform_fuzz_test.go` | | ||
| | `FuzzWirelessConfigTransforms` | `wificonfigs` | `dtoToEntity`, `entityToDTO` + link-policy nil handling | `internal/usecase/wificonfigs/transform_fuzz_test.go` | | ||
|
|
||
| **What is tested:** fuzzed field values (strings, ints, bools, timestamps, oversized/unicode/null-byte data) are passed through transform functions. Panics, non-deterministic results, and invariant violations (e.g. GUID not lowercased, nil pointer where expected) are caught. | ||
|
|
||
| #### 3. Cryptographic Parsing (2 targets) | ||
|
|
||
| | Target | Package | Function Under Test | File | | ||
| |--------|---------|---------------------|------| | ||
| | `FuzzParseCertificateFromPEM` | `certificates` | `ParseCertificateFromPEM` | `internal/certificates/generate_fuzz_test.go` | | ||
| | `FuzzDecryptAndCheckCertExpiration` | `domains` | `DecryptAndCheckCertExpiration` | `internal/usecase/domains/usecase_fuzz_test.go` | | ||
|
|
||
| **What is tested:** | ||
| - `FuzzParseCertificateFromPEM`: valid, truncated, corrupted, swapped, and random PEM cert+key pairs. Asserts determinism, no data-alongside-error, no nil-without-error, and serial/key consistency. | ||
| - `FuzzDecryptAndCheckCertExpiration`: valid, expired, corrupted, and random base64-encoded PKCS12 blobs with varied passwords. Asserts determinism, no expired-cert-without-error, and byte-level certificate consistency. | ||
|
|
||
| #### 4. String Parsing (1 target) | ||
|
|
||
| | Target | Package | Function Under Test | File | | ||
| |--------|---------|---------------------|------| | ||
| | `FuzzParseInterval` | `devices` | `ParseInterval` (ISO 8601 duration → minutes) | `internal/usecase/devices/alarms_fuzz_test.go` | | ||
shaoboon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| **What is tested:** arbitrary strings (empty, malformed, oversized, unicode/control-character, and valid ISO 8601 durations) are parsed twice. Asserts determinism, no panics, and consistent error/result pairs across invocations. | ||
|
|
||
| --- | ||
|
|
||
| ### Seed Corpus Strategy | ||
|
|
||
| All targets use explicit `f.Add()` seeds covering: | ||
| - **Happy path:** well-formed, realistic inputs. | ||
| - **Empty/zero:** empty strings, zero ints, nil-equivalent bools. | ||
| - **Unicode & control characters:** CJK, emoji, null bytes (`\x00`), newlines. | ||
| - **Oversized inputs:** strings up to 4 KB, arrays with 4096 elements. | ||
| - **Type confusion:** wrong JSON types (number where string expected, etc.). | ||
| - **Boundary values:** `int` min/max, year-zero and year-9999 timestamps, port 65535. | ||
| - **Crypto edge cases:** truncated PEM, corrupted base64, swapped cert/key, wrong passwords, expired certificates. | ||
|
|
||
| --- | ||
|
|
||
| ### Running Internal Fuzz Tests | ||
|
|
||
| #### Prerequisites | ||
|
|
||
| ```sh | ||
| cp .env.example .env # Makefile includes .env | ||
| ``` | ||
|
|
||
| #### Make Targets | ||
|
|
||
| ```sh | ||
| # List all 17 fuzz targets | ||
| make fuzz-list | ||
|
|
||
| # Run a single target (recommended for local dev) | ||
| make fuzz-one PKG=./internal/usecase/devices TARGET=FuzzParseInterval FUZZTIME=30s | ||
|
|
||
| # Quick smoke: run every target once (seed corpus only) | ||
| make fuzz-smoke | ||
|
|
||
| # Full run: every target sequentially with time budget | ||
| make fuzz-all FUZZTIME=2m | ||
| ``` | ||
|
|
||
| #### Direct go test (single target) | ||
|
|
||
| ```sh | ||
| go test ./internal/usecase/devices -run='^$' -fuzz='^FuzzParseInterval$' -fuzztime=30s | ||
| ``` | ||
|
|
||
| > **Note:** Go requires `-fuzz` to match exactly one fuzz function per package. Packages with multiple targets (e.g. `internal/entity/dto/v1` has 7) must be run one target at a time. The `make fuzz-all` target handles this automatically. | ||
|
|
||
| #### CI Usage | ||
|
|
||
| | Trigger | Command | Purpose | | ||
| |---------|---------|---------| | ||
| | Pull request | `make fuzz-smoke` | Replay seed corpus, catch regressions | | ||
| | Nightly/weekly schedule | `make fuzz-all FUZZTIME=2m` | Discover new crashes with mutation | | ||
|
|
||
| --- | ||
|
|
||
| ### File Index | ||
|
|
||
| | File | Targets | Package | | ||
| |------|---------|---------| | ||
| | `internal/certificates/generate_fuzz_test.go` | 1 | `certificates` | | ||
| | `internal/entity/dto/v1/json_fuzz_test.go` | 7 | `dto` | | ||
| | `internal/usecase/ciraconfigs/transform_fuzz_test.go` | 1 | `ciraconfigs` | | ||
| | `internal/usecase/devices/alarms_fuzz_test.go` | 1 | `devices` | | ||
shaoboon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| | `internal/usecase/devices/transform_fuzz_test.go` | 1 | `devices` | | ||
| | `internal/usecase/domains/transform_fuzz_test.go` | 1 | `domains` | | ||
| | `internal/usecase/domains/usecase_fuzz_test.go` | 1 | `domains` | | ||
| | `internal/usecase/ieee8021xconfigs/transform_fuzz_test.go` | 1 | `ieee8021xconfigs` | | ||
| | `internal/usecase/profiles/transform_fuzz_test.go` | 1 | `profiles` | | ||
| | `internal/usecase/profilewificonfigs/transform_fuzz_test.go` | 1 | `profilewificonfigs` | | ||
| | `internal/usecase/wificonfigs/transform_fuzz_test.go` | 1 | `wificonfigs` | | ||
| | **Total** | **17** | **9 packages** | | ||
|
|
||
| --- | ||
|
|
||
| ## API Fuzzing | ||
|
|
||
| *Planned — this section will document HTTP-level fuzz testing of Console's REST API endpoints.* | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package certificates | ||
|
|
||
| import ( | ||
| "crypto/rsa" | ||
| "crypto/x509" | ||
| "crypto/x509/pkix" | ||
| "encoding/pem" | ||
| "math/big" | ||
| mathrand "math/rand" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| func FuzzParseCertificateFromPEM(f *testing.F) { | ||
| validCertPEM, validKeyPEM := generateFuzzPEMCertificate(f, 1) | ||
| otherCertPEM, otherKeyPEM := generateFuzzPEMCertificate(f, 2) | ||
| truncatedCertPEM := validCertPEM[:len(validCertPEM)/2] | ||
| truncatedKeyPEM := validKeyPEM[:len(validKeyPEM)/2] | ||
| corruptedCertPEM := corruptPEMBody(validCertPEM) | ||
| corruptedKeyPEM := corruptPEMBody(validKeyPEM) | ||
|
|
||
shaoboon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| seedInputs := []struct { | ||
| certPEM string | ||
| keyPEM string | ||
| }{ | ||
| {certPEM: validCertPEM, keyPEM: validKeyPEM}, | ||
| {certPEM: otherCertPEM, keyPEM: otherKeyPEM}, | ||
| {certPEM: validCertPEM, keyPEM: otherKeyPEM}, | ||
| {certPEM: truncatedCertPEM, keyPEM: validKeyPEM}, | ||
| {certPEM: validCertPEM, keyPEM: truncatedKeyPEM}, | ||
| {certPEM: corruptedCertPEM, keyPEM: validKeyPEM}, | ||
| {certPEM: validCertPEM, keyPEM: corruptedKeyPEM}, | ||
| {certPEM: "", keyPEM: ""}, | ||
| {certPEM: "invalid-pem", keyPEM: validKeyPEM}, | ||
| {certPEM: invalidBase64PEM("CERTIFICATE"), keyPEM: validKeyPEM}, | ||
| {certPEM: validCertPEM, keyPEM: "invalid-key"}, | ||
| {certPEM: validCertPEM, keyPEM: invalidBase64PEM("RSA PRIVATE KEY")}, | ||
| {certPEM: validKeyPEM, keyPEM: validCertPEM}, | ||
| {certPEM: "前置\n" + validCertPEM + "後置", keyPEM: "🔐\n" + validKeyPEM}, | ||
| {certPEM: strings.Repeat("A", 256), keyPEM: strings.Repeat("B", 256)}, | ||
| } | ||
|
|
||
| for _, input := range seedInputs { | ||
| f.Add(input.certPEM, input.keyPEM) | ||
| } | ||
|
|
||
| f.Fuzz(func(t *testing.T, certPEM, keyPEM string) { | ||
| firstCert, firstKey, firstErr := ParseCertificateFromPEM(certPEM, keyPEM) | ||
| secondCert, secondKey, secondErr := ParseCertificateFromPEM(certPEM, keyPEM) | ||
|
|
||
| if (firstErr == nil) != (secondErr == nil) { | ||
| t.Fatalf("ParseCertificateFromPEM error mismatch for cert len=%d key len=%d: first=%v second=%v", len(certPEM), len(keyPEM), firstErr, secondErr) | ||
| } | ||
|
|
||
| if firstErr != nil { | ||
| verifyParseCertError(t, certPEM, keyPEM, firstErr, secondErr, firstCert, secondCert, firstKey, secondKey) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| verifyParseCertSuccess(t, certPEM, keyPEM, firstCert, secondCert, firstKey, secondKey) | ||
| }) | ||
| } | ||
|
|
||
| func verifyParseCertError(t *testing.T, certPEM, keyPEM string, firstErr, secondErr error, firstCert, secondCert *x509.Certificate, firstKey, secondKey *rsa.PrivateKey) { | ||
| t.Helper() | ||
|
|
||
| if firstErr.Error() != secondErr.Error() { | ||
| t.Fatalf("ParseCertificateFromPEM error text mismatch for cert len=%d key len=%d: first=%q second=%q", len(certPEM), len(keyPEM), firstErr.Error(), secondErr.Error()) | ||
| } | ||
|
|
||
| if firstCert != nil || secondCert != nil || firstKey != nil || secondKey != nil { | ||
| t.Fatalf("ParseCertificateFromPEM returned parsed data alongside an error for cert len=%d key len=%d", len(certPEM), len(keyPEM)) | ||
| } | ||
| } | ||
|
|
||
| func verifyParseCertSuccess(t *testing.T, certPEM, keyPEM string, firstCert, secondCert *x509.Certificate, firstKey, secondKey *rsa.PrivateKey) { | ||
| t.Helper() | ||
|
|
||
| if firstCert == nil || secondCert == nil || firstKey == nil || secondKey == nil { | ||
| t.Fatalf("ParseCertificateFromPEM returned nil data without an error for cert len=%d key len=%d", len(certPEM), len(keyPEM)) | ||
| } | ||
|
|
||
| if firstCert.SerialNumber.Cmp(secondCert.SerialNumber) != 0 { | ||
| t.Fatalf("ParseCertificateFromPEM serial number mismatch for cert len=%d key len=%d", len(certPEM), len(keyPEM)) | ||
| } | ||
|
|
||
| if firstCert.Subject.CommonName != secondCert.Subject.CommonName { | ||
| t.Fatalf("ParseCertificateFromPEM common name mismatch for cert len=%d key len=%d: first=%q second=%q", len(certPEM), len(keyPEM), firstCert.Subject.CommonName, secondCert.Subject.CommonName) | ||
| } | ||
|
|
||
| if firstKey.E != secondKey.E || firstKey.N.Cmp(secondKey.N) != 0 || firstKey.D.Cmp(secondKey.D) != 0 { | ||
| t.Fatalf("ParseCertificateFromPEM returned different private keys for cert len=%d key len=%d", len(certPEM), len(keyPEM)) | ||
| } | ||
| } | ||
|
|
||
| func invalidBase64PEM(blockType string) string { | ||
| return "-----BEGIN " + blockType + "-----\n!!!!\n-----END " + blockType + "-----\n" | ||
| } | ||
|
|
||
| func corruptPEMBody(input string) string { | ||
| block, _ := pem.Decode([]byte(input)) | ||
| if block == nil || len(block.Bytes) == 0 { | ||
| return input | ||
| } | ||
|
|
||
| corrupted := append([]byte(nil), block.Bytes...) | ||
| index := len(corrupted) / 2 | ||
| corrupted[index] ^= 0xff | ||
|
|
||
| return string(pem.EncodeToMemory(&pem.Block{Type: block.Type, Bytes: corrupted})) | ||
| } | ||
|
|
||
| func generateFuzzPEMCertificate(tb testing.TB, seed int64) (certPEM, keyPEM string) { | ||
| tb.Helper() | ||
|
|
||
| rng := mathrand.New(mathrand.NewSource(seed)) | ||
|
|
||
| privateKey, err := rsa.GenerateKey(rng, 1024) | ||
| if err != nil { | ||
| tb.Fatalf("failed to generate RSA key: %v", err) | ||
| } | ||
|
|
||
| serialNumber := new(big.Int).SetInt64(rng.Int63()) | ||
|
|
||
| fixedTime := time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC) | ||
|
|
||
| template := x509.Certificate{ | ||
| SerialNumber: serialNumber, | ||
| Subject: pkix.Name{ | ||
| CommonName: "fuzz-cert", | ||
| Organization: []string{"console"}, | ||
| }, | ||
| NotBefore: fixedTime.Add(-time.Hour), | ||
| NotAfter: fixedTime.Add(24 * time.Hour), | ||
| KeyUsage: x509.KeyUsageDigitalSignature, | ||
| BasicConstraintsValid: true, | ||
| } | ||
|
|
||
| certBytes, err := x509.CreateCertificate(rng, &template, &template, &privateKey.PublicKey, privateKey) | ||
| if err != nil { | ||
| tb.Fatalf("failed to create certificate: %v", err) | ||
| } | ||
|
|
||
| certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})) | ||
| keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})) | ||
|
|
||
| return certPEM, keyPEM | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.