crypto.go 6.67 KB
Newer Older
Marten Seemann's avatar
Marten Seemann committed
1 2 3
package libp2ptls

import (
Marten Seemann's avatar
Marten Seemann committed
4 5
	"crypto/ecdsa"
	"crypto/elliptic"
Marten Seemann's avatar
Marten Seemann committed
6 7 8
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
Marten Seemann's avatar
Marten Seemann committed
9 10
	"crypto/x509/pkix"
	"encoding/asn1"
Marten Seemann's avatar
Marten Seemann committed
11
	"errors"
Marten Seemann's avatar
Marten Seemann committed
12
	"fmt"
Marten Seemann's avatar
Marten Seemann committed
13 14 15
	"math/big"
	"time"

16
	crypto "github.com/libp2p/go-libp2p-crypto"
17 18
	"golang.org/x/sys/cpu"

19 20
	ic "github.com/libp2p/go-libp2p-core/crypto"
	"github.com/libp2p/go-libp2p-core/peer"
Marten Seemann's avatar
Marten Seemann committed
21 22
)

Marten Seemann's avatar
Marten Seemann committed
23
const certValidityPeriod = 100 * 365 * 24 * time.Hour // ~100 years
24
const certificatePrefix = "libp2p-tls-handshake:"
Marten Seemann's avatar
Marten Seemann committed
25 26 27 28 29 30 31

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

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

Marten Seemann's avatar
Marten Seemann committed
33 34
// Identity is used to secure connections
type Identity struct {
35
	config tls.Config
Marten Seemann's avatar
Marten Seemann committed
36 37 38
}

// NewIdentity creates a new identity
39
func NewIdentity(privKey ic.PrivKey) (*Identity, error) {
Marten Seemann's avatar
Marten Seemann committed
40
	cert, err := keyToCertificate(privKey)
Marten Seemann's avatar
Marten Seemann committed
41 42 43
	if err != nil {
		return nil, err
	}
44 45
	return &Identity{
		config: tls.Config{
46 47 48 49 50
			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},
51 52 53
			VerifyPeerCertificate: func(_ [][]byte, _ [][]*x509.Certificate) error {
				panic("tls config not specialized for peer")
			},
Marten Seemann's avatar
Marten Seemann committed
54
			SessionTicketsDisabled: true,
55 56 57 58 59 60 61
		},
	}, nil
}

// ConfigForAny is a short-hand for ConfigForPeer("").
func (i *Identity) ConfigForAny() (*tls.Config, <-chan ic.PubKey) {
	return i.ConfigForPeer("")
Marten Seemann's avatar
Marten Seemann committed
62 63
}

64 65 66 67 68 69
// 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.
70 71
func (i *Identity) ConfigForPeer(
	remote peer.ID,
72 73
) (*tls.Config, <-chan ic.PubKey) {
	keyCh := make(chan ic.PubKey, 1)
Marten Seemann's avatar
Marten Seemann committed
74 75 76
	// 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.
77
	conf := i.config.Clone()
Marten Seemann's avatar
Marten Seemann committed
78 79 80
	// 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 {
81 82
		defer close(keyCh)

Marten Seemann's avatar
Marten Seemann committed
83 84 85 86 87 88 89 90
		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
		}
91

Marten Seemann's avatar
Marten Seemann committed
92 93 94 95
		pubKey, err := getRemotePubKey(chain)
		if err != nil {
			return err
		}
96
		if remote != "" && !remote.MatchesPublicKey(pubKey) {
Marten Seemann's avatar
Marten Seemann committed
97 98
			return errors.New("peer IDs don't match")
		}
99
		keyCh <- pubKey
Marten Seemann's avatar
Marten Seemann committed
100 101
		return nil
	}
102
	return conf, keyCh
Marten Seemann's avatar
Marten Seemann committed
103 104
}

105
// getRemotePubKey derives the remote's public key from the certificate chain.
Marten Seemann's avatar
Marten Seemann committed
106 107 108 109
func getRemotePubKey(chain []*x509.Certificate) (ic.PubKey, error) {
	if len(chain) != 1 {
		return nil, errors.New("expected one certificates in the chain")
	}
Marten Seemann's avatar
Marten Seemann committed
110
	cert := chain[0]
Marten Seemann's avatar
Marten Seemann committed
111
	pool := x509.NewCertPool()
Marten Seemann's avatar
Marten Seemann committed
112 113
	pool.AddCert(cert)
	if _, err := cert.Verify(x509.VerifyOptions{Roots: pool}); err != nil {
114 115 116
		// 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)
Marten Seemann's avatar
Marten Seemann committed
117
	}
Marten Seemann's avatar
Marten Seemann committed
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135

	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)
	}
136
	pubKey, err := ic.UnmarshalPublicKey(sk.PubKey)
Marten Seemann's avatar
Marten Seemann committed
137 138 139 140
	if err != nil {
		return nil, fmt.Errorf("unmarshalling public key failed: %s", err)
	}
	certKeyPub, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
Marten Seemann's avatar
Marten Seemann committed
141 142 143
	if err != nil {
		return nil, err
	}
144
	valid, err := pubKey.Verify(append([]byte(certificatePrefix), certKeyPub...), sk.Signature)
Marten Seemann's avatar
Marten Seemann committed
145 146
	if err != nil {
		return nil, fmt.Errorf("signature verification failed: %s", err)
Marten Seemann's avatar
Marten Seemann committed
147
	}
Marten Seemann's avatar
Marten Seemann committed
148 149 150 151
	if !valid {
		return nil, errors.New("signature invalid")
	}
	return pubKey, nil
Marten Seemann's avatar
Marten Seemann committed
152 153
}

Marten Seemann's avatar
Marten Seemann committed
154 155
func keyToCertificate(sk ic.PrivKey) (*tls.Certificate, error) {
	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Marten Seemann's avatar
Marten Seemann committed
156
	if err != nil {
Marten Seemann's avatar
Marten Seemann committed
157
		return nil, err
Marten Seemann's avatar
Marten Seemann committed
158 159
	}

Marten Seemann's avatar
Marten Seemann committed
160
	keyBytes, err := crypto.MarshalPublicKey(sk.GetPublic())
Marten Seemann's avatar
Marten Seemann committed
161
	if err != nil {
Marten Seemann's avatar
Marten Seemann committed
162
		return nil, err
Marten Seemann's avatar
Marten Seemann committed
163
	}
Marten Seemann's avatar
Marten Seemann committed
164 165 166 167
	certKeyPub, err := x509.MarshalPKIXPublicKey(certKey.Public())
	if err != nil {
		return nil, err
	}
168
	signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
Marten Seemann's avatar
Marten Seemann committed
169 170 171 172 173 174 175 176 177
	if err != nil {
		return nil, err
	}
	value, err := asn1.Marshal(signedKey{
		PubKey:    keyBytes,
		Signature: signature,
	})
	if err != nil {
		return nil, err
Marten Seemann's avatar
Marten Seemann committed
178
	}
Marten Seemann's avatar
Marten Seemann committed
179 180

	sn, err := rand.Int(rand.Reader, big.NewInt(1<<62))
Marten Seemann's avatar
Marten Seemann committed
181
	if err != nil {
Marten Seemann's avatar
Marten Seemann committed
182 183 184 185 186 187 188 189 190 191
		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},
		},
Marten Seemann's avatar
Marten Seemann committed
192
	}
Marten Seemann's avatar
Marten Seemann committed
193
	certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, certKey.Public(), certKey)
Marten Seemann's avatar
Marten Seemann committed
194
	if err != nil {
Marten Seemann's avatar
Marten Seemann committed
195
		return nil, err
Marten Seemann's avatar
Marten Seemann committed
196
	}
Marten Seemann's avatar
Marten Seemann committed
197 198 199 200
	return &tls.Certificate{
		Certificate: [][]byte{certDER},
		PrivateKey:  certKey,
	}, nil
Marten Seemann's avatar
Marten Seemann committed
201
}
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223

// 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
}