Skip to content

Commit 4dbd6fc

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 5868a68 commit 4dbd6fc

File tree

4 files changed

+60
-31
lines changed

4 files changed

+60
-31
lines changed

lightning/src/offers/invoice.rs

+31-21
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,28 @@ 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+
self.contents.verify(TlvStream::new(&self.bytes), &metadata, key, secp_ctx).is_ok()
779796
}
780797

781798
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
@@ -1006,35 +1023,28 @@ impl InvoiceContents {
10061023
}
10071024

10081025
fn verify<T: secp256k1::Signing>(
1009-
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
1026+
&self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey,
1027+
secp_ctx: &Secp256k1<T>
10101028
) -> Result<PaymentId, ()> {
10111029
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
10121030
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
10131031
match record.r#type {
10141032
PAYER_METADATA_TYPE => false, // Should be outside range
1015-
INVOICE_REQUEST_PAYER_ID_TYPE => !self.derives_keys(),
1033+
INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(),
10161034
_ => true,
10171035
}
10181036
});
10191037
let tlv_stream = offer_records.chain(invreq_records);
10201038

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-
},
1039+
let payer_id = self.payer_id();
1040+
let iv_bytes = match self {
1041+
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
1042+
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES,
10281043
};
10291044

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-
}
1045+
signer::verify_payer_metadata(
1046+
metadata.as_ref(), key, iv_bytes, payer_id, tlv_stream, secp_ctx,
1047+
)
10381048
}
10391049

10401050
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {

lightning/src/offers/invoice_request.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ pub(super) struct InvoiceRequestContents {
635635
#[derive(Clone, Debug)]
636636
#[cfg_attr(test, derive(PartialEq))]
637637
pub(super) struct InvoiceRequestContentsWithoutPayerId {
638-
payer: PayerContents,
638+
pub(super) payer: PayerContents,
639639
pub(super) offer: OfferContents,
640640
chain: Option<ChainHash>,
641641
amount_msats: Option<u64>,
@@ -949,10 +949,6 @@ impl InvoiceRequestContents {
949949
self.inner.metadata()
950950
}
951951

952-
pub(super) fn derives_keys(&self) -> bool {
953-
self.inner.payer.0.derives_payer_keys()
954-
}
955-
956952
pub(super) fn chain(&self) -> ChainHash {
957953
self.inner.chain()
958954
}
@@ -1417,6 +1413,7 @@ mod tests {
14171413
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
14181414
Err(()) => panic!("verification failed"),
14191415
}
1416+
assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx));
14201417

14211418
// Fails verification with altered fields
14221419
let (
@@ -1490,6 +1487,7 @@ mod tests {
14901487
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
14911488
Err(()) => panic!("verification failed"),
14921489
}
1490+
assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx));
14931491

14941492
// Fails verification with altered fields
14951493
let (

lightning/src/offers/refund.rs

+3-5
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

+23
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
}
@@ -110,6 +128,7 @@ impl Metadata {
110128
match self {
111129
Metadata::Bytes(_) => self,
112130
Metadata::RecipientData(_) => { debug_assert!(false); self },
131+
Metadata::PayerData(_) => { debug_assert!(false); self },
113132
Metadata::Derived(_) => self,
114133
Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material),
115134
}
@@ -121,6 +140,7 @@ impl Metadata {
121140
match self {
122141
Metadata::Bytes(_) => (self, None),
123142
Metadata::RecipientData(_) => { debug_assert!(false); (self, None) },
143+
Metadata::PayerData(_) => { debug_assert!(false); (self, None) },
124144
Metadata::Derived(mut metadata_material) => {
125145
tlv_stream.write(&mut metadata_material.hmac).unwrap();
126146
(Metadata::Bytes(metadata_material.derive_metadata()), None)
@@ -146,6 +166,7 @@ impl AsRef<[u8]> for Metadata {
146166
match self {
147167
Metadata::Bytes(bytes) => &bytes,
148168
Metadata::RecipientData(nonce) => &nonce.0,
169+
Metadata::PayerData(bytes) => bytes.as_slice(),
149170
Metadata::Derived(_) => { debug_assert!(false); &[] },
150171
Metadata::DerivedSigningPubkey(_) => { debug_assert!(false); &[] },
151172
}
@@ -157,6 +178,7 @@ impl fmt::Debug for Metadata {
157178
match self {
158179
Metadata::Bytes(bytes) => bytes.fmt(f),
159180
Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f),
181+
Metadata::PayerData(bytes) => bytes.fmt(f),
160182
Metadata::Derived(_) => f.write_str("Derived"),
161183
Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"),
162184
}
@@ -173,6 +195,7 @@ impl PartialEq for Metadata {
173195
false
174196
},
175197
Metadata::RecipientData(_) => false,
198+
Metadata::PayerData(_) => false,
176199
Metadata::Derived(_) => false,
177200
Metadata::DerivedSigningPubkey(_) => false,
178201
}

0 commit comments

Comments
 (0)