//go:build windows package authstorage import ( "encoding/base64" "os" "path/filepath" "runtime" "testing" "github.com/aritmolab/thothii/tools/tht/internal/safeio" "golang.org/x/sys/windows" ) func TestProtocolCreatesRecordsWithOwnerOnlyDACLAndRejectsReparseRoot(t *testing.T) { root := filepath.Join(t.TempDir(), "auth") filename := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.json" runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))}) if err := safeio.ValidatePrivateDirectory(root); err != nil { t.Fatalf("root DACL = %v", err) } if err := safeio.ValidatePrivateDirectory(filepath.Join(root, "sessions")); err != nil { t.Fatalf("sessions DACL = %v", err) } if err := safeio.ValidatePrivateRegular(filepath.Join(root, "sessions", filename)); err != nil { t.Fatalf("record DACL = %v", err) } } func TestProtocolRejectsPermissiveDACLAndReparseRoot(t *testing.T) { root := filepath.Join(t.TempDir(), "auth") filename := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.json" runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))}) path := filepath.Join(root, "sessions", filename) if err := setPermissiveDACL(path); err != nil { t.Fatal(err) } runRejected(t, request{Version: 1, Operation: "read", Root: root, Directory: "sessions", Filename: filename}) linkedRoot := filepath.Join(t.TempDir(), "reparse-auth") if err := os.Symlink(root, linkedRoot); err != nil { t.Skipf("Windows host does not permit test symlink creation: %v", err) } runRejected(t, request{Version: 1, Operation: "create", Root: linkedRoot, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))}) } func TestProtocolRejectsRecordCreationAfterSessionsDirectoryDACLBecomesPermissive(t *testing.T) { root := filepath.Join(t.TempDir(), "auth") filename := "abababababababababababababababababababababababababababababababab.json" runRequest(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions"}) if err := setPermissiveDACL(filepath.Join(root, "sessions")); err != nil { t.Fatal(err) } runRejected(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))}) } func TestProtocolValidatesAndCreatesTheCompleteWindowsSessionLayout(t *testing.T) { root := filepath.Join(t.TempDir(), "auth") runRequest(t, request{Version: 1, Operation: "ensure-layout", Root: root}) runRequest(t, request{Version: 1, Operation: "validate-root", Root: root}) for _, directory := range []string{"sessions", "oidc"} { if err := safeio.ValidatePrivateDirectory(filepath.Join(root, directory)); err != nil { t.Fatalf("%s DACL = %v", directory, err) } } if err := setPermissiveDACL(filepath.Join(root, "oidc")); err != nil { t.Fatal(err) } runRejected(t, request{Version: 1, Operation: "validate-root", Root: root}) } func TestProtocolReadsWindowsLocalUsersWithOwnerOnlyDACLAndNoReparseFallback(t *testing.T) { root := filepath.Join(t.TempDir(), "auth") if err := safeio.EnsurePrivateDirectory(root); err != nil { t.Fatal(err) } path := filepath.Join(root, "users.yaml") 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: "users.yaml"}) if !read.Found || decodeContent(t, read) != string(contents) { t.Fatalf("read-local-users = %#v", read) } if err := setPermissiveDACL(path); err != nil { t.Fatal(err) } runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"}) if err := os.Remove(path); err != nil { t.Fatal(err) } if err := os.Symlink(filepath.Join(root, "missing-target.yaml"), path); err != nil { t.Skipf("Windows host does not permit test symlink creation: %v", err) } runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"}) } func TestProtocolValidateLayoutPinsWindowsRootAcrossMissingAndUnsafeChildren(t *testing.T) { for _, scenario := range []struct { name string originalUnsafe bool replacementUnsafe bool wantAccepted bool }{ {name: "rejects unsafe retained child", originalUnsafe: true, wantAccepted: false}, {name: "accepts missing retained child while replacement is unsafe", replacementUnsafe: true, wantAccepted: true}, } { t.Run(scenario.name, func(t *testing.T) { parent := t.TempDir() 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 { path := filepath.Join(root, "sessions") if err := safeio.EnsurePrivateDirectory(path); err != nil { t.Fatal(err) } if err := setPermissiveDACL(path); err != nil { t.Fatal(err) } } if scenario.replacementUnsafe { path := filepath.Join(replacement, "sessions") if err := safeio.EnsurePrivateDirectory(path); err != nil { t.Fatal(err) } if err := setPermissiveDACL(path); err != nil { t.Fatal(err) } } blocked := false restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) { if stage != "after-auth-root-open" || blocked { return } if err := os.Rename(root, moved); err == nil { t.Fatal("retained Windows root handle permitted rename") } blocked = true }) t.Cleanup(restore) input := request{Version: 1, Operation: "validate-root", Root: root} if scenario.wantAccepted { runRequest(t, input) } else { runRejected(t, input) } if !blocked { t.Fatal("layout validation did not retain the Windows root handle") } }) } } func setPermissiveDACL(path string) error { world, err := windows.StringToSid("S-1-1-0") if err != nil { return err } var pinner runtime.Pinner pinner.Pin(world) defer pinner.Unpin() acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{{ AccessPermissions: windows.GENERIC_READ | windows.GENERIC_WRITE, AccessMode: windows.GRANT_ACCESS, Trustee: windows.TRUSTEE{ TrusteeForm: windows.TRUSTEE_IS_SID, TrusteeType: windows.TRUSTEE_IS_GROUP, TrusteeValue: windows.TrusteeValueFromSID(world), }, }}, nil) if err != nil { return err } return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, nil, nil, acl, nil) }