Files

92 lines
3.1 KiB
Go

// 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
},
}
}