Skip to content

feat: make certificate validation / creation configurable #3213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions p2p/security/tls/certificates.go
Original file line number Diff line number Diff line change
@@ -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
}
84 changes: 44 additions & 40 deletions p2p/security/tls/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package libp2ptls

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
Expand All @@ -15,6 +13,7 @@ import (
"math/big"
"os"
"runtime/debug"
"slices"
"time"

ic "github.com/libp2p/go-libp2p/core/crypto"
Expand All @@ -29,20 +28,31 @@ 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
}

// 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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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")
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -206,6 +235,7 @@ func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) {
if !valid {
return nil, errors.New("signature invalid")
}

return pubKey, nil
}

Expand Down Expand Up @@ -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
}
Comment on lines -242 to -263
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now lives as a part of the DefaultCertManager


// certTemplate returns the template for generating an Identity's TLS certificates.
func certTemplate() (*x509.Certificate, error) {
bigNum := big.NewInt(1 << 62)
Expand Down
2 changes: 1 addition & 1 deletion p2p/security/tls/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions p2p/transport/quic/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Comment on lines -111 to +110
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MarcoPolo is it the case that the peers identity has already been verified at this point?

e.g. is the authentication done here invalid aside from ensuring an already authenticated peer don't sneakily change its LibP2P identity?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We first calculate the the Peer's ID from the TLS handshake here: https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/transport.go#L288-L292 using the ConfigForPeer function.

This part gets peer's id so we can include it in the returned connection.

We are on the listening side here, so we don't have an expectation of what the peer should be (as opposed to dialing where you expect a certain peer id). We just return the peer id we learn about from the TLS handshake

if err != nil {
return nil, err
}
Expand Down