bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+366
View File
@@ -0,0 +1,366 @@
package main
// The tray's lane to a system service (approvalpipe.go), driven through
// pipeServe: who may use it, what it lists, and that a code approves exactly
// the one request that was shown, once, and not by guessing.
//
// Every value here is invented.
import (
"encoding/json"
"os"
"regexp"
"strconv"
"strings"
"testing"
"time"
)
const (
pipeTestHost = "tray.host.example"
pipeTestSecret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" // base32 of "0123456789012345"
pipeTestUser = "S-1-5-21-1111111111-2222222222-3333333333-1001"
)
// thePerson is a client that may use the lane: the person signed in to an
// active session, connecting from it.
var thePerson = pipeClient{PID: 4242, UserSID: pipeTestUser, UserName: `EXAMPLE\pat`,
SessionID: 1, SessionUserSID: pipeTestUser, SessionActive: true}
// pipeBox is one paired Host with a factor enrolled and two pending requests,
// A and B, in an isolated config dir.
func pipeBox(t *testing.T) (dir, idA, purposeA, idB, purposeB string) {
t.Helper()
uiTestConfigDir(t)
writePairing(t, pipeTestHost)
cfgs, err := loadPairings()
if err != nil || len(cfgs) != 1 {
t.Fatalf("pairing: %v %d", err, len(cfgs))
}
dir = cfgs[0].dcnDir()
if err := stepupSave(dir, &stepupFile{SecretB32: pipeTestSecret, EnrolledAt: "2026-10-19T00:00:00Z", LastCounter: -1}); err != nil {
t.Fatal(err)
}
// The person owns this machine's approvals, as the installer records it.
writeApprovalsOwner(t, pipeTestUser)
ask := func(root string) (string, string) {
d := map[string]any{"kind": "dcn_store_grant", "server": "desk-7", "root": root,
"mask": []any{"read", "search"}, "excludes": []any{".ssh"}, "sweep": "hourly", "sensitive": false}
p, err := purposeFromDetails(d)
if err != nil {
t.Fatal(err)
}
id, err := stepupNewRequest(dir, p, d)
if err != nil {
t.Fatal(err)
}
return id, p
}
idA, purposeA = ask("/srv/example-a")
idB, purposeB = ask("/srv/example-b")
return
}
// writeApprovalsOwner records sid as the owner of this machine's approvals,
// the file `approvals-owner` writes.
func writeApprovalsOwner(t *testing.T, sid string) {
t.Helper()
p, err := approvalsOwnerPath()
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(approvalsOwnerRecord{SID: sid, SetAt: "2026-10-19T00:00:00Z"})
if err := atomicWrite(p, raw); err != nil {
t.Fatal(err)
}
}
// anotherPerson is someone else with an account on the machine, signed in to
// an active session of their own: everything pipeClientRefusal asks, and not
// the owner.
var anotherPerson = pipeClient{PID: 5151, UserSID: "S-1-5-21-1111111111-2222222222-3333333333-1002", UserName: `EXAMPLE\sam`,
SessionID: 2, SessionUserSID: "S-1-5-21-1111111111-2222222222-3333333333-1002", SessionActive: true}
func TestThePipeServesOnlyTheAccountThatOwnsTheApprovals(t *testing.T) {
dir, idA, purposeA, _, _ := pipeBox(t)
now := time.Now()
if why := pipeClientRefusal(anotherPerson); why != "" {
t.Fatalf("the fixture must pass the session rules: %s", why)
}
refusedOwner := func(resp pipeResponse, what string) {
t.Helper()
if resp.OK || !strings.Contains(resp.Error, "belong to another account") {
t.Fatalf("%s by another account must be refused: %+v", what, resp)
}
}
refusedOwner(pipeServe(pipeRequest{Op: "list"}, anotherPerson, now), "list")
code := pipeCodeNow()
refusedOwner(pipeServe(pipeRequest{Op: "approve", Host: pipeTestHost, ID: idA, Purpose: purposeDigest(purposeA), Code: code}, anotherPerson, now), "approve")
refusedOwner(pipeServe(pipeRequest{Op: "deny", Host: pipeTestHost, ID: idA, Purpose: purposeDigest(purposeA)}, anotherPerson, now), "deny")
if r := stateOf(t, dir, idA); r.State != "pending" {
t.Fatalf("a refused account changed the request: %s", r.State)
}
// Refused before any code was looked at: the owner's code still works.
if resp := pipeServe(pipeRequest{Op: "approve", Host: pipeTestHost, ID: idA, Purpose: purposeDigest(purposeA), Code: code}, thePerson, now); !resp.OK {
t.Fatalf("the owner's approve: %+v", resp)
}
// No owner recorded, or a record that cannot be read: nobody is served.
p, _ := approvalsOwnerPath()
if err := os.Remove(p); err != nil {
t.Fatal(err)
}
if resp := pipeServe(pipeRequest{Op: "list"}, thePerson, now); resp.OK || !strings.Contains(resp.Error, "no account is recorded") {
t.Fatalf("with no owner recorded the lane serves nobody: %+v", resp)
}
if err := os.WriteFile(p, []byte(`{"sid": 7`), 0o600); err != nil {
t.Fatal(err)
}
if resp := pipeServe(pipeRequest{Op: "list"}, thePerson, now); resp.OK {
t.Fatalf("an unreadable owner record must serve nobody: %+v", resp)
}
}
func pipeCodeNow() string {
raw, _ := b32decode(pipeTestSecret)
return hotp(raw, time.Now().Unix()/stepupPeriod)
}
func stateOf(t *testing.T, dir, id string) stepupRequest {
t.Helper()
r, ok := requestsLoad(dir)[id]
if !ok {
t.Fatalf("request %s is gone", id)
}
return r
}
// ── who may connect ─────────────────────────────────────────────────────────
func TestThePipeServesOnlyThePersonSignedInToTheirOwnSession(t *testing.T) {
if why := pipeClientRefusal(thePerson); why != "" {
t.Fatalf("the person at the screen was refused: %s", why)
}
other := "S-1-5-21-1111111111-2222222222-3333333333-1002"
cases := map[string]pipeClient{
"unknown account": {PID: 1, SessionID: 1, SessionUserSID: pipeTestUser, SessionActive: true},
"session 0": {PID: 1, UserSID: pipeTestUser, SessionID: 0, SessionUserSID: pipeTestUser, SessionActive: true},
"SYSTEM in a session": {PID: 1, UserSID: "S-1-5-18", SessionID: 1, SessionUserSID: pipeTestUser, SessionActive: true},
"a service account": {PID: 1, UserSID: "S-1-5-80-123-456", SessionID: 1, SessionUserSID: pipeTestUser, SessionActive: true},
"another account, run as": {PID: 1, UserSID: other, SessionID: 1, SessionUserSID: pipeTestUser, SessionActive: true},
"nobody signed in to its session": {PID: 1, UserSID: pipeTestUser, SessionID: 2, SessionActive: true},
"a disconnected session": {PID: 1, UserSID: pipeTestUser, SessionID: 1, SessionUserSID: pipeTestUser, SessionActive: false},
}
for name, c := range cases {
if pipeClientRefusal(c) == "" {
t.Errorf("%s: accepted, and must be refused", name)
}
}
// And a refused client gets nothing at all: no list, no approval, even
// with the right code for a real request.
dir, idA, purposeA, _, _ := pipeBox(t)
intruder := cases["another account, run as"]
if resp := pipeServe(pipeRequest{Op: "list"}, intruder, time.Now()); resp.OK || len(resp.Approvals) != 0 {
t.Fatalf("a refused client was shown the list: %+v", resp)
}
resp := pipeServe(pipeRequest{Op: "approve", Host: pipeTestHost, ID: idA, Purpose: purposeDigest(purposeA), Code: pipeCodeNow()},
intruder, time.Now())
if resp.OK || stateOf(t, dir, idA).State != "pending" {
t.Fatalf("a refused client approved a request: %+v", resp)
}
if f, _ := stepupLoad(dir); f.LastCounter != -1 {
t.Fatal("a refused client's code was checked and burned")
}
}
// ── what it lists ───────────────────────────────────────────────────────────
func TestThePipeListsWhatIsPendingInTheConsolesWordsAndNoSecret(t *testing.T) {
dir, idA, purposeA, idB, _ := pipeBox(t)
resp := pipeServe(pipeRequest{Op: "list"}, thePerson, time.Now())
if !resp.OK || len(resp.Approvals) != 2 {
t.Fatalf("list: %+v", resp)
}
byID := map[string]pipeApproval{}
for _, a := range resp.Approvals {
byID[a.ID] = a
}
a, ok := byID[idA]
if !ok || byID[idB].ID == "" {
t.Fatalf("both pending requests must be listed: %+v", resp.Approvals)
}
// The same words `privatae-bastion approvals` prints.
want := describeRequest(requestsLoad(dir)[idA])
if strings.Join(a.Lines, "\n") != strings.TrimSpace(want) {
t.Fatalf("the tray's words are not the console's:\n%q\n%q", a.Lines, want)
}
if a.Purpose != purposeDigest(purposeA) || a.Host != pipeTestHost {
t.Fatalf("the card must name its Host and carry its purpose's digest: %+v", a)
}
raw, _ := json.Marshal(resp)
if strings.Contains(string(raw), pipeTestSecret) {
t.Fatal("the TOTP secret crossed the pipe")
}
// An approval id is what the Host spends; it never crosses either.
if _, err := stepupApproveRequest(dir, idB, pipeCodeNow()); err != nil {
t.Fatal(err)
}
raw, _ = json.Marshal(pipeServe(pipeRequest{Op: "list"}, thePerson, time.Now()))
if appr := requestsLoad(dir)[idB].Approval; appr == "" || strings.Contains(string(raw), appr) {
t.Fatalf("an approval id crossed the pipe, or none was issued (%q)", appr)
}
}
// ── a code approves exactly what was shown ──────────────────────────────────
func TestThePipeApprovesOneRequestWithACodeBoundToItsPurpose(t *testing.T) {
dir, idA, purposeA, idB, purposeB := pipeBox(t)
now := time.Now()
approve := func(id, purposeSHA, code string) pipeResponse {
return pipeServe(pipeRequest{Op: "approve", Host: pipeTestHost, ID: id, Purpose: purposeSHA, Code: code}, thePerson, now)
}
if r := approve(idA, purposeDigest(purposeA), ""); r.OK || !strings.Contains(r.Error, "code") {
t.Fatalf("no code must not approve: %+v", r)
}
if r := approve(idA, purposeDigest(purposeA), "000000"); r.OK && pipeCodeNow() != "000000" {
t.Fatalf("a wrong code approved: %+v", r)
}
// A valid code, posted for A with the purpose B's card showed: refused,
// and the code is not spent by it.
r := approve(idA, purposeDigest(purposeB), pipeCodeNow())
if r.OK || !strings.Contains(r.Error, "not the one shown") {
t.Fatalf("a code bound to another request's purpose must be refused: %+v", r)
}
if r := approve(idA, "", pipeCodeNow()); r.OK {
t.Fatalf("a code with no purpose must be refused: %+v", r)
}
if stateOf(t, dir, idA).State != "pending" {
t.Fatal("A was approved by a mismatched submission")
}
if f, _ := stepupLoad(dir); f.LastCounter != -1 {
t.Fatal("a refused submission spent the code")
}
// The right card: approved, once.
if r := approve(idA, purposeDigest(purposeA), pipeCodeNow()); !r.OK {
t.Fatalf("the right code for the request shown must approve it: %+v", r)
}
a := stateOf(t, dir, idA)
if a.State != "approved" || a.Approval == "" {
t.Fatalf("A must be approved: %+v", a)
}
if stateOf(t, dir, idB).State != "pending" {
t.Fatal("approving A touched B")
}
// The approval binds A's purpose and nothing else.
if err := stepupCheckApproval(dir, a.Approval, purposeB); err == nil {
t.Fatal("A's approval passed for B's purpose")
}
if err := stepupConsumeApproval(dir, a.Approval, purposeA); err != nil {
t.Fatalf("A's approval must pass for A's purpose: %v", err)
}
// And the code is spent: the same code cannot approve B.
if r := approve(idB, purposeDigest(purposeB), pipeCodeNow()); r.OK {
t.Fatal("one code approved two requests")
}
}
func TestThePipeDeniesOnlyTheRequestShown(t *testing.T) {
dir, idA, purposeA, _, purposeB := pipeBox(t)
deny := func(purposeSHA string) pipeResponse {
return pipeServe(pipeRequest{Op: "deny", Host: pipeTestHost, ID: idA, Purpose: purposeSHA}, thePerson, time.Now())
}
if r := deny(purposeDigest(purposeB)); r.OK || stateOf(t, dir, idA).State != "pending" {
t.Fatalf("a deny for another card's purpose must be refused: %+v", r)
}
if r := deny(purposeDigest(purposeA)); !r.OK || stateOf(t, dir, idA).State != "denied" {
t.Fatalf("deny: %+v", r)
}
}
// ── no guessing ─────────────────────────────────────────────────────────────
func TestThePipeThrottlesWrongCodesAndLocksUntilAnotherLaneApproves(t *testing.T) {
dir, idA, purposeA, idB, purposeB := pipeBox(t)
wrong := "000000"
if pipeCodeNow() == wrong {
wrong = "000001"
}
t0 := time.Now()
try := func(code string, at time.Time) pipeResponse {
return pipeServe(pipeRequest{Op: "approve", Host: pipeTestHost, ID: idA, Purpose: purposeDigest(purposeA), Code: code}, thePerson, at)
}
for i := 0; i < pipeFreeFailures; i++ {
if r := try(wrong, t0); r.OK || strings.Contains(r.Error, "try again") {
t.Fatalf("attempt %d: the first %d wrong codes are not throttled: %+v", i+1, pipeFreeFailures, r)
}
}
// Now even the RIGHT code is not looked at until pipeSlowEvery has passed.
if r := try(pipeCodeNow(), t0.Add(time.Second)); r.OK || !strings.Contains(r.Error, "try again") {
t.Fatalf("after %d wrong codes the next attempt must wait: %+v", pipeFreeFailures, r)
}
if stateOf(t, dir, idA).State != "pending" {
t.Fatal("a throttled attempt approved")
}
if r := try(wrong, t0.Add(pipeSlowEvery+time.Second)); r.OK || strings.Contains(r.Error, "try again") {
t.Fatalf("after the wait one attempt is taken: %+v", r)
}
// At pipeLockAfter the lane takes no code at all, whatever the wait...
raw, _ := json.Marshal(pipeThrottleFile{Failures: pipeLockAfter, Last: t0.UTC().Format(time.RFC3339)})
if err := os.WriteFile(pipeThrottlePath(dir), raw, 0o600); err != nil {
t.Fatal(err)
}
if r := try(pipeCodeNow(), t0.Add(24*time.Hour)); r.OK || !strings.Contains(r.Error, "Administrator terminal") {
t.Fatalf("a locked lane must refuse and say where to approve instead: %+v", r)
}
// ...until a code is accepted on another lane (here, what `approve` at an
// Administrator terminal calls), which re-opens it.
if _, err := stepupApproveRequest(dir, idB, pipeCodeNow()); err != nil {
t.Fatal(err)
}
if f := pipeThrottleLoad(dir); f.Failures != 0 {
t.Fatalf("an accepted code on another lane must reset the count, got %d", f.Failures)
}
_ = purposeB
}
// ── the pipe's own permissions ──────────────────────────────────────────────
// TestTheApprovalPipeTakesLocalInteractiveClientsOnly reads the pipe's ACL and
// its creation flags. Interactive users may read and write, and may NOT create
// an instance of the pipe (FILE_APPEND_DATA, 0x4, is FILE_CREATE_PIPE_INSTANCE
// on a pipe): that right would let a client stand up its own end of the name.
// Remote clients are refused by the pipe itself. A Windows-only file, so this
// reads it rather than runs it.
func TestTheApprovalPipeTakesLocalInteractiveClientsOnly(t *testing.T) {
m := regexp.MustCompile(`\(A;;(0x[0-9a-fA-F]+);;;IU\)`).FindStringSubmatch(approvalPipeSDDL)
if m == nil {
t.Fatalf("no interactive-users entry in %s", approvalPipeSDDL)
}
mask, err := strconv.ParseUint(m[1], 0, 32)
if err != nil {
t.Fatal(err)
}
if mask&0x4 != 0 {
t.Fatalf("interactive users may create pipe instances (mask %#x)", mask)
}
if mask&0x3 != 0x3 || mask&0x100000 == 0 {
t.Fatalf("interactive users cannot read, write and wait on the pipe (mask %#x)", mask)
}
if strings.Count(approvalPipeSDDL, "(A;") != 3 || !strings.HasPrefix(approvalPipeSDDL, "D:P") {
t.Fatalf("the pipe's DACL must be protected and hold exactly SYSTEM, Administrators and interactive users: %s", approvalPipeSDDL)
}
src, err := os.ReadFile("approvalpipe_windows.go")
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"PIPE_REJECT_REMOTE_CLIENTS", "FILE_FLAG_FIRST_PIPE_INSTANCE", "SECURITY_IDENTIFICATION",
"GetNamedPipeClientProcessId", "pipeServerIsService(c.h)"} {
if !strings.Contains(string(src), want) {
t.Errorf("approvalpipe_windows.go no longer uses %s", want)
}
}
}