Skip to main content

Overview

With E2EE enabled:
  1. Your client fetches the model public key from attestation.
  2. Your client encrypts messages[].content locally.
  3. Gateway processes encrypted input and returns encrypted output.
  4. Your client decrypts response content locally.
This keeps plaintext prompt/response data on the client side.

Supported Endpoints

  • POST /v1/chat/completions

Required Headers

Always include:
  • X-Signing-Algo: ecdsa or ed25519
  • X-Client-Pub-Key: client public key (hex)
  • X-Model-Pub-Key: model public key from attestation (hex)
Recommended (strict v2):
  • X-E2EE-Version: 2
  • X-E2EE-Nonce: <unique, at least 16 chars>
  • X-E2EE-Timestamp: <unix seconds>

Step 1: Fetch Model Public Key (Attestation)

curl "https://api.redpill.ai/v1/attestation/report?model=phala/gpt-oss-20b&signing_algo=ecdsa" \
  -H "Authorization: Bearer $REDPILL_API_KEY"
Read signing_public_key from the response and use it as X-Model-Pub-Key.

Step 2: Generate Client Key Pair

Generate an ephemeral client key pair (matching X-Signing-Algo) for better forward secrecy.

Step 3: Encrypt Message Content

For encrypted fields (e.g. messages[i].content), use: ephemeral_public_key || nonce(12 bytes) || ciphertext Then hex-encode the result and place it into JSON.

v1 vs v2 (What’s Different)

Itemv1v2
Security modeLegacy compatibility modeStrict mode (recommended)
Extra headersNo nonce/timestamp requiredRequires nonce + timestamp
AAD bindingNot usedUsed for request/response context binding
Replay protectionNot enforced by protocol headersEnforced with nonce/timestamp validation
RecommendationFor legacy clients onlyDefault for all new integrations

Step 4A: Send E2EE Request (v1 Example)

curl "https://api.redpill.ai/v1/chat/completions" \
  -H "Authorization: Bearer $REDPILL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Signing-Algo: ecdsa" \
  -H "X-Client-Pub-Key: $CLIENT_PUB_KEY_HEX" \
  -H "X-Model-Pub-Key: $MODEL_PUB_KEY_HEX" \
  -d '{
    "model": "phala/gpt-oss-20b",
    "messages": [{"role": "user", "content": "<HEX_CIPHERTEXT>"}],
    "stream": false
  }'
Expected response headers typically include:
  • X-E2EE-Applied: true
  • X-E2EE-Version: 1
  • X-E2EE-Algo: ecdsa|ed25519

Step 4B: Send E2EE Request (v2 Example)

TS=$(date +%s)
REQ_NONCE=$(openssl rand -hex 16)

curl "https://api.redpill.ai/v1/chat/completions" \
  -H "Authorization: Bearer $REDPILL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Signing-Algo: ecdsa" \
  -H "X-Client-Pub-Key: $CLIENT_PUB_KEY_HEX" \
  -H "X-Model-Pub-Key: $MODEL_PUB_KEY_HEX" \
  -H "X-E2EE-Version: 2" \
  -H "X-E2EE-Nonce: $REQ_NONCE" \
  -H "X-E2EE-Timestamp: $TS" \
  -d '{
    "model": "phala/gpt-oss-20b",
    "messages": [{"role": "user", "content": "<HEX_CIPHERTEXT>"}],
    "stream": false
  }'
Expected response headers:
  • X-E2EE-Applied: true
  • X-E2EE-Version: 2
  • X-E2EE-Algo: ecdsa|ed25519

Step 5: Decrypt Response

Decrypt:
  • choices[*].message.content
  • choices[*].message.reasoning_content (if present)

End-to-End Python Example

import os
import time
import requests
from typing import Optional
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

BASE_URL = os.environ.get("BASE_URL", "https://api.redpill.ai").rstrip("/")
API_KEY = os.environ["API_KEY"]
MODEL_NAME = os.environ.get("MODEL_NAME", "phala/gpt-oss-20b")
HKDF_INFO = b"ecdsa_encryption"


def to_uncompressed(pub_hex: str) -> bytes:
    b = bytes.fromhex(pub_hex)
    if len(b) == 64:
        return b"\x04" + b
    if len(b) == 65 and b[0] == 0x04:
        return b
    raise ValueError(f"bad pubkey length: {len(b)}")


def derive_key(shared: bytes) -> bytes:
    return HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,
        info=HKDF_INFO,
    ).derive(shared)


def encrypt_for_model(plaintext: str, model_pub_hex: str, aad: Optional[str]):
    model_pub = ec.EllipticCurvePublicKey.from_encoded_point(
        ec.SECP256K1(), to_uncompressed(model_pub_hex)
    )
    eph_priv = ec.generate_private_key(ec.SECP256K1())
    eph_pub = eph_priv.public_key()

    shared = eph_priv.exchange(ec.ECDH(), model_pub)
    key = derive_key(shared)
    nonce = os.urandom(12)
    aad_bytes = aad.encode("utf-8") if aad else None
    ct = AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), aad_bytes)

    eph_pub_bytes = eph_pub.public_bytes(
        encoding=serialization.Encoding.X962,
        format=serialization.PublicFormat.UncompressedPoint,
    )
    return (eph_pub_bytes + nonce + ct).hex()


def decrypt_from_model(enc_hex: str, client_priv, aad: Optional[str]):
    blob = bytes.fromhex(enc_hex)
    eph_pub = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), blob[:65])
    nonce = blob[65:77]
    ct = blob[77:]

    shared = client_priv.exchange(ec.ECDH(), eph_pub)
    key = derive_key(shared)
    aad_bytes = aad.encode("utf-8") if aad else None

    pt = AESGCM(key).decrypt(nonce, ct, aad_bytes)
    return pt.decode("utf-8")


def client_pub_hex64(client_priv):
    pub = client_priv.public_key().public_bytes(
        encoding=serialization.Encoding.X962,
        format=serialization.PublicFormat.UncompressedPoint,
    )
    return pub[1:].hex()


def fetch_model_pubkey() -> str:
    r = requests.get(
        f"{BASE_URL}/v1/attestation/report",
        params={"model": MODEL_NAME, "signing_algo": "ecdsa"},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=60,
    )
    r.raise_for_status()
    data = r.json()
    key = data.get("signing_public_key")
    if not key:
        raise RuntimeError("signing_public_key not found in attestation response")
    return key


def main():
    model_pub_hex = fetch_model_pubkey()
    print(f"[OK] model pubkey length={len(model_pub_hex)}")

    client_priv = ec.generate_private_key(ec.SECP256K1())
    client_pub = client_pub_hex64(client_priv)

    nonce = "n" + str(int(time.time())) + "abcd1234abcd"
    ts = str(int(time.time()))
    prompt = "please only reply: REDPILL_E2EE_V2_OK"

    req_aad = f"v2|req|algo=ecdsa|model={MODEL_NAME}|m=0|c=-|n={nonce}|ts={ts}"
    enc_prompt = encrypt_for_model(prompt, model_pub_hex, req_aad)

    payload = {
        "model": MODEL_NAME,
        "stream": False,
        "messages": [{"role": "user", "content": enc_prompt}],
    }

    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "X-Signing-Algo": "ecdsa",
        "X-Client-Pub-Key": client_pub,
        "X-Model-Pub-Key": model_pub_hex,
        "X-E2EE-Version": "2",
        "X-E2EE-Nonce": nonce,
        "X-E2EE-Timestamp": ts,
    }

    r = requests.post(f"{BASE_URL}/v1/chat/completions", headers=headers, json=payload, timeout=120)
    r.raise_for_status()

    if r.headers.get("X-E2EE-Applied", "").lower() != "true":
        raise RuntimeError("X-E2EE-Applied is not true")

    data = r.json()
    enc_answer = data["choices"][0]["message"]["content"]

    resp_aad = (
        f"v2|resp|algo=ecdsa|model={data.get('model','')}|"
        f"id={data.get('id','')}|choice=0|field=content|n={nonce}|ts={ts}"
    )
    plain = decrypt_from_model(enc_answer, client_priv, resp_aad)
    print("decrypted:", plain)


if __name__ == "__main__":
    main()
Run:
pip install requests cryptography
export BASE_URL="https://api.redpill.ai"
export API_KEY="<your_key>"
export MODEL_NAME="phala/gpt-oss-20b"
python3 verify_e2ee.py

Common Errors

  • e2ee_header_missing
  • e2ee_invalid_signing_algo
  • e2ee_invalid_public_key
  • e2ee_model_key_mismatch
  • e2ee_invalid_version
  • e2ee_invalid_nonce
  • e2ee_invalid_timestamp
  • e2ee_replay_detected
  • e2ee_decryption_failed

Best Practices

  • Use v2 for all new integrations.
  • Use a unique nonce for every request.
  • Avoid logging plaintext prompts/responses.
  • Prefer ephemeral client keys.