crypto.go 6.64 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 54 55 56 57 58 59 60
			VerifyPeerCertificate: func(_ [][]byte, _ [][]*x509.Certificate) error {
				panic("tls config not specialized for peer")
			},
		},
	}, 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
61 62
}

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

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

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

104
// getRemotePubKey derives the remote's public key from the certificate chain.
Marten Seemann's avatar
Marten Seemann committed
105 106 107 108
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
109
	cert := chain[0]
Marten Seemann's avatar
Marten Seemann committed
110
	pool := x509.NewCertPool()
Marten Seemann's avatar
Marten Seemann committed
111 112
	pool.AddCert(cert)
	if _, err := cert.Verify(x509.VerifyOptions{Roots: pool}); err != nil {
113 114 115
		// 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
116
	}
Marten Seemann's avatar
Marten Seemann committed
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

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

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

Marten Seemann's avatar
Marten Seemann committed
159
	keyBytes, err := crypto.MarshalPublicKey(sk.GetPublic())
Marten Seemann's avatar
Marten Seemann committed
160
	if err != nil {
Marten Seemann's avatar
Marten Seemann committed
161
		return nil, err
Marten Seemann's avatar
Marten Seemann committed
162
	}
Marten Seemann's avatar
Marten Seemann committed
163 164 165 166
	certKeyPub, err := x509.MarshalPKIXPublicKey(certKey.Public())
	if err != nil {
		return nil, err
	}
167
	signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
Marten Seemann's avatar
Marten Seemann committed
168 169 170 171 172 173 174 175 176
	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
177
	}
Marten Seemann's avatar
Marten Seemann committed
178 179

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

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