package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" "net" "os" "path/filepath" "time" ) // The bus's own certificate, made by the mesh rather than borrowed from an image. Moved here from mesh-controller's // `broker certificate` with the same output (design 47's L3 s11): the command belongs to the bus module, and the // controller's binary is retired after its lease is handed away (ADR 0547 ยง11). Nothing changes at the lease write: the // certificate in /var/lib/mesh-broker-tls stays, and every pin stays valid. // // **The foundation asked a third-party image for a tool it never said must be there** (novox/hq // 04-ISSUES/146). The bootstrap made this certificate by running `openssl` inside the broker's // image, which worked while the broker was one that happened to carry it and stopped the day the // bus changed: the new one has a shell and no openssl, so the step exited 127 and no mesh could be // raised. Substituting another image the bundle names does not help โ€” none of them carry it // either. // // So the program that needs a certificate makes one. It is the mesh's own binary, already on the // machine at this point in the bootstrap (the schema step ran it), and it needs nothing from the // image it writes into but a mounted directory. // // **Self-signed, and that is the design** โ€” a host pins this server's exact certificate and // authenticates with a password (novox/hq ADR 0004). There is no authority above it to ask, and at // this moment in a bootstrap there is no mesh to ask one of. // // Idempotent, because the step is applied again on every reconcile and a second certificate would // be one the hosts that pinned the first no longer believe. // busCertificateNames is what the bus is reached by: the container name on a mesh network, and the // loopback address the machine's own foundation dials. var busCertificateNames = []string{"mesh-broker"} const busCertificateLife = 10 * 365 * 24 * time.Hour // busCertificate makes the bus's certificate in a directory, or says whether one is there. // // mesh-nats-snapshot certificate --into /tls make it if it is not there // mesh-nats-snapshot certificate --check --into /tls exit non-zero unless a usable pair is func busCertificate(args []string) error { into, check := "", false for i := 0; i < len(args); i++ { switch args[i] { case "--check": check = true case "--into": if i+1 >= len(args) { return errors.New("--into needs a directory") } into = args[i+1] i++ default: return fmt.Errorf("certificate [--check] --into : %q", args[i]) } } if into == "" { return errors.New("certificate [--check] --into ") } crt, key := filepath.Join(into, "tls.crt"), filepath.Join(into, "tls.key") if usable, err := busCertificateUsable(crt, key); err != nil { return err } else if usable { fmt.Printf("the bus already has a certificate at %s, and it was left alone\n", crt) return nil } if check { // Said as a failure, because that is what the caller asked: a bootstrap's verify runs // this and a false answer is what makes the step run. return fmt.Errorf("no usable certificate and key at %s", into) } return writeBusCertificate(crt, key) } // busCertificateUsable says whether a certificate and its key are both there and parse. // // Both, and parsed rather than stat'ed: a half-written pair is the state a bootstrap interrupted // between the two files leaves behind, and a step that treated it as done would hand the server a // certificate with no key and report success. func busCertificateUsable(crt, key string) (bool, error) { certPEM, err := os.ReadFile(crt) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, err } keyPEM, err := os.ReadFile(key) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, err } if _, err := tlsPairParses(certPEM, keyPEM); err != nil { return false, nil } return true, nil } func tlsPairParses(certPEM, keyPEM []byte) (*x509.Certificate, error) { block, _ := pem.Decode(certPEM) if block == nil || block.Type != "CERTIFICATE" { return nil, errors.New("not a certificate") } certificate, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyPEM) if keyBlock == nil { return nil, errors.New("not a key") } if _, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes); err != nil { if _, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes); err != nil { return nil, err } } return certificate, nil } func writeBusCertificate(crt, key string) error { private, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return err } template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: busCertificateNames[0]}, DNSNames: busCertificateNames, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(busCertificateLife), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } der, err := x509.CreateCertificate(rand.Reader, template, template, &private.PublicKey, private) if err != nil { return err } pkcs8, err := x509.MarshalPKCS8PrivateKey(private) if err != nil { return err } // **The key first, and only then the certificate**, so the pair a reader finds is never a // certificate whose key has not been written yet โ€” the one order in which an interruption // leaves something that looks finished (novox/hq 04-ISSUES/014, a key present and unusable). if err := os.WriteFile(key, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}), 0o600); err != nil { return err } if err := os.WriteFile(crt, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644); err != nil { return err } fmt.Printf("made the bus a certificate for %v, valid until %s\n %s\n %s\n", busCertificateNames, template.NotAfter.Format(time.RFC3339), crt, key) return nil }