Skip to content

Commit 868fee7

Browse files
committed
Add Bolt12Invoice::verify_using_payer_data
Invoices are authenticated by checking the payer metadata in the corresponding invoice request or refund. For all invoices requests and for refunds using blinded paths, this will be the encrypted payment id and a 128-bit nonce. Allows checking the unencrypted payment id and nonce explicitly instead of the payer metadata. This will be used by an upcoming change that includes the payment id and nonce in the invoice request's reply path and the refund's blinded paths instead of completely in the payer metadata, which mitigates de-anonymization attacks.
1 parent 114954c commit 868fee7

File tree

4 files changed

+63
-31
lines changed

4 files changed

+63
-31
lines changed

lightning/src/offers/invoice.rs

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,12 @@ use crate::ln::msgs::DecodeError;
119119
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
120120
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
121121
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self};
122+
use crate::offers::nonce::Nonce;
122123
use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
123124
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
124125
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef};
125126
use crate::offers::refund::{IV_BYTES as REFUND_IV_BYTES, Refund, RefundContents};
126-
use crate::offers::signer;
127+
use crate::offers::signer::{Metadata, self};
127128
use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, Readable, SeekReadable, WithoutLength, Writeable, Writer};
128129
use crate::util::string::PrintableString;
129130

@@ -770,12 +771,31 @@ impl Bolt12Invoice {
770771
self.tagged_hash.as_digest().as_ref().clone()
771772
}
772773

773-
/// Verifies that the invoice was for a request or refund created using the given key. Returns
774-
/// the associated [`PaymentId`] to use when sending the payment.
774+
/// Verifies that the invoice was for a request or refund created using the given key by
775+
/// checking the payer metadata from the invoice request.
776+
///
777+
/// Returns the associated [`PaymentId`] to use when sending the payment.
775778
pub fn verify<T: secp256k1::Signing>(
776779
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
777780
) -> Result<PaymentId, ()> {
778-
self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
781+
let metadata = match &self.contents {
782+
InvoiceContents::ForOffer { invoice_request, .. } => &invoice_request.inner.payer.0,
783+
InvoiceContents::ForRefund { refund, .. } => &refund.payer.0,
784+
};
785+
self.contents.verify(TlvStream::new(&self.bytes), metadata, key, secp_ctx)
786+
}
787+
788+
/// Verifies that the invoice was for a request or refund created using the given key by
789+
/// checking a payment id and nonce included with the [`BlindedPath`] for which the invoice was
790+
/// sent through.
791+
pub fn verify_using_payer_data<T: secp256k1::Signing>(
792+
&self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
793+
) -> bool {
794+
let metadata = Metadata::payer_data(payment_id, nonce, key);
795+
match self.contents.verify(TlvStream::new(&self.bytes), &metadata, key, secp_ctx) {
796+
Ok(extracted_payment_id) => payment_id == extracted_payment_id,
797+
Err(()) => false,
798+
}
779799
}
780800

781801
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
@@ -1006,35 +1026,28 @@ impl InvoiceContents {
10061026
}
10071027

10081028
fn verify<T: secp256k1::Signing>(
1009-
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
1029+
&self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey,
1030+
secp_ctx: &Secp256k1<T>
10101031
) -> Result<PaymentId, ()> {
10111032
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
10121033
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
10131034
match record.r#type {
10141035
PAYER_METADATA_TYPE => false, // Should be outside range
1015-
INVOICE_REQUEST_PAYER_ID_TYPE => !self.derives_keys(),
1036+
INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(),
10161037
_ => true,
10171038
}
10181039
});
10191040
let tlv_stream = offer_records.chain(invreq_records);
10201041

1021-
let (metadata, payer_id, iv_bytes) = match self {
1022-
InvoiceContents::ForOffer { invoice_request, .. } => {
1023-
(invoice_request.metadata(), invoice_request.payer_id(), INVOICE_REQUEST_IV_BYTES)
1024-
},
1025-
InvoiceContents::ForRefund { refund, .. } => {
1026-
(refund.metadata(), refund.payer_id(), REFUND_IV_BYTES)
1027-
},
1042+
let payer_id = self.payer_id();
1043+
let iv_bytes = match self {
1044+
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
1045+
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES,
10281046
};
10291047

1030-
signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx)
1031-
}
1032-
1033-
fn derives_keys(&self) -> bool {
1034-
match self {
1035-
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.derives_keys(),
1036-
InvoiceContents::ForRefund { refund, .. } => refund.derives_keys(),
1037-
}
1048+
signer::verify_payer_metadata(
1049+
metadata.as_ref(), key, iv_bytes, payer_id, tlv_stream, secp_ctx,
1050+
)
10381051
}
10391052

10401053
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {

lightning/src/offers/invoice_request.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ pub(super) struct InvoiceRequestContents {
636636
#[derive(Clone, Debug)]
637637
#[cfg_attr(test, derive(PartialEq))]
638638
pub(super) struct InvoiceRequestContentsWithoutPayerId {
639-
payer: PayerContents,
639+
pub(super) payer: PayerContents,
640640
pub(super) offer: OfferContents,
641641
chain: Option<ChainHash>,
642642
amount_msats: Option<u64>,
@@ -953,10 +953,6 @@ impl InvoiceRequestContents {
953953
self.inner.metadata()
954954
}
955955

956-
pub(super) fn derives_keys(&self) -> bool {
957-
self.inner.payer.0.derives_payer_keys()
958-
}
959-
960956
pub(super) fn chain(&self) -> ChainHash {
961957
self.inner.chain()
962958
}
@@ -1421,6 +1417,7 @@ mod tests {
14211417
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
14221418
Err(()) => panic!("verification failed"),
14231419
}
1420+
assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx));
14241421

14251422
// Fails verification with altered fields
14261423
let (
@@ -1494,6 +1491,7 @@ mod tests {
14941491
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
14951492
Err(()) => panic!("verification failed"),
14961493
}
1494+
assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx));
14971495

14981496
// Fails verification with altered fields
14991497
let (

lightning/src/offers/refund.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ pub struct Refund {
415415
#[derive(Clone, Debug)]
416416
#[cfg_attr(test, derive(PartialEq))]
417417
pub(super) struct RefundContents {
418-
payer: PayerContents,
418+
pub(super) payer: PayerContents,
419419
// offer fields
420420
description: String,
421421
absolute_expiry: Option<Duration>,
@@ -727,10 +727,6 @@ impl RefundContents {
727727
self.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
728728
}
729729

730-
pub(super) fn derives_keys(&self) -> bool {
731-
self.payer.0.derives_payer_keys()
732-
}
733-
734730
pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
735731
let payer = PayerTlvStreamRef {
736732
metadata: self.payer.0.as_bytes(),
@@ -1049,6 +1045,7 @@ mod tests {
10491045
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
10501046
Err(()) => panic!("verification failed"),
10511047
}
1048+
assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx));
10521049

10531050
let mut tlv_stream = refund.as_tlv_stream();
10541051
tlv_stream.2.amount = Some(2000);
@@ -1113,6 +1110,7 @@ mod tests {
11131110
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
11141111
Err(()) => panic!("verification failed"),
11151112
}
1113+
assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx));
11161114

11171115
// Fails verification with altered fields
11181116
let mut tlv_stream = refund.as_tlv_stream();

lightning/src/offers/signer.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ pub(super) enum Metadata {
5050
/// This variant should only be used at verification time, never when building.
5151
RecipientData(Nonce),
5252

53+
/// Metadata for deriving keys included as payer data in a blinded path.
54+
///
55+
/// This variant should only be used at verification time, never when building.
56+
PayerData([u8; PaymentId::LENGTH + Nonce::LENGTH]),
57+
5358
/// Metadata to be derived from message contents and given material.
5459
///
5560
/// This variant should only be used at building time.
@@ -62,6 +67,16 @@ pub(super) enum Metadata {
6267
}
6368

6469
impl Metadata {
70+
pub fn payer_data(payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey) -> Self {
71+
let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce);
72+
73+
let mut bytes = [0u8; PaymentId::LENGTH + Nonce::LENGTH];
74+
bytes[..PaymentId::LENGTH].copy_from_slice(encrypted_payment_id.as_slice());
75+
bytes[PaymentId::LENGTH..].copy_from_slice(nonce.as_slice());
76+
77+
Metadata::PayerData(bytes)
78+
}
79+
6580
pub fn as_bytes(&self) -> Option<&Vec<u8>> {
6681
match self {
6782
Metadata::Bytes(bytes) => Some(bytes),
@@ -73,6 +88,7 @@ impl Metadata {
7388
match self {
7489
Metadata::Bytes(_) => false,
7590
Metadata::RecipientData(_) => { debug_assert!(false); false },
91+
Metadata::PayerData(_) => { debug_assert!(false); false },
7692
Metadata::Derived(_) => true,
7793
Metadata::DerivedSigningPubkey(_) => true,
7894
}
@@ -87,6 +103,7 @@ impl Metadata {
87103
// Nonce::LENGTH had been set explicitly.
88104
Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH,
89105
Metadata::RecipientData(_) => false,
106+
Metadata::PayerData(_) => true,
90107
Metadata::Derived(_) => false,
91108
Metadata::DerivedSigningPubkey(_) => true,
92109
}
@@ -101,6 +118,7 @@ impl Metadata {
101118
// been set explicitly.
102119
Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH,
103120
Metadata::RecipientData(_) => true,
121+
Metadata::PayerData(_) => false,
104122
Metadata::Derived(_) => false,
105123
Metadata::DerivedSigningPubkey(_) => true,
106124
}
@@ -115,6 +133,7 @@ impl Metadata {
115133
match self {
116134
Metadata::Bytes(_) => self,
117135
Metadata::RecipientData(_) => { debug_assert!(false); self },
136+
Metadata::PayerData(_) => { debug_assert!(false); self },
118137
Metadata::Derived(_) => self,
119138
Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material),
120139
}
@@ -126,6 +145,7 @@ impl Metadata {
126145
match self {
127146
Metadata::Bytes(_) => (self, None),
128147
Metadata::RecipientData(_) => { debug_assert!(false); (self, None) },
148+
Metadata::PayerData(_) => { debug_assert!(false); (self, None) },
129149
Metadata::Derived(mut metadata_material) => {
130150
tlv_stream.write(&mut metadata_material.hmac).unwrap();
131151
(Metadata::Bytes(metadata_material.derive_metadata()), None)
@@ -151,6 +171,7 @@ impl AsRef<[u8]> for Metadata {
151171
match self {
152172
Metadata::Bytes(bytes) => &bytes,
153173
Metadata::RecipientData(nonce) => &nonce.0,
174+
Metadata::PayerData(bytes) => bytes.as_slice(),
154175
Metadata::Derived(_) => { debug_assert!(false); &[] },
155176
Metadata::DerivedSigningPubkey(_) => { debug_assert!(false); &[] },
156177
}
@@ -162,6 +183,7 @@ impl fmt::Debug for Metadata {
162183
match self {
163184
Metadata::Bytes(bytes) => bytes.fmt(f),
164185
Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f),
186+
Metadata::PayerData(bytes) => bytes.fmt(f),
165187
Metadata::Derived(_) => f.write_str("Derived"),
166188
Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"),
167189
}
@@ -178,6 +200,7 @@ impl PartialEq for Metadata {
178200
false
179201
},
180202
Metadata::RecipientData(_) => false,
203+
Metadata::PayerData(_) => false,
181204
Metadata::Derived(_) => false,
182205
Metadata::DerivedSigningPubkey(_) => false,
183206
}

0 commit comments

Comments
 (0)