-
Notifications
You must be signed in to change notification settings - Fork 421
Adds lightning message signing/verification/pk_recovery #844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7f405e
6f7a2bc
7c9302f
d871645
f13e38c
7bcf5a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // This file is Copyright its original authors, visible in version control | ||
| // history. | ||
| // | ||
| // This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE | ||
| // or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
| // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. | ||
| // You may not use this file except in accordance with one or both of these | ||
| // licenses. | ||
|
|
||
| // This file is auto-generated by gen_target.sh based on target_template.txt | ||
| // To modify it, modify target_template.txt and run gen_target.sh instead. | ||
|
|
||
| #![cfg_attr(feature = "libfuzzer_fuzz", no_main)] | ||
|
|
||
| extern crate lightning_fuzz; | ||
| use lightning_fuzz::zbase32::*; | ||
|
|
||
| #[cfg(feature = "afl")] | ||
| #[macro_use] extern crate afl; | ||
| #[cfg(feature = "afl")] | ||
| fn main() { | ||
| fuzz!(|data| { | ||
| zbase32_run(data.as_ptr(), data.len()); | ||
| }); | ||
| } | ||
|
|
||
| #[cfg(feature = "honggfuzz")] | ||
| #[macro_use] extern crate honggfuzz; | ||
| #[cfg(feature = "honggfuzz")] | ||
| fn main() { | ||
| loop { | ||
| fuzz!(|data| { | ||
| zbase32_run(data.as_ptr(), data.len()); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(feature = "libfuzzer_fuzz")] | ||
| #[macro_use] extern crate libfuzzer_sys; | ||
| #[cfg(feature = "libfuzzer_fuzz")] | ||
| fuzz_target!(|data: &[u8]| { | ||
| zbase32_run(data.as_ptr(), data.len()); | ||
| }); | ||
|
|
||
| #[cfg(feature = "stdin_fuzz")] | ||
| fn main() { | ||
| use std::io::Read; | ||
|
|
||
| let mut data = Vec::with_capacity(8192); | ||
| std::io::stdin().read_to_end(&mut data).unwrap(); | ||
| zbase32_run(data.as_ptr(), data.len()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn run_test_cases() { | ||
| use std::fs; | ||
| use std::io::Read; | ||
| use lightning_fuzz::utils::test_logger::StringBuffer; | ||
|
|
||
| use std::sync::{atomic, Arc}; | ||
| { | ||
| let data: Vec<u8> = vec![0]; | ||
| zbase32_run(data.as_ptr(), data.len()); | ||
| } | ||
| let mut threads = Vec::new(); | ||
| let threads_running = Arc::new(atomic::AtomicUsize::new(0)); | ||
| if let Ok(tests) = fs::read_dir("test_cases/zbase32") { | ||
| for test in tests { | ||
| let mut data: Vec<u8> = Vec::new(); | ||
| let path = test.unwrap().path(); | ||
| fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); | ||
| threads_running.fetch_add(1, atomic::Ordering::AcqRel); | ||
|
|
||
| let thread_count_ref = Arc::clone(&threads_running); | ||
| let main_thread_ref = std::thread::current(); | ||
| threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), | ||
| std::thread::spawn(move || { | ||
| let string_logger = StringBuffer::new(); | ||
|
|
||
| let panic_logger = string_logger.clone(); | ||
| let res = if ::std::panic::catch_unwind(move || { | ||
| zbase32_test(&data, panic_logger); | ||
| }).is_err() { | ||
| Some(string_logger.into_string()) | ||
| } else { None }; | ||
| thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); | ||
| main_thread_ref.unpark(); | ||
| res | ||
| }) | ||
| )); | ||
| while threads_running.load(atomic::Ordering::Acquire) > 32 { | ||
| std::thread::park(); | ||
| } | ||
| } | ||
| } | ||
| for (test, thread) in threads.drain(..) { | ||
| if let Some(output) = thread.join().unwrap() { | ||
| println!("Output of {}:\n{}", test, output); | ||
| panic!(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // This file is Copyright its original authors, visible in version control | ||
| // history. | ||
| // | ||
| // This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE | ||
| // or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
| // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. | ||
| // You may not use this file except in accordance with one or both of these | ||
| // licenses. | ||
|
|
||
| use lightning::util::zbase32; | ||
|
|
||
| use utils::test_logger; | ||
|
|
||
| #[inline] | ||
| pub fn do_test(data: &[u8]) { | ||
| let res = zbase32::encode(data); | ||
| assert_eq!(&zbase32::decode(&res).unwrap()[..], data); | ||
|
|
||
| if let Ok(s) = std::str::from_utf8(data) { | ||
| if let Ok(decoded) = zbase32::decode(s) { | ||
| assert_eq!(&zbase32::encode(&decoded), &s.to_ascii_lowercase()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn zbase32_test<Out: test_logger::Output>(data: &[u8], _out: Out) { | ||
| do_test(data); | ||
| } | ||
|
|
||
| #[no_mangle] | ||
| pub extern "C" fn zbase32_run(data: *const u8, datalen: usize) { | ||
| do_test(unsafe { std::slice::from_raw_parts(data, datalen) }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE | ||
| // or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
| // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. | ||
| // You may not use this file except in accordance with one or both of these | ||
| // licenses. | ||
|
|
||
| //! Lightning message signing and verification lives here. These tools can be used to sign messages using the node's | ||
| //! secret so receivers are sure that they come from you. You can also use this to verify that a given message comes | ||
| //! from a specific node. | ||
| //! Furthermore, these tools can be used to sign / verify messages using ephemeral keys not tied to node's identities. | ||
| //! | ||
| //! Note this is not part of the specs, but follows lnd's signing and verifying protocol, which can is defined as follows: | ||
| //! | ||
| //! signature = zbase32(SigRec(sha256d(("Lightning Signed Message:" + msg))) | ||
| //! zbase32 from https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt | ||
| //! SigRec has first byte 31 + recovery id, followed by 64 byte sig. | ||
| //! | ||
| //! This implementation is compatible with both lnd's and c-lightning's | ||
| //! | ||
| //! https://lightning.readthedocs.io/lightning-signmessage.7.html | ||
| //! https://api.lightning.community/#signmessage | ||
| use crate::util::zbase32; | ||
sr-gi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| use bitcoin::hashes::{sha256d, Hash}; | ||
| use bitcoin::secp256k1::recovery::{RecoverableSignature, RecoveryId}; | ||
| use bitcoin::secp256k1::{Error, Message, PublicKey, Secp256k1, SecretKey}; | ||
|
|
||
| static LN_MESSAGE_PREFIX: &[u8] = b"Lightning Signed Message:"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bind message to a lightning signing context to avoid replay of signed data in another context with same key material ? Though w.r.t to this concern our message API signing is safe as this is always committed by the implementation and not an argument caller, not sure if it's worthy to document. Do we have risks of harmful API misuses we should document ? I think we're good, can't foresee real ones.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's the purpose of the prefix AFAIK. I can write a comment there stating the purpose of the prefix if you feel it's necessary.
I cannot see any either. |
||
|
|
||
| fn sigrec_encode(sig_rec: RecoverableSignature) -> Vec<u8> { | ||
| let (rid, rsig) = sig_rec.serialize_compact(); | ||
| let prefix = rid.to_i32() as u8 + 31; | ||
|
|
||
sr-gi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [&[prefix], &rsig[..]].concat() | ||
| } | ||
|
|
||
| fn sigrec_decode(sig_rec: Vec<u8>) -> Result<RecoverableSignature, Error> { | ||
| let rsig = &sig_rec[1..]; | ||
| let rid = sig_rec[0] as i32 - 31; | ||
|
|
||
| match RecoveryId::from_i32(rid) { | ||
| Ok(x) => RecoverableSignature::from_compact(rsig, x), | ||
| Err(e) => Err(e) | ||
| } | ||
| } | ||
|
|
||
| /// Creates a digital signature of a message given a SecretKey, like the node's secret. | ||
| /// A receiver knowing the PublicKey (e.g. the node's id) and the message can be sure that the signature was generated by the caller. | ||
| /// Signatures are EC recoverable, meaning that given the message and the signature the PublicKey of the signer can be extracted. | ||
| pub fn sign(msg: &[u8], sk: SecretKey) -> Result<String, Error> { | ||
sr-gi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let secp_ctx = Secp256k1::signing_only(); | ||
| let msg_hash = sha256d::Hash::hash(&[LN_MESSAGE_PREFIX, msg].concat()); | ||
|
|
||
| let sig = secp_ctx.sign_recoverable(&Message::from_slice(&msg_hash)?, &sk); | ||
| Ok(zbase32::encode(&sigrec_encode(sig))) | ||
| } | ||
|
|
||
| /// Recovers the PublicKey of the signer of the message given the message and the signature. | ||
| pub fn recover_pk(msg: &[u8], sig: &str) -> Result<PublicKey, Error> { | ||
| let secp_ctx = Secp256k1::verification_only(); | ||
| let msg_hash = sha256d::Hash::hash(&[LN_MESSAGE_PREFIX, msg].concat()); | ||
|
|
||
| match zbase32::decode(&sig) { | ||
| Ok(sig_rec) => { | ||
| match sigrec_decode(sig_rec) { | ||
| Ok(sig) => secp_ctx.recover(&Message::from_slice(&msg_hash)?, &sig), | ||
| Err(e) => Err(e) | ||
| } | ||
| }, | ||
| Err(_) => Err(Error::InvalidSignature) | ||
| } | ||
| } | ||
|
|
||
| /// Verifies a message was signed by a PrivateKey that derives to a given PublicKey, given a message, a signature, | ||
| /// and the PublicKey. | ||
| pub fn verify(msg: &[u8], sig: &str, pk: PublicKey) -> bool { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested this with the sample and Haven't tested further.
Also added
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wouldn't this mean the two nodes are not properly connected (or the node that originated the signature is not on the verifiers database?). I'm wondering since you point that the recovered public key is correct, so the signature is actually valid. From lnd docs:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @valentinewallace I double checked this against However, in Checking c-lightning's docs, we can find the following:
And, If I check
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Furthermore, querying I'm not sure why that's happening though. Any thoughts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asked around #c-lightning on IRC, looks like a channel announced is required to show-up in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed offline, this seems like a reasonable explanation for why lnd's giving |
||
| match recover_pk(msg, sig) { | ||
| Ok(x) => x == pk, | ||
| Err(_) => false | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test { | ||
| use std::str::FromStr; | ||
| use util::message_signing::{sign, recover_pk, verify}; | ||
| use bitcoin::secp256k1::key::ONE_KEY; | ||
| use bitcoin::secp256k1::{PublicKey, Secp256k1}; | ||
|
|
||
| #[test] | ||
| fn test_sign() { | ||
| let message = "test message"; | ||
| let zbase32_sig = sign(message.as_bytes(), ONE_KEY); | ||
|
|
||
| assert_eq!(zbase32_sig.unwrap(), "d9tibmnic9t5y41hg7hkakdcra94akas9ku3rmmj4ag9mritc8ok4p5qzefs78c9pqfhpuftqqzhydbdwfg7u6w6wdxcqpqn4sj4e73e") | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_recover_pk() { | ||
| let message = "test message"; | ||
| let sig = "d9tibmnic9t5y41hg7hkakdcra94akas9ku3rmmj4ag9mritc8ok4p5qzefs78c9pqfhpuftqqzhydbdwfg7u6w6wdxcqpqn4sj4e73e"; | ||
sr-gi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let pk = recover_pk(message.as_bytes(), sig); | ||
|
|
||
| assert_eq!(pk.unwrap(), PublicKey::from_secret_key(&Secp256k1::signing_only(), &ONE_KEY)) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_verify() { | ||
| let message = "another message"; | ||
| let sig = sign(message.as_bytes(), ONE_KEY).unwrap(); | ||
| let pk = PublicKey::from_secret_key(&Secp256k1::signing_only(), &ONE_KEY); | ||
|
|
||
| assert!(verify(message.as_bytes(), &sig, pk)) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_verify_ground_truth_ish() { | ||
| // There are no standard tests vectors for Sign/Verify, using the same tests vectors as c-lightning to see if they are compatible. | ||
| // Taken from https://github.com/ElementsProject/lightning/blob/1275af6fbb02460c8eb2f00990bb0ef9179ce8f3/tests/test_misc.py#L1925-L1938 | ||
|
|
||
| let corpus = [ | ||
| ["@bitconner", | ||
| "is this compatible?", | ||
| "rbgfioj114mh48d8egqx8o9qxqw4fmhe8jbeeabdioxnjk8z3t1ma1hu1fiswpakgucwwzwo6ofycffbsqusqdimugbh41n1g698hr9t", | ||
| "02b80cabdf82638aac86948e4c06e82064f547768dcef977677b9ea931ea75bab5"], | ||
| ["@duck1123", | ||
| "hi", | ||
| "rnrphcjswusbacjnmmmrynh9pqip7sy5cx695h6mfu64iac6qmcmsd8xnsyczwmpqp9shqkth3h4jmkgyqu5z47jfn1q7gpxtaqpx4xg", | ||
| "02de60d194e1ca5947b59fe8e2efd6aadeabfb67f2e89e13ae1a799c1e08e4a43b"], | ||
| ["@jochemin", | ||
| "hi", | ||
| "ry8bbsopmduhxy3dr5d9ekfeabdpimfx95kagdem7914wtca79jwamtbw4rxh69hg7n6x9ty8cqk33knbxaqftgxsfsaeprxkn1k48p3", | ||
| "022b8ece90ee891cbcdac0c1cc6af46b73c47212d8defbce80265ac81a6b794931"], | ||
| ]; | ||
|
|
||
| for c in &corpus { | ||
| assert!(verify(c[1].as_bytes(), c[2], PublicKey::from_str(c[3]).unwrap())) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,9 +15,14 @@ pub(crate) mod fuzz_wrappers; | |||||||||||
| pub mod events; | ||||||||||||
| pub mod errors; | ||||||||||||
| pub mod ser; | ||||||||||||
| pub mod message_signing; | ||||||||||||
|
|
||||||||||||
| pub(crate) mod byte_utils; | ||||||||||||
| pub(crate) mod chacha20; | ||||||||||||
| #[cfg(feature = "fuzztarget")] | ||||||||||||
| pub mod zbase32; | ||||||||||||
| #[cfg(not(feature = "fuzztarget"))] | ||||||||||||
| pub(crate) mod zbase32; | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, the fuzztarget mode needs it, see CI.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added it in cf384a3 and changed
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The compile error in CI is that |
||||||||||||
| #[cfg(not(feature = "fuzztarget"))] | ||||||||||||
| pub(crate) mod poly1305; | ||||||||||||
| pub(crate) mod chacha20poly1305rfc; | ||||||||||||
|
|
||||||||||||
Uh oh!
There was an error while loading. Please reload this page.