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 runs one sandboxed biometric Engine release, provided by an Engine Provider. For a request, the Engine runs a pipeline’s checks, e.g. fraud detection, on some of the request’s entries, e.g. live frames, and compares entries, e.g. a live frame with a Credential image, under a requested match_strictness. If the Engine accepts, the Verifier signs a Flamingo Token over the Engine configuration, the compared entries, the public values of an Authenticator Assertion, if any, and the WIP-107 billing nullifier it checked, if the RP is billed. If a WIP-106 Trust Anchor Key Token (TAKT) and Authenticator Assertion Token (AAT) are provided, they are also verified. Consuming protocols such as WIP-202 use the Flamingo Token to prove the Verifier’s checks to a Relying Party (RP), optionally 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 Verifier attests which Engine release it runs, and the RP accepts only releases the Engine Provider signed.
3. Overview
This section is non-normative. It sketches the parties and the one-shot verification flow.
flowchart LR
EP[Engine Provider] -.->|bundle + manifest| Host
EP -.->|release keys| RP
AP[Authenticator Provider] -.->|accepted PCRs| A[Authenticator]
subgraph Host
subgraph Enclave[Verifier enclave]
V[Verifier] -->|3 payload| S[Sandbox: Engine]
S -->|4 Pass / error| V
end
end
V -->|1 attestation + manifest| A
A -->|2 sealed request| V
V -->|5 Flamingo Token| A
A -->|6 token or proof| RP
Dashed arrows are set up once. Numbered arrows are steps 1–6 of one verification; in step 7 the RP verifies the token locally:
- The Verifier attests its measurement and its loaded release, and sends that release’s manifest (Attestation and Channel) to the Authenticator. The Authenticator checks both and chooses a configuration. The results of this step can be cached and the attested channel can be re-used.
- The Authenticator seals a request to the Verifier. It carries the
entries(images or embeddings) and names which of them the pipeline checks (pipeline_inputs) and which the Engine compares (compare) (Authenticator, Request and Response). - The Verifier checks the request and passes it to the sandboxed Engine (Request Processing).
- The Engine runs the pipeline’s checks on the pipeline inputs and compares the selected entries (Request Processing).
- On a pass, the Verifier builds a Flamingo Token from the request and the loaded release, and signs it (Flamingo Token).
- The Authenticator passes the token, or a proof over it such as WIP-202, to the RP.
- The RP verifies it (Consuming Flamingo Tokens).
How the RP requests a Flamingo Token or a proof over it, and what it learns, is defined by the consuming protocol (Terminology).
Components and parties:
- RP: requests and verifies the result, as its consuming protocol defines (Consuming Flamingo Tokens).
- Authenticator: checks the attestation and the manifest before sending biometrics (Authenticator).
- Verifier: open source; checks and forwards requests, enforces response timing and signs tokens. It runs one Engine release (Verifier).
- Sandbox and Engine: closed source and closed weights; run fraud detection and comparison, with no way to export data (Engine, Sandboxed Engine).
- Host and operator: run the enclave and relay its ciphertext; trusted for availability only (Request Processing).
- Engine Provider: signs Engine releases and publishes them (Release and Release Manifest, Engine Provider Metadata).
- Authenticator Provider: decides which Verifier measurements its Authenticators accept and ships them, e.g. in the app release (Authenticator).
What each party is trusted for is in Trust Assumptions.
4. Specification
4.1 Terminology
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 Verifier is the TEE service this spec defines.
- The Engine is the sandboxed component of the Verifier that performs extraction, fraud detection and comparison. This spec defines the Engine’s interface (Engine), not its models or thresholds.
- The Engine Provider builds the Engine and signs its release manifests. It is identified by
iss, anhttpsURL without query or fragment (RFC 8414 §2, Rationale item 6). It MAY be the same organization as an Authenticator Provider. - The Authenticator Provider is as defined in 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).
- A pipeline is an Engine-defined set of pass/fail checks on its pipeline inputs, e.g. fraud detection such as presentation attack or deepfake detection.
- Match strictness levels say how similar compared entries must be to count as a match (Match Strictness Levels).
- A consuming protocol defines how a Flamingo Token is requested and used, e.g. a proof type such as WIP-202, or an RP that verifies tokens directly (Consuming Flamingo Tokens).
- 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$.
4.2 Constants
| Constant | Value | Description |
|---|---|---|
MAX_COMPARED | 4 | Maximum compared entries, fixed by the Flamingo Token claims. |
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_REQUEST_LIFETIME | 600 | Maximum time from the Verifier’s time to a request’s expires_at, in seconds (10 minutes). |
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. |
4.3 Engine
An Engine is developed and released by an Engine Provider. It operates in a stateless request/response model. It encompasses the following high-level concepts:
- Request entries. The Engine accepts requests that contain an array of
entries. Each entry is one image or embedding with associated metadata (Request and response). - Pipeline. The request further specifies
pipeline_id, to select the pipeline to run on a subset ofentries. The subset is specified via apipeline_inputsarray that contains entry indices (Request Processing). - Comparisons. Similarly, an array of indices called
comparespecifies a subset of entries, possibly overlapping withpipeline_inputs, which are compared pair-wise using Engine-specific models at the requestedmatch_strictness(Match Strictness Levels). - Response. The Engine only returns whether every check passed and every comparison matched (Request and response).
- Releases. An Engine Provider publishes a signed release manifest for every Engine version. The manifest specifies the release’s pipelines, their inputs and response slots, and its match strictness levels (Release and Release Manifest, Release Versioning, Engine Provider Metadata).
4.3.1 Request and response
The Verifier passes the Engine request below to the Engine unchanged and passes nothing else. On the sandbox channel this WIP does not specify its encoding; in the Verifier request it is the payload map (Request and Response), and the types below are CBOR types (RFC 8949).
Engine request fields:
| Field | Type | Bounds | Description |
|---|---|---|---|
pipeline_id | uint | 1..2^16 | A pipeline of the loaded release, chosen by the Authenticator (Authenticator). |
match_strictness | uint | < 2^8 | A level of the loaded release, chosen by the Authenticator. |
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 the pipeline. |
entries | array | The request’s images or embeddings, each with the entry fields below. Every entry MUST be in pipeline_inputs or compare. | |
pipeline_inputs | array of uint | exactly the pipeline’s inputs count | Distinct indices into entries the pipeline checks; pipeline_inputs[i] is the entry the pipeline’s inputs[i] describes. |
compare | array of uint | 2..=MAX_COMPARED | Distinct indices into entries. Order matters: compare[k] becomes compared_entry_hash_k. |
Entry fields, for each item of entries:
| Field | Type | Bounds | Description |
|---|---|---|---|
data | bytes | An encoded image or an embedding. | |
meta | bytes | ≤ 256 bytes | How the Engine reads data, e.g. which frame of a live capture it is. |
See Appendix A2 for an example request and response.
Engine response variants:
| Variant | Contents |
|---|---|
Pass | Every check of the pipeline passed and every two entries in compare match at match_strictness. Optional debug_report. |
error | Implementation-defined. Optional debug_report. |
4.3.2 Match Strictness Levels
- Meaning. A match strictness level says how similar compared entries must be to count as a match. A request picks one with
match_strictness. Internally, the Engine MAY use different similarity thresholds for a level depending on the inputs, e.g. their image type or quality, as long as the level keeps its documented meaning. - Trade-off. A higher level lowers the chance of a false match, i.e. accepting entries that show different persons, but raises the chance of a false non-match, i.e. rejecting entries that show the same person.
- Rules. Levels are defined per release, as the manifest’s ascending
match_strictness_levels. A level MUST mean the same whicheverpipeline_ida request uses, and a higher level MUST be at least as strict for every two compared entries. The Engine Provider MUST document what each level means.
4.3.3 Request Processing
For each Engine request, in a reset instance (Sandboxed Engine), the Engine:
- Validates the request against its release:
pipeline_idnames one of its pipelines,match_strictnessis listed, and the pipeline inputs match the pipeline’sinputsin count and media type. Otherwise it MUST returnerror. - Checks the pipeline inputs: it MUST run the pipeline’s checks, e.g. presentation attack or deepfake detection, on the entries
pipeline_inputsnames, reading each entry’smetaas the pipeline defines. The checks MUST NOT depend onmatch_strictness. - Compares the entries
comparenames: it extracts what the release compares, e.g. face embeddings, and MUST match every two of them atmatch_strictness(Match Strictness Levels). - Responds: it MUST return
Passonly if every check passed and every comparison matched, anderrorotherwise (Request and response).
4.3.4 Release and Release Manifest
A release is one version of an Engine: its bundle, i.e. code, model weights and configuration (Sandboxed Engine), and the release manifest that describes it. The manifest names the release’s pipelines and strictness levels, and its hash, release_hash, identifies the release. A Verifier runs one release. See Appendix A2 for an example manifest.
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:
Manifest payload fields:
| Field | Type | Description |
|---|---|---|
iss | string | The Engine Provider (Terminology). |
engine_hash | string | SHA-384 of the Engine bundle, 96 lowercase hex digits. |
version | integer | See Release Versioning. |
match_strictness_levels | array of integers | The release’s levels, non-empty, strictly ascending, each < 2^8. |
pipelines | array | The release’s pipelines; see below. pipeline_id values MUST be nonzero and unique within a manifest. |
Pipeline fields, for each item of pipelines:
| Field | Type | Description |
|---|---|---|
pipeline_id | integer | The pipeline identifier, < 2^16. |
inputs | array | A non-empty array of at most 255 items; see below. Item i describes the entry pipeline_inputs[i] names. |
response_slots | array of integers | Non-empty, strictly ascending, positive release times in milliseconds; see Response Timing and Replay. |
Pipeline input fields, for each item of a pipeline’s inputs:
| Field | Type | Description |
|---|---|---|
role | string | What the input is, e.g. live_frame. Roles MAY repeat within a pipeline. |
media_types | array of strings | Accepted media types, e.g. image/jpeg. MUST NOT be empty. |
- 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. - Signatures. RPs MUST verify a release manifest under an active release key of its
iss(Engine Provider Metadata) before relying on it. The Verifier and Authenticators do not verify it.
4.3.5 Release Versioning
versionMUST be greater than theversionof every earlier release of the sameiss. Every change to the payload is a new release.- A later release MUST NOT change a pipeline’s
inputs, increase the number of itsresponse_slots, or drop amatch_strictnesslevel; such a change uses a newpipeline_idvalue. A later release MAY drop a pipeline, but MUST NOT reuse itspipeline_idfor differentinputs. - A later release MUST NOT give an existing
match_strictnessvalue a different meaning; a changed meaning uses a new value. It MAY change the Engine’s thresholds to keep a level’s meaning, e.g. for a new model.
4.3.6 Engine Provider Metadata
The Engine Provider MUST expose a JSON file over HTTPS at <iss>/.well-known/world-id-engine.json:
Metadata fields:
| Field | Type | Description |
|---|---|---|
version | integer | MUST be 1. |
release_keys | array | Each entry: jwk (a P-256 JWK with kid) and status (active, retired or revoked). |
manifests | array | The release manifests the Engine Provider offers, each a compact JWS. Removing one withdraws its release. |
A reader MUST NOT follow redirects to another origin, and MUST ignore manifests whose iss is not the one it fetched the file for. The Verifier never fetches the Engine Provider metadata. RPs compute the engine_config_hash values they accept from it, and SHOULD refresh it at least every 24 hours, so a withdrawn release or a revoked key stops being accepted.
4.4 Verifier
A Verifier is an open-source, reproducibly built AWS Nitro enclave image, run by an operator on a host. It runs one Engine release and serves requests from Authenticators. It encompasses the following high-level concepts:
- Request and response. A request carries the RP’s
audandnonce, an expiry, the Engine request, and optionally an Authenticator Assertion. On aPass, the Verifier builds a Flamingo Token from the request, the verified AAT and the loaded release, signs it and returns it (Request and Response, Flamingo Token). - Attestation. The Verifier attests its channel key together with the loaded release, and its token signing key, in Nitro attestation documents. Authenticators encrypt their requests to the attested channel key. The Verifier syncs its clock to each new attestation’s timestamp (Attestation and Channel).
- Processing. The Verifier checks the request, rejects expired and replayed requests, verifies the Authenticator Assertion, and passes the Engine request to the Engine unchanged. It releases each response only at fixed time slots, so its timing reveals little (Request Processing, Response Timing and Replay).
- Sandboxed Engine. At start-up, the Verifier loads one Engine release and runs it in a sandbox that cannot export data (Sandboxed Engine).
- Measurement. Anyone can rebuild the image from source and check its PCR values, which identify the Verifier to Authenticators and RPs (Build and Measurement).
4.4.1 Request and Response
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 the deterministic CBOR encoding (RFC 8949 §4.2.1) of the Engine request. It is carried as a byte string, so the Verifier checks the Authenticator Assertion against exactly the bytes it received and passes them to the Engine unchanged.
Verifier request fields:
| Field | Type | Description |
|---|---|---|
version | uint | MUST be 1. |
payload | bytes | The encoded Engine request; see above. |
aud | Field element | The rpId of the request; MUST be less than $2^{64}$. |
nonce | Field element | The RP’s nonce for the request, sampled uniformly from the nonzero elements of $F$ with a CSPRNG. |
expires_at | uint | Unix seconds after which the Verifier rejects the request. MUST NOT be more than MAX_REQUEST_LIFETIME ahead of the Verifier’s time (Response Timing and Replay). |
billing_nullifier | Field element | The WIP-107 billingNullifier of the Authenticator’s registration for aud in an active billing period, or 0 if it holds none. |
aat_inputs | map, optional | Inputs for verifying a TAKT and an AAT; see below. |
billing_nullifier stays outside payload, so it never reaches the Engine and the AAT’s cdh does not cover it. The Verifier uses it only as WIP-107 §4.7 requires, and signs the value that section determines into the Flamingo Token, not necessarily the one received.
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 inputs:
| 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 | The Verifier’s time, in Unix seconds (Attestation and Channel). |
min_build_version | uint32 | aat_inputs.min_build_version, set as the consuming protocol defines (Consuming Flamingo Tokens). |
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. |
The response is a deterministic CBOR map with exactly the fields of one variant below; token is present if and only if it is a Success.
Success fields, when the Engine returned Pass:
| Field | Type | Description |
|---|---|---|
token | bytes | The Flamingo Token (Flamingo Token). |
signing_key_attestation | bytes | The latest attestation of the token signing key (Attestation and Channel). |
debug_report | bytes, optional | The Engine response’s debug_report, if present. |
Failed fields, when the Engine returned an error or the Verifier rejected the request without a Pass:
| Field | Type | Description |
|---|---|---|
error | text | Implementation-defined and bounded in size. The Engine’s error, passed through, or the Verifier’s own when it rejected the request. |
debug_report | bytes, optional | The Engine response’s debug_report, if present. |
- The response MUST be encrypted to the requester and MUST be padded to a fixed length, hiding the outcome and the report size.
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.
4.4.2 Attestation and Channel
- Keys. At boot, the Verifier MUST generate a channel key and a Flamingo Token signing key inside the enclave, from its hardware RNG, and MUST NOT export or persist either. It SHOULD replace its channel key frequently, e.g. at each refresh, and accept the previous key for
MAX_REQUEST_LIFETIMEafterwards. - Documents. The Verifier MUST attest each key in its own Nitro attestation document, a
COSE_Sign1chaining to the AWS Nitro Root G1, withpublic_keyanduser_dataas in the table below. A document’stimestampis its creation time, in milliseconds since the Unix epoch.
Attestation documents:
| Document | public_key | user_data | Used by |
|---|---|---|---|
| Channel attestation | SHA-256(CHANNEL_KEY_DOMAIN || channel_key) | The loaded release_hash. | The Authenticator, to check the Verifier and encrypt a request; discarded afterwards. |
signing_key_attestation | The Flamingo Token signing key (kid) | — | RPs, with every Flamingo Token. |
- Manifest. The Verifier MUST set the channel attestation’s
user_datato therelease_hashit computed from the manifest it loaded (Sandboxed Engine), and MUST send that manifest, as received, with the attestation. - 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. - Refresh. The Verifier MUST re-attest its channel key and its signing key at least every
MAX_ATTESTATION_AGE, and return its latestsigning_key_attestation. - Time. The Verifier MUST sync its clock to the
timestampof each new Nitro attestation and advance it on its monotonic clock in between. It MUST NOT use any other time source.
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, a fresh X-Wing encapsulation key; the rest is the Request and Response. - Rejection. The Verifier MUST reject a request shorter than 1216 bytes or with an invalid
response_key.
4.4.3 Request Processing
The host MAY drop requests above a size limit it chooses. For each sealed request it relays, the Verifier, in order:
- Checks. After decrypting (Channel Encoding), it MUST reject a request that does not meet the Verifier request (Request and Response) and Engine request (Request and response) formats, including their bounds and their references to the loaded release.
- Expiry and replay. It MUST reject an expired or replayed request (Response Timing and Replay).
- Authenticator Assertion. If
aat_inputsis present, every check of WIP-106 §3.8 MUST hold, or it rejects the request. The TAKT, the AAT, theassertion_keyandbuild_versionMUST NOT leave the Verifier. - Billing. It MUST check
billing_nullifieras WIP-107 §4.7 requires, or it rejects the request. - Engine call. It MUST pass the Engine request to the sandboxed Engine unchanged, and nothing else (Sandboxed Engine).
- Response. On
Pass, it signs a Flamingo Token (Flamingo Token). It then returns aSuccessorFailedresponse (Request and Response), released at a response slot (Response Timing and Replay).
4.4.4 Response Timing and Replay
The host sees when each response leaves the Verifier; slots and replay rejection bound what that reveals (Security item 9).
- Slots. Each pipeline’s
response_slotslist its release times in milliseconds after a request’s arrival (Release and Release Manifest). - Release. The Verifier MUST hold each response until the first slot at or after the Engine’s finish, measured on its monotonic clock from the request’s arrival, and MUST NOT release it earlier, whatever the outcome. If the Engine has not finished by the last slot, the request fails and the Verifier MUST reset the Engine instance. Rejections before the Engine call MAY be released immediately; they do not depend on the entries.
- Choosing slots. Engine Providers SHOULD put the first slot at or above the pipeline’s fastest complete run and the last above its 99.9th-percentile run time, with slots in between following its run-time distribution.
- Expiry. The Verifier MUST reject a request whose
expires_athas passed or is more thanMAX_REQUEST_LIFETIMEahead of its time (Attestation and Channel). - Replay. The Verifier MUST reject a request whose (
aud,nonce) it has already processed (Security item 9). It keeps each pair until the request’sexpires_at, so the set holds at mostMAX_REQUEST_LIFETIMEof requests. A reboot discards the set with the channel keys, so no older request can be opened.
4.4.5 Sandboxed Engine
- Loading. At start-up, the Verifier receives exactly one Engine bundle and its release manifest from the host. It MUST check that the bundle hashes to the manifest’s
engine_hashand that the manifest is well-formed (Release and Release Manifest), without verifying its signature, and MUST NOT fetch anything else. - Sandbox. The Verifier MUST run the Engine with no network access, no writable persistent storage, no process creation, no access to the Verifier’s keys, nothing outside its bundle, and only one bounded request-response channel. See the reference implementation’s sandbox for one way to meet these requirements.
- Per request. Before each request, the Verifier MUST reset the Engine to a state no earlier request has influenced, e.g. by starting a new instance or restoring one prepared before the first request. The reset MUST NOT depend on the Engine’s cooperation, e.g. the Verifier restarts the Engine process itself. It MUST bound the Engine’s memory, and MUST return an
errorif the Engine exceeds that bound, crashes, or returns anything other than an Engine response (Request and response). Its runtime is bounded by the last response slot (Response Timing and Replay).
4.4.6 Build and Measurement
The Verifier’s measurement MUST be verifiable from source:
- Publication. For each published Verifier measurement, an immutable reference to its source, e.g. a commit hash in a public repository, and its build instructions MUST be publicly available.
- 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.
- Configuration. Everything that affects the Verifier’s behavior MUST be in the EIF. The host supplies only the Engine bundle, its release manifest (Sandboxed Engine) and requests.
4.5 Flamingo Token
On a Pass, the Verifier builds a Flamingo Token from the request, the verified AAT, if any, and the loaded release, as specified below.
Envelope. 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 below.
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(data)) of the entry compare[k] names, over its data only, 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 | Public TAKT and AAT values, packed as below; 0 without an AAT. |
has_aat | -90008 | 1 if the Verifier verified an AAT, otherwise 0. |
now | -90009 | The Verifier’s time when it signed the token, in Unix seconds. |
engine_config_hash | -90010 | The Engine configuration that ran; see below. |
compare_sources | -90011 | Which pipeline input each compared entry is; see below. |
billing_nullifier | -90012 | The WIP-107 billingNullifier the Verifier checked for a billed RP, or 0 for an RP that is not billed. |
aat_flags,has_aat,nowandcompare_sourcesare 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.- Byte
kofcompare_sources, least significant first, isi + 1ifcompare[k]ispipeline_inputs[i], and0otherwise, including whencomparehas no positionk. engine_config_hashisH_4(DS_WIP_201_ENGINE; R(release_hash), pipeline_id, match_strictness)per WIP-100 §3.3.2, with $k = 3$.- The Verifier MUST set
billing_nullifieras WIP-107 §4.7 requires, whatever value the request carries.
aat_flags bits. aat_flags packs public values of two WIP-106 objects into one field element: the TAKT’s sec_flags layout (bits 0–51), with the public min_build_version in place of the private build_version, and the AAT’s authenticator_meta in bits WIP-106 leaves 0.
| Bits | Value | From |
|---|---|---|
| 0–7 | platform | TAKT sec_flags |
| 8–15 | sec_level | TAKT sec_flags |
| 16–47 | min_build_version | aat_inputs, in place of the TAKT’s build_version |
| 48–51 | sec_meta | TAKT sec_flags |
| 52–56 | authenticator_meta | AAT |
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, compare_sources, engine_config_hash,
billing_nullifier)
per WIP-100 §3.3.2, with $k = 13$.
4.6 Authenticator
The Authenticator MUST accept only PCR0–2 values its Authenticator Provider ships with it, e.g. in the app release or as signed configuration. Before sending biometrics, it completes these steps in order, and MUST NOT send biometrics if one fails:
-
Attestation. It MUST check that the channel attestation (Attestation and Channel):
- 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, which mark debug mode, and has PCR0–2 it accepts;
- has
public_key = SHA-256(CHANNEL_KEY_DOMAIN || channel_key)for thechannel_keyit encrypts to; - has
user_dataequal to therelease_hashof the manifest the Verifier sent (Release and Release Manifest); - has a
timestampat mostMAX_ATTESTATION_AGEold.
It MAY reuse a checked attestation for later requests while these hold.
- is a valid
-
Configuration. It MUST choose a pipeline the loaded release defines and whose
inputsthe device can produce, and amatch_strictnessthe release lists, following the consuming protocol’s requirements or, where it sets none, the Authenticator Provider’s policy. It MUST buildpipeline_inputsandcompareso that they give thecompare_sourcesthe consuming protocol requires (Flamingo Token). If no pipeline and level qualify, it MAY use another Verifier instance. -
Response timing. It MUST NOT use a pipeline whose
response_slots(Response Timing and Replay) list more release times than its configured maximum. The maximum SHOULD be at most 8, so a response’s timing reveals at most 3 bits (Security item 9). -
Seal. It MUST seal
response_key || requesttochannel_key(Channel Encoding), send it, and open the response withresponse_key.
4.7 Consuming Flamingo Tokens
A consuming protocol (Terminology), e.g. WIP-202, defines how an RP requests and uses Flamingo Tokens. An RP MUST NOT accept a Flamingo Token, directly or through the protocol’s proof, unless every check below holds.
Checks. The checks use the Flamingo Token’s claims (Flamingo Token), with aat_flags unpacked into platform, sec_level, min_build_version, sec_meta and authenticator_meta, its signature sig, and the signing key verifier_key. A proof that hides the token MUST enforce checks 2, 3 and 8. For the other checks, it either reveals the values they need or takes the RP’s accepted sets as public inputs.
-
Attestation. 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, which mark debug mode; - has PCR0–2 the RP accepts;
- attests
verifier_key(kidis only a hint); - has a
timestampat mostMAX_ATTESTATION_AGEold.
An RP that receives the attestation MAY cache it by key; it identifies the Verifier instance, not the user.
- chains to the AWS Nitro Root G1, with every certificate valid at its
-
Signature.
sigverifies underverifier_keyper WIP-100 §3.5.1, over the signed digest computed from the values (Flamingo Token), withaat_flags = platform + 2^8 * sec_level + 2^16 * min_build_version + 2^48 * sec_meta + 2^52 * authenticator_meta. -
Well-formed values.
has_aatis0or1. Theaat_flagssub-fields are range-checked to their widths, with theuser_presencebits ofauthenticator_metabelow5.compare_sources < 2^32. Withhas_aat = 0,trust_anchor_key_hashand theaat_flagssub-fields are0. -
Request.
audis the RP’s own;nonceis one the RP issued, has not expired and has not been used before; andnowis recent by the RP’s clock. -
Authenticator Assertion. With
has_aat = 0, the RP accepts tokens without an AAT. Otherwisemin_build_version,trust_anchor_key_hashand theaat_flagssub-fields pass WIP-106 §4. -
Engine.
engine_config_hashis the hash of a release,pipeline_idandmatch_strictnessthe RP accepts, from an active release manifest it verified under an active key of the manifest’siss(Engine Provider Metadata), including the RP’s minimumversionperiss. A circuit MAY take the hash’s components as private values and recompute it, to constrain them. -
Statement. The compared entries are the ones the consuming protocol’s statement depends on:
compare_sourcesis the value it requires, e.g.256, bytes[0, 1, 0, 0]: the entry compared at position1is pipeline input0, and the others are references. Eachcompared_entry_hashthe statement depends on is bound to the protocol’s own inputs, e.g. a Credential claim or the RP’s challenge. -
Billing.
billing_nullifieris0, or the consuming protocol binds it to the World ID the Flamingo Token is used for, as WIP-202 constraint 7 does.
5. Trust Assumptions
The protocol assumes the isolation, measurement and attestation of AWS Nitro Enclaves are sound. If they fail, neither privacy nor integrity holds. Support for other TEEs, so that AWS is not the only option, is a Future Consideration. Every party in the table below is trusted only for the listed parts, and a compromise of one only affects those parts, except AWS.
Parties:
| 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, keeping each pipeline stable across releases, and documenting what each level means. | Privacy: the sandbox keeps the Engine from exporting data. |
| Authenticator Provider | Verifying the Authenticator’s integrity, as stated in the TAKT, and the Verifier measurements its Authenticators accept. | — |
| 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 (Response Timing and Replay). |
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: Sandboxed Engine and Response Timing and Replay bound what they learn per request (Security item 9).
6. Rationale
- Match strictness instead of scores (Match Strictness Levels). 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. Levels apply to comparisons, which every pipeline of a release shares, so the release defines them.
- A separate Engine Provider signs the Engine (Release and Release Manifest). Models may come from different parties, and trust in the models stays separate from trust in the capture environment. A pipeline keeps its meaning across an Engine Provider’s releases (Release Versioning).
- Only the RP checks manifest signatures (Release and Release Manifest). Who signed a release matters only for whether its results are trusted, which the RP decides; the sandbox protects privacy whoever wrote the Engine. So Authenticators need only accepted PCRs, and the Verifier image contains no Engine Provider keys, so one image serves every Engine Provider.
SHA-384for the Engine bundle (Release and Release Manifest). Only the Verifier computesengine_hash, over the bundle file, and AWS Nitro Enclaves measures images withSHA-384too, so the Verifier’s measurement and its bundle check share one algorithm. Every other hash in this WIP isSHA-256, because it is reduced into the field withR(d)or is defined by another spec, e.g.cdhby WIP-106.- Minimum versions and stable pipelines (Release Versioning). RPs accept a minimum version per
issinstead of exact releases, so a new release reaches them without changing their requests, and Verifiers can move to it. A breaking change gets a newpipeline_idvalue, which RPs opt into. As with FIDO metadata’sauthenticatorVersionand SUIT sequence numbers, the version is a plain integer that only orders releases. issas the Engine Provider identifier (Engine Provider Metadata). As in JWT (RFC 7519) and OAuth metadata (RFC 8414), the signer is named by anhttpsURL from which its metadata location follows. Each manifest repeats it, so a manifest stays bound to its provider wherever it is read.- The AAT is verified in the Verifier (Request and Response). The Verifier is a WIP-106 §3.8 verifier: it keeps the Private inputs confidential and signs the Public values into the token, so RPs apply their WIP-106 §4 policy as usual.
cdhcovers the exactpayload, so the AAT vouches for exactly the compared input. - The AAT is optional (Request and Response). 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.
- Opaque entries (Request Processing). The Verifier never decodes entries; only the Engine interprets them. A new capture method or image format then needs a new Engine release, not a new Verifier image, request or token format.
- Listed slots per pipeline (Response Timing and Replay). A list rather than a formula lets each Engine fit its slots to its run times, and in the signed manifest the latency trade-off is public. Slots are per pipeline because run times differ; shared slots would let a slow pipeline’s early exits stand out.
- Request expiry (Response Timing and Replay). Replay rejection needs a set of processed (
aud,nonce) pairs, and the set has to stay bounded. Anexpires_atin the sealed request lets the Verifier forget each pair once its request expires.MAX_REQUEST_LIFETIMEcaps the expiry, because anyone can seal a request to the public channel key. The Verifier takes its time from its attestations, so it needs no clock the host sets. - Hashes over the raw images (Flamingo Token).
compared_entry_hashis theSHA-256of the image bytes the Verifier received. Other parties can compute the same hash: the RP hashes its challenge image, and a PoH Credential already lists theSHA-256of its face image. Embeddings wouldn’t work, because only the Engine can compute them. - Signature over claim values, not CBOR bytes (Flamingo Token). The signature covers a Poseidon2 hash of the claim values, so a circuit verifies the Flamingo Token without parsing CBOR.
- Verifier time in the token (Flamingo Token). The Verifier checks the AAT at its own time and signs that time as
now, so an Authenticator cannot pass off an expired TAKT or AAT by stating an old time. Every token carries it, so RPs can check a token’s age with or without an AAT; the single-usenoncestill makes it fresh. - Engine configuration hash (Flamingo Token). 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. Its release input hashes the manifest’s JWS Payload rather than the bundle, so the pipelines and slots the Verifier enforces are the ones the RP accepted, and re-signing a release keeps the hash stable.
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.
- Exact
release_hashlists. The RP accepts only listed releases. Rejected: every release would require RPs to update their accepted sets, and Verifiers to keep old releases loaded until they do. - Semantic versions.
MAJOR.MINOR.PATCHwith breaking changes in a new major. Rejected: the only boundary RPs need is “breaking or not”, which a newpipeline_idvalue expresses without range rules.
8. Security
- Post-quantum channel (Attestation and 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.
- Classical token signatures (Flamingo Token). A quantum computer that recovers the Verifier’s signing key can forge Flamingo Tokens for unspent nonces, claiming comparisons no Engine ran. RPs MUST stop accepting
BabyJubJub-EdDSA-Poseidon2tokens before that becomes practical, and migrate to a post-quantum scheme. - Authenticator integrity and missing AATs (Request and Response). 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. - Unchecked entries (Flamingo Token). An entry the pipeline did not check, e.g. a replayed photo, can be compared like any other.
compare_sourcesshows which compared entries the pipeline checked, so a consuming protocol that requires a checked live capture rejects tokens where it was only compared. - Unreviewed Engines (Release and Release Manifest). A host may load any Engine, and the channel attestation reveals which one. Authenticators do not check who signed it, so they may send biometrics to an unreviewed Engine; the sandbox still keeps it from exporting data (item 6), and its Flamingo Token carries an
engine_config_hashno RP accepts. A leaked release key lets an attacker publish release manifests RPs accept until the Engine Provider marks the keyrevokedand RPs refresh its metadata (Engine Provider Metadata). RPs accept new releases unreviewed, so a leaked key or a faulty release reaches them at once; the Engine Provider withdraws a release by removing its manifest, and RPs MAY raise the minimumversionthey accept. - Closed Engine (Sandboxed 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 (Consuming Flamingo Tokens).
compared_entry_hashof a credential entry is constant per user, and a nonzerobilling_nullifierper user, RP and billing period, so together withaudandnoncea Flamingo Token links a user to a specific RP request. Consuming protocols MUST keep Flamingo Tokens private, e.g. as private inputs to WIP-202. - Verifier keys (Attestation and Channel). 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 (Response Timing and Replay). The host sees when each response leaves the Verifier, and a malicious Engine can modulate its run time with the input. Releasing responses only at slots limits each request to revealing its slot, at most $\log_2 n$ bits for $n$ slots, a budget the Authenticator checks before sending biometrics (Authenticator). Replay rejection keeps the bound per request: otherwise the host could replay one request, and the Engine could leak a different part of the input on each run. An Engine that crashed the enclave could signal outside the slots, so the Verifier bounds the Engine’s memory and turns any Engine failure into an
errorreleased at a slot (Sandboxed Engine). Rejections before the Engine call depend only on the request’s structure, its expiry, the replay set and the AAT, so they are released immediately. 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. - Stale measurements (Authenticator). An Authenticator keeps accepting a compromised Verifier measurement until its Authenticator Provider removes it. Shipping the accepted PCRs as signed, expiring configuration, rather than only with app releases, shortens that window.
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 Channel.
- Several Engines per Verifier. A Verifier that loads more than one release. As for pipelines today, the Authenticator would choose among them, and the RP accepts only the configurations it allows (Consuming Flamingo Tokens check 6).
- Published measurements. A standard format for publishing Verifier PCR values with a status, from which RPs and Authenticator Providers build their accepted sets.
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_id = 1, match_strictness = 2, empty hints, three entries with empty meta and data "credential image", "live image" and "challenge image" (ASCII), pipeline_inputs = [1], and compare = [0, 1, 2]. The loaded release has release_hash 32 bytes of 0x2a. The pipeline’s only input has role live_frame, and compare_sources = 256. The Verifier’s time now is 1783446000 in both vectors.
With an AAT
payload = a66568696e74734067636f6d706172658300010267656e747269657383a264646174615063726564656e7469616c20696d616765646d65746140a264646174614a6c69766520696d616765646d65746140a264646174614f6368616c6c656e676520696d616765646d657461406b706970656c696e655f6964016f706970656c696e655f696e707574738101706d617463685f7374726963746e65737302
cdh = 0x03ca8744a87789b5684fd3569cc3ac735de925464f2cd29e87bdfc2a45591d27
aud = 1928118
nonce = 42
now = 1783446000 (Verifier time)
min_build_version = 0
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 = 8443a10126a058a3a6034800000000001d6bb6041a6a4d3d8d0a5820000000000000000000000000000000000000000000000000000000000000002a190109781c68747470733a2f2f776f726c642e6f72672f6561742f6161742f76313a0001387f582003ca8744a87789b5684fd3569cc3ac735de925464f2cd29e87bdfc2a45591d273a000138805820000000000000000000000000000000000000000000000000000000000000000a58403c24fd0eb59bdcb7ca3177348f69f7ad45affbce19eb3aa85b1a51dcc3499683e84559b276e8bf5d1b6b7e28d2d45dd7ad86eb3e90c09292370f7e2c27feb09f
(exp 1783446925; authenticator_meta 10: user_presence 2, provider bits 0b01)
compare_sources = 256
billing_nullifier = 0
engine_config_hash = 0x2f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d
has_aat = 1
aat_flags = 0x00a3000000000102
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 = 0x24a02c43c27878dc6623224138ec390efe9044be4271c8a2f82bb5a78910bc4f
kid = f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12
sig = 60c450c9c5a170c975d8871958997c0cbbaf8295218cd97ea4745b18ed5587a24dc37ee1ef39bcf8783a56e1097b9b4cad7904a426b6e69ab05595d8fd835d04
cwt = 84582aa2013a00010000045820f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12a05901acae190109782168747470733a2f2f776f726c642e6f72672f6561742f666c616d696e676f2f76313a00015f8f582021e9fe96ff18807a727830c56281c96af824005400bcb8ef0f463b34cb20c4153a00015f905820010cff8544f0007179c076ddb49de551bd5743797bfe05cb3b26f55c696f507b3a00015f9158202abb4a81875eb0e308f0fbfb8c0a0f57e77243e095b145ae7973e0b2771e044d3a00015f92582000000000000000000000000000000000000000000000000000000000000000003a00015f93582000000000000000000000000000000000000000000000000000000000001d6bb63a00015f945820000000000000000000000000000000000000000000000000000000000000002a3a00015f95582014b904063236db16ecda3a3fa9aaa42b3f605706baa2fdb25afeab8df8f252d83a00015f961b00a30000000001023a00015f97013a00015f981a6a4d39f03a00015f9958202f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d3a00015f9a1901003a00015f9b58200000000000000000000000000000000000000000000000000000000000000000584060c450c9c5a170c975d8871958997c0cbbaf8295218cd97ea4745b18ed5587a24dc37ee1ef39bcf8783a56e1097b9b4cad7904a426b6e69ab05595d8fd835d04
Without an AAT
Same payload, aud, nonce, compare_sources, engine_config_hash and billing_nullifier, with has_aat = 0, trust_anchor_key_hash = 0 and aat_flags = 0.
digest = 0x045442bd20ef9bbcf96e1d5534508467891d17532204659aa56e80c8a5ff0b06
sig = 34ba37c01cf069bc44799804624a3167b200adb62b90349526decc1ec1966296935b04a880fe4cc5e1f0a6b481075395837af053456ca3baba9575bec055b100
cwt = 84582aa2013a00010000045820f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12a05901a4ae190109782168747470733a2f2f776f726c642e6f72672f6561742f666c616d696e676f2f76313a00015f8f582021e9fe96ff18807a727830c56281c96af824005400bcb8ef0f463b34cb20c4153a00015f905820010cff8544f0007179c076ddb49de551bd5743797bfe05cb3b26f55c696f507b3a00015f9158202abb4a81875eb0e308f0fbfb8c0a0f57e77243e095b145ae7973e0b2771e044d3a00015f92582000000000000000000000000000000000000000000000000000000000000000003a00015f93582000000000000000000000000000000000000000000000000000000000001d6bb63a00015f945820000000000000000000000000000000000000000000000000000000000000002a3a00015f95582000000000000000000000000000000000000000000000000000000000000000003a00015f96003a00015f97003a00015f981a6a4d39f03a00015f9958202f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d3a00015f9a1901003a00015f9b58200000000000000000000000000000000000000000000000000000000000000000584034ba37c01cf069bc44799804624a3167b200adb62b90349526decc1ec1966296935b04a880fe4cc5e1f0a6b481075395837af053456ca3baba9575bec055b100
Billed RP
Same as With an AAT, with billing_nullifier = 7.
digest = 0x247f7f653466f20e24fbac90c9c36055c7036d2c8d26c100907164723c7bb118
sig = 40b3fbfc863edd8d56a30459aab04ce582e8bd41ebc7dae538dbd311ba6326234ceaff9d5d9dabfb04c7d8413e9df587215b91a2b4d2c3648e2b79513a79b402
cwt = 84582aa2013a00010000045820f2d46cc6aa89d38add66256beb34e089ebd1e33951c5085b3c228ed5dd8f1c12a05901acae190109782168747470733a2f2f776f726c642e6f72672f6561742f666c616d696e676f2f76313a00015f8f582021e9fe96ff18807a727830c56281c96af824005400bcb8ef0f463b34cb20c4153a00015f905820010cff8544f0007179c076ddb49de551bd5743797bfe05cb3b26f55c696f507b3a00015f9158202abb4a81875eb0e308f0fbfb8c0a0f57e77243e095b145ae7973e0b2771e044d3a00015f92582000000000000000000000000000000000000000000000000000000000000000003a00015f93582000000000000000000000000000000000000000000000000000000000001d6bb63a00015f945820000000000000000000000000000000000000000000000000000000000000002a3a00015f95582014b904063236db16ecda3a3fa9aaa42b3f605706baa2fdb25afeab8df8f252d83a00015f961b00a30000000001023a00015f97013a00015f981a6a4d39f03a00015f9958202f0bcb30b9051ec78ed6ac1bb206c612546c6ad9872ad0463e55984e2c78ad1d3a00015f9a1901003a00015f9b58200000000000000000000000000000000000000000000000000000000000000007584040b3fbfc863edd8d56a30459aab04ce582e8bd41ebc7dae538dbd311ba6326234ceaff9d5d9dabfb04c7d8413e9df587215b91a2b4d2c3648e2b79513a79b402
Appendix A2. Examples
This appendix is non-normative. The release and request match the vectors of Appendix A1.
Release manifest payload (Release and Release Manifest):
{
"iss": "https://engines.example.com",
"engine_hash": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
"version": 7,
"match_strictness_levels": [1, 2, 3],
"pipelines": [
{
"pipeline_id": 1,
"inputs": [{ "role": "live_frame", "media_types": ["image/jpeg", "image/png"] }],
"response_slots": [500, 1587, 5039, 16000]
}
]
}
Engine request, in CBOR diagnostic notation (RFC 8949 §8), the payload of Appendix A1 (Request and response):
{
"hints": h'',
"compare": [0, 1, 2],
"entries": [
{"data": h'63726564656e7469616c20696d616765', "meta": h''},
{"data": h'6c69766520696d616765', "meta": h''},
{"data": h'6368616c6c656e676520696d616765', "meta": h''}
],
"pipeline_id": 1,
"pipeline_inputs": [1],
"match_strictness": 2
}
Entry 1 is the pipeline’s live_frame input and is also compared; entries 0 and 2 (a Credential image and a challenge) are only compared, so compare_sources is 256.
Engine response (Request and response): Pass, with no debug_report, if the live frame passes the pipeline’s checks and all three entries match each other at level 2; otherwise an error, e.g. "presentation attack detected".