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) } }) } }