Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

  1. The Engine is the sandboxed component of the Verifier that performs extraction, fraud detection and comparison. This spec defines none of them.
  2. The Engine Provider builds the Engine and signs its release manifests. It MAY be the same organization as an Authenticator Provider (WIP-106).
  3. 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).
  4. The Field ($F$), the hash function ($H_t$), the signature scheme (BabyJubJub-EdDSA-Poseidon2) and the PublicKey representation MUST follow WIP-100. R(d) reads a 256-bit digest d as a big-endian integer modulo $q$.

3.1 Constants

ConstantValueDescription
MAX_ENTRIES8Entries per request.
MAX_COMPARED4Maximum compared entries, fixed by the Flamingo Token layout.
DS_WIP_201127603488023523044162070750169730750022280897185614 (b"WORLD-ID/WIP-201/SIGN")Domain separator for the Flamingo Token digest.
DS_WIP_201_ENGINE8362622191109606222205468683123474433460185506268139077 (b"WORLD-ID/WIP-201/ENGINE")Domain separator for engine_config_hash.
MAX_ATTESTATION_AGE10800Maximum age of an attestation document at verification, in seconds (3 hours, the validity of a Nitro attestation’s certificate).
MAX_DEBUG_REPORT65536Maximum debug_report size, in bytes.
CHANNEL_DOMAINWORLD-ID/WIP-201/CHANNELDomain of the request channel.
CHANNEL_KEY_DOMAINWORLD-ID/WIP-201/CHANNEL-KEY\0Domain of the channel key commitment, as bytes including the trailing 0x00.
EAT_PROFILEhttps://world.org/eat/flamingo/v1The 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.

FieldTypeDescription
versionuintMUST be 1.
payloadbytesThe encoded payload map; see below.
audField elementThe rpId of the request.
nonceField elementThe RP’s nonce for the request, sampled uniformly from the nonzero elements of $F$ with a CSPRNG.
aat_inputsmap, optionalInputs for verifying a TAKT and an AAT (Authenticator Assertion).
payload fieldTypeBoundsDescription
pipelineuint1..2^16Engine-defined pipeline, chosen by the Authenticator among the configurations the RP offers (Engine Selection).
match_strictnessuint< 2^8Matching strictness level, defined by the Engine Provider for all pipelines.
hintsbytes≤ 1 KiBRequest-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.
entriesarray of maps1..=MAX_ENTRIESInputs to the Engine, each { data, meta }.
entries[i].databytesAn encoded image or an embedding.
entries[i].metabytes≤ 256 bytesHow the Engine reads data, e.g. which frame of a capture it is.
comparearray of uint2..=MAX_COMPARED distinct indices into entriesOrder matters: compare[k] becomes compared_entry_hash_k, and pipeline assigns each position its role.
release_hashbytes32 bytesThe 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.
  1. The Verifier checks the payload’s shape and bounds, resolves release_hash, pipeline and match_strictness against the loaded release (Release Manifests), and hashes the entries compare names, in order, into compared_entry_hash (Claims). It interprets nothing else and forwards the payload to the Engine. Roles (Credential item, live frame, challenge) are fixed by pipeline through positions in compare.
  2. The Verifier MUST reject a request of any other shape, exceeding a bound, or whose compare repeats or names an out-of-range index. It MUST reject a sealed request larger than the largest max_request_bytes of its loaded releases before decrypting it, and one larger than its pipeline’s max_request_bytes after.

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 inputTypeSource
trust_anchor_keyPublicKeyaat_inputs.trust_anchor_key
taktbytesaat_inputs.takt
aatbytesaat_inputs.aat
nowuint32aat_inputs.now
min_build_versionuint32aat_inputs.min_build_version, the RP’s minimum; 0 sets no minimum.
aud, nonceuint64, Field elementThe request’s aud and nonce. The Verifier MUST reject an aud not less than $2^{64}$.
cdhField elementComputed as R(SHA-256(payload)).
platform, sec_level, sec_metauint8Unpacked from the TAKT’s sec_flags.
authenticator_metauint8The AAT’s authenticator_meta.
  1. Verification. If aat_inputs is 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 the signing_key_attestation.
  2. Outputs. The public inputs reach the Flamingo Token as trust_anchor_key_hash, now and aat_flags (Claims); aud and nonce as themselves. The TAKT, the AAT, the assertion_key and build_version MUST NOT leave the Verifier, and aat_inputs MUST NOT be passed to the Engine.
  3. Without an AAT. The Verifier MUST set has_aat to 1 if it verified a TAKT and an AAT and to 0 otherwise. With has_aat = 0, now, trust_anchor_key_hash and aat_flags MUST be 0.

3.4 Response

VariantContents
Successtoken (Flamingo Token), signing_key_attestation (Attestation and Transport), debug_report (optional)
Failederror, debug_report (optional)
  1. The response MUST be encrypted to the requester and MUST be padded to a fixed length, hiding the outcome and the report size.
  2. error is implementation-defined and bounded in size; it comes from the Verifier or, passed through, from the Engine.
  3. debug_report is optional Engine output outside the Flamingo Token. The Verifier MUST NOT emit a report larger than MAX_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.
  4. The Verifier MUST emit a Flamingo Token only when the Engine returns Pass.

3.5 Engine

  1. Each pipeline defines the entry layout, how each entry’s meta is read, and which checks run. Each pipeline sets the match thresholds that meet each match_strictness level. Fraud detection is fixed by pipeline and does not vary with match_strictness.
  2. Each release manifest defines the match_strictness levels independently of pipeline: 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 a match_strictness the release manifest does not define.
  3. 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.
  4. The Verifier MUST pass the payload fields to the Engine unchanged. The Engine MUST return Pass, meaning every check passed and every compared pair cleared match_strictness, or an error, either with an optional debug_report and 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:

FieldTypeDescription
engine_hashstringSHA-256 of the Engine bundle, hex.
versionstringThe Engine Provider’s label for the release, unique per Engine Provider, for people and logs.
pipelinesarrayEach 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_levelsarrayEach entry: match_strictness (integer) and description (string), ascending.

pipeline values MUST be unique within a manifest, and so MUST match_strictness values.

  1. Release hash. release_hash is the SHA-256 of the manifest’s JWS Payload (RFC 7515 §2). It excludes the protected header and the signature, so re-signing a release keeps its release_hash.
  2. 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 its pipeline and match_strictness values are unique, and that every response_slots has count >= 1 and 0 < min <= max, with min < max if count > 1. It MUST NOT let an Engine load anything outside its bundle, and MUST run a request only on the release whose release_hash the request names, with a pipeline and match_strictness that release defines. It verifies no signature and fetches nothing.
  3. Signatures. RPs and Authenticators MUST verify a release manifest under an active release key of an Engine Provider they trust before relying on it.
  4. Metadata. The Engine Provider MUST expose a JSON file over HTTPS at https://<FQDN>/.well-known/world-id-engine.json:
FieldTypeDescription
versionintegerMUST be 1.
provider_idstringStable identifier, which RPs MAY allowlist.
release_keysarrayEach entry: jwk (a P-256 JWK with kid) and status (active, retired or revoked).
manifestsarrayEach 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

  1. 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.
  2. Availability. The Verifier MUST advertise the release_hash values 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.
  3. Choice. The Authenticator MUST choose the first offered tuple its device supports and the Verifier has loaded, and set release_hash, pipeline and match_strictness from it.
  4. Check. The RP accepts only the engine_config_hash of 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

  1. Slots. Each pipeline’s response_slots (Release Manifests) define count release times between min and max milliseconds, spaced geometrically: slot i is min * (max / min)^(i / (count - 1)) for 0 <= i < count, rounded down to whole milliseconds. With count = 1, the only slot is max, and every response takes exactly that long.
  2. 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.
  3. Choosing slots. Engine Providers SHOULD set min at or above the pipeline’s fastest complete run, so early exits, e.g. a rejected input, are released together with completed runs, and max above its 99.9th-percentile run time on the intended hardware, since slower requests fail.
  4. Leakage budget. A response reveals at most $\log_2$ count bits 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).

  1. The protected header MUST contain alg (label 1) = -65537 (Private Use, BabyJubJub-EdDSA-Poseidon2) and kid (label 4) = the compressed signing public key.
  2. The signature MUST be a BabyJubJub-EdDSA-Poseidon2 signature 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.

ClaimKeyValue
eat_profile265EAT_PROFILE.
compared_entry_hash_k, k = 0..3-90000 - kR(SHA-256(entries[compare[k]].data)), or 0 if compare has no position k.
aud-90004aud from the request.
nonce-90005nonce from the request.
trust_anchor_key_hash-90006WIP-106 hash of the trust_anchor_key that signed the verified TAKT, or 0 without an AAT.
aat_flags-90007platform + 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-900081 if the Verifier verified an AAT, otherwise 0.
now-90009now of the verified AAT, or 0 without an AAT.
engine_config_hash-90010The Engine configuration that ran (Engine Selection).
  1. aat_flags, has_aat and now are CBOR uints. All other claims except eat_profile are 32-byte byte strings, the canonical big-endian encoding of a field element.
  2. eat_profile is 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:

  1. Source. Each Verifier release MUST name its source by an immutable reference, e.g. a commit hash in a public repository.
  2. Build. Building that source with the documented toolchain MUST produce a bit-identical Enclave Image File (EIF).
  3. Measurement. PCR0, PCR1 and PCR2 of that EIF, as AWS Nitro Enclaves computes them, are the Verifier’s measurement.
  4. Publication. The Verifier operator MUST publish, per release, the source reference, the build instructions and PCR0–2.
  5. 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.

Documentpublic_keyUsed by
Channel attestationSHA-256(CHANNEL_KEY_DOMAIN || channel_key)The Authenticator, to encrypt a request; discarded afterwards.
signing_key_attestationThe Flamingo Token signing key (kid)Consumers, with every Flamingo Token.
  1. Channel. Before sending a request, the Authenticator MUST check that the channel attestation:

    • is a valid COSE_Sign1 whose 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 the channel_key it encrypts to;
    • has a timestamp at most MAX_ATTESTATION_AGE old.

    It then seals response_key || request to channel_key (Channel Encoding), where response_key is a fresh X-Wing encapsulation key and request the plaintext of Request.

  2. Response. The Verifier MUST seal the padded response to response_key. Only the requester holds response_key, so the response is bound to its request; a Success is authenticated by its Flamingo Token.

  3. Replay. The Verifier MUST reject a request whose (aud, nonce) it has already processed under the same channel_key, before running the Engine. The channel_key MUST 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.

  4. Refresh. The Verifier MUST re-attest its signing key at least every MAX_ATTESTATION_AGE and return its latest signing_key_attestation.

  5. 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.

  1. Info. info is CHANNEL_DOMAIN || 0x00 for a request and CHANNEL_DOMAIN || 0x01 for a response.
  2. Request plaintext. The first 1216 bytes are response_key, an X-Wing encapsulation key; the rest is the request.
  3. 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:

  1. Choose a configuration (Engine Selection).
  2. Verify its release manifest (Release Manifests), including its response_slots (Response Timing).
  3. Verify the channel attestation (Channel), including PCR0–2 it trusts (Verifiability).
  4. 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.

InputVisibilityTypeDescription
verifier_keyPublicPublicKeyThe Verifier’s signing key, checked against its signing_key_attestation.
nowPublicuint32Current time, in seconds since the Unix epoch. With an AAT it is the now of WIP-106 §3.8.
aud, noncePublicField elementThe RP’s request.
has_aatPublicboolWhether the Flamingo Token carries an AAT result.
trust_anchor_key_hashPublicField elementHash of an allowlisted trust_anchor_key.
platform, sec_level, sec_meta, authenticator_metaPublicuint8See WIP-106 §3.6.
min_build_versionPublicuint32Minimum build_version the RP accepts.
compared_entry_hashPrivate[Field element; 4]From the Flamingo Token.
engine_config_hashPublicField elementFrom the Flamingo Token.
sigPrivateSignatureThe Flamingo Token signature.

The circuit MUST enforce:

  1. Signature. The Verifier signed the Flamingo Token.
  2. Request binding. The Flamingo Token is for this request.
    • The digest uses the public aud and nonce.
  3. Authenticator Assertion. The Flamingo Token carries the AAT result, or none.
    • has_aat is 0 or 1, and the digest uses it.
    • The digest uses the public trust_anchor_key_hash, and has_aat * now as the Flamingo Token’s now.
    • Range-check platform, sec_level, sec_meta, authenticator_meta and min_build_version to their widths, and constrain the user_presence bits of authenticator_meta below 5.
    • 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_meta and min_build_version MUST be 0; now stays available to the embedding circuit.
  4. 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 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.

PartyTrusted forNot trusted for
AWS (Nitro Enclaves)Isolation, measurement and attestation of the enclave.Nothing: if this fails, neither privacy nor integrity holds.
Engine ProviderEngine correctness: fraud detection, matching, and accurate release manifest descriptions.Privacy: the sandbox keeps the Engine from exporting data.
Authenticator ProviderVerifying the Authenticator’s integrity, as stated in the TAKT.—
AuthenticatorThe source and freshness of the live capture.—
Host and operatorAvailability 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:

  1. 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 (kid is only a hint);
    • has a timestamp at most MAX_ATTESTATION_AGE old.
  2. The signature verifies under that key.
  3. The request matches:
    • aud is the RP’s own;
    • nonce is one the RP issued, has not expired and has not been used before.
  4. 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_hash and the aat_flags sub-fields.
  5. engine_config_hash is 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

  1. 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.
  2. 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 cdh covers the exact payload, the AAT vouches for exactly the compared input.
  3. 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.
  4. 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_version stays private as WIP-106 requires; min_build_version takes its place.
  5. SHA-256 over received bytes. RPs and Issuers compute or check these hashes outside circuits, where SHA-256 is available everywhere. They cover the bytes the Verifier received, not embeddings derived from them, which no other party could reproduce.
  6. 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=16 permutation costs less than unpacking packed claims in-circuit.
  7. No Verifier time. Most TEEs have no trusted clock, so the Verifier does not timestamp the Flamingo Token; the RP’s single-use nonce makes it fresh. The Authenticator states now for the AAT: the Verifier checks the AAT is valid at that now and signs it, and the RP checks now against its own clock (WIP-106 §4).
  8. 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.
  9. 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.
  10. 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.
  11. Signatures outside the Verifier. The RP’s allowlist and the Authenticator’s release_hash give the protection a signature check in the TEE would, so one audited Verifier image serves every Engine Provider without trusted keys in its measurement.
  12. 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$ count bits 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.
  13. 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 ES256 signatures are randomized.

7. Alternatives Considered

  1. 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.
  2. 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.
  3. 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

  1. 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 nonce only prevents replay of an existing token, not forgery of tokens for nonces not yet spent once BabyJubJub-EdDSA-Poseidon2 becomes 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.
  2. 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 accept has_aat = 0 accept that risk.
  3. 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 an engine_config_hash no RP accepts. A leaked release key lets an attacker publish release manifests RPs and Authenticators accept until the Engine Provider marks the key revoked. Even a malicious Engine cannot export data directly (item 4).
  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_report reaches only the requester and is forwarded only with the user’s consent.
  5. Flamingo Token privacy. compared_entry_hash of a credential entry is constant per user, so together with aud and nonce a Flamingo Token links a user to a specific RP request. Consumers MUST keep Flamingo Tokens private, e.g. as private inputs to WIP-202.
  6. 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_attestation was generated by that measurement and each boot uses a new key. A key cannot be revoked; RPs stop accepting a compromised measurement, and MAX_ATTESTATION_AGE bounds how long its attestation stays acceptable.
  7. Timing. Each response reveals which slot of its pipeline it was released in (Response Timing), at most $\log_2$ count bits 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

  1. 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