Files

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
}