Files
rollcall/cmd/rollcall/ca_test.go
T
Ilker ManapandClaude Opus 5.5 a495c33b66
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 41s
Rack certificate renewal: rack-owned state, atomic, grace period
Renewal could not work: rack join writes the certificate and key as
root, and the rack (user rollcall) failed to overwrite them. A lost
renewal answer also locked the rack out, since the region accepted only
the new serial.

- the renewed certificate and key go into one file in
  /var/lib/rollcall/rack-cert (cert_state_dir), written atomically, and
  are preferred at start when they are this rack's, from the CA, newer
- after a renewal the region accepts the previous serial for 7 days; a
  new join does not keep it
- rollcall rack renew renews now (run as the service user)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:22:20 +01:00

314 lines
11 KiB
Go

package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestCAAndRegionCert(t *testing.T) {
dir := t.TempDir()
ca, err := loadOrCreateCA(dir, "test CA")
if err != nil {
t.Fatal(err)
}
again, err := loadOrCreateCA(dir, "test CA")
if err != nil || again.Fingerprint() != ca.Fingerprint() {
t.Fatalf("reload gives another CA: %v", err)
}
pair, err := regionCert(ca, dir, []string{"rollcall.example", "fd00:213:77::1"})
if err != nil {
t.Fatal(err)
}
leaf, _ := x509.ParseCertificate(pair.Certificate[0])
for _, h := range []string{"rollcall.example", "fd00:213:77::1", "127.0.0.1", "localhost"} {
if err := leaf.VerifyHostname(h); err != nil {
t.Errorf("region certificate does not cover %s: %v", h, err)
}
}
if len(pair.Certificate) != 2 {
t.Errorf("the chain should carry the CA for pinning clients")
}
// unchanged names: the same certificate is reused
p2, _ := regionCert(ca, dir, []string{"rollcall.example", "fd00:213:77::1"})
if string(p2.Certificate[0]) != string(pair.Certificate[0]) {
t.Errorf("certificate reissued without a reason")
}
}
func TestJoinCodeAndPinnedTLS(t *testing.T) {
dir := t.TempDir()
ca, _ := loadOrCreateCA(dir, "test CA")
pair, _ := regionCert(ca, dir, nil)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) }))
srv.TLS = &tls.Config{Certificates: []tls.Certificate{pair}}
srv.StartTLS()
defer srv.Close()
var c joinCode
sum := certFingerprint(ca.Cert)
copy(c.Fingerprint[:], sum[:16])
c.Secret[0] = 7
c.Expires = time.Now().Add(time.Hour).Unix()
c.Addr = strings.TrimPrefix(srv.URL, "https://")
got, err := parseJoinCode(c.String())
if err != nil || got != c {
t.Fatalf("round trip: %+v %v", got, err)
}
ok := &http.Client{Transport: &http.Transport{TLSClientConfig: pinnedTLS(c.Fingerprint)}}
if resp, err := ok.Get(srv.URL); err != nil {
t.Fatalf("pinned CA rejected: %v", err)
} else {
resp.Body.Close()
}
other, _ := newCA("another")
osum := sha256.Sum256(other.Cert.Raw)
var wrong [16]byte
copy(wrong[:], osum[:16])
bad := &http.Client{Transport: &http.Transport{TLSClientConfig: pinnedTLS(wrong)}}
if _, err := bad.Get(srv.URL); err == nil {
t.Errorf("a server from another authority was accepted")
}
expired := c
expired.Expires = time.Now().Add(-time.Minute).Unix()
if _, err := parseJoinCode(expired.String()); err == nil {
t.Errorf("an expired code was accepted")
}
}
func TestSignRackCSR(t *testing.T) {
ca, _ := newCA("test CA")
a := &App{ca: ca, cfg: &Config{}}
_, csr, err := newCSR("rack:hall-b")
if err != nil {
t.Fatal(err)
}
_, cert, err := a.signRackCSR(csr, "hall-b")
if err != nil {
t.Fatal(err)
}
pool := x509.NewCertPool()
pool.AddCert(ca.Cert)
if _, err := cert.Verify(x509.VerifyOptions{Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}); err != nil {
t.Errorf("rack certificate is no client certificate: %v", err)
}
// a rack is a client only: no names, no server use, so it cannot pose as the region
if cert.Subject.CommonName != "rack:hall-b" || len(cert.IPAddresses)+len(cert.DNSNames) > 0 {
t.Errorf("subject %s, SANs %v %v", cert.Subject.CommonName, cert.IPAddresses, cert.DNSNames)
}
if _, err := cert.Verify(x509.VerifyOptions{Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}); err == nil {
t.Error("rack certificate verifies as a server certificate")
}
}
func TestIntermediate(t *testing.T) {
root, err := newCA("test root")
if err != nil {
t.Fatal(err)
}
if !root.canDelegate() {
t.Fatal("new roots must be able to delegate one level")
}
dns, nets, err := parsePermits("int.example.com, 10.213.0.0/16")
if err != nil || len(dns) != 1 || len(nets) != 1 {
t.Fatalf("permits: %v %v %v", dns, nets, err)
}
ikey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
_, icert, err := root.intermediate(&ikey.PublicKey, "caddy", dns, nets, 30*24*time.Hour)
if err != nil {
t.Fatal(err)
}
if !icert.IsCA || !icert.MaxPathLenZero || !icert.PermittedDNSDomainsCritical {
t.Fatalf("intermediate shape: %+v", icert)
}
sub := &CA{Cert: icert, Key: ikey}
roots, inters := x509.NewCertPool(), x509.NewCertPool()
roots.AddCert(root.Cert)
inters.AddCert(icert)
verify := func(hosts ...string) error {
_, leaf, err := sub.sign(&ikey.PublicKey, hosts[0], hosts, x509.ExtKeyUsageServerAuth)
if err != nil {
return err
}
_, err = leaf.Verify(x509.VerifyOptions{Roots: roots, Intermediates: inters, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}})
return err
}
if err := verify("node001.int.example.com", "10.213.77.50"); err != nil {
t.Errorf("leaf inside the constraints: %v", err)
}
if err := verify("www.google.com"); err == nil {
t.Error("leaf outside the DNS constraint verified")
}
if err := verify("node001.int.example.com", "192.168.1.1"); err == nil {
t.Error("leaf outside the IP constraint verified")
}
// nothing it issues is a client identity (admin:, rack:)
if _, leaf, err := sub.sign(&ikey.PublicKey, "admin:mallory", nil, x509.ExtKeyUsageClientAuth); err == nil {
if _, err := leaf.Verify(x509.VerifyOptions{Roots: roots, Intermediates: inters, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}); err == nil {
t.Error("the intermediate issued a client certificate that verifies")
}
}
// DNS only: no address may be certified (the region's IP, say)
dnsOnly, _, _ := parsePermits("int.example.com")
_, ic2, err := root.intermediate(&ikey.PublicKey, "dns-only", dnsOnly, nil, time.Hour)
if err != nil {
t.Fatal(err)
}
inters2 := x509.NewCertPool()
inters2.AddCert(ic2)
if _, leaf, err := (&CA{Cert: ic2, Key: ikey}).sign(&ikey.PublicKey, "x", []string{"10.0.0.26"}, x509.ExtKeyUsageServerAuth); err == nil {
if _, err := leaf.Verify(x509.VerifyOptions{Roots: roots, Intermediates: inters2}); err == nil {
t.Error("a DNS-only intermediate certified an IP address")
}
}
for _, bad := range []string{"*.", "", "foo bar"} {
if _, _, err := parsePermits(bad); err == nil {
t.Errorf("permit %q accepted", bad)
}
}
// a second level below the intermediate is refused by path length 0
k2, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
_, c2, err := sub.intermediate(&k2.PublicKey, "deeper", dns, nil, time.Hour)
if err == nil {
inters.AddCert(c2)
_, leaf, _ := (&CA{Cert: c2, Key: k2}).sign(&k2.PublicKey, "x.int.example.com", []string{"x.int.example.com"}, x509.ExtKeyUsageServerAuth)
if _, err := leaf.Verify(x509.VerifyOptions{Roots: roots, Intermediates: inters}); err == nil {
t.Error("two levels of intermediates verified")
}
}
// old roots (path length 0) refuse to delegate
old := *root
oc := *root.Cert
oc.MaxPathLen, oc.MaxPathLenZero = 0, true
old.Cert = &oc
if _, _, err := old.intermediate(&ikey.PublicKey, "x", dns, nil, time.Hour); err == nil {
t.Error("path-length-0 root signed an intermediate")
}
}
func TestUIRedirectsToHTTPS(t *testing.T) {
ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
try := func(admin, listen, remote, host string) (int, string) {
a := &App{cfg: &Config{AdminHTTP: admin, TLSListen: listen}}
req := httptest.NewRequest("GET", "/index.html?x=1", nil)
req.RemoteAddr, req.Host = remote, host
rec := httptest.NewRecorder()
a.uiOverHTTPS(ok).ServeHTTP(rec, req)
return rec.Code, rec.Header().Get("Location")
}
if c, l := try("loopback", ":443", "10.0.0.5:5555", "10.0.0.234"); c != 308 || l != "https://10.0.0.234/index.html?x=1" {
t.Errorf("remote over http: %d %s", c, l)
}
if c, l := try("deny", ":8443", "10.0.0.5:5555", "pxe.example:80"); c != 308 || l != "https://pxe.example:8443/index.html?x=1" {
t.Errorf("other port: %d %s", c, l)
}
if c, l := try("loopback", ":443", "[fd00::5]:5555", "[fd00::1]"); c != 308 || l != "https://[fd00::1]/index.html?x=1" {
t.Errorf("ipv6: %d %s", c, l)
}
if c, _ := try("loopback", ":443", "127.0.0.1:5555", "localhost"); c != 200 {
t.Errorf("loopback client redirected: %d", c)
}
if c, _ := try("allow", ":443", "10.0.0.5:5555", "10.0.0.234"); c != 200 {
t.Errorf("allow redirected: %d", c)
}
if c, _ := try("loopback", "", "10.0.0.5:5555", "10.0.0.234"); c != 200 {
t.Errorf("no TLS listener but redirected: %d", c)
}
}
func TestAdminClientCertificate(t *testing.T) {
ca, _ := newCA("test")
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
_, admin, _ := ca.sign(&key.PublicKey, "admin:t440s", nil, x509.ExtKeyUsageClientAuth)
_, server, _ := ca.sign(&key.PublicKey, "admin:t440s", []string{"x.example"}, x509.ExtKeyUsageServerAuth)
_, rack, _ := ca.sign(&key.PublicKey, "rack:r2", nil, x509.ExtKeyUsageClientAuth)
a := &App{cfg: &Config{AdminToken: "secret"}}
a.adminCC.serial, a.adminCC.at = map[string]string{admin.SerialNumber.Text(16): "t440s", server.SerialNumber.Text(16): "t440s"}, time.Now()
req := func(c *x509.Certificate, verified bool) *http.Request {
r := httptest.NewRequest("GET", "/api/v1/machines", nil)
r.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{c}}
if verified {
r.TLS.VerifiedChains = [][]*x509.Certificate{{c, ca.Cert}}
}
return r
}
if !a.isAdmin(req(admin, true)) {
t.Error("registered admin certificate refused")
}
if a.isAdmin(req(admin, false)) {
t.Error("unverified certificate accepted")
}
if a.isAdmin(req(server, true)) {
t.Error("server certificate accepted as admin")
}
if a.isAdmin(req(rack, true)) {
t.Error("rack certificate accepted as admin")
}
a.adminCC.serial = map[string]string{} // revoked
if a.isAdmin(req(admin, true)) {
t.Error("revoked certificate accepted")
}
tok := httptest.NewRequest("GET", "/", nil)
tok.Header.Set("Authorization", "Bearer secret")
if !a.isAdmin(tok) {
t.Error("token no longer works")
}
}
// A renewed certificate is kept in the rack's own directory and preferred
// when it is this rack's, from the CA and newer.
func TestRackRenewedCertificate(t *testing.T) {
ca, _ := newCA("test CA")
a := &App{ca: ca, cfg: &Config{}}
pool := x509.NewCertPool()
pool.AddCert(ca.Cert)
issue := func(name string) ([]byte, []byte, *x509.Certificate) {
key, csr, _ := newCSR("rack:" + name)
certPEM, cert, err := a.signRackCSR(csr, name)
if err != nil {
t.Fatal(err)
}
return certPEM, key, cert
}
_, _, joined := issue("hall-b")
dir := t.TempDir()
p := filepath.Join(dir, "rack.pem")
if loadRenewed(p, pool, "hall-b", joined) != nil {
t.Error("nothing renewed yet")
}
time.Sleep(1100 * time.Millisecond) // a later NotAfter
certPEM, key, renewed := issue("hall-b")
if err := writeAtomic(p, append(certPEM, key...), 0o600); err != nil {
t.Fatal(err)
}
got := loadRenewed(p, pool, "hall-b", joined)
if got == nil || got.Leaf.SerialNumber.Cmp(renewed.SerialNumber) != 0 {
t.Fatalf("renewed certificate not used: %v", got)
}
if loadRenewed(p, pool, "hall-c", joined) != nil {
t.Error("another rack's certificate was used")
}
if loadRenewed(p, pool, "hall-b", renewed) != nil {
t.Error("an older or equal certificate replaced the current one")
}
other, _ := newCA("other CA")
op := x509.NewCertPool()
op.AddCert(other.Cert)
if loadRenewed(p, op, "hall-b", joined) != nil {
t.Error("a certificate from another CA was used")
}
if st, _ := os.Stat(p); st.Mode().Perm() != 0o600 {
t.Errorf("mode %v", st.Mode())
}
}