From 80c93cb4a98142aacce7437b56cce6ebb0b9ab44 Mon Sep 17 00:00:00 2001 From: "Ethan J. Gallant" Date: Wed, 26 Feb 2025 20:42:35 -0400 Subject: [PATCH] Make Certificate Validation / Creation Configurable --- p2p/security/tls/certificates.go | 149 +++++++++++++++++++++++++++++++ p2p/security/tls/crypto.go | 84 ++++++++--------- p2p/security/tls/crypto_test.go | 2 +- p2p/transport/quic/listener.go | 3 +- 4 files changed, 195 insertions(+), 43 deletions(-) create mode 100644 p2p/security/tls/certificates.go diff --git a/p2p/security/tls/certificates.go b/p2p/security/tls/certificates.go new file mode 100644 index 0000000000..630158488a --- /dev/null +++ b/p2p/security/tls/certificates.go @@ -0,0 +1,149 @@ +package libp2ptls + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + ic "github.com/libp2p/go-libp2p/core/crypto" +) + +// DefaultCertManager is the default certificate manager that creates self-signed certificates +type DefaultCertManager struct{} + +// CreateCertificate generates a new ECDSA private key and corresponding x509 certificate. +// The certificate includes an extension that cryptographically ties it to the provided libp2p +// private key to authenticate TLS connections. +func (m *DefaultCertManager) CreateCertificate(privKey ic.PrivKey, template *x509.Certificate) (*tls.Certificate, error) { + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + // Generate the signed extension that binds the certificate to the libp2p key + extension, err := GenerateSignedExtension(privKey, certKey.Public()) + if err != nil { + return nil, err + } + + template.ExtraExtensions = append(template.ExtraExtensions, extension) + + // Self-signed certificate + certDER, err := x509.CreateCertificate(rand.Reader, template, template, certKey.Public(), certKey) + if err != nil { + return nil, err + } + + return &tls.Certificate{ + Certificate: [][]byte{certDER}, + PrivateKey: certKey, + }, nil +} + +// CACertManager is a certificate manager that uses a CA to sign certificates +type CACertManager struct { + CACert *x509.Certificate + CAPrivKey crypto.PrivateKey + CAPool *x509.CertPool + defaultCertManager *DefaultCertManager +} + +// NewCACertManager creates a new CA certificate manager from a file +func NewCACertManager(caCertPath string) (*CACertManager, error) { + caCertPEM, err := os.ReadFile(caCertPath) + if err != nil { + return nil, err + } + + block, rest := pem.Decode(caCertPEM) + if block == nil || block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no valid CA cert found in %s", caCertPath) + } + + caCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + blockKey, _ := pem.Decode(rest) + if blockKey == nil { + return nil, fmt.Errorf("no CA private key found in %s", caCertPath) + } + + caKey, err := x509.ParsePKCS8PrivateKey(blockKey.Bytes) + if err != nil { + return nil, err + } + + caPool := x509.NewCertPool() + caPool.AddCert(caCert) + + return &CACertManager{ + CACert: caCert, + CAPrivKey: caKey, + CAPool: caPool, + }, nil +} + +// CreateCertificate generates a CA-signed certificate +func (m *CACertManager) CreateCertificate(privKey ic.PrivKey, template *x509.Certificate) (*tls.Certificate, error) { + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + extension, err := GenerateSignedExtension(privKey, certKey.Public()) + if err != nil { + return nil, err + } + + template.ExtraExtensions = append(template.ExtraExtensions, extension) + + // CA-signed certificate (not self-signed) + certDER, err := x509.CreateCertificate(rand.Reader, template, m.CACert, certKey.Public(), m.CAPrivKey) + if err != nil { + return nil, err + } + + return &tls.Certificate{ + Certificate: [][]byte{certDER, m.CACert.Raw}, + PrivateKey: certKey, + }, nil +} + +// VerifyCertChain first adds CA verification on top of the default certificate verification +func (m *CACertManager) VerifyCertChain(chain []*x509.Certificate) (ic.PubKey, error) { + // Ensure there's at least one certificate beyond the CA certificate. + if len(chain) < 2 { + return nil, fmt.Errorf("insufficient certificate chain length") + } + + opts := x509.VerifyOptions{ + Roots: m.CAPool, // trusted root certificate + Intermediates: x509.NewCertPool(), + } + + for _, cert := range chain[1:] { + opts.Intermediates.AddCert(cert) + } + + // Verify the leaf certificate + _, err := chain[0].Verify(opts) + if err != nil { + return nil, fmt.Errorf("full chain verification failed: %v", err) + } + + // Verify first cert against the default cert manager + pubKey, err := m.defaultCertManager.VerifyCertChain(chain[0:1]) + if err != nil { + return nil, fmt.Errorf("cert verification failed: %v", err) + } + + return pubKey, nil +} diff --git a/p2p/security/tls/crypto.go b/p2p/security/tls/crypto.go index 70a594d060..44d57da2ee 100644 --- a/p2p/security/tls/crypto.go +++ b/p2p/security/tls/crypto.go @@ -2,8 +2,6 @@ package libp2ptls import ( "crypto" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" @@ -15,6 +13,7 @@ import ( "math/big" "os" "runtime/debug" + "slices" "time" ic "github.com/libp2p/go-libp2p/core/crypto" @@ -29,6 +28,15 @@ const alpn string = "libp2p" var extensionID = getPrefixedExtensionID([]int{1, 1}) var extensionCritical bool // so we can mark the extension critical in tests +// CertManager defines an interface for TLS certificate management operations +type CertManager interface { + // CreateCertificate generates a certificate using the provided private key and template + CreateCertificate(privateKey ic.PrivKey, template *x509.Certificate) (*tls.Certificate, error) + + // VerifyCertChain verifies the certificate chain and extracts the public key + VerifyCertChain(chain []*x509.Certificate) (ic.PubKey, error) +} + type signedKey struct { PubKey []byte Signature []byte @@ -36,13 +44,15 @@ type signedKey struct { // Identity is used to secure connections type Identity struct { - config tls.Config + config tls.Config + certManager CertManager } // IdentityConfig is used to configure an Identity type IdentityConfig struct { CertTemplate *x509.Certificate KeyLogWriter io.Writer + CertManager CertManager } // IdentityOption transforms an IdentityConfig to apply optional settings. @@ -55,6 +65,13 @@ func WithCertTemplate(template *x509.Certificate) IdentityOption { } } +// WithCertManager sets a custom certificate manager +func WithCertManager(cm CertManager) IdentityOption { + return func(c *IdentityConfig) { + c.CertManager = cm + } +} + // WithKeyLogWriter optionally specifies a destination for TLS master secrets // in NSS key log format that can be used to allow external programs // such as Wireshark to decrypt TLS connections. @@ -74,19 +91,26 @@ func NewIdentity(privKey ic.PrivKey, opts ...IdentityOption) (*Identity, error) opt(&config) } - var err error + // Use default cert manager if none provided + if config.CertManager == nil { + config.CertManager = &DefaultCertManager{} + } + if config.CertTemplate == nil { + var err error config.CertTemplate, err = certTemplate() if err != nil { return nil, err } } - cert, err := keyToCertificate(privKey, config.CertTemplate) + cert, err := config.CertManager.CreateCertificate(privKey, config.CertTemplate) if err != nil { return nil, err } + return &Identity{ + certManager: config.CertManager, config: tls.Config{ MinVersion: tls.VersionTLS13, InsecureSkipVerify: true, // This is not insecure here. We will verify the cert chain ourselves. @@ -102,6 +126,12 @@ func NewIdentity(privKey ic.PrivKey, opts ...IdentityOption) (*Identity, error) }, nil } +// CertManager returns the certificate manager used by the identity +// to create and verify certificates. +func (i *Identity) CertManager() CertManager { + return i.certManager +} + // ConfigForPeer creates a new single-use tls.Config that verifies the peer's // certificate chain and returns the peer's public key via the channel. If the // peer ID is empty, the returned config will accept any peer. @@ -136,10 +166,11 @@ func (i *Identity) ConfigForPeer(remote peer.ID) (*tls.Config, <-chan ic.PubKey) chain[i] = cert } - pubKey, err := PubKeyFromCertChain(chain) + pubKey, err := i.certManager.VerifyCertChain(chain) if err != nil { return err } + if remote != "" && !remote.MatchesPublicKey(pubKey) { peerID, err := peer.IDFromPublicKey(pubKey) if err != nil { @@ -154,7 +185,7 @@ func (i *Identity) ConfigForPeer(remote peer.ID) (*tls.Config, <-chan ic.PubKey) } // PubKeyFromCertChain verifies the certificate chain and extract the remote's public key. -func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) { +func (m *DefaultCertManager) VerifyCertChain(chain []*x509.Certificate) (ic.PubKey, error) { if len(chain) != 1 { return nil, errors.New("expected one certificates in the chain") } @@ -168,13 +199,11 @@ func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) { if extensionIDEqual(ext.Id, extensionID) { keyExt = ext found = true - for i, oident := range cert.UnhandledCriticalExtensions { - if oident.Equal(ext.Id) { - // delete the extension from UnhandledCriticalExtensions - cert.UnhandledCriticalExtensions = append(cert.UnhandledCriticalExtensions[:i], cert.UnhandledCriticalExtensions[i+1:]...) - break - } - } + //delete the extension from UnhandledCriticalExtensions + cert.UnhandledCriticalExtensions = slices.DeleteFunc(cert.UnhandledCriticalExtensions, func(oid asn1.ObjectIdentifier) bool { + return oid.Equal(ext.Id) + }) + break } } @@ -206,6 +235,7 @@ func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) { if !valid { return nil, errors.New("signature invalid") } + return pubKey, nil } @@ -236,32 +266,6 @@ func GenerateSignedExtension(sk ic.PrivKey, pubKey crypto.PublicKey) (pkix.Exten return pkix.Extension{Id: extensionID, Critical: extensionCritical, Value: value}, nil } -// keyToCertificate generates a new ECDSA private key and corresponding x509 certificate. -// The certificate includes an extension that cryptographically ties it to the provided libp2p -// private key to authenticate TLS connections. -func keyToCertificate(sk ic.PrivKey, certTmpl *x509.Certificate) (*tls.Certificate, error) { - certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - - // after calling CreateCertificate, these will end up in Certificate.Extensions - extension, err := GenerateSignedExtension(sk, certKey.Public()) - if err != nil { - return nil, err - } - certTmpl.ExtraExtensions = append(certTmpl.ExtraExtensions, extension) - - certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, certTmpl, certKey.Public(), certKey) - if err != nil { - return nil, err - } - return &tls.Certificate{ - Certificate: [][]byte{certDER}, - PrivateKey: certKey, - }, nil -} - // certTemplate returns the template for generating an Identity's TLS certificates. func certTemplate() (*x509.Certificate, error) { bigNum := big.NewInt(1 << 62) diff --git a/p2p/security/tls/crypto_test.go b/p2p/security/tls/crypto_test.go index efec949095..c288fe5cc2 100644 --- a/p2p/security/tls/crypto_test.go +++ b/p2p/security/tls/crypto_test.go @@ -89,7 +89,7 @@ func TestVectors(t *testing.T) { cert, err := x509.ParseCertificate(data) require.NoError(t, err) - key, err := PubKeyFromCertChain([]*x509.Certificate{cert}) + key, err := (&DefaultCertManager{}).VerifyCertChain([]*x509.Certificate{cert}) if tc.error != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.error) diff --git a/p2p/transport/quic/listener.go b/p2p/transport/quic/listener.go index 30868e49eb..0440f60fc8 100644 --- a/p2p/transport/quic/listener.go +++ b/p2p/transport/quic/listener.go @@ -9,7 +9,6 @@ import ( "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" tpt "github.com/libp2p/go-libp2p/core/transport" - p2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" "github.com/libp2p/go-libp2p/p2p/transport/quicreuse" ma "github.com/multiformats/go-multiaddr" "github.com/quic-go/quic-go" @@ -108,7 +107,7 @@ func (l *listener) wrapConnWithScope(qconn quic.Connection, connScope network.Co // Since we don't have any way of knowing which tls.Config was used though, // we have to re-determine the peer's identity here. // Therefore, this is expected to never fail. - remotePubKey, err := p2ptls.PubKeyFromCertChain(qconn.ConnectionState().TLS.PeerCertificates) + remotePubKey, err := l.transport.identity.CertManager().VerifyCertChain(qconn.ConnectionState().TLS.PeerCertificates) if err != nil { return nil, err }