use crate::types::{Tct, TctBinding};
use crate::TctError;
use aitp_core::{base64url, jcs, Aid, Timestamp};
use aitp_crypto::{AitpSigningKey, AitpVerifyingKey};
use serde::Serialize;
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub struct TctBuilder<'a> {
issuer_key: &'a AitpSigningKey,
subject: Option<Aid>,
audience: Option<Aid>,
grants: Vec<String>,
ttl_secs: i64,
subject_pubkey: Option<AitpVerifyingKey>,
now_override: Option<Timestamp>,
jti_override: Option<Uuid>,
}
impl<'a> TctBuilder<'a> {
pub fn new(issuer_key: &'a AitpSigningKey) -> Self {
Self {
issuer_key,
subject: None,
audience: None,
grants: Vec::new(),
ttl_secs: crate::DEFAULT_TCT_TTL_SECS,
subject_pubkey: None,
now_override: None,
jti_override: None,
}
}
pub fn subject(mut self, subject: Aid) -> Self {
self.subject = Some(subject);
self
}
pub fn audience(mut self, audience: Aid) -> Self {
self.audience = Some(audience);
self
}
pub fn grants<I, S>(mut self, grants: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.grants = grants.into_iter().map(Into::into).collect();
self
}
pub fn ttl_secs(mut self, ttl: i64) -> Self {
self.ttl_secs = ttl;
self
}
pub fn subject_pubkey(mut self, pk: AitpVerifyingKey) -> Self {
self.subject_pubkey = Some(pk);
self
}
pub fn issued_at(mut self, ts: Timestamp) -> Self {
self.now_override = Some(ts);
self
}
pub fn jti(mut self, jti: Uuid) -> Self {
self.jti_override = Some(jti);
self
}
pub fn build(self) -> Result<Tct, TctError> {
let subject = self.subject.ok_or(TctError::MissingField("subject"))?;
let audience = self.audience.ok_or(TctError::MissingField("audience"))?;
let subject_pk = self
.subject_pubkey
.ok_or(TctError::MissingField("subject_pubkey"))?;
if self.grants.is_empty() {
return Err(TctError::EmptyGrants);
}
for g in &self.grants {
if g.chars().any(char::is_whitespace) {
return Err(TctError::GrantWhitespace(g.clone()));
}
}
if subject != audience {
return Err(TctError::AudienceMismatch);
}
let cnf = base64url::encode(&subject_pk.to_compressed());
debug_assert!(matches!(cnf.len(), 43 | 44));
let jti = self.jti_override.unwrap_or_else(Uuid::new_v4);
let issued_at = self.now_override.unwrap_or_else(Timestamp::now);
let expires_at = issued_at.plus_secs(self.ttl_secs);
let issuer = self.issuer_key.aid().clone();
let binding = TctBinding { cnf };
let view = TctSigningView {
version: "aitp/0.1",
jti: &jti,
issuer: &issuer,
subject: &subject,
audience: &audience,
issued_at: &issued_at,
expires_at: &expires_at,
grants: &self.grants,
binding: &binding,
};
let canonical = jcs::canonicalize_serializable(&view)
.map_err(|e| TctError::Canonicalization(e.to_string()))?;
let digest = Sha256::digest(&canonical);
let signature = self.issuer_key.sign(&digest);
Ok(Tct {
version: "aitp/0.1".into(),
jti,
issuer,
subject,
audience,
issued_at,
expires_at,
grants: self.grants,
binding,
signature: signature.into_string(),
})
}
}
#[derive(Serialize)]
pub(crate) struct TctSigningView<'a> {
pub version: &'a str,
pub jti: &'a Uuid,
pub issuer: &'a Aid,
pub subject: &'a Aid,
pub audience: &'a Aid,
pub issued_at: &'a Timestamp,
pub expires_at: &'a Timestamp,
pub grants: &'a [String],
pub binding: &'a TctBinding,
}