Skip to content

Commit

Permalink
Merge pull request #144 from orbitalturtle/tls-cfg-options
Browse files Browse the repository at this point in the history
odds and ends: tls SAN cfg and payer note option
  • Loading branch information
orbitalturtle authored Aug 5, 2024
2 parents c505ca4 + b3f9c30 commit 25e3e2d
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 38 deletions.
17 changes: 10 additions & 7 deletions config_spec.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,29 @@ type = "String"
optional = false
doc = "LND GRPC address, kindly note that the address must start with https:// ip address : port."


[[param]]
name = "cert_path"
type = "std::path::PathBuf"
optional = true
doc = "The path to LND tls certificate file. Note: the abosolute tls certificate file path is required here."


[[param]]
name = "macaroon_path"
type = "std::path::PathBuf"
name = "cert_pem"
type = "String"
optional = true
doc = "The path to LND macaroon file. Note: the abosolute macaroon file path is required here."
doc = "The PEM-encoded tls certificate to pass directly into LNDK."

[[param]]
name = "cert_pem"
name = "tls_ip"
type = "String"
optional = true
doc = "The PEM-encoded tls certificate to pass directly into LNDK."
doc = "Add an ip or domain to the certificate used to access the LNDK server. To add multiple ip addresses, separate each ip address with a comma. Like: '192.168.0.1,lndkisdope.org'"

[[param]]
name = "macaroon_path"
type = "std::path::PathBuf"
optional = true
doc = "The path to LND macaroon file. Note: the abosolute macaroon file path is required here."

[[param]]
name = "macaroon_hex"
Expand Down
16 changes: 16 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ which you can do in [most languages](https://grpc.io/docs/languages/).
Again, since LNDK needs to connect to LND, you'll need to pass in your LND macaroon to establish a connection. Note that:
- The client must pass in this data via gRPC metadata. You can find an example of this in the [Rust client](https://github.com/lndk-org/lndk/blob/master/src/cli.rs) used to connect `lndk-cli` to the server.

## Baking a custom macaroon

Rather than use the admin.macaroon with unrestricted permission to an LND node, we can bake a macaroon using lncli with much more specific permissions for better security. Note also that the macaroon required for [starting up a LNDK instance](https://github.com/lndk-org/lndk?tab=readme-ov-file#custom-macaroon) requires different permissions than when making a payment.

When using `pay-offer`, you can generate a macaroon which will give LNDK only the specific grpc endpoints it needs to hit:

```
lncli bakemacaroon --save_to=<FILEPATH>/lndk-pay.macaroon uri:/walletrpc.WalletKit/DeriveKey uri:/signrpc.Signer/SignMessage uri:/lnrpc.Lightning/GetNodeInfo uri:/lnrpc.Lightning/ConnectPeer uri:/lnrpc.Lightning/GetInfo uri:/lnrpc.Lightning/ListPeers uri:/lnrpc.Lightning/GetChanInfo uri:/lnrpc.Lightning/QueryRoutes uri:/routerrpc.Router/SendToRouteV2 uri:/routerrpc.Router/TrackPaymentV2
```

If you're using just the `get-invoice` command, you can bake a macaroon with less permissions:

```
lncli bakemacaroon --save_to=<FILEPATH>/lndk-pay.macaroon uri:/walletrpc.WalletKit/DeriveKey uri:/signrpc.Signer/SignMessage uri:/lnrpc.Lightning/GetNodeInfo uri:/lnrpc.Lightning/ConnectPeer uri:/lnrpc.Lightning/GetInfo uri:/lnrpc.Lightning/ListPeers uri:/lnrpc.Lightning/GetChanInfo
```

## TLS: Running `lndk-cli` remotely

When `LNDK` is started up, self-signed TLS credentials are automatically generated and stored in `~/.lndk`. If you're running `lndk-cli` locally, it'll know where to find the certificate file it needs to establish a secure connection with the LNDK server.
Expand Down
3 changes: 3 additions & 0 deletions proto/lndkrpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ service Offers {
message PayOfferRequest {
string offer = 1;
optional uint64 amount = 2;
optional string payer_note = 3;
}

message PayOfferResponse {
Expand All @@ -20,6 +21,7 @@ message PayOfferResponse {
message GetInvoiceRequest {
string offer = 1;
optional uint64 amount = 2;
optional string payer_note = 3;
}

message DecodeInvoiceRequest {
Expand Down Expand Up @@ -52,6 +54,7 @@ message Bolt12InvoiceContents {
PublicKey node_id = 9;
string signature = 10;
repeated FeatureBit features = 11;
optional string payer_note = 12;
}

message PaymentHash {
Expand Down
50 changes: 35 additions & 15 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,25 @@ enum Commands {
/// whatever the offer amount is.
#[arg(required = false)]
amount: Option<u64>,

/// A payer-provided note which will be seen by the recipient.
#[arg(required = false)]
payer_note: Option<String>,
},
/// GetInvoice fetch a BOLT 12 invoice, which will be returned as a hex-encoded string. It
/// fetches the invoice from a BOLT 12 offer, provided as a 'lno'-prefaced offer string.
GetInvoice {
/// The offer string.
offer_string: String,

/// Amount the user would like to pay. If this isn't set, we'll assume the user is paying
/// whatever the offer amount is.
#[arg(required = false)]
amount: Option<u64>,

/// A payer-provided note which will be seen by the recipient.
#[arg(required = false)]
payer_note: Option<String>,
},
/// PayInvoice pays a hex-encoded BOLT12 invoice.
PayInvoice {
Expand Down Expand Up @@ -144,9 +153,10 @@ async fn main() {
Commands::PayOffer {
ref offer_string,
amount,
payer_note,
} => {
let tls = read_cert_from_args(&args);
let grpc_host = args.grpc_host.clone();
let tls = read_cert_from_args(args.cert_pem);
let grpc_host = args.grpc_host;
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| {
Expand Down Expand Up @@ -179,10 +189,12 @@ async fn main() {
}
};

let macaroon = read_macaroon_from_args(&args);
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(PayOfferRequest {
offer: offer.to_string(),
amount,
payer_note,
});
add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1));

Expand All @@ -197,9 +209,10 @@ async fn main() {
Commands::GetInvoice {
ref offer_string,
amount,
payer_note,
} => {
let tls = read_cert_from_args(&args);
let grpc_host = args.grpc_host.clone();
let tls = read_cert_from_args(args.cert_pem);
let grpc_host = args.grpc_host;
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| {
Expand Down Expand Up @@ -231,10 +244,12 @@ async fn main() {
}
};

let macaroon = read_macaroon_from_args(&args);
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(GetInvoiceRequest {
offer: offer.to_string(),
amount,
payer_note,
});
add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1));
match client.get_invoice(request).await {
Expand All @@ -251,7 +266,7 @@ async fn main() {
ref invoice_string,
amount,
} => {
let tls = read_cert_from_args(&args);
let tls = read_cert_from_args(args.cert_pem.clone());
let grpc_host = args.grpc_host.clone();
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
Expand All @@ -272,7 +287,8 @@ async fn main() {
});

let mut client = OffersClient::new(channel);
let macaroon = read_macaroon_from_args(&args);
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(PayInvoiceRequest {
invoice: invoice_string.to_owned(),
amount,
Expand Down Expand Up @@ -307,9 +323,9 @@ fn read_macaroon_from_file(path: PathBuf) -> Result<String, std::io::Error> {
Ok(hex::encode(buffer))
}

fn read_cert_from_args(args: &Cli) -> ClientTlsConfig {
fn read_cert_from_args(cert_pem: Option<String>) -> ClientTlsConfig {
let data_dir = home::home_dir().unwrap().join(DEFAULT_DATA_DIR);
let pem = match &args.cert_pem {
let pem = match &cert_pem {
Some(pem) => pem.clone(),
None => {
// If no cert pem string is provided, we'll look for the tls certificate in the
Expand All @@ -326,24 +342,28 @@ fn read_cert_from_args(args: &Cli) -> ClientTlsConfig {
.domain_name("localhost")
}

fn read_macaroon_from_args(args: &Cli) -> String {
fn read_macaroon_from_args(
macaroon_path: Option<PathBuf>,
macaroon_hex: Option<String>,
network: &str,
) -> String {
// Make sure both macaroon options are not set.
if args.macaroon_path.is_some() && args.macaroon_hex.is_some() {
if macaroon_path.is_some() && macaroon_hex.is_some() {
println!("ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set.");
exit(1)
}

// Let's grab the macaroon string now. If neither macaroon_path nor macaroon_hex are
// set, use the default macaroon path.
match &args.macaroon_path {
match macaroon_path {
Some(path) => read_macaroon_from_file(path.clone()).unwrap_or_else(|e| {
println!("ERROR reading macaroon from file {e:?}");
exit(1)
}),
None => match &args.macaroon_hex {
None => match &macaroon_hex {
Some(macaroon) => macaroon.clone(),
None => {
let path = get_macaroon_path_default(&args.network.clone());
let path = get_macaroon_path_default(network);
read_macaroon_from_file(path).unwrap_or_else(|e| {
println!("ERROR reading macaroon from file {e:?}");
exit(1)
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ pub struct PaymentInfo {
pub struct PayOfferParams {
pub offer: Offer,
pub amount: Option<u64>,
pub payer_note: Option<String>,
pub network: Network,
pub client: Client,
/// The destination the offer creator provided, which we will use to send the invoice request.
Expand Down Expand Up @@ -318,9 +319,9 @@ impl OfferHandler {
.create_invoice_request(
cfg.client.clone(),
cfg.offer.clone(),
vec![],
cfg.network,
cfg.amount,
cfg.payer_note,
)
.await?;

Expand Down
42 changes: 33 additions & 9 deletions src/lndk_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use async_trait::async_trait;
use bitcoin::hashes::sha256::Hash;
use bitcoin::network::constants::Network;
use bitcoin::secp256k1::schnorr::Signature;
use bitcoin::secp256k1::{PublicKey, Secp256k1};
use bitcoin::secp256k1::{PublicKey, Secp256k1, SignOnly};
use futures::executor::block_on;
use lightning::blinded_path::{BlindedPath, Direction, IntroductionNode};
use lightning::ln::channelmanager::PaymentId;
use lightning::offers::invoice_request::{
InvoiceRequest, SignInvoiceRequestFn, UnsignedInvoiceRequest,
ExplicitPayerId, InvoiceRequest, InvoiceRequestBuilder, SignInvoiceRequestFn,
UnsignedInvoiceRequest,
};
use lightning::offers::merkle::SignError;
use lightning::offers::offer::{Amount, Offer};
Expand Down Expand Up @@ -176,9 +177,9 @@ impl OfferHandler {
&self,
mut signer: impl MessageSigner + std::marker::Send + 'static,
offer: Offer,
_metadata: Vec<u8>,
network: Network,
msats: Option<u64>,
payer_note: Option<String>,
) -> Result<(InvoiceRequest, PaymentId, u64), OfferError> {
let validated_amount = validate_amount(offer.amount(), msats).await?;

Expand All @@ -203,7 +204,7 @@ impl OfferHandler {
// invoice once returned from the offer maker. Once we get an invoice back, this metadata
// will help us to determine: 1) That the invoice is truly for the invoice request we sent.
// 2) We don't pay duplicate invoices.
let unsigned_invoice_req = offer
let builder: InvoiceRequestBuilder<'_, '_, ExplicitPayerId, SignOnly> = offer
.request_invoice_deriving_metadata(
pubkey,
&self.expanded_key,
Expand All @@ -214,10 +215,15 @@ impl OfferHandler {
.chain(network)
.map_err(OfferError::BuildUIRFailure)?
.amount_msats(validated_amount)
.map_err(OfferError::BuildUIRFailure)?
.build()
.map_err(OfferError::BuildUIRFailure)?;

let builder = match payer_note {
Some(payer_note_str) => builder.payer_note(payer_note_str),
None => builder,
};

let unsigned_invoice_req = builder.build().map_err(OfferError::BuildUIRFailure)?;

// To create a valid invoice request, we also need to sign it. This is spawned in a blocking
// task because we need to call block_on on sign_message so that sign_closure can be a
// synchronous closure.
Expand Down Expand Up @@ -799,7 +805,13 @@ mod tests {
let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
let resp = handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, Some(amount))
.create_invoice_request(
signer_mock,
offer,
Network::Regtest,
Some(amount),
Some("".to_string()),
)
.await;
assert!(resp.is_ok())
}
Expand All @@ -819,7 +831,13 @@ mod tests {
let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, Some(10000))
.create_invoice_request(
signer_mock,
offer,
Network::Regtest,
Some(10000),
Some("".to_string())
)
.await
.is_err())
}
Expand All @@ -842,7 +860,13 @@ mod tests {
let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, Some(10000))
.create_invoice_request(
signer_mock,
offer,
Network::Regtest,
Some(10000),
Some("".to_string())
)
.await
.is_err())
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ async fn main() -> Result<(), ()> {

// The user passed in a TLS cert to help us establish a secure connection to LND. But now we
// need to generate a TLS credentials for connecting securely to the LNDK server.
generate_tls_creds(data_dir.clone()).map_err(|e| {
generate_tls_creds(data_dir.clone(), config.tls_ip).map_err(|e| {
error!("Error generating tls credentials: {e}");
})?;
let identity = read_tls(data_dir).map_err(|e| {
Expand Down
Loading

0 comments on commit 25e3e2d

Please sign in to comment.