Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "blindfold-proxy"
version = "0.5.1"
version = "0.5.4"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: Contract version left inconsistent across docs/scripts

This PR advances the published contract to 0.5.4 (bumping CONTRACT_VERSION and contract/Cargo.toml, which jumped 0.5.1 → 0.5.4). However several docs and scripts still hardcode older versions: README.md and explain.md reference 0.5.3, and usage.md, current_status.md, scripts/test-v5-release.ts and scripts/grant-and-call.ts reference 0.5.1. Since the PR explicitly documents a publish gotcha (new contract id resets the secrets-map read ACL, requiring grantContractReads(newId)), stale version strings in the operational scripts (scripts/grant-and-call.ts, scripts/test-v5-release.ts) are the most likely to mislead an operator into targeting the wrong contract id. Consider having these scripts derive the version from CONTRACT_VERSION rather than hardcoding it, and update the docs to 0.5.4. This is non-blocking for the examples/demos themselves, which correctly use the shared client and type definitions.

Derive the version from the single source of truth instead of hardcoding it, so scripts stay in sync with future bumps.:

// scripts/grant-and-call.ts / test-v5-release.ts
import { CONTRACT_VERSION } from "../packages/blindfold/src/constants.ts";
// ...use CONTRACT_VERSION instead of the hardcoded "0.5.1" string
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

edition = "2021"

[lib]
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Runnable, copy-paste examples of using a **sealed** secret — across the three
| [`gemini/`](gemini/) | Google Gemini · Node | proxy + sentinel (`x-goog-api-key`) | one line · **real live call, non-Bearer auth** |
| [`stripe/`](stripe/) | Stripe · Node | proxy + sentinel | **real test-mode read+write, injection can't steal the key** |
| [`prompt-injection/`](prompt-injection/) | GitHub · Node | proxy + sentinel | **real live credential-theft attack, defeated structurally** |
| [`twilio/`](twilio/) | HTTP Basic · Node | in-enclave `base64(user:secret)` | **proven live via httpbin (200) — the Twilio scheme** |
| [`aws/`](aws/) | AWS SigV4 · Node | in-enclave request signing | **proven live vs real S3 + byte-exact AWS vectors** |

> **Integration depth:** Blindfold ships first-class support for 12 providers across 6 industries and 3 in-enclave auth schemes (bearer, HTTP Basic, AWS SigV4). See **[../integration-stack.md](../integration-stack.md)**.

Expand Down
52 changes: 52 additions & 0 deletions examples/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# AWS SigV4 in the enclave — proven live

SigV4 is the strongest proof that Blindfold is provider-aware: the secret access
key **never travels in the request** — it *signs* a canonical request via an HMAC
chain, **inside TDX**. A generic proxy structurally cannot do this.

Correctness is proven two ways:

1. **Byte-exact unit vectors** (`contract/auth-tests`) against AWS's published
"get-vanilla" signature + signing-key derivation.
2. **Live**, here: with AWS's example access key, real S3 returns
**403 `InvalidAccessKeyId`** — meaning AWS *parsed* our SigV4 header and
reached credential lookup, rather than `AuthorizationHeaderMalformed` /
`IncompleteSignature` (what a malformed signature yields). A real IAM key → 200.

## Setup (one time)

```bash
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' \
npm run blindfold -- register --name aws_secret_access_key --from-env aws_secret_access_key
npm run blindfold -- grant --host s3.us-east-1.amazonaws.com,<keep your other hosts>
```

## Run

```bash
npx tsx examples/aws/agent.ts
```

## Output

```
🔒 AWS SigV4 — signature computed inside the TDX enclave (secret never sent).
access key: AKIDEXAMPLE (AWS example key ⇒ expect InvalidAccessKeyId)

✅ AWS parsed our SigV4 header: HTTP 403 InvalidAccessKeyId
AWS reached credential/time evaluation — the signature is well-formed.
```

## Real AWS mode

Seal a real (throwaway/limited) IAM secret and set the key id + region:

```bash
aws_secret_access_key=<real> npm run blindfold -- register --name aws_secret_access_key --from-env aws_secret_access_key
# AWS_ACCESS_KEY_ID=AKIA… AWS_REGION=us-east-1 in env
npx tsx examples/aws/agent.ts # → HTTP 200
```

The demo classifies AWS's response: signature-**structure** errors fail the run
(a real bug); credential/time errors pass (the signature was well-formed). See
[`../../integration-stack.md`](../../integration-stack.md).
79 changes: 79 additions & 0 deletions examples/aws/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* AWS Signature Version 4 computed INSIDE the enclave, proven live.
*
* SigV4 is the strongest proof that Blindfold is provider-aware: the secret
* access key does not travel in the request at all — it *signs* a canonical
* request via an HMAC chain, inside TDX. A generic proxy structurally cannot do
* this.
*
* Correctness is proven two ways:
* 1. Byte-exact unit vectors (contract/auth-tests) vs AWS's published
* "get-vanilla" signature + signing-key derivation.
* 2. Live, here: with AWS's example access key, real S3 returns
* 403 InvalidAccessKeyId — meaning AWS PARSED our SigV4 header and reached
* credential lookup (not AuthorizationHeaderMalformed / IncompleteSignature,
* which is what a malformed signature yields). A real IAM key → 200.
*
* Prereqs for this proof (one time):
* aws_secret_access_key='wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' \
* npm run blindfold -- register --name aws_secret_access_key --from-env aws_secret_access_key
* npm run blindfold -- grant --host s3.us-east-1.amazonaws.com,<your other hosts...>
*
* Run:
* npx tsx examples/aws/agent.ts
* Real mode: seal a real IAM secret + set AWS_ACCESS_KEY_ID / AWS_REGION.
*/
import { loadBlindfoldEnv } from "../../packages/blindfold/src/env.ts";
import { openT3Client } from "../../packages/blindfold/src/t3-client.ts";
import { amzDate } from "../../packages/blindfold/src/providers.ts";

const ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "AKIDEXAMPLE"; // example key ⇒ InvalidAccessKeyId
const REGION = process.env.AWS_REGION || "us-east-1";

// Signature-format failures (a real bug) vs credential/time failures (fine — the
// signature was well-formed enough for AWS to parse and evaluate it).
const MALFORMED = ["AuthorizationHeaderMalformed", "IncompleteSignature", "MissingAuthenticationToken"];
const WELL_FORMED = ["InvalidAccessKeyId", "SignatureDoesNotMatch", "RequestTimeTooSkewed", "InvalidClientTokenId"];

async function main(): Promise<void> {
const env = loadBlindfoldEnv();
if (env.mock) {
console.log("This is a REAL demo — set real T3 creds in .env. Mock mode is off-limits here.");
process.exit(1);
}
const usingExample = ACCESS_KEY_ID === "AKIDEXAMPLE";
const t3 = await openT3Client(env);
console.log(`🔒 AWS SigV4 — signature computed inside the TDX enclave (secret never sent).`);
console.log(` access key: ${ACCESS_KEY_ID}${usingExample ? " (AWS example key ⇒ expect InvalidAccessKeyId)" : ""}\n`);
try {
const res = await t3.invokeForward({
method: "GET",
url: `https://s3.${REGION}.amazonaws.com/`,
headers: [],
secret_key: "aws_secret_access_key",
auth: { scheme: "sigv4", access_key_id: ACCESS_KEY_ID, region: REGION, service: "s3", amz_date: amzDate() },
});
const body = typeof res.body === "string" ? res.body : Buffer.from(res.body as number[]).toString("utf8");
const code = body.match(/<Code>([^<]+)<\/Code>/)?.[1] ?? (res.status === 200 ? "OK" : "(none)");

if (res.status === 200) {
console.log(`✅ Real AWS 200 — SigV4 fully valid with a live IAM key.`);
} else if (WELL_FORMED.includes(code)) {
console.log(`✅ AWS parsed our SigV4 header: HTTP ${res.status} ${code}`);
console.log(` AWS reached credential/time evaluation — the signature is well-formed.`);
console.log(` (With a real IAM key this is a 200. The enclave's SigV4 machinery is proven.)`);
} else if (MALFORMED.includes(code)) {
console.log(`✗ AWS rejected the signature STRUCTURE: HTTP ${res.status} ${code} — this is a real bug.`);
process.exitCode = 1;
} else {
console.log(`? HTTP ${res.status} ${code} — ${body.slice(0, 160).replace(/\s+/g, " ")}`);
}
} finally {
await t3.close();
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
49 changes: 49 additions & 0 deletions examples/twilio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# HTTP Basic auth in the enclave (the Twilio scheme) — proven live

Twilio authenticates with HTTP Basic: `base64(AccountSID:AuthToken)`. That base64
can only be computed **after** the secret is joined — so a generic "swap the
sentinel" proxy cannot do it. Blindfold computes it **inside the TDX enclave**.

This demo proves that end-to-end **without a Twilio account**: it seals a known
password, the agent sends **no** credential, and the enclave calls
`httpbin.org/basic-auth/<user>/<pass>` — which returns **200 only if** the
enclave's base64 is exactly right. Twilio uses the identical mechanism.

## Setup (one time)

```bash
httpbin_basic_pass='s3cr3t-basic-test' \
npm run blindfold -- register --name httpbin_basic_pass --from-env httpbin_basic_pass
npm run blindfold -- grant --host httpbin.org,<keep your other granted hosts>
```
(`grant` **replaces** the allowlist — list every host you still need in one call.)

## Run

```bash
npx tsx examples/twilio/agent.ts
```

## Output

```
✅ httpbin validated the credential: HTTP 200
{ "authenticated": true, "user": "blindfold" }
The enclave built Basic base64(user:secret) correctly — the agent never
had the password.
```

## Real Twilio mode

Seal your Auth Token and set your Account SID, then use the `/twilio/` route
through the proxy:

```bash
npm run blindfold -- register --name twilio_auth_token --from-env twilio_auth_token
npm run blindfold -- grant --host api.twilio.com,<others>
# TWILIO_ACCOUNT_SID=ACxxxx in env (the Basic-auth username; not secret)
# POST http://127.0.0.1:8787/twilio/2010-04-01/Accounts/ACxxxx/Messages.json
```

Same enclave computation — the only difference is who validates the base64.
See [`../../integration-stack.md`](../../integration-stack.md).
68 changes: 68 additions & 0 deletions examples/twilio/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* HTTP Basic auth computed INSIDE the enclave (the Twilio scheme), proven live.
*
* Twilio authenticates with HTTP Basic: `base64(AccountSID:AuthToken)`. That
* base64 can only be computed AFTER the secret is joined — so a generic
* "swap the sentinel" proxy cannot do it. Blindfold computes it inside TDX.
*
* This demo proves that end-to-end WITHOUT needing a Twilio account: it seals a
* known password, sends NO credential from the agent, and calls
* httpbin.org/basic-auth/<user>/<pass>, which returns 200 ONLY if the enclave's
* base64 is exactly right. Twilio uses the identical mechanism.
*
* Real Twilio mode: seal your Auth Token and set your Account SID, then call the
* /twilio/ route through the proxy (see README).
*
* Prereqs for this proof (one time):
* httpbin_basic_pass='s3cr3t-basic-test' \
* npm run blindfold -- register --name httpbin_basic_pass --from-env httpbin_basic_pass
* npm run blindfold -- grant --host httpbin.org,<your other hosts...>
*
* Run:
* npx tsx examples/twilio/agent.ts
*/
import { loadBlindfoldEnv } from "../../packages/blindfold/src/env.ts";
import { openT3Client } from "../../packages/blindfold/src/t3-client.ts";

const USER = "blindfold";
const KNOWN_PASS = "s3cr3t-basic-test"; // the value sealed as httpbin_basic_pass

async function main(): Promise<void> {
const env = loadBlindfoldEnv();
if (env.mock) {
console.log("This is a REAL demo — set real T3 creds in .env. Mock mode is off-limits here.");
process.exit(1);
}
const t3 = await openT3Client(env);
console.log("🔒 HTTP Basic auth — computed inside the TDX enclave, proven against httpbin.\n");
try {
// The agent supplies NO password. The enclave joins user:secret and base64s
// it into `Authorization: Basic …` on the outbound call.
const res = await t3.invokeForward({
method: "GET",
url: `https://httpbin.org/basic-auth/${USER}/${KNOWN_PASS}`,
headers: [],
secret_key: "httpbin_basic_pass",
auth: { scheme: "basic", username: USER },
});
const body = typeof res.body === "string" ? res.body : Buffer.from(res.body as number[]).toString("utf8");

if (res.status === 200 && /"authenticated":\s*true/.test(body)) {
console.log(`✅ httpbin validated the credential: HTTP ${res.status}`);
console.log(` ${body.slice(0, 120).replace(/\s+/g, " ")}`);
console.log("\n The enclave built Basic base64(user:secret) correctly — the agent never");
console.log(" had the password. This is exactly how Twilio auth is computed in TDX.");
} else {
console.log(`✗ Unexpected: HTTP ${res.status} — ${body.slice(0, 160)}`);
console.log(" (Seal httpbin_basic_pass='s3cr3t-basic-test' and grant --host httpbin.org first.)");
process.exitCode = 1;
}
} finally {
await t3.close();
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
33 changes: 33 additions & 0 deletions integration-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,39 @@ Running Stripe live surfaced real constraints in the **current T3 host** egress
issue — auth and key protection are unaffected; these are host-egress
maturity gaps on testnet.

### basic + sigv4 proven LIVE (contract 0.5.4)

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 calls
`httpbin.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 InvalidAccessKeyId
```
AWS **parsed** our SigV4 header (it echoed back `AWSAccessKeyId=AKIDEXAMPLE`)
and reached credential lookup — it did not return `AuthorizationHeaderMalformed`
/ `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"`.

### Two operational gotchas (cost real diagnosis time — documented so they don't again)

- **`blindfold grant` REPLACES the egress allowlist, it doesn't append.**
Expand Down
2 changes: 1 addition & 1 deletion packages/blindfold/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const DEFAULT_PORT = 8787;
export const CONTRACT_TAIL = "blindfold-proxy";

/** Default contract version (semver, used at register time). Bump on every change to contract/. */
export const CONTRACT_VERSION = "0.5.3";
export const CONTRACT_VERSION = "0.5.4";

/** Marker the proxy uses to surface "registered but mock" status in /health. */
export const HEALTH_BANNER = "blindfold/0.1.0";
Expand Down
Loading