From d18f9decdaeeb7adf186dde192ef499ce94be7dd Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 Apr 2020 16:20:26 +0200 Subject: [PATCH 1/3] crypto: add ECDH interface and implementation With this commit we create a new interface that abstracts away the ECDH implementation. We also create one implementation that satisfies the interface that can be used if the private key is known directly. Later we will change the generateSharedSecret function to use this interface instead of doing the ECDH operation directly. --- crypto.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crypto.go b/crypto.go index 3b0d521..d3c2798 100644 --- a/crypto.go +++ b/crypto.go @@ -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 +// 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. +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 From a601ae1da374561f3b3c5f04a77770f7846af8d4 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 Apr 2020 16:25:26 +0200 Subject: [PATCH 2/3] crypto+sphinx: add error return value This is a preparatory commit that adds an error return value to the generateSharedSecret and generateSharedSecrets method. This is needed because the interface we want to abstract the onion key behind has an error return value too. --- crypto.go | 15 ++++++++++----- obfuscation_test.go | 10 ++++++++-- sphinx.go | 19 ++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/crypto.go b/crypto.go index d3c2798..c14cf51 100644 --- a/crypto.go +++ b/crypto.go @@ -198,8 +198,7 @@ func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { } // Compute our shared secret. - sharedSecret = generateSharedSecret(dhKey, r.onionKey) - return sharedSecret, nil + return generateSharedSecret(dhKey, r.onionKey) } // generateSharedSecret generates the shared secret for a particular hop. The @@ -208,11 +207,13 @@ func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { // 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 { +func generateSharedSecret(pub *btcec.PublicKey, priv *btcec.PrivateKey) (Hash256, + error) { + s := &btcec.PublicKey{} s.X, s.Y = btcec.S256().ScalarMult(pub.X, pub.Y, priv.D.Bytes()) - return sha256.Sum256(s.SerializeCompressed()) + return sha256.Sum256(s.SerializeCompressed()), nil } // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a @@ -249,10 +250,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 diff --git a/obfuscation_test.go b/obfuscation_test.go index 3f0026b..c4ebf6e 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -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{ @@ -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 diff --git a/sphinx.go b/sphinx.go index dad38c9..2fbdc99 100644 --- a/sphinx.go +++ b/sphinx.go @@ -117,7 +117,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 @@ -131,8 +131,14 @@ 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][:]) + sharedSecret, err := generateSharedSecret(paymentPath[0], sessionKey) + 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 @@ -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 @@ -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) From d019eaf53e32bdb9882ac3dcc0d10bad5d367acf Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 Apr 2020 16:29:15 +0200 Subject: [PATCH 3/3] multi: hide long-term private key behind interface To be able to eventually extract the private keys out of the node itself into a hardware wallet or HSM, we need to abstract the ECDH operation against the long-term key behind an interface. --- cmd/main.go | 5 ++++- crypto.go | 17 +---------------- sphinx.go | 19 ++++++------------- sphinx_test.go | 6 ++++-- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index d74c58e..3b43868 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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() diff --git a/crypto.go b/crypto.go index c14cf51..9988a75 100644 --- a/crypto.go +++ b/crypto.go @@ -198,22 +198,7 @@ func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { } // Compute our shared secret. - return generateSharedSecret(dhKey, r.onionKey) -} - -// 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, - error) { - - s := &btcec.PublicKey{} - s.X, s.Y = btcec.S256().ScalarMult(pub.X, pub.Y, priv.D.Bytes()) - - return sha256.Sum256(s.SerializeCompressed()), nil + return r.onionKey.ECDH(dhKey) } // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a diff --git a/sphinx.go b/sphinx.go index 2fbdc99..f8e1a15 100644 --- a/sphinx.go +++ b/sphinx.go @@ -2,7 +2,6 @@ package sphinx import ( "bytes" - "crypto/ecdsa" "crypto/hmac" "crypto/sha256" "fmt" @@ -131,7 +130,8 @@ 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() - sharedSecret, err := generateSharedSecret(paymentPath[0], sessionKey) + sessionKeyECDH := &PrivKeyECDH{PrivKey: sessionKey} + sharedSecret, err := sessionKeyECDH.ECDH(paymentPath[0]) if err != nil { return nil, err } @@ -488,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())) @@ -505,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, } } diff --git a/sphinx_test.go b/sphinx_test.go index f2f5054..ef7edef 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -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(), ) } @@ -543,7 +544,8 @@ func newEOBRoute(numHops uint32, } nodes[i] = NewRouter( - privKey, &chaincfg.MainNetParams, NewMemoryReplayLog(), + &PrivKeyECDH{PrivKey: privKey}, &chaincfg.MainNetParams, + NewMemoryReplayLog(), ) }