192 lines
5.8 KiB
Go
192 lines
5.8 KiB
Go
// Package crypto stellt die Transportverschlüsselung zwischen Agent und
|
||
// Server sicher (Spec Abschnitt 3.1):
|
||
//
|
||
// - Server: selbstsigniertes ECDSA-P-256-Zertifikat, einmalig erzeugt und
|
||
// dauerhaft im Datenverzeichnis abgelegt; nur TLS 1.3.
|
||
// - Agent: Zertifikats-Pinning über den SHA-256-Fingerprint, unabhängig
|
||
// von der Adresse. Einen blinden Erstkontakt gibt es nicht: Der
|
||
// Fingerprint kommt aus der Einladung, danach aus node.json.
|
||
// - Enrollment-Token: sltn_enroll_<secret>.<fingerprint>[.<server-url>].
|
||
// Der Server speichert nur das Secret (gehasht), der Agent nutzt
|
||
// Fingerprint und Adresse für den Erstkontakt.
|
||
package crypto
|
||
|
||
import (
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/base64"
|
||
"encoding/pem"
|
||
"errors"
|
||
"fmt"
|
||
"math/big"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
serverKeyFile = "server.key"
|
||
serverCertFile = "server.crt"
|
||
certValidity = 20 * 365 * 24 * time.Hour
|
||
)
|
||
|
||
var (
|
||
// ErrIncompleteCert: Nur Schlüssel oder nur Zertifikat vorhanden. Es
|
||
// wird bewusst nichts neu erzeugt, weil das alle Pins der Agents brechen
|
||
// würde.
|
||
ErrIncompleteCert = errors.New("crypto: server.key und server.crt müssen beide vorhanden sein oder beide fehlen")
|
||
// ErrInsecureKeyFile: server.key ist für Gruppe oder andere lesbar.
|
||
ErrInsecureKeyFile = errors.New("crypto: server.key hat zu offene Rechte (erwartet 0600)")
|
||
// ErrInvalidFingerprint: Kein gültiger Fingerprint.
|
||
ErrInvalidFingerprint = errors.New("crypto: ungültiger Fingerprint")
|
||
)
|
||
|
||
// Fingerprint ist der SHA-256 des DER-kodierten Zertifikats, Base64url ohne
|
||
// Padding (43 Zeichen) – kompakt genug für den Enrollment-Token.
|
||
type Fingerprint string
|
||
|
||
// FingerprintOf berechnet den Fingerprint eines Zertifikats.
|
||
func FingerprintOf(cert *x509.Certificate) Fingerprint {
|
||
sum := sha256.Sum256(cert.Raw)
|
||
return Fingerprint(base64.RawURLEncoding.EncodeToString(sum[:]))
|
||
}
|
||
|
||
// ParseFingerprint prüft und übernimmt einen Fingerprint.
|
||
func ParseFingerprint(s string) (Fingerprint, error) {
|
||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||
if err != nil || len(b) != sha256.Size {
|
||
return "", fmt.Errorf("%w: %q", ErrInvalidFingerprint, s)
|
||
}
|
||
return Fingerprint(s), nil
|
||
}
|
||
|
||
// ServerCert ist das TLS-Zertifikat des Servers samt Fingerprint.
|
||
type ServerCert struct {
|
||
Certificate tls.Certificate
|
||
Fingerprint Fingerprint
|
||
}
|
||
|
||
// Leaf liefert das geparste Zertifikat.
|
||
func (s ServerCert) Leaf() *x509.Certificate { return s.Certificate.Leaf }
|
||
|
||
// LoadOrCreateServerCert lädt server.key/server.crt aus dir oder erzeugt sie
|
||
// beim allerersten Start. Das Zertifikat ist 20 Jahre gültig: Agents pinnen
|
||
// den Fingerprint, ein Wechsel würde alle aussperren.
|
||
func LoadOrCreateServerCert(dir string) (ServerCert, error) {
|
||
keyPath, certPath := filepath.Join(dir, serverKeyFile), filepath.Join(dir, serverCertFile)
|
||
keyExists, err := exists(keyPath)
|
||
if err != nil {
|
||
return ServerCert{}, err
|
||
}
|
||
certExists, err := exists(certPath)
|
||
if err != nil {
|
||
return ServerCert{}, err
|
||
}
|
||
switch {
|
||
case keyExists && certExists:
|
||
case !keyExists && !certExists:
|
||
if err := createServerCert(keyPath, certPath); err != nil {
|
||
return ServerCert{}, err
|
||
}
|
||
default:
|
||
return ServerCert{}, fmt.Errorf("%w (in %s)", ErrIncompleteCert, dir)
|
||
}
|
||
|
||
info, err := os.Stat(keyPath)
|
||
if err != nil {
|
||
return ServerCert{}, err
|
||
}
|
||
if info.Mode().Perm()&0o077 != 0 {
|
||
return ServerCert{}, fmt.Errorf("%w: %s hat %o", ErrInsecureKeyFile, keyPath, info.Mode().Perm())
|
||
}
|
||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||
if err != nil {
|
||
return ServerCert{}, fmt.Errorf("crypto: Server-Zertifikat laden: %w", err)
|
||
}
|
||
if cert.Leaf == nil {
|
||
if cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil {
|
||
return ServerCert{}, err
|
||
}
|
||
}
|
||
return ServerCert{Certificate: cert, Fingerprint: FingerprintOf(cert.Leaf)}, nil
|
||
}
|
||
|
||
func exists(path string) (bool, error) {
|
||
_, err := os.Stat(path)
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
return false, nil
|
||
}
|
||
return err == nil, err
|
||
}
|
||
|
||
func createServerCert(keyPath, certPath string) error {
|
||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
now := time.Now()
|
||
tmpl := &x509.Certificate{
|
||
SerialNumber: serial,
|
||
Subject: pkix.Name{CommonName: "sultan-server"},
|
||
NotBefore: now.Add(-time.Hour),
|
||
NotAfter: now.Add(certValidity),
|
||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
BasicConstraintsValid: true,
|
||
}
|
||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := writeNew(keyPath, 0o600, &pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}); err != nil {
|
||
return err
|
||
}
|
||
if err := writeNew(certPath, 0o644, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
|
||
os.Remove(keyPath)
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// writeNew legt path neu an (nie überschreiben) und setzt die Rechte
|
||
// unabhängig von der umask.
|
||
func writeNew(path string, perm os.FileMode, block *pem.Block) error {
|
||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
err = pem.Encode(f, block)
|
||
if cerr := f.Close(); err == nil {
|
||
err = cerr
|
||
}
|
||
if err == nil {
|
||
err = os.Chmod(path, perm)
|
||
}
|
||
if err != nil {
|
||
os.Remove(path)
|
||
}
|
||
return err
|
||
}
|
||
|
||
// ServerTLSConfig liefert die TLS-Konfiguration für den Agent-Port (:8443):
|
||
// nur TLS 1.3.
|
||
func ServerTLSConfig(sc ServerCert) *tls.Config {
|
||
return &tls.Config{
|
||
Certificates: []tls.Certificate{sc.Certificate},
|
||
MinVersion: tls.VersionTLS13,
|
||
}
|
||
}
|