How Blindfold went from a 4-endpoint LLM-key proxy to a real, multi-industry secret proxy with in-enclave provider auth — what changed, why, and the impact.
Product feedback (Terminal 3 PM, competitive review): Blindfold ranked just outside the top 10. The two named gaps:
- Integration coverage / stack score is lower than the rest.
- The problem being solved isn't as concrete or distinct as other projects.
Both traced to the same root cause in the code.
The enclave's substitution was a blind string replace (contract/src/forward.rs):
.map(|(k, v)| (k.clone(), v.replace(SENTINEL, &secret)))That only works for one auth scheme — Authorization: Bearer <token>. So the
proxy could only ever route providers that share it. Unsurprisingly, all four
supported upstreams were LLM APIs (OpenAI, Anthropic, xAI, Groq). Meanwhile the
pitch claimed "your agent holds its OpenAI / Stripe / Anthropic key" —
Stripe and every non-LLM provider were unbacked. A judge scoring "integration
stack" saw four near-identical LLM endpoints, not a moat.
The instruction that shaped the fix: "I do not want generic — I want proper industry-based, not faking generic." A generic host-allowlist passthrough that keeps doing the dumb replace against more hostnames would look like breadth while adding none. Real breadth means teaching the enclave each provider's actual auth computation.
The enclave now applies the real auth computation for a provider, selected by
a typed, tagged AuthSpec:
| Scheme | Providers | What the enclave computes (inside TDX) |
|---|---|---|
bearer |
OpenAI, Anthropic, xAI, Groq, Stripe, GitHub, SendGrid, Slack, Gemini | sentinel → secret swap (Gemini swaps in x-goog-api-key, not Authorization) |
basic |
Twilio | base64(username : secret) — the secret is joined then base64-encoded in the enclave |
sigv4 |
AWS S3, AWS SES | full AWS Signature V4 — the secret signs a canonical request via an HMAC chain and is never transmitted |
The basic and sigv4 schemes are the proof that this is proper, not generic:
their secret is consumed by a computation, not pasted into a header. A generic
"swap the sentinel" proxy structurally cannot reach Twilio or AWS, because:
- Twilio's
base64(SID:token)must be computed after the secret is joined — you can't pre-place a sentinel inside a base64 blob. - AWS SigV4's signature is an HMAC over the request keyed by the secret; the secret appears nowhere in the outbound bytes.
So for a whole class of major APIs, the raw secret now never exists as a header value anywhere — it's only ever an input to a computation that runs in sealed TDX memory.
The auth logic lives in a dependency-isolated module (no wit/host imports) so it is unit-testable natively.
Not a generic router — a list of named, first-class integrations, each with its exact upstream host, its own sealed-secret name, and its auth scheme:
| Provider | Industry | Host | Sealed secret | Auth |
|---|---|---|---|---|
| OpenAI / Anthropic / xAI / Groq | LLM | api.openai.com, … | (default) | bearer |
| Gemini | LLM | generativelanguage.googleapis.com | gemini_api_key |
bearer via x-goog-api-key |
| Stripe | Payments | api.stripe.com | stripe_secret_key |
bearer |
| GitHub | Dev infra | api.github.com | github_token |
bearer |
| SendGrid | api.sendgrid.com | sendgrid_api_key |
bearer | |
| Slack | Comms | slack.com/api | slack_bot_token |
bearer |
| Twilio | Telephony | api.twilio.com | twilio_auth_token |
basic |
| AWS SES | Cloud | email.<region>.amazonaws.com | aws_secret_access_key |
sigv4 |
| AWS S3 | Cloud | s3.<region>.amazonaws.com | aws_secret_access_key |
sigv4 |
12 named integrations across 6 industries (LLM, payments, dev, email, comms, cloud) and all three auth schemes. Longest-prefix routing; non-secret config (Twilio Account SID, AWS access-key-id / region) comes from env, never sealed.
Closed and curated by design — no generic passthrough, and no generic-feeling entries. There is no catch-all route and no user-config/wildcard host; providers are added only in code. To make the Bearer providers genuinely first-class (not "a host with a bearer token"), each carries its real required headers, which the integration injects only when the agent didn't set them:
| Provider | Required headers the integration supplies |
|---|---|
| GitHub | User-Agent (GitHub 403s without it), Accept: application/vnd.github+json, X-GitHub-Api-Version |
| Anthropic | anthropic-version (required by the raw REST API) |
| Stripe | Stripe-Version (pins API behaviour) |
| Slack | Content-Type: application/json; charset=utf-8 |
| SendGrid | Content-Type: application/json |
Proven live: GET /github/user through the proxy with no User-Agent from the
agent returns 200 — the integration supplied GitHub's mandatory headers. An
agent doesn't have to know each provider's quirks; the integration does. That's
the difference between a real integration and a generic host map.
ForwardRequestgained an optionalauthfield that serialises 1:1 into the contract'sAuthSpec(types.ts).proxy.tsresolves the provider, sets the per-providersecret_key, and plants the sentinel in the right header (or omits it entirely for basic/sigv4, where the enclave builds the wholeAuthorization).- The old
upstreamForPathswitch was deleted; routing lives inproviders.ts. - Backward compatible:
authdefaults tobearer, and an older published contract simply ignores the field — existing LLM flows are unchanged.
- Native crypto vectors —
contract/auth-testsrunsauth.rsnatively and passes 4/4, including:- AWS SigV4 "get-vanilla" full-signature vector (byte-exact
Signature=5fa00fa3…fbf31). - AWS signing-key derivation vector (
f4780e2d…db404d). - base64 RFC-4648 vectors and the Twilio Basic-auth shape.
- AWS SigV4 "get-vanilla" full-signature vector (byte-exact
- Enclave rebuilds clean —
blindfold_proxy.wasm, 227,364 bytes, with sha2 + hmac compiled in. - Provider resolution + full mock proxy run — all paths route to the right
upstream with the right sealed secret and the right
authdescriptor.
Both examples run against the live enclave (tenant
did:t3n:58f5f5f9…, testnet) — no mock, no stub.
Sealed gemini_api_key, granted egress to generativelanguage.googleapis.com,
and made a real generateContent call with the agent holding no key:
✅ Real Gemini answer (key never left the enclave):
An Intel TDX enclave is a hardware-isolated execution environment that
protects the confidentiality and integrity of code and data, even from
the hypervisor.
🕵️ If a prompt-injection dumped this agent's credentials, it would get:
• env vars containing a real Gemini key: (none) ← scans ALL of process.env
• auth header the agent sends: x-goog-api-key: __BLINDFOLD__
Notable: Gemini validated that the auth model is genuinely provider-aware — its
key rides in x-goog-api-key, not Authorization, and Blindfold handles that.
Real, authenticated GitHub call through the enclave (agent authenticates as a real login), then an injected "issue" tries to exfiltrate the token:
✅ Legit call succeeded — agent is authenticated to GitHub as "FiscalMindset".
📤 If the agent dumped its credentials, the attacker would get:
• env vars containing a real GitHub token: (none) ← scans ALL of process.env
• Authorization header the agent sends: Bearer __BLINDFOLD__
🛡️ Attacker receives only the sentinel. Nothing usable.
Retargets to a Stripe-refund injection by sealing stripe_secret_key and
changing one path — the resistance is structural, so it's identical.
Sealed a real Stripe test key (sk_test_…) as stripe_secret_key, granted
egress to api.stripe.com, and ran live through the enclave:
✅ Authenticated to a REAL Stripe account (test mode, livemode=false). # GET /v1/balance → 200
✅ Real WRITE succeeded — created customer cus_UnzQbHl4Iv4zUN (livemode=false). # POST /v1/customers → 200
📤 Exfil check (scans ALL of process.env): env vars with a real Stripe key: (none); auth header: Bearer __BLINDFOLD__
🛡️ Attacker receives only the sentinel.
The agent has genuine read and write power over a real payments account, yet
the injection gets nothing. The demo asserts livemode === false and refuses to
run on a live key, so it can never touch real money.
Two real host-egress findings (documented, not hidden)
Running Stripe live surfaced real constraints in the current T3 host egress
(host:interfaces/http call) — worth recording so nobody re-diagnoses them:
- The host parses request bodies as JSON. A non-JSON payload fails with
http.parse_payload: expected value at line 1 column 1. ✅ Solved client-side: the proxy now detects aapplication/x-www-form-urlencodedbody, moves its params into the URL query string, and sends an empty body — so the host's JSON parser is never triggered and the agent's code is unchanged (it can POST a normal form). Form-encoded providers (Stripe, Twilio) accept params in the query string, so this is transparent. JSON APIs are untouched. - On testnet, request headers aren't always forwarded (residual host bug).
Even with the params in the query string, Stripe still requires a
content-typeheader, and the testnet host intermittently drops it — so a form write succeeds ~4/5 of the time and fails the rest with "check that your POST content type…". Mitigations: the proxy always injects the correctcontent-typefor form providers (so it's present when the host does forward it), and clients retry. The residual drop is genuinely host-side (reads are 100%); it's the one item worth raising with the T3 team. Auth and key protection are unaffected either way.
The basic/sigv4 schemes were built and unit-tested but, at first, never
published — the live enclave ran the old bearer-only wasm. That's now fixed:
contract 0.5.4 (id 461) is published, and both schemes are proven end-to-end
against the live enclave.
-
HTTP Basic (Twilio scheme) — definitive live proof, no Twilio account. A known secret is sealed; the agent sends no password; the enclave builds
Authorization: Basic base64(user:secret)and callshttpbin.org/basic-auth/<user>/<secret>, which validates the credential:BASIC_STATUS 200 { "authenticated": true, "user": "blindfold" }httpbin returns 200 only if the enclave's base64 is exactly right. Twilio uses the identical mechanism.
-
AWS SigV4 — byte-exact math + live structural proof. The unit vectors already prove the signature is byte-for-byte correct. Live, against real S3 with AWS's example access key:
SIGV4_STATUS 403 AWS_CODE InvalidAccessKeyIdAWS parsed our SigV4 header (it echoed back
AWSAccessKeyId=AKIDEXAMPLE) and reached credential lookup — it did not returnAuthorizationHeaderMalformed/IncompleteSignature, which is what a malformed signature yields. So the enclave's SigV4 header is well-formed and reaches AWS's auth layer. A real IAM key turns this into a 200; the machinery is proven either way.
Publishing gotcha found here: a new contract id resets the secrets-map read
ACL (readers: only:[id]), so grantContractReads(newId) must run after every
publish or all forward calls fail with cannot read map "…:secrets".
blindfold grantused to REPLACE the egress allowlist. ✅ Fixed. T3 replacesallowedHostson every agent-auth update, sograntnow unions new hosts with the previously-granted set (cached per tenant in.blindfold/egress-hosts.json) and sends the full list — grants are additive.grantprints the complete authorized set;grant --replaceforces a reset.- The proxy hid real enclave errors behind
internal proxy error. ✅ Fixed. The proxy now returns the real T3 error as JSON — status,code,detail,request_id, and an actionablehint(e.g. egress not authorized → the exactblindfold grantto run;fuel_per_minute→ "rate limited, retry in ~60s"; secrets-ACL → "run blindfold init"; JSON-payload → "use query-string params"). - Testnet tenants have a per-minute compute quota (
fuel_per_minute). Hammering the enclave (tight reliability loops, retry storms, repeated demo runs) tripsHTTP 500 too_many_requests: quota exceeded (fuel_per_minute), now surfaced clearly (see above) instead of a generic 500. It looks like an outage but resets within a minute — space calls out.
The demos originally deleted a few hardcoded env-var names and then checked only
those names — which false-passed when the .env var was renamed (the real key
was still in process.env under the new name). They now scan the entire
process.env for the provider's real key pattern (sk_…, AIza…/AQ.…,
ghp_…) and report a leftover as a genuine leak rather than hiding it. A clean
pass therefore means the key truly isn't reachable, not that we looked away.
Integration coverage. 4 LLM endpoints → 12 named integrations across 6
industries and 3 auth schemes, with two schemes (basic, sigv4) that a generic
proxy fundamentally cannot offer. The "integration stack" is now real depth, not
a switch statement.
Problem concreteness. The harm is now demonstrable and watchable: a real, privileged credential, real untrusted content, a real exfiltration attempt that comes back empty. The distinct, defensible claim — the enclave performs the provider's real auth (base64, SigV4 signing) so the secret is never in the agent even for non-bearer APIs — is one no generic-proxy competitor can make.
contract/src/auth.rs— base64 / Basic / SigV4, computed in-enclave.contract/src/forward.rs—AuthSpecenum + per-scheme header building.contract/auth-tests/— native crypto-vector tests (4/4).packages/blindfold/src/providers.ts— the concrete provider registry.packages/blindfold/src/proxy.ts,types.ts— routing + auth wiring.examples/gemini/,examples/prompt-injection/— live end-to-end demos.