Skip to main content
This is the protocol the Nito Browser App implements, written down for anyone building their own Encrypted client against POST /v1/chat/completions. The gateway relays what you send and checks its shape. The cryptography runs on your device and inside the sealed hardware.

Variants

The attestation you fetch before the call selects the variant. Read its proof_format. The same marker may also appear inside the raw evidence.

Keys

The model’s public key comes from the attestation as model_public_key, with signing_algo naming its type: ed25519 (32 bytes) or ecdsa on secp256k1 (64 bytes, or 65 with the 04 prefix). Verify the evidence before using it. See Verify It Yourself. Your key pair matches the model key’s type and lives for the conversation:
  • ed25519: generate an ed25519 key pair. The header carries the 32-byte public key as hex. For key agreement, convert your secret key and the model’s public key to their X25519 forms.
  • ecdsa: generate a secp256k1 key pair. The header carries the uncompressed 65-byte public key as hex. A 64-byte model key is prefixed with 04 before use.
An ephemeral key per message. Every encryption generates a fresh ephemeral key pair of the same type, so no two envelopes share a symmetric key.

Encrypting a Message

For each messages[].content string:
  1. Agree a shared secret between the ephemeral secret key and the model’s public key. ed25519: X25519 over the converted keys. ecdsa: the x coordinate (32 bytes) of the secp256k1 ECDH point.
  2. Derive the symmetric key with HKDF-SHA256: input the shared secret, no salt, 32 bytes out, with the info string ed25519_encryption for ed25519 or ecdsa_encryption for ecdsa.
  3. Encrypt the UTF-8 plaintext. Standard and Legacy with an ed25519 key: XChaCha20-Poly1305 with a random 24-byte nonce. Every other case: AES-256-GCM with a random 12-byte nonce. On the Authenticated variant, pass the request’s additional authenticated data (below).
  4. Build the envelope: the ephemeral public key, the nonce, then the ciphertext with its authentication tag, concatenated and hex encoded. Replace the content string with it.
Content must be a nonblank string. Content arrays are rejected.

Additional Authenticated Data (Authenticated variant)

Generate one request nonce of 24 random bytes as hex, as the Browser App does (the gateway requires at least 16 characters), and a timestamp of the current Unix time in seconds, which the backend may reject when stale. Send both in the headers and bind them into every envelope:
m is the zero-based position of the message in messages, and c is - for a string content. For model, bind the attestation’s provider_model_id with any privacy suffix removed and, where it carries a backend prefix, rewritten to the model maker’s prefix (for example z-ai/glm-5.3). That is what the Browser App binds.

Headers

Older spellings of these headers are accepted during migration. If the same header is sent under two names with different values, the request is rejected.

Decrypting the Reply

choices[].message.content comes back as an envelope of the same shape. Split off the ephemeral public key and the nonce by the lengths above, agree the shared secret between your conversation secret key and that ephemeral public key, derive the symmetric key with the same HKDF parameters, and decrypt. A reasoning summary, where the model returns one, is an envelope too. On the Authenticated variant the reply’s additional authenticated data is:
with id taken from the response body. The backend’s model value is not fixed, so try candidates in this order until one authenticates: the response body’s model, the value you bound on the request, the attestation’s provider_model_id, and the public model ID, each with any privacy suffix removed and, where it carries a backend prefix, its model-maker alias. Try the response id and an empty id. For an ecdsa reply the Browser App accepts a key derived with either the ecdsa_encryption info string or secp256k1-aes-256-gcm-hkdf-sha256, and the algorithm spellings ecdsa, ecdsa-secp256k1, or secp256k1-aes-256-gcm-hkdf-sha256. Mirroring that candidate search is the conservative choice for a new client. With stream: true on the Standard variant, each delta’s content is an envelope. Reassemble the stream and decrypt. On the Authenticated variant use the non-streaming path, as the Browser App does.

Response Signature Evidence

On supported backends, nito-encrypted-response-evidence: 1 with stream: false returns the original upstream request and response bytes plus signature proof. Your client verifies the signature and its binding to fresh attestation before consuming a reply that requires this assurance. See Response Evidence for fields, verification steps, and limits.

The Encrypted Tool Loop

With nito-encrypted-encrypt-all-fields: 1 on the Standard variant, function tools work with every field encrypted. Encrypt, as separate envelopes, each tool’s function.name, function.description, and function.parameters (the JSON schema, serialized to a string), any tool_choice function name, and, on later turns, each assistant tool_calls[].function.name and .arguments and each tool message’s content. The reply’s tool calls arrive with the id in plaintext and the name and arguments as envelopes. Decrypt them, run the tool on your side, encrypt the result, and send the next request. Only function tools are accepted in this mode.

What the Gateway Rejects

All return 400 invalid_request before any provider call.
  • Any Encrypted header without nito-encrypted: 1.
  • The headers with a model that is not an :encrypted entry, or an :encrypted model without the headers.
  • A signing algorithm other than ed25519 or ecdsa.
  • A version other than 2, or a version on the Legacy variant.
  • A nonce or timestamp on the Legacy variant.
  • A missing nonce, a nonce shorter than 16 characters, or a missing timestamp on the Authenticated variant.
  • nito-encrypted-encrypt-all-fields off the Standard variant, or with a value other than 1.
  • Content arrays, a content string that is not nonblank even-length hex, and raw image, file, or document parts.
  • web_search or web_fetch on the request, prompt caching, and uncensored=true.
  • Encrypted headers on /v1/responses, /v1/messages, /v1/messages/count_tokens, or MCP.
A provider reply whose content, reasoning summary, or tool-call fields are not even-length hex is rejected by the gateway and never returned to you.

Libraries

The Browser App uses the @noble/curves, @noble/ciphers, and @noble/hashes packages for the operations above.

Encrypted on the API

The reference page.

Get Attestation

Where the key comes from.

How It Works

The call, step by step.