Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,11 @@ func main() {
}

privkey, _ := btcec.PrivKeyFromBytes(btcec.S256(), binKey)
privKeyECDH := &sphinx.PrivKeyECDH{PrivKey: privkey}
replayLog := sphinx.NewMemoryReplayLog()
s := sphinx.NewRouter(privkey, &chaincfg.TestNet3Params, replayLog)
s := sphinx.NewRouter(
privKeyECDH, &chaincfg.TestNet3Params, replayLog,
)

replayLog.Start()
defer replayLog.Stop()
Expand Down
71 changes: 55 additions & 16 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ const (
// the output of a SHA256 hash.
type Hash256 [sha256.Size]byte

// SingleKeyECDH is an abstraction interface that hides the implementation of an
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering still if SingleKeyECDH is the right name. It also suggests some fancy algorithm in which you exchange keys with yourself. Thinking about alternatives. Maybe ExternalECDH or just Signer (even though it isn't really signing)

// ECDH operation against a specific private key. We use this abstraction for
// the long term keys which we eventually want to be able to keep in a hardware
// wallet or HSM.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, which hardware wallets currently support ECDH?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea, to be honest. HW wallets are my blind spot. But I imagine implementing that is pretty easy as it's not much different from signing a message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Roasbeef do they exist?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ledger and trezor have support in their APIs which can be leveraged by custom applications. With this though, I think we're more so targeting remote/restricted signers which can emulate these calls by using the existing lnd gRPC interface and sub-servers, or something else entirely.

type SingleKeyECDH interface {
// PubKey returns the public key of the private key that is abstracted
// away by the interface.
PubKey() *btcec.PublicKey

// ECDH performs a scalar multiplication (ECDH-like operation) between
// the abstracted private key and a remote public key. The output
// returned will be the sha256 of the resulting shared point serialized
// in compressed format.
ECDH(pubKey *btcec.PublicKey) ([32]byte, error)
}

// PrivKeyECDH is an implementation of the SingleKeyECDH in which we do have the
// full private key. This can be used to wrap a temporary key to conform to the
// SingleKeyECDH interface.
type PrivKeyECDH struct {
// PrivKey is the private key that is used for the ECDH operation.
PrivKey *btcec.PrivateKey
}

// PubKey returns the public key of the private key that is abstracted away by
// the interface.
//
// NOTE: This is part of the SingleKeyECDH interface.
func (p *PrivKeyECDH) PubKey() *btcec.PublicKey {
return p.PrivKey.PubKey()
}

// ECDH performs a scalar multiplication (ECDH-like operation) between the
// abstracted private key and a remote public key. The output returned will be
// the sha256 of the resulting shared point serialized in compressed format. If
// k is our private key, and P is the public key, we perform the following
// operation:
//
// sx := k*P
// s := sha256(sx.SerializeCompressed())
//
// NOTE: This is part of the SingleKeyECDH interface.
func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) {
s := &btcec.PublicKey{}
s.X, s.Y = btcec.S256().ScalarMult(pub.X, pub.Y, p.PrivKey.D.Bytes())

return sha256.Sum256(s.SerializeCompressed()), nil
}

// DecryptedError contains the decrypted error message and its sender.
type DecryptedError struct {
// Sender is the node that sent the error. Note that a node may occur in
Expand Down Expand Up @@ -149,21 +198,7 @@ func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) {
}

// Compute our shared secret.
sharedSecret = generateSharedSecret(dhKey, r.onionKey)
return sharedSecret, nil
}

// generateSharedSecret generates the shared secret for a particular hop. The
// shared secret is generated by taking the group element contained in the
// mix-header, and performing an ECDH operation with the node's long term onion
// key. We then take the _entire_ point generated by the ECDH operation,
// serialize that using a compressed format, then feed the raw bytes through a
// single SHA256 invocation. The resulting value is the shared secret.
func generateSharedSecret(pub *btcec.PublicKey, priv *btcec.PrivateKey) Hash256 {
s := &btcec.PublicKey{}
s.X, s.Y = btcec.S256().ScalarMult(pub.X, pub.Y, priv.D.Bytes())

return sha256.Sum256(s.SerializeCompressed())
return r.onionKey.ECDH(dhKey)
}

// onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a
Expand Down Expand Up @@ -200,10 +235,14 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
len(encryptedData))
}

sharedSecrets := generateSharedSecrets(
sharedSecrets, err := generateSharedSecrets(
o.circuit.PaymentPath,
o.circuit.SessionKey,
)
if err != nil {
return nil, fmt.Errorf("error generating shared secret: %v",
err)
}

var (
sender int
Expand Down
10 changes: 8 additions & 2 deletions obfuscation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ func TestOnionFailure(t *testing.T) {
errorPath := paymentPath[:len(paymentPath)-1]

failureData := bytes.Repeat([]byte{'A'}, onionErrorLength-sha256.Size)
sharedSecrets := generateSharedSecrets(paymentPath, sessionKey)
sharedSecrets, err := generateSharedSecrets(paymentPath, sessionKey)
if err != nil {
t.Fatalf("Unexpected error while generating secrets: %v", err)
}

// Emulate creation of the obfuscator on node where error have occurred.
obfuscator := &OnionErrorEncrypter{
Expand Down Expand Up @@ -194,7 +197,10 @@ func TestOnionFailureSpecVector(t *testing.T) {
}

var obfuscatedData []byte
sharedSecrets := generateSharedSecrets(paymentPath, sessionKey)
sharedSecrets, err := generateSharedSecrets(paymentPath, sessionKey)
if err != nil {
t.Fatalf("Unexpected error while generating secrets: %v", err)
}
for i, test := range onionErrorData {

// Decode the shared secret and check that it matchs with
Expand Down
36 changes: 19 additions & 17 deletions sphinx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sphinx

import (
"bytes"
"crypto/ecdsa"
"crypto/hmac"
"crypto/sha256"
"fmt"
Expand Down Expand Up @@ -117,7 +116,7 @@ type OnionPacket struct {
// generateSharedSecrets by the given nodes pubkeys, generates the shared
// secrets.
func generateSharedSecrets(paymentPath []*btcec.PublicKey,
sessionKey *btcec.PrivateKey) []Hash256 {
sessionKey *btcec.PrivateKey) ([]Hash256, error) {

// Each hop performs ECDH with our ephemeral key pair to arrive at a
// shared secret. Additionally, each hop randomizes the group element
Expand All @@ -131,8 +130,15 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey,
// Within the loop each new triplet will be computed recursively based
// off of the blinding factor of the last hop.
lastEphemeralPubKey := sessionKey.PubKey()
hopSharedSecrets[0] = generateSharedSecret(paymentPath[0], sessionKey)
lastBlindingFactor := computeBlindingFactor(lastEphemeralPubKey, hopSharedSecrets[0][:])
sessionKeyECDH := &PrivKeyECDH{PrivKey: sessionKey}
sharedSecret, err := sessionKeyECDH.ECDH(paymentPath[0])
if err != nil {
return nil, err
}
hopSharedSecrets[0] = sharedSecret
lastBlindingFactor := computeBlindingFactor(
lastEphemeralPubKey, hopSharedSecrets[0][:],
)

// The cached blinding factor will contain the running product of the
// session private key x and blinding factors b_i, computed as
Expand Down Expand Up @@ -184,7 +190,7 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey,
)
}

return hopSharedSecrets
return hopSharedSecrets, nil
}

// NewOnionPacket creates a new onion packet which is capable of obliviously
Expand All @@ -211,9 +217,12 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
return nil, fmt.Errorf("packet filler must be specified")
}

hopSharedSecrets := generateSharedSecrets(
hopSharedSecrets, err := generateSharedSecrets(
paymentPath.NodeKeys(), sessionKey,
)
if err != nil {
return nil, fmt.Errorf("error generating shared secret: %v", err)
}

// Generate the padding, called "filler strings" in the paper.
filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets)
Expand Down Expand Up @@ -479,14 +488,14 @@ type Router struct {
nodeID [AddressSize]byte
nodeAddr *btcutil.AddressPubKeyHash

onionKey *btcec.PrivateKey
onionKey SingleKeyECDH

log ReplayLog
}

// NewRouter creates a new instance of a Sphinx onion Router given the node's
// currently advertised onion private key, and the target Bitcoin network.
func NewRouter(nodeKey *btcec.PrivateKey, net *chaincfg.Params, log ReplayLog) *Router {
func NewRouter(nodeKey SingleKeyECDH, net *chaincfg.Params, log ReplayLog) *Router {
var nodeID [AddressSize]byte
copy(nodeID[:], btcutil.Hash160(nodeKey.PubKey().SerializeCompressed()))

Expand All @@ -496,15 +505,8 @@ func NewRouter(nodeKey *btcec.PrivateKey, net *chaincfg.Params, log ReplayLog) *
return &Router{
nodeID: nodeID,
nodeAddr: nodeAddr,
onionKey: &btcec.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: btcec.S256(),
X: nodeKey.X,
Y: nodeKey.Y,
},
D: nodeKey.D,
},
log: log,
onionKey: nodeKey,
log: log,
}
}

Expand Down
6 changes: 4 additions & 2 deletions sphinx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke
}

nodes[i] = NewRouter(
privKey, &chaincfg.MainNetParams, NewMemoryReplayLog(),
&PrivKeyECDH{PrivKey: privKey}, &chaincfg.MainNetParams,
NewMemoryReplayLog(),
)
}

Expand Down Expand Up @@ -543,7 +544,8 @@ func newEOBRoute(numHops uint32,
}

nodes[i] = NewRouter(
privKey, &chaincfg.MainNetParams, NewMemoryReplayLog(),
&PrivKeyECDH{PrivKey: privKey}, &chaincfg.MainNetParams,
NewMemoryReplayLog(),
)
}

Expand Down