use serde::{Deserialize, Serialize};
use crate::admission::{AdmissionPath, Confirmation};
use crate::artifact::SignedArtifact;
use crate::lease::{Lease, LeaseError};
use crate::principal::{PrincipalId, PrincipalKind};
pub trait DeviceSigned: SignedArtifact {
fn signer(&self) -> &PrincipalId;
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum KnockError {
#[error("{0} is not a key principal")]
NotAKey(PrincipalId),
#[error("an Admit lease must carry exp")]
MissingExp,
#[error(transparent)]
Lease(#[from] LeaseError),
}
fn require_key(p: &PrincipalId) -> Result<(), KnockError> {
if p.kind == PrincipalKind::Key {
Ok(())
} else {
Err(KnockError::NotAKey(p.clone()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Knock {
pub requester: PrincipalId,
pub kind: String,
pub id: String,
pub relation: String,
pub nonce: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub standing: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub renews: Option<String>,
pub iat: i64,
}
impl Knock {
pub fn validate(&self) -> Result<(), KnockError> {
require_key(&self.requester)
}
}
impl SignedArtifact for Knock {
const IMPLICIT_ASSERTION: &'static [u8] = b"urn:cheers:artifact:knock:v1";
fn issuer(&self) -> &str {
&self.requester.id
}
}
impl DeviceSigned for Knock {
fn signer(&self) -> &PrincipalId {
&self.requester
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Offer {
pub issuer: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer: Option<PrincipalId>,
pub kind: String,
pub id: String,
pub relation: String,
pub max_uses: u32,
pub jti: String,
pub iat: i64,
pub exp: i64,
}
impl SignedArtifact for Offer {
const IMPLICIT_ASSERTION: &'static [u8] = b"urn:cheers:artifact:offer:v1";
fn issuer(&self) -> &str {
&self.issuer
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", tag = "type", content = "hash")]
pub enum AdmitSource {
Knock(String),
Offer(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Admit {
pub approver: PrincipalId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approver_binding: Option<String>,
pub authority: String,
pub requester: PrincipalId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requester_binding: Option<String>,
pub kind: String,
pub id: String,
pub relation: String,
pub source: AdmitSource,
pub confirmation: Confirmation,
pub epoch: u64,
pub jti: String,
pub iat: i64,
#[serde(flatten)]
pub lease: Lease,
}
impl Admit {
pub fn validate(&self) -> Result<(), KnockError> {
require_key(&self.approver)?;
require_key(&self.requester)?;
if self.lease.exp().is_none() {
return Err(KnockError::MissingExp);
}
self.lease.validate(self.iat)?;
Ok(())
}
pub fn path(&self) -> AdmissionPath {
match (self.confirmation, &self.source) {
(Confirmation::Scan, _) => AdmissionPath::Scan,
(_, AdmitSource::Knock(_)) => AdmissionPath::Knock,
(_, AdmitSource::Offer(_)) => AdmissionPath::Offer,
}
}
}
impl SignedArtifact for Admit {
const IMPLICIT_ASSERTION: &'static [u8] = b"urn:cheers:artifact:admit:v1";
fn issuer(&self) -> &str {
&self.approver.id
}
}
impl DeviceSigned for Admit {
fn signer(&self) -> &PrincipalId {
&self.approver
}
}
#[cfg(test)]
mod tests {
use super::*;
fn admit(refresh_after: i64, exp: Option<i64>) -> Admit {
Admit {
approver: PrincipalId::from_public_key(&[1; 32]),
approver_binding: None,
authority: "https://c.example".into(),
requester: PrincipalId::from_public_key(&[2; 32]),
requester_binding: None,
kind: "namespace".into(),
id: "ed".into(),
relation: "guest".into(),
source: AdmitSource::Knock("h".into()),
confirmation: Confirmation::Compare,
epoch: 3,
jti: "j".into(),
iat: 1_000,
lease: serde_json::from_value(serde_json::json!({"refresh_after": refresh_after, "exp": exp})).unwrap(),
}
}
#[test]
fn admit_requires_exp_and_the_midpoint() {
assert!(admit(1_500, Some(2_000)).validate().is_ok());
assert_eq!(admit(1_500, None).validate(), Err(KnockError::MissingExp));
assert!(matches!(admit(1_501, Some(2_000)).validate(), Err(KnockError::Lease(_))));
let mut a = admit(1_500, Some(2_000));
a.approver = PrincipalId::user("alice");
assert!(matches!(a.validate(), Err(KnockError::NotAKey(_))));
}
#[test]
fn wire_flattens_the_lease_and_spells_levels_lowercase() {
let v = serde_json::to_value(admit(1_500, Some(2_000))).unwrap();
assert_eq!(v["refresh_after"], 1_500);
assert_eq!(v["exp"], 2_000);
assert_eq!(v["confirmation"], "compare");
assert_eq!(v["source"], serde_json::json!({"type": "knock", "hash": "h"}));
let mut a = admit(1_500, Some(2_000));
assert_eq!(a.path(), AdmissionPath::Knock);
a.confirmation = Confirmation::Scan;
assert_eq!(a.path(), AdmissionPath::Scan);
}
#[test]
fn each_kind_has_its_own_assertion() {
let all = [Knock::IMPLICIT_ASSERTION, Offer::IMPLICIT_ASSERTION, Admit::IMPLICIT_ASSERTION];
assert_eq!(all[0], b"urn:cheers:artifact:knock:v1");
assert_eq!(all[1], b"urn:cheers:artifact:offer:v1");
assert_eq!(all[2], b"urn:cheers:artifact:admit:v1");
assert_ne!(Admit::IMPLICIT_ASSERTION, crate::StandingBinding::IMPLICIT_ASSERTION);
}
}