Host identity mapping
Use identity mapping when an already authenticated gateway caller needs a
short-lived, scoped Cohesix ticket derived from an enrolled external identity.
An exchange grants gateway authority only; Root must still admit a write and
the native provider must still prove its outcome. This is the enrollment and
exchange contract for OIDC JWTs, SPIFFE JWT-SVIDs,
Kubernetes service-account JWTs and local kernel effective uids through
providers.identity_mappings in the compiler input. Shipped mappings are
disabled. Enrollment names an exact issuer, one audience, signing algorithm,
public-JWKS SHA-256 allowlist, external subject, required groups, unique
normalized subject, role, paths, provider actions, operation count and TTL.
RS256, ES256 and EdDSA are supported. Tokens cannot provide discovery URLs,
keys, policy, role, lifetime or scope overrides. A write mapping must use the
Queen role and exactly /host/tickets/spec; every allowed action must exist
in the compiler-owned provider registry. Read mappings retain path and role
ownership checks. A mapping does not enable an unavailable provider.
hive-gateway --delegation-key-ref file:/secure/delegation-key
--identity-keysets /etc/cohesix/identity-keysets.json reads a bounded public
key enrollment once at startup. The JSON object maps enabled policy ids to
absolute JWKS file paths, for example {"enterprise":"/etc/cohesix/enterprise.jwks"}.
Duplicate ids, unknown/disabled/local policies, non-regular files, final symlinks,
oversized files and unpinned bytes are refused. Public keys may be distributed
with deployment configuration; the delegation secret remains independently
enrolled. Explicit restart reloads enrollment. Generated mapping or graph
changes invalidate previously mapped tickets.
POST /v1/identity/exchange requires ordinary gateway request authentication,
with exactly mapping_id and credential in its JSON body. It does not require
an existing delegated ticket. The body is limited to 9,216 bytes, the credential
to 8,192 bytes, and concurrent cryptographic verification to four operations.
Verification runs off the async executor. Unknown fields, duplicate required
fields, wrong issuer/audience/groups, forged signatures, unmapped subjects,
future/stale tokens and unsupported remote local-uid assertions fail before
any console or provider request. Public JWKS bytes are pinned; there is no
network discovery. Rotate a pinned keyset by updating compiler enrollment and
restarting with its exact public bytes.
A successful response has schema cohesix-identity-exchange/v1, status=OK,
authoritative=false, identity_class=gateway_enforced, mapping_id,
provider_graph_sha256, credential_sha256, expires_unix_s and ticket.
Every response carries Cache-Control: no-store. HTTP 401 means missing/invalid
request authentication; 400 means malformed or bounded input; 403 means identity
or mapping refusal; 429 means the verification capacity is occupied; 503 means
the clock or verification worker is unavailable. The framework rejects oversized
HTTP bodies before issuance. Audit records retain only credential hashes and
fixed refusal classes, never the raw external credential or issued ticket.
Issuance establishes delegated gateway authority, not admission, native execution,
or Worker completion.
The mapped signing domain is derived from SHA-256 of
cohesix-gateway-mapped-issuer/v1\0 || enrolled-secret, hex encoded and passed
to the existing BLAKE3 ticket issuer. Thus an issued mapped ticket cannot
authenticate directly to the VM or the ordinary delegated verifier even if
the operator reused the underlying secret. Subjects reserve mi- plus
base64url encoding of the first 16 SHA-256 bytes over the serialized tuple
("cohesix-mapped-delegation/v1", policy_sha256, normalized_subject, provider_graph_sha256).
That binding allows the gateway to resolve only the exact current generated
rule. An unknown mapped subject never falls back to ordinary ticket authority.
Both single writes and every line of a batch must name an allowed provider action before quota charging or upstream dispatch. Duplicate action fields, trailing JSON and other control paths are refused. Root remains the complete host-ticket schema/admission validator. Read ownership uses the rule’s normalized subject; ticket accounting uses the original signed token hash. JWT issuer time and expiry are preserved, so exchanging the same signed claims again cannot create a fresh quota identity or extend their lifetime. Quotas use the gateway’s existing live-ticket table; issuance does not claim durable accounting across a gateway restart. Key rotation, missing policy and expiration fail closed.
Local mapping is explicitly host-local. coh identity --mapping ID --local
reads this process’s kernel effective uid. JWT mapping uses
coh identity --mapping ID --jwks /absolute/public.jwks and token bytes on stdin.
Both normally print cohesix-identity-request/v1, a verified proposal with no
capability. An already authorized host issuer may add
--issuer-key-ref file:/secure/delegation-key to issue the same gateway-only
response and write a credential-hash audit record to stderr. Possession of an
issuer secret is privileged; it is never supplied by a remote exchange caller.
Protect this host, its generated mapping and the credential file as issuer
infrastructure. An externally supplied numeric uid is not local authentication.
The Python cohesix.exchange_identity helper accepts the gateway URL, mapping
id, external credential reference, request-auth reference and independently
enrolled provider graph SHA-256. It performs one bounded request, accepts HTTPS
or explicit loopback HTTP, refuses redirects and ambient proxies, and checks
response bindings.
IdentityTicket.ticket is excluded from its representation. Pass it explicitly
to RestBackend(..., delegated_ticket=issued.ticket); there is no implicit token
refresh or mutation retry. cohesix.local_subject() returns only the kernel euid
for host enrollment diagnostics, never a remote identity assertion.
Ticket wire version 2
Mapped issuance uses ticket payload version 2 within the unchanged
cohesix-ticket-<hex-payload>.<64-hex-MAC> envelope and 224-byte maximum.
Only budget ticks/operations/TTL and issued_at_ms use canonical unsigned
LEB128 instead of little-endian fixed-width u64. Each integer is at most ten
bytes; byte ten is at most one. Overflow, truncation and redundant terminal
zero groups are refused. Role, flags, u16-length UTF-8 strings, scope count,
verbs, rates and fixed-width quota fields retain their version-1 layout.
Unknown flag bits, explicit empty scopes/quotas and trailing payload bytes are
noncanonical in version 2. The MAC covers the exact versioned payload.
The shared Rust verifier and Python claim inspector accept both versions.
Ordinary TicketIssuer::issue and generated fixtures still emit version 1;
issue_compact explicitly selects version 2. Re-encoding preserves the decoded
version, so ticket hash/quota identity cannot change during verification.
Python parsing does not verify a MAC. Tickets too large for the unchanged
console bound fail with ELIMIT; multiple scopes are not silently dropped.
This ticket-codec version does not change the manifest schema, generated
ticket defaults, host-ticket request schema or Secure9P framing.