Files
Privatae 081763a675
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 4m30s
ci / fuzz (push) Successful in 3m25s
bastion 0.57.0
2026-10-28 20:35:45 +00:00

627 lines
27 KiB
Go

//go:build linux
// The pairing code a Cloud Host's card hands out (pair_code.go): one string
// carrying the token, the enroll address and the Host CA's fingerprint, and
// the question a local machine asks before it serves a second Host
// (pair_terminal.go).
//
// Uses the fixtures of local_host_linux_test.go and local_host_tcp_linux_test.go
// (newTestCA, pairingBox, startTCPEnrollHost, homeToken), so it carries the same
// build tag.
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
// makePairCode builds a code the way a Host's card does: skp_ and base64url,
// no padding, of the JSON object fields.
func makePairCode(t *testing.T, fields map[string]any) string {
t.Helper()
raw, err := json.Marshal(fields)
if err != nil {
t.Fatal(err)
}
return pairCodePrefix + base64.RawURLEncoding.EncodeToString(raw)
}
func pairCodeFingerprint(ca *testCA) string {
sum := sha256.Sum256(ca.cert.Raw)
return hex.EncodeToString(sum[:])
}
// chainWithCA is the enroll port's certificate as a Host from this release
// serves it: the leaf, then the CA.
func chainWithCA(t *testing.T, ca *testCA) tls.Certificate {
t.Helper()
c := ca.serverCert(t, tcpServerName)
c.Certificate = append(c.Certificate, ca.cert.Raw)
return c
}
func goodCodeFields(url, fp string) map[string]any {
return map[string]any{"v": 1, "u": url, "ca": fp, "t": homeToken()}
}
// ── decoding ────────────────────────────────────────────────────────────────
func TestAPairingCodeDecodes(t *testing.T) {
fp := strings.Repeat("ab", 32)
code := makePairCode(t, goodCodeFields("https://maple.example-id.cloud.example:8810/v1/local/redeem", fp))
pc, err := decodePairCode(code)
if err != nil {
t.Fatal(err)
}
if pc.RedeemURL != "https://maple.example-id.cloud.example:8810/v1/local/redeem" || pc.CASHA256 != fp || pc.Token != homeToken() {
t.Fatalf("decoded: %+v", pc)
}
token, opts, err := pairOptsFor(code, "", "", false)
if err != nil || token != homeToken() || opts.redeemURL != pc.RedeemURL || opts.caSHA256 != fp || opts.hostCAFile != "" || opts.askSecond == nil {
t.Fatalf("pair's options from a code: %q %+v %v", token, opts, err)
}
if _, _, err := pairOptsFor(code, "", "host-ca.pem", false); err == nil {
t.Fatal("a code carries its own certificate: --host-ca beside it must be refused")
}
if tok, opts, err := pairOptsFor(homeToken(), "", "", false); err != nil || tok != homeToken() || opts.caSHA256 != "" {
t.Fatalf("a bare token passes through as it was: %q %+v %v", tok, opts, err)
}
}
// A code this bastion cannot read is refused with what was wrong, never
// with its content: a log of a failed install must not hold the token.
func TestAPairingCodeIsReadStrictly(t *testing.T) {
fp := strings.Repeat("0f", 32)
u := "https://maple.example-id.cloud.example:8810/v1/local/redeem"
with := func(edit func(m map[string]any)) string {
m := goodCodeFields(u, fp)
edit(m)
return makePairCode(t, m)
}
for name, code := range map[string]string{
"garbled": "skp_%%%not-base64%%%",
"not JSON": pairCodePrefix + base64.RawURLEncoding.EncodeToString([]byte("secret-x home")),
"wrong version": with(func(m map[string]any) { m["v"] = 2 }),
"version as text": with(func(m map[string]any) { m["v"] = "1" }),
"extra key": with(func(m map[string]any) { m["x"] = "y" }),
"missing key": with(func(m map[string]any) { delete(m, "ca") }),
"another key": with(func(m map[string]any) { delete(m, "t"); m["token"] = homeToken() }),
"http address": with(func(m map[string]any) { m["u"] = "http://maple.example-id.cloud.example:8810/v1/local/redeem" }),
"address with user": with(func(m map[string]any) { m["u"] = "https://a@maple.example-id.cloud.example/v1/local/redeem" }),
"short fingerprint": with(func(m map[string]any) { m["ca"] = fp[:63] }),
"upper fingerprint": with(func(m map[string]any) { m["ca"] = strings.ToUpper(fp) }),
"not hex": with(func(m map[string]any) { m["ca"] = strings.Repeat("zz", 32) }),
"no skb_ token": with(func(m map[string]any) { m["t"] = "secret-x" }),
"malformed token": with(func(m map[string]any) { m["t"] = "skb_bm9kb3Q" }), // base64url("nodot")
} {
_, err := decodePairCode(code)
if err == nil {
t.Errorf("%s: refused nothing", name)
continue
}
if strings.Contains(err.Error(), "secret-x") || strings.Contains(err.Error(), homeToken()) {
t.Errorf("%s: the refusal repeats the token: %v", name, err)
}
}
_, err := decodePairCode(with(func(m map[string]any) { m["v"] = 2 }))
if err == nil || !strings.Contains(err.Error(), "update the bastion") {
t.Fatalf("a newer code says to update the bastion: %v", err)
}
}
// ── the pinned CA ───────────────────────────────────────────────────────────
// SECURITY: the CA is taken from the enroll port's chain only when its
// SHA-256 is the code's, and the port's own certificate must verify under it
// for tcpServerName. A chain without the CA, another CA, or a leaf another CA
// signed are refused before the token is spent, and nothing is saved.
func TestAPairingCodePinsTheHostCAByFingerprint(t *testing.T) {
ca := newTestCA(t, "maple host CA")
impostor := newTestCA(t, "impostor")
leafByImpostor := impostor.serverCert(t, tcpServerName)
leafByImpostor.Certificate = append(leafByImpostor.Certificate, ca.cert.Raw)
for name, c := range map[string]struct {
serve tls.Certificate
fp string
want string
}{
"no CA in the chain": {ca.serverCert(t, tcpServerName), pairCodeFingerprint(ca), "did not present"},
"another CA in the chain": {chainWithCA(t, impostor), pairCodeFingerprint(ca), "other than"},
"another fingerprint": {chainWithCA(t, ca), pairCodeFingerprint(impostor), "other than"},
"a leaf another CA signed": {leafByImpostor, pairCodeFingerprint(ca), "not signed by"},
"the CA, under another name": {func() tls.Certificate {
c := ca.serverCert(t, "maple.example")
c.Certificate = append(c.Certificate, ca.cert.Raw)
return c
}(), pairCodeFingerprint(ca), "not signed by"},
} {
t.Run(name, func(t *testing.T) {
pairingBox(t)
h := startTCPEnrollHost(t, ca, c.serve, "127.0.0.1:18821")
token, opts, err := pairOptsFor(makePairCode(t, goodCodeFields(h.url, c.fp)), "", "", false)
if err != nil {
t.Fatal(err)
}
_, _, err = pairWith(token, opts)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Fatalf("want a refusal naming %q, got %v", c.want, err)
}
if h.hits.Load() != 0 {
t.Fatal("the token went to an enroll port whose CA was never checked")
}
if cfgs, _ := loadPairings(); len(cfgs) != 0 {
t.Fatalf("a refused pairing saved %d pairings", len(cfgs))
}
})
}
}
// A good code pairs exactly as --redeem-url u --host-ca <that CA> t does: the
// same TCP pairing, the same CA pinned.
func TestAPairingCodePairsAsTheHostCAFileDoes(t *testing.T) {
pairingBox(t)
ca := newTestCA(t, "maple host CA")
h := startTCPEnrollHost(t, ca, chainWithCA(t, ca), "127.0.0.1:18822")
token, opts, err := pairOptsFor(makePairCode(t, goodCodeFields(h.url, pairCodeFingerprint(ca))), "", "", false)
if err != nil {
t.Fatal(err)
}
cfg, _, err := pairWith(token, opts)
if err != nil {
t.Fatal(err)
}
if !cfg.tcpTransport() || !sameCAs(cfg.CACert, ca.pem) || cfg.LaneAddr != "127.0.0.1:18822" || cfg.HostName != "home.example.test" {
t.Fatalf("the pairing: transport %q lane %q host %q", cfg.Transport, cfg.LaneAddr, cfg.HostName)
}
h.mu.Lock()
req, sni := h.last, h.sni
h.mu.Unlock()
if req.Secret != "secret-x" || req.Handle != "home" || sni != tcpServerName {
t.Fatalf("the redeem is today's: secret %q handle %q sni %q", req.Secret, req.Handle, sni)
}
// The same code again: already paired with this Host, nothing changed,
// and the error is the one `pair` exits 3 on.
before, _ := os.ReadFile(filepath.Join(mustPairingsDir(t), "home.example.test.json"))
token, opts, _ = pairOptsFor(makePairCode(t, goodCodeFields(h.url, pairCodeFingerprint(ca))), "", "", false)
_, _, err = pairWith(token, opts)
var already *alreadyPairedError
if !errors.As(err, &already) {
t.Fatalf("pairing the same Host again must be alreadyPairedError (exit %d), got %v", pairExitAlreadyPaired, err)
}
after, _ := os.ReadFile(filepath.Join(mustPairingsDir(t), "home.example.test.json"))
if string(before) != string(after) || h.hits.Load() != 1 {
t.Fatal("an already-paired Host must change nothing and spend no token")
}
// Another Host of the same name (Cloud names repeat): a different CA, so
// never "already connected".
other := newTestCA(t, "another maple")
h2 := startTCPEnrollHost(t, other, chainWithCA(t, other), "127.0.0.1:18823")
token, opts, _ = pairOptsFor(makePairCode(t, goodCodeFields(h2.url, pairCodeFingerprint(other))), "", "", false)
_, _, err = pairWith(token, opts)
if err == nil || errors.As(err, &already) || !strings.Contains(err.Error(), "different Host named") {
t.Fatalf("a same-named Host under another CA is a plain refusal, not exit 3: %v", err)
}
}
func mustPairingsDir(t *testing.T) string {
t.Helper()
d, err := pairingsDir()
if err != nil {
t.Fatal(err)
}
return d
}
// ── an enroll port that does not answer ─────────────────────────────────────
// A Cloud Host's pairing port drops every address but the one the card opened
// it to. A pair from elsewhere says so within pairAnswerTimeout, in plain
// words, instead of waiting for the system's connect timeout. Both the code
// and --host-ca paths.
func TestAnEnrollPortThatDoesNotAnswerNamesThePairingWindow(t *testing.T) {
pairingBox(t)
oldTimeout, oldDial := pairAnswerTimeout, pairDial
t.Cleanup(func() { pairAnswerTimeout, pairDial = oldTimeout, oldDial })
pairAnswerTimeout = 150 * time.Millisecond
pairDial = func(ctx context.Context, _, _ string) (net.Conn, error) {
<-ctx.Done()
return nil, ctx.Err()
}
u := "https://maple.example-id.cloud.example:8810/v1/local/redeem"
ca := newTestCA(t, "maple host CA")
check := func(name string, err error, took time.Duration) {
t.Helper()
var we *pairWindowError
if !errors.As(err, &we) {
t.Fatalf("%s: want the pairing-window message, got %v", name, err)
}
msg := err.Error()
if !strings.Contains(msg, "maple.example-id.cloud.example's pairing port didn't answer") ||
!strings.Contains(msg, "15 minutes") || !strings.Contains(msg, "another network") || !strings.Contains(msg, "Nothing was paired") {
t.Fatalf("%s: the message: %s", name, msg)
}
if took > 2*time.Second {
t.Fatalf("%s: took %s, the timeout is %s", name, took, pairAnswerTimeout)
}
}
start := time.Now()
token, opts, _ := pairOptsFor(makePairCode(t, goodCodeFields(u, pairCodeFingerprint(ca))), "", "", false)
_, _, err := pairWith(token, opts)
check("pairing code", err, time.Since(start))
start = time.Now()
_, _, err = pairWithTokenVia(homeToken(), u, writeCA(t, ca), false, false)
check("--host-ca", err, time.Since(start))
}
// ── a local machine that already serves a Host ──────────────────────────────
// fakeTerminal stands in a person at this machine: their answer, and what
// the question said. answer "" with noTerm set is a run with no terminal.
type fakeTerminal struct {
answer string
noTerm bool
asked strings.Builder
opened int
}
func (f *fakeTerminal) use(t *testing.T) {
t.Helper()
old := openTerminal
t.Cleanup(func() { openTerminal = old })
openTerminal = func() (io.Reader, io.Writer, func(), error) {
f.opened++
if f.noTerm {
return nil, nil, nil, errNoTerminal
}
return strings.NewReader(f.answer), &f.asked, func() {}, nil
}
}
func otherToken() string { return "skb_b3RoZXIuc2VjcmV0LXk" } // base64url("other.secret-y")
// SECURITY: a local machine that serves a Host adds another only on a yes
// typed at this machine's terminal. No terminal, or any answer but y or yes,
// refuses before the token is spent, and the first pairing is untouched.
// Every caller but `pair` (claim-anchor, the pairing op) keeps the old
// refusal: it has no person to ask.
func TestALocalMachineAddsASecondHostOnlyOnAYesTypedHere(t *testing.T) {
pairingBox(t)
ca := newTestCA(t, "home host CA")
home := startTCPEnrollHost(t, ca, ca.serverCert(t, tcpServerName), "127.0.0.1:18831")
if _, _, err := pairWithTokenVia(homeToken(), home.url, writeCA(t, ca), false, false); err != nil {
t.Fatal(err)
}
first, _ := os.ReadFile(filepath.Join(mustPairingsDir(t), "home.example.test.json"))
otherCA := newTestCA(t, "other host CA")
other := startTCPEnrollHost(t, otherCA, chainWithCA(t, otherCA), "127.0.0.1:18832")
other.set(func(h *tcpEnrollHost) {
h.edit = func(ans map[string]any) { ans["host_handle"], ans["host_name"] = "other", "other.example.test" }
})
code := makePairCode(t, map[string]any{"v": 1, "u": other.url, "ca": pairCodeFingerprint(otherCA), "t": otherToken()})
pairOther := func() error {
token, opts, err := pairOptsFor(code, "", "", false)
if err != nil {
t.Fatal(err)
}
_, _, err = pairWith(token, opts)
return err
}
untouched := func(why string) {
t.Helper()
now, _ := os.ReadFile(filepath.Join(mustPairingsDir(t), "home.example.test.json"))
cfgs, _ := loadPairings()
if string(now) != string(first) {
t.Fatalf("%s: the first pairing changed", why)
}
if why != "yes" && (len(cfgs) != 1 || other.hits.Load() != 0) {
t.Fatalf("%s: %d pairings, %d redeems", why, len(cfgs), other.hits.Load())
}
}
// No terminal: refused, and it says where to run it instead.
ft := &fakeTerminal{noTerm: true}
ft.use(t)
if err := pairOther(); err == nil || !strings.Contains(err.Error(), "terminal window on this computer") {
t.Fatalf("no terminal must refuse in plain words: %v", err)
}
untouched("no terminal")
// Anything but y or yes.
for _, a := range []string{"\n", "n\n", "no\n", "yess\n", "sure\n", ""} {
ft := &fakeTerminal{answer: a}
ft.use(t)
if err := pairOther(); err == nil || !strings.Contains(err.Error(), "stays the only Host") {
t.Fatalf("answer %q must refuse: %v", a, err)
}
if !strings.Contains(ft.asked.String(), "already serves your Host home.example.test. Add other as well?") {
t.Fatalf("the question: %q", ft.asked.String())
}
untouched("answer " + a)
}
// The callers with no person to ask keep the refusal, and ask nobody.
ft = &fakeTerminal{answer: "y\n"}
ft.use(t)
var second *localSecondHostError
if _, _, err := pairWithTokenVia(otherToken(), other.url, writeCA(t, otherCA), false, false); !errors.As(err, &second) {
t.Fatalf("pairWithTokenVia (no person to ask) must keep the refusal: %v", err)
}
if ft.opened != 0 {
t.Fatal("a caller with no person to ask opened the terminal")
}
untouched("no asker")
// A typed yes adds it beside the first.
ft = &fakeTerminal{answer: "Yes\n"}
ft.use(t)
if err := pairOther(); err != nil {
t.Fatal(err)
}
cfgs, _ := loadPairings()
if len(cfgs) != 2 || pairingFor(cfgs, "other.example.test") == nil {
t.Fatalf("after yes: %d pairings", len(cfgs))
}
untouched("yes")
}
// secondHostStdinChild set to 1 makes this test binary the child of
// TestTheSecondHostAnswerIsNeverReadFromStandardInput.
const secondHostStdinChild = "PRIVATAE_TEST_SECOND_HOST_STDIN_CHILD"
// SECURITY: the answer to the second-Host question comes from the controlling
// terminal, never from standard input. The installers hand the pairing code to
// `pair -` on standard input, so a "y" piped in after the code must not add a
// Host. The real openTerminal runs in a child with no controlling terminal (a
// session of its own) and "y" waiting on its standard input: it must find no
// terminal, and leave standard input unread.
func TestTheSecondHostAnswerIsNeverReadFromStandardInput(t *testing.T) {
if os.Getenv(secondHostStdinChild) == "1" {
yes, err := confirmAddHost("home.example.test", "other")
rest, _ := io.ReadAll(os.Stdin)
fmt.Printf("child: yes=%v refused=%v stdin-left=%q\n", yes, err != nil && strings.Contains(err.Error(), "no terminal"), rest)
os.Exit(0)
}
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(self, "-test.run=^TestTheSecondHostAnswerIsNeverReadFromStandardInput$")
cmd.Env = append(os.Environ(), secondHostStdinChild+"=1")
cmd.Stdin = strings.NewReader("y\n")
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("the child: %v\n%s", err, out)
}
if !strings.Contains(string(out), `child: yes=false refused=true stdin-left="y\n"`) {
t.Fatalf("a y on standard input must neither answer the question nor be read: %s", out)
}
}
// The refusal names this system's own way to a networked bastion, and says
// that the Host already here would have to be paired again.
func TestTheLocalRefusalSaysWhatANetworkedReinstallCosts(t *testing.T) {
msg := localOneHostRefusal("home.example.test")
if !strings.Contains(msg, "install.sh --networked") || strings.Contains(msg, "install.ps1") {
t.Fatalf("on linux it names install.sh: %s", msg)
}
if !strings.Contains(msg, "home.example.test has to be paired with it again") || !strings.Contains(msg, "answer yes at this computer's terminal") {
t.Fatalf("the refusal: %s", msg)
}
}
// ── what the Host's card actually makes ─────────────────────────────────────
// Made by the C-Series Host's own pairing_code (bastion_enroll.py) from the CA
// below and an invented token, for an invented address: compact JSON with the
// keys v, u, ca and t in base64url without padding. The bastion must read
// exactly that, and the fingerprint must be the one caSHA256 computes from
// the same CA, or a real code would never pin.
const hostMadePairCode = "skp_eyJ2IjoxLCJ1IjoiaHR0cHM6Ly9tYXBsZS5leGFtcGxlLWlkLmNsb3VkLmV4YW1wbGU6ODgxMC92MS9sb2NhbC9yZWRlZW0iLCJjYSI6IjcyMzNkOWU4ODU5MzIyZjE1YTJiNmMzYWIyMmZiNjMxNGI4M2I0Njg2ZmNhYmZiYjRmNGU1NDE5NDA1MTFkMDciLCJ0Ijoic2tiX2JXRndiR1V1YzJWamNtVjBMV2x1ZG1WdWRHVmtMVEF3TURFIn0"
const hostMadePairCodeCA = `-----BEGIN CERTIFICATE-----
MIIBTTCB9KADAgECAgIQkjAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhleGFtcGxl
IGludmVudGVkIGhvc3QgQ0EwIBcNMjYwMTAxMDAwMDAwWhgPMjEyNTEyMDgwMDAw
MDBaMCMxITAfBgNVBAMMGGV4YW1wbGUgaW52ZW50ZWQgaG9zdCBDQTBZMBMGByqG
SM49AgEGCCqGSM49AwEHA0IABA32JMrh4sMMLvNNoSBq5FgXFgHd/2n71Qk9ayje
Kj9rdt+Q3CFYWgtCqFHc6ZLUfXexKkdWvnET3px2hGqZWbKjFjAUMBIGA1UdEwEB
/wQIMAYBAf8CAQAwCgYIKoZIzj0EAwIDSAAwRQIhAIQ6sKtiqxcAQTPq7+toiEl0
qrWb4CKKB2UtvcQoC9YwAiBYGKbmvG9ZszE8X7uKUGexjP9x6lDOwqfmnLoruuVg
Kg==
-----END CERTIFICATE-----
`
func TestACodeTheHostMadeDecodesAndPins(t *testing.T) {
pc, err := decodePairCode(hostMadePairCode)
if err != nil {
t.Fatal(err)
}
if pc.RedeemURL != "https://maple.example-id.cloud.example:8810/v1/local/redeem" ||
pc.Token != "skb_bWFwbGUuc2VjcmV0LWludmVudGVkLTAwMDE" { // base64url("maple.secret-invented-0001")
t.Fatalf("decoded: %+v", pc)
}
if got := caSHA256(hostMadePairCodeCA); got != pc.CASHA256 {
t.Fatalf("the Host's fingerprint %s is not caSHA256 of its CA (%s)", pc.CASHA256, got)
}
if h, _, err := decodeToken(pc.Token); err != nil || h != "maple" {
t.Fatalf("the token's handle: %q %v", h, err)
}
}
// SECURITY: a key given twice is an extra key. A map would keep only the
// last, so two codes that differ in which token they carry could both read as
// "exactly v, u, ca and t".
func TestAPairingCodeWithAKeyGivenTwiceIsRefused(t *testing.T) {
fp := strings.Repeat("0f", 32)
u := "https://maple.example-id.cloud.example:8810/v1/local/redeem"
raw := `{"v":1,"u":"` + u + `","ca":"` + fp + `","t":"` + otherToken() + `","t":"` + homeToken() + `"}`
if _, err := decodePairCode(pairCodePrefix + base64.RawURLEncoding.EncodeToString([]byte(raw))); err == nil {
t.Fatal("a code naming t twice was read")
}
once := `{"v":1,"u":"` + u + `","ca":"` + fp + `","t":"` + homeToken() + `"}`
if _, err := decodePairCode(pairCodePrefix + base64.RawURLEncoding.EncodeToString([]byte(once))); err != nil {
t.Fatalf("the same code with t once: %v", err)
}
}
// pinnedChainCert is a certificate for the pinned-chain cases: signed by
// parent (self-signed when parent is nil), a CA or not, valid from nb to na.
func pinnedChainCert(t *testing.T, cn string, isCA bool, nb, na time.Time, parent *x509.Certificate, parentKey *ecdsa.PrivateKey, dns ...string) (*x509.Certificate, *ecdsa.PrivateKey) {
t.Helper()
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
tmpl := &x509.Certificate{SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: cn},
NotBefore: nb, NotAfter: na, DNSNames: dns, BasicConstraintsValid: true, IsCA: isCA}
if isCA {
tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
} else {
tmpl.KeyUsage, tmpl.ExtKeyUsage = x509.KeyUsageDigitalSignature, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
}
signer, signKey := tmpl, key
if parent != nil {
signer, signKey = parent, parentKey
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, signer, &key.PublicKey, signKey)
if err != nil {
t.Fatal(err)
}
c, _ := x509.ParseCertificate(der)
return c, key
}
// SECURITY: the cases of the pinned chain the TLS tests do not reach. A
// certificate with the code's fingerprint that is not a CA, and a CA that has
// expired, are refused; the pinned CA anywhere after the leaf, beside other
// certificates, is the one returned.
func TestThePinnedChainTakesOnlyAValidCAWithTheFingerprint(t *testing.T) {
now := time.Now()
fpOf := func(c *x509.Certificate) string { s := sha256.Sum256(c.Raw); return hex.EncodeToString(s[:]) }
ca, caKey := pinnedChainCert(t, "maple host CA", true, now.Add(-time.Hour), now.Add(24*time.Hour), nil, nil)
leaf, _ := pinnedChainCert(t, "leaf", false, now.Add(-time.Hour), now.Add(time.Hour), ca, caKey, tcpServerName)
other, _ := pinnedChainCert(t, "another CA", true, now.Add(-time.Hour), now.Add(24*time.Hour), nil, nil)
got, err := verifyPinnedChain([]*x509.Certificate{leaf, other, ca}, fpOf(ca))
if err != nil || caSHA256(got) != fpOf(ca) {
t.Fatalf("the pinned CA after another certificate: %v", err)
}
// A self-signed certificate that is not a CA, signing the leaf.
notCA, notCAKey := pinnedChainCert(t, "not a CA", false, now.Add(-time.Hour), now.Add(24*time.Hour), nil, nil)
leaf2, _ := pinnedChainCert(t, "leaf", false, now.Add(-time.Hour), now.Add(time.Hour), notCA, notCAKey, tcpServerName)
if _, err := verifyPinnedChain([]*x509.Certificate{leaf2, notCA}, fpOf(notCA)); err == nil {
t.Fatal("a certificate with the fingerprint that is not a CA was pinned")
}
// An expired CA, with a leaf it signed that is still in its dates.
old, oldKey := pinnedChainCert(t, "expired host CA", true, now.Add(-48*time.Hour), now.Add(-time.Hour), nil, nil)
leaf3, _ := pinnedChainCert(t, "leaf", false, now.Add(-2*time.Hour), now.Add(time.Hour), old, oldKey, tcpServerName)
if _, err := verifyPinnedChain([]*x509.Certificate{leaf3, old}, fpOf(old)); err == nil {
t.Fatal("an expired CA was pinned")
}
if _, err := verifyPinnedChain([]*x509.Certificate{leaf}, fpOf(ca)); err == nil {
t.Fatal("a chain of the leaf alone was accepted")
}
}
// Exit 3 means the same Host, so the same CA, on every TCP pairing: a
// --host-ca pair to a Host of a name already paired here under another CA is
// a plain failure, as the pairing code's is.
func TestHostCAFileWithASameNamedHostUnderAnotherCAIsNotAlreadyPaired(t *testing.T) {
pairingBox(t)
ca := newTestCA(t, "home host CA")
h := startTCPEnrollHost(t, ca, ca.serverCert(t, tcpServerName), "127.0.0.1:18841")
if _, _, err := pairWithTokenVia(homeToken(), h.url, writeCA(t, ca), false, false); err != nil {
t.Fatal(err)
}
var already *alreadyPairedError
if _, _, err := pairWithTokenVia(homeToken(), h.url, writeCA(t, ca), false, false); !errors.As(err, &already) {
t.Fatalf("the same Host under its own CA is already paired (exit 3): %v", err)
}
other := newTestCA(t, "another home")
h2 := startTCPEnrollHost(t, other, other.serverCert(t, tcpServerName), "127.0.0.1:18842")
_, _, err := pairWithTokenVia(homeToken(), h2.url, writeCA(t, other), false, false)
if err == nil || errors.As(err, &already) || !strings.Contains(err.Error(), "different Host named") {
t.Fatalf("a same-named Host under another CA is a plain refusal, not exit 3: %v", err)
}
if h2.hits.Load() != 0 {
t.Fatal("the refusal spent the other Host's token")
}
}
// SECURITY: the name in the question is the token's handle, which whoever
// made the token chose. A control character or escape sequence in it reaches
// the person's screen escaped, so it cannot rewrite the question.
func TestTheSecondHostQuestionEscapesTheTokensName(t *testing.T) {
ft := &fakeTerminal{answer: "n\n"}
ft.use(t)
if _, err := confirmAddHost("home.example.test", "evil\r\x1b[2K\x1b[1Ainnocent"); err != nil {
t.Fatal(err)
}
q := ft.asked.String()
if strings.ContainsAny(q, "\x1b\r") {
t.Fatalf("the question carries the handle's control bytes raw: %q", q)
}
if !strings.Contains(q, `Add "evil\r\x1b[2K\x1b[1Ainnocent" as well?`) {
t.Fatalf("the question: %q", q)
}
ft = &fakeTerminal{answer: "n\n"}
ft.use(t)
if _, err := confirmAddHost("home.example.test", "maple"); err != nil || !strings.Contains(ft.asked.String(), "Add maple as well?") {
t.Fatalf("a plain name is shown as it is: %q %v", ft.asked.String(), err)
}
}
// The name the redeem answers with is checked again after the redeem (the
// token's handle can differ from it). There too, the same name under another
// CA is a plain failure, never exit 3.
func TestARedeemAnsweringAPairedNameUnderAnotherCAIsNotAlreadyPaired(t *testing.T) {
pairingBox(t)
ca := newTestCA(t, "home host CA")
h := startTCPEnrollHost(t, ca, ca.serverCert(t, tcpServerName), "127.0.0.1:18843")
if _, _, err := pairWithTokenVia(homeToken(), h.url, writeCA(t, ca), false, false); err != nil {
t.Fatal(err)
}
other := newTestCA(t, "another home")
h2 := startTCPEnrollHost(t, other, chainWithCA(t, other), "127.0.0.1:18844")
// otherToken's handle is "other"; the Host answers home.example.test.
token, opts, err := pairOptsFor(makePairCode(t, map[string]any{"v": 1, "u": h2.url, "ca": pairCodeFingerprint(other), "t": otherToken()}), "", "", false)
if err != nil {
t.Fatal(err)
}
opts.askSecond = func(string, string) (bool, error) { return true, nil }
_, _, err = pairWith(token, opts)
var already *alreadyPairedError
if err == nil || errors.As(err, &already) || !strings.Contains(err.Error(), "different Host named home.example.test") {
t.Fatalf("a redeem naming a paired Host under another CA is a plain refusal, not exit 3: %v", err)
}
if cfgs, _ := loadPairings(); len(cfgs) != 1 {
t.Fatalf("%d pairings after the refusal", len(cfgs))
}
}