bastion 0.60.0
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user