erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
1 parent
9d5789c5d7
commit
c8df9d7651
41 files changed
+6455
-36
No files matched your search
@@ -0,0 +1,191 @@
|
||||
// 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
|
||||
// (known_servers). Einen blinden Erstkontakt gibt es nicht: Beim ersten
|
||||
// Verbinden muss der Fingerprint aus dem Enrollment-Token passen.
|
||||
// - Enrollment-Token: sltn_enroll_<secret>.<fingerprint>. Der Server
|
||||
// speichert nur das Secret (gehasht), der Agent nutzt den Fingerprint
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user