l402_middleware 2.3.1

A middleware library for rust that provides handler functions to accept microtransactions before serving ad-free content or any paid APIs.
Documentation
use lightning::types::payment::{PaymentHash, PaymentPreimage};
use macaroon::{Macaroon, Verifier, MacaroonKey};
use rocket::{request, Request};
use hex;

use crate::l402;
use crate::caveats::RequestBinding;

pub const L402_TYPE_FREE: &str = "FREE";
pub const L402_TYPE_PAYMENT_REQUIRED: &str = "PAYMENT REQUIRED";
pub const L402_TYPE_PAID: &str = "PAID";
pub const L402_TYPE_ERROR: &str = "ERROR";
pub const L402_HEADER: &str = "L402";
pub const L402_HEADER_NAME: &str = "Accept-Authenticate";
pub const L402_AUTHENTICATE_HEADER_NAME: &str = "WWW-Authenticate";
pub const L402_AUTHORIZATION_HEADER_NAME: &str = "Authorization";

/// Format the `WWW-Authenticate` challenge value for an L402 `402` response.
///
/// Produces the RFC 7235-style header with **quoted** auth-param values:
/// `L402 macaroon="<macaroon>", invoice="<bolt11>"`. This is the single source
/// of truth for the challenge wire format — consumers must call this rather than
/// hand-rolling the string, so the quoting can't drift between them.
pub fn format_challenge(macaroon: &str, invoice: &str) -> String {
    format!(
        "{} macaroon=\"{}\", invoice=\"{}\"",
        L402_HEADER, macaroon, invoice
    )
}

#[derive(Clone)]
pub struct L402Info {
	pub	l402_type: String,
	pub preimage: Option<PaymentPreimage>,
	pub payment_hash: Option<PaymentHash>,
	pub error: Option<String>,
    pub auth_header: Option<String>,
}

#[rocket::async_trait]
impl<'r> request::FromRequest<'r> for L402Info {
    type Error = &'static str;

    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
        // Retrieve L402Info from the local cache
        let l402_info = request.local_cache::<L402Info, _>(|| {
            L402Info {
                l402_type: l402::L402_TYPE_ERROR.to_string(),
                error: Some("No L402 header present".to_string()),
                preimage: None,
                payment_hash: None,
                auth_header: None,
            }
        });

        request::Outcome::Success(l402_info.clone())
    }
}

fn macaroon_id_matches_payment_hash(id_bytes: &[u8], payment_hash: &PaymentHash) -> bool {
    let expected = &payment_hash.0;
    if id_bytes.len() == 33 && id_bytes[0] == 0xff {
        &id_bytes[1..] == expected
    } else if id_bytes.len() == 32 {
        id_bytes == expected
    } else {
        // Fallback for unexpected identifier lengths: hex substring match.
        hex::encode(id_bytes).contains(&hex::encode(expected))
    }
}

pub fn verify_l402(
    mac: &Macaroon,
    caveats: Vec<String>,
    root_key: Vec<u8>,
    preimage: PaymentPreimage,
) -> Result<(), Box<dyn std::error::Error>> {
    // caveat verification
    let mac_caveats = mac.first_party_caveats();
    if caveats.len() > mac_caveats.len() {
        return Err("Error validating macaroon: Caveats don't match".into());
    }

    let mac_key = MacaroonKey::generate(&root_key);
    let mut verifier = Verifier::default();
    
    for caveat in caveats {
        verifier.satisfy_exact(caveat.into());
    }

    match verifier.verify(&mac, &mac_key, Default::default()) {
        Ok(_) => {
            let payment_hash: PaymentHash = PaymentHash::from(preimage);
            let id_bytes = &mac.identifier().clone().0;
            if macaroon_id_matches_payment_hash(id_bytes, &payment_hash) {
                Ok(())
            } else {
                Err(format!(
                    "Invalid PaymentHash {} for macaroon {}",
                    hex::encode(payment_hash.0), hex::encode(id_bytes)
                ).into())
            }
        },
        Err(error) => {
            Err(format!("Error validating macaroon: {:?}", error).into())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::format_challenge;

    #[test]
    fn challenge_uses_quoted_rfc_style() {
        // Values MUST be quoted (RFC 7235). This locks the format so the two
        // consumers can't drift apart again.
        assert_eq!(
            format_challenge("AGIAJEem", "lnbc10n1p"),
            r#"L402 macaroon="AGIAJEem", invoice="lnbc10n1p""#
        );
    }
}

/// Verify an L402 macaroon against a [`RequestBinding`] — the high-level entry
/// point. Builds the binding's enforcing verifier (exact scope/method match +
/// `ExpiresAt` time check + the reject-unknown-predicate guard) and checks the
/// macaroon signature and payment-hash binding in one call.
///
/// Prefer this over [`verify_l402_with_verifier`] unless you need a bespoke
/// verifier: it keeps the security-critical caveat policy in this crate, so
/// consumers can't accidentally omit the guard.
pub fn verify_l402_binding(
    mac: &Macaroon,
    binding: &RequestBinding,
    root_key: Vec<u8>,
    preimage: PaymentPreimage,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut verifier = binding.verifier();
    verify_l402_with_verifier(mac, &mut verifier, root_key, preimage)
}

/// Verify L402 using a provided Verifier instance
pub fn verify_l402_with_verifier(
    mac: &Macaroon,
    verifier: &mut Verifier,
    root_key: Vec<u8>,
    preimage: PaymentPreimage,
) -> Result<(), Box<dyn std::error::Error>> {
    let mac_key = MacaroonKey::generate(&root_key);
    
    match verifier.verify(&mac, &mac_key, Default::default()) {
        Ok(_) => {
            let payment_hash: PaymentHash = PaymentHash::from(preimage);
            let id_bytes = &mac.identifier().clone().0;
            if macaroon_id_matches_payment_hash(id_bytes, &payment_hash) {
                Ok(())
            } else {
                Err(format!(
                    "Invalid PaymentHash {} for macaroon {}",
                    hex::encode(payment_hash.0), hex::encode(id_bytes)
                ).into())
            }
        },
        Err(error) => {
            Err(format!("Error validating macaroon: {:?}", error).into())
        }
    }
}