421 lines
12 KiB
Go
421 lines
12 KiB
Go
package tls
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
dmcrypto "git.clan.lol/clan/data-mesher/pkg/crypto"
|
|
ic "github.com/libp2p/go-libp2p/core/crypto"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
"github.com/libp2p/go-libp2p/core/peerstore"
|
|
"github.com/libp2p/go-libp2p/core/sec"
|
|
)
|
|
|
|
const (
|
|
PeerMetaNetwork string = "network"
|
|
PeerMetaName string = "name"
|
|
|
|
alpn string = "data-mesher"
|
|
certificatePrefix = "data-mesher-tls-handshake:"
|
|
certValidityPeriod = 100 * 365 * 24 * time.Hour // ~100 years
|
|
)
|
|
|
|
func PutPeerMetaNetwork(ps peerstore.Peerstore, id peer.ID, networkID *dmcrypto.PublicKey) error {
|
|
if err := ps.Put(id, PeerMetaNetwork, networkID); err != nil {
|
|
return fmt.Errorf("failed to put peer network ID in peer store: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetPeerMetaNetwork(ps peerstore.Peerstore, id peer.ID) (*dmcrypto.PublicKey, error) {
|
|
entry, err := ps.Get(id, PeerMetaNetwork)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get peer network ID from peer store: %w", err)
|
|
}
|
|
|
|
networkID, ok := entry.(*dmcrypto.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("peer network ID is not a *dmcrypto.PublicKey: %T", entry)
|
|
}
|
|
|
|
return networkID, nil
|
|
}
|
|
|
|
func PutPeerName(ps peerstore.Peerstore, id peer.ID, machineName string) error {
|
|
if err := ps.Put(id, PeerMetaName, machineName); err != nil {
|
|
return fmt.Errorf("failed to put peer name in peer store: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetPeerName(ps peerstore.Peerstore, id peer.ID) (string, error) {
|
|
entry, err := ps.Get(id, PeerMetaName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get peer name from peer store: %w", err)
|
|
}
|
|
|
|
machineName, ok := entry.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("peer name is not a string: %T", entry)
|
|
}
|
|
|
|
return machineName, nil
|
|
}
|
|
|
|
type signedKey struct {
|
|
PubKey []byte
|
|
Signature []byte
|
|
}
|
|
|
|
// Identity is used to secure connections.
|
|
type Identity struct {
|
|
config tls.Config
|
|
caKeys []*dmcrypto.PublicKey
|
|
}
|
|
|
|
// NewIdentity creates a new TLS identity that embeds both the libp2p signed key extension and the data-mesher CA-signed
|
|
// certificate in the TLS certificate.
|
|
func NewIdentity(
|
|
identityKey ic.PrivKey,
|
|
identityCert *dmcrypto.Certificate,
|
|
caKeys []*dmcrypto.PublicKey,
|
|
) (*Identity, error) {
|
|
certTmpl, err := certTemplate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cert, err := keyToCertificate(identityKey, certTmpl, identityCert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Identity{
|
|
config: tls.Config{
|
|
MinVersion: tls.VersionTLS13,
|
|
InsecureSkipVerify: true, //nolint:gosec // G402: peer certificate verification is done in VerifyPeerCertificate
|
|
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,
|
|
},
|
|
caKeys: caKeys,
|
|
}, nil
|
|
}
|
|
|
|
// ConfigForPeer creates a new single-use tls.Config that verifies the peer's certificate chain (both libp2p key binding
|
|
// and CA certificate) and returns the peer's public key via the channel.
|
|
func (i *Identity) ConfigForPeer(remote peer.ID, peerstore peerstore.Peerstore) (*tls.Config, <-chan ic.PubKey) {
|
|
keyCh := make(chan ic.PubKey, 1)
|
|
conf := i.config.Clone()
|
|
|
|
conf.VerifyConnection = func(_ tls.ConnectionState) error { return nil }
|
|
conf.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) (err error) {
|
|
// recover from panics in the peer certificate verification logic
|
|
defer func() {
|
|
if recoverErr := recover(); recoverErr != nil {
|
|
_, _ = fmt.Fprintf(
|
|
os.Stderr,
|
|
"panic when processing peer certificate in TLS handshake: %s\n%s\n",
|
|
recoverErr, debug.Stack(),
|
|
)
|
|
|
|
err = fmt.Errorf("panic when processing peer certificate in TLS handshake: %s", recoverErr)
|
|
}
|
|
}()
|
|
|
|
// ensure the key channel is closed when we're done
|
|
defer close(keyCh)
|
|
|
|
// parse the certificate chain
|
|
chain := make([]*x509.Certificate, len(rawCerts))
|
|
for idx := range rawCerts {
|
|
cert, parseErr := x509.ParseCertificate(rawCerts[idx])
|
|
if parseErr != nil {
|
|
return fmt.Errorf("failed to parse certificate: %w", parseErr)
|
|
}
|
|
|
|
chain[idx] = cert
|
|
}
|
|
|
|
// step 1: extract and verify the libp2p public key (same logic as upstream)
|
|
pubKey, pubKeyErr := PubKeyFromCertChain(chain)
|
|
if pubKeyErr != nil {
|
|
return pubKeyErr
|
|
}
|
|
|
|
// step 2: check peer ID if expected
|
|
peerID, err := peer.IDFromPublicKey(pubKey)
|
|
if err != nil {
|
|
peerID = peer.ID(fmt.Sprintf("(not determined: %s)", err.Error()))
|
|
}
|
|
|
|
if remote != "" && !remote.MatchesPublicKey(pubKey) {
|
|
return sec.ErrPeerIDMismatch{Expected: remote, Actual: peerID}
|
|
}
|
|
|
|
// step 3: verify the data-mesher CA certificate
|
|
caKey, machineName, verifyErr := VerifyCACert(chain, i.caKeys, pubKey)
|
|
if verifyErr != nil {
|
|
return verifyErr
|
|
}
|
|
|
|
// step 4: record the caKey (network id) and machine name in the peerstore
|
|
if err = PutPeerMetaNetwork(peerstore, peerID, caKey); err != nil {
|
|
return fmt.Errorf("failed to record network ID in peer store: %w", err)
|
|
}
|
|
|
|
if err = PutPeerName(peerstore, peerID, machineName); err != nil {
|
|
return fmt.Errorf("failed to record machine name in peer store: %w", err)
|
|
}
|
|
|
|
// send the public key to the channel
|
|
keyCh <- pubKey
|
|
|
|
return nil
|
|
}
|
|
|
|
return conf, keyCh
|
|
}
|
|
|
|
// PubKeyFromCertChain verifies the certificate chain and extracts the remote's
|
|
// libp2p public key from the signed key extension.
|
|
func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) {
|
|
if len(chain) != 1 {
|
|
return nil, errors.New("expected one certificate in the chain")
|
|
}
|
|
|
|
cert := chain[0]
|
|
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(cert)
|
|
|
|
var (
|
|
found bool
|
|
keyExt pkix.Extension
|
|
)
|
|
|
|
for _, ext := range cert.Extensions {
|
|
if extensionIDEqual(ext.Id, libp2pKeyExtensionID()) {
|
|
found = true
|
|
keyExt = ext
|
|
|
|
for idx, ident := range cert.UnhandledCriticalExtensions {
|
|
if ident.Equal(ext.Id) {
|
|
cert.UnhandledCriticalExtensions = append(
|
|
cert.UnhandledCriticalExtensions[:idx],
|
|
cert.UnhandledCriticalExtensions[idx+1:]...,
|
|
)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, errors.New("expected certificate to contain the data-mesher key extension")
|
|
}
|
|
|
|
if _, err := cert.Verify(x509.VerifyOptions{Roots: pool}); err != nil {
|
|
return nil, fmt.Errorf("certificate verification failed: %w", err)
|
|
}
|
|
|
|
var sk signedKey
|
|
if _, err := asn1.Unmarshal(keyExt.Value, &sk); err != nil {
|
|
return nil, fmt.Errorf("unmarshalling signed certificate failed: %w", err)
|
|
}
|
|
|
|
pubKey, err := ic.UnmarshalPublicKey(sk.PubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshalling public key failed: %w", err)
|
|
}
|
|
|
|
certKeyPub, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal certificate public key: %w", err)
|
|
}
|
|
|
|
valid, err := pubKey.Verify(append([]byte(certificatePrefix), certKeyPub...), sk.Signature)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("signature verification failed: %w", err)
|
|
}
|
|
|
|
if !valid {
|
|
return nil, errors.New("signature invalid")
|
|
}
|
|
|
|
return pubKey, nil
|
|
}
|
|
|
|
// VerifyCACert extracts the data-mesher certificate from the x509 chain's second extension, verifies it against the
|
|
// trusted CA keys, and checks that the certificate's public key matches the libp2p public key.
|
|
func VerifyCACert(
|
|
chain []*x509.Certificate,
|
|
caKeys []*dmcrypto.PublicKey,
|
|
expectedPubKey ic.PubKey,
|
|
) (*dmcrypto.PublicKey, string, error) {
|
|
// extract the certificate from the chain
|
|
if len(chain) != 1 {
|
|
return nil, "", errors.New("expected one certificate in the chain")
|
|
}
|
|
|
|
cert := chain[0]
|
|
|
|
// find the data-mesher cert extension
|
|
var (
|
|
found bool
|
|
dmExt pkix.Extension
|
|
)
|
|
|
|
for _, ext := range cert.Extensions {
|
|
if extensionIDEqual(ext.Id, dmCertExtensionID()) {
|
|
dmExt = ext
|
|
found = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, "", errors.New("expected certificate to contain the data-mesher certificate extension")
|
|
}
|
|
|
|
// parse the data-mesher certificate
|
|
dmCert, err := dmcrypto.ParseCertificateBytes(dmExt.Value)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to parse data-mesher certificate: %w", err)
|
|
}
|
|
|
|
// verify the cert's public key matches the libp2p key
|
|
dmKey, err := dmCert.IdentityKey.ToLibP2P()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to convert data-mesher cert pubkey to libp2p: %w", err)
|
|
}
|
|
|
|
if !dmKey.Equals(expectedPubKey) {
|
|
return nil, "", errors.New("data-mesher certificate public key does not match libp2p identity")
|
|
}
|
|
|
|
// verify the certificate against at least one trusted CA
|
|
for _, caKey := range caKeys {
|
|
if err := dmCert.Verify(caKey); err == nil {
|
|
return caKey, dmCert.Name, nil
|
|
}
|
|
}
|
|
|
|
return nil, "", errors.New("data-mesher certificate not signed by any trusted CA")
|
|
}
|
|
|
|
// GenerateSignedExtension uses the provided private key to sign the public key and returns the signature within a
|
|
// pkix.Extension.
|
|
// This extension is included in a certificate to cryptographically tie it to the libp2p private key.
|
|
func GenerateSignedExtension(sk ic.PrivKey, pubKey crypto.PublicKey) (pkix.Extension, error) {
|
|
keyBytes, err := ic.MarshalPublicKey(sk.GetPublic())
|
|
if err != nil {
|
|
return pkix.Extension{}, fmt.Errorf("failed to marshal public key: %w", err)
|
|
}
|
|
|
|
certKeyPub, err := x509.MarshalPKIXPublicKey(pubKey)
|
|
if err != nil {
|
|
return pkix.Extension{}, fmt.Errorf("failed to marshal certificate public key: %w", err)
|
|
}
|
|
|
|
signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
|
|
if err != nil {
|
|
return pkix.Extension{}, fmt.Errorf("failed to sign certificate key: %w", err)
|
|
}
|
|
|
|
value, err := asn1.Marshal(signedKey{
|
|
PubKey: keyBytes,
|
|
Signature: signature,
|
|
})
|
|
if err != nil {
|
|
return pkix.Extension{}, fmt.Errorf("failed to marshal signed key: %w", err)
|
|
}
|
|
|
|
return pkix.Extension{Id: libp2pKeyExtensionID(), Value: value}, nil
|
|
}
|
|
|
|
// keyToCertificate generates a new ECDSA P-256 private key and corresponding x509 certificate.
|
|
// The certificate includes the libp2p signed key extension and the data-mesher CA certificate extension.
|
|
func keyToCertificate(
|
|
sk ic.PrivKey,
|
|
certTmpl *x509.Certificate,
|
|
identityCert *dmcrypto.Certificate,
|
|
) (*tls.Certificate, error) {
|
|
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate ECDSA key: %w", err)
|
|
}
|
|
|
|
// add the libp2p signed key extension
|
|
extension, err := GenerateSignedExtension(sk, certKey.Public())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certTmpl.ExtraExtensions = append(certTmpl.ExtraExtensions, extension)
|
|
|
|
// add the data-mesher certificate extension
|
|
dmExt := pkix.Extension{
|
|
Id: dmCertExtensionID(),
|
|
Value: identityCert.Bytes(),
|
|
}
|
|
|
|
certTmpl.ExtraExtensions = append(certTmpl.ExtraExtensions, dmExt)
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, certTmpl, certKey.Public(), certKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create x509 certificate: %w", err)
|
|
}
|
|
|
|
return &tls.Certificate{
|
|
Certificate: [][]byte{certDER},
|
|
PrivateKey: certKey,
|
|
}, nil
|
|
}
|
|
|
|
// certTemplate returns the template for generating an Identity's TLS certificates.
|
|
func certTemplate() (*x509.Certificate, error) {
|
|
bigNum := big.NewInt(1 << 62)
|
|
|
|
sn, err := rand.Int(rand.Reader, bigNum)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
|
}
|
|
|
|
subjectSN, err := rand.Int(rand.Reader, bigNum)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate subject serial number: %w", err)
|
|
}
|
|
|
|
return &x509.Certificate{
|
|
SerialNumber: sn,
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(certValidityPeriod),
|
|
// According to RFC 3280, the issuer field must be set,
|
|
// see https://datatracker.ietf.org/doc/html/rfc3280#section-4.1.2.4.
|
|
Subject: pkix.Name{SerialNumber: subjectSN.String()},
|
|
}, nil
|
|
}
|