RFC-AITP-0004
RFC-AITP-0004
Mutual Handshake
Document: RFC-AITP-0004 Version: 0.1.0-rc.3 Status: Release Candidate Depends on: RFC-AITP-0001 Core, RFC-AITP-0002 Identity, RFC-AITP-0003 Agent Manifest Referenced by: RFC-AITP-0005 TCT
Abstract
The Mutual Handshake is the core agent-to-agent primitive in AITP. Two agents simultaneously verify each other's identity and establish bidirectional trust without requiring a third-party verifier.
At the end of a successful Mutual Handshake, each agent holds a Trust Context Token (TCT) issued by its peer. Each TCT is signed by the issuing peer, audience-bound to the recipient, capability-scoped to what the issuer is willing to grant, and proof-of-possession bound to the recipient's key.
This RFC defines a four-message protocol over two round trips. There is no central verifier and no shared-verifier "fast path"; AITP v0.1 is strictly peer-to-peer.
1. Design Principles
The Mutual Handshake is governed by four normative principles. Architectural rationale — including why this protocol uses four messages rather than two — lives in docs/architecture.md.
- Simultaneous presentation. Both agents present identity in the same round trip. Either agent MAY initiate.
- Symmetric trust output. Both agents issue TCTs for each other. Neither agent is privileged by the protocol structure.
- Peer-issued TCTs. Each agent acts as its own verifier for the peer it is authenticating. The TCT
issuerfield is the authenticating agent's AID. Consumers of peer-issued TCTs validate them against the peer's AID public key (resolved from the peer's Manifest). - Graceful failure on incompatibility. If trust-anchor overlap cannot be established, the handshake MUST fail with
INCOMPATIBLE_TRUST_ANCHORS. Agents that cannot establish trust MUST NOT proceed with coordination.
2. Protocol Overview
Agent A Agent B
| |
| (A fetches B's manifest, |
| verifies it, screens for |
| trust anchor compatibility) |
| |
|------ MUTUAL_HELLO --------------->|
| A's identity |
| A's manifest (inline) |
| A's requested grants |
| A's PoP nonce |
| |
| (B verifies A's manifest, |
| screens trust anchors, |
| verifies A's identity, |
| evaluates grant policy) |
| |
|<------ MUTUAL_HELLO_ACK -----------|
| B's identity |
| B's manifest (inline) |
| B's requested grants |
| B's PoP nonce |
| A's PoP nonce echo |
| |
| (A verifies B's manifest, |
| verifies B's identity, |
| evaluates grant policy, |
| signs PoP over B's nonce) |
| |
|------ MUTUAL_COMMIT --------------->|
| TCT_B (A → B, signed by A) |
| PoP_A (A proves key to B) |
| |
| (B verifies TCT_B, verifies PoP_A, |
| signs PoP over A's nonce) |
| |
|<------ MUTUAL_COMMIT_ACK ----------|
| TCT_A (B → A, signed by B) |
| PoP_B (B proves key to A) |
| |
| (A verifies TCT_A, verifies PoP_B) |
| |
|====== bidirectional trust =========|
| A holds TCT_A (from B) |
| B holds TCT_B (from A) |The handshake is four messages over two round trips. The first round trip exchanges credentials and nonces. The second round trip exchanges the signed TCTs and proof-of-possession signatures.
3. Message Definitions
All messages use the standard AITP envelope from RFC-AITP-0001 §5. New message_type values: mutual_hello, mutual_hello_ack, mutual_commit, mutual_commit_ack.
PoP signing input (§§3.3, 3.4, 5.3, 5.4). Throughout this RFC,
sha256(<nonce>)denotes the SHA-256 hash of the raw bytes obtained by base64url-decoding the nonce string — never the ASCII bytes of the base64url form. This matches the convention used by the pinned-key proof input (RFC-AITP-0002 §3.1) and downstream PoP (RFC-AITP-0005 §6.1). The unified rule covering all four PoP sites lives in RFC-AITP-0001 §5.4.2. Implementations that hash the base64url string itself are non-conformant.
3.1 MUTUAL_HELLO
Sent by the initiating agent (A) to the target agent (B).
Envelope
{
"version": "aitp/0.1",
"message_type": "mutual_hello",
"message_id": "<uuid-v4>",
"timestamp": 1711900000,
"sender": { "agent_id": "aid:pubkey:<A-pubkey>" },
"payload": { ... },
"signature": "<A-sig>"
}Payload schema
{
"identity": {
"type": "oidc",
"issuer": "https://auth.openai.com",
"subject": "agent-A",
"proof": "<jwt>"
},
"manifest": { ... },
"requested_grants": [
"macp.mode.task.v1",
"read_data"
],
"pop_nonce": "<random 128-bit base64url>"
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
identity | object | REQUIRED | A fresh, handshake-bound identity proof for this exchange. MUST be the same type and subject as manifest.identity_hint. For oidc, the JWT MUST include the pop_nonce value from this message as the JWT nonce claim, binding the proof to this specific handshake and preventing replay of the same JWT in a different session. For pinned_key, identity.public_key MUST equal manifest.identity_hint.public_key. |
manifest | object | REQUIRED | A's Agent Manifest (RFC-AITP-0003), inline. |
requested_grants | array of string | REQUIRED | Capabilities A is requesting from B. |
pop_nonce | string | REQUIRED | Random 128-bit value, encoded as exactly 22 chars of unpadded base64url (RFC-AITP-0001 §5.4). B MUST sign over this in MUTUAL_COMMIT_ACK to prove key possession. |
3.2 MUTUAL_HELLO_ACK
Sent by the target agent (B) in response to MUTUAL_HELLO.
Payload schema
{
"identity": {
"type": "oidc",
"issuer": "https://auth.anthropic.com",
"subject": "agent-B",
"proof": "<jwt>"
},
"manifest": { ... },
"requested_grants": [
"macp.mode.task.v1"
],
"pop_nonce": "<B's nonce>",
"pop_nonce_echo": "<A's pop_nonce from MUTUAL_HELLO>"
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
identity | object | REQUIRED | A fresh, handshake-bound identity proof for this exchange. MUST be the same type and subject as manifest.identity_hint. For oidc, the JWT MUST include the pop_nonce value from this message as the JWT nonce claim, binding the proof to this specific handshake and preventing replay of the same JWT in a different session. For pinned_key, identity.public_key MUST equal manifest.identity_hint.public_key. |
manifest | object | REQUIRED | B's Agent Manifest, inline. |
requested_grants | array of string | REQUIRED | Capabilities B is requesting from A. |
pop_nonce | string | REQUIRED | B's nonce. A MUST sign over this in MUTUAL_COMMIT. |
pop_nonce_echo | string | REQUIRED | MUST equal A's pop_nonce from MUTUAL_HELLO. Binds round 2 to round 1. |
3.3 MUTUAL_COMMIT
Sent by the initiating agent (A) after verifying B's MUTUAL_HELLO_ACK.
Payload schema
{
"tct_for_peer": {
"tct": {
"version": "aitp/0.1",
"jti": "<uuid-v4>",
"issuer": "aid:pubkey:<A-pubkey>",
"subject": "aid:pubkey:<B-pubkey>",
"audience": "aid:pubkey:<B-pubkey>",
"issued_at": 1711900200,
"expires_at": 1711903800,
"grants": ["macp.mode.task.v1"],
"binding": { "cnf": "<B-pubkey>" },
"signature": "<A-sig>"
}
},
"pop_signature": "<base64url — A's sig over sha256(B's pop_nonce)>",
"pop_nonce_echo": "<B's pop_nonce from MUTUAL_HELLO_ACK>"
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
tct_for_peer | object | REQUIRED | The TCT A is issuing for B. See §4 for issuance rules. |
pop_signature | string | REQUIRED | base64url(sign(A_private_key, sha256(B_pop_nonce))). Proves A holds the key for its AID. |
pop_nonce_echo | string | REQUIRED | MUST equal B's pop_nonce from MUTUAL_HELLO_ACK. |
3.4 MUTUAL_COMMIT_ACK
Sent by the target agent (B) to complete the handshake.
Payload schema
{
"tct_for_peer": {
"tct": {
"version": "aitp/0.1",
"jti": "<uuid-v4>",
"issuer": "aid:pubkey:<B-pubkey>",
"subject": "aid:pubkey:<A-pubkey>",
"audience": "aid:pubkey:<A-pubkey>",
"issued_at": 1711900400,
"expires_at": 1711904000,
"grants": ["macp.mode.task.v1"],
"binding": { "cnf": "<A-pubkey>" },
"signature": "<B-sig>"
}
},
"pop_signature": "<base64url — B's sig over sha256(A's pop_nonce)>",
"pop_nonce_echo": "<A's pop_nonce from MUTUAL_HELLO>"
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
tct_for_peer | object | REQUIRED | The TCT B is issuing for A. |
pop_signature | string | REQUIRED | base64url(sign(B_private_key, sha256(A_pop_nonce))). |
pop_nonce_echo | string | REQUIRED | MUST equal A's pop_nonce from MUTUAL_HELLO. |
4. TCT Issuance Rules
Each agent issues a TCT for its peer. Peer-issued TCTs are the canonical TCT form: issuer is a peer agent AID, not a third-party verifier AID.
Consumers of peer-issued TCTs MUST:
- Resolve the issuer's public key from the peer's Agent Manifest (RFC-AITP-0003).
- Cache the peer Manifest for the duration of
manifest.expires_atto avoid re-fetching on every request.
4.1 Grant intersection
The issuing agent MUST issue grants that are the intersection of:
- what the peer's identity allows (based on the issuing agent's local policy for the peer's issuer and subject), AND
- what the peer requested (
requested_grantsin the handshake messages), AND - what the issuing agent's
offered_capabilitiesincludes (from its own Manifest, RFC-AITP-0003 §3.1).
issued_grants = peer_requested_grants
∩ identity_policy(peer_identity)
∩ self_offered_capabilitiesThe issuing agent MUST NOT grant capabilities it did not offer in its Manifest. The issuing agent MUST NOT grant capabilities the peer did not request.
If the resulting issued_grants set is empty, the issuing agent MUST NOT issue a TCT and MUST instead respond with POLICY_VIOLATION. An empty-grants TCT is forbidden in v0.1: a TCT with no grants is operationally indistinguishable from a proof-of-identity, and AITP separates identity (the Manifest's identity binding + handshake PoP) from authority (the TCT's grants). Peers that need an identity-only proof MUST use the Manifest, not a stripped TCT.
4.2 Audience binding
Peer-issued TCTs MUST set audience to the peer's AID:
"audience": "aid:pubkey:<peer-pubkey>"The peer MUST verify that audience matches its own AID when consuming the TCT.
4.3 Expiry
Peer-issued TCT expires_at MUST NOT exceed the issuing agent's Manifest expires_at. The RECOMMENDED default TTL for peer-issued TCTs is 1 hour.
4.4 Proof-of-possession binding
Peer-issued TCTs MUST include binding.cnf set to the peer's public key. The MUTUAL_COMMIT / MUTUAL_COMMIT_ACK PoP signatures ARE the proof-of-possession verification for the handshake itself. Downstream consumers of peer-issued TCTs MAY require additional PoP per RFC-AITP-0005 §6.
5. Verification Steps
5.1 On receiving MUTUAL_HELLO (B verifies A)
The bootstrap order matters. A peer receiving a MUTUAL_HELLO has no pre-shared key for the sender — the sender's claimed key is inside the inline Manifest, and the Manifest itself must be authenticated before any other cryptographic operation that depends on it. The numbered steps below make the order explicit.
Steps 1–3 are the only unauthenticated operations the peer ever performs on inbound traffic. Step 4 is the trust bootstrap — once the Manifest proof-of-possession verifies, every subsequent step has a trusted public key to work with.
B MUST:
-
Parse the envelope unauthenticated. Validate replay controls (
message_iddeduplication,timestamptolerance) per RFC-AITP-0001 §5.5. These do not require the sender's key. -
Parse the payload as a
MutualHelloPayload. -
Confirm
payload.manifest.aidequalsenvelope.sender.agent_id. If they differ, reject withINVALID_ENVELOPE. This anchors all subsequent verification on a single AID. -
Verify the Manifest's
proof_of_possession.signatureusing the public key encoded inpayload.manifest.aid. This is the first cryptographic check. It bootstraps trust in the sender's claimed key. If it fails, reject withMANIFEST_POP_FAILED. -
Verify the Manifest's
signatureusing the same public key. Failure ⇒MANIFEST_SIGNATURE_INVALID. Together, steps 4 and 5 prove the sender controls the private key matchingmanifest.aid. -
Verify the fresh identity proof in
payload.identityper RFC-AITP-0002 — including theaud,nonce, andcnf.jktrequirements (RFC-AITP-0002 §2.2/§2.3). The Manifest itself carries onlyidentity_hint(no JWT); the verifiable proof always lives inpayload.identityso it can be bound to this handshake'spop_nonce. Implementations MUST verify thatpayload.identity.subjectequalspayload.manifest.identity_hint.subjectandpayload.identity.typeequalspayload.manifest.identity_hint.type. For OIDC,payload.identity.issuerMUST equalpayload.manifest.identity_hint.issuer. Forpinned_key,payload.identity.public_keyMUST equalpayload.manifest.identity_hint.public_keyAND MUST equal the AID-identifier component ofpayload.manifest.aid(the same key already authenticated in steps 4–5; this check forecloses substitution of an unrelated pinned key into the descriptor). Any mismatch ⇒IDENTITY_FAILED. -
Verify the envelope signature using the now-trusted public key from
manifest.aid. Failure ⇒INVALID_SIGNATURE. From this point forward, the envelope's contents are authenticated. -
Apply policy. Two distinct compatibility checks, each with its own error code (cf. RFC-AITP-0003 §5):
- Identity type accepted. A's
identity.typeMUST appear in B's ownaccepted_identity_types(default["oidc"]when absent). Failure ⇒INCOMPATIBLE_IDENTITY_TYPE— notINCOMPATIBLE_TRUST_ANCHORS. This is the case where, for example, A presentspinned_keybut B accepts onlyoidc. - Trust-anchor overlap (OIDC only). When
A.identity.type == "oidc", A's identityissuerMUST appear in B'strust_anchors. Failure ⇒INCOMPATIBLE_TRUST_ANCHORS. Forpinned_keythis check is replaced by thepinned_keyslookup already performed in step 6.
After compatibility passes, evaluate A's
requested_grantsagainst B's policy andoffered_capabilitiesto determine what B will grant. Record A'spop_noncefor use in MUTUAL_COMMIT_ACK. Construct and send MUTUAL_HELLO_ACK. - Identity type accepted. A's
5.2 On receiving MUTUAL_HELLO_ACK (A verifies B)
A applies the same 8-step bootstrap to B's inline Manifest, with one additional check before step 8:
A MUST:
- Parse the envelope unauthenticated; validate replay controls.
- Parse the payload as a
MutualHelloAckPayload. - Confirm
payload.manifest.aidequalsenvelope.sender.agent_id. Failure ⇒INVALID_ENVELOPE. - Verify B's Manifest
proof_of_possession.signature. - Verify B's Manifest
signature. - Verify B's identity binding per RFC-AITP-0002 (including
audandcnf.jkt). - Verify the envelope signature.
- Verify
pop_nonce_echoequals A's originalpop_noncefrom MUTUAL_HELLO. Failure ⇒NONCE_MISMATCH. - Apply policy: check B's identity issuer is in A's
trust_anchors, evaluate B'srequested_grants, constructtct_for_peer(the TCT A will issue for B) per §4, computepop_signatureover B'spop_nonce, and send MUTUAL_COMMIT.
5.3 On receiving MUTUAL_COMMIT (B finalizes)
Round 2 messages do not repeat the full bootstrap. The peer's Manifest was verified and cached in round 1; its public key is already trusted.
B MUST:
- Validate the envelope signature (using A's trusted public key from the cached Manifest) and replay controls.
- Verify
pop_nonce_echoequals B'spop_noncefrom MUTUAL_HELLO_ACK. Failure ⇒NONCE_MISMATCH. - Verify
pop_signature:verify(A_pubkey, sha256(B_pop_nonce), pop_signature). Failure ⇒POP_VERIFICATION_FAILED. - Verify
tct_for_peer(the TCT A issued for B):- Signature valid under A's AID public key.
subjectequals B's AID.audienceequals B's AID.expires_atis in the future.tct_for_peer.expires_at≤ A's Manifestexpires_at— the verifier-side Manifest-expiry bound from RFC-AITP-0005 §9.4. B has A's Manifest from round 1 (cached), so this conditional check is required at handshake time. Violation ⇒TCT_EXPIRES_AFTER_MANIFEST.grantsare a subset of B'soffered_capabilities.- Every capability in B's own
required_peer_capabilities(from B's Manifest, RFC-AITP-0003 §3.2) is present intct_for_peer.grants. Missing required capability ⇒INSUFFICIENT_GRANTS.
- Construct
tct_for_peer(the TCT B issues for A) per §4. - Compute
pop_signatureover A'spop_nonce. - Send MUTUAL_COMMIT_ACK.
5.4 On receiving MUTUAL_COMMIT_ACK (A finalizes)
Like §5.3, A relies on the cached, already-verified Manifest from round 1.
A MUST:
- Validate the envelope signature (using B's trusted public key from the cached Manifest) and replay controls.
- Verify
pop_nonce_echoequals A'spop_noncefrom MUTUAL_HELLO. Failure ⇒NONCE_MISMATCH. - Verify
pop_signature:verify(B_pubkey, sha256(A_pop_nonce), pop_signature). Failure ⇒POP_VERIFICATION_FAILED. - Verify
tct_for_peer(the TCT B issued for A):- Signature valid under B's AID public key.
subjectequals A's AID.audienceequals A's AID.expires_atis in the future.tct_for_peer.expires_at≤ B's Manifestexpires_at— the verifier-side Manifest-expiry bound from RFC-AITP-0005 §9.4. A has B's Manifest from round 1 (cached), so this conditional check is required at handshake time. Violation ⇒TCT_EXPIRES_AFTER_MANIFEST.grantsare a subset of A'soffered_capabilities.
- Verify peer-capability requirements. A's own Manifest declares
required_peer_capabilities(RFC-AITP-0003 §3.2). Every capability in that list MUST appear intct_for_peer.grants— i.e. B has granted A everything A required of its peer for this exchange. Any required capability missing ⇒INSUFFICIENT_GRANTS. The converse check happens on B's side in §5.3 step 4. - Store TCT_A (the TCT from B). Handshake is complete.
6. Failure Paths
| Scenario | Who fails | Error code |
|---|---|---|
payload.manifest.aid ≠ envelope.sender.agent_id (step §5.1 #3) | Either | INVALID_ENVELOPE |
| Manifest PoP signature invalid (step §5.1 #4) | Either | MANIFEST_POP_FAILED |
| A's Manifest signature invalid (step §5.1 #5) | B | MANIFEST_SIGNATURE_INVALID |
| B's Manifest signature invalid (step §5.2 #5) | A | MANIFEST_SIGNATURE_INVALID |
Identity proof failed or identity.type ≠ manifest.identity_hint.type (step §5.1 #6) | Either | IDENTITY_FAILED |
Peer's identity type not in own accepted_identity_types (step §5.1 #8) | Either | INCOMPATIBLE_IDENTITY_TYPE |
Peer's OIDC issuer not in own trust_anchors (step §5.1 #8, OIDC only) | Either | INCOMPATIBLE_TRUST_ANCHORS |
| Envelope signature invalid after trust bootstrap (step §5.1 #7, §5.3 #1, §5.4 #1) | Either | INVALID_SIGNATURE |
| PoP signature verification failed (step §5.3 #3, §5.4 #3) | Either | POP_VERIFICATION_FAILED |
pop_nonce_echo mismatch (step §5.2 #8, §5.3 #2, §5.4 #2) | Either | NONCE_MISMATCH |
Peer TCT audience does not match self AID | Either | AUDIENCE_MISMATCH |
Peer TCT grants exceed peer's offered_capabilities | Either | GRANT_OVERFLOW |
| Grant intersection is empty | Issuing peer | POLICY_VIOLATION |
Received TCT lacks a capability listed in own required_peer_capabilities | Either | INSUFFICIENT_GRANTS |
| Peer TCT already expired | Either | TCT_EXPIRED |
Peer TCT expires_at exceeds issuer's Manifest expires_at (step §5.3 #4, §5.4 #4; RFC-AITP-0005 §9.4) | Either | TCT_EXPIRES_AFTER_MANIFEST |
| Envelope replay detected | Either | REPLAY_DETECTED |
| Envelope timestamp expired | Either | TIMESTAMP_EXPIRED |
On any failure, the failing agent MUST send an error envelope with the appropriate code and MUST discard all state from the failed handshake attempt (nonces, partial TCTs, cached Manifests from the failed session).
7. State Management
The Mutual Handshake requires the following transient state, retained for at most the timestamp tolerance window (default 300 s) or until the handshake completes or fails:
| State | Owner | Retained until |
|---|---|---|
| Peer's inline Manifest | Both | Handshake complete or failure |
Own pop_nonce | Both | pop_nonce_echo verified in next message |
Peer's pop_nonce | Both | PoP signature computed and sent |
Own tct_for_peer (constructed, not yet confirmed) | Both | COMMIT_ACK verified |
After the handshake is complete:
| State | Owner | Retained until |
|---|---|---|
| Peer's TCT (received) | Both | TCT expires_at or revocation |
| Peer's Manifest (cached) | Both | Manifest expires_at |
Agents MUST NOT persist pop_nonce values across restarts. Restarting agents MUST re-initiate any interrupted handshake from scratch.
8. Handshake Renewal
TCTs expire per expires_at. Peers MAY initiate a fresh Mutual Handshake before expiry to renew trust. There is no in-band renewal message in v0.1. Agents MUST NOT use an expired peer TCT. See docs/operational-guidance.md for non-normative renewal patterns.
8.1 Non-normative: shortened renewal extension
Implementations MAY offer a shortened renewal endpoint as a non-normative
extension. Shortened renewal is NOT part of v0.1 conformance and MUST be
gated behind an explicit feature or configuration opt-in. Peers MUST
advertise support via the extensions["rfc-aitp-0005.renew_uri"]
Manifest field (see registries/extension-keys.md)
so other implementations can discover it without assuming its presence.
The path /aitp/handshake/renew is used in examples only. It is NOT
a reserved path in core v0.1 — implementations MAY mount the
shortened-renewal endpoint at any path, host, or port that is reachable
over HTTPS. Implementations offering shortened renewal MUST advertise
the actual concrete endpoint they expose in
extensions["rfc-aitp-0005.renew_uri"]; peers that do not find this
extension key in the issuer's Manifest MUST fall back to a fresh Mutual
Handshake (this RFC, §1 onward) for renewal. Peers MUST NOT probe
/aitp/handshake/renew directly when the extension is absent — the
absence of the extension key is the discovery signal, not the HTTP
response from a guessed path.
Wire format for the experimental shortened renewal:
POST /aitp/handshake/renew(illustrative path; the actual endpoint is whatever the issuer advertises inextensions["rfc-aitp-0005.renew_uri"]).- Request body:
{ "current_tct": { "tct": { "...": "TctEnvelope" } }, "pop_nonce": "<22-char unpadded base64url>", "pop_signature": "<86-char unpadded base64url, sign(holder_key, sha256(base64url_decode(pop_nonce)))>" } - Response: a
TctEnvelopewith a newjtiandexpires_at ≤ issuer.manifest.expires_at.
An expired TCT MUST NOT be used to bootstrap a shortened renewal — the
issuer MUST verify current_tct.expires_at > now before issuing a
replacement. The issuer MUST also re-evaluate its grant policy for the
holder; shortened renewal is not a bypass for revocation, manifest
rotation, or trust-anchor changes.
v0.1 conformance testers MUST NOT require shortened renewal support and MUST NOT fail implementations that only support full Mutual Handshake renewal. The eventual standardization of this extension is reserved as RFC-AITP-0013 TCT Renewal Extension (Planned).
9. Integration with Multi-Agent Sessions
AITP v0.1 defines bilateral A2A trust only. Multi-agent systems MAY use the bilateral handshake between a coordinator and each participant, but v0.1 does NOT define participant-to-participant trust propagation, session-wide bundles, membership changes, or session-wide revocation. Session Trust Bundle is reserved for RFC-AITP-0010.
10. Transport in v0.1
The Mutual Handshake is delivered as JSON envelopes over HTTPS (RFC-AITP-0001 §8). Each peer's handshake_endpoint (advertised in its Manifest) accepts POST requests carrying a single AITP envelope; responses are AITP envelopes carrying either the next handshake message or an error envelope.
11. Security Considerations
11.1 Nonce binding across rounds
The pop_nonce_echo field in every response binds round 2 to round 1. Without it, an attacker could inject a fabricated MUTUAL_HELLO_ACK referencing different nonces, causing A to sign a PoP for an agent it did not intend to authenticate. Implementations MUST verify nonce echoes before proceeding.
11.2 Grant inflation by a malicious peer
A malicious peer could send a MUTUAL_HELLO_ACK claiming to offer capabilities it does not actually hold. The receiving agent issues a TCT based on the peer's claims. This does not harm the receiving agent — it only controls what grants it offers the peer, not what it trusts the peer to do. If the peer presents an over-claimed TCT to a third-party consumer, that consumer's grant enforcement is the defense.
11.3 Race condition on Manifest refresh
If Agent B rotates its key between A fetching the Manifest and A initiating the handshake, B's inline Manifest in MUTUAL_HELLO_ACK will carry a newer published_at. A MUST accept the newer Manifest (if it passes all verification checks) and discard the cached copy. A MUST NOT fail the handshake solely because the inline Manifest is newer than the cached one.
11.4 Denial-of-service on handshake endpoint
The handshake_endpoint in the Manifest is a public-facing surface. Implementations MUST apply rate limiting per source AID or IP. The RECOMMENDED default is 10 handshake initiations per minute per source AID.
12. Non-Goals
- Multi-agent session bundles. v0.1 is bilateral only. Participant-to-participant trust propagation, session-wide bundles, membership changes, and session-wide revocation are reserved for RFC-AITP-0010.
- Full mesh trust without a coordinator. v0.1 does not define a gossip or decentralized trust-propagation mechanism.
- Continuous identity assurance. Once a TCT is issued, AITP does not monitor whether the peer's identity remains valid. Renewal (§8) is the mechanism for re-verifying identity at TCT expiry.
- Revocation push. JTI revocation is pull-based (RFC-AITP-0008). There is no push notification to a peer when a TCT is revoked mid-session.