Skip to content

Commit 9e45bc6

Browse files
committed
test(e2e): trust the node CA and check the node by its IP
Once a node commits its CA, the management listener presents a leaf signed by that CA for the network.address IP and the EST hostnames, without localhost, followed by the CA chain. The suite pinned the self-signed leaf and checked the name localhost, so the first call after the Root's ceremony failed the handshake. The suite now checks the node by its address, trusts the last certificate the listener presents (the self-signed leaf before a CA, the root after), switches to the root once the ceremony, the subordinate submit or the key import commits a CA, and tells boots apart by the leaf rather than the trust file. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
1 parent 3628e01 commit 9e45bc6

1 file changed

Lines changed: 54 additions & 19 deletions

File tree

‎test/integration/image_suite_test.go‎

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ type vm struct {
261261
keys io.WriteCloser
262262
serial *serialLog
263263
trust string
264+
leaf []byte
264265
admin admin
265266
stopped bool
266267
}
@@ -370,10 +371,11 @@ func (v *vm) startTPM(t *testing.T) string {
370371
}
371372

372373
// waitUp waits for the management listener after a boot that began at
373-
// started and re-pins the node's server certificate. The node mints a new one
374-
// on every boot, so a certificate other than prev means this boot is serving;
375-
// prev is empty for a first boot. The serial log is not used for this: init
376-
// logs through /dev/kmsg, which the kernel rate-limits, so a line can be lost.
374+
// started and re-pins the node's trust. The node mints a new server
375+
// certificate on every boot, so a leaf other than prev means this boot is
376+
// serving; prev is empty for a first boot. The serial log is not used for
377+
// this: init logs through /dev/kmsg, which the kernel rate-limits, so a line
378+
// can be lost.
377379
func (v *vm) waitUp(t *testing.T, prev []byte, started time.Time, what string) {
378380
t.Helper()
379381
limit := 3 * time.Minute
@@ -382,27 +384,56 @@ func (v *vm) waitUp(t *testing.T, prev []byte, started time.Time, what string) {
382384
}
383385
addr := "127.0.0.1:" + v.mgmtPort
384386
deadline := time.Now().Add(limit)
385-
var cert []byte
387+
var chain []*x509.Certificate
386388
for {
387-
cert = probeServerCert(addr)
388-
if cert != nil && !bytes.Equal(cert, prev) {
389+
chain = probeServerChain(addr)
390+
if chain != nil && !bytes.Equal(certPEM(chain[0]), prev) {
389391
break
390392
}
391393
if time.Now().After(deadline) {
392394
t.Fatalf("%s: %s did not bring the management API up within %s:\n%s", v.name, what, limit, lastLines(v.serial.String(), 60))
393395
}
394396
time.Sleep(time.Second)
395397
}
396-
v.trust = filepath.Join(v.dir, "trust.crt")
397-
writeFile(t, v.trust, cert)
398+
v.pin(t, chain)
398399
took := time.Since(started).Round(time.Second)
399400
t.Logf("%s: %s up in %s", v.name, what, took)
400401
v.s.timing("%s %s: management API up %s after %s", v.name, what, took, map[bool]string{true: "QEMU start", false: "the reboot request"}[prev == nil])
401402
}
402403

403-
// probeServerCert returns the PEM of the certificate the listener at addr
404-
// presents, or nil while it does not answer.
405-
func probeServerCert(addr string) []byte {
404+
// pin trusts the last certificate the listener presented: the self-signed
405+
// leaf before the node has a CA, and the root its CA-signed leaf chains to
406+
// after. The leaf is kept to tell the next boot's certificate apart.
407+
func (v *vm) pin(t *testing.T, chain []*x509.Certificate) {
408+
t.Helper()
409+
v.leaf = certPEM(chain[0])
410+
v.trust = filepath.Join(v.dir, "trust.crt")
411+
writeFile(t, v.trust, certPEM(chain[len(chain)-1]))
412+
}
413+
414+
// trustCA waits for the listener to present a leaf signed by the node's new
415+
// CA, which it switches to without a restart once the CA is committed, and
416+
// trusts the root that leaf chains to.
417+
func (v *vm) trustCA(t *testing.T) {
418+
t.Helper()
419+
addr := "127.0.0.1:" + v.mgmtPort
420+
deadline := time.Now().Add(2 * time.Minute)
421+
for {
422+
chain := probeServerChain(addr)
423+
if len(chain) > 1 && chain[len(chain)-1].IsCA && chain[0].CheckSignatureFrom(chain[1]) == nil {
424+
v.pin(t, chain)
425+
return
426+
}
427+
if time.Now().After(deadline) {
428+
t.Fatalf("%s: the management listener did not switch to a CA-signed certificate within 2m", v.name)
429+
}
430+
time.Sleep(time.Second)
431+
}
432+
}
433+
434+
// probeServerChain returns the certificates the listener at addr presents,
435+
// leaf first, or nil while it does not answer.
436+
func probeServerChain(addr string) []*x509.Certificate {
406437
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 2 * time.Second}, "tcp", addr,
407438
&tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS13}) //nolint:gosec // pinning grab
408439
if err != nil {
@@ -413,7 +444,11 @@ func probeServerCert(addr string) []byte {
413444
if len(certs) == 0 {
414445
return nil
415446
}
416-
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certs[0].Raw})
447+
return certs
448+
}
449+
450+
func certPEM(c *x509.Certificate) []byte {
451+
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})
417452
}
418453

419454
// ctl runs cryptosctl against the node as the bootstrap admin. The binary is
@@ -422,7 +457,7 @@ func (v *vm) ctl(stdin string, args ...string) (string, error) {
422457
full := append([]string{
423458
"--endpoint", "127.0.0.1:" + v.mgmtPort,
424459
"--identity", v.admin.cert, "--identity-key", v.admin.key,
425-
"--trust", v.trust, "--server-name", "localhost",
460+
"--trust", v.trust, "--server-name", suiteNodeAddr,
426461
}, args...)
427462
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
428463
defer cancel()
@@ -462,10 +497,7 @@ func (v *vm) status(t *testing.T) string {
462497
// come back.
463498
func (v *vm) reboot(t *testing.T, cn string) {
464499
t.Helper()
465-
prev, err := os.ReadFile(v.trust)
466-
if err != nil {
467-
t.Fatalf("%s: read the pinned server certificate: %v", v.name, err)
468-
}
500+
prev := v.leaf
469501
bootsBefore := bootCount(v.status(t))
470502
started := time.Now()
471503
out := v.mustCtl(t, "reboot", "--confirm", cn)
@@ -859,6 +891,7 @@ func stepRootCeremony(t *testing.T, s *suite, st *suiteState) {
859891
t.Fatalf("ceremony output missing %q:\n%s", want, out)
860892
}
861893
}
894+
st.root.trustCA(t)
862895
st.rootPEM = st.root.mustCtl(t, "identity", "show", "-o", "pem")
863896
st.rootCert = parsePEMCerts(t, []byte(st.rootPEM))[0]
864897
writeFile(t, filepath.Join(s.env.out, "root.pem"), []byte(st.rootPEM))
@@ -895,6 +928,7 @@ func stepIntermediate(t *testing.T, s *suite, st *suiteState) {
895928
chainPath := filepath.Join(st.inter.dir, "subordinate-chain.pem")
896929
writeFile(t, chainPath, []byte(chain))
897930
st.inter.mustCtl(t, "ca", "submit-subordinate-cert", "--chain", chainPath)
931+
st.inter.trustCA(t)
898932

899933
st.intPEM = st.inter.mustCtl(t, "identity", "show", "-o", "pem")
900934
certs := parsePEMCerts(t, []byte(st.intPEM))
@@ -1325,7 +1359,7 @@ func stepUpgrade(t *testing.T, s *suite, st *suiteState, v *vm, cn string) {
13251359
if !strings.Contains(staged, "Reboot pending: yes") {
13261360
t.Fatalf("%s: after staging, want a reboot pending:\n%s", v.name, staged)
13271361
}
1328-
prev, _ := os.ReadFile(v.trust)
1362+
prev := v.leaf
13291363
started := time.Now()
13301364
v.mustCtl(t, "image", "activate", "--confirm", cn)
13311365
v.waitUp(t, prev, started, "upgrade")
@@ -1406,6 +1440,7 @@ func stepEscrowImport(t *testing.T, s *suite, st *suiteState) {
14061440
if out, err := st.spare.ctl(st.backupPwd+"\n", "ca", "import-key", "--backup", st.backup); err != nil {
14071441
t.Fatalf("import the Root backup: %v\n%s", err, out)
14081442
}
1443+
st.spare.trustCA(t)
14091444
got := parsePEMCerts(t, []byte(st.spare.mustCtl(t, "identity", "show", "-o", "pem")))[0]
14101445
if !bytes.Equal(got.Raw, st.rootCert.Raw) {
14111446
t.Fatalf("the restored node holds %s (serial %s), not the Root", got.Subject, got.SerialNumber.Text(16))

0 commit comments

Comments
 (0)