use crate::types::{IssuedTct, TctRenewalPayload};
use crate::{verify_tct, TctBuilder, TctError, TctVerifyContext};
use aitp_core::{base64url, Timestamp};
use aitp_crypto::{AitpSigningKey, AitpVerifyingKey, Signature};
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub fn build_renewal_request(
holder_key: &AitpSigningKey,
current_tct: String,
pop_nonce: String,
) -> Result<TctRenewalPayload, TctError> {
let nonce_bytes = base64url::decode_strict(&pop_nonce)
.map_err(|e| TctError::ClaimsMalformed(format!("pop_nonce: {e}")))?;
let digest = Sha256::digest(&nonce_bytes);
let pop_signature = holder_key.sign(&digest).into_string();
Ok(TctRenewalPayload {
current_tct,
pop_nonce,
pop_signature,
})
}
pub fn process_renewal_request(
request: &TctRenewalPayload,
issuer_key: &AitpSigningKey,
manifest_expires_at: Timestamp,
now: Timestamp,
ttl_secs: i64,
) -> Result<IssuedTct, TctError> {
let issuer_aid = issuer_key.aid();
let peek = aitp_crypto::jws::decode_payload_unverified(&request.current_tct)
.map_err(TctError::Crypto)?;
let peek_claims: crate::types::TctClaims =
serde_json::from_slice(&peek).map_err(|e| TctError::ClaimsMalformed(e.to_string()))?;
let ctx = TctVerifyContext {
expected_audience: &peek_claims.aud,
issuer: issuer_aid,
now,
issuer_manifest_expires_at: None,
revocation_check: None,
};
let verified = verify_tct(&request.current_tct, &ctx)?;
let claims = verified.claims;
let holder_pk = AitpVerifyingKey::from_aid(&claims.sub).map_err(TctError::Crypto)?;
let nonce_bytes = base64url::decode_strict(&request.pop_nonce)
.map_err(|e| TctError::ClaimsMalformed(format!("pop_nonce: {e}")))?;
let digest = Sha256::digest(&nonce_bytes);
let sig = Signature::parse(&request.pop_signature).map_err(|_| TctError::SignatureInvalid)?;
holder_pk
.verify(&digest, &sig)
.map_err(|_| TctError::SignatureInvalid)?;
if manifest_expires_at.0 <= now.0 {
return Err(TctError::Expired);
}
let effective_ttl = ttl_secs.min(manifest_expires_at.0 - now.0);
if effective_ttl <= 0 {
return Err(TctError::Expired);
}
TctBuilder::new(issuer_key)
.subject(claims.sub.clone())
.audience(claims.aud.clone())
.grants(claims.grants.clone())
.ttl_secs(effective_ttl)
.subject_pubkey(holder_pk)
.issued_at(now)
.jti(Uuid::new_v4())
.build()
}
#[cfg(test)]
mod tests {
use super::*;
fn issue(
issuer: &AitpSigningKey,
holder: &AitpSigningKey,
now: Timestamp,
ttl: i64,
) -> IssuedTct {
TctBuilder::new(issuer)
.subject(holder.aid().clone())
.audience(holder.aid().clone())
.grants(["demo.echo"])
.ttl_secs(ttl)
.subject_pubkey(holder.verifying_key())
.issued_at(now)
.build()
.unwrap()
}
#[test]
fn round_trip_renewal_succeeds() {
let issuer = AitpSigningKey::from_seed(&[0x01; 32]);
let holder = AitpSigningKey::from_seed(&[0x02; 32]);
let now = Timestamp(1_700_000_000);
let manifest_exp = Timestamp(now.0 + 86_400);
let original = issue(&issuer, &holder, now, 60);
let request = build_renewal_request(
&holder,
original.token.clone(),
base64url::encode(&[0x33; 16]),
)
.unwrap();
let renewed = process_renewal_request(&request, &issuer, manifest_exp, now, 3600).unwrap();
assert_ne!(renewed.claims.jti, original.claims.jti);
assert_eq!(renewed.claims.sub, original.claims.sub);
assert_eq!(renewed.claims.grants, original.claims.grants);
assert_eq!(renewed.claims.exp.0, now.0 + 3600);
assert!(renewed.voucher.is_some(), "renewal mints a fresh voucher");
}
#[test]
fn renewal_with_wrong_holder_key_rejected() {
let issuer = AitpSigningKey::from_seed(&[0x10; 32]);
let holder = AitpSigningKey::from_seed(&[0x11; 32]);
let attacker = AitpSigningKey::from_seed(&[0x12; 32]);
let now = Timestamp(1_700_000_000);
let original = issue(&issuer, &holder, now, 60);
let request = build_renewal_request(
&attacker, original.token,
base64url::encode(&[0x44; 16]),
)
.unwrap();
let err = process_renewal_request(&request, &issuer, Timestamp(now.0 + 86_400), now, 3600)
.unwrap_err();
assert!(matches!(err, TctError::SignatureInvalid), "got {err:?}");
}
#[test]
fn renewal_bounded_by_manifest_expiry() {
let issuer = AitpSigningKey::from_seed(&[0x20; 32]);
let holder = AitpSigningKey::from_seed(&[0x21; 32]);
let now = Timestamp(1_700_000_000);
let manifest_exp = Timestamp(now.0 + 600); let original = issue(&issuer, &holder, now, 60);
let request =
build_renewal_request(&holder, original.token, base64url::encode(&[0x55; 16])).unwrap();
let renewed = process_renewal_request(&request, &issuer, manifest_exp, now, 3600).unwrap();
assert_eq!(
renewed.claims.exp.0, manifest_exp.0,
"TTL must be capped by manifest expiry"
);
}
#[test]
fn renewal_after_manifest_expired_rejected() {
let issuer = AitpSigningKey::from_seed(&[0x30; 32]);
let holder = AitpSigningKey::from_seed(&[0x31; 32]);
let now = Timestamp(1_700_000_000);
let original = issue(&issuer, &holder, now, 60);
let request =
build_renewal_request(&holder, original.token, base64url::encode(&[0x66; 16])).unwrap();
let err = process_renewal_request(
&request,
&issuer,
Timestamp(now.0 - 1), now,
3600,
)
.unwrap_err();
assert!(matches!(err, TctError::Expired));
}
#[test]
fn renewal_of_foreign_issuer_token_rejected() {
let issuer = AitpSigningKey::from_seed(&[0x40; 32]);
let other_issuer = AitpSigningKey::from_seed(&[0x41; 32]);
let holder = AitpSigningKey::from_seed(&[0x42; 32]);
let now = Timestamp(1_700_000_000);
let foreign = issue(&other_issuer, &holder, now, 60);
let request =
build_renewal_request(&holder, foreign.token, base64url::encode(&[0x77; 16])).unwrap();
assert!(
process_renewal_request(&request, &issuer, Timestamp(now.0 + 86_400), now, 3600)
.is_err()
);
}
}