Skip to content

Repository files navigation

Embedding Verifier

Rust workspaces for the embedding verifier host and secure enclave.

Structure

Two workloads over the same host/enclave shape — the DeepFace verifier and the DeepIdentifier migration. Each owns a top-level directory; what both would duplicate lives in shared/. Crate names compose from the path: deepface/host is deepface-host.

embedding-verifier/
├── Cargo.toml             # Host-side workspace  -> Cargo.lock
├── shared/
│   ├── attested-channel/  # Client↔enclave channel and the attestation it rests on; destined for pontifex
│   └── enclave-types/     # The vsock contract both workloads share: health, errors, key attestation
├── deepface/
│   ├── host/              # Axum HTTP API — the untrusted side of the boundary
│   ├── enclave/           # Nitro enclave workload — the trusted side. Own workspace -> own Cargo.lock
│   ├── types/             # The match's own vsock contract
│   ├── protocol/          # Match inputs and outputs; travels sealed, the host links none of it. Will likely move to `world-id-protocol`.
│   ├── client/            # Attestation-verifying client
│   └── e2e/               # End-to-end harness driving host and enclave together
└── di/                    # Skeleton — dirs and crates only, no behaviour yet
    ├── host/
    ├── enclave/           # Own workspace -> own Cargo.lock
    └── types/

Splitting the types is what keeps one workload out of the other's enclave image. di-host and di-enclave log and exit non-zero — a skeleton that idled would read as healthy. See Spec: DeepIdentifier Migration TEE Setup v1 for what goes in them.

Three workspaces, three lockfiles

Each enclave is its own cargo workspace. One lockfile for the whole repository meant a deepface-host dependency bump re-resolved the enclave graph and moved PCR0, which clients pin. Now an EIF's inputs are its own Cargo.toml, the Cargo.lock beside it, and the path crates they name.

attested-channel, enclave-types, deepface-protocol, deepface-types and di-types are in both an enclave graph and the host-side one. They are members of the root workspace but inherit nothing from it — not [workspace.dependencies], not [workspace.package] — so the root manifest cannot reach an enclave graph either. Treat them as standalone crates: write the version, and the edition, in their own manifest.

Package metadata matters as much as the dependency versions here. An inherited edition would change how an enclave compiles when the root workspace moved, and bumping the root [workspace.package].version would leave both enclave lockfiles stale against --locked.

face-engine now lives only in deepface/enclave, so it is the one build that needs a token for worldcoin/biometric-engines. CI runs the other lanes without one, which is what keeps that true.

Development

Every command takes a --manifest-path, because there are three workspaces:

# From the repository root — cargo-deny reads deny.toml from the working directory.
for ws in . deepface/enclave di/enclave; do
  cargo fmt    --manifest-path "$ws/Cargo.toml" --all -- --check
  cargo clippy --manifest-path "$ws/Cargo.toml" --all-targets --all-features --
  cargo test   --manifest-path "$ws/Cargo.toml" --all
  cargo deny   --manifest-path "$ws/Cargo.toml" --all-features check
done
# Run the host on http://localhost:8000
# ENCLAVE_CID and ENCLAVE_PORT are required; the process panics without them.
RUST_LOG=info ENCLAVE_CID=16 ENCLAVE_PORT=1000 cargo run --bin deepface-host
curl http://localhost:8000/health

# Run the secure enclave placeholder
RUST_LOG=info cargo run --manifest-path deepface/enclave/Cargo.toml --bin deepface-enclave

Building images

Each workload has a host image and an enclave image. build-docker.yml builds all four on every PR but publishes only the hosts — an enclave image is an input to the EIF, and it is the EIF's PCRs that clients attest.

# EIF + PCRs. Linux x86_64 + Docker; Nitro hardware only needed to run, not to build.
scripts/build-eif.sh --workload deepface   # -> target/eif/deepface-enclave.eif, deepface-pcrs.json
scripts/build-eif.sh --workload di         # -> target/eif/di-enclave.eif, di-pcrs.json

# Carrier image that launches an EIF on a Nitro node
docker build -f scripts/Dockerfile.carrier --build-arg EIF_FILE=di-enclave.eif target/eif

GIT_HUB_TOKEN and HUGGING_FACE_TOKEN are both deepface-only. A build now resolves one enclave's workspace rather than the whole repository, and nothing in di's graph is private.

di-enclave exits non-zero on start, so its EIF builds and measures but will not stay running, until the boot sequence lands.

Enclave assignment

POST /v1/enclave-assignment returns the enclave's encryption-key attestation and nothing else:

{ "attestation": "<base64 COSE_Sign1>" }

The enclave's identity (module_id) and expiry (the leaf certificate's notAfter) are read from the document after verifying it, never from fields the untrusted host could set.

Documents are served from an in-enclave cache. After boot starts background refresh, a task re-attests every 10 minutes (MAX_CACHED_AGE); requests always receive the last successful document immediately, including past MAX_CACHED_AGE if a refresh is in flight or has failed. Attest errors do not block serving until the document is older than MAX_SERVABLE_AGE (1 hour), at which point the refresh task exits and the enclave process exits. The cache is boot-scoped, so a restart takes it along and there is nothing for the host to invalidate.

deepface-client verifies the document — the COSE signature, the certificate chain up to the pinned AWS Nitro root, and the expected measurements. It is configured by a JSON file, in the shape world-id-protocol uses for an authenticator:

{
  "host_url": "http://localhost:8000",
  "allowed_pcr_configs": [
    [{ "index": 0, "value": "<PCR0 hex from deepface-pcrs.json>" }]
  ],
  "max_attestation_age_millis": 3600000,
  "allow_debug_measurements": false
}

Only host_url and allowed_pcr_configs are required; the rest have defaults. A configuration that pins no measurements is rejected — with nothing pinned, verification only proves a document came from some enclave. A --debug-mode enclave reports all-zero PCRs and its memory is readable from the parent instance, so it is rejected unless allow_debug_measurements is set.

deepface-e2e reads that file from VERIFIER_CONFIG and fetches its encryption key through the host, exercising the assignment route and the client together:

VERIFIER_CONFIG=./client.json cargo run --bin deepface-e2e -- <credential> <live> <challenge>

Matches

POST /v1/matches compares a credential image against a live frame and the RP's challenge frame. The host relays but cannot read either input:

{ "challenge_image_url": "https://…", "ciphertext": "<base64 enc || ciphertext>" }

ciphertext is the match inputs sealed to the enclave's attested encryption key — both images, hashes.json, and the AES-256-GCM key and IV for the challenge image. The challenge image itself never travels: the RP uploads it encrypted, and the host fetches that blob from challenge_image_url holding no key for it. A substituted URL or swapped object therefore fails inside the enclave rather than changing the result.

{ "response_ciphertext": "<base64 nonce || ciphertext>", "key_attestation": "<base64 COSE_Sign1>" }

The sealed response carries either a COSE_Sign1 match statement or the reason no statement was issued; key_attestation is the signing key's attestation, so a client can verify the statement it just received. Only the requester can open it — a second channel to the same enclave key cannot.

The host learns only that the enclave answered. Once a request has been opened there is a sealed channel to reply on, so everything the enclave discovers from that point — a malformed payload, an unusable hashes.json, an image refused on quality grounds, a below-threshold score, a challenge blob that would not decrypt — travels inside response_ciphertext. None of it reaches the status code.

Status Meaning
200 The enclave answered; the sealed payload holds the outcome
409 reassign_required The request did not open, so there was no channel to reply on; re-assign and re-seal, once
400 invalid_challenge_url The URL was rejected before any request was made
502 challenge_fetch_failed The challenge image could not be fetched
500 internal_error Enclave fault

409 is the only input failure with a status of its own, because with no channel open there is nothing to seal a reply into. Everything else the host might want — how often matches fail, how often the RP's objects are stale — has to come from enclave-side metrics rather than from status codes.

No SSRF destination control. challenge_image_url is only constrained in shape — HTTPS, a domain rather than an IP literal, no credentials, no redirects, a 5s timeout and a 4 MiB cap. Nothing pins where a fetch may go, so this endpoint must not take untrusted callers as-is. See TODO(SSRF) in deepface/host/src/challenge_fetcher.rs.

Nitro-enabled development host

Use an Amazon Linux 2023 EC2 instance type that supports Nitro Enclaves and launch it with Nitro Enclaves enabled. Limit inbound SSH access to your current public IP.

On the instance, install the development tools and Nitro Enclaves runtime:

sudo dnf install -y \
  git wget jq tmux tree unzip tar gzip \
  gcc gcc-c++ make cmake clang pkgconf-pkg-config openssl-devel \
  bubblewrap docker aws-nitro-enclaves-cli aws-nitro-enclaves-cli-devel

sudo usermod -aG ne "$USER"
sudo usermod -aG docker "$USER"
sudo systemctl enable --now nitro-enclaves-allocator.service
sudo systemctl enable --now docker

These commands follow the AWS Nitro Enclaves setup for Amazon Linux 2023.

The allocator defaults to 2 vCPUs and 512 MiB. Adjust /etc/nitro_enclaves/allocator.yaml before starting the service when the enclave needs more. Log out and reconnect after changing group membership, then verify the host:

nitro-cli --version
nitro-cli describe-enclaves
docker version

Install Rust and the components these workspaces use:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
rustup component add rustfmt clippy
for ws in . deepface/enclave di/enclave; do cargo test --manifest-path "$ws/Cargo.toml" --all; done

For private repository access, install and authenticate the GitHub CLI using its official RPM instructions.

Optionally install Codex for development on the remote host:

curl -fsSL https://chatgpt.com/codex/install.sh | sh
exec "$SHELL" -l
codex login --device-auth
codex doctor

To use the Codex desktop app, add a concrete host alias to your local ~/.ssh/config, confirm ssh <alias> works, then select the host and repository path under Settings > Connections. See the Codex remote connection guide.

About

embedding-verifier repository

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages