bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
//go:build !windows
|
||||
|
||||
// Uses setupBox, withMarkerAt and withMachineAuthority, which live behind
|
||||
// //go:build !windows (see anchor_test.go's header).
|
||||
|
||||
package main
|
||||
|
||||
// release-anchor (anchor_release.go): a Host deleted with its data and set up
|
||||
// again can be paired and anchored on this box, through the administrator,
|
||||
// and through nobody else. Every value here is invented.
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// releaseTestCA is an invented self-signed CA certificate, PEM.
|
||||
func releaseTestCA(t *testing.T, cn string) string {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: cn},
|
||||
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
|
||||
IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
||||
}
|
||||
|
||||
func releaseTestCAFile(t *testing.T, pemText string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(t.TempDir(), "host-ca.pem")
|
||||
if err := os.WriteFile(p, []byte(pemText), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// releaseTestBox is a box anchored to tAnchor, which pinned oldCA, with the
|
||||
// state the dead Host left. It returns that state's directory and oldCA.
|
||||
func releaseTestBox(t *testing.T) (dcn, oldCA string) {
|
||||
t.Helper()
|
||||
setupBox(t)
|
||||
// A marker of this test's own: the machine-scoped one belongs to whatever
|
||||
// bastion is installed where the suite runs (TestClaimAnchorWalls says why).
|
||||
withMarkerAt(t, filepath.Join(t.TempDir(), "machine.json"))
|
||||
prev := unpairNeedsRoot
|
||||
unpairNeedsRoot = func() bool { return false }
|
||||
t.Cleanup(func() { unpairNeedsRoot = prev })
|
||||
prevConsole := releaseConsole
|
||||
releaseConsole = func() bool { return false }
|
||||
t.Cleanup(func() { releaseConsole = prevConsole })
|
||||
cfgs, err := loadPairings()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := pairingFor(cfgs, tAnchor)
|
||||
oldCA = releaseTestCA(t, "old host CA (invented)")
|
||||
c.CACert = oldCA
|
||||
if err := c.save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dcn = c.dcnDir()
|
||||
// What the dead Host left: its pinned signing key and the factor enrolled for it.
|
||||
if err := os.WriteFile(filepath.Join(dcn, "dcn.json"), []byte(`{"verify_pubkey":"`+base64.StdEncoding.EncodeToString(make([]byte, 32))+`"}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := stepupSave(dcn, &stepupFile{SecretB32: pipeTestSecret, EnrolledAt: "2026-10-01T00:00:00Z", LastCounter: -1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dcn, oldCA
|
||||
}
|
||||
|
||||
// stillAnchoredAndPaired fails the test unless nothing was released.
|
||||
func stillAnchoredAndPaired(t *testing.T, dcn, why string) {
|
||||
t.Helper()
|
||||
if a := currentAnchor(); a == nil || a.AnchorHost != tAnchor {
|
||||
t.Fatalf("%s: the anchor was dropped", why)
|
||||
}
|
||||
cfgs, _ := loadPairings()
|
||||
if pairingFor(cfgs, tAnchor) == nil {
|
||||
t.Fatalf("%s: the Host was unpaired", why)
|
||||
}
|
||||
if !fileExists(filepath.Join(dcn, "stepup.json")) {
|
||||
t.Fatalf("%s: the Host's factor was set aside", why)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAnchorRefusesAHostWhoseCAIsUnchanged(t *testing.T) {
|
||||
dcn, oldCA := releaseTestBox(t)
|
||||
withMachineAuthority(t, true)
|
||||
// The same CA is the same Host, alive: refused, and nothing changes.
|
||||
err := cmdReleaseAnchor(tAnchor, releaseTestCAFile(t, oldCA), false)
|
||||
if err == nil || !strings.Contains(err.Error(), "is not gone") {
|
||||
t.Fatalf("the same Host's CA must be refused: %v", err)
|
||||
}
|
||||
stillAnchoredAndPaired(t, dcn, "same CA")
|
||||
// No CA at all: nothing shows the Host is gone.
|
||||
if err := cmdReleaseAnchor(tAnchor, "", false); err == nil || !strings.Contains(err.Error(), "--host-ca") {
|
||||
t.Fatalf("a release with no new CA must be refused: %v", err)
|
||||
}
|
||||
stillAnchoredAndPaired(t, dcn, "no CA")
|
||||
// --force is for a person at a terminal; an installer's captured output is not one.
|
||||
if err := cmdReleaseAnchor(tAnchor, "", true); err == nil || !strings.Contains(err.Error(), "Administrator terminal") {
|
||||
t.Fatalf("--force off a console must be refused: %v", err)
|
||||
}
|
||||
stillAnchoredAndPaired(t, dcn, "--force off a console")
|
||||
// At a terminal, --force releases even the same CA.
|
||||
releaseConsole = func() bool { return true }
|
||||
if err := cmdReleaseAnchor(tAnchor, releaseTestCAFile(t, oldCA), true); err != nil {
|
||||
t.Fatalf("--force at a terminal: %v", err)
|
||||
}
|
||||
if currentAnchor() != nil {
|
||||
t.Fatal("--force at a terminal did not release the anchor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAnchorRefusesWhenThisBoxPinnedNoCA(t *testing.T) {
|
||||
dcn, _ := releaseTestBox(t)
|
||||
withMachineAuthority(t, true)
|
||||
cfgs, _ := loadPairings()
|
||||
c := pairingFor(cfgs, tAnchor)
|
||||
c.CACert = ""
|
||||
if err := c.save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := cmdReleaseAnchor(tAnchor, releaseTestCAFile(t, releaseTestCA(t, "new host CA (invented)")), false)
|
||||
if err == nil || !strings.Contains(err.Error(), "pinned no Host CA") {
|
||||
t.Fatalf("with no pin to compare, only --force releases: %v", err)
|
||||
}
|
||||
stillAnchoredAndPaired(t, dcn, "no pin")
|
||||
}
|
||||
|
||||
func TestAReinstalledHostCannotPairAnAnchoredBoxWithoutTheRelease(t *testing.T) {
|
||||
releaseTestBox(t)
|
||||
cfgs, _ := loadPairings()
|
||||
// The same name: refused as already paired, and --replace keeps the old CA
|
||||
// (pair_tcp.go), so the new Host has no way in.
|
||||
if _, _, err := pairPrecheck(cfgs, handleOf(tAnchor), false, false); err == nil {
|
||||
t.Fatal("a second pairing under the anchor's name was let through")
|
||||
}
|
||||
// Another name: it would wait, pending, for an anchor that is gone.
|
||||
if _, state, err := pairPrecheck(cfgs, "fresh-host", false, false); err != nil || state != pairingPending {
|
||||
t.Fatalf("a new Host on an anchored box waits for the anchor: state %q err %v", state, err)
|
||||
}
|
||||
// And the anchor cannot be unpaired.
|
||||
if _, err := unpairHost(tAnchor); err == nil {
|
||||
t.Fatal("the anchor was unpaired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAnchorNeedsMachineAuthorityAndTheAnchorsName(t *testing.T) {
|
||||
releaseTestBox(t)
|
||||
newCA := releaseTestCAFile(t, releaseTestCA(t, "new host CA (invented)"))
|
||||
withMachineAuthority(t, false)
|
||||
if err := cmdReleaseAnchor(tAnchor, newCA, false); err == nil || !strings.Contains(err.Error(), "machine authority") {
|
||||
t.Fatalf("release without machine authority: %v", err)
|
||||
}
|
||||
withMachineAuthority(t, true)
|
||||
writePairing(t, tMember)
|
||||
if err := cmdReleaseAnchor(tMember, newCA, false); err == nil || !strings.Contains(err.Error(), "anchored to "+tAnchor) {
|
||||
t.Fatalf("releasing a host that is not the anchor must be refused: %v", err)
|
||||
}
|
||||
if a := currentAnchor(); a == nil || a.AnchorHost != tAnchor {
|
||||
t.Fatal("a refused release changed the anchor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAnchorLetsANewHostPairAndAnchor(t *testing.T) {
|
||||
dcn, _ := releaseTestBox(t)
|
||||
newCA := releaseTestCAFile(t, releaseTestCA(t, "new host CA (invented)"))
|
||||
withMachineAuthority(t, true)
|
||||
if p, err := anchorConsentPath(); err == nil {
|
||||
_ = os.WriteFile(p, []byte(`{}`), 0o600)
|
||||
}
|
||||
if err := cmdReleaseAnchor(tAnchor, newCA, false); err != nil {
|
||||
t.Fatalf("release: %v", err)
|
||||
}
|
||||
if currentAnchor() != nil {
|
||||
t.Fatal("still anchored")
|
||||
}
|
||||
if p, _ := anchorConsentPath(); fileExists(p) {
|
||||
t.Fatal("a rotation consent for the released anchor was left behind")
|
||||
}
|
||||
cfgs, _ := loadPairings()
|
||||
if pairingFor(cfgs, tAnchor) != nil {
|
||||
t.Fatal("the released Host is still paired")
|
||||
}
|
||||
if fileExists(dcn) {
|
||||
t.Fatal("the released Host's DCN state is still where a new pairing of that name would read it")
|
||||
}
|
||||
aside, _ := filepath.Glob(dcn + ".released-*")
|
||||
if len(aside) != 1 || !fileExists(filepath.Join(aside[0], "stepup.json")) || !fileExists(filepath.Join(aside[0], "dcn.json")) {
|
||||
t.Fatalf("its state must be kept, set aside: %v", aside)
|
||||
}
|
||||
// Again: nothing is left to release, and that is not an error, so an
|
||||
// installer can retry a step that stopped half-way.
|
||||
if err := cmdReleaseAnchor(tAnchor, newCA, false); err != nil {
|
||||
t.Fatalf("a second release must succeed with nothing to do: %v", err)
|
||||
}
|
||||
|
||||
// The new Host, under the same name: the precheck lets it in as active,
|
||||
// and it starts with no inherited pin and no inherited factor.
|
||||
if _, state, err := pairPrecheck(cfgs, handleOf(tAnchor), false, false); err != nil || (state != "" && state != pairingActive) {
|
||||
t.Fatalf("the new Host must pair as active: state %q err %v", state, err)
|
||||
}
|
||||
writePairing(t, tAnchor)
|
||||
cfgs, _ = loadPairings()
|
||||
fresh := pairingFor(cfgs, tAnchor).dcnDir()
|
||||
if stepupEnrolled(fresh) || fileExists(filepath.Join(fresh, "dcn.json")) {
|
||||
t.Fatal("the new pairing inherited the old Host's pin or factor")
|
||||
}
|
||||
if err := cmdClaimAnchor("", false, "", ""); err != nil {
|
||||
t.Fatalf("claim-anchor after the release: %v", err)
|
||||
}
|
||||
if a := currentAnchor(); a == nil || a.AnchorHost != tAnchor {
|
||||
t.Fatalf("not anchored to the new Host: %+v", a)
|
||||
}
|
||||
if anchorVerifyKey() != "" {
|
||||
t.Fatal("the new anchor carries a key pin; it must be pinned on the new Host's first contact")
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
_, err := os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user