Further flesh out sphinx implementation

* Over head is quite small, at just 225 bytes if assuming 5 hops
* Most likely also don’t even need the message at all?
  * What would it contain?

* Bunch of TODO’s still left in the code, need to write the portion for
processing and forwarding mix headers received by the nodes.

* After the processing stuff, need to write an extensive set of tests
to ensure correctness.

* Also, need to figure out the division between nodeID and lightning
address. For sphinx, they must be “prefix free”.
This commit is contained in:
Olaoluwa Osuntokun
2015-10-10 22:18:00 -07:00
parent 90316e6426
commit a67603051c
2 changed files with 291 additions and 20 deletions

View File

@@ -100,6 +100,9 @@ type DataPacket struct {
Onion [FSLength * NumMaxHops]byte // TODO(roasbeef): or, is it NumMaxHops - 1? Onion [FSLength * NumMaxHops]byte // TODO(roasbeef): or, is it NumMaxHops - 1?
} }
type SphinxHeader struct {
}
// SessionSetupPacket... // SessionSetupPacket...
type SessionSetupPacket struct { type SessionSetupPacket struct {
Chdr CommonHeader Chdr CommonHeader

308
sphinx.go
View File

@@ -1,31 +1,72 @@
package main package main
import ( import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256" "crypto/sha256"
"math/big" "math/big"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/wire" )
"github.com/btcsuite/btcutil"
const (
// So, 256-bit EC curve, 128-bit keys symmetric encryption, 128-bit keys for
// HMAC, etc. Stored in bytes, here.
securityParameter = 16
// Default message size in bytes. This is probably 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 16 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) * 16 = 224 bytes! But if we use secp256k1 instead of
// Curve25519, then we've have an extra byte for the compressed keys.
mixHeaderOverhead = 225
// Basically an upper limit on the diameter on our node graph.
numMaxHops = 5
// Special destination to indicate we're at the end of the path.
nullDestination = 0x00
// (2r + 3)k = (2*5 + 3) * 16 = 208
numStreamBytes = 208
sharedSecretSize = 32
// node_id + mac + (2*5-1)*16
// 16 + 16 + 144
routingInfoSize = 176
) )
type LnEndpoint string type LnEndpoint string
type LnAddr btcutil.Address
type NodePubKey *btcec.PublicKey
type SharedSecret wire.ShaHash //type LnAddr btcutil.Address
type LnAddr string
var zeroNode wire.ShaHash type SharedSecret [sharedSecretSize]byte
type MixHeader []byte var zeroNode [securityParameter]byte
var nullDest byte
// SphinxHeader... // MixHeader...
type SphinxHeader struct { type MixHeader struct {
DHKey *btcec.PublicKey
RoutingInfo [routingInfoSize]byte
HeaderMAC [securityParameter]byte
} }
// GenerateSphinxHeader... // GenerateSphinxHeader...
// TODO(roasbeef): or pass in identifiers as payment path? have map from id -> pubkey // TODO(roasbeef): or pass in identifiers as payment path? have map from id -> pubkey
func GenerateSphinxHeader(dest LnAddr, identifier wire.ShaHash, paymentPath []NodePubKey) (*MixHeader, []SharedSecret, error) { func GenerateSphinxHeader(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 // Each hop performs ECDH with our ephemeral key pair to arrive at a
// shared secret. Additionally, each hop randomizes the group element // shared secret. Additionally, each hop randomizes the group element
// for the next hop by multiplying it by the blinding factor. This way // for the next hop by multiplying it by the blinding factor. This way
@@ -33,7 +74,7 @@ func GenerateSphinxHeader(dest LnAddr, identifier wire.ShaHash, paymentPath []No
// a session back to us if they have several nodes in the path. // a session back to us if they have several nodes in the path.
numHops := len(paymentPath) numHops := len(paymentPath)
hopEphemeralPubKeys := make([]*btcec.PublicKey, numHops) hopEphemeralPubKeys := make([]*btcec.PublicKey, numHops)
hopSharedSecret := make([][sha256.Size]byte, numHops) hopSharedSecrets := make([][sha256.Size]byte, numHops)
hopBlindingFactors := make([][sha256.Size]byte, numHops) hopBlindingFactors := make([][sha256.Size]byte, numHops)
// Generate a new ephemeral key to use for ECDH for this session. // Generate a new ephemeral key to use for ECDH for this session.
@@ -46,32 +87,259 @@ func GenerateSphinxHeader(dest LnAddr, identifier wire.ShaHash, paymentPath []No
// Within the loop each new triplet will be computed recursively based // Within the loop each new triplet will be computed recursively based
// off of the blinding factor of the last hop. // off of the blinding factor of the last hop.
hopEphemeralPubKeys[0] = sessionKey.PubKey() hopEphemeralPubKeys[0] = sessionKey.PubKey()
hopSharedSecret[0] = sha256.Sum256(btcec.GenerateSharedSecret(sessionKey, paymentPath[0])) hopSharedSecrets[0] = sha256.Sum256(btcec.GenerateSharedSecret(sessionKey, paymentPath[0]))
hopBlindingFactors[0] = computeBlindingFactor(hopEphemeralPubKeys[0], hopSharedSecret[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. // 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 := new(big.Int).Mul(
sessionKey.X, new(big.Int).SetBytes(hopBlindingFactors[0][:]),
)
cummulativeBlind.Mod(cummulativeBlind, btcec.S256().N) cummulativeBlind.Mod(cummulativeBlind, btcec.S256().N)
// Now recursively compute the ephemeral ECDH pub keys, the shared // Now recursively compute the ephemeral ECDH pub keys, the shared
// secret, and blinding factor for each hop. // secret, and blinding factor for each hop.
for i := 1; i < numHops-1; i++ { for i := 1; i < numHops-1; i++ {
// a_{n} = a_{n-1} x c_{n-1} -> (Y_prev_pub_key x prevBlindingFactor) // a_{n} = a_{n-1} x c_{n-1} -> (Y_prev_pub_key x prevBlindingFactor)
hopEphemeralPubKeys[i] = blindGroupElement(hopEphemeralPubKeys[i-1], hopBlindingFactors[i-1][:]) 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) // s_{n} = sha256( y_{n} x c_{n-1} ) ->
hopSharedSecret[i] = sha256.Sum256(blindGroupElement(paymentPath[i], cummulativeBlind.Bytes()).X.Bytes()) // 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(),
)
// b_{n} = sha256(a_{n} || s_{n})
hopBlindingFactors[i] = computeBlindingFactor(hopEphemeralPubKeys[i], hopSharedSecret[i][:])
// TODO(roasbeef): prob don't need to store all blinding factors, only the prev... // 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 // c_{n} = c_{n-1} * b_{n} mod curve_order
cummulativeBlind.Mul(cummulativeBlind, new(big.Int).SetBytes(hopBlindingFactors[i][:])) cummulativeBlind.Mul(cummulativeBlind, new(big.Int).SetBytes(hopBlindingFactors[i][:]))
cummulativeBlind.Mod(cummulativeBlind, btcec.S256().N) cummulativeBlind.Mod(cummulativeBlind, btcec.S256().N)
} }
return nil, nil, nil // 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 := generateRandBytes(generateKey("rho", hopSharedSecrets[numHops-1]))
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 := generateRandBytes(generateKey("rho", hopSharedSecrets[i]))
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{
DHKey: hopEphemeralPubKeys[0],
RoutingInfo: r,
HeaderMAC: headerMac,
}
return header, hopSharedSecrets, nil
}
// generateHeaderPadding...
// 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) and then decrypt
// 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 := generateRandBytes(generateKey("rho", sharedSecrets[i-1]))
xor(filler, tempBuf.Bytes(), streamBytes[slice:])
}
return filler
}
// CreateForwardingMessage...
// TODO(roasbeef): Identifier should also been the same size as the security paramter.
// Should probably go with k = 32 then.
func CreateForwardingMessage(route []*btcec.PublicKey, dest LnAddr,
identifier [securityParameter]byte, message []byte) (*MixHeader, *[messageSize]byte, error) {
routeLength := len(route)
// Compute the mix header, and shared secerts for each hop.
mixHeader, secrets, err := GenerateSphinxHeader([]byte{nullDest}, zeroNode, route)
if err != nil {
return nil, 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 mixHeader, &onion, nil
}
// lionEncode...
// block cipher with a block size equivalent to our message size
// http://www.cl.cam.ac.uk/~rja14/Papers/bear-lion.pdf (section 6)
func lionessEncode(key [securityParameter]byte, message [messageSize]byte) [messageSize]byte {
//var l [securityParameter]byte
//var r [messageSize + securityParameter]byte
sha := sha256.New()
var cipherText [messageSize]byte
// Round 1.
sha.Write(message[securityParameter:])
sha.Write(key[:])
sha.Write([]byte{1})
xor(cipherText[:], sha.Sum(nil)[:securityParameter], message[:securityParameter])
copy(cipherText[securityParameter:], message[securityParameter:])
// Round 2.
var k2 [securityParameter]byte
xor(k2[:], cipherText[:securityParameter], key[:])
block, _ := aes.NewCipher(k2[:])
stream := cipher.NewCTR(block, bytes.Repeat([]byte{0}, aes.BlockSize))
stream.XORKeyStream(cipherText[securityParameter:], cipherText[securityParameter:])
sha.Reset()
// Round 3.
sha.Write(cipherText[securityParameter:])
sha.Write(key[:])
sha.Write([]byte{3})
xor(cipherText[:], sha.Sum(nil)[:securityParameter], cipherText[:securityParameter])
// Round 4.
var k4 [securityParameter]byte
xor(k4[:], cipherText[:securityParameter], key[:])
block, _ = aes.NewCipher(k4[:])
stream = cipher.NewCTR(block, bytes.Repeat([]byte{0}, aes.BlockSize))
stream.XORKeyStream(cipherText[securityParameter:], cipherText[securityParameter:])
return cipherText
}
// lionDecode...
func lionessDecode() {
}
// calcMac....
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...
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 generateRandBytes(key [securityParameter]byte) [numStreamBytes]byte {
var r [numStreamBytes]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.
randBytes := make([]byte, numStreamBytes)
plainText := bytes.Repeat([]byte{0}, numStreamBytes)
// Our IV is just zero....
iv := bytes.Repeat([]byte{0}, aes.BlockSize)
stream := cipher.NewCTR(block, iv)
stream.XORKeyStream(randBytes, plainText)
copy(r[:], randBytes)
return r
} }
// ComputeBlindingFactor for the next hop given the ephemeral pubKey and // ComputeBlindingFactor for the next hop given the ephemeral pubKey and