Files
ThothII/tools/tht/internal/authprojection/format_test.go
T

244 lines
7.7 KiB
Go

package authprojection
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
)
func TestNewSnapshotUsesDomainSeparatedGeneration(t *testing.T) {
got, err := NewSnapshot("local", []byte("auth\n"), []byte("users\n"))
if err != nil {
t.Fatal(err)
}
auth := sha256.Sum256([]byte("auth\n"))
users := sha256.Sum256([]byte("users\n"))
record := fmt.Sprintf("thothii-auth-projection-v1\nmode=local\nauth=%x\nusers=%x\n", auth, users)
generation := sha256.Sum256([]byte(record))
if got.Generation != hex.EncodeToString(generation[:]) {
t.Fatalf("generation = %q", got.Generation)
}
if got.CanonicalRevision != "sha256:"+got.Generation {
t.Fatalf("revision = %q", got.CanonicalRevision)
}
if string(got.Auth) != "auth\n" || string(got.Users) != "users\n" {
t.Fatal("snapshot did not preserve supplied bytes")
}
}
func TestNewSnapshotRejectsInvalidModeAndUsersShape(t *testing.T) {
for _, tc := range []struct {
mode string
users []byte
}{
{mode: "unknown", users: nil},
{mode: "local", users: nil},
{mode: "oidc", users: []byte("unexpected")},
} {
if _, err := NewSnapshot(tc.mode, []byte("auth"), tc.users); !errors.Is(err, ErrIntegrity) {
t.Fatalf("NewSnapshot(%q) error = %v", tc.mode, err)
}
}
}
func TestNewSnapshotCopiesAndBoundsInput(t *testing.T) {
auth := []byte("auth")
users := []byte("users")
got, err := NewSnapshot("local", auth, users)
if err != nil {
t.Fatal(err)
}
auth[0] = 'x'
users[0] = 'x'
if string(got.Auth) != "auth" || string(got.Users) != "users" {
t.Fatal("snapshot aliases caller buffers")
}
if _, err := NewSnapshot("oidc", make([]byte, maximumAuthBytes+1), nil); !errors.Is(err, ErrIntegrity) {
t.Fatalf("oversized auth error = %v", err)
}
if _, err := NewSnapshot("local", []byte("auth"), make([]byte, maximumUsersBytes+1)); !errors.Is(err, ErrIntegrity) {
t.Fatalf("oversized users error = %v", err)
}
}
func TestSelectorStrictlyDecodesReadyAndBlocked(t *testing.T) {
tx := strings.Repeat("a", 32)
gen := strings.Repeat("b", 64)
previous := strings.Repeat("c", 64)
for _, tc := range []struct {
name string
body string
want Selector
}{
{
name: "ready",
body: fmt.Sprintf(`{"version":1,"state":"ready","transaction":"%s","generation":"%s","previousGenerations":["%s"]}`, tx, gen, previous),
want: Selector{Version: 1, State: "ready", Transaction: tx, Generation: gen, PreviousGenerations: []string{previous}},
},
{
name: "blocked",
body: fmt.Sprintf(`{"version":1,"state":"blocked","transaction":"%s"}`, tx),
want: Selector{Version: 1, State: "blocked", Transaction: tx},
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := decodeSelector(append([]byte(tc.body), '\n'))
if err != nil {
t.Fatal(err)
}
if fmt.Sprintf("%#v", got) != fmt.Sprintf("%#v", tc.want) {
t.Fatalf("selector = %#v, want %#v", got, tc.want)
}
encoded, err := encodeSelector(got)
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(string(encoded), "\n") || strings.Count(string(encoded), "\n") != 1 {
t.Fatalf("encoded selector is not canonical newline-terminated JSON: %q", encoded)
}
})
}
}
func TestSelectorRejectsDuplicatesUnknownsAndInvalidIdentifiers(t *testing.T) {
tx := strings.Repeat("a", 32)
gen := strings.Repeat("b", 64)
cases := []string{
fmt.Sprintf(`{"version":1,"version":1,"state":"blocked","transaction":"%s"}`, tx),
fmt.Sprintf(`{"version":1,"state":"blocked","transaction":"%s","extra":true}`, tx),
fmt.Sprintf(`{"version":1,"state":"ready","transaction":"%s","generation":"%s","previousGenerations":["%s","%s"]}`, tx, gen, gen, gen),
fmt.Sprintf(`{"version":1,"state":"ready","transaction":"%s","generation":"%s","previousGenerations":["%s","%s","%s"]}`, tx, gen, strings.Repeat("c", 64), strings.Repeat("d", 64), strings.Repeat("e", 64)),
fmt.Sprintf(`{"version":1,"state":"ready","transaction":"%s","generation":"%s"}`, strings.Repeat("A", 32), gen),
fmt.Sprintf(`{"version":1,"state":"blocked","transaction":"%s","generation":"%s"}`, tx, gen),
fmt.Sprintf(`{"version":1,"state":"ready","transaction":"%s","generation":"%s"} trailing`, tx, gen),
}
for _, body := range cases {
if _, err := decodeSelector([]byte(body)); !errors.Is(err, ErrIntegrity) {
t.Fatalf("decodeSelector(%q) error = %v", body, err)
}
}
}
func TestManifestStrictlyValidatesExactModeDependentFiles(t *testing.T) {
snapshot, err := NewSnapshot("local", []byte("auth"), []byte("users"))
if err != nil {
t.Fatal(err)
}
manifest, err := newManifest(snapshot)
if err != nil {
t.Fatal(err)
}
encoded, err := encodeManifest(manifest)
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(string(encoded), "\n") || strings.Count(string(encoded), "\n") != 1 {
t.Fatalf("manifest is not canonical newline-terminated JSON: %q", encoded)
}
got, err := decodeManifest(encoded)
if err != nil {
t.Fatal(err)
}
if err := validateManifest(got, snapshot); err != nil {
t.Fatal(err)
}
bad := strings.Replace(string(encoded), `"users.yaml"`, `"other.yaml"`, 1)
if _, err := decodeManifest([]byte(bad)); !errors.Is(err, ErrIntegrity) {
t.Fatalf("bad manifest error = %v", err)
}
}
func TestSelectorAndManifestRequireCanonicalNewlineTerminatedJSON(t *testing.T) {
selector := Selector{Version: SchemaVersion, State: "blocked", Transaction: strings.Repeat("a", 32)}
selectorData, err := encodeSelector(selector)
if err != nil {
t.Fatal(err)
}
if _, err := decodeSelector(bytes.TrimSuffix(selectorData, []byte("\n"))); !errors.Is(err, ErrIntegrity) {
t.Fatalf("noncanonical selector error = %v", err)
}
snapshot, err := NewSnapshot("oidc", []byte("auth"), nil)
if err != nil {
t.Fatal(err)
}
value, err := newManifest(snapshot)
if err != nil {
t.Fatal(err)
}
manifestData, err := encodeManifest(value)
if err != nil {
t.Fatal(err)
}
if _, err := decodeManifest(bytes.TrimSuffix(manifestData, []byte("\n"))); !errors.Is(err, ErrIntegrity) {
t.Fatalf("noncanonical manifest error = %v", err)
}
}
func TestManifestRejectsOIDCUsersAndSnapshotDigestOrSizeMismatch(t *testing.T) {
snapshot, err := NewSnapshot("oidc", []byte("auth"), nil)
if err != nil {
t.Fatal(err)
}
value, err := newManifest(snapshot)
if err != nil {
t.Fatal(err)
}
withUsers := value
withUsers.Files = append(withUsers.Files, manifestFile{Name: "users.yaml", Size: 1, SHA256: strings.Repeat("a", 64)})
data, err := json.Marshal(withUsers)
if err != nil {
t.Fatal(err)
}
if _, err := decodeManifest(append(data, '\n')); !errors.Is(err, ErrIntegrity) {
t.Fatalf("oidc users error = %v", err)
}
for _, mutate := range []func(*manifest){
func(value *manifest) { value.Files[0].Size++ },
func(value *manifest) { value.Files[0].SHA256 = strings.Repeat("a", 64) },
} {
changed := value
changed.Files = append([]manifestFile(nil), value.Files...)
mutate(&changed)
if err := validateManifest(changed, snapshot); !errors.Is(err, ErrIntegrity) {
t.Fatalf("changed manifest error = %v", err)
}
}
}
func TestManifestRejectsFileSizesAboveReadBounds(t *testing.T) {
for _, tc := range []struct {
name string
mode string
users []byte
file int
tooLarge int
}{
{name: "auth", mode: "oidc", file: 0, tooLarge: maximumAuthBytes + 1},
{name: "users", mode: "local", users: []byte("users"), file: 1, tooLarge: maximumUsersBytes + 1},
} {
t.Run(tc.name, func(t *testing.T) {
snapshot, err := NewSnapshot(tc.mode, []byte("auth"), tc.users)
if err != nil {
t.Fatal(err)
}
value, err := newManifest(snapshot)
if err != nil {
t.Fatal(err)
}
value.Files[tc.file].Size = tc.tooLarge
data, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
if _, err := decodeManifest(append(data, '\n')); !errors.Is(err, ErrIntegrity) {
t.Fatalf("oversized %s manifest error = %v", tc.name, err)
}
})
}
}