use serde::{Deserialize, Serialize};
use crate::artifact::SignedArtifact;
use crate::claims::{DeviceId, PeerKey, UserId};
use crate::lease::Lease;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandingBinding {
pub issuer: String,
pub sub: UserId,
pub device: DeviceId,
pub peer_key: PeerKey,
pub seq: u64,
pub iat: i64,
pub jti: String,
#[serde(flatten)]
pub lease: Lease,
}
impl StandingBinding {
pub fn is_bound_to(&self, presented: &PeerKey) -> bool {
&self.peer_key == presented
}
}
impl SignedArtifact for StandingBinding {
const IMPLICIT_ASSERTION: &'static [u8] = b"urn:cheers:artifact:standing-binding:v1";
fn issuer(&self) -> &str {
&self.issuer
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lease::LeaseState;
use crate::RevocationSet;
fn binding() -> StandingBinding {
StandingBinding {
issuer: "https://c.example".into(),
sub: UserId::new("alice"),
device: DeviceId::new("node:1"),
peer_key: PeerKey::ed25519([7; 32]),
seq: 42,
iat: 1_000,
jti: "j1".into(),
lease: Lease::new(1_000, 2_000, None).unwrap(),
}
}
#[test]
fn wire_shape_has_no_exp_and_round_trips() {
let v = serde_json::to_value(binding()).unwrap();
assert_eq!(
v,
serde_json::json!({
"issuer": "https://c.example",
"sub": "alice",
"device": "node:1",
"peer_key": {"alg": "ed25519", "key": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"},
"seq": 42,
"iat": 1_000,
"jti": "j1",
"refresh_after": 2_000,
})
);
let back: StandingBinding = serde_json::from_value(v).unwrap();
assert_eq!(back, binding());
}
#[test]
fn refresh_after_is_advisory_and_binding_compares_keys() {
let b = binding();
assert_eq!(b.lease.state_at(1_999), LeaseState::Current);
assert_eq!(b.lease.state_at(2_000), LeaseState::Warning { exp: None });
assert!(b.is_bound_to(&PeerKey::ed25519([7; 32])));
assert!(!b.is_bound_to(&PeerKey::ed25519([8; 32])));
}
const GOLDEN: &str = r#"{"issuer":"https://c.example","sub":"alice","device":"node:1","peer_key":{"alg":"ed25519","key":"BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"},"seq":42,"iat":1000,"jti":"j1","refresh_after":2000}"#;
#[test]
fn flattened_lease_is_byte_identical_to_the_old_wire() {
assert_eq!(serde_json::to_string(&binding()).unwrap(), GOLDEN);
let back: StandingBinding = serde_json::from_str(GOLDEN).unwrap();
assert_eq!(back, binding());
assert_eq!(serde_json::to_string(&back).unwrap(), GOLDEN);
}
#[test]
fn exp_rides_the_flattened_lease() {
let mut b = binding();
b.lease = Lease::new(1_000, 1_500, Some(2_000)).unwrap();
let json = serde_json::to_string(&b).unwrap();
assert!(json.ends_with(r#""jti":"j1","refresh_after":1500,"exp":2000}"#), "{json}");
assert_eq!(serde_json::from_str::<StandingBinding>(&json).unwrap(), b);
}
#[test]
fn implicit_assertion_is_its_own() {
assert!(!StandingBinding::IMPLICIT_ASSERTION.is_empty());
assert_ne!(StandingBinding::IMPLICIT_ASSERTION, RevocationSet::IMPLICIT_ASSERTION);
}
}