fix(auth): pin native auth storage operations

This commit is contained in:
2026-08-17 14:52:21 +02:00
parent 6d438f4c7e
commit cb0e873ed7
23 changed files with 3016 additions and 1385 deletions
@@ -167,6 +167,369 @@ func TestProtocolEnsuresTheCompletePrivateSessionLayout(t *testing.T) {
runRejected(t, request{Version: 1, Operation: "ensure-layout", Root: root, Directory: "sessions"})
}
func TestProtocolPinsTheOriginalPrivateRootBeforeEveryRecordMutation(t *testing.T) {
parent := privateTestRoot(t)
root := filepath.Join(parent, "auth")
replacement := filepath.Join(parent, "replacement")
moved := filepath.Join(parent, "auth-original")
filename := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.json"
for _, directory := range []string{root, replacement} {
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
t.Fatal(err)
}
for _, child := range []string{"sessions", "oidc"} {
if err := safeio.EnsurePrivateDirectory(filepath.Join(directory, child)); err != nil {
t.Fatal(err)
}
}
}
swapped := false
blocked := false
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "after-auth-root-open" || swapped || blocked {
return
}
if runtime.GOOS == "windows" {
if err := os.Rename(root, moved); err == nil {
t.Fatal("retained Windows root handle permitted rename")
}
blocked = true
return
}
if err := os.Rename(root, moved); err != nil {
t.Fatal(err)
}
if err := os.Rename(replacement, root); err != nil {
t.Fatal(err)
}
swapped = true
})
t.Cleanup(restore)
runRequest(t, request{
Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename,
ContentBase64: base64.StdEncoding.EncodeToString([]byte("pinned-root-record")),
})
if !swapped && !blocked {
t.Fatal("record operation did not expose the retained-root test hook")
}
if runtime.GOOS == "windows" {
if _, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(root, "sessions", filename), maximumSessionBytes); err != nil {
t.Fatalf("record missing from retained Windows root: %v", err)
}
return
}
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(moved, "sessions", filename), maximumSessionBytes)
if err != nil || string(contents) != "pinned-root-record" {
t.Fatalf("pinned root contents = %q error = %v", contents, err)
}
if _, err := os.Lstat(filepath.Join(root, "sessions", filename)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("replacement root was mutated: %v", err)
}
}
func TestProtocolPinsTheOriginalPrivateRootForEveryRecordOperation(t *testing.T) {
filename := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.json"
otherFilename := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.json"
for _, operation := range []string{
"create", "read", "replace", "remove", "list", "claim-consume", "read-claim", "remove-claim",
} {
t.Run(operation, func(t *testing.T) {
parent := privateTestRoot(t)
root := filepath.Join(parent, "auth")
replacement := filepath.Join(parent, "replacement")
moved := filepath.Join(parent, "auth-original")
for _, directory := range []string{root, replacement} {
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
t.Fatal(err)
}
for _, child := range []string{"sessions", "oidc"} {
if err := safeio.EnsurePrivateDirectory(filepath.Join(directory, child)); err != nil {
t.Fatal(err)
}
}
}
write := func(base, directory, name, contents string) {
t.Helper()
if err := safeio.WriteCanonicalNewPrivateFile(filepath.Join(base, directory, name), []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
pair := func(base, name, contents string) {
t.Helper()
write(base, "oidc", name, contents)
if err := os.Link(filepath.Join(base, "oidc", name), filepath.Join(base, "oidc", asClaimFilename(name))); err != nil {
t.Fatal(err)
}
}
input := request{Version: 1, Operation: operation, Root: root}
switch operation {
case "create":
input.Directory, input.Filename = "sessions", filename
input.ContentBase64 = base64.StdEncoding.EncodeToString([]byte("original-created"))
case "read":
write(root, "sessions", filename, "original-read")
write(replacement, "sessions", filename, "replacement-read")
input.Directory, input.Filename = "sessions", filename
case "replace":
write(root, "sessions", filename, "original-before")
write(replacement, "sessions", filename, "replacement-before")
input.Directory, input.Filename = "sessions", filename
input.ContentBase64 = base64.StdEncoding.EncodeToString([]byte("original-replaced"))
case "remove":
write(root, "sessions", filename, "original-remove")
write(replacement, "sessions", filename, "replacement-remove")
input.Directory, input.Filename = "sessions", filename
case "list":
write(root, "sessions", filename, "original-list")
write(replacement, "sessions", otherFilename, "replacement-list")
input.Directory = "sessions"
case "claim-consume":
write(root, "oidc", filename, "original-claim")
write(replacement, "oidc", filename, "replacement-claim")
input.Directory, input.Filename = "oidc", filename
case "read-claim", "remove-claim":
pair(root, filename, "original-pair")
pair(replacement, filename, "replacement-pair")
input.Directory, input.Filename = "oidc", filename
}
swapped := false
blocked := false
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "after-auth-root-open" || swapped || blocked {
return
}
if runtime.GOOS == "windows" {
if err := os.Rename(root, moved); err == nil {
t.Fatal("retained Windows root handle permitted rename")
}
blocked = true
return
}
if err := os.Rename(root, moved); err != nil {
t.Fatal(err)
}
if err := os.Rename(replacement, root); err != nil {
t.Fatal(err)
}
swapped = true
})
t.Cleanup(restore)
output := runRequest(t, input)
if !swapped && !blocked {
t.Fatal("record operation did not expose the retained-root test hook")
}
original := root
replacementRoot := replacement
if swapped {
original = moved
replacementRoot = root
}
switch operation {
case "create":
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(original, "sessions", filename), maximumSessionBytes)
if err != nil || string(contents) != "original-created" {
t.Fatalf("pinned create contents = %q error = %v", contents, err)
}
if _, err := os.Lstat(filepath.Join(replacementRoot, "sessions", filename)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("replacement root was mutated: %v", err)
}
case "read":
if !output.Found || decodeContent(t, output) != "original-read" {
t.Fatalf("pinned read = %#v", output)
}
case "replace":
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(original, "sessions", filename), maximumSessionBytes)
if err != nil || string(contents) != "original-replaced" {
t.Fatalf("pinned replace contents = %q error = %v", contents, err)
}
replacementContents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(replacementRoot, "sessions", filename), maximumSessionBytes)
if err != nil || string(replacementContents) != "replacement-before" {
t.Fatalf("replacement contents = %q error = %v", replacementContents, err)
}
case "remove":
if _, err := os.Lstat(filepath.Join(original, "sessions", filename)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("pinned remove left original record: %v", err)
}
if _, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(replacementRoot, "sessions", filename), maximumSessionBytes); err != nil {
t.Fatalf("replacement record was mutated: %v", err)
}
case "list":
if output.Entries == nil || len(*output.Entries) != 1 || (*output.Entries)[0].Name != filename {
t.Fatalf("pinned list = %#v", output)
}
case "claim-consume":
if !output.Found || decodeContent(t, output) != "original-claim" {
t.Fatalf("pinned claim consume = %#v", output)
}
if _, err := os.Lstat(filepath.Join(original, "oidc", filename)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("pinned claim source remains: %v", err)
}
if contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(replacementRoot, "oidc", filename), maximumOIDCStateBytes); err != nil || string(contents) != "replacement-claim" {
t.Fatalf("replacement claim contents = %q error = %v", contents, err)
}
case "read-claim":
if !output.Found || decodeContent(t, output) != "original-pair" {
t.Fatalf("pinned claim read = %#v", output)
}
case "remove-claim":
for _, name := range []string{filename, asClaimFilename(filename)} {
if _, err := os.Lstat(filepath.Join(original, "oidc", name)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("pinned claim remove left original %s: %v", name, err)
}
if _, err := os.Lstat(filepath.Join(replacementRoot, "oidc", name)); err != nil {
t.Fatalf("replacement claim pair was mutated: %v", err)
}
}
}
})
}
}
func TestProtocolPinsTheOriginalPrivateRootForLayoutCreation(t *testing.T) {
parent := privateTestRoot(t)
root := filepath.Join(parent, "auth")
replacement := filepath.Join(parent, "replacement")
moved := filepath.Join(parent, "auth-original")
for _, directory := range []string{root, replacement} {
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
t.Fatal(err)
}
}
swapped := false
blocked := false
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "after-auth-root-open" || swapped || blocked {
return
}
if runtime.GOOS == "windows" {
if err := os.Rename(root, moved); err == nil {
t.Fatal("retained Windows root handle permitted rename")
}
blocked = true
return
}
if err := os.Rename(root, moved); err != nil {
t.Fatal(err)
}
if err := os.Rename(replacement, root); err != nil {
t.Fatal(err)
}
swapped = true
})
t.Cleanup(restore)
runRequest(t, request{Version: 1, Operation: "ensure-layout", Root: root})
if !swapped && !blocked {
t.Fatal("layout creation did not expose the retained-root test hook")
}
original := root
replacementRoot := replacement
if swapped {
original = moved
replacementRoot = root
}
for _, child := range []string{"sessions", "oidc"} {
if err := safeio.ValidatePrivateDirectory(filepath.Join(original, child)); err != nil {
t.Fatalf("pinned %s child: %v", child, err)
}
if _, err := os.Lstat(filepath.Join(replacementRoot, child)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("replacement root received layout mutation: %v", err)
}
}
}
func TestProtocolValidateRootRechecksChildrenObservedMissingUnderTheRetainedRoot(t *testing.T) {
root := filepath.Join(privateTestRoot(t), "auth")
if err := safeio.EnsurePrivateDirectory(root); err != nil {
t.Fatal(err)
}
installedUnsafeChild := false
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "after-auth-layout-first-pass" || installedUnsafeChild {
return
}
if err := os.Mkdir(filepath.Join(root, "sessions"), 0o755); err != nil {
t.Fatal(err)
}
installedUnsafeChild = true
})
t.Cleanup(restore)
runRejected(t, request{Version: 1, Operation: "validate-root", Root: root})
if !installedUnsafeChild {
t.Fatal("layout validation did not expose the missing-child recheck hook")
}
}
func TestProtocolValidateLayoutPinsTheOriginalPOSIXRootAcrossMissingAndUnsafeChildren(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("native Windows retained-handle coverage uses DACLs in storage_windows_test.go")
}
for _, scenario := range []struct {
name string
originalUnsafe bool
replacementUnsafe bool
wantAccepted bool
}{
{name: "rejects an unsafe retained child after a safe lexical replacement", originalUnsafe: true, wantAccepted: false},
{name: "accepts missing retained children despite an unsafe lexical replacement", replacementUnsafe: true, wantAccepted: true},
} {
t.Run(scenario.name, func(t *testing.T) {
parent := privateTestRoot(t)
root := filepath.Join(parent, "auth")
replacement := filepath.Join(parent, "replacement")
moved := filepath.Join(parent, "auth-original")
for _, directory := range []string{root, replacement} {
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
t.Fatal(err)
}
}
if scenario.originalUnsafe {
if err := os.Mkdir(filepath.Join(root, "sessions"), 0o755); err != nil {
t.Fatal(err)
}
}
if scenario.replacementUnsafe {
if err := os.Mkdir(filepath.Join(replacement, "sessions"), 0o755); err != nil {
t.Fatal(err)
}
}
swapped := false
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "after-auth-root-open" || swapped {
return
}
if err := os.Rename(root, moved); err != nil {
t.Fatal(err)
}
if err := os.Rename(replacement, root); err != nil {
t.Fatal(err)
}
swapped = true
})
t.Cleanup(restore)
input := request{Version: 1, Operation: "validate-root", Root: root}
if scenario.wantAccepted {
runRequest(t, input)
} else {
runRejected(t, input)
}
if !swapped {
t.Fatal("layout validation did not retain and expose the original root")
}
for _, child := range []string{"sessions", "oidc"} {
if _, err := os.Lstat(filepath.Join(moved, child)); !errors.Is(err, os.ErrNotExist) && !(scenario.originalUnsafe && child == "sessions") {
t.Fatalf("validate-root mutated retained %s child: %v", child, err)
}
}
})
}
}
func TestProtocolReadsOnlyBoundedPrivateAuthConfig(t *testing.T) {
root := privateTestRoot(t)
filename := "auth.yaml"
@@ -199,6 +562,36 @@ func TestProtocolReadsOnlyBoundedPrivateAuthConfig(t *testing.T) {
runRejected(t, request{Version: 1, Operation: "read-auth-config", Root: root, Directory: "sessions", Filename: filename})
}
func TestProtocolReadsLocalUsersOnlyAsABoundedPrivateRegularFile(t *testing.T) {
root := privateTestRoot(t)
filename := "users.yaml"
path := filepath.Join(root, filename)
contents := []byte("version: 1\nusers: []\n")
if err := safeio.WriteCanonicalNewPrivateFile(path, contents, 0o600); err != nil {
t.Fatal(err)
}
read := runRequest(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: filename})
if !read.Found || decodeContent(t, read) != string(contents) {
t.Fatalf("read-local-users = %#v", read)
}
hardLink := filepath.Join(root, "users-copy.yaml")
if err := os.Link(path, hardLink); err != nil {
t.Fatal(err)
}
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: filename})
if err := os.Remove(hardLink); err != nil {
t.Fatal(err)
}
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := safeio.WriteCanonicalNewPrivateFile(path, bytes.Repeat([]byte("x"), maximumAuthConfigBytes+1), 0o600); err != nil {
t.Fatal(err)
}
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: filename})
}
func TestProtocolPermitsBoundedReservationSlotsOnlyForOIDCRecords(t *testing.T) {
root := filepath.Join(privateTestRoot(t), "auth")
slot := "slot-00.json"