Skip to content
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

Support generating custom x509 certificates #1481

Merged
Merged
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
100 changes: 74 additions & 26 deletions p2p/security/tls/crypto.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package libp2ptls

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
Expand Down Expand Up @@ -38,9 +39,37 @@ type Identity struct {
config tls.Config
}

// IdentityConfig is used to configure an Identity
type IdentityConfig struct {
CertTemplate *x509.Certificate
}

// IdentityOption transforms an IdentityConfig to apply optional settings.
type IdentityOption func(r *IdentityConfig)

// WithCertTemplate specifies the template to use when generating a new certificate.
func WithCertTemplate(template *x509.Certificate) IdentityOption {
return func(c *IdentityConfig) {
c.CertTemplate = template
}
}

// NewIdentity creates a new identity
func NewIdentity(privKey ic.PrivKey) (*Identity, error) {
cert, err := keyToCertificate(privKey)
func NewIdentity(privKey ic.PrivKey, opts ...IdentityOption) (*Identity, error) {
config := IdentityConfig{}
for _, opt := range opts {
opt(&config)
}

var err error
if config.CertTemplate == nil {
config.CertTemplate, err = certTemplate()
if err != nil {
return nil, err
}
}

cert, err := keyToCertificate(privKey, config.CertTemplate)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -167,60 +196,79 @@ func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) {
return pubKey, nil
}

func keyToCertificate(sk ic.PrivKey) (*tls.Certificate, error) {
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}

// GenerateSignedExtension uses the provided private key to sign the public key, and returns the
// signature within a pkix.Extension.
// This extension is included in a certificate to cryptographically tie it to the libp2p private key.
func GenerateSignedExtension(sk ic.PrivKey, pubKey crypto.PublicKey) (pkix.Extension, error) {
keyBytes, err := ic.MarshalPublicKey(sk.GetPublic())
if err != nil {
return nil, err
return pkix.Extension{}, err
}
certKeyPub, err := x509.MarshalPKIXPublicKey(certKey.Public())
certKeyPub, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return nil, err
return pkix.Extension{}, err
}
signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
if err != nil {
return nil, err
return pkix.Extension{}, err
}
value, err := asn1.Marshal(signedKey{
PubKey: keyBytes,
Signature: signature,
})
if err != nil {
return pkix.Extension{}, err
}

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)
sn, err := rand.Int(rand.Reader, bigNum)
if err != nil {
return nil, err
}

subjectSN, err := rand.Int(rand.Reader, bigNum)
if err != nil {
return nil, err
}
tmpl := &x509.Certificate{

return &x509.Certificate{
SerialNumber: sn,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(certValidityPeriod),
// According to RFC 3280, the issuer field must be set,
// see https://datatracker.ietf.org/doc/html/rfc3280#section-4.1.2.4.
Subject: pkix.Name{SerialNumber: subjectSN.String()},
// after calling CreateCertificate, these will end up in Certificate.Extensions
ExtraExtensions: []pkix.Extension{
{Id: extensionID, Critical: extensionCritical, Value: value},
},
}
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, certKey.Public(), certKey)
if err != nil {
return nil, err
}
return &tls.Certificate{
Certificate: [][]byte{certDER},
PrivateKey: certKey,
}, nil
}

Expand All @@ -229,7 +277,7 @@ func keyToCertificate(sk ic.PrivKey) (*tls.Certificate, error) {
// x86->x86: AES, ARM->x86: ChaCha, x86->ARM: ChaCha and ARM->ARM: Chacha
// This function returns true if we don't have AES hardware support, and false otherwise.
// Thus, ARM servers will always use their own cipher suite preferences (ChaCha first),
// and x86 servers will aways use the client's cipher suite preferences.
// and x86 servers will always use the client's cipher suite preferences.
func preferServerCipherSuites() bool {
// Copied from the Go TLS implementation.

Expand Down
48 changes: 48 additions & 0 deletions p2p/security/tls/crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package libp2ptls

import (
"crypto/x509"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewIdentityCertificates(t *testing.T) {
_, key := createPeer(t)
cn := "a.test.name"
email := "unittest@example.com"

t.Run("NewIdentity with default template", func(t *testing.T) {
// Generate an identity using the default template
id, err := NewIdentity(key)
assert.NoError(t, err)

// Extract the x509 certificate
x509Cert, err := x509.ParseCertificate(id.config.Certificates[0].Certificate[0])
assert.NoError(t, err)

// verify the common name and email are not set
assert.Empty(t, x509Cert.Subject.CommonName)
assert.Empty(t, x509Cert.EmailAddresses)
})

t.Run("NewIdentity with custom template", func(t *testing.T) {
tmpl, err := certTemplate()
assert.NoError(t, err)

tmpl.Subject.CommonName = cn
tmpl.EmailAddresses = []string{email}

// Generate an identity using the custom template
id, err := NewIdentity(key, WithCertTemplate(tmpl))
assert.NoError(t, err)

// Extract the x509 certificate
x509Cert, err := x509.ParseCertificate(id.config.Certificates[0].Certificate[0])
assert.NoError(t, err)

// verify the common name and email are set
assert.Equal(t, cn, x509Cert.Subject.CommonName)
assert.Equal(t, email, x509Cert.EmailAddresses[0])
})
}
54 changes: 44 additions & 10 deletions p2p/security/tls/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ func TestHandshakeSucceeds(t *testing.T) {
clientID, clientKey := createPeer(t)
serverID, serverKey := createPeer(t)

handshake := func(t *testing.T) {
clientTransport, err := New(clientKey)
require.NoError(t, err)
serverTransport, err := New(serverKey)
require.NoError(t, err)

handshake := func(t *testing.T, clientTransport *Transport, serverTransport *Transport) {
clientInsecureConn, serverInsecureConn := connect(t)

serverConnChan := make(chan sec.SecureConn)
Expand Down Expand Up @@ -109,15 +104,54 @@ func TestHandshakeSucceeds(t *testing.T) {
require.Equal(t, string(b), "foobar")
}

t.Run("with extension not critical", func(t *testing.T) {
handshake(t)
// Use standard transports with default TLS configuration
clientTransport, err := New(clientKey)
require.NoError(t, err)
serverTransport, err := New(serverKey)
require.NoError(t, err)

t.Run("standard TLS with extension not critical", func(t *testing.T) {
handshake(t, clientTransport, serverTransport)
})

t.Run("standard TLS with extension critical", func(t *testing.T) {
extensionCritical = true
t.Cleanup(func() { extensionCritical = false })

handshake(t, clientTransport, serverTransport)
})

// Use transports with custom TLS certificates

// override client identity to use a custom certificate
clientCertTmlp, err := certTemplate()
require.NoError(t, err)

clientCertTmlp.Subject.CommonName = "client.test.name"
clientCertTmlp.EmailAddresses = []string{"client-unittest@example.com"}

clientTransport.identity, err = NewIdentity(clientKey, WithCertTemplate(clientCertTmlp))
require.NoError(t, err)

// override server identity to use a custom certificate
serverCertTmpl, err := certTemplate()
require.NoError(t, err)

serverCertTmpl.Subject.CommonName = "server.test.name"
serverCertTmpl.EmailAddresses = []string{"server-unittest@example.com"}

serverTransport.identity, err = NewIdentity(serverKey, WithCertTemplate(serverCertTmpl))
require.NoError(t, err)

t.Run("custom TLS with extension not critical", func(t *testing.T) {
handshake(t, clientTransport, serverTransport)
})

t.Run("with extension critical", func(t *testing.T) {
t.Run("custom TLS with extension critical", func(t *testing.T) {
extensionCritical = true
t.Cleanup(func() { extensionCritical = false })

handshake(t)
handshake(t, clientTransport, serverTransport)
})
}

Expand Down