From: Jeffrey Czyz Date: Tue, 23 Aug 2022 22:31:46 +0000 (-0500) Subject: Invoice request raw byte encoding and decoding X-Git-Tag: v0.0.113~8^2~4 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=59a7bd29fe02f10f6fcdc192c7994629675c7a30;p=rust-lightning Invoice request raw byte encoding and decoding When reading an offer, an `invoice_request` message is sent over the wire. Implement Writeable for encoding the message and TryFrom for decoding it by defining in terms of TLV streams. These streams represent content for the payer metadata (0), reflected `offer` (1-79), `invoice_request` (80-159), and signature (240). --- diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 43ae18d2c..6cedf3eda 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -12,9 +12,15 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::secp256k1::PublicKey; use bitcoin::secp256k1::schnorr::Signature; +use core::convert::TryFrom; +use crate::io; use crate::ln::features::InvoiceRequestFeatures; -use crate::offers::offer::OfferContents; -use crate::offers::payer::PayerContents; +use crate::ln::msgs::DecodeError; +use crate::offers::merkle::{SignatureTlvStream, self}; +use crate::offers::offer::{Amount, OfferContents, OfferTlvStream}; +use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::{PayerContents, PayerTlvStream}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; use crate::prelude::*; @@ -34,7 +40,7 @@ pub struct InvoiceRequest { /// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. #[derive(Clone, Debug)] -pub(crate) struct InvoiceRequestContents { +pub(super) struct InvoiceRequestContents { payer: PayerContents, offer: OfferContents, chain: Option, @@ -75,9 +81,9 @@ impl InvoiceRequest { &self.contents.features } - /// The quantity of the offer's item conforming to [`Offer::supported_quantity`]. + /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. /// - /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity + /// [`Offer::is_valid_quantity`]: crate::offers::offer::Offer::is_valid_quantity pub fn quantity(&self) -> Option { self.contents.quantity } @@ -99,3 +105,114 @@ impl InvoiceRequest { self.signature } } + +impl Writeable for InvoiceRequest { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { + (80, chain: ChainHash), + (82, amount: (u64, HighZeroBytesDroppedBigSize)), + (84, features: InvoiceRequestFeatures), + (86, quantity: (u64, HighZeroBytesDroppedBigSize)), + (88, payer_id: PublicKey), + (89, payer_note: (String, WithoutLength)), +}); + +type FullInvoiceRequestTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceRequestTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, signature)) + } +} + +type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); + +impl TryFrom> for InvoiceRequest { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let invoice_request = ParsedMessage::::try_from(bytes)?; + let ParsedMessage { bytes, tlv_stream } = invoice_request; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + + if let Some(signature) = &signature { + let tag = concat!("lightning", "invoice_request", "signature"); + merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?; + } + + Ok(InvoiceRequest { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceRequestContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceRequestTlvStream) -> Result { + let ( + PayerTlvStream { metadata }, + offer_tlv_stream, + InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note }, + ) = tlv_stream; + + let payer = match metadata { + None => return Err(SemanticError::MissingPayerMetadata), + Some(metadata) => PayerContents(metadata), + }; + let offer = OfferContents::try_from(offer_tlv_stream)?; + + if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) { + return Err(SemanticError::UnsupportedChain); + } + + let amount_msats = match (offer.amount(), amount) { + (None, None) => return Err(SemanticError::MissingAmount), + (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency), + (_, amount_msats) => amount_msats, + }; + + let features = features.unwrap_or_else(InvoiceRequestFeatures::empty); + + let expects_quantity = offer.expects_quantity(); + let quantity = match quantity { + None if expects_quantity => return Err(SemanticError::MissingQuantity), + Some(_) if !expects_quantity => return Err(SemanticError::UnexpectedQuantity), + Some(quantity) if !offer.is_valid_quantity(quantity) => { + return Err(SemanticError::InvalidQuantity); + } + quantity => quantity, + }; + + { + let amount_msats = amount_msats.unwrap_or(offer.amount_msats()); + let quantity = quantity.unwrap_or(1); + if amount_msats < offer.expected_invoice_amount_msats(quantity) { + return Err(SemanticError::InsufficientAmount); + } + } + + let payer_id = match payer_id { + None => return Err(SemanticError::MissingPayerId), + Some(payer_id) => payer_id, + }; + + Ok(InvoiceRequestContents { + payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note, + }) + } +} diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index c7aafb0cf..95183bea2 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -10,6 +10,8 @@ //! Tagged hashes for use in signature calculation and verification. use bitcoin::hashes::{Hash, HashEngine, sha256}; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::schnorr::Signature; use crate::io; use crate::util::ser::{BigSize, Readable}; @@ -18,6 +20,25 @@ use crate::prelude::*; /// Valid type range for signature TLV records. const SIGNATURE_TYPES: core::ops::RangeInclusive = 240..=1000; +tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { + (240, signature: Signature), +}); + +/// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message +/// digest. +/// +/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. +pub(super) fn verify_signature( + signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result<(), secp256k1::Error> { + let tag = sha256::Hash::hash(tag.as_bytes()); + let merkle_root = root_hash(bytes); + let digest = Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap(); + let pubkey = pubkey.into(); + let secp_ctx = Secp256k1::verification_only(); + secp_ctx.verify_schnorr(signature, &digest, &pubkey) +} + /// Computes a merkle root hash for the given data, which must be a well-formed TLV stream /// containing at least one TLV record. fn root_hash(data: &[u8]) -> sha256::Hash { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 1ac8b0bde..1403fbd22 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -270,6 +270,11 @@ impl Offer { self.contents.chains() } + /// Returns whether the given chain is supported by the offer. + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.contents.supports_chain(chain) + } + // TODO: Link to corresponding method in `InvoiceRequest`. /// Opaque bytes set by the originator. Useful for authentication and validating fields since it /// is reflected in `invoice_request` messages along with all the other fields from the `offer`. @@ -279,7 +284,7 @@ impl Offer { /// The minimum amount required for a successful payment of a single item. pub fn amount(&self) -> Option<&Amount> { - self.contents.amount.as_ref() + self.contents.amount() } /// A complete description of the purpose of the payment. Intended to be displayed to the user @@ -329,6 +334,18 @@ impl Offer { self.contents.supported_quantity() } + /// Returns whether the given quantity is valid for the offer. + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + self.contents.is_valid_quantity(quantity) + } + + /// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub fn expects_quantity(&self) -> bool { + self.contents.expects_quantity() + } + /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { self.contents.signing_pubkey.unwrap() @@ -355,10 +372,48 @@ impl OfferContents { ChainHash::using_genesis_block(Network::Bitcoin) } + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.chains().contains(&chain) + } + + pub fn amount(&self) -> Option<&Amount> { + self.amount.as_ref() + } + + pub fn amount_msats(&self) -> u64 { + match self.amount() { + None => 0, + Some(&Amount::Bitcoin { amount_msats }) => amount_msats, + Some(&Amount::Currency { .. }) => unreachable!(), + } + } + + pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 { + self.amount_msats() * quantity + } + pub fn supported_quantity(&self) -> Quantity { self.supported_quantity } + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + match self.supported_quantity { + Quantity::Bounded(n) => { + let n = n.get(); + if n == 1 { false } + else { quantity > 0 && quantity <= n } + }, + Quantity::Unbounded => quantity > 0, + } + } + + pub fn expects_quantity(&self) -> bool { + match self.supported_quantity { + Quantity::Bounded(n) => n.get() != 1, + Quantity::Unbounded => true, + } + } + fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), @@ -572,6 +627,7 @@ mod tests { assert_eq!(offer.bytes, buffer.as_slice()); assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); + assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin))); assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); assert_eq!(offer.description(), PrintableString("foo")); @@ -610,6 +666,7 @@ mod tests { .chain(Network::Bitcoin) .build() .unwrap(); + assert!(offer.supports_chain(mainnet)); assert_eq!(offer.chains(), vec![mainnet]); assert_eq!(offer.as_tlv_stream().chains, None); @@ -617,6 +674,7 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); @@ -625,6 +683,7 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); @@ -633,6 +692,8 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(mainnet)); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![mainnet, testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet])); } diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 013970981..b9815b811 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -11,6 +11,7 @@ use bitcoin::bech32; use bitcoin::bech32::{FromBase32, ToBase32}; +use bitcoin::secp256k1; use core::convert::TryFrom; use core::fmt; use crate::io; @@ -115,23 +116,37 @@ pub enum ParseError { Decode(DecodeError), /// The parsed message has invalid semantics. InvalidSemantics(SemanticError), + /// The parsed message has an invalid signature. + InvalidSignature(secp256k1::Error), } /// Error when interpreting a TLV stream as a specific type. #[derive(Debug, PartialEq)] pub enum SemanticError { + /// The provided chain hash does not correspond to a supported chain. + UnsupportedChain, /// An amount was expected but was missing. MissingAmount, /// The amount exceeded the total bitcoin supply. InvalidAmount, + /// An amount was provided but was not sufficient in value. + InsufficientAmount, /// A currency was provided that is not supported. UnsupportedCurrency, /// A required description was not provided. MissingDescription, /// A signing pubkey was not provided. MissingSigningPubkey, + /// A quantity was expected but was missing. + MissingQuantity, /// An unsupported quantity was provided. InvalidQuantity, + /// A quantity or quantity bounds was provided but was not expected. + UnexpectedQuantity, + /// Payer metadata was expected but was missing. + MissingPayerMetadata, + /// A payer id was expected but was missing. + MissingPayerId, } impl From for ParseError { @@ -151,3 +166,9 @@ impl From for ParseError { Self::InvalidSemantics(error) } } + +impl From for ParseError { + fn from(error: secp256k1::Error) -> Self { + Self::InvalidSignature(error) + } +} diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index 1705be85e..e389a8f6d 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -9,6 +9,8 @@ //! Data structures and encoding for `invoice_request_metadata` records. +use crate::util::ser::WithoutLength; + use crate::prelude::*; /// An unpredictable sequence of bytes typically containing information needed to derive @@ -16,4 +18,8 @@ use crate::prelude::*; /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id #[derive(Clone, Debug)] -pub(crate) struct PayerContents(pub Vec); +pub(super) struct PayerContents(pub Vec); + +tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { + (0, metadata: (Vec, WithoutLength)), +}); diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 6cc9d9477..231320ac1 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -506,7 +506,7 @@ macro_rules! tlv_stream { #[derive(Debug)] pub(super) struct $name { $( - $field: Option, + pub(super) $field: Option, )* }