92 lines
3.1 KiB
Go
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
|
|
},
|
|
}
|
|
}
|