Trust Anchor API Reference
The Trust Anchor (TA) provides the OpenID Federation protocol endpoints. These are public endpoints consumed by federation participants.
Base URL: https://federation.example.com/ (or http://localhost:8080 for development)
OpenID Federation Endpoints
Entity Configuration
GET /.well-known/openid-federation
Returns the Trust Anchor’s entity configuration as a signed JWT.
Response:
Content-Type:
application/entity-statement+jwtBody: Signed JWT
JWT Payload Example:
{
"iss": "https://federation.example.com",
"sub": "https://federation.example.com",
"iat": 1705315200,
"exp": 1736851200,
"jwks": {
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"kid": "key-1",
"use": "sig",
"alg": "ES256"
}
]
},
"metadata": {
"federation_entity": {
"federation_fetch_endpoint": "https://federation.example.com/fetch",
"federation_list_endpoint": "https://federation.example.com/list",
"federation_resolve_endpoint": "https://federation.example.com/resolve",
"federation_trust_mark_status_endpoint": "https://federation.example.com/trust_mark_status",
"federation_trust_mark_list_endpoint": "https://federation.example.com/trust_mark_list",
"federation_trust_mark_endpoint": "https://federation.example.com/trust_mark",
"federation_historical_keys_endpoint": "https://federation.example.com/historical_keys"
}
},
"trust_marks": [
{
"trust_mark_type": "https://example.com/trustmarks/ta",
"trust_mark": "eyJ..."
}
]
}
Example:
curl https://federation.example.com/.well-known/openid-federation
List Subordinates
GET /list
Returns a list of subordinate entity IDs registered with this Trust Anchor.
Query Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Filter by entity type (see below) |
|
boolean |
Only return entities with at least one trust mark |
|
string |
Only return entities with this specific trust mark type |
|
boolean |
Only return intermediate authorities (federation_entity) |
Entity Types:
openid_provider- OpenID Providers (OPs)openid_relying_party- Relying Parties (RPs)federation_entity- Intermediate Authorities (IAs)
Response (200 OK):
[
"https://example-rp.com",
"https://example-op.com",
"https://other-entity.com"
]
Examples:
# List all subordinates
curl https://federation.example.com/list
# List only OpenID Providers
curl "https://federation.example.com/list?entity_type=openid_provider"
# List entities with a specific trust mark
curl "https://federation.example.com/list?trust_mark_type=https://example.com/trustmarks/member"
# List only intermediate authorities
curl "https://federation.example.com/list?intermediate=true"
Fetch Subordinate Statement
GET /fetch
Fetches the subordinate statement for a specific entity.
Query Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Required. Entity ID to fetch |
|
string |
Optional issuer filter |
Response (200 OK):
Content-Type:
application/entity-statement+jwtBody: Signed subordinate statement JWT
JWT Payload Example:
{
"iss": "https://federation.example.com",
"sub": "https://example-rp.com",
"iat": 1705315200,
"exp": 1736851200,
"jwks": {
"keys": ["...key data..."]
},
"metadata": {
"openid_relying_party": {
"redirect_uris": ["https://example-rp.com/callback"]
}
},
"metadata_policy": {}
}
Response (404 Not Found):
{
"error": "not_found",
"error_description": "No statement for https://example-rp.com"
}
Example:
curl "https://federation.example.com/fetch?sub=https://example-rp.com"
Resolve Entity
GET /resolve
Resolves an entity through the trust chain to the Trust Anchor, performing tree-walking and policy application.
Query Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Required. Subject entity ID to resolve |
|
string |
Trust anchor(s) to resolve against (can be repeated) |
|
string |
Filter to specific entity types (can be repeated) |
Response (200 OK):
Content-Type:
application/resolve-response+jwtBody: Signed resolution response JWT
JWT Payload Example:
{
"iss": "https://federation.example.com",
"sub": "https://example-rp.com",
"iat": 1705315200,
"exp": 1705401600,
"metadata": {
"openid_relying_party": {
"redirect_uris": ["https://example-rp.com/callback"],
"response_types": ["code"]
}
},
"trust_chain": [
"eyJ...(entity config of example-rp.com)...",
"eyJ...(subordinate statement from TA)...",
"eyJ...(TA entity config)..."
]
}
The trust_chain array contains:
Subject’s entity configuration (self-signed)
Subordinate statement from the TA (or intermediate)
TA’s entity configuration
Per OpenID Federation §4.3, the same chain also rides in the JWS header
of the resolve response as the trust_chain header parameter. Clients
may read either form; the header lets them short-circuit chain resolution
without parsing the payload.
Examples:
# Resolve an entity
curl "https://federation.example.com/resolve?sub=https://example-rp.com&trust_anchor=https://federation.example.com"
# Resolve with specific entity type
curl "https://federation.example.com/resolve?sub=https://example-op.com&entity_type=openid_provider&trust_anchor=https://federation.example.com"
# Resolve with multiple entity types
curl "https://federation.example.com/resolve?sub=https://example-op.com&entity_type=openid_provider&entity_type=federation_entity&trust_anchor=https://federation.example.com"
Trust Mark Endpoints
Get Trust Mark
GET /trust_mark
Retrieves a specific trust mark JWT for an entity.
Query Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Required. Trust mark type URL |
|
string |
Required. Subject entity ID |
Response (200 OK):
Content-Type:
application/trust-mark+jwtBody: Signed trust mark JWT
JWT Payload Example:
{
"iss": "https://federation.example.com",
"sub": "https://example-rp.com",
"iat": 1705315200,
"exp": 1736851200,
"trust_mark_type": "https://example.com/trustmarks/member",
"ref": "https://github.com/example/verification"
}
Response (404 Not Found):
{
"error": "not_found",
"error_description": "Trust mark not found."
}
Example:
curl "https://federation.example.com/trust_mark?trust_mark_type=https://example.com/trustmarks/member&sub=https://example-rp.com"
List Trust Mark Holders
GET /trust_mark_list
Returns a list of entity IDs that have been issued a specific trust mark type.
Query Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Required. Trust mark type URL |
Response (200 OK):
[
"https://example-rp.com",
"https://other-entity.com",
"https://federation.example.com"
]
Example:
curl "https://federation.example.com/trust_mark_list?trust_mark_type=https://example.com/trustmarks/member"
Validate Trust Mark Status
POST /trust_mark_status
Validates a trust mark JWT and returns its current status.
Request:
Content-Type:
application/x-www-form-urlencoded
Form Parameters:
Parameter |
Description |
|---|---|
|
The trust mark JWT to validate |
Response (200 OK):
Content-Type:
application/trust-mark-status+jwtBody: Signed status response JWT
JWT Payload Example (Active):
{
"iss": "https://federation.example.com",
"iat": 1705315200,
"status": "active",
"sub": "https://example-rp.com",
"trust_mark_type": "https://example.com/trustmarks/member"
}
Status Values:
active- Trust mark is valid and not revokedrevoked- Trust mark has been revoked by the TAexpired- Trust mark JWT has expiredinvalid- Trust mark signature verification failed or unknown issuer
Example:
# First, get a trust mark
TRUST_MARK=$(curl -s "https://federation.example.com/trust_mark?trust_mark_type=https://example.com/trustmarks/member&sub=https://example-rp.com")
# Then validate it
curl -X POST https://federation.example.com/trust_mark_status \
-d "trust_mark=$TRUST_MARK"
Historical Keys
GET /historical_keys
Returns a signed JWT containing the Trust Anchor’s historical (expired/rotated) keys. This allows verification of old signatures after key rotation.
Response (200 OK):
Content-Type:
application/jwk-set+jwtBody: Signed JWK Set JWT
JWT Payload Example:
{
"iss": "https://federation.example.com",
"iat": 1705315200,
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"kid": "old-key-1",
"use": "sig",
"alg": "ES256",
"exp": 1704067200
},
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"kid": "old-key-2",
"use": "sig",
"alg": "ES256",
"exp": 1701388800,
"revoked": {
"revoked_at": 1701388800,
"reason": "superseded"
}
}
]
}
Key Revocation Reasons:
unspecified- No specific reason givencompromised- Key has been compromisedsuperseded- Key replaced by a newer key
Example:
curl https://federation.example.com/historical_keys
Other Endpoints
Entity Collection
GET /collection
Returns a list of all entities discovered in the federation tree, per the Entity Collection Endpoint specification (draft 00).
This endpoint reads pre-populated data from Redis. The data is populated by running the
inmor-collection CLI tool, which walks the federation tree from a trust anchor and
stores entity information. See Entity Collection CLI (inmor-collection) for details.
Request Parameters:
All parameters are optional and passed as query parameters.
Parameter |
Description |
|---|---|
|
Repeatable. Filter to entities that include the given Entity Type. Multiple values combine with OR. |
|
Repeatable. Filter to entities that have a verified Trust Mark of the given type. Multiple values combine with AND. |
|
The Trust Anchor the collection was built from. When omitted it
defaults to the collected Trust Anchor; a value that does not match it
returns |
|
Free-text filter. Matched case-insensitively as a substring against the entity_id and any UI display names. |
|
Maximum number of entities in the response. Defaults to 100 and is capped at 500. |
|
Opaque pagination cursor. Pass the |
Filters of different kinds combine with AND.
Response (200 OK):
Content-Type:
application/json
{
"entities": [
{
"entity_id": "https://op.example.com",
"entity_types": ["openid_provider", "federation_entity"],
"ui_infos": {
"openid_provider": {
"display_name": "Example OP",
"logo_uri": "https://op.example.com/logo.png"
},
"federation_entity": {
"display_name": "Example Organization"
}
},
"trust_marks": [
{"id": "https://ta.example.com/tm/member", "trust_mark": "eyJ..."}
]
},
{
"entity_id": "https://rp.example.com",
"entity_types": ["openid_relying_party", "federation_entity"],
"ui_infos": {
"openid_relying_party": {
"display_name": "Example RP"
}
}
}
],
"next": "aHR0cHM6Ly9ycC5leGFtcGxlLmNvbQ",
"last_updated": 1770983002
}
The next field is present only when more results are available beyond this
page; pass it back as the from parameter to retrieve them.
Response Fields:
Field |
Description |
|---|---|
|
Array of entity objects discovered in the federation tree |
|
The entity identifier (URL) |
|
Array of entity types ( |
|
Optional. UI information per entity type (display_name, logo_uri, policy_uri) |
|
Optional. Array of trust marks attached to the entity |
|
Optional. Opaque pagination cursor; present only when more results exist |
|
Unix timestamp of the last collection walk |
Example:
curl https://federation.example.com/collection
If no collection data has been populated yet, the response will be an empty entity list
with last_updated: 0.
Errors:
Error responses are JSON objects with error and error_description fields.
Error |
Status |
Cause |
|---|---|---|
|
400 |
The request used a query parameter the endpoint does not support |
|
400 |
A malformed parameter – a bad |
|
404 |
The |
Index Page
GET /
Returns a simple index page confirming the server is running.
Response (200 OK):
Index page.
Error Responses
All error responses follow this format:
{
"error": "error_code",
"error_description": "Human-readable description"
}
Common Error Codes:
not_found- Resource not foundinvalid_request- Missing or invalid parametersinvalid_trust_chain- Trust chain build, signature, temporal, or policy validation failed.error_descriptioncarries a precise reason in two cases that the resolver propagates verbatim:Leaf entity-configuration verification failures detected at the start of the walk (bad self-signature, missing
jwks,iss/submismatch under §3.1, or an unknowncritentry under §3.1.1 – the offending claim name appears in the message).Metadata-policy merge failures from the
oidfed_metadata_policycrate (§6.1.3.2), including the offending critical operator name whenmetadata_policy_critrejects it.
Other chain-walking failures (subordinate-statement signature, constraint violation, permitted/excluded-subtree mismatch) cause the walker to skip the offending authority and try another branch. If no chain reaches a trust anchor after exploring all branches, the response is the generic
"Failed to find trust chain"– the per-authority reasons are recorded in the server log rather than surfaced to the client.server_error- Internal server error
Chain constraints (§6.2)
Subordinate Statements can carry a constraints object that restricts
which entities may appear below the SS subject in the chain. inmor parses
and enforces all four standard constraint shapes during trust-chain
walking:
max_path_length(§6.2.1) — the SS rejects any chain where the leaf is more thanmax_path_lengthentities below the SS subject. inmor also retains a fixed depth backstop (MAX_RESOLVE_DEPTH = 10).permitted_subtrees(§6.2.2) — array of URL prefixes. The resolve subject MUST be a subordinate of at least one entry. Matching is URL-component-aware: scheme + host case-insensitive, port (or scheme default) equal, path is a prefix at a path-segment boundary, trailing slashes normalized. A subtree containing a query or fragment is treated as malformed.excluded_subtrees(§6.2.2) — array of URL prefixes the resolve subject MUST NOT be a subordinate of.allowed_entity_types/allowed_leaf_entity_types(§6.2.3) — every entity type the subject declares (top-level keys of itsmetadata) MUST appear in the allowlist (subset semantics). A subject that declares even one disallowed type is rejected, so an entity cannot mix an allowed type with a disallowed one to slip through. A subject with no declared entity types is also rejected when the allowlist is set (fail-closed: a missing or emptymetadatacannot bypass the restriction). Theleafvariant applies only when the subject is the chain leaf.
A violation by any single Subordinate Statement causes the walker to skip that authority and try sibling branches. The conjunction across all superiors falls out of these per-SS checks against the same leaf.
Example — a Subordinate Statement issued by an intermediate that wants to scope its subordinates to one URL subtree and limit chain depth:
{
"iss": "https://intermediate.example",
"sub": "https://leaf.example",
"constraints": {
"max_path_length": 1,
"permitted_subtrees": ["https://leaf.example/"],
"excluded_subtrees": ["https://leaf.example/banned/"],
"allowed_entity_types": ["openid_relying_party", "federation_entity"]
}
}
Trust Mark Delegation (§7.2)
A Trust Mark Owner can delegate authority to issue marks of a given type
to one or more Trust Mark Issuers. Inmor enforces this on the consume
side – when /resolve verifies a subject’s trust marks, marks carrying a
delegation claim are validated against the owner’s keys pinned by the
Trust Anchor itself in its Entity Configuration’s trust_mark_owners
claim. Owner keys discovered any other way (e.g. self-asserted by the
delegation’s claimed issuer) are NOT trusted – the binding owner-to-keys
must be anchored in the TA so the federation has a single authoritative
source for who owns what.
Configure pinned owners in localsettings.py:
TA_TRUST_MARK_OWNERS = {
"https://refeds.org/sirtfi": {
"sub": "https://refeds.org",
"jwks": {
"keys": [
{"kty": "RSA", "kid": "owner-key-1", "n": "...", "e": "AQAB"}
]
},
},
}
The regenerate-entity command validates this dict strictly and fails fast on any malformed entry so the TA never publishes a half-baked owner record. Each entry must satisfy:
the map key (the trust mark type) must be an
http/httpsURL with a host – values likenot-a-urlor non-http schemes are rejected;submust likewise be anhttp/httpsURL with a host, not merely a non-empty string;jwksmust contain a non-emptykeysarray, and each key must carrykidandkty.
Recognition is additive: a trust mark type is recognised when either
trust_mark_issuers lists it OR trust_mark_owners pins an owner for
it. When the owner is pinned, the owner is the authority and the
trust_mark_issuers gate is bypassed – a valid delegation suffices.
When no owner is pinned, the existing trust_mark_issuers recognition
gate is the only authority.
The delegation gate is applied uniformly to both TA-issued and externally- issued marks. The failure matrix:
Owner pinned? |
Delegation present? |
|
Decision |
|---|---|---|---|
No |
No |
– |
Existing |
No |
Yes |
– |
The delegation cannot be validated against any anchored owner. The
resolver logs a warning and falls through to the
|
Yes |
Yes |
– |
Validate the delegation JWT against the pinned owner’s keys.
Required: signature, |
Yes |
No |
Yes |
Direct issuance by the owner. Accept. |
Yes |
No |
No |
The pinned owner did not authorize this issuer to mint marks of this type. Drop. |
delegation.exp is enforced via the JWT temporal claims check but is
NOT folded into the resolve response’s exp claim. The response
exp remains min(chain exp, mark exp); stale delegations are
caught on the next resolve.
Signed JWKS URIs (§5.2.1)
When an Entity Configuration sets signed_jwks_uri, inmor’s JWKS
resolver fetches the URL and treats the response body as a JWT whose
payload contains a JWKS. The JWT MUST be signed by a key that appears in
that same inner JWKS (self-signing, mirroring the entity-configuration
pattern). Verification covers signature, exp / nbf / iat,
kid resolution against the inner JWKS, and the crit allowlist.
Preference order when multiple key sources are present:
Inline
jwksclaim — used directly without a network round-trip.signed_jwks_uri— fetched and verified.jwks_uri— fetched, no signature on the response itself; this is the unsigned fallback path.
If signed_jwks_uri fetch or verification fails, the resolver logs a
warning and falls back to jwks_uri when one is configured. This
preserves resilience during signed-JWKS rollout but does introduce a
documented downgrade-attack surface: a network attacker who can disrupt
the signed path AND tamper with the unsigned path can force the
downgrade. Operators that want strict-signed-only behavior should omit
the plain jwks_uri.
Verified signed-JWKS responses are not cached today. A Redis cache
parallel to inmor:jwks_cache:* (keyed by the URI hash, TTL bounded by
the inner JWT’s exp) is tracked as future work.
Transient-error retry semantics (§10.5)
Outbound federation fetches (Entity Configurations, Subordinate Statements, JWKS) classify upstream failures into two categories:
Transient — HTTP 5xx, HTTP 429, connection-level errors (TCP reset, connect timeout, DNS failure).
get_queryretries up toFETCH_MAX_RETRIES = 2times with exponential backoff starting at 250 ms, capped at 1 s. When the upstream provides aRetry-Afterheader, the resolver honours it (capped at the backoff ceiling).Permanent — HTTP 4xx other than 429, SSRF gate denial, body cap exceeded, malformed UTF-8. Not retried.
The chain walker also enforces a 15-second wall-clock budget for the
combined chain-fetch phase of a /resolve request. Exceeding it is
treated as transient.
At the /resolve boundary:
Transient failure → HTTP 503
Service Unavailablewith aRetry-Afterheader (10 s default, or the upstream’s value if shorter) and a JSON body{"error": "temporarily_unavailable", "error_description": "..."}. Clients should back off and retry.Permanent failure → existing HTTP 400 with
{"error": "invalid_trust_chain", ...}.
Critical-claim handling
Per OpenID Federation §3.1.1, an issuer may attach a crit claim listing
claim names that recipients MUST understand. The Trust Anchor enforces this
inside verify_jwt_with_jwks against a static allowlist (KNOWN_CLAIMS
in src/lib.rs) of every claim this codebase parses. A statement whose
crit contains an unknown name is rejected. During trust-chain walking
this manifests as the offending authority being skipped (the walker tries
sibling authorities); for an entity’s self-signed configuration the
verification error propagates to the caller.
The allowlist is parser capability, not per-federation policy. Producer-side
per-tenant crit policy on outgoing statements is deferred to the
multi-tenant track tracked in multitenant_plan.md.
Content Types
The Trust Anchor uses standard OpenID Federation content types:
Content-Type |
Used For |
|---|---|
|
Entity configurations and subordinate statements |
|
Trust mark JWTs |
|
Trust mark status responses |
|
Entity resolution responses |
|
Historical keys JWT |
|
List endpoints |