Initial structure
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jasper/quptime/internal/trust"
|
||||
)
|
||||
|
||||
// MinTLS is the minimum protocol version both sides require.
|
||||
const MinTLS = tls.VersionTLS13
|
||||
|
||||
// TLSAssets bundles the on-disk material needed to spin up either a
|
||||
// listener or a dialer. Build it once at daemon start and pass to
|
||||
// ServerConfig / ClientConfig.
|
||||
type TLSAssets struct {
|
||||
Cert []byte // PEM-encoded leaf cert
|
||||
Key *rsa.PrivateKey
|
||||
Trust *trust.Store
|
||||
}
|
||||
|
||||
// tlsCert wraps the local PEM cert + RSA key into a tls.Certificate.
|
||||
func (a *TLSAssets) tlsCert() (tls.Certificate, error) {
|
||||
block, _ := pem.Decode(a.Cert)
|
||||
if block == nil {
|
||||
return tls.Certificate{}, errors.New("cert PEM has no block")
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("parse leaf: %w", err)
|
||||
}
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{block.Bytes},
|
||||
PrivateKey: a.Key,
|
||||
Leaf: leaf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ServerConfig produces a tls.Config suitable for an inter-node
|
||||
// listener. Peers must present a certificate, and that certificate's
|
||||
// fingerprint must already be present in the trust store.
|
||||
func (a *TLSAssets) ServerConfig() (*tls.Config, error) {
|
||||
cert, err := a.tlsCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: MinTLS,
|
||||
ClientAuth: tls.RequireAnyClientCert,
|
||||
InsecureSkipVerify: true, // we do our own pinning via VerifyPeerCertificate
|
||||
VerifyPeerCertificate: a.Trust.VerifyPeerCert,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClientConfig produces a tls.Config suitable for dialing a peer.
|
||||
// expectedNodeID is optional: if non-empty, the handshake also
|
||||
// verifies that the cert's fingerprint matches the trust entry for
|
||||
// that node ID.
|
||||
func (a *TLSAssets) ClientConfig(expectedNodeID string) (*tls.Config, error) {
|
||||
cert, err := a.tlsCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verify := a.Trust.VerifyPeerCert
|
||||
if expectedNodeID != "" {
|
||||
verify = a.makeStrictVerifier(expectedNodeID)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: MinTLS,
|
||||
InsecureSkipVerify: true, // we do our own pinning via VerifyPeerCertificate
|
||||
VerifyPeerCertificate: verify,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InsecureBootstrapConfig is the client-side TLS config used only by
|
||||
// the TOFU prefetch (FetchPeerCert). It accepts any peer cert because
|
||||
// the caller has not yet established trust; the certificate is
|
||||
// surfaced to the operator for manual approval before being added to
|
||||
// the store. Never use this anywhere else.
|
||||
func (a *TLSAssets) InsecureBootstrapConfig() (*tls.Config, error) {
|
||||
cert, err := a.tlsCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: MinTLS,
|
||||
InsecureSkipVerify: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// makeStrictVerifier returns a VerifyPeerCertificate callback that
|
||||
// pins the connection to the trust entry of a specific node ID.
|
||||
func (a *TLSAssets) makeStrictVerifier(expectedNodeID string) func([][]byte, [][]*x509.Certificate) error {
|
||||
return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
if len(rawCerts) == 0 {
|
||||
return errors.New("peer presented no certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse peer cert: %w", err)
|
||||
}
|
||||
entry, ok := a.Trust.Get(expectedNodeID)
|
||||
if !ok {
|
||||
return fmt.Errorf("no trust entry for node %s", expectedNodeID)
|
||||
}
|
||||
got := fingerprintOf(cert)
|
||||
if got != entry.Fingerprint {
|
||||
return fmt.Errorf("fingerprint mismatch for %s: got %s want %s",
|
||||
expectedNodeID, got, entry.Fingerprint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user