TCT renewal (RFC-AITP-0013)
Status: draft / opt-in. The shortened renewal exchange is described non-normatively in RFC-AITP-0004 §8.1 and will be standardized in RFC-AITP-0013 (Planned). Implemented in
aitp-tctunder theexperimental-renewalfeature; the high-level driveraitp::renew_tctand the binding methods (build_renewal_request/process_renewal_request) ride the same flag. No wire-stability promise until ratified.
Motivation
A TCT is short-lived (bounded by the issuer's Manifest validity). Before it
expires, the holder needs a fresh one — but replaying the full four-message
Mutual Handshake is wasteful: identity was already established and is encoded in
the existing TCT's subject + binding.cnf. Renewal is a shortened
exchange: the holder presents the existing TCT plus a proof-of-possession,
and the issuer mints a fresh TCT for the same subject/grants.
Wire format
Renewal request payload (TctRenewalPayload):
{
"current_tct": { "tct": { /* the TCT being renewed */ } },
"pop_nonce": "<22-char base64url, fresh>",
"pop_signature": "<sign(holder_key, sha256(base64url_decode(pop_nonce)))>"
}The PoP construction is identical to the handshake's pinned-key proof: the
holder signs sha256(decoded_nonce_bytes) with its long-term key — the key
bound by the existing TCT's binding.cnf. This proves the request comes from
the same holder that originally received the TCT, without re-establishing
identity from scratch.
The high-level aitp::renew_tct POSTs this payload to the peer's
/aitp/handshake/renew endpoint and returns the fresh TctEnvelope.
Verification algorithm (process_renewal_request, issuer side)
- Verify
current_tctunder the issuer's own AID (verify_tctwithexpected_audience = current_tct.audience). The issuer only renews TCTs it itself issued. - PoP check. Decode
binding.cnf(algorithm-agile: 32 B Ed25519 raw or 33 B SEC1-compressed P-256), then verifypop_signatureoversha256(decode(pop_nonce))under that key. Failure ⇒SignatureInvalid. - Issuer Manifest still valid.
manifest_expires_at > now, elseExpired— a holder cannot renew across an issuer key-rotation boundary. - Mint. Build a fresh
Tctwith: samesubject/audience/grants/cnf; new randomjti;issued_at = now;expires_at = min(now + ttl, manifest_expires_at)— the same upper bound the original handshake applied (RFC-AITP-0004 §4.3).effective_ttl <= 0⇒Expired.
The fresh TCT is otherwise indistinguishable from a handshake-issued one and
verifies under the normal verify_tct path.
Known limitations
- Same subject/grants only. Renewal cannot widen scope or rebind to a new key — those require a fresh handshake (or delegation).
- Issuer key-rotation boundary. If the issuer's Manifest has expired, renewal fails closed; the holder must re-handshake.
- Draft / opt-in: gated by
experimental-renewal, excluded from the v0.1 conformance gate.
SDK example (holder Python ↔ issuer)
# Holder side: build the renewal request bound to a fresh nonce.
req_json = holder_agent.build_renewal_request(current_tct_json) # experimental-renewal
# Issuer side: verify PoP + mint a fresh TCT (bounded by the issuer manifest).
fresh_tct_json = issuer_agent.process_renewal_request(
req_json, manifest_exp_unix_secs, new_ttl_secs,
)The bindings/aitp-py/tests/test_renewal.py (and .mjs) suites cover the full
holder → issuer round-trip plus the wrong-holder-key rejection.