-
Notifications
You must be signed in to change notification settings - Fork 0
feat: prove basic + sigv4 live end-to-end (publish 0.5.4 + twilio/aws examples) #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| edition = "2021" | ||
|
|
||
| [lib] | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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_VERSIONandcontract/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, requiringgrantContractReads(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 fromCONTRACT_VERSIONrather 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.:
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎