use lightning::types::payment::{PaymentHash, PaymentPreimage};
use macaroon::{Macaroon, Verifier, MacaroonKey, Caveat};
#[cfg(feature = "rocket")]
use rocket::{request, Request};
use hex;
#[cfg(feature = "rocket")]
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";
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>,
}
#[cfg(feature = "rocket")]
#[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> {
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 {
id_bytes.windows(32).any(|w| w == expected)
}
}
pub fn verify_l402(
mac: &Macaroon,
caveats: Vec<String>,
root_key: Vec<u8>,
preimage: PaymentPreimage,
) -> Result<(), Box<dyn std::error::Error>> {
let mac_predicates: Vec<Vec<u8>> = mac
.first_party_caveats()
.into_iter()
.filter_map(|c| match c {
Caveat::FirstParty(fp) => Some(fp.predicate().0),
_ => None,
})
.collect();
for required in &caveats {
if !mac_predicates.iter().any(|p| p.as_slice() == required.as_bytes()) {
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() {
assert_eq!(
format_challenge("AGIAJEem", "lnbc10n1p"),
r#"L402 macaroon="AGIAJEem", invoice="lnbc10n1p""#
);
}
#[test]
fn payment_binding_matches_bytes_not_hex_nibbles() {
use lightning::types::payment::PaymentHash;
let ph = PaymentHash([0xabu8; 32]);
assert!(super::macaroon_id_matches_payment_hash(&ph.0, &ph));
let mut prefixed = vec![0xffu8];
prefixed.extend_from_slice(&ph.0);
assert!(super::macaroon_id_matches_payment_hash(&prefixed, &ph));
let mut framed = vec![0u8; 8];
framed.extend_from_slice(&ph.0);
framed.extend_from_slice(&[1u8; 4]);
assert!(super::macaroon_id_matches_payment_hash(&framed, &ph));
let mut nibble = vec![0x0au8];
nibble.extend_from_slice(&[0xbau8; 32]);
assert!(
!super::macaroon_id_matches_payment_hash(&nibble, &ph),
"nibble-offset hex match must not bind"
);
}
#[test]
fn every_required_caveat_must_be_present() {
use lightning::types::payment::{PaymentHash, PaymentPreimage};
use macaroon::{ByteString, Macaroon, MacaroonKey};
let root_key = vec![7u8; 32];
let key = MacaroonKey::generate(&root_key);
let preimage = PaymentPreimage([0x11u8; 32]);
let payment_hash = PaymentHash::from(preimage);
let mint = |caveats: &[&str]| {
let mut mac =
Macaroon::create(Some("L402".into()), &key, payment_hash.0.into()).unwrap();
for c in caveats {
mac.add_first_party_caveat(ByteString::from(*c));
}
mac
};
let required = || vec!["Scope = a".to_string(), "Tier = premium".to_string()];
assert!(
super::verify_l402(
&mint(&["Scope = a", "Scope = a"]),
required(),
root_key.clone(),
preimage
)
.is_err(),
"duplicate caveat must not satisfy a different required caveat"
);
assert!(super::verify_l402(
&mint(&["Scope = a", "Tier = premium"]),
required(),
root_key.clone(),
preimage
)
.is_ok());
assert!(super::verify_l402(
&mint(&["Scope = a", "Tier = premium", "ExpiresAt = 123"]),
required(),
root_key,
preimage
)
.is_err());
}
}
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)
}
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())
}
}
}