Skip to content
This repository has been archived by the owner on Aug 19, 2022. It is now read-only.

Commit

Permalink
Merge pull request #20 from libp2p/new-handshake
Browse files Browse the repository at this point in the history
implement the new handshake
  • Loading branch information
marten-seemann authored Apr 9, 2019
2 parents 8b16d92 + 11dcfe4 commit a01e537
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ before_install:

script:
# some tests are randomized. Run them a few times.
- for i in `seq 1 3`; do
- for i in `seq 1 10`; do
ginkgo -r -v --cover --randomizeAllSpecs --randomizeSuites --trace --progress;
done

Expand Down
171 changes: 115 additions & 56 deletions crypto.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
package libp2ptls

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"time"

"golang.org/x/sys/cpu"

crypto "github.com/libp2p/go-libp2p-crypto"
ic "github.com/libp2p/go-libp2p-crypto"
pb "github.com/libp2p/go-libp2p-crypto/pb"
peer "github.com/libp2p/go-libp2p-peer"
)

const certValidityPeriod = 180 * 24 * time.Hour
const certValidityPeriod = 100 * 365 * 24 * time.Hour // ~100 years
const certificatePrefix = "libp2p-tls-handshake:"

var extensionID = getPrefixedExtensionID([]int{1, 1})

type signedKey struct {
PubKey []byte
Signature []byte
}

// Identity is used to secure connections
type Identity struct {
Expand All @@ -24,22 +37,21 @@ type Identity struct {

// NewIdentity creates a new identity
func NewIdentity(privKey ic.PrivKey) (*Identity, error) {
key, cert, err := keyToCertificate(privKey)
cert, err := keyToCertificate(privKey)
if err != nil {
return nil, err
}
return &Identity{
config: tls.Config{
MinVersion: tls.VersionTLS13,
InsecureSkipVerify: true, // This is not insecure here. We will verify the cert chain ourselves.
ClientAuth: tls.RequireAnyClientCert,
Certificates: []tls.Certificate{{
Certificate: [][]byte{cert.Raw},
PrivateKey: key,
}},
MinVersion: tls.VersionTLS13,
PreferServerCipherSuites: preferServerCipherSuites(),
InsecureSkipVerify: true, // This is not insecure here. We will verify the cert chain ourselves.
ClientAuth: tls.RequireAnyClientCert,
Certificates: []tls.Certificate{*cert},
VerifyPeerCertificate: func(_ [][]byte, _ [][]*x509.Certificate) error {
panic("tls config not specialized for peer")
},
SessionTicketsDisabled: true,
},
}, nil
}
Expand Down Expand Up @@ -95,70 +107,117 @@ func getRemotePubKey(chain []*x509.Certificate) (ic.PubKey, error) {
if len(chain) != 1 {
return nil, errors.New("expected one certificates in the chain")
}
cert := chain[0]
pool := x509.NewCertPool()
pool.AddCert(chain[0])
if _, err := chain[0].Verify(x509.VerifyOptions{Roots: pool}); err != nil {
pool.AddCert(cert)
if _, err := cert.Verify(x509.VerifyOptions{Roots: pool}); err != nil {
// If we return an x509 error here, it will be sent on the wire.
// Wrap the error to avoid that.
return nil, fmt.Errorf("certificate verification failed: %s", err)
}
remotePubKey, err := x509.MarshalPKIXPublicKey(chain[0].PublicKey)

var found bool
var keyExt pkix.Extension
// find the libp2p key extension, skipping all unknown extensions
for _, ext := range cert.Extensions {
if extensionIDEqual(ext.Id, extensionID) {
keyExt = ext
found = true
break
}
}
if !found {
return nil, errors.New("expected certificate to contain the key extension")
}
var sk signedKey
if _, err := asn1.Unmarshal(keyExt.Value, &sk); err != nil {
return nil, fmt.Errorf("unmarshalling signed certificate failed: %s", err)
}
pubKey, err := crypto.UnmarshalPublicKey(sk.PubKey)
if err != nil {
return nil, fmt.Errorf("unmarshalling public key failed: %s", err)
}
certKeyPub, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, err
}
switch chain[0].PublicKeyAlgorithm {
case x509.RSA:
return ic.UnmarshalRsaPublicKey(remotePubKey)
case x509.ECDSA:
return ic.UnmarshalECDSAPublicKey(remotePubKey)
default:
return nil, fmt.Errorf("unexpected public key algorithm: %d", chain[0].PublicKeyAlgorithm)
valid, err := pubKey.Verify(append([]byte(certificatePrefix), certKeyPub...), sk.Signature)
if err != nil {
return nil, fmt.Errorf("signature verification failed: %s", err)
}
if !valid {
return nil, errors.New("signature invalid")
}
return pubKey, nil
}

func keyToCertificate(sk ic.PrivKey) (crypto.PrivateKey, *x509.Certificate, error) {
sn, err := rand.Int(rand.Reader, big.NewInt(1<<62))
func keyToCertificate(sk ic.PrivKey) (*tls.Certificate, error) {
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
tmpl := &x509.Certificate{
SerialNumber: sn,
NotBefore: time.Now().Add(-24 * time.Hour),
NotAfter: time.Now().Add(certValidityPeriod),
return nil, err
}

var privateKey crypto.PrivateKey
var publicKey crypto.PublicKey
raw, err := sk.Raw()
keyBytes, err := crypto.MarshalPublicKey(sk.GetPublic())
if err != nil {
return nil, nil, err
return nil, err
}
switch sk.Type() {
case pb.KeyType_RSA:
k, err := x509.ParsePKCS1PrivateKey(raw)
if err != nil {
return nil, nil, err
}
publicKey = &k.PublicKey
privateKey = k
case pb.KeyType_ECDSA:
k, err := x509.ParseECPrivateKey(raw)
if err != nil {
return nil, nil, err
}
publicKey = &k.PublicKey
privateKey = k
// TODO: add support for Ed25519
default:
return nil, nil, errors.New("unsupported key type for TLS")
certKeyPub, err := x509.MarshalPKIXPublicKey(certKey.Public())
if err != nil {
return nil, err
}
signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
if err != nil {
return nil, err
}
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, publicKey, privateKey)
value, err := asn1.Marshal(signedKey{
PubKey: keyBytes,
Signature: signature,
})
if err != nil {
return nil, nil, err
return nil, err
}
cert, err := x509.ParseCertificate(certDER)

sn, err := rand.Int(rand.Reader, big.NewInt(1<<62))
if err != nil {
return nil, nil, err
return nil, err
}
return privateKey, cert, nil
tmpl := &x509.Certificate{
SerialNumber: sn,
NotBefore: time.Time{},
NotAfter: time.Now().Add(certValidityPeriod),
// after calling CreateCertificate, these will end up in Certificate.Extensions
ExtraExtensions: []pkix.Extension{
{Id: extensionID, 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
}

// We want nodes without AES hardware (e.g. ARM) support to always use ChaCha.
// Only if both nodes have AES hardware support (e.g. x86), AES should be used.
// 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.
func preferServerCipherSuites() bool {
// Copied from the Go TLS implementation.

// Check the cpu flags for each platform that has optimized GCM implementations.
// Worst case, these variables will just all be false.
var (
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
// Keep in sync with crypto/aes/cipher_s390x.go.
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM)

hasGCMAsm = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X
)
return !hasGCMAsm
}
22 changes: 22 additions & 0 deletions extension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package libp2ptls

var extensionPrefix = []int{1, 3, 6, 1, 4, 1, 53594}

// getPrefixedExtensionID returns an Object Identifier
// that can be used in x509 Certificates.
func getPrefixedExtensionID(suffix []int) []int {
return append(extensionPrefix, suffix...)
}

// extensionIDEqual compares two extension IDs.
func extensionIDEqual(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
19 changes: 19 additions & 0 deletions extension_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package libp2ptls

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Extensions", func() {
It("generates a prefixed extension ID", func() {
Expect(getPrefixedExtensionID([]int{13, 37})).To(Equal([]int{1, 3, 6, 1, 4, 1, 53594, 13, 37}))
})

It("compares extension IDs", func() {
Expect(extensionIDEqual([]int{1, 2, 3, 4}, []int{1, 2, 3, 4})).To(BeTrue())
Expect(extensionIDEqual([]int{1, 2, 3, 4}, []int{1, 2, 3})).To(BeFalse())
Expect(extensionIDEqual([]int{1, 2, 3}, []int{1, 2, 3, 4})).To(BeFalse())
Expect(extensionIDEqual([]int{1, 2, 3, 4}, []int{4, 3, 2, 1})).To(BeFalse())
})
})
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
golang.org/x/net v0.0.0-20190310074541-c10a0554eabf // indirect
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa // indirect
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"hash": "QmbyjEKtxXmZdiByBiNhfayzEuEPPBvuD2dLpHky8cHUvy",
"name": "go-conn-security",
"version": "0.1.20"
},
{
"author": "marten-seemann",
"hash": "QmauyZ2BbYbcTJr1RudEsbQoKNgKkZi9pDZBJVyzw3uJD4",
"name": "cpu",
"version": "0.1.0"
}
],
"gxVersion": "0.12.1",
Expand Down
Loading

0 comments on commit a01e537

Please sign in to comment.