mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-04-18 00:41:05 +02:00
353 lines
13 KiB
Go
353 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"math/big"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
)
|
|
|
|
const (
|
|
// So, 256-bit EC curve pubkeys, 256-bit keys symmetric encryption,
|
|
// 256-bit keys for HMAC, etc. Represented in bytes.
|
|
securityParameter = 32
|
|
|
|
// Default message size in bytes. This is probably *much* too big atm?
|
|
messageSize = 1024
|
|
|
|
// Mix header over head. If we assume 5 hops (which seems sufficient for
|
|
// LN, for now atleast), 32 byte group element to be re-randomized each
|
|
// hop, and 32 byte symmetric key.
|
|
// Overhead is: p + (2r + 2)s
|
|
// * p = pub key size (in bytes, for DH each hop)
|
|
// * r = max number of hops
|
|
// * s = summetric key size (in bytes)
|
|
// It's: 32 + (2*5 + 2) * 32 = 416 bytes! But if we use secp256k1 instead of
|
|
// Curve25519, then we've have an extra byte for the compressed keys.
|
|
mixHeaderOverhead = 417
|
|
|
|
// The maximum path length. This should be set to an
|
|
// estiamate of the upper limit of the diameter of the node graph.
|
|
numMaxHops = 5
|
|
|
|
// Special destination to indicate we're at the end of the path.
|
|
nullDestination = 0x00
|
|
|
|
// (2r + 3)k = (2*5 + 3) * 32 = 416
|
|
// The number of bytes produced by our CSPRG for the key stream
|
|
// implementing our stream cipher to encrypt/decrypt the mix header. The
|
|
// last 2 * securityParameter bytes are only used in order to generate/check
|
|
// the MAC over the header.
|
|
numStreamBytes = (2*numMaxHops + 3) * securityParameter
|
|
|
|
sharedSecretSize = 32
|
|
|
|
// node_id + mac + (2*5-1)*32
|
|
// 32 + 32 + 288
|
|
routingInfoSize = 352
|
|
)
|
|
|
|
type LnEndpoint string
|
|
|
|
//type LnAddr btcutil.Address
|
|
type LnAddr string
|
|
|
|
type SharedSecret [sharedSecretSize]byte
|
|
|
|
var zeroNode [securityParameter]byte
|
|
var nullDest byte
|
|
|
|
// MixHeader is the onion wrapped hop-to-hop routing information neccessary to
|
|
// propagate a message through the mix-net without intermediate nodes having
|
|
// knowledge of their position within the route, the source, the destination,
|
|
// and finally the identities of the past/future nodes in the route. At each hop
|
|
// the ephemeral key is used by the node to perform ECDH between itself and the
|
|
// source node. This derived secret key is used to check the MAC of the entire mix
|
|
// header, decrypt the next set of routing information, and re-randomize the
|
|
// ephemeral key for the next node in the path. This per-hop re-randomization
|
|
// allows us to only propgate a single group element through the onion route.
|
|
// TODO(roasbeef): serialize/deserialize methods..
|
|
type MixHeader struct {
|
|
EphemeralKey *btcec.PublicKey
|
|
RoutingInfo [routingInfoSize]byte
|
|
HeaderMAC [securityParameter]byte
|
|
}
|
|
|
|
// GenerateSphinxHeader...
|
|
// TODO(roasbeef): or pass in identifiers as payment path? have map from id -> pubkey
|
|
func NewMixHeader(dest []byte, identifier [securityParameter]byte,
|
|
paymentPath []*btcec.PublicKey) (*MixHeader, [][sharedSecretSize]byte, error) {
|
|
// Each hop performs ECDH with our ephemeral key pair to arrive at a
|
|
// shared secret. Additionally, each hop randomizes the group element
|
|
// for the next hop by multiplying it by the blinding factor. This way
|
|
// we only need to transmit a single group element, and hops can't link
|
|
// a session back to us if they have several nodes in the path.
|
|
numHops := len(paymentPath)
|
|
hopEphemeralPubKeys := make([]*btcec.PublicKey, numHops)
|
|
hopSharedSecrets := make([][sha256.Size]byte, numHops)
|
|
hopBlindingFactors := make([][sha256.Size]byte, numHops)
|
|
|
|
// Generate a new ephemeral key to use for ECDH for this session.
|
|
sessionKey, err := btcec.NewPrivateKey(btcec.S256())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Compute the triplet for the first hop outside of the main loop.
|
|
// Within the loop each new triplet will be computed recursively based
|
|
// off of the blinding factor of the last hop.
|
|
hopEphemeralPubKeys[0] = sessionKey.PubKey()
|
|
hopSharedSecrets[0] = sha256.Sum256(btcec.GenerateSharedSecret(sessionKey, paymentPath[0]))
|
|
hopBlindingFactors[0] = computeBlindingFactor(hopEphemeralPubKeys[0], hopSharedSecrets[0][:])
|
|
|
|
// x * b_{0} mod n. Becomes x * b_{0} * b_{1} * ..... * b_{n} mod curve_order, etc.
|
|
cummulativeBlind := new(big.Int).Mul(
|
|
sessionKey.X, new(big.Int).SetBytes(hopBlindingFactors[0][:]),
|
|
)
|
|
cummulativeBlind.Mod(cummulativeBlind, btcec.S256().N)
|
|
|
|
// Now recursively compute the ephemeral ECDH pub keys, the shared
|
|
// secret, and blinding factor for each hop.
|
|
for i := 1; i < numHops-1; i++ {
|
|
// a_{n} = a_{n-1} x c_{n-1} -> (Y_prev_pub_key x prevBlindingFactor)
|
|
hopEphemeralPubKeys[i] = blindGroupElement(hopEphemeralPubKeys[i-1],
|
|
hopBlindingFactors[i-1][:])
|
|
|
|
// s_{n} = sha256( y_{n} x c_{n-1} ) ->
|
|
// Y_their_pub_key x (x_our_priv * all prev blinding factors mod curve_order)
|
|
hopSharedSecrets[i] = sha256.Sum256(
|
|
blindGroupElement(paymentPath[i], cummulativeBlind.Bytes()).X.Bytes(),
|
|
)
|
|
|
|
// TODO(roasbeef): prob don't need to store all blinding factors, only the prev...
|
|
// b_{n} = sha256(a_{n} || s_{n})
|
|
hopBlindingFactors[i] = computeBlindingFactor(hopEphemeralPubKeys[i],
|
|
hopSharedSecrets[i][:])
|
|
|
|
// c_{n} = c_{n-1} * b_{n} mod curve_order
|
|
cummulativeBlind.Mul(cummulativeBlind, new(big.Int).SetBytes(hopBlindingFactors[i][:]))
|
|
cummulativeBlind.Mod(cummulativeBlind, btcec.S256().N)
|
|
}
|
|
|
|
// Generate the padding, called "filler strings" in the paper.
|
|
filler := generateHeaderPadding(numHops, hopSharedSecrets)
|
|
|
|
// First we generate the routing info + MAC for the very last hop.
|
|
mixHeader := make([]byte, 0, routingInfoSize)
|
|
mixHeader = append(mixHeader, dest...)
|
|
mixHeader = append(mixHeader, identifier[:]...)
|
|
mixHeader = append(mixHeader,
|
|
bytes.Repeat([]byte{0}, ((2*(numMaxHops-numHops)+2)*securityParameter-len(dest)))...)
|
|
|
|
// Encrypt the header for the final hop with the shared secret the
|
|
// destination will eventually derive, then pad the message out to full
|
|
// size with the "random" filler bytes.
|
|
streamBytes := generateCipherStream(generateKey("rho", hopSharedSecrets[numHops-1]), numStreamBytes)
|
|
xor(mixHeader, mixHeader, streamBytes[:(2*(numMaxHops-numHops)+3)*securityParameter])
|
|
mixHeader = append(mixHeader, filler...)
|
|
|
|
// Calculate a MAC over the encrypted mix header for the last hop, using
|
|
// the same shared secret key as used for encryption above.
|
|
headerMac := calcMac(generateKey("mu", hopSharedSecrets[numHops-1]), mixHeader)
|
|
|
|
// Now we compute the routing information for each hop, along with a
|
|
// MAC of the routing info using the shared key for that hop.
|
|
for i := numHops - 2; i > 0; i-- {
|
|
// TODO(roasbeef): The node is needs to be the same length as the
|
|
// security paramter in bytes. If we use Curve25519, then our ID's
|
|
// are just the serialized pub keys possibly. Or, should a node's ID
|
|
// be something P2KH style? In that case, using SHA-256 instead of
|
|
// RIPEMD? Just serializing and truncating for now.
|
|
nodeID := paymentPath[i+1].SerializeCompressed()[:securityParameter]
|
|
|
|
var b bytes.Buffer
|
|
// ID for next hop.
|
|
b.Write(nodeID)
|
|
// MAC for mix header.
|
|
b.Write(headerMac[:])
|
|
// Mix header itself.
|
|
b.Write(mixHeader[:(2*numMaxHops-1)*securityParameter])
|
|
|
|
streamBytes := generateCipherStream(generateKey("rho", hopSharedSecrets[i]), numStreamBytes)
|
|
xor(mixHeader, b.Bytes(), streamBytes[:(2*numMaxHops+1)*securityParameter])
|
|
headerMac = calcMac(generateKey("mu", hopSharedSecrets[i]), mixHeader)
|
|
}
|
|
|
|
var r [routingInfoSize]byte
|
|
copy(r[:], mixHeader)
|
|
header := &MixHeader{
|
|
EphemeralKey: hopEphemeralPubKeys[0],
|
|
RoutingInfo: r,
|
|
HeaderMAC: headerMac,
|
|
}
|
|
|
|
return header, hopSharedSecrets, nil
|
|
}
|
|
|
|
// generateHeaderPadding derives the bytes for padding the mix header to ensure
|
|
// it remains fixed sized throughout route transit. At each step, we add
|
|
// 2*securityParameter padding of zeroes, concatenate it to the previous
|
|
// filler, then decrypt it (XOR) with the secret key of the current hop. When
|
|
// encrypting the mix header we essentially do the reverse of this operation:
|
|
// we "encrypt" the padding, and drop 2*k number of zeroes. As nodes process
|
|
// the mix header they add the padding (2*k) in order to check the MAC and
|
|
// decrypt the next routing information eventually leaving only the original
|
|
// "filler" bytes produced by this function at the last hop. Using this
|
|
// methodology, the size of the mix header stays constant at each hop.
|
|
func generateHeaderPadding(numHops int, sharedSecrets [][sharedSecretSize]byte) []byte {
|
|
var filler []byte
|
|
for i := 1; i < numHops; i++ {
|
|
slice := (2*(numMaxHops-1) + 3) * securityParameter
|
|
padding := bytes.Repeat([]byte{0}, 2*securityParameter)
|
|
|
|
var tempBuf bytes.Buffer
|
|
tempBuf.Write(filler)
|
|
tempBuf.Write(padding)
|
|
|
|
streamBytes := generateCipherStream(generateKey("rho", sharedSecrets[i-1]),
|
|
numStreamBytes)
|
|
|
|
xor(filler, tempBuf.Bytes(), streamBytes[slice:])
|
|
}
|
|
|
|
return filler
|
|
}
|
|
|
|
// ForwardingMessage represents a forwarding message containing onion wrapped
|
|
// hop-to-hop routing information along with an onion encrypted payload message
|
|
// addressed to the final destination.
|
|
// TODO(roasbeef): serialize/deserialize methods..
|
|
type ForwardingMessage struct {
|
|
header *MixHeader
|
|
msg [messageSize]byte
|
|
}
|
|
|
|
// NewForwardingMessage generates the a mix header containing the neccessary
|
|
// onion routing information required to propagate the message through the
|
|
// mixnet, eventually reaching the final node specified by 'identifier'. The
|
|
// onion encrypted message payload is then to be delivered to the specified 'dest'
|
|
// address.
|
|
func NewForwardingMessage(route []*btcec.PublicKey, dest LnAddr,
|
|
identifier [securityParameter]byte, message []byte) (*ForwardingMessage, error) {
|
|
routeLength := len(route)
|
|
|
|
// Compute the mix header, and shared secerts for each hop. We pass in
|
|
// the null destinatino and zero identifier in order for the final node
|
|
// in the route to be able to distinguish the payload as addressed to
|
|
// itself.
|
|
mixHeader, secrets, err := NewMixHeader([]byte{nullDest}, zeroNode, route)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now for the body of the message. The next-node ID is set to all
|
|
// zeroes in order to notify the final op that the message is meant for
|
|
// them. m = 0^k || dest || msg || padding.
|
|
var body [messageSize]byte
|
|
n := copy(body[:], bytes.Repeat([]byte{0}, securityParameter))
|
|
// TODO(roasbeef): destination vs identifier (node id) format.
|
|
n += copy(body[n:], []byte(dest))
|
|
n += copy(body[n:], message)
|
|
// TODO(roasbeef): make pad and unpad functions.
|
|
n += copy(body[n:], []byte{0x7f})
|
|
n += copy(body[n:], bytes.Repeat([]byte{0xff}, messageSize-len(body)))
|
|
|
|
// Now we construct the onion. Walking backwards from the last hop, we
|
|
// encrypt the message with the shared secret for each hop in the path.
|
|
onion := lionessEncode(generateKey("pi", secrets[routeLength-1]), body)
|
|
for i := routeLength - 2; i > 0; i-- {
|
|
onion = lionessEncode(generateKey("pi", secrets[i]), onion)
|
|
}
|
|
|
|
return &ForwardingMessage{header: mixHeader, msg: onion}, nil
|
|
}
|
|
|
|
// calcMac calculates HMAC-SHA-256 over the message using the passed secret key as
|
|
// input to the HMAC.
|
|
func calcMac(key [securityParameter]byte, msg []byte) [securityParameter]byte {
|
|
hmac := hmac.New(sha256.New, key[:])
|
|
hmac.Write(msg)
|
|
h := hmac.Sum(nil)
|
|
|
|
var mac [securityParameter]byte
|
|
copy(mac[:], h[:securityParameter])
|
|
|
|
return mac
|
|
}
|
|
|
|
// xor computes the byte wise XOR of a and b, storing the result in dst.
|
|
func xor(dst, a, b []byte) int {
|
|
n := len(a)
|
|
if len(b) < n {
|
|
n = len(b)
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
dst[i] = a[i] ^ b[i]
|
|
}
|
|
return n
|
|
}
|
|
|
|
// generateKey...
|
|
// used to key rand padding generation, mac, and lionness
|
|
func generateKey(keyType string, sharedKey [sharedSecretSize]byte) [securityParameter]byte {
|
|
mac := hmac.New(sha256.New, []byte(keyType))
|
|
mac.Write(sharedKey[:])
|
|
h := mac.Sum(nil)
|
|
|
|
var key [securityParameter]byte
|
|
copy(key[:], h[:securityParameter])
|
|
|
|
return key
|
|
}
|
|
|
|
// generateRandBytes...
|
|
// generates
|
|
func generateCipherStream(key [securityParameter]byte, numBytes uint) []byte {
|
|
block, _ := aes.NewCipher(key[:])
|
|
|
|
// We use AES in CTR mode to generate a psuedo randmom stream of bytes
|
|
// by encrypting a plaintext of all zeroes.
|
|
cipherStream := make([]byte, numBytes)
|
|
plainText := bytes.Repeat([]byte{0}, int(numBytes))
|
|
|
|
// Our IV is just zero....
|
|
iv := bytes.Repeat([]byte{0}, aes.BlockSize)
|
|
|
|
stream := cipher.NewCTR(block, iv)
|
|
stream.XORKeyStream(cipherStream, plainText)
|
|
|
|
return cipherStream
|
|
}
|
|
|
|
// ComputeBlindingFactor for the next hop given the ephemeral pubKey and
|
|
// sharedSecret for this hop. The blinding factor is computed as the
|
|
// sha-256(pubkey || sharedSecret).
|
|
func computeBlindingFactor(hopPubKey *btcec.PublicKey, hopSharedSecret []byte) [sha256.Size]byte {
|
|
sha := sha256.New()
|
|
sha.Write(hopPubKey.SerializeCompressed())
|
|
sha.Write(hopSharedSecret)
|
|
|
|
var hash [sha256.Size]byte
|
|
copy(hash[:], sha.Sum(nil))
|
|
return hash
|
|
}
|
|
|
|
// blindGroupElement blinds the group element by performing scalar
|
|
// multiplication of the group element by blindingFactor: G x blindingFactor.
|
|
func blindGroupElement(hopPubKey *btcec.PublicKey, blindingFactor []byte) *btcec.PublicKey {
|
|
newX, newY := hopPubKey.Curve.ScalarMult(hopPubKey.X, hopPubKey.Y, blindingFactor[:])
|
|
return &btcec.PublicKey{hopPubKey.Curve, newX, newY}
|
|
}
|
|
|
|
// SphinxPayload...
|
|
type SphinxPayload struct {
|
|
}
|
|
|
|
// SphinxPacket...
|
|
type SphinxPacket struct {
|
|
}
|