diff options
Diffstat (limited to 'internal/protocol/handshake.go')
| -rw-r--r-- | internal/protocol/handshake.go | 159 | +65 −94 |
1 files changed, 65 insertions, 94 deletions
diff --git a/internal/protocol/handshake.go b/internal/protocol/handshake.go index 0ad164a..f25cb0a 100644 --- a/internal/protocol/handshake.go +++ b/internal/protocol/handshake.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// Copyright (c) 2026 Nikolay Govorov // SPDX-License-Identifier: AGPL-3.0-or-later package protocol @@ -6,123 +6,94 @@ package protocol import ( "crypto/ed25519" "crypto/rand" - "crypto/tls" - "crypto/x509" - "encoding/pem" "errors" - "fmt" - "math/big" - "net/url" - "os" + "slices" "time" - - "dimidiumlabs/mirum/internal/config" ) var ( - ErrKeyNotPEM = errors.New("key file does not contain a PEM block") - ErrKeyNotPKCS8 = errors.New("key file does not contain a PKCS8 private key") - ErrKeyNotPKIX = errors.New("key file does not contain a PKIX public key") - ErrKeyNotEd25519 = errors.New("key file does not contain an ed25519 key") - - ErrClockSkew = errors.New("clock skew too large") + ErrInvalidPublicKey = errors.New("invalid public key size") + ErrInvalidNonce = errors.New("invalid nonce size") + ErrInvalidSignature = errors.New("invalid signature") + ErrClockSkew = errors.New("clock skew too large") + ErrServerRejected = errors.New("server rejected handshake") ) -// WorkerMeta describes the worker for embedding in a self-signed X.509 -// certificate as a URI SAN (mirum:worker?name=...&version=...&...). -type WorkerMeta struct { - Name string - Version string - Os string - Arch string - Runtime string +const NonceSize = 32 + +const EKMLabel = "mirum-handshake" +const EKMLength = 32 + +func generateNonce() ([]byte, error) { + nonce := make([]byte, NonceSize) + _, err := rand.Read(nonce) + return nonce, err } -// URI encodes worker metadata as a mirum: URI. -func (m *WorkerMeta) URI() *url.URL { - return &url.URL{ - Scheme: "mirum", - Opaque: "worker", - RawQuery: url.Values{ - "name": {m.Name}, - "version": {m.Version}, - "os": {m.Os}, - "arch": {m.Arch}, - "runtime": {m.Runtime}, - }.Encode(), - } +// ServerHandshake holds state for the server side of the handshake protocol. +type ServerHandshake struct { + publicKey ed25519.PublicKey + serverNonce []byte + ekm []byte // TLS Exported Keying Material, nil if not bound } -// ParseWorkerMeta extracts WorkerMeta from a certificate's URI SANs. -// Returns nil if no mirum:worker URI is found. -func ParseWorkerMeta(cert *x509.Certificate) *WorkerMeta { - for _, u := range cert.URIs { - if u.Scheme == "mirum" && u.Opaque == "worker" { - q := u.Query() - return &WorkerMeta{ - Name: q.Get("name"), - Version: q.Get("version"), - Os: q.Get("os"), - Arch: q.Get("arch"), - Runtime: q.Get("runtime"), - } - } - } - return nil +func NewServerHandshake() *ServerHandshake { + return &ServerHandshake{} } -// LoadPrivateKey reads a PEM-encoded PKCS8 ed25519 private key from path. -func LoadPrivateKey(path string) (ed25519.PrivateKey, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read key: %w", err) +// Challenge validates the worker public key and returns a random nonce. +// If ekm is non-nil, channel binding is enabled and the EKM will be +// included in the signed data during verification. +func (h *ServerHandshake) Challenge(publicKey, ekm []byte) (serverNonce []byte, err error) { + if len(publicKey) != ed25519.PublicKeySize { + return nil, ErrInvalidPublicKey } - block, _ := pem.Decode(data) - if block == nil { - return nil, ErrKeyNotPEM + h.publicKey = publicKey + h.ekm = ekm + h.serverNonce, err = generateNonce() + if err != nil { + return nil, err } - key, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrKeyNotPKCS8, err) + return h.serverNonce, nil +} + +// Verify checks the worker's ed25519 signature and clock skew. +// The signed data is nonce (or nonce || ekm if channel binding is enabled). +func (h *ServerHandshake) Verify(signature []byte, workerTime time.Time) error { + signed := slices.Concat(h.serverNonce, h.ekm) + if !ed25519.Verify(h.publicKey, signed, signature) { + return ErrInvalidSignature } - edKey, ok := key.(ed25519.PrivateKey) - if !ok { - return nil, ErrKeyNotEd25519 + skew := time.Since(workerTime).Abs() + if skew > time.Minute { + return ErrClockSkew } - return edKey, nil + return nil } -// SelfSignedCert generates a self-signed X.509 certificate from an ed25519 -// private key with worker metadata encoded as a URI SAN. The server extracts -// the public key for authentication, metadata from the URI, and uses NotBefore -// for clock skew detection. -func SelfSignedCert(key ed25519.PrivateKey, meta *WorkerMeta) (tls.Certificate, error) { - serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - return tls.Certificate{}, fmt.Errorf("generate serial: %w", err) - } +// ClientHandshake holds state for the client side of the handshake protocol. +type ClientHandshake struct { + privateKey ed25519.PrivateKey +} - now := time.Now() - tmpl := &x509.Certificate{ - SerialNumber: serial, - NotBefore: now, - NotAfter: now.Add(config.WorkerCertLifetime), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - URIs: []*url.URL{meta.URI()}, - } +func NewClientHandshake(privateKey ed25519.PrivateKey) *ClientHandshake { + return &ClientHandshake{privateKey: privateKey} +} - certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - if err != nil { - return tls.Certificate{}, fmt.Errorf("create certificate: %w", err) - } +// PublicKey returns the 32-byte ed25519 public key. +func (h *ClientHandshake) PublicKey() []byte { + return h.privateKey.Public().(ed25519.PublicKey) +} - return tls.Certificate{ - Certificate: [][]byte{certDER}, - PrivateKey: key, - }, nil +// Sign signs the server nonce (and optional EKM) with the worker's private key. +// If ekm is non-nil, the signed data is nonce || ekm. +func (h *ClientHandshake) Sign(serverNonce, ekm []byte) ([]byte, error) { + if len(serverNonce) != NonceSize { + return nil, ErrInvalidNonce + } + return ed25519.Sign(h.privateKey, slices.Concat(serverNonce, ekm)), nil } |
