use macaroon::Verifier;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Scope {
Path(String),
Realm(String),
}
impl Scope {
fn caveat(&self) -> String {
match self {
Scope::Path(p) => format!("RequestPath = {}", p),
Scope::Realm(r) => format!("Realm = {}", r),
}
}
}
#[derive(Clone, Debug)]
pub struct RequestBinding {
pub scope: Scope,
pub method: String,
pub expires_at: Option<i64>,
}
impl RequestBinding {
pub fn path(path: impl Into<String>, method: impl Into<String>) -> Self {
Self {
scope: Scope::Path(path.into()),
method: method.into(),
expires_at: None,
}
}
pub fn realm(realm: impl Into<String>, method: impl Into<String>) -> Self {
Self {
scope: Scope::Realm(realm.into()),
method: method.into(),
expires_at: None,
}
}
pub fn with_expiry(mut self, expires_at: Option<i64>) -> Self {
self.expires_at = expires_at;
self
}
pub fn to_caveats(&self) -> Vec<String> {
let mut caveats = vec![
self.scope.caveat(),
format!("RequestMethod = {}", self.method),
];
if let Some(ts) = self.expires_at {
caveats.push(format!("ExpiresAt = {}", ts));
}
caveats
}
pub fn verifier(&self) -> Verifier {
let mut verifier = Verifier::default();
verifier.satisfy_exact(self.scope.caveat().into());
verifier.satisfy_exact(format!("RequestMethod = {}", self.method).into());
verifier.satisfy_general(|predicate| {
let s = match std::str::from_utf8(&predicate.0) {
Ok(s) => s,
Err(_) => return false,
};
if let Some(secs) = s.strip_prefix("ExpiresAt = ") {
if let Ok(ts) = secs.parse::<i64>() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
return now <= ts;
}
}
if s.starts_with("RequestPath = ")
|| s.starts_with("Realm = ")
|| s.starts_with("RequestMethod = ")
{
return false;
}
true
});
verifier
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l402::verify_l402_binding;
use crate::macaroon_util::get_macaroon_as_string;
use crate::utils::parse_l402_header;
use lightning::types::payment::{PaymentHash, PaymentPreimage};
const ROOT_KEY: [u8; 4] = [1, 2, 3, 4];
fn mint(caveats: Vec<String>) -> (macaroon::Macaroon, PaymentPreimage) {
let preimage = PaymentPreimage([7u8; 32]);
let payment_hash = PaymentHash::from(preimage);
let mac_str = get_macaroon_as_string(payment_hash, caveats, ROOT_KEY.to_vec()).unwrap();
let hex_preimage = hex::encode(preimage.0);
let auth = format!("{}:{}", mac_str, hex_preimage);
parse_l402_header(&auth).unwrap()
}
#[test]
fn path_token_verifies_on_its_path() {
let (mac, preimage) = mint(RequestBinding::path("/a", "GET").to_caveats());
let binding = RequestBinding::path("/a", "GET");
assert!(verify_l402_binding(&mac, &binding, ROOT_KEY.to_vec(), preimage).is_ok());
}
#[test]
fn path_token_rejected_on_other_path() {
let (mac, preimage) = mint(RequestBinding::path("/a", "GET").to_caveats());
let binding = RequestBinding::path("/b", "GET");
assert!(verify_l402_binding(&mac, &binding, ROOT_KEY.to_vec(), preimage).is_err());
}
#[test]
fn realm_token_verifies_across_paths_in_the_realm() {
let (mac, preimage) = mint(RequestBinding::realm("blobs", "GET").to_caveats());
for path in ["/aaaa", "/bbbb", "/whatever"] {
let _ = path; let binding = RequestBinding::realm("blobs", "GET");
assert!(
verify_l402_binding(&mac, &binding, ROOT_KEY.to_vec(), preimage).is_ok(),
"realm token should cover every path in the realm"
);
}
}
#[test]
fn realm_token_rejected_on_different_realm() {
let (mac, preimage) = mint(RequestBinding::realm("read", "GET").to_caveats());
let binding = RequestBinding::realm("write", "GET");
assert!(verify_l402_binding(&mac, &binding, ROOT_KEY.to_vec(), preimage).is_err());
}
#[test]
fn token_rejected_cross_method() {
let (mac, preimage) = mint(RequestBinding::realm("blobs", "GET").to_caveats());
let binding = RequestBinding::realm("blobs", "PUT");
assert!(verify_l402_binding(&mac, &binding, ROOT_KEY.to_vec(), preimage).is_err());
}
#[test]
fn path_token_cannot_pose_as_realm_token() {
let (mac, preimage) = mint(RequestBinding::realm("blobs", "GET").to_caveats());
let path_binding = RequestBinding::path("/aaaa", "GET");
assert!(verify_l402_binding(&mac, &path_binding, ROOT_KEY.to_vec(), preimage).is_err());
}
#[test]
fn expired_token_rejected() {
let past = 1_000_000_000i64; let (mac, preimage) = mint(
RequestBinding::realm("blobs", "GET")
.with_expiry(Some(past))
.to_caveats(),
);
let binding = RequestBinding::realm("blobs", "GET");
assert!(verify_l402_binding(&mac, &binding, ROOT_KEY.to_vec(), preimage).is_err());
}
}