223 lines
6.7 KiB
Go
223 lines
6.7 KiB
Go
package libp2ptls
|
|
|
|
import (
|
|
"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"
|
|
|
|
ic "github.com/libp2p/go-libp2p-core/crypto"
|
|
"github.com/libp2p/go-libp2p-core/peer"
|
|
)
|
|
|
|
const certValidityPeriod = 100 * 365 * 24 * time.Hour // ~100 years
|
|
const certificatePrefix = "libp2p-tls-handshake:"
|
|
const alpn string = "libp2p"
|
|
|
|
var extensionID = getPrefixedExtensionID([]int{1, 1})
|
|
|
|
type signedKey struct {
|
|
PubKey []byte
|
|
Signature []byte
|
|
}
|
|
|
|
// Identity is used to secure connections
|
|
type Identity struct {
|
|
config tls.Config
|
|
}
|
|
|
|
// NewIdentity creates a new identity
|
|
func NewIdentity(privKey ic.PrivKey) (*Identity, error) {
|
|
cert, err := keyToCertificate(privKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Identity{
|
|
config: tls.Config{
|
|
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")
|
|
},
|
|
NextProtos: []string{alpn},
|
|
SessionTicketsDisabled: true,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// ConfigForAny is a short-hand for ConfigForPeer("").
|
|
func (i *Identity) ConfigForAny() (*tls.Config, <-chan ic.PubKey) {
|
|
return i.ConfigForPeer("")
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// It should be used to create a new tls.Config before securing either an
|
|
// incoming or outgoing connection.
|
|
func (i *Identity) ConfigForPeer(remote peer.ID) (*tls.Config, <-chan ic.PubKey) {
|
|
keyCh := make(chan ic.PubKey, 1)
|
|
// We need to check the peer ID in the VerifyPeerCertificate callback.
|
|
// The tls.Config it is also used for listening, and we might also have concurrent dials.
|
|
// Clone it so we can check for the specific peer ID we're dialing here.
|
|
conf := i.config.Clone()
|
|
// We're using InsecureSkipVerify, so the verifiedChains parameter will always be empty.
|
|
// We need to parse the certificates ourselves from the raw certs.
|
|
conf.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
|
defer close(keyCh)
|
|
|
|
chain := make([]*x509.Certificate, len(rawCerts))
|
|
for i := 0; i < len(rawCerts); i++ {
|
|
cert, err := x509.ParseCertificate(rawCerts[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chain[i] = cert
|
|
}
|
|
|
|
pubKey, err := PubKeyFromCertChain(chain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if remote != "" && !remote.MatchesPublicKey(pubKey) {
|
|
return errors.New("peer IDs don't match")
|
|
}
|
|
keyCh <- pubKey
|
|
return nil
|
|
}
|
|
return conf, keyCh
|
|
}
|
|
|
|
// PubKeyFromCertChain verifies the certificate chain and extract the remote's public key.
|
|
func PubKeyFromCertChain(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(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)
|
|
}
|
|
|
|
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 := ic.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
|
|
}
|
|
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) (*tls.Certificate, error) {
|
|
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyBytes, err := ic.MarshalPublicKey(sk.GetPublic())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
value, err := asn1.Marshal(signedKey{
|
|
PubKey: keyBytes,
|
|
Signature: signature,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sn, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|