Skip to content
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

odds and ends: tls SAN cfg and payer note option #144

Merged
merged 6 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually been a while since I wrote protos but I'm quite glad they brought back optional fields to v3 finally :)

}

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 @@

// 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| {

Check warning on line 88 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L88

Added line #L88 was not covered by tests
error!("Error generating tls credentials: {e}");
})?;
let identity = read_tls(data_dir).map_err(|e| {
Expand Down
Loading
Loading