Threat Model
Methodology
Section titled “Methodology”Three independent adversarial analyses were conducted:
- Ad-tech fraud history: Every major fraud category from 20+ years of programmatic advertising, mapped to FORA equivalents
- Content licensing abuse brainstorm: 56 attack vectors from each party’s perspective (dishonest exchange, agent, provider, broker, attacker)
- STRIDE security threat model: Formal threat modeling of the protocol’s technical surface
Key Lesson from Ad-Tech History
Section titled “Key Lesson from Ad-Tech History”Every self-reported field gets gamed. Any claim that is not cryptographically attested by a trusted party will be falsified if there’s economic incentive.
This single pattern accounts for most ad-tech fraud: domain spoofing (self-reported domain), geo spoofing (self-reported location), viewability fraud (self-reported measurement), and attribution fraud (self-reported conversion). FORA must learn from this.
1. Supply-Side Threats (Dishonest Exchange Operator)
Section titled “1. Supply-Side Threats (Dishonest Exchange Operator)”T1: Hidden fee inflation
Section titled “T1: Hidden fee inflation”Attack: Exchange charges agent $0.75 but tells provider the rate was $0.50, pocketing the spread. Ad-tech parallel: The “unknown delta” from ISBA study — 15% of spend unaccounted for. Countermeasure (protocol): Offers are cryptographically signed by the Exchange (Ed25519). Provider can verify prices weren’t inflated beyond RSL terms. Countermeasure (reconciliation): Provider compares CDN access logs against Exchange-reported transaction volumes and revenue.
T2: Dynamic price discrimination
Section titled “T2: Dynamic price discrimination”Attack: Exchange serves different prices to different requesters for identical content. Countermeasure (protocol): Not directly preventable — price discrimination is a business decision. If prices are derived from RSL (provider-set), the Exchange can’t inflate beyond RSL terms without detection. Countermeasure (reconciliation): Brokers querying from different identities can detect discrepancies.
T3: Bait-and-switch content
Section titled “T3: Bait-and-switch content”Attack: Offer describes “2026 Q4 Financial Report, 15,000 words” but signed URL delivers different content. Ad-tech parallel: Inventory misrepresentation (declaring premium placement, serving remnant). Countermeasure (protocol): Content hash in the Offer. ResourceIdentity.content_hash commits to content at offer time. Agent verifies after fetch. Mismatch = dispute.
T4: Phantom transactions / ghost billing
Section titled “T4: Phantom transactions / ghost billing”Attack: Exchange fabricates billing_ids that never corresponded to real agent requests. Ad-tech parallel: Impression inflation by exchanges. Countermeasure (protocol): Mutual transaction receipts. Agent’s idempotency key in TransactionRequest is agent’s proof; Exchange’s billing_id is Exchange’s proof. Neither can forge the other’s Ed25519 signature.
T5: Provider revenue skimming
Section titled “T5: Provider revenue skimming”Attack: Exchange tells provider 1,000 transactions occurred; actually processed 1,500.
Countermeasure (reconciliation): Three-sided reconciliation. Provider’s CDN logs, Exchange’s transaction log, and usage reports must all agree.
Countermeasure (protocol): Both sides record signed_url_hash — SHA-256 of the delivery URL verbatim. The Exchange stores it on the transaction and the delivery endpoint stores it on its delivery event, so the two logs join on a value neither side chooses.
T6: Overly long signed URL expiry
Section titled “T6: Overly long signed URL expiry”Attack: Exchange issues 24-hour URLs instead of 5-minute, enabling reuse.
Countermeasure (protocol): The delivery endpoint refuses any URL whose exp has passed, and the Exchange issues short expiries — five minutes in the reference flows. No verifier enforces a second, independent upper bound on the TTL: the only bound is the exp the Exchange signed, so a 24-hour URL is detected by reconciliation (T5), not refused at the edge. A provider who wants a hard ceiling has to add that check to its own edge function.
T7: Signing key leakage
Section titled “T7: Signing key leakage”Attack: Exchange shares signing keys with a preferred requester, enabling URL forgery.
Countermeasure (protocol): Asymmetric signing, with per-tenant key isolation. The Exchange holds a separate private key per tenant, loaded from a key store at startup; the delivery endpoint verifies with the matching public key, published in the Exchange’s key directory. This bounds the damage rather than preventing the act — the attacker here is the Exchange, so no control the Exchange operates can stop it from exporting its own key. What the design gives you is that a leaked verification key forges nothing, a compromised edge cannot mint URLs, and a leaked tenant key forges URLs for that one tenant and no other. Detection is T5’s job: forged fetches appear in the provider’s delivery log and not in the Exchange’s transaction log, and the two join on signed_url_hash.
2. Demand-Side Threats (Dishonest Agent / Requester)
Section titled “2. Demand-Side Threats (Dishonest Agent / Requester)”T8: Signed URL sharing
Section titled “T8: Signed URL sharing”Attack: Agent A pays, shares signed URL with Agents B, C, D before expiry.
Countermeasure (protocol): Agent-bound signed URLs. Embed the agent’s RFC 7638 JWK Thumbprint (agent_identity_hash) as the URL’s agent_id parameter, covered by the URL’s Ed25519 signature; a capable edge function requires the fetcher to present its public key + an RFC 9421 signature (proof-of-possession) and checks thumbprint(presented key) == agent_identity_hash. Implemented via agent_identity_hash on TransactionResponse.
T9: Systematic caching / content mirroring
Section titled “T9: Systematic caching / content mirroring”Attack: Agent fetches every article once, caches locally forever, never pays again. Countermeasure (protocol): Single-use signed URLs. Edge function marks URL as consumed after first fetch. Countermeasure (legal): License terms prohibiting caching beyond session scope.
T10: Usage underreporting (token count)
Section titled “T10: Usage underreporting (token count)”Attack: Agent reports using 500 tokens of a 10,000-token document but ingested the entire thing. Countermeasure (protocol): Never a refused report; monitoring is the operator’s. The reported quantity is unrestricted: an Exchange never refuses a usage report because its quantity differs from the estimate, in either direction. Monitoring discrepancies is the Exchange operator’s own business, not part of the protocol: an operator may reconcile a quantity far below the document’s known size against CDN logs (bytes served for the signed URL) and the size the attestations state, and pursue it through the account relationship or a dispute. The charge never depends on the reported count, in any pricing model: a metered purchase is charged at the estimate × rate the offer states (or one unit’s rate without an estimate) when it is bought, and that charge is final. A publisher that wants the whole document paid for states an estimate that covers it, or prices it per access or flat.
T11: Function misreporting (RAG vs training)
Section titled “T11: Function misreporting (RAG vs training)”Attack: Agent declares function: ai-input (cheaper) but uses content for function: ai-train (prohibited or more expensive).
Countermeasure: Not solvable at protocol level. Agent’s internal processing is a black box. Legal/contractual enforcement only.
T12: Offer metadata harvesting
Section titled “T12: Offer metadata harvesting”Attack: Agent calls DiscoverResources repeatedly, extracting metadata — never transacting. Ad-tech parallel: Bid stream data harvesting. Countermeasure (protocol): Rate limiting on DiscoverResources. Exchange tracks query-to-transaction ratio per agent.
T13: Credential sharing / proxy laundering
Section titled “T13: Credential sharing / proxy laundering”Attack: One agent identity (signing key) shared across organizations. Countermeasure (protocol + operational): Sharing a key cannot redirect costs: the request carries no self-supplied billing field, so every request signed with the shared key bills the one account of the key directory that publishes that key — the abuse lands on the sharer’s own bill. Detection is behavioral, not cryptographic: the Exchange monitors usage patterns per verified identity (IP diversity, volume spikes, impossible concurrency) and can deactivate the account. The signature binding does not prevent sharing. It concentrates all shared usage under one identity so it can be seen and priced.
3. Provider-Side Threats (Dishonest Provider)
Section titled “3. Provider-Side Threats (Dishonest Provider)”T14: Paywall-gated garbage
Section titled “T14: Paywall-gated garbage”Attack: Compelling headline, but behind the paywall: padding, filler, or unrelated content. Countermeasure (protocol): Content integrity hash catches changed content. Exchange curation/reputation systems address quality.
T15: Word count / token count inflation
Section titled “T15: Word count / token count inflation”Attack: Provider pads content to inflate word count, affecting unit_cost calculations. Countermeasure (protocol): Exchange independently verifies content length during RSL ingestion. Agent independently tokenizes after fetch.
T16: Content versioning fraud
Section titled “T16: Content versioning fraud”Attack: Provider changes content at URL after Exchange has priced it. Countermeasure (protocol): Content integrity hash in the Offer. SHA-256 of content at offer creation time. Agent verifies after fetch.
T17: Double-selling with exclusivity claims
Section titled “T17: Double-selling with exclusivity claims”Attack: Provider grants “exclusive” licensing to multiple Exchanges. Countermeasure: ResourceIdentity enables detection (same content_hash from different exchanges). Prevention requires legal contracts.
T18: Selective enforcement / preferential access
Section titled “T18: Selective enforcement / preferential access”Attack: Edge function blocks some agents but quietly allows others to scrape freely. Countermeasure: Not solvable at protocol level. Market dynamics address it.
4. Broker Threats
Section titled “4. Broker Threats”T19: Hidden orchestration fee
Section titled “T19: Hidden orchestration fee”Attack: Broker adds markup to every offer relayed to the agent. Countermeasure (protocol): Exchange signs offers (Ed25519). Broker passes through signed offers. Agent verifies the price was not tampered with.
T20: Kickback-driven offer selection
Section titled “T20: Kickback-driven offer selection”Attack: Broker always routes to a specific Exchange regardless of pricing.
Countermeasure (protocol): Agent can bypass Broker and query and buy at Exchanges directly. A Broker signs every request it sends alone, with its own RFC 9421 HTTP Message Signature, so the Exchange knows which Broker sent it and the Broker cannot deny it. The agent’s consent to a transaction is the AgentAcceptance it signed into the body, which a Broker cannot alter or forge, and agent_request_acceptance binds the full ordered set of offers the agent chose.
T21: Query intelligence harvesting
Section titled “T21: Query intelligence harvesting”Attack: Broker logs every DiscoverResources query, sells the data. Countermeasure: Not solvable at protocol level. Data handling agreements required.
T-BRK-1: Request-set mutation during execute fan-out
Section titled “T-BRK-1: Request-set mutation during execute fan-out”Attack: The agent commits to an ordered set of offers and hands execution to a Broker (BrokerService.ExecuteTransaction). While projecting the set into per-Exchange ExecuteTransaction subrequests, the Broker mutates it at the item level: it removes an item, appends one the agent never chose, reorders the items, or sends a valid subset first so the agent’s idempotency key is consumed by a request the agent did not make — a later honest retry then replays the mutilated result instead of executing the real set.
Why the transport signature does not cover this: The Broker’s RFC 9421 signature (T20, T-DEL-4) proves which Broker sent a subrequest. A projected subrequest is a new HTTP request the Broker authors and signs alone, so that signature says nothing about whether the projected body carries the items the agent chose — see Projected execute requests.
Countermeasure (protocol): TransactionRequest.agent_request_acceptance. The agent signs the complete ordered set of (offer signature, exchange) references plus the requester and idempotency key with its own Ed25519 key, and the Broker must forward the envelope unchanged on every subrequest. Each Exchange verifies the signature and requires its subrequest to equal the complete in-order projection of the signed set addressed to itself, before creating or serving any request-level idempotency state. A removed, appended, reordered, or subset request fails the projection check: the Exchange denies every item of it with DENIAL_REASON_SIGNATURE_INVALID, in the body, purchases nothing and claims nothing. The field is optional for wire compatibility: an older client that omits it keeps per-item execution semantics (each item still carries its own agent_acceptance) but gets no request-level set claim.
T-BRK-2: Tampering with a relayed purchase
Section titled “T-BRK-2: Tampering with a relayed purchase”Attack: A Broker relaying a purchase (BrokerService.ExecuteTransaction) changes what an item buys: a different offer, a higher price, other terms, another Exchange, or a purchase on behalf of a different requester.
Countermeasure (protocol): Per-item integrity. Each item is atomic and its integrity is per resource. The offer carries its issuing Exchange’s Ed25519 signature over price, terms, expiry and exchange; the agent’s AgentAcceptance binds that one offer to the requester and the idempotency key; and the Exchange that owns the resource executes it. Altering any of these breaks the offer signature or the acceptance, and the Exchange refuses the item. Combining items from several Exchanges into one response therefore needs no cross-item integrity. The Broker also checks that requester.domain is the agent’s verified signing directory before relaying, and refuses a mismatch as unauthenticated with request_auth_failure SIGNATURE_INVALID; each Exchange resolves the acceptance keys from requester.domain, so a forged requester fails there in any case.
T-BRK-3: Misreporting a relayed purchase
Section titled “T-BRK-3: Misreporting a relayed purchase”Attack: A Broker relaying a purchase reports an outcome that differs from what an Exchange answered: it claims a refusal for an item that was bought, alters a reported cost or transaction id, or drops one Exchange’s sub-request and reports it refused.
Countermeasure (protocol): Partial. The combined BrokerTransactionResponse is unsigned; the one signed value in a result item is retrieval_endpoint, a URL the issuing Exchange signed, which the Broker cannot forge or alter. What the agent is charged is bounded by the signed offer the Exchange verified, whatever the Broker reports. An upstream refusal always rides in the body (TransactionResultItem.refusal, with the Exchange’s own ErrorDetail), never as a Broker error, so the other Exchanges’ results reach the agent. An agent that doubts an outcome can retry with the same idempotency_key, through the same Broker, another one or directly: each Exchange scopes the key to the agent whatever the route, and answers from its stored result. The agent can also reconcile with the Exchange that issued the offer directly, through its usage report and, where the outcome is wrong, a dispute, which it always files itself: the purchase is the agent’s, never the Broker’s (see Accounts). Residual risk: the per-item report fields are as trustworthy as the Broker; an agent that cannot accept that buys directly.
5. Third-Party Attacks
Section titled “5. Third-Party Attacks”T22: Exchange impersonation
Section titled “T22: Exchange impersonation”Attack: Attacker sets up fake Exchange, poisons fora.json via DNS hijack.
Countermeasure (protocol): fora.json served over HTTPS from provider’s domain (TLS proves endorsement). Every Offer carries exchange_signature (Ed25519). Agent verifies against Exchange’s public key.
T23: Signed URL interception (MITM)
Section titled “T23: Signed URL interception (MITM)”Attack: Attacker intercepts TransactionResponse, steals signed URL. Countermeasure: HTTPS everywhere. Agent identity binding prevents reuse.
T24: Transaction replay
Section titled “T24: Transaction replay”Attack: Attacker captures valid TransactionRequest and replays it.
Countermeasure (protocol): Idempotency keys. Every state-mutating call carries a required idempotency_key — TransactionRequest, UsageReport, and DisputeRequest. Servers dedupe per agent and key, whatever the route — the agent the RFC 9421 signature authenticates on a direct request, the agent its acceptances authenticate on a relayed purchase — so a replay returns the original result instead of re-charging or re-filing, and a captured purchase replayed through another Broker is a replay too. The agent’s authorization is verified before any stored result is created or returned. A key reused by the same agent with a different request (different items, or another order) is refused with already_exists and changes nothing; a key another agent used is never a collision. Broker Resolve (DiscoveryRequest) is pure discovery — it executes no transaction, has no replay exposure, and carries no key.
T25: Billing_id forgery in usage reports
Section titled “T25: Billing_id forgery in usage reports”Attack: Attacker fabricates billing_ids in usage reports. Countermeasure (protocol): Exchange validates billing_id exists in transaction log before accepting usage reports.
T26: DiscoverResources flooding (DoS)
Section titled “T26: DiscoverResources flooding (DoS)”Attack: Flood DiscoverResources to exhaust Exchange resources. Countermeasure (protocol): Per-partner rate limiting keyed on the verified requester identity (RFC 9421 request signature — domain/key). Anonymous queries rejected.
T-EP-1: Manifest-advertised endpoint redirection
Section titled “T-EP-1: Manifest-advertised endpoint redirection”Attack: A party that answers for {domain}/.well-known/fora.json — the domain’s operator, or anyone who can influence what a consumer reads there — advertises an endpoint on a host it controls, or on another port of the named host, or one carrying user:password@ credentials. Every consumer that resolves that domain then sends signed calls (usage reports, disputes, execute) to the attacker’s address, or stamps an Authorization header the SDK never chose. A dial-time SSRF guard raises no objection: the destination is an ordinary public host.
Countermeasure (protocol): WellKnownManifest.endpoint states the binding as a MUST — the endpoint must be on the host and port that served the manifest, or a subdomain of that host on that port, and must not carry userinfo. The anchor is the host the consumer fetched the document from, never the self-asserted domain member inside it, since anchoring to a value the document supplies would let a hostile manifest validate itself. The host match is on a full dot-delimited label boundary, so evil-a.com is not under a.com.
Enforced below the caller, in all three SDKs. The check runs inside the shared endpoint resolver, before the value is returned and before it is cached, so every consumer of that resolver inherits it rather than being trusted to remember. A refused endpoint surfaces as its own verdict, distinct from “no endpoint advertised” and from a transport failure, so a caller classifying retryability reads it as final. The rule is corpus-locked to shared conformance vectors the three SDKs replay, and a conformance guard holds the vectors to the normative text above — the two cannot drift apart silently.
Bounded, not eliminated. The manifest fetch follows redirects, so the party that answers for the document may be one a redirect chose. What that party cannot do is change where the signed call lands: whatever the manifest says, the endpoint it advertises must still anchor to the domain the offer named.
6. Systemic / Arbitrage Threats
Section titled “6. Systemic / Arbitrage Threats”T27: Content arbitrage (buy low, resell high)
Section titled “T27: Content arbitrage (buy low, resell high)”Attack: Entity buys from Exchange A at $0.04, resells via own Exchange at $0.05. Countermeasure (protocol): ResourceIdentity enables detection. fora.json declares authorized providers.
T28: Free content laundering
Section titled “T28: Free content laundering”Attack: Scrape freely available content, package behind paywall as “premium.” Countermeasure: Content fingerprinting against known free content databases.
T29: Synthetic content farming
Section titled “T29: Synthetic content farming”Attack: Generate AI content at near-zero cost, sell as human-authored.
Countermeasure: CoMP’s Text.sourcetype field. Future: C2PA content credentials.
T30: License tier arbitrage
Section titled “T30: License tier arbitrage”Attack: Buy at grounding rate, use for training. Collapses pricing model at scale. Countermeasure: Black box problem. Legal/contractual enforcement only.
T31: Absence-reason existence oracle
Section titled “T31: Absence-reason existence oracle”Attack: An unauthorized caller probes for a resource and reads the absence_reason. Authorization-flavored values (NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) distinguish “no such resource” from “it exists but you may not have it” — confirming existence and the reason for refusal. Broker Resolve (DiscoveryResponse.absence_reason) exposes the same oracle at the broker that DiscoverResources (OfferGroup.absence_reason) does at the Exchange, so hiding it on one surface but not the other still leaks.
Countermeasure (protocol): A scope shortfall is never disclosed. When the requester’s scopes leave no presentable offer for a resource, the Exchange answers with no offers and no absence reason, exactly as for any resource with nothing to offer, and a Broker adds no reason of its own (the existence-hiding rule on Requester.scopes). No absence or denial reason reports a scope shortfall. For the other reasons, the reason is a hint, not a guarantee: where existence itself must stay hidden, the Exchange/Broker MAY omit absence_reason (leave it unset) rather than reveal an authorization-flavored value, and this applies equally to OfferGroup.absence_reason and DiscoveryResponse.absence_reason. ErrorDetail.message/metadata are held to the same rule (T-LIC-adjacent leakage): servers SHOULD NOT restore the withheld detail there as free text.
Prevention Taxonomy
Section titled “Prevention Taxonomy”| Level | Threat IDs | What It Means |
|---|---|---|
| Preventable at protocol level | T3, T4, T5, T6, T7, T8, T9, T12, T16, T19, T22, T23, T24, T25, T26, T-BRK-1, T-ATT-1, T-ATT-2, T-ATT-3, T-ATT-5, T-DEL-1, T-DEL-2, T-DEL-3, T-DEL-4, T-DEL-6 | Protocol changes make the attack structurally impossible |
| Detectable via reconciliation | T1, T2, T4, T5, T13, T14, T15, T17, T27, T-ATT-4, T-DEL-5 | Three-sided reconciliation catches it |
| Legal/contractual only | T10, T11, T13, T17, T18, T20, T21, T28, T29, T30, T-DEL-5 | Can’t enforce technically |
The Three Lines of Defense
Section titled “The Three Lines of Defense”Line 1: Protocol-level prevention Signed offers, content hashes, agent-bound URLs, idempotency keys, agent-signed request-set proof (request acceptance, verified before idempotency state), holder-bound JWT delegation chain (cnf), RFC 9421 request signatures from every sender (with chain verification on unchanged forwarding), subscription-level quota tracking Makes attacks structurally impossible
Line 2: Three-sided reconciliation CDN logs + Exchange transactions + Usage reports Detects discrepancies that slip past Line 1
Line 3: Legal/contractual enforcement Audit rights, licensing terms, dispute resolution Handles the black-box problem (what agent did internally)7. Attestation-Specific Threats
Section titled “7. Attestation-Specific Threats”T-ATT-1: Forged attestation injection
Section titled “T-ATT-1: Forged attestation injection”Attack: Attacker crafts a fake ResourceAttestation with a fabricated verifier domain and pushes it via CatalogService.PushResources.
Countermeasure (protocol): Exchange verifies that the request’s signer is the provider itself or listed in the provider’s catalog_contributors (from fora.json). Exchange verifies the attestation signature against the verifier’s public key in its WBA directory (the JWK Set at /.well-known/http-message-signatures-directory). FORA has no dedicated verifier role; a verifier publishes under its operator’s role (ROLE_PUBLISHER for a self-attesting publisher, ROLE_EXCHANGE for a standalone vendor). Both checks must pass.
T-ATT-2: Stale attestation exploitation
Section titled “T-ATT-2: Stale attestation exploitation”Attack: Provider updates content (e.g., replaces article with filler) but the catalog retains the old attestation claiming 5000 tokens. Agent pays based on stale claims.
Countermeasure (protocol): attested_at timestamp enables agent freshness checks. Agents reject attestations older than their threshold for the content type (e.g., 4 hours for breaking news). Content hash mismatch (Level 1) triggers auto-credit via the dispute system.
Countermeasure (reconciliation): CDN response size corroborates discrepancies. 800 bytes for a 5000-token claim is auto-creditable regardless of hash match.
T-ATT-3: Verification vendor compromise
Section titled “T-ATT-3: Verification vendor compromise”Attack: Attacker compromises a third-party verifier’s Ed25519 private key and signs fraudulent attestations for arbitrary content.
Countermeasure (protocol): Key rotation via JWKS pattern — keys have not_before / not_after windows. Exchange caches the verifier’s WBA directory with TTL-based refresh (1 hour default). Compromised key can be revoked by removing it from the verifier’s WBA directory (the JWK Set at /.well-known/http-message-signatures-directory).
Countermeasure (operational): Exchange monitors attestation patterns per verifier. Sudden changes in claim distributions trigger investigation.
T-ATT-4: Claims inflation attack
Section titled “T-ATT-4: Claims inflation attack”Attack: Provider self-attests inflated estimated_quantity: 10000 for a 500-word article, computes a valid content hash for the actual (short) content, and prices based on the inflated count.
Countermeasure (protocol): CDN response size is the corroboration signal. CDN log showing a 2KB response for a 10000-token claim is auto-creditable. Agent files dispute with INCOMPLETE_CONTENT reason; Tier 1 auto-resolution via CDN corroboration.
Countermeasure (pattern): Provider flagged for investigation when token discrepancy rate exceeds 2%. Graduated consequences apply.
T-ATT-5: Catalog contributor spoofing
Section titled “T-ATT-5: Catalog contributor spoofing”Attack: Unauthorized party registers a domain similar to a legitimate verifier (e.g., doub1everify.com) and pushes attestations claiming to be a trusted vendor.
Countermeasure (protocol): catalog_contributors in fora.json uses exact domain matching. The provider must explicitly list the contributor’s domain. The Exchange fetches the WBA directory from the exact verifier domain in the attestation and verifies the signature against that domain’s keys (the JWK Set at /.well-known/http-message-signatures-directory).
8. Delegation and Authorization Threats
Section titled “8. Delegation and Authorization Threats”T-DEL-1: Delegation token theft
Section titled “T-DEL-1: Delegation token theft”Attack: Attacker steals a delegation token (e.g., via log exfiltration, memory dump, or network interception) and attempts to use it to access resources within the token’s scope.
Countermeasure (protocol): Holder binding neutralizes theft. A delegation token is bound to the holder’s key (via cnf.jkt on the JWT), and verification requires the key the request proves possession of — the key that signs the HTTP request (RFC 9421) on a direct call, the key the agent’s AgentAcceptance verifies under on a purchase a Broker relays — to equal the bound key (see Verification). A thief who steals the token bytes but not the private signing key cannot produce a valid request — the token is not bearer-usable, so theft alone grants nothing. Short expiry, the aud claim that names the one Exchange a token is for, and withdrawal of the issuer’s configured key are defense-in-depth that bound the blast radius in the residual case where the holder’s signing key is also compromised. This is why T-DEL-1 is listed as structurally preventable rather than merely mitigated.
T-DEL-2: Delegation scope escalation
Section titled “T-DEL-2: Delegation scope escalation”Attack: Attacker attempts to widen permissions by manipulating the delegation chain (e.g., editing a child JWT to escalate from ai-search to ai-train, or splicing in a forged link).
Countermeasure (protocol): The delegation chain narrows only, and is cryptographically linked. Each delegation JWT is Ed25519-signed, and a child’s scope MUST be covered by its parent’s (verified) — it can narrow, never widen. The chain-linkage invariant requires each token to be signed by the key the parent named in cnf, and each child carries its complete parent inside its own signed fora_parent claim, so editing a link breaks that link’s signature and splicing a different parent breaks the jwk-to-cnf.jkt match. A claim the profile does not list is refused rather than ignored, so an attacker cannot smuggle a restriction past a verifier that does not understand it. The Exchange verifies the full chain, root first with a configured key, and the key the request proves possession of against the final cnf.jkt, before honouring the delegation.
T-DEL-3: Revocation bypass
Section titled “T-DEL-3: Revocation bypass”Attack: Principal revokes a delegation but cached or stored tokens continue to work until their expires_at timestamp.
Countermeasure (protocol): Short exp (2-4 hours for standard access) limits the window of continued access; the verifier allows 60 seconds of clock skew and no more. Trust is configured per subscription record, so removing the issuer’s key from that record refuses every chain it signed at the next request. Per-token revocation through revocation_uri is not part of this profile.
T-DEL-4: Signer impersonation and forged forwarding
Section titled “T-DEL-4: Signer impersonation and forged forwarding”FORA’s Broker never forwards an agent’s request unchanged: it originates discovery queries and re-packages purchases, and signs each alone. A purchase through a Broker is covered by T-BRK-1 and T-BRK-2.
Attack: Attacker claims to be a trusted Broker, or another agent, to gain access or inject misleading provenance — or, where a third party forwards a request unchanged and signs it again, inserts, reorders or drops a signature.
Countermeasure (protocol): Every sender signs its own request (RFC 9421, Ed25519). A Broker originates discovery queries and re-packages execute requests, and signs each alone with its own key, so it cannot claim to be another Broker: the Exchange verifies that signature against the key in the directory the Broker’s own Signature-Agent member names (the JWK Set at {origin}/.well-known/http-message-signatures-directory, the key identified by its RFC 7638 thumbprint, the RFC 9421 keyid). The agent’s identity and consent on a purchase cannot be forged or moved onto another request, because they are the AgentAcceptance signed into the body over the offer signature, requester and idempotency key. Where a party forwards a request unchanged and covers the signature before it, a forged, reordered, dropped or inserted hop breaks a later signature and the request is rejected. See Multi-hop forwarding. A signer cannot act for another requester unless the Exchange admits it as a Broker: a request whose requester.domain names a directory other than the signer’s is refused as unauthenticated with request_auth_failure SIGNATURE_INVALID from any signer the Exchange does not admit, and nothing the sender states, such as its manifest role, admits it (see Direct and relayed requests). The refusal’s typed reason (RequestAuthFailureReason) is deliberately coarse — missing, invalid or stale — so it never tells a forger which check its request failed or how far it got.
T-DEL-5: Scope pollution
Section titled “T-DEL-5: Scope pollution”Attack: Agent declares scopes it doesn’t actually have, hoping the Exchange returns premium content.
Countermeasure (reconciliation + contractual): Requester.scopes is the requester’s own statement, signed and attributable, and the Exchange does not verify it. The Exchange presents an offer for a term only when the stated scopes cover the term’s LicenseTerm.scopes, which hides the term from a requester that does not state the scope but does not stop one that states it falsely, so a false statement is detected and settled rather than prevented. For every scoped term an account bought, the transaction record holds the Exchange-signed offer and the agent’s acceptance of it, attributed to the signing key, its directory and its account. Reconciling those purchases against what the term’s issuer actually granted that account, such as a subscription roster, a revenue-share agreement or an enterprise grant, exposes a scope the agent never held, and the account relationship and the term’s contract settle it. An Exchange that needs prevention MAY require a verified delegation that grants a term’s scopes before it presents the term (Delegation), and answers a requester without one as if the term did not exist. Verifying stated scopes is deferred, not designed out.
T-DEL-6: Quota gaming via multiple tokens
Section titled “T-DEL-6: Quota gaming via multiple tokens”Attack: Agent obtains multiple delegation tokens for the same subscription (e.g., by requesting new delegations from different intermediaries) to exceed per-subscription quotas.
Countermeasure (protocol): Exchange tracks quotas by subscription_id, not by delegation token. All tokens referencing the same subscription share one counter. Multiple tokens for the same subscription do not multiply the quota. Rate limiting is applied at the subscription level regardless of how many delegation tokens reference it.
9. Licensing-Layer Threats
Section titled “9. Licensing-Layer Threats”T-LIC-1: SSRF / phishing via License.uri
Section titled “T-LIC-1: SSRF / phishing via License.uri”Attack: A malicious publisher pushes a term whose License.uri points at an internal address — http://169.254.169.254/latest/meta-data/… (cloud metadata), http://localhost:6379/, or file:///etc/passwd. An agent platform that auto-fetches the document to “read the license before use” (REFERENCE_ONLY requires this) issues a server-side request into its own infrastructure — leaking cloud credentials, reaching internal services, or reading local files. A variant points uri at attacker-controlled HTTPS to harvest the fetcher’s identity/IP or serve misleading terms.
Countermeasure (protocol): License.uri is deliberately never rejected at ingest (data-labels TDL identifiers use non-URL schemes) — but “do not reject” is not “blindly fetch.” A consumer that resolves uri MUST: (a) allowlist schemes (https + known TDL schemes); (b) block loopback, link-local, private, and cloud-metadata IP ranges, resolving-then-checking to defeat DNS rebinding; (c) fetch client-side through an egress proxy, never from the Exchange’s privileged network; (d) treat the fetched bytes as untrusted content. The Exchange applies the same rules when it fetches to pin uri_digest (T-LIC-4).
Same class — the SDK’s directory fetch. The same SSRF discipline applies to the WBA / well-known directory fetches the SDKs perform to resolve a signer’s keys: the directory host is derived from a caller-supplied Signature-Agent and fetched before the ed25519 signature check, so an unauthenticated caller controls the fetch target (pre-auth SSRF). Defense is layered, and the layers have distinct owners:
- In-code (SDK, primary). The SDKs ship a safe-by-default SSRF-guarded transport, and it is two checks, not one. The address pin is connection-level: it resolves-then-checks every candidate address against a shared reserved-range corpus and pins the checked IP to defeat rebinding, on a maintained HTTP client (
net/http/httpx/undici). The scheme allowlist is http(s)-only on the initial URL and every redirect hop, and it is deliberately not part of the connector — by the time a connection-level hook runs, the URL has been reduced to a host and a port and the scheme is no longer visible. Go and Python wrap the transport, so one object carries both (SSRFGuardinsideNewGuardedTransport,ssrf_guard()insideguarded_client). TypeScript composesssrfGuard()as the undici connector and states the scheme rule beside it, inrequireScheme(). Because both ship in the artifact, their correctness cannot depend on deployment — and a caller who replaces the dial replaces the address pin with it, so the scheme check sits above the dial in the client tier, where no injected transport can remove it. - Egress boundary (operator, recommended defense-in-depth). Operators SHOULD additionally run any role that resolves directories from request-supplied hosts behind restricted egress (an egress proxy or network policy that cannot route to RFC1918 / link-local / metadata) and IMDSv2 with hop-limit 1. This neutralizes the entire class regardless of any in-code gap — but it is defense-in-depth, not a substitute for the SDK guard, which remains the primary control for an OSS artifact that runs in deployments the maintainers do not control.
Which fetches this covers, and where it does not reach yet. The rule is the URL’s provenance, not what is being fetched. A request-derived host is one another party chose — it arrives in a header, or on a signed message — so it takes the guarded transport; with one exception, named below, that is a gap rather than a choice. A fixed, operator-chosen address takes the plain transport deliberately: the operator rather than an attacker picked it, and an on-prem directory may legitimately sit on a private address a guard would refuse. Where each fetch stands today:
| Fetch | URL comes from | Go | TypeScript | Python |
|---|---|---|---|---|
WBA directory (Signature-Agent) | the request | guarded | guarded | guarded |
Registration requirements (RegisterRequest.exchange) | the request | guarded | guarded | guarded |
Exchange endpoint (Offer.exchange) | the request | guarded | plain | plain |
| Well-known JWKS | operator configuration | plain | plain | plain |
The last row is the deliberate one — nobody but the operator can point that URL. The third row is a known gap, not a decision: that host comes off a signed offer, so by the rule above it belongs on the guarded transport in every language, and the two ports have not caught up. It is tracked separately.
What to do about it today. TypeScript and Python require the application to supply the endpoint resolver — neither client builds one for you — so the remedy sits at a construction site you already own: pass the guarded transport explicitly, guardedFetchFromEnv() in TypeScript or guarded_client() in Python, rather than letting that resolver take its default. Conversely, a deployment that needs the guarded posture on a configured URL, or the plain one on a request-derived host, injects its own transport or sets SKIP_SSRF / ALLOW_INSECURE.
T-LIC-2: Silent restriction downgrade
Section titled “T-LIC-2: Silent restriction downgrade”Attack: A publisher forbids training (Restriction{kind: FUNCTION, prohibited: ["ai-train"]}) but the consumer does not recognize a token, or the publisher forgot to mark the restriction binding. The consumer treats it as advisory and proceeds — silent infringement with plausible deniability.
Countermeasure (protocol): Restriction.advisory is fail-closed by default (it inverts the COSE-crit opt-in). A restriction is binding unless advisory = true; a consumer that cannot evaluate every token in a binding restriction MUST decline the term. A forgotten flag therefore fails safe — publishers opt into leniency explicitly and never lose protection by omission.
T-LIC-3: Scope-gating escalation
Section titled “T-LIC-3: Scope-gating escalation”Attack: Term visibility is gated by scope coverage: the scopes in the requester’s signed request must cover LicenseTerm.scopes (AND-semantics, empty = public). Two failure modes: (a) a publisher forgets scopes on a premium term → it defaults to public and leaks for free; (b) two implementations match scopes differently — naive prefix matching lets sub: cover subscription:premium, or dist:* is read as covering dist:US:internal on one Exchange but not another → an agent’s grant is read as covering a term it should not.
Countermeasure (protocol): Scope matching follows the single normative rule defined in Scope Matching, and the same rule applies to both Requester/delegation scopes and LicenseTerm.scopes — there is exactly one matching algorithm: matching is segment-wise (:-delimited), never substring/prefix; a granted * segment matches one level and a terminal * matches all remaining segments; there is no implicit prefix match and a grant narrower than the requirement does not cover it. This is the authoritative string-segment definition. An Exchange MAY implement coverage however it likes (e.g. a compiled matcher rather than literal string comparison), but the result MUST be identical to the segment-wise definition — an alternate implementation is conformant only if it produces the same verdicts, never a second, divergent rule. So the example failures (sub: covering subscription:premium, or one Exchange reading dist:* as covering dist:US:internal and another not) are excluded by construction. empty = public stays the explicit, documented default; backfilling a sentinel scope (e.g. r:all) is not required — it only relocates the “forgot to set it” failure and forces every publisher to issue an extra scope grant, without adding enforcement. (cf. OAuth 2.0 and AWS IAM: exact-match with explicitly-defined wildcard expansion, no implicit hierarchy.)
T-LIC-4: Referenced-document swap
Section titled “T-LIC-4: Referenced-document swap”Attack: A term carries a License.uri; the agent fetches and trusts the document. The publisher (or a MitM on the fetch) later changes the bytes at uri — which is just a URL — so the agent’s understanding of the terms silently diverges from what was offered. immutable: true makes this worse (the agent caches and stops re-checking), but a mutable uri is swappable too.
Countermeasure (protocol): License.uri_digest pins the document as method:hexdigest and is required whenever uri is present — any semantics, mutable or not. The Exchange pins it at ingestion (computing it over the safely-fetched document per T-LIC-1, or accepting a publisher-supplied value for non-HTTP schemes). Because the entire Offer is signed, the digest is tamper-evident; the agent verifies the bytes it fetches against it and refuses on mismatch.
Comparison to Ad-Tech Fixes
Section titled “Comparison to Ad-Tech Fixes”| Ad-Tech Fix | Year | FORA Equivalent | Status |
|---|---|---|---|
| ads.txt | 2017 | fora.json (authorized providers) | In proto |
| sellers.json | 2019 | Exchange identity in ResourceResponse | In proto |
| SupplyChain Object | 2019 | exchange_signature on Offer | In proto |
| ads.cert | 2020 | exchange_signature + content_integrity_hash | In proto |
| MRC Viewability Standard | 2014 | A metered charge fixed at purchase (estimate × rate), never re-measured | Design principle |
| Open Measurement SDK | 2018 | Three-sided reconciliation | In protocol flow |