Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Fuzz

on:
schedule:
- cron: '23 3 * * *'
workflow_dispatch:

permissions:
contents: read

jobs:
fuzz:
name: Fuzz ${{ matrix.target }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- target: FuzzApplyTransforms
package: ./internal/controller
- target: FuzzHashSecretData
package: ./internal/controller
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod

- name: Fuzz
run: go test -run='^$' -fuzz="^${{ matrix.target }}\$" -fuzztime=300s ${{ matrix.package }}

- name: Failing inputs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fuzz-testdata-${{ matrix.target }}
path: internal/controller/testdata/fuzz
if-no-files-found: ignore
retention-days: 30
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ make fmt
# Unit tests
make test

# Fuzz a target (see .github/workflows/fuzz.yml for the list)
go test -run='^$' -fuzz=FuzzApplyTransforms -fuzztime=60s ./internal/controller

# Integration tests (envtest)
make test-integration

Expand Down
11 changes: 9 additions & 2 deletions cli/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ fn parse_pin(s: &str) -> Result<[u8; 32]> {
}

fn hex_decode(s: &str) -> Result<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return Err(anyhow!("odd hex length"));
if !s.is_ascii() || !s.len().is_multiple_of(2) {
return Err(anyhow!("hex must be an even number of ASCII characters"));
}
(0..s.len())
.step_by(2)
Expand Down Expand Up @@ -266,4 +266,11 @@ mod tests {
let res = parse_pin("deadbeef");
assert!(res.is_err());
}

#[test]
fn non_ascii_pin_is_an_error_not_a_panic() {
let pin = format!("a{}{}", "é", "a".repeat(61));
assert_eq!(pin.len(), 64);
assert!(parse_pin(&pin).is_err());
}
}
102 changes: 102 additions & 0 deletions internal/controller/transforms_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package controller

import (
"encoding/base64"
"errors"
"reflect"
"testing"

fvv1alpha1 "github.com/FerrLabs/FerrVault/api/ferrvault/v1alpha1"
)

var fuzzTransformTypes = []string{
TransformPrefix,
TransformSuffix,
TransformRename,
TransformBase64Decode,
TransformJSONExpand,
"unknown",
}

func FuzzApplyTransforms(f *testing.F) {
f.Add(uint8(4), "CONFIG", `{"a":{"b":1},"c":[1,2],"d":null,"e":true}`, "", "", "")
f.Add(uint8(3), "TOKEN", "c2VjcmV0", "", "", "")
f.Add(uint8(3), "TOKEN", "c2VjcmV0_-8=", "", "", "")
f.Add(uint8(0), "K", "v", "APP_", "", "")
f.Add(uint8(1), "K", "v", "_V2", "", "")
f.Add(uint8(2), "K", "v", "", "K", "K2")
f.Add(uint8(2), "K", "v", "", "K", "K")
f.Add(uint8(4), "CONFIG", `{"x":1e400}`, "", "", "")

f.Fuzz(func(t *testing.T, kind uint8, key, val, affix, from, to string) {
typ := fuzzTransformTypes[int(kind)%len(fuzzTransformTypes)]
Comment thread
BryanFRD marked this conversation as resolved.
Resolved
if key == "OTHER" {
t.Skip()
}
input := map[string]string{key: val, "OTHER": "o"}
snapshot := map[string]string{key: val, "OTHER": "o"}
transforms := []fvv1alpha1.SecretTransform{
{Type: typ, Value: affix, From: from, To: to, Key: key, Keys: []string{key}},
}

out, err := ApplyTransforms(input, transforms)
if !reflect.DeepEqual(input, snapshot) {
t.Fatalf("input mutated: before %q after %q", snapshot, input)
}
if err != nil {
if te := (*TransformError)(nil); !errors.As(err, &te) {
t.Fatalf("error is %T, want *TransformError", err)
}
}

out2, err2 := ApplyTransforms(input, transforms)
if (err == nil) != (err2 == nil) || !reflect.DeepEqual(out, out2) {
t.Fatalf("not deterministic: %q/%v vs %q/%v", out, err, out2, err2)
}

if err != nil {
return
}
switch typ {
case TransformPrefix:
if got := out[affix+key]; got != val {
t.Fatalf("prefix lost value for %q: %q", key, got)
}
case TransformSuffix:
if got := out[key+affix]; got != val {
t.Fatalf("suffix lost value for %q: %q", key, got)
}
case TransformBase64Decode:
if want, ok := decodeEither(val); ok && out[key] != want {
t.Fatalf("base64Decode %q: got %q want %q", val, out[key], want)
}
}
})
}

func decodeEither(s string) (string, bool) {
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
return string(b), true
}
if b, err := base64.URLEncoding.DecodeString(s); err == nil {
return string(b), true
}
return "", false
}

func FuzzHashSecretData(f *testing.F) {
f.Add("a", "1", "b", "2")
f.Add("a\x00b", "c", "a", "b\x00c")
f.Add("", "", "", "")

f.Fuzz(func(t *testing.T, k1, v1, k2, v2 string) {
if k1 == k2 {
t.Skip()
}
a := map[string]string{k1: v1, k2: v2}
b := map[string]string{k2: v2, k1: v1}
if hashSecretData(a) != hashSecretData(b) {
t.Fatalf("hash depends on construction order for %q", a)
}
})
}
Loading