use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::{Message, PublicKey};
+use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
use bitcoin::secp256k1::schnorr::Signature;
use core::convert::TryFrom;
use crate::io;
use crate::ln::PaymentHash;
use crate::ln::features::InvoiceRequestFeatures;
+use crate::ln::inbound_payment::ExpandedKey;
use crate::ln::msgs::DecodeError;
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
-use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
+use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
}
+ /// Verifies that the request was for an offer created using the given key.
+ pub fn verify<T: secp256k1::Signing>(
+ &self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+ ) -> bool {
+ self.contents.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx)
+ }
+
#[cfg(test)]
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
use crate::ln::msgs::MAX_VALUE_MSAT;
use crate::offers::invoice_request::InvoiceRequestBuilder;
+use crate::offers::merkle::TlvStream;
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
-use crate::offers::signer::{Metadata, MetadataMaterial};
+use crate::offers::signer::{Metadata, MetadataMaterial, self};
use crate::onion_message::BlindedPath;
use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
/// recipient privacy by using a different signing pubkey for each offer. Otherwise, the
/// provided `node_id` is used for the signing pubkey.
///
- /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used to
- /// verify that an [`InvoiceRequest`] was produced for the offer given an [`ExpandedKey`].
+ /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used by
+ /// [`InvoiceRequest::verify`] to determine if the request was produced for the offer given an
+ /// [`ExpandedKey`].
///
- /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+ /// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
pub fn deriving_signing_pubkey<ES: Deref>(
description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
self.signing_pubkey
}
+ /// Verifies that the offer metadata was produced from the offer in the TLV stream.
+ pub(super) fn verify<T: secp256k1::Signing>(
+ &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+ ) -> bool {
+ match self.metadata() {
+ Some(metadata) => {
+ let tlv_stream = tlv_stream.range(OFFER_TYPES).filter(|record| {
+ match record.r#type {
+ OFFER_METADATA_TYPE => false,
+ OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(),
+ _ => true,
+ }
+ });
+ signer::verify_metadata(
+ metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
+ )
+ },
+ None => false,
+ }
+ }
+
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
let (currency, amount) = match &self.amount {
None => (None, None),
}
}
-tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
+/// Valid type range for offer TLV records.
+const OFFER_TYPES: core::ops::Range<u64> = 1..80;
+
+/// TLV record type for [`Offer::metadata`].
+const OFFER_METADATA_TYPE: u64 = 4;
+
+/// TLV record type for [`Offer::signing_pubkey`].
+const OFFER_NODE_ID_TYPE: u64 = 22;
+
+tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
(2, chains: (Vec<ChainHash>, WithoutLength)),
- (4, metadata: (Vec<u8>, WithoutLength)),
+ (OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
(6, currency: CurrencyCode),
(8, amount: (u64, HighZeroBytesDroppedBigSize)),
(10, description: (String, WithoutLength)),
(16, paths: (Vec<BlindedPath>, WithoutLength)),
(18, issuer: (String, WithoutLength)),
(20, quantity_max: (u64, HighZeroBytesDroppedBigSize)),
- (22, node_id: PublicKey),
+ (OFFER_NODE_ID_TYPE, node_id: PublicKey),
});
impl Bech32Encode for Offer {
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
+ use bitcoin::secp256k1::Secp256k1;
use core::convert::TryFrom;
use core::num::NonZeroU64;
use core::time::Duration;
+ use crate::chain::keysinterface::KeyMaterial;
use crate::ln::features::OfferFeatures;
+ use crate::ln::inbound_payment::ExpandedKey;
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
use crate::offers::parse::{ParseError, SemanticError};
use crate::offers::test_utils::*;
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
}
+ #[test]
+ fn builds_offer_with_metadata_derived() {
+ let desc = "foo".to_string();
+ let node_id = recipient_pubkey();
+ let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+ let entropy = FixedEntropy {};
+ let secp_ctx = Secp256k1::new();
+
+ let offer = OfferBuilder
+ ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+ .amount_msats(1000)
+ .build().unwrap();
+ assert_eq!(offer.signing_pubkey(), node_id);
+
+ let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ assert!(invoice_request.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered offer field
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.amount = Some(100);
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ let invoice_request = Offer::try_from(encoded_offer).unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered metadata
+ let mut tlv_stream = offer.as_tlv_stream();
+ let metadata = tlv_stream.metadata.unwrap().iter().copied().rev().collect();
+ tlv_stream.metadata = Some(&metadata);
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ let invoice_request = Offer::try_from(encoded_offer).unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
+ }
+
+ #[test]
+ fn builds_offer_with_derived_signing_pubkey() {
+ let desc = "foo".to_string();
+ let node_id = recipient_pubkey();
+ let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+ let entropy = FixedEntropy {};
+ let secp_ctx = Secp256k1::new();
+
+ let blinded_path = BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
+ BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
+ ],
+ };
+
+ let offer = OfferBuilder
+ ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+ .amount_msats(1000)
+ .path(blinded_path)
+ .build().unwrap();
+ assert_ne!(offer.signing_pubkey(), node_id);
+
+ let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ assert!(invoice_request.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered offer field
+ let mut tlv_stream = offer.as_tlv_stream();
+ tlv_stream.amount = Some(100);
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ let invoice_request = Offer::try_from(encoded_offer).unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered signing pubkey
+ let mut tlv_stream = offer.as_tlv_stream();
+ let signing_pubkey = pubkey(1);
+ tlv_stream.node_id = Some(&signing_pubkey);
+
+ let mut encoded_offer = Vec::new();
+ tlv_stream.write(&mut encoded_offer).unwrap();
+
+ let invoice_request = Offer::try_from(encoded_offer).unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
+ }
+
#[test]
fn builds_offer_with_amount() {
let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 };
//! Utilities for signing offer messages and verifying metadata.
use bitcoin::hashes::{Hash, HashEngine};
+use bitcoin::hashes::cmp::fixed_time_eq;
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
use bitcoin::hashes::sha256::Hash as Sha256;
-use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey, self};
-use core::convert::TryInto;
+use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self};
+use core::convert::TryFrom;
use core::fmt;
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
+use crate::offers::merkle::TlvRecord;
use crate::util::ser::Writeable;
use crate::prelude::*;
pub fn derives_keys(&self) -> bool {
match self {
- Metadata::Bytes(_) => false,
+ // Infer whether Metadata::derived_from was called on Metadata::DerivedSigningPubkey to
+ // produce Metadata::Bytes. This is merely to determine which fields should be included
+ // when verifying a message. It doesn't necessarily indicate that keys were in fact
+ // derived, as wouldn't be the case if a Metadata::Bytes with length Nonce::LENGTH had
+ // been set explicitly.
+ Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH,
Metadata::Derived(_) => false,
Metadata::DerivedSigningPubkey(_) => true,
}
(self.nonce.as_slice().to_vec(), keys)
}
}
+
+/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
+/// - a 128-bit [`Nonce`] and possibly
+/// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`].
+///
+/// If the latter is not included in the metadata, the TLV stream is used to check if the given
+/// `signing_pubkey` can be derived from it.
+pub(super) fn verify_metadata<'a, T: secp256k1::Signing>(
+ metadata: &Vec<u8>, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
+ signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
+ secp_ctx: &Secp256k1<T>
+) -> bool {
+ if metadata.len() < Nonce::LENGTH {
+ return false;
+ }
+
+ let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) {
+ Ok(nonce) => nonce,
+ Err(_) => return false,
+ };
+ let mut hmac = expanded_key.hmac_for_offer(nonce, iv_bytes);
+
+ for record in tlv_stream {
+ hmac.input(record.record_bytes);
+ }
+
+ if metadata.len() == Nonce::LENGTH {
+ hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT);
+ let hmac = Hmac::from_engine(hmac);
+ let derived_pubkey = SecretKey::from_slice(hmac.as_inner()).unwrap().public_key(&secp_ctx);
+ fixed_time_eq(&signing_pubkey.serialize(), &derived_pubkey.serialize())
+ } else if metadata[Nonce::LENGTH..].len() == Sha256::LEN {
+ hmac.input(DERIVED_METADATA_HMAC_INPUT);
+ fixed_time_eq(&metadata[Nonce::LENGTH..], &Hmac::from_engine(hmac).into_inner())
+ } else {
+ false
+ }
+}