Offer metadata and signing pubkey derivation
authorJeffrey Czyz <jkczyz@gmail.com>
Wed, 8 Feb 2023 01:13:08 +0000 (19:13 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Thu, 20 Apr 2023 02:06:37 +0000 (21:06 -0500)
Add support for deriving a transient signing pubkey for each Offer from
an ExpandedKey and a nonce. This facilitates recipient privacy by not
tying any Offer to any other nor to the recipient's node id.

Additionally, support stateless Offer verification by setting its
metadata using an HMAC over the nonce and the remaining TLV records,
which will be later verified when receiving an InvoiceRequest.

lightning/src/ln/inbound_payment.rs
lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs
lightning/src/offers/mod.rs
lightning/src/offers/offer.rs
lightning/src/offers/payer.rs
lightning/src/offers/refund.rs
lightning/src/offers/signer.rs [new file with mode: 0644]

index 058339cbc1d2451e385307fc0fa5dbc0bf96ab2a..e6668a33cee2d12cf2710a0cab4088523e156ab9 100644 (file)
@@ -26,7 +26,7 @@ use crate::util::logger::Logger;
 use core::convert::TryInto;
 use core::ops::Deref;
 
-const IV_LEN: usize = 16;
+pub(crate) const IV_LEN: usize = 16;
 const METADATA_LEN: usize = 16;
 const METADATA_KEY_LEN: usize = 32;
 const AMT_MSAT_LEN: usize = 8;
@@ -66,6 +66,52 @@ impl ExpandedKey {
                        offers_base_key,
                }
        }
+
+       /// Returns an [`HmacEngine`] used to construct [`Offer::metadata`].
+       ///
+       /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
+       #[allow(unused)]
+       pub(crate) fn hmac_for_offer(
+               &self, nonce: Nonce, iv_bytes: &[u8; IV_LEN]
+       ) -> HmacEngine<Sha256> {
+               let mut hmac = HmacEngine::<Sha256>::new(&self.offers_base_key);
+               hmac.input(iv_bytes);
+               hmac.input(&nonce.0);
+               hmac
+       }
+}
+
+/// A 128-bit number used only once.
+///
+/// Needed when constructing [`Offer::metadata`] and deriving [`Offer::signing_pubkey`] from
+/// [`ExpandedKey`]. Must not be reused for any other derivation without first hashing.
+///
+/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
+/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
+#[allow(unused)]
+#[derive(Clone, Copy)]
+pub(crate) struct Nonce([u8; Self::LENGTH]);
+
+impl Nonce {
+       /// Number of bytes in the nonce.
+       pub const LENGTH: usize = 16;
+
+       /// Creates a `Nonce` from the given [`EntropySource`].
+       pub fn from_entropy_source<ES: Deref>(entropy_source: ES) -> Self
+       where
+               ES::Target: EntropySource,
+       {
+               let mut bytes = [0u8; Self::LENGTH];
+               let rand_bytes = entropy_source.get_secure_random_bytes();
+               bytes.copy_from_slice(&rand_bytes[..Self::LENGTH]);
+
+               Nonce(bytes)
+       }
+
+       /// Returns a slice of the underlying bytes of size [`Nonce::LENGTH`].
+       pub fn as_slice(&self) -> &[u8] {
+               &self.0
+       }
 }
 
 enum Method {
index 2c530760ce3fd05216e1c8c7ad7d05128c042cd4..b0783a306e5c5b0e99a0046180599cfa89bf74e2 100644 (file)
@@ -313,7 +313,8 @@ impl<'a> UnsignedInvoice<'a> {
 /// [`Offer`]: crate::offers::offer::Offer
 /// [`Refund`]: crate::offers::refund::Refund
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub struct Invoice {
        bytes: Vec<u8>,
        contents: InvoiceContents,
@@ -324,7 +325,8 @@ pub struct Invoice {
 ///
 /// [`Offer`]: crate::offers::offer::Offer
 /// [`Refund`]: crate::offers::refund::Refund
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 enum InvoiceContents {
        /// Contents for an [`Invoice`] corresponding to an [`Offer`].
        ///
index db540ce62ff1e84a316ee0927b3e61711d82b632..8bb5737c3f789b1f473872320697a9dc7fe3fb41 100644 (file)
@@ -250,7 +250,8 @@ impl<'a> UnsignedInvoiceRequest<'a> {
 ///
 /// [`Invoice`]: crate::offers::invoice::Invoice
 /// [`Offer`]: crate::offers::offer::Offer
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub struct InvoiceRequest {
        pub(super) bytes: Vec<u8>,
        pub(super) contents: InvoiceRequestContents,
@@ -260,7 +261,8 @@ pub struct InvoiceRequest {
 /// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`].
 ///
 /// [`Invoice`]: crate::offers::invoice::Invoice
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub(super) struct InvoiceRequestContents {
        payer: PayerContents,
        pub(super) offer: OfferContents,
index c2b0d6aea18cb9cd3e57bc7fc14ece87054fc904..0fb20f42d79e61b394cf46b59e6794e7b42a76fe 100644 (file)
@@ -19,5 +19,7 @@ pub mod offer;
 pub mod parse;
 mod payer;
 pub mod refund;
+#[allow(unused)]
+pub(crate) mod signer;
 #[cfg(test)]
 mod test_utils;
index a1445c6f792277d24760569329eeefc929b7fc47..a5935c87b8ab0482144a4df710a77343943d6ef8 100644 (file)
 
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::PublicKey;
+use bitcoin::secp256k1::{PublicKey, Secp256k1, self};
 use core::convert::TryFrom;
 use core::num::NonZeroU64;
+use core::ops::Deref;
 use core::str::FromStr;
 use core::time::Duration;
+use crate::chain::keysinterface::EntropySource;
 use crate::io;
 use crate::ln::features::OfferFeatures;
+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::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
+use crate::offers::signer::{Metadata, MetadataMaterial};
 use crate::onion_message::BlindedPath;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
@@ -87,30 +91,89 @@ use crate::prelude::*;
 #[cfg(feature = "std")]
 use std::time::SystemTime;
 
+const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~";
+
 /// Builds an [`Offer`] for the "offer to be paid" flow.
 ///
 /// See [module-level documentation] for usage.
 ///
 /// [module-level documentation]: self
-pub struct OfferBuilder {
+pub struct OfferBuilder<'a, M: MetadataStrategy, T: secp256k1::Signing> {
        offer: OfferContents,
+       metadata_strategy: core::marker::PhantomData<M>,
+       secp_ctx: Option<&'a Secp256k1<T>>,
 }
 
-impl OfferBuilder {
+/// Indicates how [`Offer::metadata`] may be set.
+pub trait MetadataStrategy {}
+
+/// [`Offer::metadata`] may be explicitly set or left empty.
+pub struct ExplicitMetadata {}
+
+/// [`Offer::metadata`] will be derived.
+pub struct DerivedMetadata {}
+
+impl MetadataStrategy for ExplicitMetadata {}
+impl MetadataStrategy for DerivedMetadata {}
+
+impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> {
        /// Creates a new builder for an offer setting the [`Offer::description`] and using the
        /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered
        /// while the offer is valid.
        ///
        /// Use a different pubkey per offer to avoid correlating offers.
        pub fn new(description: String, signing_pubkey: PublicKey) -> Self {
-               let offer = OfferContents {
-                       chains: None, metadata: None, amount: None, description,
-                       features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
-                       supported_quantity: Quantity::One, signing_pubkey,
-               };
-               OfferBuilder { offer }
+               OfferBuilder {
+                       offer: OfferContents {
+                               chains: None, metadata: None, amount: None, description,
+                               features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
+                               supported_quantity: Quantity::One, signing_pubkey,
+                       },
+                       metadata_strategy: core::marker::PhantomData,
+                       secp_ctx: None,
+               }
+       }
+
+       /// Sets the [`Offer::metadata`] to the given bytes.
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub fn metadata(mut self, metadata: Vec<u8>) -> Result<Self, SemanticError> {
+               self.offer.metadata = Some(Metadata::Bytes(metadata));
+               Ok(self)
        }
+}
 
+impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
+       /// Similar to [`OfferBuilder::new`] except, if [`OfferBuilder::path`] is called, the signing
+       /// pubkey is derived from the given [`ExpandedKey`] and [`EntropySource`]. This provides
+       /// 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`].
+       ///
+       /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+       /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
+       pub fn deriving_signing_pubkey<ES: Deref>(
+               description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+               secp_ctx: &'a Secp256k1<T>
+       ) -> Self where ES::Target: EntropySource {
+               let nonce = Nonce::from_entropy_source(entropy_source);
+               let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
+               let metadata = Metadata::DerivedSigningPubkey(derivation_material);
+               OfferBuilder {
+                       offer: OfferContents {
+                               chains: None, metadata: Some(metadata), amount: None, description,
+                               features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
+                               supported_quantity: Quantity::One, signing_pubkey: node_id,
+                       },
+                       metadata_strategy: core::marker::PhantomData,
+                       secp_ctx: Some(secp_ctx),
+               }
+       }
+}
+
+impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
        /// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called,
        /// the chain hash of [`Network::Bitcoin`] is assumed to be the only one supported.
        ///
@@ -127,14 +190,6 @@ impl OfferBuilder {
                self
        }
 
-       /// Sets the [`Offer::metadata`].
-       ///
-       /// Successive calls to this method will override the previous setting.
-       pub fn metadata(mut self, metadata: Vec<u8>) -> Self {
-               self.offer.metadata = Some(metadata);
-               self
-       }
-
        /// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`].
        ///
        /// Successive calls to this method will override the previous setting.
@@ -204,28 +259,50 @@ impl OfferBuilder {
                        }
                }
 
+               Ok(self.build_without_checks())
+       }
+
+       fn build_without_checks(mut self) -> Offer {
+               // Create the metadata for stateless verification of an InvoiceRequest.
+               if let Some(mut metadata) = self.offer.metadata.take() {
+                       if metadata.has_derivation_material() {
+                               if self.offer.paths.is_none() {
+                                       metadata = metadata.without_keys();
+                               }
+
+                               let mut tlv_stream = self.offer.as_tlv_stream();
+                               debug_assert_eq!(tlv_stream.metadata, None);
+                               tlv_stream.metadata = None;
+                               if metadata.derives_keys() {
+                                       tlv_stream.node_id = None;
+                               }
+
+                               let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
+                               metadata = derived_metadata;
+                               if let Some(keys) = keys {
+                                       self.offer.signing_pubkey = keys.public_key();
+                               }
+                       }
+
+                       self.offer.metadata = Some(metadata);
+               }
+
                let mut bytes = Vec::new();
                self.offer.write(&mut bytes).unwrap();
 
-               Ok(Offer {
-                       bytes,
-                       contents: self.offer,
-               })
+               Offer { bytes, contents: self.offer }
        }
 }
 
 #[cfg(test)]
-impl OfferBuilder {
+impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
        fn features_unchecked(mut self, features: OfferFeatures) -> Self {
                self.offer.features = features;
                self
        }
 
        pub(super) fn build_unchecked(self) -> Offer {
-               let mut bytes = Vec::new();
-               self.offer.write(&mut bytes).unwrap();
-
-               Offer { bytes, contents: self.offer }
+               self.build_without_checks()
        }
 }
 
@@ -242,7 +319,8 @@ impl OfferBuilder {
 ///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 /// [`Invoice`]: crate::offers::invoice::Invoice
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub struct Offer {
        // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
        // fields.
@@ -254,10 +332,11 @@ pub struct Offer {
 ///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 /// [`Invoice`]: crate::offers::invoice::Invoice
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub(super) struct OfferContents {
        chains: Option<Vec<ChainHash>>,
-       metadata: Option<Vec<u8>>,
+       metadata: Option<Metadata>,
        amount: Option<Amount>,
        description: String,
        features: OfferFeatures,
@@ -292,7 +371,7 @@ impl Offer {
        /// 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`.
        pub fn metadata(&self) -> Option<&Vec<u8>> {
-               self.contents.metadata.as_ref()
+               self.contents.metadata()
        }
 
        /// The minimum amount required for a successful payment of a single item.
@@ -406,6 +485,10 @@ impl OfferContents {
                self.chains().contains(&chain)
        }
 
+       pub fn metadata(&self) -> Option<&Vec<u8>> {
+               self.metadata.as_ref().and_then(|metadata| metadata.as_bytes())
+       }
+
        #[cfg(feature = "std")]
        pub(super) fn is_expired(&self) -> bool {
                match self.absolute_expiry {
@@ -498,7 +581,7 @@ impl OfferContents {
 
                OfferTlvStreamRef {
                        chains: self.chains.as_ref(),
-                       metadata: self.metadata.as_ref(),
+                       metadata: self.metadata(),
                        currency,
                        amount,
                        description: Some(&self.description),
@@ -616,6 +699,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
                        issuer, quantity_max, node_id,
                } = tlv_stream;
 
+               let metadata = metadata.map(|metadata| Metadata::Bytes(metadata));
+
                let amount = match (currency, amount) {
                        (None, None) => None,
                        (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => {
@@ -765,15 +850,15 @@ mod tests {
        #[test]
        fn builds_offer_with_metadata() {
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .metadata(vec![42; 32])
+                       .metadata(vec![42; 32]).unwrap()
                        .build()
                        .unwrap();
                assert_eq!(offer.metadata(), Some(&vec![42; 32]));
                assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .metadata(vec![42; 32])
-                       .metadata(vec![43; 32])
+                       .metadata(vec![42; 32]).unwrap()
+                       .metadata(vec![43; 32]).unwrap()
                        .build()
                        .unwrap();
                assert_eq!(offer.metadata(), Some(&vec![43; 32]));
index 7e1da769edab6cec527c4de425bb7711fc50fa1f..12b471c6ce4ee4e36934d0962becb7e5f8c51f2b 100644 (file)
@@ -17,7 +17,8 @@ use crate::prelude::*;
 /// [`InvoiceRequest::payer_id`].
 ///
 /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub(super) struct PayerContents(pub Vec<u8>);
 
 tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
index 9eda6ecd50c673e104b5c224beb59c1b5714d685..6d44eb3da6d54e4a5e505a70d1aeee7316fd53cf 100644 (file)
@@ -216,7 +216,8 @@ impl RefundBuilder {
 ///
 /// [`Invoice`]: crate::offers::invoice::Invoice
 /// [`Offer`]: crate::offers::offer::Offer
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub struct Refund {
        pub(super) bytes: Vec<u8>,
        pub(super) contents: RefundContents,
@@ -225,7 +226,8 @@ pub struct Refund {
 /// The contents of a [`Refund`], which may be shared with an [`Invoice`].
 ///
 /// [`Invoice`]: crate::offers::invoice::Invoice
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub(super) struct RefundContents {
        payer: PayerContents,
        // offer fields
diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs
new file mode 100644 (file)
index 0000000..e1a1a4d
--- /dev/null
@@ -0,0 +1,150 @@
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Utilities for signing offer messages and verifying metadata.
+
+use bitcoin::hashes::{Hash, HashEngine};
+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 core::fmt;
+use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
+use crate::util::ser::Writeable;
+
+use crate::prelude::*;
+
+const DERIVED_METADATA_HMAC_INPUT: &[u8; 16] = &[1; 16];
+const DERIVED_METADATA_AND_KEYS_HMAC_INPUT: &[u8; 16] = &[2; 16];
+
+/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
+/// verified.
+#[derive(Clone)]
+pub(super) enum Metadata {
+       /// Metadata as parsed, supplied by the user, or derived from the message contents.
+       Bytes(Vec<u8>),
+
+       /// Metadata to be derived from message contents and given material.
+       Derived(MetadataMaterial),
+
+       /// Metadata and signing pubkey to be derived from message contents and given material.
+       DerivedSigningPubkey(MetadataMaterial),
+}
+
+impl Metadata {
+       pub fn as_bytes(&self) -> Option<&Vec<u8>> {
+               match self {
+                       Metadata::Bytes(bytes) => Some(bytes),
+                       Metadata::Derived(_) => None,
+                       Metadata::DerivedSigningPubkey(_) => None,
+               }
+       }
+
+       pub fn has_derivation_material(&self) -> bool {
+               match self {
+                       Metadata::Bytes(_) => false,
+                       Metadata::Derived(_) => true,
+                       Metadata::DerivedSigningPubkey(_) => true,
+               }
+       }
+
+       pub fn derives_keys(&self) -> bool {
+               match self {
+                       Metadata::Bytes(_) => false,
+                       Metadata::Derived(_) => false,
+                       Metadata::DerivedSigningPubkey(_) => true,
+               }
+       }
+
+       pub fn without_keys(self) -> Self {
+               match self {
+                       Metadata::Bytes(_) => self,
+                       Metadata::Derived(_) => self,
+                       Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material),
+               }
+       }
+
+       pub fn derive_from<W: Writeable, T: secp256k1::Signing>(
+               self, tlv_stream: W, secp_ctx: Option<&Secp256k1<T>>
+       ) -> (Self, Option<KeyPair>) {
+               match self {
+                       Metadata::Bytes(_) => (self, None),
+                       Metadata::Derived(mut metadata_material) => {
+                               tlv_stream.write(&mut metadata_material.hmac).unwrap();
+                               (Metadata::Bytes(metadata_material.derive_metadata()), None)
+                       },
+                       Metadata::DerivedSigningPubkey(mut metadata_material) => {
+                               tlv_stream.write(&mut metadata_material.hmac).unwrap();
+                               let secp_ctx = secp_ctx.unwrap();
+                               let (metadata, keys) = metadata_material.derive_metadata_and_keys(secp_ctx);
+                               (Metadata::Bytes(metadata), Some(keys))
+                       },
+               }
+       }
+}
+
+impl fmt::Debug for Metadata {
+       fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+               match self {
+                       Metadata::Bytes(bytes) => bytes.fmt(f),
+                       Metadata::Derived(_) => f.write_str("Derived"),
+                       Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"),
+               }
+       }
+}
+
+#[cfg(test)]
+impl PartialEq for Metadata {
+       fn eq(&self, other: &Self) -> bool {
+               match self {
+                       Metadata::Bytes(bytes) => if let Metadata::Bytes(other_bytes) = other {
+                               bytes == other_bytes
+                       } else {
+                               false
+                       },
+                       Metadata::Derived(_) => false,
+                       Metadata::DerivedSigningPubkey(_) => false,
+               }
+       }
+}
+
+/// Material used to create metadata for a message.
+#[derive(Clone)]
+pub(super) struct MetadataMaterial {
+       nonce: Nonce,
+       hmac: HmacEngine<Sha256>,
+}
+
+impl MetadataMaterial {
+       pub fn new(nonce: Nonce, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN]) -> Self {
+               Self {
+                       nonce,
+                       hmac: expanded_key.hmac_for_offer(nonce, iv_bytes),
+               }
+       }
+
+       fn derive_metadata(mut self) -> Vec<u8> {
+               self.hmac.input(DERIVED_METADATA_HMAC_INPUT);
+
+               let mut bytes = self.nonce.as_slice().to_vec();
+               bytes.extend_from_slice(&Hmac::from_engine(self.hmac).into_inner());
+               bytes
+       }
+
+       fn derive_metadata_and_keys<T: secp256k1::Signing>(
+               mut self, secp_ctx: &Secp256k1<T>
+       ) -> (Vec<u8>, KeyPair) {
+               self.hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT);
+
+               let hmac = Hmac::from_engine(self.hmac);
+               let privkey = SecretKey::from_slice(hmac.as_inner()).unwrap();
+               let keys = KeyPair::from_secret_key(secp_ctx, &privkey);
+               (self.nonce.as_slice().to_vec(), keys)
+       }
+}