From d9b2482415a888d90e8d2a0f486bf96788e84b6f Mon Sep 17 00:00:00 2001 From: ZmnSCPxj jxPCSnmZ Date: Tue, 4 Feb 2020 13:53:17 +0800 Subject: [PATCH] lightningd/hsm_control.c: Implement `getsharedsecret`. ChangeLog-Added: New `getsharedsecret` command, which lets you compute a shared secret with this node knowing only a public point. This implements the BOLT standard of hashing the ECDH point, and is incompatible with ECIES. --- common/jsonrpc_errors.h | 3 + contrib/pyln-client/pyln/client/lightning.py | 12 +++ doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-getsharedsecret.7 | 96 ++++++++++++++++++++ doc/lightning-getsharedsecret.7.md | 93 +++++++++++++++++++ doc/lightning-listpeers.7 | 7 +- hsmd/hsmd.c | 3 +- lightningd/hsm_control.c | 44 +++++++++ tests/test_misc.py | 26 ++++++ 10 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 doc/lightning-getsharedsecret.7 create mode 100644 doc/lightning-getsharedsecret.7.md diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index 0e4f206f8..e42d0b58e 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -61,4 +61,7 @@ static const errcode_t INVOICE_HINTS_GAVE_NO_ROUTES = 902; static const errcode_t INVOICE_EXPIRED_DURING_WAIT = 903; static const errcode_t INVOICE_WAIT_TIMED_OUT = 904; +/* Errors from HSM crypto operations. */ +static const errcode_t HSM_ECDH_FAILED = 800; + #endif /* LIGHTNING_COMMON_JSONRPC_ERRORS_H */ diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 0264dfbcd..f84cb265c 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1126,3 +1126,15 @@ class LightningRpc(UnixDomainSocketRpc): "pubkey": pubkey, } return self.call("checkmessage", payload) + + def getsharedsecret(self, point, **kwargs): + """ + Compute the hash of the Elliptic Curve Diffie Hellman shared + secret point from this node private key and an + input {point}. + """ + payload = { + "point": point + } + payload.update({k: v for k, v in kwargs.items()}) + return self.call("getsharedsecret", payload) diff --git a/doc/Makefile b/doc/Makefile index 3aeb170e2..2f2c4c59c 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -23,6 +23,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-fundchannel_complete.7 \ doc/lightning-fundchannel_cancel.7 \ doc/lightning-getroute.7 \ + doc/lightning-getsharedsecret.7 \ doc/lightning-invoice.7 \ doc/lightning-listchannels.7 \ doc/lightning-listforwards.7 \ diff --git a/doc/index.rst b/doc/index.rst index c0e6bad57..f412296f4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -44,6 +44,7 @@ c-lightning Documentation lightning-fundchannel_complete lightning-fundchannel_start lightning-getroute + lightning-getsharedsecret lightning-invoice lightning-listchannels lightning-listforwards diff --git a/doc/lightning-getsharedsecret.7 b/doc/lightning-getsharedsecret.7 new file mode 100644 index 000000000..d21750114 --- /dev/null +++ b/doc/lightning-getsharedsecret.7 @@ -0,0 +1,96 @@ +.TH "LIGHTNING-GETSHAREDSECRET" "7" "" "" "lightning-getsharedsecret" +.SH NAME +lightning-getsharedsecret - Command for computing an ECDH +.SH SYNOPSIS + +\fBgetsharedsecret\fR \fIpoint\fR + +.SH DESCRIPTION + +The \fBgetsharedsecret\fR RPC command computes a shared secret from a +given public \fIpoint\fR, and the secret key of this node\. +The \fIpoint\fR is a hexadecimal string of the compressed public +key DER-encoding of the SECP256K1 point\. + +.SH RETURN VALUE + +On success, \fBgetsharedsecret\fR returns a field \fIshared_secret\fR, +which is a hexadecimal string of the 256-bit SHA-2 of the +compressed public key DER-encoding of the SECP256K1 point +that is the shared secret generated using the +Elliptic Curve Diffie-Hellman algorithm\. +This field is 32 bytes (64 hexadecimal characters in a string)\. + + +This command may fail if communications with the HSM has a +problem; +by default lightningd uses a software "HSM" which should +never fail in this way\. +(As of the time of this writing there is no true hardware +HSM that lightningd can use, but we are leaving this +possibilty open in the future\.) +In that case, it will return with an error code of 800\. + +.SH CRYPTOGRAPHIC STANDARDS + +This serves as a key agreement scheme in elliptic-curve based +cryptographic standards\. + + +However, note that most key agreement schemes based on +Elliptic-Curve Diffie-Hellman do not hash the DER-compressed +point\. +Standards like SECG SEC-1 ECIES specify using the X coordinate +of the point instead\. +The Lightning BOLT standard (which \fBlightningd\fR uses), unlike +most other cryptographic standards, specifies the SHA-256 hash +of the DER-compressed encoding of the point\. + + +It is not possible to extract the X coordinate of the ECDH point +via this API, since there is no known way to reverse the 256-bit +SHA-2 hash function\. +Thus there is no way to implement ECIES and similar standards using +this API\. + + +If you know the secret key behind \fIpoint\fR, you do not need to +even call \fBgetsharedsecret\fR, you can just multiply the secret key +with the node public key\. + + +Typically, a sender will generate an ephemeral secret key +and multiply it with the node public key, +then use the result to derive an encryption key +for a symmetric encryption scheme +to encrypt a message that can be read only by that node\. +Then the ephemeral secret key is multiplied +by the standard generator point, +and the ephemeral public key and the encrypted message is +sent to the node, +which then uses \fBgetsharedsecret\fR to derive the same key\. + + +The above sketch elides important details like +key derivation function, stream encryption scheme, +message authentication code, and so on\. +You should follow an established standard and avoid +rolling your own crypto\. + +.SH AUTHOR + +ZmnSCPxj \fI is mainly responsible\. + +.SH SEE ALSO +.SH RESOURCES +.RS +.IP \[bu] +BOLT 4: \fIhttps://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#shared-secret\fR +.IP \[bu] +BOLT 8: \fIhttps://github.com/lightningnetwork/lightning-rfc/blob/master/08-transport.md#handshake-state\fR +.IP \[bu] +SECG SEC-1 ECIES: \fIhttps://secg.org/sec1-v2.pdf\fR +.IP \[bu] +Main web site: \fIhttps://github.com/ElementsProject/lightning\fR + +.RE diff --git a/doc/lightning-getsharedsecret.7.md b/doc/lightning-getsharedsecret.7.md new file mode 100644 index 000000000..da7479a3e --- /dev/null +++ b/doc/lightning-getsharedsecret.7.md @@ -0,0 +1,93 @@ +lightning-getsharedsecret -- Command for computing an ECDH +========================================================== + +SYNOPSIS +-------- + +**getsharedsecret** *point* + +DESCRIPTION +----------- + +The **getsharedsecret** RPC command computes a shared secret from a +given public *point*, and the secret key of this node. +The *point* is a hexadecimal string of the compressed public +key DER-encoding of the SECP256K1 point. + +RETURN VALUE +------------ + +On success, **getsharedsecret** returns a field *shared\_secret*, +which is a hexadecimal string of the 256-bit SHA-2 of the +compressed public key DER-encoding of the SECP256K1 point +that is the shared secret generated using the +Elliptic Curve Diffie-Hellman algorithm. +This field is 32 bytes (64 hexadecimal characters in a string). + +This command may fail if communications with the HSM has a +problem; +by default lightningd uses a software "HSM" which should +never fail in this way. +(As of the time of this writing there is no true hardware +HSM that lightningd can use, but we are leaving this +possibilty open in the future.) +In that case, it will return with an error code of 800. + +CRYPTOGRAPHIC STANDARDS +----------------------- + +This serves as a key agreement scheme in elliptic-curve based +cryptographic standards. + +However, note that most key agreement schemes based on +Elliptic-Curve Diffie-Hellman do not hash the DER-compressed +point. +Standards like SECG SEC-1 ECIES specify using the X coordinate +of the point instead. +The Lightning BOLT standard (which `lightningd` uses), unlike +most other cryptographic standards, specifies the SHA-256 hash +of the DER-compressed encoding of the point. + +It is not possible to extract the X coordinate of the ECDH point +via this API, since there is no known way to reverse the 256-bit +SHA-2 hash function. +Thus there is no way to implement ECIES and similar standards using +this API. + +If you know the secret key behind *point*, you do not need to +even call **getsharedsecret**, you can just multiply the secret key +with the node public key. + +Typically, a sender will generate an ephemeral secret key +and multiply it with the node public key, +then use the result to derive an encryption key +for a symmetric encryption scheme +to encrypt a message that can be read only by that node. +Then the ephemeral secret key is multiplied +by the standard generator point, +and the ephemeral public key and the encrypted message is +sent to the node, +which then uses **getsharedsecret** to derive the same key. + +The above sketch elides important details like +key derivation function, stream encryption scheme, +message authentication code, and so on. +You should follow an established standard and avoid +rolling your own crypto. + +AUTHOR +------ + +ZmnSCPxj <> is mainly responsible. + +SEE ALSO +-------- + +RESOURCES +--------- + +* BOLT 4: +* BOLT 8: +* SECG SEC-1 ECIES: +* Main web site: + diff --git a/doc/lightning-listpeers.7 b/doc/lightning-listpeers.7 index 5bf11d928..500d54824 100644 --- a/doc/lightning-listpeers.7 +++ b/doc/lightning-listpeers.7 @@ -205,11 +205,16 @@ a number followed by a string unit\. The peer imposes this on us, default is 1% of the total channel capacity\. .IP \[bu] \fIspendable_msat\fR: A string describing an \fB\fIestimate\fR\fR of how much we -can send out over this channel; +can send out over this channel in a single payment (or payment-part for +multi-part payments); a number followed by a string unit\. This is an \fB\fIestimate\fR\fR, which can be wrong because adding HTLCs requires an increase in fees paid to onchain miners, and onchain fees change dynamically according to onchain activity\. +For a sufficiently-large channel with capacity on your side, this can +be limited by the rules imposed under certain blockchains; +for example, individual Bitcoin mainnet payment-parts cannot exceed +42\.94967295 mBTC\. .IP \[bu] \fIminimum_htlc_in_msat\fR: A string describing the minimum amount that an HTLC must have before we accept it\. diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c index 93c202bd9..39ad012b1 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -2055,7 +2055,8 @@ int main(int argc, char *argv[]) status_setup_async(status_conn); uintmap_init(&clients); - master = new_client(NULL, NULL, NULL, 0, HSM_CAP_MASTER | HSM_CAP_SIGN_GOSSIP | HSM_CAP_ECDH, + master = new_client(NULL, NULL, NULL, 0, + HSM_CAP_MASTER | HSM_CAP_SIGN_GOSSIP | HSM_CAP_ECDH, REQ_FD); /* First client == lightningd. */ diff --git a/lightningd/hsm_control.c b/lightningd/hsm_control.c index f220f9b38..1beef51f7 100644 --- a/lightningd/hsm_control.c +++ b/lightningd/hsm_control.c @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -12,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -123,3 +128,42 @@ void hsm_init(struct lightningd *ld) errx(1, "HSM did not give init reply"); } } + +static struct command_result *json_getsharedsecret(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct lightningd *ld = cmd->ld; + struct pubkey *point; + struct secret ss; + u8 *msg; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("point", ¶m_pubkey, &point), + NULL)) + return command_param_failed(); + + msg = towire_hsm_ecdh_req(NULL, point); + if (!wire_sync_write(ld->hsm_fd, take(msg))) + return command_fail(cmd, HSM_ECDH_FAILED, + "Failed to request ECDH to HSM"); + msg = wire_sync_read(tmpctx, ld->hsm_fd); + if (!fromwire_hsm_ecdh_resp(msg, &ss)) + return command_fail(cmd, HSM_ECDH_FAILED, + "Failed HSM response for ECDH"); + + response = json_stream_success(cmd); + json_add_secret(response, "shared_secret", &ss); + return command_success(cmd, response); +} + +static const struct json_command getsharedsecret_command = { + "getsharedsecret", + "utility", /* FIXME: Or "crypto"? */ + &json_getsharedsecret, + "Compute the hash of the Elliptic Curve Diffie Hellman shared secret point from " + "this node private key and an input {point}." +}; +AUTODATA(json_command, &getsharedsecret_command); diff --git a/tests/test_misc.py b/tests/test_misc.py index 05315ccc9..1e03ae0e9 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2151,3 +2151,29 @@ def test_sendcustommsg(node_factory): l4.daemon.wait_for_log( r'Got a custom message {serialized} from peer {peer_id}'.format( serialized=serialized, peer_id=l2.info['id'])) + + +@unittest.skipIf(not DEVELOPER, "needs --dev-force-privkey") +def test_getsharedsecret(node_factory): + """ + Test getsharedsecret command. + """ + # From BOLT 8 test vectors. + options = [ + {"dev-force-privkey": "1212121212121212121212121212121212121212121212121212121212121212"}, + {} + ] + l1, l2 = node_factory.get_nodes(2, opts=options) + + # Check BOLT 8 test vectors. + shared_secret = l1.rpc.getsharedsecret("028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f7")['shared_secret'] + assert (shared_secret == "1e2fb3c8fe8fb9f262f649f64d26ecf0f2c0a805a767cf02dc2d77a6ef1fdcc3") + + # Clear the forced privkey of l1. + del l1.daemon.opts["dev-force-privkey"] + l1.restart() + + # l1 and l2 can generate the same shared secret + # knowing only the public key of the other. + assert (l1.rpc.getsharedsecret(l2.info["id"])["shared_secret"] + == l2.rpc.getsharedsecret(l1.info["id"])["shared_secret"])