// Package pin lets agents trust the master without a certificate authority. // The master serves a self-signed certificate for its own key pair; agents // are configured with the fingerprint of that public key and accept the // connection only if it matches. The TLS handshake proves the master holds // the private key, the same way SSH host keys work. package pin import ( "crypto" "crypto/ed25519" "crypto/rand" "crypto/sha256" "crypto/subtle" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "errors" "fmt" "math/big" "time" ) // GenerateKey creates a new master key pair. func GenerateKey() (ed25519.PrivateKey, error) { _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("generate key: %w", err) } return priv, nil } // Certificate returns a self-signed TLS certificate for key. Its validity is // effectively unlimited because trust comes from the pinned fingerprint, not // from the certificate's dates or issuer. func Certificate(key ed25519.PrivateKey) (tls.Certificate, error) { tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "package-updater-master"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().AddDate(100, 0, 0), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) if err != nil { return tls.Certificate{}, fmt.Errorf("create certificate: %w", err) } return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}, nil } // Fingerprint returns "sha256:" + hex SHA-256 of pub's PKIX DER encoding. // This is the MASTER_KEY value agents are configured with. func Fingerprint(pub crypto.PublicKey) (string, error) { der, err := x509.MarshalPKIXPublicKey(pub) if err != nil { return "", fmt.Errorf("marshal public key: %w", err) } sum := sha256.Sum256(der) return "sha256:" + hex.EncodeToString(sum[:]), nil } // ClientConfig returns a TLS config that accepts only a server whose // certificate carries the public key with the given fingerprint. func ClientConfig(fingerprint string) *tls.Config { return &tls.Config{ MinVersion: tls.VersionTLS13, // Chain and hostname verification are replaced by the fingerprint // check below: the master's certificate is self-signed by design. // The handshake still verifies the server's signature with the // certificate's key, so presenting a copied certificate fails. InsecureSkipVerify: true, //nolint:gosec // see comment above VerifyPeerCertificate: func(raw [][]byte, _ [][]*x509.Certificate) error { if len(raw) == 0 { return errors.New("pin: server sent no certificate") } cert, err := x509.ParseCertificate(raw[0]) if err != nil { return fmt.Errorf("pin: parse certificate: %w", err) } got, err := Fingerprint(cert.PublicKey) if err != nil { return err } if subtle.ConstantTimeCompare([]byte(got), []byte(fingerprint)) != 1 { return fmt.Errorf("pin: fingerprint mismatch: server presented %s", got) } return nil }, } }