bastion 0.60.0
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 3m36s
ci / fuzz (push) Successful in 3m35s

This commit is contained in:
Privatae
2026-11-02 00:10:58 +00:00
parent 4a5c310482
commit 4f8285ef18
100 changed files with 11537 additions and 840 deletions
+160
View File
@@ -0,0 +1,160 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
const (
machineTestSecret = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"
hostTestSecret = "KRSXG5CTMVRXEZLUKRSXG5CTMVRXEZLU"
)
func codeFor(t *testing.T, secretB32 string) string {
t.Helper()
raw, err := b32decode(secretB32)
if err != nil {
t.Fatal(err)
}
return hotp(raw, time.Now().Unix()/stepupPeriod)
}
func enrolFactorAt(t *testing.T, dir, secretB32 string) {
t.Helper()
f := &stepupFile{SecretB32: secretB32, EnrolledAt: time.Now().UTC().Format(time.RFC3339), LastCounter: -1}
if err := stepupSave(dir, f); err != nil {
t.Fatal(err)
}
}
// With no Host paired, the owner's factor is the only one there is, so a
// command that names none reaches it. It used to refuse ("not paired yet"),
// which left a bastion on its own with no way to prove a person approved
// anything.
func TestWithNoHostTheSecondFactorIsTheMachineOwners(t *testing.T) {
uiTestConfigDir(t)
tg, err := resolveStepupTarget("", false)
if err != nil {
t.Fatalf("no Host paired and no factor reachable: %v", err)
}
if !tg.machine() {
t.Fatalf("with no Host the factor named %q, not the machine's", tg.host)
}
d, _ := configDir()
if tg.dir != filepath.Join(d, "machine-factor") {
t.Errorf("the owner's factor lives at %s", tg.dir)
}
if runtime.GOOS != "windows" {
st, err := os.Stat(tg.dir)
if err != nil {
t.Fatal(err)
}
if st.Mode().Perm() != 0o700 {
t.Errorf("the owner's factor directory is %v, not private", st.Mode().Perm())
}
}
}
// With a Host paired, a bare command keeps meaning that Host's factor, as it
// always did; --machine reaches the owner's; both at once is refused rather
// than guessed.
func TestMachineAndHostFactorsAreNamedApart(t *testing.T) {
uiTestConfigDir(t)
const host = "h1.host.privatae.ai"
givenPairing(t, host, pairingActive)
tg, err := resolveStepupTarget("", false)
if err != nil {
t.Fatal(err)
}
if tg.machine() || tg.host != host {
t.Errorf("with one Host paired a bare command reached %+v, not that Host's factor", tg)
}
mt, err := resolveStepupTarget("", true)
if err != nil {
t.Fatal(err)
}
if !mt.machine() || mt.dir == tg.dir {
t.Errorf("--machine reached %+v, the same place as the Host's factor", mt)
}
if _, err := resolveStepupTarget(host, true); err == nil {
t.Error("--machine with --host was accepted; one of them was silently ignored")
}
}
// The owner's factor and a Host's are two secrets: a code from the Host's
// authenticator approves nothing asked of the machine, and the other way round.
func TestAHostsCodeDoesNotApproveWhatIsAskedOfTheMachine(t *testing.T) {
uiTestConfigDir(t)
const host = "h1.host.privatae.ai"
givenPairing(t, host, pairingActive)
ht, err := resolveStepupTarget(host, false)
if err != nil {
t.Fatal(err)
}
mt, err := resolveStepupTarget("", true)
if err != nil {
t.Fatal(err)
}
enrolFactorAt(t, ht.dir, hostTestSecret)
enrolFactorAt(t, mt.dir, machineTestSecret)
// Refused for the right reason: both are enrolled, so "not enrolled"
// would mean this test proves nothing.
if _, err := stepupIssueApproval(mt.dir, codeFor(t, hostTestSecret), "test|machine"); err == nil ||
!strings.Contains(err.Error(), "code refused") {
t.Errorf("the Host's code against the machine's factor: %v, want code refused", err)
}
if _, err := stepupIssueApproval(ht.dir, codeFor(t, machineTestSecret), "test|host"); err == nil ||
!strings.Contains(err.Error(), "code refused") {
t.Errorf("the owner's code against the Host's factor: %v, want code refused", err)
}
if _, err := stepupIssueApproval(mt.dir, codeFor(t, machineTestSecret), "test|machine"); err != nil {
t.Errorf("the owner's own code did not approve: %v", err)
}
}
// The console's code boxes are reached by anything driving the person's
// browser, which on a whole-machine install cannot read the owner's secret:
// guessing is its only way in, so wrong codes there are throttled, as on the
// tray (approvalpipe.go). Before this, every console act took unlimited
// guesses at a six-digit code.
func TestWrongCodesAtTheConsoleAreThrottled(t *testing.T) {
uiTestConfigDir(t)
d, err := machineFactorDir()
if err != nil {
t.Fatal(err)
}
enrolFactorAt(t, d, machineTestSecret)
for i := 0; i < pipeFreeFailures; i++ {
if why := markCodeRefusal("fp1", "000000"); !strings.Contains(why, "code refused") {
t.Fatalf("wrong code %d: %q", i+1, why)
}
}
right := codeFor(t, machineTestSecret)
if why := markCodeRefusal("fp1", right); !strings.Contains(why, "wrong codes in a row") {
t.Errorf("after %d wrong codes a mark still took a code: %q", pipeFreeFailures, why)
}
if why := ownerCodeRefusal(right, "local_owner|x"); !strings.Contains(why, "wrong codes in a row") {
t.Errorf("after %d wrong codes an owner act still took a code: %q", pipeFreeFailures, why)
}
if msg, _ := recordBaselineFromConsole(right); !strings.Contains(msg, "wrong codes in a row") {
t.Errorf("after %d wrong codes a baseline still took a code: %q", pipeFreeFailures, msg)
}
// The wait passes: the right code is taken and the count starts again.
if _, err := machineCodeSpend(d, time.Now().Add(pipeSlowEvery+time.Second), func() (string, error) {
return stepupIssueApproval(d, right, "local_mark|fp1")
}); err != nil {
t.Fatalf("after the wait the right code was refused: %v", err)
}
if f := pipeThrottleLoad(d); f.Failures != 0 {
t.Errorf("an accepted code left %d failures counted", f.Failures)
}
// Locked after pipeLockAfter: no wait opens it.
_ = atomicWrite(pipeThrottlePath(d), []byte(`{"failures": 100, "last": "2000-01-01T00:00:00Z"}`))
if why := markCodeRefusal("fp1", codeFor(t, machineTestSecret)); !strings.Contains(why, "takes no more codes") {
t.Errorf("a locked console took a code: %q", why)
}
}