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 17
	"golang.org/x/sys/cpu"

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

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 136 137 138 139 140

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