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

implement the new handshake #20

Merged
merged 8 commits into from
Apr 9, 2019
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
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})
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved

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,
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved
},
}, 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)
Copy link
Member

Choose a reason for hiding this comment

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

Is this deterministic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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)
Copy link
Member

Choose a reason for hiding this comment

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

Are there any other curves we can use...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The Go TLS implementation doesn't support P224, and P256 is curve that produces the smallest signatures. We could also use P384 and P512.

Copy link
Member

Choose a reason for hiding this comment

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

Does it support curve25519?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

keyToCertificate does (as well as secp256k1), but the Go standard library doesn't. This was one of the reasons we had to invent this new handshake protocol.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, I made a mistake and thought it was ecdh not ecdsa. (curve25519 is DH, ed25519 is signature).

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{},
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved
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