use crate::error::SessionBundleError;
use crate::types::{ParticipantEntry, SessionTrustBundle};
use aitp_core::{
check_members, from_serde_error, jcs, reject_duplicate_keys, Aid, ExtensionsMap, Timestamp,
};
use aitp_crypto::{AitpSigner, CryptoError, Digest32, DynAsyncAitpSigner, Signature};
use aitp_tct::{TctClaims, TCT_CLAIMS_MEMBERS};
use serde::Serialize;
use uuid::Uuid;
pub const DEFAULT_BUNDLE_VERSION: &str = aitp_core::PROTOCOL_VERSION;
pub(crate) fn peek_tct_claims(token: &str) -> Result<TctClaims, SessionBundleError> {
let payload = aitp_crypto::jws::decode_payload_unverified(token)
.map_err(|e| SessionBundleError::Canonicalization(format!("participant tct: {e}")))?;
if let Err(e) = reject_duplicate_keys(&payload) {
return Err(SessionBundleError::Canonicalization(format!(
"participant tct claims: {e}"
)));
}
if let Ok(peek_value) = serde_json::from_slice::<serde_json::Value>(&payload) {
check_members("TctClaims", &peek_value, TCT_CLAIMS_MEMBERS)
.map_err(|e| SessionBundleError::UnknownField(e.field))?;
}
serde_json::from_slice(&payload).map_err(|e| {
if let Some(field) = from_serde_error(&e) {
SessionBundleError::UnknownField(field)
} else {
SessionBundleError::Canonicalization(format!("participant tct claims: {e}"))
}
})
}
pub struct SessionBundleBuilder<'a, S: ?Sized + 'a = dyn AitpSigner + 'a> {
coordinator_key: &'a S,
session_id: Option<Uuid>,
participants: Vec<ParticipantEntry>,
issued_at: Option<Timestamp>,
extensions: Option<ExtensionsMap>,
}
impl<'a> SessionBundleBuilder<'a, dyn AitpSigner + 'a> {
pub fn new(coordinator_key: &'a dyn AitpSigner) -> Self {
Self::with_coordinator(coordinator_key)
}
pub fn build(self) -> Result<SessionTrustBundle, SessionBundleError> {
let (draft, signer) = self.draft()?;
let signature =
aitp_crypto::sign_jcs(signer, &draft.digest).map_err(CryptoError::Signer)?;
Ok(draft.finish(signature))
}
}
impl<'a> SessionBundleBuilder<'a, dyn DynAsyncAitpSigner + 'a> {
pub fn new_async(coordinator_key: &'a dyn DynAsyncAitpSigner) -> Self {
Self::with_coordinator(coordinator_key)
}
pub async fn build_async(self) -> Result<SessionTrustBundle, SessionBundleError> {
let (draft, signer) = self.draft()?;
let signature = aitp_crypto::sign_jcs_async(signer, &draft.digest)
.await
.map_err(CryptoError::Signer)?;
Ok(draft.finish(signature))
}
}
trait CoordinatorAid {
fn coordinator_aid(&self) -> &Aid;
}
impl CoordinatorAid for dyn AitpSigner + '_ {
fn coordinator_aid(&self) -> &Aid {
self.aid()
}
}
impl CoordinatorAid for dyn DynAsyncAitpSigner + '_ {
fn coordinator_aid(&self) -> &Aid {
self.aid()
}
}
struct Draft {
digest: Digest32,
session_id: Uuid,
coordinator: Aid,
issued_at: Timestamp,
expires_at: Timestamp,
participants: Vec<ParticipantEntry>,
extensions: Option<ExtensionsMap>,
}
impl Draft {
fn finish(self, signature: Signature) -> SessionTrustBundle {
SessionTrustBundle {
version: DEFAULT_BUNDLE_VERSION.to_string(),
session_id: self.session_id,
coordinator: self.coordinator,
issued_at: self.issued_at,
expires_at: self.expires_at,
participants: self.participants,
extensions: self.extensions,
signature: signature.into_string(),
}
}
}
#[allow(private_bounds)]
impl<'a, S: ?Sized + CoordinatorAid> SessionBundleBuilder<'a, S> {
fn with_coordinator(coordinator_key: &'a S) -> Self {
Self {
coordinator_key,
session_id: None,
participants: Vec::new(),
issued_at: None,
extensions: None,
}
}
pub fn session_id(mut self, id: Uuid) -> Self {
self.session_id = Some(id);
self
}
pub fn issued_at(mut self, ts: Timestamp) -> Self {
self.issued_at = Some(ts);
self
}
pub fn extensions(mut self, extensions: ExtensionsMap) -> Self {
self.extensions = Some(extensions);
self
}
pub fn participant(mut self, aid: Aid, tct: String) -> Self {
self.participants.push(ParticipantEntry { aid, tct });
self
}
fn draft(self) -> Result<(Draft, &'a S), SessionBundleError> {
if self.participants.is_empty() {
return Err(SessionBundleError::EmptyParticipants);
}
let coordinator = self.coordinator_key.coordinator_aid().clone();
let session_id = self.session_id.unwrap_or_else(Uuid::new_v4);
let issued_at = self.issued_at.unwrap_or_else(Timestamp::now);
let mut min_exp: Option<Timestamp> = None;
for entry in &self.participants {
let claims = peek_tct_claims(&entry.tct)?;
if claims.iss != coordinator {
return Err(SessionBundleError::CoordinatorIssuerMismatch);
}
if claims.aud != entry.aid {
return Err(SessionBundleError::AudienceMismatch);
}
min_exp = Some(match min_exp {
Some(m) if m.0 <= claims.exp.0 => m,
_ => claims.exp,
});
}
let expires_at = min_exp.ok_or(SessionBundleError::EmptyParticipants)?;
let canonical = bundle_signing_bytes(&BundleSigningBody {
version: DEFAULT_BUNDLE_VERSION,
session_id: &session_id,
coordinator: &coordinator,
issued_at: &issued_at,
expires_at: &expires_at,
participants: &self.participants,
extensions: self.extensions.as_ref(),
})?;
let digest = Digest32::sha256(&canonical);
Ok((
Draft {
digest,
session_id,
coordinator,
issued_at,
expires_at,
participants: self.participants,
extensions: self.extensions,
},
self.coordinator_key,
))
}
}
pub(crate) fn bundle_signing_bytes(
body: &BundleSigningBody<'_>,
) -> Result<Vec<u8>, SessionBundleError> {
jcs::canonicalize_serializable(body)
.map_err(|e| SessionBundleError::Canonicalization(e.to_string()))
}
#[derive(Serialize)]
pub(crate) struct BundleSigningBody<'a> {
pub version: &'a str,
pub session_id: &'a Uuid,
pub coordinator: &'a Aid,
pub issued_at: &'a Timestamp,
pub expires_at: &'a Timestamp,
pub participants: &'a [ParticipantEntry],
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<&'a ExtensionsMap>,
}
#[cfg(test)]
mod signing_input_tests {
use super::*;
use crate::types::SessionTrustBundle;
#[test]
fn production_signing_bytes_match_the_pinned_vector() {
let kat_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("repo root")
.join("tests/schemas/known-answer/jcs-sha256.json");
let kat: serde_json::Value =
serde_json::from_slice(&std::fs::read(kat_path).expect("read kat")).unwrap();
let v = kat["vectors"]
.as_array()
.unwrap()
.iter()
.find(|v| v["id"].as_str() == Some("kat-session-bundle-001"))
.expect("kat-session-bundle-001 present");
let mut obj = v["object"].as_object().expect("object").clone();
obj.insert("signature".into(), serde_json::Value::from(""));
let bundle: SessionTrustBundle =
serde_json::from_value(serde_json::Value::Object(obj)).expect("vector deserializes");
let canonical = bundle_signing_bytes(&BundleSigningBody {
version: &bundle.version,
session_id: &bundle.session_id,
coordinator: &bundle.coordinator,
issued_at: &bundle.issued_at,
expires_at: &bundle.expires_at,
participants: &bundle.participants,
extensions: bundle.extensions.as_ref(),
})
.expect("canonicalize");
assert_eq!(
canonical.len(),
v["jcs_canonical_len_bytes"].as_u64().unwrap() as usize,
"production signing bytes diverge in length from kat-session-bundle-001"
);
assert_eq!(
hex::encode(&canonical),
v["jcs_canonical_hex"].as_str().unwrap(),
"production signing bytes diverge from kat-session-bundle-001 — the \
implementation canonicalizes a different shape than the spec signs"
);
}
}