package main import ( "path/filepath" "slices" "strings" "testing" "time" ) // The parsers that are handed bytes from outside: an id token, a claim, a URL // path, and a secret typed into a form. Each target states the property the // caller relies on, so the seed corpus runs with the rest of the tests and a // longer search is one flag away. // splitJWT takes a token apart before anything is known about it, and // verifySignature is then handed whatever came out. func FuzzSplitJWT(f *testing.F) { f.Add("aGVhZGVy.cGF5bG9hZA.c2ln") f.Add("...") f.Add("") f.Fuzz(func(t *testing.T, token string) { signed, sig, kid, alg, _, err := splitJWT(token) if err != nil { return } if verifySignature(jwk{Kty: "RSA", N: kid, E: "AQAB"}, alg, signed, sig) == nil { t.Fatalf("a made up rsa key verified %q", token) } if verifySignature(jwk{Kty: "EC", Crv: "P-256", X: kid, Y: kid}, alg, signed, sig) == nil { t.Fatalf("a made up ec key verified %q", token) } }) } // The name becomes a key in repos.json and a component of a URL, out of a // claim from somewhere else. func FuzzSanitizeName(f *testing.F) { f.Add("Carol") f.Add("../../config") f.Fuzz(func(t *testing.T, in string) { if out := sanitizeName(in); out != "" && !localNameRE.MatchString(out) { t.Fatalf("sanitizeName(%q) = %q, which is not a usable account name", in, out) } }) } // Whatever this lets through is handed to both CGIs in an environment // variable. func FuzzServablePath(f *testing.F) { f.Add("/notes/log/") f.Add("/a/../b") f.Fuzz(func(t *testing.T, p string) { if !servablePath(p) { return } if hasControlByte(p) { t.Fatalf("servablePath(%q) accepted a control byte", p) } parts := strings.Split(strings.TrimPrefix(p, "/"), "/") for i, part := range parts { if part == "." || part == ".." || (part == "" && i < len(parts)-1) { t.Fatalf("servablePath(%q) accepted a %q component", p, part) } } }) } // Whatever this lets through is joined onto the repos directory, for the // repository and for both of its siblings. func FuzzValidRepoPath(f *testing.F) { f.Add("infra/dns") f.Add("../etc") f.Fuzz(func(t *testing.T, p string) { if !validRepoPath(p) { return } for _, suffix := range []string{".git", ".issues.git", ".wiki.git"} { joined := filepath.Join("/repos", p+suffix) if joined != filepath.Clean(joined) || !strings.HasPrefix(joined, "/repos/") { t.Fatalf("validRepoPath(%q) accepted a path that lands on %q", p, joined) } if strings.Count(strings.TrimPrefix(joined, "/repos/"), "/") > 1 { t.Fatalf("validRepoPath(%q) accepted more than one section deep", p) } } }) } // A page listed in the index has to open the file it was listed from. A push // can put any name in the tree. func FuzzWikiNameRoundTrip(f *testing.F) { f.Add("docs/index.md") f.Add("myindex.md") f.Add(".md") f.Fuzz(func(t *testing.T, file string) { page := wikiPage(file) files, ok := wikiFiles(page) if !ok || !slices.Contains(files, file) { return // not listed, so never linked to } if wikiPage(files[0]) != page { t.Fatalf("%q is listed as %q, which reads back as %q", file, page, wikiPage(files[0])) } }) } // The secret being enrolled arrives in a form, so this decodes whatever was // typed. func FuzzTOTPSecret(f *testing.F) { f.Add("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ") f.Add("!!!") f.Fuzz(func(t *testing.T, secret string) { code := totpCode(secret, time.Unix(1234567890, 0)) if code != "" && len(code) != totpDigits { t.Fatalf("totpCode(%q) = %q", secret, code) } if code == "" && totpStep(secret, "", time.Unix(1234567890, 0)) >= 0 { t.Fatalf("an unreadable secret accepted an empty code") } }) }