Note
This spec outlines a draft functionality for beta testing. Changes are expected.
1. Abstract
This spec defines the Flamingo Verifier (Verifier), a trusted execution environment (TEE) service. It receives up to eight images or embeddings, a pipeline and a match_strictness, and runs a sandboxed biometric Engine, provided by an Engine Provider, for fraud detection and comparison. If the Engine accepts, the Verifier signs a Flamingo Token over the Engine configuration, the compared entries and the public values of an Authenticator Assertion, if any. If a WIP-106 Trust Anchor Key Token (TAKT) and Authenticator Assertion Token (AAT) are provided, they are also verified. WIP-202 consumes the Flamingo Token to prove the comparison to a Relying Party (RP) without revealing any entry.
2. Motivation
Many Proof-of-Human applications like deep fake prevention or human presence verification require A) running fraud detection models like presentation attack detection (PAD) on a set of input images and B) comparing images using embedding models, e.g., to verify that the person on a stream is the same as the one performing a liveness check on a phone.
Building a privacy preserving system for such image comparisons is very difficult. For example, running the models and comparisons locally on the user’s device is privacy-preserving but it has very low security guarantees: a mobile device’s output is not trustworthy, e.g., a jailbroken device could report that fraud detection and embedding comparison were performed, while simply running a spoofed client that does not perform them at all. Therefore, an RP cannot be sure that the claimed checks have actually been executed. Additionally, fraud detection models’ security relies on obscurity and obtaining the model weights, or even just the model architecture, opens up a wide range of attack vectors.
To achieve verifiability in a privacy-preserving way, this document introduces a general approach that runs fraud detection models and embedding models in an attested Trusted Execution Environment (TEE). This allows users to verify that the TEE operator is not able to access any biometric data, and a signature by the TEE proves to RPs that all claimed checks have been correctly executed. The Verifier image MUST be open source and reproducibly built, so anyone can check that its attested measurement matches the source and that no raw media or embeddings leave the TEE. Fraud detection models cannot be open, so the Verifier runs them as a closed Engine inside a kernel-enforced sandbox that has no network or storage access. The Authenticator chooses the Engine by its hash, from a release manifest the Engine Provider signs.
3. Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
- The Engine is the sandboxed component of the Verifier that performs extraction, fraud detection and comparison. This spec defines none of them.
- The Engine Provider builds the Engine and signs its release manifests. It MAY be the same organization as an Authenticator Provider (WIP-106).
- The host is the machine that runs the Verifier’s enclave and relays all its traffic, on AWS Nitro the parent EC2 instance. The operator runs the host. Neither is trusted beyond availability (Trust Assumptions).
- The Field ($F$), the hash function ($H_t$), the signature scheme (
BabyJubJub-EdDSA-Poseidon2) and thePublicKeyrepresentation MUST follow WIP-100.R(d)reads a 256-bit digestdas a big-endian integer modulo $q$.
3.1 Constants
| Constant | Value | Description |
|---|---|---|
MAX_ENTRIES | 8 | Entries per request. |
MAX_COMPARED | 4 | Maximum compared entries, fixed by the Flamingo Token layout. |
DS_WIP_201 | 127603488023523044162070750169730750022280897185614 (b"WORLD-ID/WIP-201/SIGN") | Domain separator for the Flamingo Token digest. |
DS_WIP_201_ENGINE | 8362622191109606222205468683123474433460185506268139077 (b"WORLD-ID/WIP-201/ENGINE") | Domain separator for engine_config_hash. |
MAX_ATTESTATION_AGE | 10800 | Maximum age of an attestation document at verification, in seconds (3 hours, the validity of a Nitro attestation’s certificate). |
MAX_DEBUG_REPORT | 65536 | Maximum debug_report size, in bytes. |
CHANNEL_DOMAIN | WORLD-ID/WIP-201/CHANNEL | Domain of the request channel. |
CHANNEL_KEY_DOMAIN | WORLD-ID/WIP-201/CHANNEL-KEY\0 | Domain of the channel key commitment, as bytes including the trailing 0x00. |
EAT_PROFILE | https://world.org/eat/flamingo/v1 | The eat_profile claim (RFC 9711 §4.3.2) of a Flamingo Token. |
3.2 Request
The request is a deterministic CBOR map (RFC 8949 §4.2.1) with the fields below. The Authenticator encrypts the entire encoded map to the Verifier (Channel Encoding). Field elements are 32-byte byte strings, the canonical big-endian encoding.
payload is a CBOR byte string that contains a second deterministic CBOR map, the payload fields below. It is nested as bytes so that cdh commits to its exact encoding.
| Field | Type | Description |
|---|---|---|
version | uint | MUST be 1. |
payload | bytes | The encoded payload map; see below. |
aud | Field element | The rpId of the request. |
nonce | Field element | The RP’s nonce for the request, sampled uniformly from the nonzero elements of $F$ with a CSPRNG. |
aat_inputs | map, optional | Inputs for verifying a TAKT and an AAT (Authenticator Assertion). |
payload field | Type | Bounds | Description |
|---|---|---|---|
pipeline | uint | 1..2^16 | Engine-defined pipeline, chosen by the Authenticator among the configurations the RP offers (Engine Selection). |
match_strictness | uint | < 2^8 | Matching strictness level, defined by the Engine Provider for all pipelines. |
hints | bytes | ≤ 1 KiB | Request-level Engine hints, e.g. experiment flags. MUST NOT change any security assumptions or weaken match_strictness; anything that changes the verdict belongs in pipeline. |
entries | array of maps | 1..=MAX_ENTRIES | Inputs to the Engine, each { data, meta }. |
entries[i].data | bytes | An encoded image or an embedding. | |
entries[i].meta | bytes | ≤ 256 bytes | How the Engine reads data, e.g. which frame of a capture it is. |
compare | array of uint | 2..=MAX_COMPARED distinct indices into entries | Order matters: compare[k] becomes compared_entry_hash_k, and pipeline assigns each position its role. |
release_hash | bytes | 32 bytes | The release_hash of the chosen release manifest (Release Manifests), taken from the RP’s offer (Engine Selection). The Authenticator MUST verify that manifest under a trusted Engine Provider key before sending biometrics. |
- The Verifier checks the payload’s shape and bounds, resolves
release_hash,pipelineandmatch_strictnessagainst the loaded release (Release Manifests), and hashes the entriescomparenames, in order, intocompared_entry_hash(Claims). It interprets nothing else and forwards the payload to the Engine. Roles (Credential item, live frame, challenge) are fixed bypipelinethrough positions incompare. - The Verifier MUST reject a request of any other shape, exceeding a bound, or whose
comparerepeats or names an out-of-range index. It MUST reject a sealed request larger than the largestmax_request_bytesof its loaded releases before decrypting it, and one larger than its pipeline’smax_request_bytesafter.
3.3 Authenticator Assertion
aat_inputs is OPTIONAL. Authenticators without an Authenticator Provider that issues TAKTs omit it. If present, it is a map with the fields marked aat_inputs. below, and the Verifier obtains every input of WIP-106 §3.8 as follows:
| WIP-106 §3.8 input | Type | Source |
|---|---|---|
trust_anchor_key | PublicKey | aat_inputs.trust_anchor_key |
takt | bytes | aat_inputs.takt |
aat | bytes | aat_inputs.aat |
now | uint32 | aat_inputs.now |
min_build_version | uint32 | aat_inputs.min_build_version, the RP’s minimum; 0 sets no minimum. |
aud, nonce | uint64, Field element | The request’s aud and nonce. The Verifier MUST reject an aud not less than $2^{64}$. |
cdh | Field element | Computed as R(SHA-256(payload)). |
platform, sec_level, sec_meta | uint8 | Unpacked from the TAKT’s sec_flags. |
authenticator_meta | uint8 | The AAT’s authenticator_meta. |
- Verification. If
aat_inputsis present, every check of WIP-106 §3.8 MUST hold before the Verifier calls the Engine; otherwise it rejects the request. The Verifier is a WIP-106 TEE verifier: the Authenticator authenticates it through the channel attestation (Attestation and Transport), and the RP through thesigning_key_attestation. - Outputs. The public inputs reach the Flamingo Token as
trust_anchor_key_hash,nowandaat_flags(Claims);audandnonceas themselves. The TAKT, the AAT, theassertion_keyandbuild_versionMUST NOT leave the Verifier, andaat_inputsMUST NOT be passed to the Engine. - Without an AAT. The Verifier MUST set
has_aatto1if it verified a TAKT and an AAT and to0otherwise. Withhas_aat = 0,now,trust_anchor_key_hashandaat_flagsMUST be0.
3.4 Response
| Variant | Contents |
|---|---|
Success | token (Flamingo Token), signing_key_attestation (Attestation and Transport), debug_report (optional) |
Failed | error, debug_report (optional) |
- The response MUST be encrypted to the requester and MUST be padded to a fixed length, hiding the outcome and the report size.
erroris implementation-defined and bounded in size; it comes from the Verifier or, passed through, from the Engine.debug_reportis optional Engine output outside the Flamingo Token. The Verifier MUST NOT emit a report larger thanMAX_DEBUG_REPORT. Clients MUST NOT forward it without the user’s explicit consent, and only to the Engine Provider. It SHOULD let the user inspect a report before forwarding it.- The Verifier MUST emit a Flamingo Token only when the Engine returns
Pass.
3.5 Engine
- Each
pipelinedefines the entry layout, how each entry’smetais read, and which checks run. Each pipeline sets the match thresholds that meet eachmatch_strictnesslevel. Fraud detection is fixed bypipelineand does not vary withmatch_strictness. - Each release manifest defines the
match_strictnesslevels independently ofpipeline: every pipeline MUST support every level, a level MUST mean the same in every pipeline, and a higher level MUST be at least as strict for every compared pair. The Engine MUST reject amatch_strictnessthe release manifest does not define. - Engine Provider. The Engine Provider builds the Engine and signs each release manifest (Release Manifests). A Verifier MAY load several Engines. It MUST reject a request unless it loaded the release named by
release_hash. - The Verifier MUST pass the
payloadfields to the Engine unchanged. The Engine MUST returnPass, meaning every check passed and every compared pair clearedmatch_strictness, or an error, either with an optionaldebug_reportand never with scores. This WIP does not specify the encoding.
3.5.1 Isolation
The Engine sees raw biometrics and runs closed, injected models. The Verifier MUST give it no network access, no writable persistent storage, no process creation and no access to the Verifier’s keys, and only one bounded request-response channel. Each request MUST run in a fresh or reset Engine instance, so no data from one request reaches another. The Verifier MUST bound the Engine’s memory and return an error when it is exceeded. Response Timing bounds its runtime and the capacity of its timing channel to the host. See the reference implementation’s sandbox for one way to meet these requirements.
3.5.2 Release Manifests
A release manifest is a JWS (RFC 7515) in compact serialization, signed with ES256 by the Engine Provider’s release key. Its protected header MUST contain alg = ES256, kid and typ = world-id-engine-manifest. Its payload is a JSON object:
| Field | Type | Description |
|---|---|---|
engine_hash | string | SHA-256 of the Engine bundle, hex. |
version | string | The Engine Provider’s label for the release, unique per Engine Provider, for people and logs. |
pipelines | array | Each entry: pipeline (integer), description (string), which states its checks and the meaning of each compare position, response_slots (min and max in milliseconds, count; see Response Timing) and max_request_bytes (integer), the largest sealed request it accepts. |
match_strictness_levels | array | Each entry: match_strictness (integer) and description (string), ascending. |
pipeline values MUST be unique within a manifest, and so MUST match_strictness values.
- Release hash.
release_hashis theSHA-256of the manifest’s JWS Payload (RFC 7515 §2). It excludes the protected header and the signature, so re-signing a release keeps itsrelease_hash. - Loading. The Engine bundle is everything the Engine runs: code, model weights and configuration. The host passes each bundle at start-up, as one artifact, together with its release manifest. The Verifier MUST check that the bundle hashes to the manifest’s
engine_hash, that itspipelineandmatch_strictnessvalues are unique, and that everyresponse_slotshascount >= 1and0 < min <= max, withmin < maxifcount > 1. It MUST NOT let an Engine load anything outside its bundle, and MUST run a request only on the release whoserelease_hashthe request names, with apipelineandmatch_strictnessthat release defines. It verifies no signature and fetches nothing. - Signatures. RPs and Authenticators MUST verify a release manifest under an active release key of an Engine Provider they trust before relying on it.
- Metadata. The Engine Provider MUST expose a JSON file over HTTPS at
https://<FQDN>/.well-known/world-id-engine.json:
| Field | Type | Description |
|---|---|---|
version | integer | MUST be 1. |
provider_id | string | Stable identifier, which RPs MAY allowlist. |
release_keys | array | Each entry: jwk (a P-256 JWK with kid) and status (active, retired or revoked). |
manifests | array | Each entry: manifest (the compact JWS) and status (active, retired or revoked). |
The Verifier never fetches the metadata. RPs compute the engine_config_hash values they accept from it, and Authenticators read pipelines to lay out compare.
3.5.3 Engine Selection
- Offer. The RP lists the configurations it accepts as (
release_hash,pipeline,match_strictness) tuples, in order of preference. How it conveys them is outside this WIP. - Availability. The Verifier MUST advertise the
release_hashvalues of its loaded releases to the Authenticator, alongside its channel attestation. The list is not covered by the attestation; a wrong list only makes requests fail. - Choice. The Authenticator MUST choose the first offered tuple its device supports and the Verifier has loaded, and set
release_hash,pipelineandmatch_strictnessfrom it. - Check. The RP accepts only the
engine_config_hashof a tuple it offered (Reference Verification).
engine_config_hash = H_4(DS_WIP_201_ENGINE; R(release_hash), pipeline, match_strictness) identifies the chosen tuple. The Verifier signs it into the Flamingo Token, and the RP checks it against the tuples it offered.
3.5.4 Response Timing
- Slots. Each pipeline’s
response_slots(Release Manifests) definecountrelease times betweenminandmaxmilliseconds, spaced geometrically: slotiismin * (max / min)^(i / (count - 1))for0 <= i < count, rounded down to whole milliseconds. Withcount = 1, the only slot ismax, and every response takes exactly that long. - Release. The Verifier MUST release each response at the first slot of the request’s pipeline at which the Engine has finished, measured from the request’s arrival on its monotonic clock, whatever the outcome. A request whose Engine has not finished by the last slot fails; the Verifier MUST then terminate and reset the Engine instance.
- Choosing slots. Engine Providers SHOULD set
minat or above the pipeline’s fastest complete run, so early exits, e.g. a rejected input, are released together with completed runs, andmaxabove its 99.9th-percentile run time on the intended hardware, since slower requests fail. - Leakage budget. A response reveals at most $\log_2$
countbits about its run time. Authenticators MUST refuse a release whose pipeline exceeds the slot count they accept before sending biometrics, and SHOULD accept at most 8.
3.6 Flamingo Token
The Flamingo Token is the primary output of a Verifier after a comparison. A Flamingo Token is a CWT (RFC 8392) in an untagged COSE_Sign1, encoded as deterministic CBOR (RFC 8949 §4.2.1).
- The protected header MUST contain
alg(label1) =-65537(Private Use,BabyJubJub-EdDSA-Poseidon2) andkid(label4) = the compressed signing public key. - The signature MUST be a
BabyJubJub-EdDSA-Poseidon2signature per WIP-100 over the signed digest.
3.6.1 Claims
The payload MUST contain exactly these claims, in the bytewise order of their encoded keys.
| Claim | Key | Value |
|---|---|---|
eat_profile | 265 | EAT_PROFILE. |
compared_entry_hash_k, k = 0..3 | -90000 - k | R(SHA-256(entries[compare[k]].data)), or 0 if compare has no position k. |
aud | -90004 | aud from the request. |
nonce | -90005 | nonce from the request. |
trust_anchor_key_hash | -90006 | WIP-106 hash of the trust_anchor_key that signed the verified TAKT, or 0 without an AAT. |
aat_flags | -90007 | platform + 2^8 * sec_level + 2^16 * min_build_version + 2^48 * sec_meta + 2^52 * authenticator_meta: the WIP-106 sec_flags layout with the public min_build_version in place of build_version, and authenticator_meta in bits 52-56; 0 without an AAT. |
has_aat | -90008 | 1 if the Verifier verified an AAT, otherwise 0. |
now | -90009 | now of the verified AAT, or 0 without an AAT. |
engine_config_hash | -90010 | The Engine configuration that ran (Engine Selection). |
aat_flags,has_aatandnoware CBOR uints. All other claims excepteat_profileare 32-byte byte strings, the canonical big-endian encoding of a field element.eat_profileis not in the signed digest and MUST be treated as an untrusted hint.
3.6.2 Signed Digest
The signed message is not the COSE Sig_structure. It MUST be
H_16(DS_WIP_201; now,
compared_entry_hash_0, compared_entry_hash_1, compared_entry_hash_2, compared_entry_hash_3,
aud, nonce, trust_anchor_key_hash, aat_flags, has_aat, engine_config_hash)
per WIP-100 §3.3.2, with $k = 11$. A compared_entry_hash_k without a compared entry at position k is 0.
Four slots remain free.
3.7 Verifiability and Reproducible Builds
The Verifier runs as an AWS Nitro Enclaves image, and its measurement MUST be verifiable from source:
- Source. Each Verifier release MUST name its source by an immutable reference, e.g. a commit hash in a public repository.
- Build. Building that source with the documented toolchain MUST produce a bit-identical Enclave Image File (EIF).
- Measurement. PCR0, PCR1 and PCR2 of that EIF, as AWS Nitro Enclaves computes them, are the Verifier’s measurement.
- Publication. The Verifier operator MUST publish, per release, the source reference, the build instructions and PCR0–2.
- Acceptance. RPs and Authenticators MUST accept only PCR values they trust.
Engines are injected at runtime and bound by release_hash (Engine).
3.8 Attestation and Transport
The Verifier generates a channel key and a Flamingo Token signing key at boot and attests each in its own Nitro attestation document: a COSE_Sign1 chaining to the AWS Nitro Root G1. Its PCR0–2 are the measurement, its timestamp in milliseconds divided by 1000 is the attestation time, and its public_key binds the key below. A document with all-zero PCRs comes from debug mode and MUST be rejected.
| Document | public_key | Used by |
|---|---|---|
| Channel attestation | SHA-256(CHANNEL_KEY_DOMAIN || channel_key) | The Authenticator, to encrypt a request; discarded afterwards. |
signing_key_attestation | The Flamingo Token signing key (kid) | Consumers, with every Flamingo Token. |
-
Channel. Before sending a request, the Authenticator MUST check that the channel attestation:
- is a valid
COSE_Sign1whose certificate chain leads to the AWS Nitro Root G1 and is valid at the current time; - does not have all-zero PCRs, and has PCR0–2 it accepts (Verifiability);
- has
public_key = SHA-256(CHANNEL_KEY_DOMAIN || channel_key)for thechannel_keyit encrypts to; - has a
timestampat mostMAX_ATTESTATION_AGEold.
It then seals
response_key || requesttochannel_key(Channel Encoding), whereresponse_keyis a fresh X-Wing encapsulation key andrequestthe plaintext of Request. - is a valid
-
Response. The Verifier MUST seal the padded response to
response_key. Only the requester holdsresponse_key, so the response is bound to its request; aSuccessis authenticated by its Flamingo Token. -
Replay. The Verifier MUST reject a request whose (
aud,nonce) it has already processed under the samechannel_key, before running the Engine. Thechannel_keyMUST NOT be exported or persisted across boots, so an in-memory set suffices: a reset discards the key along with the set, and no other instance can open a request sealed to it. -
Refresh. The Verifier MUST re-attest its signing key at least every
MAX_ATTESTATION_AGEand return its latestsigning_key_attestation. -
Carrying. A proof or message that relies on a Flamingo Token MUST carry the Flamingo Token’s
signing_key_attestation.
The Engine needs no attestation of its own: the measured Verifier signs its engine_config_hash into each Flamingo Token. One signing_key_attestation covers every Flamingo Token of a boot, so consumers MAY cache it by key. It identifies the Verifier instance, not the user.
3.8.1 Channel Encoding
Both directions use the public-key encryption of WIP-100 §3.9.
- Info.
infoisCHANNEL_DOMAIN || 0x00for a request andCHANNEL_DOMAIN || 0x01for a response. - Request plaintext. The first 1216 bytes are
response_key, an X-Wing encapsulation key; the rest is the request. - Rejection. The Verifier MUST reject a request shorter than 1216 bytes or with an invalid
response_key.
3.8.2 Authenticator Behavior
For each request, the Authenticator MUST, in order:
- Choose a configuration (Engine Selection).
- Verify its release manifest (Release Manifests), including its
response_slots(Response Timing). - Verify the channel attestation (Channel), including PCR0–2 it trusts (Verifiability).
- Seal and send the request (Channel Encoding), and open the response with its
response_key.
It MUST NOT send biometrics if any step fails.
3.9 Flamingo Token Verification
Flamingo Token verification is specified as a component for circuits (e.g. WIP-202); a consumer outside a circuit MUST perform the same checks (Reference Verification). The component MUST introduce these inputs.
| Input | Visibility | Type | Description |
|---|---|---|---|
verifier_key | Public | PublicKey | The Verifier’s signing key, checked against its signing_key_attestation. |
now | Public | uint32 | Current time, in seconds since the Unix epoch. With an AAT it is the now of WIP-106 §3.8. |
aud, nonce | Public | Field element | The RP’s request. |
has_aat | Public | bool | Whether the Flamingo Token carries an AAT result. |
trust_anchor_key_hash | Public | Field element | Hash of an allowlisted trust_anchor_key. |
platform, sec_level, sec_meta, authenticator_meta | Public | uint8 | See WIP-106 §3.6. |
min_build_version | Public | uint32 | Minimum build_version the RP accepts. |
compared_entry_hash | Private | [Field element; 4] | From the Flamingo Token. |
engine_config_hash | Public | Field element | From the Flamingo Token. |
sig | Private | Signature | The Flamingo Token signature. |
The circuit MUST enforce:
- Signature. The Verifier signed the Flamingo Token.
- Compute the signed digest from the inputs.
- Verify
sigunderverifier_keyper WIP-100 §3.5.1.
- Request binding. The Flamingo Token is for this request.
- The digest uses the public
audandnonce.
- The digest uses the public
- Authenticator Assertion. The Flamingo Token carries the AAT result, or none.
has_aatis0or1, and the digest uses it.- The digest uses the public
trust_anchor_key_hash, andhas_aat * nowas the Flamingo Token’snow. - Range-check
platform,sec_level,sec_meta,authenticator_metaandmin_build_versionto their widths, and constrain theuser_presencebits ofauthenticator_metabelow5. - The digest uses
aat_flags = platform + 2^8 * sec_level + 2^16 * min_build_version + 2^48 * sec_meta + 2^52 * authenticator_meta. - When
has_aat = 0,trust_anchor_key_hash,platform,sec_level,sec_meta,authenticator_metaandmin_build_versionMUST be0;nowstays available to the embedding circuit.
- Engine. The Flamingo Token names the Engine configuration that ran.
- The digest uses the public
engine_config_hash. - An embedding circuit MAY take its components as private inputs and recompute it, to constrain them.
- The digest uses the public
The component returns compared_entry_hash. The embedding circuit MUST bind each hash its statement depends on to its own inputs (e.g. the Issuer’s claim or the RP’s challenge).
4. Trust Assumptions
The protocol assumes the isolation, measurement and attestation of AWS Nitro Enclaves are sound. If they fail, neither privacy nor integrity holds. Every party in the table below is trusted only for the listed parts, and a compromise of one only affects those parts, except AWS.
| Party | Trusted for | Not trusted for |
|---|---|---|
| AWS (Nitro Enclaves) | Isolation, measurement and attestation of the enclave. | Nothing: if this fails, neither privacy nor integrity holds. |
| Engine Provider | Engine correctness: fraud detection, matching, and accurate release manifest descriptions. | Privacy: the sandbox keeps the Engine from exporting data. |
| Authenticator Provider | Verifying the Authenticator’s integrity, as stated in the TAKT. | — |
| Authenticator | The source and freshness of the live capture. | — |
| Host and operator | Availability only. | Privacy and integrity: they see only ciphertext, and the Verifier rejects replayed requests (Attestation and Transport). |
Out of scope. Collusion between an RP and the user’s Authenticator, which can then produce any proof the RP accepts, and between an RP, an Engine Provider and the operator, which can only gather more than one response’s worth of timing information through requests the user consents to one by one. Collusion between an Engine Provider and the operator alone is in scope: Isolation and Response Timing bound what they learn per request, and Replay keeps them from repeating a request.
5. Reference Verification
A consumer accepts a Flamingo Token if:
- The
signing_key_attestation:- chains to the AWS Nitro Root G1, with every certificate valid at its
timestamp, and does not have all-zero PCRs; - has PCR0–2 the RP accepts (Verifiability);
- attests the key that signed the Flamingo Token (
kidis only a hint); - has a
timestampat mostMAX_ATTESTATION_AGEold.
- chains to the AWS Nitro Root G1, with every certificate valid at its
- The signature verifies under that key.
- The request matches:
audis the RP’s own;nonceis one the RP issued, has not expired and has not been used before.
- If
has_aat = 0, the RP accepts requests without an AAT. Otherwise the public values pass WIP-106 §4:now,min_build_version,trust_anchor_key_hashand theaat_flagssub-fields. engine_config_hashis the hash of a tuple the RP offered (Engine Selection), from an active release manifest it verified under an active key of a trusted Engine Provider.
6. Rationale
- The Verifier interprets nothing. Roles, frame semantics and thresholds live in the Engine and the consumer. A new capture method or credential format never changes the request or Flamingo Token.
- The AAT is verified in the Verifier. WIP-106 §3.8 requires a verifier that keeps the Private inputs confidential and reports only the Public values; the attested Verifier is one. The Verifier already handles the media, and consumers save one signature verification. As
cdhcovers the exactpayload, the AAT vouches for exactly the compared input. - The AAT is optional. Not every Authenticator has an Authenticator Provider that issues AATs, and requiring one would exclude them. The RP decides per request whether it accepts a Flamingo Token without one.
- Public values as claims. The Flamingo Token carries the Public values of WIP-106 §3.8, so a consumer applies the same RP policy either way. Consumers read them directly; the Verifier is trusted to derive them from one request, as it is for the comparison.
build_versionstays private as WIP-106 requires;min_build_versiontakes its place. SHA-256over received bytes. RPs and Issuers compute or check these hashes outside circuits, whereSHA-256is available everywhere. They cover the bytes the Verifier received, not embeddings derived from them, which no other party could reproduce.- Signature over claim values, not CBOR bytes. The signature covers a Poseidon2 hash of the claim values, so a circuit verifies the Flamingo Token without parsing CBOR. Each claim keeps its own field element; the wider
t=16permutation costs less than unpacking packed claims in-circuit. - No Verifier time. Most TEEs have no trusted clock, so the Verifier does not timestamp the Flamingo Token; the RP’s single-use
noncemakes it fresh. The Authenticator statesnowfor the AAT: the Verifier checks the AAT is valid at thatnowand signs it, and the RP checksnowagainst its own clock (WIP-106 §4). - Match strictness instead of scores. A score’s meaning depends on the model and the pair, so an RP cannot pick a meaningful threshold, and scores are biometric signals. An ordered strictness survives model upgrades and keeps scores out of every Flamingo Token.
- A separate Engine Provider signs the Engine. Models may come from different parties, and trust in the models stays separate from trust in the capture environment. Pipelines only have meaning relative to an Engine release manifest.
- Engine configuration hash. As in remote attestation (RFC 9334), the Verifier reports a measurement and the RP appraises it against reference values, here signed release manifests. One hash gives RPs exact control over accepted configurations, keeps the individual values private, and lets circuits constrain its components.
- Signatures outside the Verifier. The RP’s allowlist and the Authenticator’s
release_hashgive the protection a signature check in the TEE would, so one audited Verifier image serves every Engine Provider without trusted keys in its measurement. - Quantized response times. The host sees when each response leaves, and an Engine’s execution time can depend on, or be modulated by, the biometric input. Releasing responses only at a few slots limits what that reveals to the slot index, at most $\log_2$
countbits per request, while a geometric spacing keeps the wait short. The slot count is in the signed manifest, so each Engine sets its latency trade-off in the open and Authenticators enforce their limit. A first slot no shorter than the fastest complete run keeps early exits from standing out, and the last slot bounds the Engine’s runtime. Rejecting replayed requests keeps the host from collecting several observations of the same request. Slots are per pipeline because run times differ by pipeline; shared slots would let early exits of a slow pipeline stand out. - Release hash. The token commits to the signed release rather than to the bundle, so the pipelines and response slots the Verifier enforces are those the RP accepted. Hashing the JWS Payload instead of the JWS keeps the hash stable when a release is re-signed, e.g. after a key rotation, since
ES256signatures are randomized.
7. Alternatives Considered
- Running the models in a ZK circuit. The Authenticator proves extraction and fraud detection in ZK. Rejected: proving these models is still computationally infeasible for most devices and most models, and the prover would need the model weights, while fraud detection relies on keeping them secret.
- MPC. Parties jointly run the models over secret-shared inputs and weights. Rejected: it runs the models orders of magnitude slower and adds the assumption that the parties don’t collude.
- Random or fixed response times. Random delays only lower the timing channel’s bandwidth, since repeated requests average the noise out. A single fixed response time removes the channel but makes every request wait for the worst case. Rejected in favor of quantized response times.
8. Security
- Post-quantum channel. Biometrics don’t expire, so a party that records ciphertexts today (e.g. the host) MUST NOT be able to decrypt them with a future quantum computer. X-Wing combines ML-KEM-768 with X25519 and stays secure while either holds, assuming its SHA3 hash functions are secure. Flamingo Token signatures are not post-quantum; the single-use
nonceonly prevents replay of an existing token, not forgery of tokens for nonces not yet spent onceBabyJubJub-EdDSA-Poseidon2becomes forgeable (e.g. a quantum computer recovers the Verifier’s signing key), letting a forger claim a live comparison no Engine ever verified. RPs MUST stop accepting Flamingo Tokens signed with this scheme once it is considered forgeable (e.g. once a cryptographically relevant quantum computer is considered feasible), i.e. before forgery becomes practical, and migrate to a post-quantum signature scheme. - Authenticator integrity and missing AATs. A TAKT and an AAT show that a genuine Authenticator produced
payload, which keeps modified or emulated Authenticators from injecting media. Without an AAT anyone can submit arbitrary media, and the verdict becomes an oracle for tuning attacks; RPs that accepthas_aat = 0accept that risk. - Unreviewed Engines. A host may load any Engine, but an honest request reaches only the Engine whose hash the Authenticator chose, bound by
cdh, and a Flamingo Token from another Engine carries anengine_config_hashno RP accepts. A leaked release key lets an attacker publish release manifests RPs and Authenticators accept until the Engine Provider marks the keyrevoked. Even a malicious Engine cannot export data directly (item 4). - Closed Engine. The Engine is not trusted with privacy. The sandbox removes its channels out, the per-request reset keeps one request’s data from the next, and
debug_reportreaches only the requester and is forwarded only with the user’s consent. - Flamingo Token privacy.
compared_entry_hashof a credential entry is constant per user, so together withaudandnoncea Flamingo Token links a user to a specific RP request. Consumers MUST keep Flamingo Tokens private, e.g. as private inputs to WIP-202. - Verifier keys. The Verifier generates its signing key inside the TEE at boot from the hardware RNG and never exports it, so a key in a valid
signing_key_attestationwas generated by that measurement and each boot uses a new key. A key cannot be revoked; RPs stop accepting a compromised measurement, andMAX_ATTESTATION_AGEbounds how long its attestation stays acceptable. - Timing. Each response reveals which slot of its pipeline it was released in (Response Timing), at most $\log_2$
countbits per request, a budget the Authenticator checks before sending biometrics; with per-pipeline slots, timing may also reveal the release and pipeline. Microarchitectural side channels on shared hardware remain outside the guarantees of AWS Nitro Enclaves.
9. Future Considerations
- Other TEEs. Support for TEEs beyond AWS Nitro Enclaves, through attestation profiles that map each provider’s document, root and measurement to the rules of Attestation and Transport.
10. Backwards Compatibility
This WIP introduces a new interface; no existing contracts or functionality are affected.
Appendix A1. Test Vectors
Field elements are hex unless decimal, bytes are hex. The trust_anchor_key secret is 32 bytes of 0x07, the assertion_key secret 32 bytes of 0x09 (P-256, signed per RFC 6979), the Verifier’s signing key secret 32 bytes of 0x15. payload has pipeline = 1, match_strictness = 2, empty hints and three entries with empty meta and data "credential image", "live image" and "challenge image" (ASCII), with compare = [0, 1, 2] and release_hash 32 bytes of 0x2a.
With an AAT
payload = a66568696e74734067636f6d706172658300010267656e747269657383a264646174615063726564656e7469616c20696d616765646d65746140a264646174614a6c69766520696d616765646d65746140a264646174614f6368616c6c656e676520696d616765646d6574614068706970656c696e65016c72656c656173655f6861736858202a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a706d617463685f7374726963746e65737302
cdh = 0x06559c686e02e6fdd317c3faf3d3a871f1660465552dc3c15e36290a44346150
aud = 1928118
nonce = 42
now = 1783446000
min_build_version = 2006
trust_anchor_key.x = 19037598474602150174935475944965340829216795940473064039209388058233204431288
trust_anchor_key.y = 3549932221586364715003722955756497910920276078443163728621283280434115857197
takt_message = 0x17a7e201265aef7edbe89f804b16ff8c98a7f87e1da7ed566bcaa91340053879
takt = 84582aa2013a000100000458202d4bdf6ee60feda0975c770bb7a23dc6e4e0ed1e35ff3c2426cded4ec030d987a05885a4041a6a4e8b7008a101a4010220012158207135fa4fd93a09dce98bbf681b4bfcf50e7c0d6354e62afb0bff2a3429617865225820ed4c1f02ddb9023ee56a557e515d6a9dc66c11f220960de594334df588776724190109781d68747470733a2f2f776f726c642e6f72672f6561742f74616b742f76313a0001116f480003000007d6010258409a97b6c24c4873234fc4e72a4a239cedec22652cc9206f87a7b9735db9d54885aa069707c532d0ae86c41add78279ac40c949459193361e166c2705d3cca4c05
(exp 1783532400; sec_flags 0x0003000007d60102: platform 2, sec_level 1, build_version 2006, sec_meta 3)
aat = 8443a10126a058a3a6034800000000001d6bb6041a6a4d3d8d0a5820000000000000000000000000000000000000000000000000000000000000002a190109781c68747470733a2f2f776f726c642e6f72672f6561742f6161742f76313a0001387f582006559c686e02e6fdd317c3faf3d3a871f1660465552dc3c15e36290a443461503a000138805820000000000000000000000000000000000000000000000000000000000000000a5840c9deddf6657d8d624fc90f035ca1818de26d24aeb9becd946877d7a9f37520ad605b173c60660179227e019370ae9ea824b35f26fefb5b7592b2ab1865263d27
(exp 1783446925; authenticator_meta 10: user_presence 2, provider bits 0b01)
engine_config_hash = 0x2f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d
has_aat = 1
aat_flags = 0x00a3000007d60102
compared_entry_hash_0 = 0x21e9fe96ff18807a727830c56281c96af824005400bcb8ef0f463b34cb20c415
compared_entry_hash_1 = 0x010cff8544f0007179c076ddb49de551bd5743797bfe05cb3b26f55c696f507b
compared_entry_hash_2 = 0x2abb4a81875eb0e308f0fbfb8c0a0f57e77243e095b145ae7973e0b2771e044d
compared_entry_hash_3 = 0
trust_anchor_key_hash = 0x14b904063236db16ecda3a3fa9aaa42b3f605706baa2fdb25afeab8df8f252d8
digest = 0x0f493d3e4f266f81950ad5c9ebe359b2085f1ab82173e7b3d6cff4c30c17fa5e
kid = f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12
sig = 78ee99f6c862bfc10345cdeded780ae0d7cc7ed3d82904a245a6df5ba9f966030e06db33888dab460704558a8c5095d0b3d2c4fb3c6c67a7a428e75ae5e6d900
cwt = 84582aa2013a00010000045820f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12a059017dac190109782168747470733a2f2f776f726c642e6f72672f6561742f666c616d696e676f2f76313a00015f8f582021e9fe96ff18807a727830c56281c96af824005400bcb8ef0f463b34cb20c4153a00015f905820010cff8544f0007179c076ddb49de551bd5743797bfe05cb3b26f55c696f507b3a00015f9158202abb4a81875eb0e308f0fbfb8c0a0f57e77243e095b145ae7973e0b2771e044d3a00015f92582000000000000000000000000000000000000000000000000000000000000000003a00015f93582000000000000000000000000000000000000000000000000000000000001d6bb63a00015f945820000000000000000000000000000000000000000000000000000000000000002a3a00015f95582014b904063236db16ecda3a3fa9aaa42b3f605706baa2fdb25afeab8df8f252d83a00015f961b00a3000007d601023a00015f97013a00015f981a6a4d39f03a00015f9958202f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d584078ee99f6c862bfc10345cdeded780ae0d7cc7ed3d82904a245a6df5ba9f966030e06db33888dab460704558a8c5095d0b3d2c4fb3c6c67a7a428e75ae5e6d900
Without an AAT
Same payload, aud, nonce and engine_config_hash, with has_aat = 0, now = 0, trust_anchor_key_hash = 0 and aat_flags = 0.
digest = 0x046b836383d9d9fd350fbed1197e1c1c17f2b187fb6ef1571e5f9b42ec6fc8fc
sig = a2c1883fd414440ca4df81723869407caa35ef840b0438ec1fb9280757b572abe2ebb272225105813188bff8dd3d5d236636ad7c9aa002413121164697265705
cwt = 84582aa2013a00010000045820f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12a0590171ac190109782168747470733a2f2f776f726c642e6f72672f6561742f666c616d696e676f2f76313a00015f8f582021e9fe96ff18807a727830c56281c96af824005400bcb8ef0f463b34cb20c4153a00015f905820010cff8544f0007179c076ddb49de551bd5743797bfe05cb3b26f55c696f507b3a00015f9158202abb4a81875eb0e308f0fbfb8c0a0f57e77243e095b145ae7973e0b2771e044d3a00015f92582000000000000000000000000000000000000000000000000000000000000000003a00015f93582000000000000000000000000000000000000000000000000000000000001d6bb63a00015f945820000000000000000000000000000000000000000000000000000000000000002a3a00015f95582000000000000000000000000000000000000000000000000000000000000000003a00015f96003a00015f97003a00015f98003a00015f9958202f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d5840a2c1883fd414440ca4df81723869407caa35ef840b0438ec1fb9280757b572abe2ebb272225105813188bff8dd3d5d236636ad7c9aa002413121164697265705