diff --git a/Cargo.lock b/Cargo.lock index 397daca82..4570d9694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,10 +1064,12 @@ dependencies = [ name = "getting-started" version = "0.1.0" dependencies = [ + "anyhow", "bip39", "gl-client", "hex", "rand 0.8.5", + "runeauth", "tokio 1.34.0", ] diff --git a/docs/src/getting-started/recover.md b/docs/src/getting-started/recover.md index 0ceb6a663..b1113c530 100644 --- a/docs/src/getting-started/recover.md +++ b/docs/src/getting-started/recover.md @@ -11,16 +11,7 @@ In order to recover access all you need to do is recover the `seed` from the BIP === "Rust" ```rust - use gl_client::{Signer, TlsConfig, Scheduler, Bitcoin}; - - let cert = ...; // Your developer certificate (client.crt) - let key = ...; // Your developer key (client-key.pem) - - let tls = TlsConfig().identity(cert, key); - let signer = Signer(seed, Network::Bitcoin, tls); - let scheduler = Scheduler::new(signer.node_id(), Network::Bitcoin).await; - - let res = scheduler.recover(&signer).await?; +--8<-- "main.rs:recover_node" ``` === "Python" diff --git a/docs/src/getting-started/register.md b/docs/src/getting-started/register.md index 0e0e99dc7..6b7a3c7d5 100644 --- a/docs/src/getting-started/register.md +++ b/docs/src/getting-started/register.md @@ -50,7 +50,7 @@ phrase and then convert it into a seed secret we can use: === "Rust" ```rust ---8<-- "main.rs:70:82" +--8<-- "main.rs:create_seed" ``` === "Python" @@ -75,20 +75,15 @@ phrase and then convert it into a seed secret we can use: funds on the node will be lost forever! We mean it when we say _you're the only one with access to the seed_! -## Registering the node +## Initializing the signer -We'll configure mTLS using our developer identity. Any connection using the -`TlsConfig`-object specified below will allow you to register new Greenlight +To initialize a signer we'll first need to configure `Nobody` credentials so we can talk to the scheduler using mTLS. Nobody credentials require data from the files downloaded from the Greenlight Developer Console, so the files must be accessible from wherever the node registration program is run. Any connection using the +`developer_creds` object will allow you to register new Greenlight nodes. === "Rust" ```rust - use gl_client::tls::TlsConfig; - - # Creating a new `TlsConfig` object using your developer certificate - # - cert: contains the content of `client.crt` - # - key: contains the content of `client-key.pem` - let tls = TlsConfig::new().identity(cert, key); +--8<-- "main.rs:dev_creds" ``` === "Python" @@ -115,9 +110,7 @@ We'll pick `bitcoin`, because ... reckless 😉 === "Rust" ```rust - use gl_client::signer::Signer; - use gl_client::bitcoin::Network; - let signer = Signer::new(secret, Network::Bitcoin, tls).unwrap(); +--8<-- "main.rs:init_signer" ``` === "Python" @@ -142,15 +135,7 @@ node's private key. Since the private key is managed exclusively by the === "Rust" ```rust - use gl_client::scheduler::Scheduler; - use gl_client::bitcoin::Network; - - let scheduler = Scheduler::new(signer.node_id(), Network::Bitcoin).await.unwrap(); - - // Passing in the signer is required because the client needs to prove - // ownership of the `node_id` - scheduler.register(&signer, None).await.unwrap(); - +--8<-- "main.rs:register_node" ``` === "Python" @@ -176,13 +161,8 @@ going forward to talk to the scheduler and the node itself. these credentials can access your node. === "Rust" - ``` - let tls = TlsConfig::new().unwrap().identity(res.device_cert, res.device_key); - - // Use the configured `tls` instance when creating `Scheduler` and `Signer` - // instance going forward - let signer = Signer(seed, Network::Bitcoin, tls); - let scheduler = Scheduler::with(signer.node_id(), Network::Bitcoin, "uri", &tls).await?; + ```rust +--8<-- "main.rs:get_node" ``` === "Python" diff --git a/docs/src/getting-started/schedule.md b/docs/src/getting-started/schedule.md index 2255672b1..e09f90f49 100644 --- a/docs/src/getting-started/schedule.md +++ b/docs/src/getting-started/schedule.md @@ -26,14 +26,7 @@ Greenlight infrastructure: === "Rust" ```rust - use hex; - let node_id = hex::decode("02058e8b6c2ad363ec59aa136429256d745164c2bdc87f98f0a68690ec2c5c9b0b")?; - let network = "testnet"; - - let tls = TlsConfig::new().unwrap().identity(device_cert, device_key); - - let scheduler = gl_client::scheduler::Scheduler(node_id, network)?; - let node: gl_client::node::ClnClient = scheduler.schedule(tls).await?; +--8<-- "main.rs:start_node" ``` === "Python" @@ -52,9 +45,7 @@ Once we have an instance of the `Node` we can start interacting with it via the === "Rust" ```rust - use gl_client::pb::cln; - let info = node.get_info(cln::GetinfoRequest::default()).await?; - let peers = node.list_peers(gl_client::pb::cln::ListpeersRequest::default()).await?; +--8<-- "main.rs:list_peers" ``` === "Python" ```python @@ -71,11 +62,7 @@ only component with access to your key. === "Rust" ```rust - node.invoice(cln::InvoiceRequest { - label: "label".to_string(), - description: "description".to_string(), - ..Default::default(), - }).await?; +--8<-- "main.rs:create_invoice" ``` === "Python" @@ -100,15 +87,7 @@ in the last chapter, instantiate the signer with it and then start it. === "Rust" ```rust - let seed = ... // Load from wherever you stored it - let (cert, key) = ... // Load the cert and key you got from the `register` call - - // The signer task will run until we send a shutdown signal on this channel - let (tx, mut rx) = tokio::sync::mpsc::channel(1); - - let tls = TlsConfig().identity(cert, key); - signer = Signer(seed, Network::Bitcoin, tls); - signer.run_forever(rx).await?; +--8<-- "main.rs:start_signer" ``` Notice that `signer.run_forever()` returns a `Future` which you can spawn a diff --git a/examples/rust/.gitignore b/examples/rust/.gitignore new file mode 100644 index 000000000..8708cc20c --- /dev/null +++ b/examples/rust/.gitignore @@ -0,0 +1,2 @@ +seed +creds \ No newline at end of file diff --git a/examples/rust/getting-started/src/extensions.rs b/examples/rust/getting-started/src/extensions.rs index 39bdcdb82..b335d69dc 100644 --- a/examples/rust/getting-started/src/extensions.rs +++ b/examples/rust/getting-started/src/extensions.rs @@ -1,7 +1,8 @@ use anyhow::{anyhow, Result}; use gl_client::{ - credentials::{Device, Nobody}, + credentials::{Device, Nobody, TlsConfigProvider}, signer::Signer, + util::get_node_id_from_tls_config, }; pub trait CredentialExt { @@ -34,12 +35,23 @@ impl CredentialExt for Device { } } +pub trait DeviceExt { + fn node_id(&self) -> Result>; +} + +impl DeviceExt for Device { + fn node_id(&self) -> Result> { + get_node_id_from_tls_config(&self.tls_config()) + } +} + pub trait SignerExt { // I would name this create_default_rune but it might cause confusion // with the Default::default() used in the Device's default fn add_base_rune_to_device_credentials(&self, creds: Device) -> Result; } +//TODO: Delete after Device::upgrade is made idempotent impl SignerExt for Signer { fn add_base_rune_to_device_credentials(&self, mut creds: Device) -> Result { if creds.rune != String::default() { diff --git a/examples/rust/getting-started/src/main.rs b/examples/rust/getting-started/src/main.rs index 56d370873..f6b15af96 100644 --- a/examples/rust/getting-started/src/main.rs +++ b/examples/rust/getting-started/src/main.rs @@ -1,4 +1,8 @@ +use std::fs::{self, File}; +use std::io::Write; + use anyhow::{anyhow, Result}; +use bip39::{Language, Mnemonic}; use gl_client::credentials::{Device, Nobody, RuneProvider, TlsConfigProvider}; use gl_client::node::ClnClient; use gl_client::pb::{self, cln}; @@ -10,35 +14,48 @@ mod extensions; use extensions::*; #[tokio::main] -async fn main() { - let seed = create_seed(); +async fn main() {} + +fn save_to_file(file_name: &str, data: Vec) { + fs::write(file_name, data).unwrap(); } -async fn create_seed() -> Vec { - use bip39::{Language, Mnemonic}; +fn read_file(file_name: &str) -> Vec{ + fs::read(file_name).unwrap() +} +async fn create_seed() -> Vec { + // ---8<--- [start: create_seed] let mut rng = rand::thread_rng(); let m = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap(); - let phrase = m.word_iter().fold("".to_string(), |c, n| c + " " + n); - // Prompt user to safely store the phrase + //Show seed phrase to user + let phrase = m.word_iter().fold("".to_string(), |c, n| c + " " + n); const EMPTY_PASSPHRASE: &str = ""; let seed = &m.to_seed(EMPTY_PASSPHRASE)[0..32]; // Only need the first 32 bytes // Store the seed on the filesystem, or secure configuration system - seed[0..32].to_vec() -} + save_to_file("seed", seed.to_vec()); -async fn register_node(seed: Vec, developer_cert: Vec, developer_key: Vec) { - // Creating a new `TlsConfig` object using your developer certificate - // cert: contains the content of `client.crt` - // key: contains the content of `client-key.pem` + // ---8<--- [end: create_seed] + seed.to_vec() +} +async fn register_node(seed: Vec, developer_cert_path: String, developer_key_path: String) { + // ---8<--- [start: dev_creds] + let developer_cert = std::fs::read(developer_cert_path).unwrap_or_default(); + let developer_key = std::fs::read(developer_key_path).unwrap_or_default(); let developer_creds = Nobody::with_identity(developer_cert, developer_key); - let signer = Signer::new(seed, Network::Bitcoin, developer_creds.clone()).unwrap(); + // ---8<--- [end: dev_creds] + + // ---8<--- [start: init_signer] + let network = Network::Bitcoin; + let signer = Signer::new(seed, network, developer_creds.clone()).unwrap(); + // ---8<--- [end: init_signer] - let scheduler = Scheduler::new(signer.node_id(), Network::Bitcoin, developer_creds) + // ---8<--- [start: register_node] + let scheduler = Scheduler::new(signer.node_id(), network, developer_creds) .await .unwrap(); @@ -46,18 +63,21 @@ async fn register_node(seed: Vec, developer_cert: Vec, developer_key: Ve // ownership of the `node_id` let registration_response = scheduler.register(&signer, None).await.unwrap(); - // Authenticating the scheduler let device_creds = Device::from_bytes(registration_response.creds); + save_to_file("creds", device_creds.to_bytes()); + // ---8<--- [end: register_node] - // Save the credentials somewhere safe - + // ---8<--- [start: get_node] let scheduler = scheduler.authenticate(device_creds).await.unwrap(); let mut node: ClnClient = scheduler.node().await.unwrap(); + // ---8<--- [end: get_node] } -async fn start_node(signer: Signer, device_creds: Device) { +async fn start_node(device_creds_path: String) { + // ---8<--- [start: start_node] + let device_creds = Device::from_path(device_creds_path); let scheduler = gl_client::scheduler::Scheduler::new( - signer.node_id(), + device_creds.node_id().unwrap(), gl_client::bitcoin::Network::Bitcoin, device_creds.clone(), ) @@ -65,12 +85,28 @@ async fn start_node(signer: Signer, device_creds: Device) { .unwrap(); let mut node: gl_client::node::ClnClient = scheduler.node().await.unwrap(); + // ---8<--- [end: start_node] + + // ---8<--- [start: list_peers] + let info = node.getinfo(cln::GetinfoRequest::default()).await.unwrap(); + let peers = node + .list_peers(gl_client::pb::cln::ListpeersRequest::default()) + .await + .unwrap(); + // ---8<--- [end: list_peers] + + // ---8<--- [start: start_signer] + let network = Network::Bitcoin; + let seed = read_file("seed"); + let signer = Signer::new(seed, network, device_creds.clone()).unwrap(); let (_tx, rx) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { signer.run_forever(rx).await.unwrap(); }); + // ---8<--- [end: start_signer] + // ---8<--- [start: create_invoice] node.invoice(cln::InvoiceRequest { label: "label".to_string(), description: "description".to_string(), @@ -78,13 +114,15 @@ async fn start_node(signer: Signer, device_creds: Device) { }) .await .unwrap(); + // ---8<--- [end: create_invoice] } async fn recover_node( device_cert: Vec, device_key: Vec, - seed: Vec, ) -> Result { + // ---8<--- [start: recover_node] + let seed = read_file("seed"); let network = gl_client::bitcoin::Network::Bitcoin; let signer_creds = Device::with_identity(device_cert.clone(), device_key.clone()); let signer = gl_client::signer::Signer::new(seed, network, signer_creds.clone()).unwrap(); @@ -92,6 +130,7 @@ async fn recover_node( let scheduler_creds = signer .add_base_rune_to_device_credentials(signer_creds) .unwrap(); + let scheduler = gl_client::scheduler::Scheduler::new( signer.node_id(), gl_client::bitcoin::Network::Bitcoin, @@ -101,4 +140,5 @@ async fn recover_node( .unwrap(); scheduler.recover(&signer).await + // ---8<--- [end: recover_node] } diff --git a/libs/gl-client/src/lib.rs b/libs/gl-client/src/lib.rs index e665eb511..729ed9b6c 100644 --- a/libs/gl-client/src/lib.rs +++ b/libs/gl-client/src/lib.rs @@ -59,7 +59,7 @@ pub mod credentials; /// Functionality to integrate greenlight with a Lightning Service Provider pub mod lsps; -mod util; +pub mod util; use thiserror::Error; diff --git a/libs/gl-client/src/scheduler.rs b/libs/gl-client/src/scheduler.rs index d79bdd289..e135f4970 100644 --- a/libs/gl-client/src/scheduler.rs +++ b/libs/gl-client/src/scheduler.rs @@ -2,6 +2,7 @@ use crate::credentials::{self, RuneProvider, TlsConfigProvider}; use crate::node::{self, GrpcClient}; use crate::pb::scheduler::scheduler_client::SchedulerClient; use crate::tls::{self}; +use crate::util::get_node_id_from_tls_config; use crate::utils::scheduler_uri; use crate::{pb, signer::Signer}; use anyhow::{anyhow, Result}; @@ -433,7 +434,7 @@ where where T: GrpcClient, { - let cert_node_id = self.get_node_id_from_tls_config()?; + let cert_node_id = get_node_id_from_tls_config(&self.creds.tls_config())?; if cert_node_id != self.node_id { return Err(anyhow!("The node_id defined on the Credential's certificate does not match the node_id the scheduler was initialized with\nExpected {}, got {}", hex::encode(&self.node_id), hex::encode(&cert_node_id))); @@ -467,7 +468,7 @@ where &self, uri: String ) -> Result { - let node_id = self.get_node_id_from_tls_config()?; + let node_id = get_node_id_from_tls_config(&self.creds.tls_config())?; let res = self .client .clone() @@ -482,7 +483,7 @@ where pub async fn list_outgoing_webhooks( &self, ) -> Result { - let node_id = self.get_node_id_from_tls_config()?; + let node_id = get_node_id_from_tls_config(&self.creds.tls_config())?; let res = self .client .clone() @@ -497,7 +498,7 @@ where &self, webhook_ids: Vec ) -> Result { - let node_id = self.get_node_id_from_tls_config()?; + let node_id = get_node_id_from_tls_config(&self.creds.tls_config())?; let res = self .client .clone() @@ -513,7 +514,7 @@ where &self, webhook_id: i64, ) -> Result { - let node_id = self.get_node_id_from_tls_config()?; + let node_id = get_node_id_from_tls_config(&self.creds.tls_config())?; let res = self .client .clone() @@ -524,31 +525,6 @@ where .await?; Ok(res.into_inner()) } - - fn get_node_id_from_tls_config(&self) -> Result> { - let tls_config = self.creds.tls_config(); - let subject_common_name = match &tls_config.x509_cert { - Some(x) => match x.subject_common_name() { - Some(cn) => cn, - None => { - return Err(anyhow!( - "Failed to parse the subject common name in the provided x509 certificate" - )) - } - }, - None => { - return Err(anyhow!( - "The certificate could not be parsed in the x509 format" - )) - } - }; - - let split_subject_common_name = subject_common_name.split("/").collect::>(); - - assert!(split_subject_common_name[1] == "users"); - Ok(hex::decode(split_subject_common_name[2]) - .expect("Failed to parse the node_id from the TlsConfig to bytes")) - } } #[cfg(test)] diff --git a/libs/gl-client/src/util.rs b/libs/gl-client/src/util.rs index fead0f6dc..97c8c654c 100644 --- a/libs/gl-client/src/util.rs +++ b/libs/gl-client/src/util.rs @@ -1,7 +1,5 @@ - - - - +use anyhow::{anyhow, Result}; +use crate::tls::TlsConfig; pub fn is_feature_bit_enabled(bitmap : &[u8], index : usize) -> bool { let n_bytes = bitmap.len(); @@ -19,7 +17,29 @@ pub fn is_feature_bit_enabled(bitmap : &[u8], index : usize) -> bool { return (selected_byte & bit_mask) != 0 } - +pub fn get_node_id_from_tls_config(tls_config: &TlsConfig) -> Result> { + let subject_common_name = match &tls_config.x509_cert { + Some(x) => match x.subject_common_name() { + Some(cn) => cn, + None => { + return Err(anyhow!( + "Failed to parse the subject common name in the provided x509 certificate" + )) + } + }, + None => { + return Err(anyhow!( + "The certificate could not be parsed in the x509 format" + )) + } + }; + + let split_subject_common_name = subject_common_name.split("/").collect::>(); + + assert!(split_subject_common_name[1] == "users"); + Ok(hex::decode(split_subject_common_name[2]) + .expect("Failed to parse the node_id from the TlsConfig to bytes")) +} #[cfg(test)] mod test {