Verify inbound ReleaseHeldHtlc messages via hmac.

See AsyncPaymentsContext::hmac, but this prevents the recipient from
deanonymizing us. Without this, if they are able to guess the correct payment
id, then they could create a blinded path to us and confirm our identity.

We also move the PAYMENT_HASH_HMAC_INPUT const to use &[7; 16], which is safe
because this const was added since the last release. This ordering reads more
smoothly.
This commit is contained in:
Valentine Wallace 2024-09-05 17:32:08 -04:00
parent 5a7f52313b
commit 615eefb543
No known key found for this signature in database
GPG key ID: FD3E106A2CE099B4
3 changed files with 71 additions and 12 deletions

View file

@ -383,7 +383,18 @@ pub enum AsyncPaymentsContext {
/// which of our pending outbound payments should be released to its often-offline payee.
///
/// [`Offer`]: crate::offers::offer::Offer
payment_id: PaymentId
payment_id: PaymentId,
/// A nonce used for authenticating that a [`ReleaseHeldHtlc`] message is valid for a preceding
/// [`HeldHtlcAvailable`] message.
///
/// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc
/// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable
nonce: Nonce,
/// Authentication code for the [`PaymentId`].
///
/// Prevents the recipient from being able to deanonymize us by creating a blinded path to us
/// containing the expected [`PaymentId`].
hmac: Hmac<Sha256>,
},
}
@ -412,6 +423,8 @@ impl_writeable_tlv_based_enum!(OffersContext,
impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
(0, OutboundPayment) => {
(0, payment_id, required),
(2, nonce, required),
(4, hmac, required),
},
);

View file

@ -453,6 +453,24 @@ pub struct PaymentId(pub [u8; Self::LENGTH]);
impl PaymentId {
/// Number of bytes in the id.
pub const LENGTH: usize = 32;
/// Constructs an HMAC to include in [`AsyncPaymentsContext::OutboundPayment`] for the payment id
/// along with the given [`Nonce`].
#[cfg(async_payments)]
pub fn hmac_for_async_payment(
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Hmac<Sha256> {
signer::hmac_for_async_payment_id(*self, nonce, expanded_key)
}
/// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an
/// [`AsyncPaymentsContext::OutboundPayment`].
#[cfg(async_payments)]
pub fn verify_for_async_payment(
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Result<(), ()> {
signer::verify_async_payment_id(*self, hmac, nonce, expanded_key)
}
}
impl Verification for PaymentId {
@ -4353,8 +4371,12 @@ where
}
};
let nonce = Nonce::from_entropy_source(&*self.entropy_source);
let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key);
let reply_paths = match self.create_blinded_paths(
MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id })
MessageContext::AsyncPayments(
AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac }
)
) {
Ok(paths) => paths,
Err(()) => {
@ -11209,7 +11231,8 @@ where
fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {
#[cfg(async_payments)] {
let AsyncPaymentsContext::OutboundPayment { payment_id } = _context;
let AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } = _context;
if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return }
if let Err(e) = self.send_payment_for_static_invoice(payment_id, _message.payment_release_secret) {
log_trace!(
self.logger, "Failed to release held HTLC with payment id {} and release secret {:02x?}: {:?}",

View file

@ -39,9 +39,12 @@ const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16];
// HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`.
const OFFER_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16];
// HMAC input for a `PaymentId`. The HMAC is used in `AsyncPaymentsContext::OutboundPayment`.
#[cfg(async_payments)]
const ASYNC_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[6; 16];
// HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`.
const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[6; 16];
const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16];
/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
/// verified.
@ -402,14 +405,7 @@ fn hmac_for_message<'a>(
pub(crate) fn hmac_for_offer_payment_id(
payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey,
) -> Hmac<Sha256> {
const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment ID ~";
let mut hmac = expanded_key.hmac_for_offer();
hmac.input(IV_BYTES);
hmac.input(&nonce.0);
hmac.input(OFFER_PAYMENT_ID_HMAC_INPUT);
hmac.input(&payment_id.0);
Hmac::from_engine(hmac)
hmac_for_payment_id(payment_id, nonce, OFFER_PAYMENT_ID_HMAC_INPUT, expanded_key)
}
pub(crate) fn verify_offer_payment_id(
@ -436,3 +432,30 @@ pub(crate) fn verify_payment_hash(
) -> Result<(), ()> {
if hmac_for_payment_hash(payment_hash, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
}
#[cfg(async_payments)]
pub(crate) fn hmac_for_async_payment_id(
payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey,
) -> Hmac<Sha256> {
hmac_for_payment_id(payment_id, nonce, ASYNC_PAYMENT_ID_HMAC_INPUT, expanded_key)
}
#[cfg(async_payments)]
pub(crate) fn verify_async_payment_id(
payment_id: PaymentId, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &ExpandedKey,
) -> Result<(), ()> {
if hmac_for_async_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
}
fn hmac_for_payment_id(
payment_id: PaymentId, nonce: Nonce, hmac_input: &[u8; 16], expanded_key: &ExpandedKey,
) -> Hmac<Sha256> {
const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment ID ~";
let mut hmac = expanded_key.hmac_for_offer();
hmac.input(IV_BYTES);
hmac.input(&nonce.0);
hmac.input(hmac_input);
hmac.input(&payment_id.0);
Hmac::from_engine(hmac)
}