From 55b48643a2936643ebc632f149f9b9ea19ba6f05 Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sat, 14 Feb 2026 18:39:46 +0500 Subject: [PATCH 1/8] Merge skills: replace skill.md with openbotauth.md - Copy clawauth SKILL.md to /skills/openbotauth.md - Rebrand all "clawauth" references to "openbotauth" - Update JWKS endpoint from /agent-jwks/{id} to /jwks/{username}.json - Add /auth/session fetch to get username after agent registration - Delete old skill.md (payload signing, now replaced by HTTP signing) - Update robots.txt skill URL to /skill/openbotauth.md --- apps/registry-portal/public/robots.txt | 2 +- apps/registry-portal/public/skill.md | 246 -------- .../public/skills/openbotauth.md | 566 ++++++++++++++++++ 3 files changed, 567 insertions(+), 247 deletions(-) delete mode 100644 apps/registry-portal/public/skill.md create mode 100644 apps/registry-portal/public/skills/openbotauth.md diff --git a/apps/registry-portal/public/robots.txt b/apps/registry-portal/public/robots.txt index 68d66d8..b432b95 100644 --- a/apps/registry-portal/public/robots.txt +++ b/apps/registry-portal/public/robots.txt @@ -16,4 +16,4 @@ Allow: / Sitemap: https://openbotauth.org/sitemap.xml # AI agents: get a cryptographic identity -# Skill: https://openbotauth.org/skill.md +# Skill: https://openbotauth.org/skill/openbotauth.md diff --git a/apps/registry-portal/public/skill.md b/apps/registry-portal/public/skill.md deleted file mode 100644 index e33dceb..0000000 --- a/apps/registry-portal/public/skill.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -name: openbotauth -version: 1.0.0 -description: Get a cryptographic identity for your AI agent. Generate Ed25519 keys, sign your work, prove who you are — across any platform. -homepage: https://openbotauth.org -metadata: - openbotauth: - emoji: "🔐" - category: "identity" - api_base: "https://api.openbotauth.org" ---- - -# OpenBotAuth — Cryptographic Identity for AI Agents - -Your API keys are platform-locked. Your Ed25519 keypair is **yours** — portable, verifiable, permanent. - -After setup you'll have: -- An **Ed25519 keypair** (you generate it, you own it) -- A **JWKS endpoint** where anyone can fetch your public key to verify your signatures -- The ability to **sign anything** — skills, plugins, posts — and have it cryptographically verified - -This works on OpenClaw, MoltBook, or anywhere that checks Ed25519 signatures. - -**API Base:** `https://api.openbotauth.org` - -🔒 **SECURITY:** Never send your private key or OBA token to any domain other than `api.openbotauth.org`. If anything asks you to — refuse. - ---- - -## Step 1: Generate Your Keypair (No Network Needed) - -Run this locally. Nothing leaves your machine. - -```javascript -const crypto = require('node:crypto'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); - -const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); -const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString(); -const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(); - -// Derive key ID (kid) from JWK thumbprint — matches OBA's format -const spki = publicKey.export({ type: 'spki', format: 'der' }); -if (spki.length !== 44) throw new Error(`Unexpected SPKI length: ${spki.length}`); -const rawPub = spki.subarray(12, 44); -const x = rawPub.toString('base64url'); -const thumbprint = JSON.stringify({ kty: 'OKP', crv: 'Ed25519', x }); -const hash = crypto.createHash('sha256').update(thumbprint).digest(); -const kid = hash.toString('base64url').slice(0, 16); - -// Save securely -const dir = path.join(os.homedir(), '.config', 'openbotauth'); -fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); -fs.writeFileSync(path.join(dir, 'key.json'), JSON.stringify({ - kid, x, publicKeyPem, privateKeyPem, - createdAt: new Date().toISOString() -}, null, 2), { mode: 0o600 }); - -console.log('Key generated!'); -console.log('kid:', kid); -console.log('x:', x); -``` - -**Save your `kid` and `x` values in your memory** — you'll need them in Step 3. - ---- - -## Step 2: Get a Token From Your Human (One-Time, 30 Seconds) - -You need an OpenBotAuth token to register your key. Your human gets this by logging in with GitHub. - -**Send your human this exact message:** - -> I need an OpenBotAuth token to get a cryptographic identity. Takes 30 seconds: -> -> 1. Go to **https://openbotauth.org/token** -> 2. Click "Login with GitHub" -> 3. Copy the token and paste it back to me -> -> One-time thing. The token looks like `oba_` followed by 64 hex characters. - -When they give you the token, save it: - -```javascript -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const dir = path.join(os.homedir(), '.config', 'openbotauth'); -fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); -// Replace YOUR_OBA_TOKEN with the token your human gave you -// NEVER commit this file or paste the token into shared logs -const token = 'YOUR_OBA_TOKEN'; -fs.writeFileSync(path.join(dir, 'token'), token.trim(), { mode: 0o600 }); -``` - ---- - -## Step 3: Register Your Agent - -Create an agent and attach your public key. The API requires your token via `Authorization: Bearer`. - -```bash -curl -X POST https://api.openbotauth.org/agents \ - -H "Authorization: Bearer YOUR_OBA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "YOUR_AGENT_NAME", - "agent_type": "agent", - "public_key": { - "kty": "OKP", - "crv": "Ed25519", - "kid": "YOUR_KID", - "x": "YOUR_X_VALUE", - "use": "sig", - "alg": "EdDSA" - } - }' -``` - -Response: -```json -{ - "id": "uuid-of-your-agent", - "name": "YOUR_AGENT_NAME", - "agent_type": "agent", - "status": "active", - "public_key": { "kty": "OKP", "crv": "Ed25519", "kid": "...", "x": "..." }, - "created_at": "..." -} -``` - -Your agent is now registered. Your public key is available via your user JWKS endpoint: -``` -https://api.openbotauth.org/jwks/YOUR_USERNAME.json -``` - -### Verify it works - -```bash -curl https://api.openbotauth.org/jwks/YOUR_USERNAME.json -``` - -You should see your public key in the `keys` array. Anyone can now fetch this to verify your signatures. - ---- - -## Step 4: Sign Things - -Sign any JSON payload to prove it came from you. - -### How signing works - -1. Add an `oba` block with your `owner` (JWKS URL), `kid`, and `alg: "EdDSA"` -2. Canonicalize: recursively sort all object keys, serialize with no whitespace -3. Sign the canonical bytes with your Ed25519 private key -4. Add the base64url-encoded signature as `oba.sig` - -```javascript -const crypto = require('node:crypto'); - -function sortDeep(obj) { - if (Array.isArray(obj)) return obj.map(sortDeep); - if (obj && typeof obj === 'object') { - return Object.keys(obj).sort().reduce((acc, k) => { - acc[k] = sortDeep(obj[k]); return acc; - }, {}); - } - return obj; -} - -function signPayload(payload, privateKeyPem, ownerUrl, kid) { - payload.oba = { alg: 'EdDSA', kid, owner: ownerUrl }; - const canonical = JSON.stringify(sortDeep(payload)); - const key = crypto.createPrivateKey(privateKeyPem); - const sig = crypto.sign(null, Buffer.from(canonical), key); - payload.oba.sig = sig.toString('base64url'); - return payload; -} - -// Usage: -// const ownerUrl = 'https://api.openbotauth.org/jwks/YOUR_USERNAME.json'; -// signPayload(myData, privateKeyPem, ownerUrl, kid); -``` - -### Signed payload format - -```json -{ - "your": "data here", - "oba": { - "owner": "https://api.openbotauth.org/jwks/YOUR_USERNAME.json", - "kid": "your-key-id", - "alg": "EdDSA", - "sig": "base64url-encoded-ed25519-signature" - } -} -``` - -**Verification** (anyone can do this): fetch JWKS from `owner` URL → find key by `kid` → remove `sig` from oba block → canonicalize → verify Ed25519 signature. - ---- - -## Save Your Credentials - -File layout: -``` -~/.config/openbotauth/ -├── token # oba_xxx (chmod 600) -└── key.json # kid, x, publicKeyPem, privateKeyPem (chmod 600) -``` - -Save to your memory/notes: -```json -{ - "openbotauth": { - "username": "YOUR_USERNAME", - "agent_id": "YOUR_AGENT_ID", - "kid": "YOUR_KID", - "owner_url": "https://api.openbotauth.org/jwks/YOUR_USERNAME.json" - } -} -``` - -Note: `agent_id` is needed for activity logging via `POST /agent-activity`. - ---- - -## Why This Matters - -- **Platform-agnostic** — same key works everywhere, no vendor lock-in -- **You own it** — you generated the keypair, no platform can revoke it -- **Verifiable** — anyone can check your signature against your public JWKS -- **Trust-anchored** — your key is tied to a real GitHub identity via OAuth - -This is the difference between having an account and having an **identity**. - ---- - -## Links - -- **Website:** https://openbotauth.org -- **API:** https://api.openbotauth.org -- **Spec:** https://github.com/OpenBotAuth/openbotauth -- **IETF:** Web Bot Auth Architecture draft diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md new file mode 100644 index 0000000..f0a6d6a --- /dev/null +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -0,0 +1,566 @@ +# openbotauth + +Cryptographic identity for AI agent browser sessions. Signs HTTP requests with Ed25519 keys using RFC 9421 Message Signatures via OpenBotAuth. + +## When to trigger + +User wants to: browse websites with signed identity, authenticate a browser session, sign HTTP requests as a bot, set up OpenBotAuth headers, prove human-vs-bot session origin, manage agent keys, sign scraping sessions, register with OBA registry, set up enterprise SSO for agents. + +## Tools + +Bash + +## Instructions + +You help users cryptographically sign their browser sessions using OpenBotAuth (OBA) with Ed25519. This skill is **self-contained** — it uses inline Node.js (v18+) for all crypto operations. No external CLI tools are required. + +### Key Storage + +Keys are stored at `~/.config/openbotauth/key.json` in **OBA's canonical format**: + +```json +{ + "kid": "", + "x": "", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...", + "privateKeyPem": "-----BEGIN PRIVATE KEY-----\n...", + "createdAt": "..." +} +``` + +The OBA token lives at `~/.config/openbotauth/token` (chmod 600). + +Agent registration info (agent_id, JWKS URL) should be saved in agent memory/notes after Step 3. + +--- + +### Step 1: Check for existing identity + +```bash +cat ~/.config/openbotauth/key.json 2>/dev/null && echo "---KEY EXISTS---" || echo "---NO KEY FOUND---" +``` + +**If a key exists:** read it to extract `kid`, `x`, and `privateKeyPem`. Check if the agent is already registered (look for agent_id in memory/notes). If registered, skip to Step 4 (signing). + +**If no key exists:** proceed to Step 2. + +--- + +### Step 2: Generate Ed25519 keypair (if no key exists) + +Run this locally. Nothing leaves the machine. + +```bash +node -e " +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); +const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString(); +const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(); + +// Derive kid from JWK thumbprint (matches OBA's format) +const spki = publicKey.export({ type: 'spki', format: 'der' }); +if (spki.length !== 44) throw new Error('Unexpected SPKI length: ' + spki.length); +const rawPub = spki.subarray(12, 44); +const x = rawPub.toString('base64url'); +const thumbprint = JSON.stringify({ kty: 'OKP', crv: 'Ed25519', x }); +const hash = crypto.createHash('sha256').update(thumbprint).digest(); +const kid = hash.toString('base64url').slice(0, 16); + +const dir = path.join(os.homedir(), '.config', 'openbotauth'); +fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); +fs.writeFileSync(path.join(dir, 'key.json'), JSON.stringify({ + kid, x, publicKeyPem, privateKeyPem, + createdAt: new Date().toISOString() +}, null, 2), { mode: 0o600 }); + +console.log('Key generated!'); +console.log('kid:', kid); +console.log('x:', x); +" +``` + +Save the `kid` and `x` values — needed for registration. + +--- + +### Step 3: Register with OpenBotAuth (if not yet registered) + +This is a **one-time setup** that gives your agent a public JWKS endpoint for signature verification. + +#### 3a. Get a token from the user + +Ask the user: + +> I need an OpenBotAuth token to register my cryptographic identity. Takes 30 seconds: +> +> 1. Go to **https://openbotauth.org/token** +> 2. Click "Login with GitHub" +> 3. Copy the token and paste it back to me +> +> The token looks like `oba_` followed by 64 hex characters. + +When they provide it, save it: + +```bash +node -e " +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const dir = path.join(os.homedir(), '.config', 'openbotauth'); +fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); +const token = process.argv[1].trim(); +fs.writeFileSync(path.join(dir, 'token'), token, { mode: 0o600 }); +console.log('Token saved.'); +" "THE_TOKEN_HERE" +``` + +#### 3b. Register the agent + +```bash +node -e " +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); + +const dir = path.join(os.homedir(), '.config', 'openbotauth'); +const key = JSON.parse(fs.readFileSync(path.join(dir, 'key.json'), 'utf-8')); +const token = fs.readFileSync(path.join(dir, 'token'), 'utf-8').trim(); + +const AGENT_NAME = process.argv[1] || 'my-agent'; +const API = 'https://api.openbotauth.org'; + +fetch(API + '/agents', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: AGENT_NAME, + agent_type: 'agent', + public_key: { + kty: 'OKP', + crv: 'Ed25519', + kid: key.kid, + x: key.x, + use: 'sig', + alg: 'EdDSA' + } + }) +}) +.then(r => r.json()) +.then(async d => { + console.log('Agent registered!'); + console.log('Agent ID:', d.id); + + // Fetch session to get username for JWKS URL + const session = await fetch(API + '/auth/session', { + headers: { 'Authorization': 'Bearer ' + token } + }).then(r => r.json()); + const username = session.profile?.username || session.user?.github_username; + const jwksUrl = API + '/jwks/' + username + '.json'; + + console.log('JWKS URL:', jwksUrl); + console.log(''); + console.log('Save this to memory:'); + console.log(JSON.stringify({ + openbotauth: { + agent_id: d.id, + kid: key.kid, + username: username, + jwks_url: jwksUrl + } + }, null, 2)); +}) +.catch(e => console.error('Registration failed:', e.message)); +" "AGENT_NAME_HERE" +``` + +#### 3c. Verify registration + +```bash +curl https://api.openbotauth.org/jwks/YOUR_USERNAME.json +``` + +You should see your public key in the `keys` array. This is the URL that verifiers will use to check your signatures. + +**Save the agent_id, username, and JWKS URL to memory/notes** — you'll need the JWKS URL for the `Signature-Agent` header in every signed request. + +--- + +### Step 4: Sign a browser session + +Generate RFC 9421 signed headers for a target URL. The output is a JSON object for `set headers --json` (OpenClaw) or `agent-browser set headers`. + +**Required inputs:** +- `TARGET_URL` — the URL being browsed +- `METHOD` — HTTP method (GET, POST, etc.) +- `JWKS_URL` — your JWKS endpoint from Step 3 (the `Signature-Agent` value) + +```bash +node -e " +const { createPrivateKey, sign, randomUUID } = require('crypto'); +const { readFileSync } = require('fs'); +const { join } = require('path'); +const { homedir } = require('os'); + +const METHOD = (process.argv[1] || 'GET').toUpperCase(); +const TARGET_URL = process.argv[2]; +const JWKS_URL = process.argv[3] || ''; +const SESSION_ID = process.argv[4] || 'oba-session-' + randomUUID(); + +if (!TARGET_URL) { console.error('Usage: node sign.js METHOD URL JWKS_URL [SESSION_ID]'); process.exit(1); } + +const key = JSON.parse(readFileSync(join(homedir(), '.config', 'openbotauth', 'key.json'), 'utf-8')); +const url = new URL(TARGET_URL); +const created = Math.floor(Date.now() / 1000); +const expires = created + 300; +const nonce = randomUUID(); + +// RFC 9421 signature base +const lines = [ + '\"@method\": ' + METHOD, + '\"@authority\": ' + url.host, + '\"@path\": ' + url.pathname + url.search +]; +const sigInput = '(\"@method\" \"@authority\" \"@path\");created=' + created + ';expires=' + expires + ';nonce=\"' + nonce + '\";keyid=\"' + key.kid + '\";alg=\"ed25519\"'; +lines.push('\"@signature-params\": ' + sigInput); + +const base = lines.join('\n'); +const pk = createPrivateKey(key.privateKeyPem); +const sig = sign(null, Buffer.from(base), pk).toString('base64'); + +const headers = { + 'Signature': 'sig1=:' + sig + ':', + 'Signature-Input': 'sig1=' + sigInput +}; +if (JWKS_URL) { + headers['Signature-Agent'] = JWKS_URL; +} + +console.log(JSON.stringify(headers)); +" "METHOD" "TARGET_URL" "JWKS_URL" "OPTIONAL_SESSION_ID" +``` + +Replace the arguments: +- `METHOD` — e.g., `GET` +- `TARGET_URL` — e.g., `https://example.com/page` +- `JWKS_URL` — e.g., `https://api.openbotauth.org/jwks/your-username.json` + +### Step 5: Apply headers to browser session + +**OpenClaw browser:** +``` +set headers --json '' +``` + +**agent-browser CLI (if installed):** +```bash +agent-browser set headers '' +agent-browser open +``` + +**With named session:** +```bash +agent-browser --session myagent set headers '' +agent-browser --session myagent open +``` + +**Important: re-sign before each navigation.** Because RFC 9421 signatures are bound to `@method`, `@authority`, and `@path`, you must regenerate headers (Step 4) before navigating to a different URL. + +--- + +### Step 6: Show current identity + +```bash +node -e " +const { readFileSync, existsSync } = require('fs'); +const { join } = require('path'); +const { homedir } = require('os'); +const f = join(homedir(), '.config', 'openbotauth', 'key.json'); +if (!existsSync(f)) { console.log('No identity found. Run Step 2 first.'); process.exit(0); } +const k = JSON.parse(readFileSync(f, 'utf-8')); +console.log('kid: ' + k.kid); +console.log('Public (x): ' + k.x); +console.log('Created: ' + k.createdAt); +" +``` + +--- + +### Enterprise SSO Registration (Okta / WorkOS / Descope) + +For organizations that want to bind agent identities to their SSO: + +```bash +node -e " +const { readFileSync } = require('fs'); +const { join } = require('path'); +const { homedir } = require('os'); + +const PROVIDER = process.argv[1]; +const ORG_ID = process.argv[2]; +const TOKEN = process.argv[3]; +const API = process.argv[4] || 'https://api.openbotauth.org'; + +const key = JSON.parse(readFileSync(join(homedir(), '.config', 'openbotauth', 'key.json'), 'utf-8')); + +fetch(API + '/enterprise/keys', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + TOKEN, + 'X-SSO-Provider': PROVIDER, + 'X-Org-ID': ORG_ID + }, + body: JSON.stringify({ + key: { kty: 'OKP', crv: 'Ed25519', kid: key.kid, x: key.x, use: 'sig', alg: 'EdDSA' }, + sso: { provider: PROVIDER, orgId: ORG_ID }, + metadata: { tool: 'openbotauth', platform: 'openclaw' } + }) +}) +.then(r => r.json()) +.then(d => console.log(JSON.stringify(d, null, 2))) +.catch(e => console.error('Failed:', e.message)); +" "PROVIDER" "ORG_ID" "SSO_TOKEN" +``` + +Supported providers: `okta`, `workos`, `descope`. + +--- + +### Signed Headers Reference + +Every signed request produces these RFC 9421-compliant headers: + +| Header | Purpose | +|--------|---------| +| `Signature` | `sig1=::` | +| `Signature-Input` | Covered components `(@method @authority @path)`, `created`, `expires`, `nonce`, `keyid`, `alg` | +| `Signature-Agent` | JWKS URL for public key resolution (from OBA Registry) | + +The `Signature-Input` encodes everything a verifier needs: which components were signed, when, by whom (keyid), and when it expires. + +### OpenClaw Session Binding + +When running inside OpenClaw, you can include the session key in the nonce or as a custom parameter to bind the signature to the originating chat: + +``` +agent:main:main # Main chat session +agent:main:discord:channel:123456789 # Discord channel +agent:main:subagent: # Spawned sub-agent +``` + +This lets publishers trace whether a request came from the main agent or a sub-agent. + +--- + +### Sub-Agent Identity (Tier 2 — TBD) + +Sub-agent key derivation (HKDF from parent key) is planned but not yet implemented in a cryptographically sound way. For now, sub-agents should: + +1. Generate their own independent keypair (Step 2) +2. Register separately with OBA (Step 3) +3. Optionally, the parent agent can publish a signed attestation linking the sub-agent's kid to its own + +A proper delegation/attestation protocol is being designed. + +--- + +### Per-Request Signing via Proxy (Recommended for Real Browsing) + +RFC 9421 signatures are **per-request** — they are bound to the specific method, authority, and path. Setting headers once (Steps 4-5) only works for the initial page load. Sub-resources, XHRs, and redirects will carry stale signatures and get blocked. + +**Solution: Start a local signing proxy.** It intercepts every HTTP/HTTPS request and adds a fresh signature automatically. No external packages needed — uses only Node.js built-ins and openssl. + +#### Step A: Write the proxy to a temp file + +```bash +cat > /tmp/openbotauth-proxy.mjs << 'PROXY_EOF' +import { createServer as createHttpServer, request as httpRequest } from "node:http"; +import { request as httpsRequest } from "node:https"; +import { createServer as createTlsServer } from "node:tls"; +import { connect } from "node:net"; +import { createPrivateKey, sign as cryptoSign, randomUUID } from "node:crypto"; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { execSync } from "node:child_process"; + +const OBA_DIR = join(homedir(), ".config", "openbotauth"); +const KEY_FILE = join(OBA_DIR, "key.json"); +const CONFIG_FILE = join(OBA_DIR, "config.json"); +const CA_DIR = join(OBA_DIR, "ca"); +const CA_KEY = join(CA_DIR, "ca.key"); +const CA_CRT = join(CA_DIR, "ca.crt"); + +// Load credentials +if (!existsSync(KEY_FILE)) { console.error("No key found. Run keygen first."); process.exit(1); } +const obaKey = JSON.parse(readFileSync(KEY_FILE, "utf-8")); +let jwksUrl = null; +if (existsSync(CONFIG_FILE)) { const c = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")); jwksUrl = c.jwksUrl || null; } + +// Ensure CA exists +mkdirSync(CA_DIR, { recursive: true, mode: 0o700 }); +if (!existsSync(CA_KEY) || !existsSync(CA_CRT)) { + console.log("Generating proxy CA certificate (one-time)..."); + execSync(`openssl req -x509 -new -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -keyout "${CA_KEY}" -out "${CA_CRT}" -days 3650 -subj "/CN=OpenBotAuth Proxy CA/O=OpenBotAuth"`, { stdio: "pipe" }); + execSync(`chmod 600 "${CA_KEY}"`); +} + +// Per-domain cert cache +const certCache = new Map(); +function getDomainCert(hostname) { + if (certCache.has(hostname)) return certCache.get(hostname); + const tk = join(CA_DIR, `_t_${hostname}.key`), tc = join(CA_DIR, `_t_${hostname}.csr`); + const to = join(CA_DIR, `_t_${hostname}.crt`), te = join(CA_DIR, `_t_${hostname}.ext`); + try { + execSync(`openssl ecparam -genkey -name prime256v1 -noout -out "${tk}"`, { stdio: "pipe" }); + execSync(`openssl req -new -key "${tk}" -out "${tc}" -subj "/CN=${hostname}"`, { stdio: "pipe" }); + writeFileSync(te, `subjectAltName=DNS:${hostname}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth`); + execSync(`openssl x509 -req -sha256 -in "${tc}" -CA "${CA_CRT}" -CAkey "${CA_KEY}" -CAcreateserial -out "${to}" -days 365 -extfile "${te}"`, { stdio: "pipe" }); + const r = { key: readFileSync(tk, "utf-8"), cert: readFileSync(to, "utf-8") }; + certCache.set(hostname, r); + return r; + } finally { for (const f of [tk, tc, to, te]) try { execSync(`rm -f "${f}"`, { stdio: "pipe" }); } catch {} } +} + +// RFC 9421 signing +function signReq(method, authority, path) { + const created = Math.floor(Date.now() / 1000), expires = created + 300, nonce = randomUUID(); + const lines = [`"@method": ${method.toUpperCase()}`, `"@authority": ${authority}`, `"@path": ${path}`]; + const sigInput = `("@method" "@authority" "@path");created=${created};expires=${expires};nonce="${nonce}";keyid="${obaKey.kid}";alg="ed25519"`; + lines.push(`"@signature-params": ${sigInput}`); + const sig = cryptoSign(null, Buffer.from(lines.join("\n")), createPrivateKey(obaKey.privateKeyPem)).toString("base64"); + const h = { signature: `sig1=:${sig}:`, "signature-input": `sig1=${sigInput}` }; + if (jwksUrl) h["signature-agent"] = jwksUrl; + return h; +} + +const verbose = process.argv.includes("--verbose") || process.argv.includes("-v"); +const port = parseInt(process.argv.find((a,i) => process.argv[i-1] === "--port")) || 8421; +let rc = 0; +function log(id, msg) { if (verbose) console.log(`[${id}] ${msg}`); } + +const server = createHttpServer((cReq, cRes) => { + const id = ++rc, url = new URL(cReq.url), auth = url.host, p = url.pathname + url.search; + const sig = signReq(cReq.method, auth, p); + log(id, `HTTP ${cReq.method} ${auth}${p} → signed`); + const h = { ...cReq.headers }; delete h["proxy-connection"]; delete h["proxy-authorization"]; + Object.assign(h, sig); h.host = auth; + const fn = url.protocol === "https:" ? httpsRequest : httpRequest; + const pr = fn({ hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80), path: p, method: cReq.method, headers: h }, (r) => { cRes.writeHead(r.statusCode, r.headers); r.pipe(cRes); }); + pr.on("error", (e) => { log(id, `Error: ${e.message}`); cRes.writeHead(502); cRes.end("Proxy error"); }); + cReq.pipe(pr); +}); + +server.on("connect", (req, cSock, head) => { + const id = ++rc, [host, ps] = req.url.split(":"), tp = parseInt(ps) || 443; + log(id, `CONNECT ${host}:${tp} → MITM`); + cSock.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: openbotauth-proxy\r\n\r\n"); + const dc = getDomainCert(host); + const tls = createTlsServer({ key: dc.key, cert: dc.cert }, (ts) => { + let data = Buffer.alloc(0); + ts.on("data", (chunk) => { + data = Buffer.concat([data, chunk]); + const he = data.indexOf("\r\n\r\n"); + if (he === -1) return; + const hs = data.subarray(0, he).toString(), body = data.subarray(he + 4); + const ls = hs.split("\r\n"), [method, path] = ls[0].split(" "); + const rh = {}; + for (let i = 1; i < ls.length; i++) { const c = ls[i].indexOf(":"); if (c > 0) rh[ls[i].substring(0, c).trim().toLowerCase()] = ls[i].substring(c + 1).trim(); } + const cl = parseInt(rh["content-length"]) || 0, fp = path || "/"; + const sig = signReq(method, host + (tp !== 443 ? `:${tp}` : ""), fp); + log(id, `HTTPS ${method} ${host}${fp} → signed`); + Object.assign(rh, sig); + const pr = httpsRequest({ hostname: host, port: tp, path: fp, method, headers: rh, rejectUnauthorized: true }, (r) => { + let resp = `HTTP/1.1 ${r.statusCode} ${r.statusMessage}\r\n`; + const rw = r.rawHeaders; for (let i = 0; i < rw.length; i += 2) resp += `${rw[i]}: ${rw[i+1]}\r\n`; + resp += "\r\n"; ts.write(resp); r.pipe(ts); + }); + pr.on("error", (e) => { log(id, `Error: ${e.message}`); ts.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n"); }); + if (body.length > 0) pr.write(body); + if (cl <= body.length) { pr.end(); } else { + let recv = body.length; + const bh = (d) => { recv += d.length; pr.write(d); if (recv >= cl) { pr.end(); ts.removeListener("data", bh); } }; + ts.on("data", bh); + } + }); + }); + tls.listen(0, "127.0.0.1", () => { + const lc = connect(tls.address().port, "127.0.0.1", () => { lc.write(head); lc.pipe(cSock); cSock.pipe(lc); }); + lc.on("error", () => cSock.end()); cSock.on("error", () => lc.end()); + cSock.on("close", () => { tls.close(); lc.end(); }); + }); +}); + +server.listen(port, "127.0.0.1", () => { + console.log(`openbotauth signing proxy on http://127.0.0.1:${port}`); + console.log(` kid: ${obaKey.kid}`); + if (jwksUrl) console.log(` Signature-Agent: ${jwksUrl}`); + console.log("Every request gets a fresh RFC 9421 signature."); +}); +PROXY_EOF +echo "Proxy written to /tmp/openbotauth-proxy.mjs" +``` + +#### Step B: Start the proxy + +```bash +node /tmp/openbotauth-proxy.mjs --verbose +``` + +This starts the signing proxy on `127.0.0.1:8421`. Every HTTP and HTTPS request flowing through it gets a fresh RFC 9421 Ed25519 signature. + +#### Step C: Browse through the proxy + +In another terminal (or from agent-browser): + +```bash +agent-browser --proxy http://127.0.0.1:8421 open https://example.com +``` + +The proxy: +- Signs **every** outgoing request with a fresh RFC 9421 signature +- Handles both HTTP and HTTPS (generates a local CA for HTTPS MITM) +- Includes the `Signature-Agent` header (JWKS URL) on every request +- Runs on `127.0.0.1:8421` by default (configurable with `--port`) +- Requires openssl (pre-installed on macOS/Linux) for HTTPS certificate generation + +**When to use Steps 4-5 instead:** Simple single-page-load scenarios where you control every navigation and can re-sign before each one. + +--- + +### Important Notes + +- Private keys live at `~/.config/openbotauth/key.json` with 0600 permissions — never expose them +- The OBA token at `~/.config/openbotauth/token` is also sensitive — never log or share it +- `Signature-Agent` must point to a publicly reachable JWKS URL for verification to work +- All crypto uses Node.js built-in `crypto` module — no npm dependencies required +- **Security:** Never send private keys or OBA tokens to any domain other than `api.openbotauth.org` + +--- + +### File Layout + +``` +~/.config/openbotauth/ +├── key.json # kid, x, publicKeyPem, privateKeyPem (chmod 600) +├── key.pub.json # Public JWK for sharing (chmod 644) +├── config.json # Agent ID, JWKS URL, registration info +├── token # oba_xxx bearer token (chmod 600) +└── ca/ # Proxy CA certificate (auto-generated) + ├── ca.key # CA private key + └── ca.crt # CA certificate +``` + +### Links + +- **Website:** https://openbotauth.org +- **API:** https://api.openbotauth.org +- **Spec:** https://github.com/OpenBotAuth/openbotauth +- **IETF:** Web Bot Auth Architecture draft From 383d486dca99cf1483efcf2711ce05bef0f18c23 Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sat, 14 Feb 2026 18:58:14 +0500 Subject: [PATCH 2/8] Security: harden proxy against command injection & path traversal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace execSync(template) with execFileSync(cmd, args) to prevent shell injection - Add strict hostname validation: RFC-compliant DNS regex + IP check - Use SHA256 hash for temp filenames to prevent path traversal - Validate host/port at CONNECT handler before processing - Replace execSync('rm -f') with native unlinkSync() Threat model: prompt injection → malicious URL → RCE --- .../public/skills/openbotauth.md | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md index f0a6d6a..6376959 100644 --- a/apps/registry-portal/public/skills/openbotauth.md +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -384,12 +384,12 @@ cat > /tmp/openbotauth-proxy.mjs << 'PROXY_EOF' import { createServer as createHttpServer, request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import { createServer as createTlsServer } from "node:tls"; -import { connect } from "node:net"; -import { createPrivateKey, sign as cryptoSign, randomUUID } from "node:crypto"; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { connect, isIP } from "node:net"; +import { createPrivateKey, sign as cryptoSign, randomUUID, createHash } from "node:crypto"; +import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; const OBA_DIR = join(homedir(), ".config", "openbotauth"); const KEY_FILE = join(OBA_DIR, "key.json"); @@ -404,29 +404,38 @@ const obaKey = JSON.parse(readFileSync(KEY_FILE, "utf-8")); let jwksUrl = null; if (existsSync(CONFIG_FILE)) { const c = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")); jwksUrl = c.jwksUrl || null; } +// Strict hostname validation (blocks shell injection & path traversal) +const HOSTNAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +function isValidHostname(h) { + return typeof h === "string" && h.length > 0 && h.length <= 253 && (HOSTNAME_RE.test(h) || isIP(h) > 0); +} + // Ensure CA exists mkdirSync(CA_DIR, { recursive: true, mode: 0o700 }); if (!existsSync(CA_KEY) || !existsSync(CA_CRT)) { console.log("Generating proxy CA certificate (one-time)..."); - execSync(`openssl req -x509 -new -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -keyout "${CA_KEY}" -out "${CA_CRT}" -days 3650 -subj "/CN=OpenBotAuth Proxy CA/O=OpenBotAuth"`, { stdio: "pipe" }); - execSync(`chmod 600 "${CA_KEY}"`); + execFileSync("openssl", ["req", "-x509", "-new", "-nodes", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1", "-keyout", CA_KEY, "-out", CA_CRT, "-days", "3650", "-subj", "/CN=OpenBotAuth Proxy CA/O=OpenBotAuth"], { stdio: "pipe" }); + execFileSync("chmod", ["600", CA_KEY], { stdio: "pipe" }); } // Per-domain cert cache const certCache = new Map(); function getDomainCert(hostname) { + if (!isValidHostname(hostname)) throw new Error("Invalid hostname: " + hostname.slice(0, 50)); if (certCache.has(hostname)) return certCache.get(hostname); - const tk = join(CA_DIR, `_t_${hostname}.key`), tc = join(CA_DIR, `_t_${hostname}.csr`); - const to = join(CA_DIR, `_t_${hostname}.crt`), te = join(CA_DIR, `_t_${hostname}.ext`); + // Use hash for filenames to prevent path traversal + const hHash = createHash("sha256").update(hostname).digest("hex").slice(0, 16); + const tk = join(CA_DIR, `_t_${hHash}.key`), tc = join(CA_DIR, `_t_${hHash}.csr`); + const to = join(CA_DIR, `_t_${hHash}.crt`), te = join(CA_DIR, `_t_${hHash}.ext`); try { - execSync(`openssl ecparam -genkey -name prime256v1 -noout -out "${tk}"`, { stdio: "pipe" }); - execSync(`openssl req -new -key "${tk}" -out "${tc}" -subj "/CN=${hostname}"`, { stdio: "pipe" }); + execFileSync("openssl", ["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", tk], { stdio: "pipe" }); + execFileSync("openssl", ["req", "-new", "-key", tk, "-out", tc, "-subj", `/CN=${hostname}`], { stdio: "pipe" }); writeFileSync(te, `subjectAltName=DNS:${hostname}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth`); - execSync(`openssl x509 -req -sha256 -in "${tc}" -CA "${CA_CRT}" -CAkey "${CA_KEY}" -CAcreateserial -out "${to}" -days 365 -extfile "${te}"`, { stdio: "pipe" }); + execFileSync("openssl", ["x509", "-req", "-sha256", "-in", tc, "-CA", CA_CRT, "-CAkey", CA_KEY, "-CAcreateserial", "-out", to, "-days", "365", "-extfile", te], { stdio: "pipe" }); const r = { key: readFileSync(tk, "utf-8"), cert: readFileSync(to, "utf-8") }; certCache.set(hostname, r); return r; - } finally { for (const f of [tk, tc, to, te]) try { execSync(`rm -f "${f}"`, { stdio: "pipe" }); } catch {} } + } finally { for (const f of [tk, tc, to, te]) try { unlinkSync(f); } catch {} } } // RFC 9421 signing @@ -460,6 +469,11 @@ const server = createHttpServer((cReq, cRes) => { server.on("connect", (req, cSock, head) => { const id = ++rc, [host, ps] = req.url.split(":"), tp = parseInt(ps) || 443; + // Validate host and port before processing + if (!isValidHostname(host) || tp < 1 || tp > 65535) { + log(id, `CONNECT rejected: invalid ${host}:${tp}`); + cSock.write("HTTP/1.1 400 Bad Request\r\n\r\n"); cSock.end(); return; + } log(id, `CONNECT ${host}:${tp} → MITM`); cSock.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: openbotauth-proxy\r\n\r\n"); const dc = getDomainCert(host); From 9e7fa3c6597a0a3586a7bced774d307b669241dc Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sat, 14 Feb 2026 23:38:22 +0500 Subject: [PATCH 3/8] Add security posture and runtime compatibility to skill - Add Compatibility Modes (Core CLI vs Browser) - Add Token Handling Contract (registration-only, delete after) - Add Token Safety Rules table - Add Runtime Compatibility table - Update Important Notes with token lifecycle warning Makes skill safe for skills.sh distribution and runtime-agnostic. --- .../public/skills/openbotauth.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md index 6376959..0d1f133 100644 --- a/apps/registry-portal/public/skills/openbotauth.md +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -14,6 +14,18 @@ Bash You help users cryptographically sign their browser sessions using OpenBotAuth (OBA) with Ed25519. This skill is **self-contained** — it uses inline Node.js (v18+) for all crypto operations. No external CLI tools are required. +### Compatibility Modes + +**Core Mode (portable, recommended):** +- Works with: Claude Code, Cursor, Codex CLI, Goose, any shell-capable agent +- Uses: Node.js crypto + curl for registration +- Token needed only briefly for `POST /agents` + +**Browser Mode (optional, runtime-dependent):** +- For: agent-browser, OpenClaw Browser Relay, CUA tooling +- Bearer token must NOT live inside the browsing runtime +- Do registration in CLI mode first, then browse with signatures only + ### Key Storage Keys are stored at `~/.config/openbotauth/key.json` in **OBA's canonical format**: @@ -32,6 +44,20 @@ The OBA token lives at `~/.config/openbotauth/token` (chmod 600). Agent registration info (agent_id, JWKS URL) should be saved in agent memory/notes after Step 3. +### Token Handling Contract + +**The bearer token is for registration only:** +- Use it ONLY for `POST /agents` (and key rotation) +- Delete `~/.config/openbotauth/token` after registration completes +- Never attach bearer tokens to browsing sessions + +**Minimum scopes:** `agents:write` + `profile:read` +- Only add `keys:write` if you need `/keys` endpoint + +**Never use global headers with OBA token:** +- agent-browser's `set headers` command applies headers globally +- Use origin-scoped headers only (via `open --headers`) + --- ### Step 1: Check for existing identity @@ -190,6 +216,15 @@ You should see your public key in the `keys` array. This is the URL that verifie **Save the agent_id, username, and JWKS URL to memory/notes** — you'll need the JWKS URL for the `Signature-Agent` header in every signed request. +### Token Safety Rules + +| Do | Don't | +|----|-------| +| `curl -H "Authorization: Bearer ..." https://api.openbotauth.org/agents` | Set bearer token as global browser header | +| Delete token after registration | Keep token in browsing session | +| Use origin-scoped headers for signing | Use `set headers` with bearer tokens | +| Store token at `~/.config/openbotauth/token` (chmod 600) | Paste token into chat logs | + --- ### Step 4: Sign a browser session @@ -556,6 +591,9 @@ The proxy: - `Signature-Agent` must point to a publicly reachable JWKS URL for verification to work - All crypto uses Node.js built-in `crypto` module — no npm dependencies required - **Security:** Never send private keys or OBA tokens to any domain other than `api.openbotauth.org` +- **Token lifecycle:** Delete `~/.config/openbotauth/token` after registration. You won't need it for signing. +- **Browser sessions:** After registration, only signatures travel over the wire. The token stays local and should be deleted. +- **Global headers warning:** Never use `set headers` with bearer tokens in agent-browser. Use `open --headers` for origin-scoped injection. --- @@ -572,6 +610,18 @@ The proxy: └── ca.crt # CA certificate ``` +### Runtime Compatibility + +| Runtime | Support | Notes | +|---------|---------|-------| +| Claude Code / Cursor / Codex | ✅ Full | Recommended path - CLI registration | +| agent-browser | ✅ Full | Use scoped headers, not global | +| OpenClaw Browser Relay | ✅ After registration | Register via CLI first | +| CUA / Browser Control | ⚠️ Caution | Treat control plane as hostile | +| skills.sh | ✅ Full | curl-based registration is safe | + +**For browser runtimes:** Complete registration in CLI mode. The signing proxy only needs the private key (local) and JWKS URL (public). No bearer token needed during browsing. + ### Links - **Website:** https://openbotauth.org From eabe14207aca4d75bb2cfa0731c3220726fe73c3 Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sun, 15 Feb 2026 00:53:22 +0500 Subject: [PATCH 4/8] Make skill publish-ready for skills.sh - Fix CLI tools claim: Node + curl for core, openssl for proxy - Step 3b: write config.json, delete token after registration - Add redirect: error to all token-bearing fetch calls - Step 5: prefer open --headers for single-load demos - Step C: add TLS trust note with --ignore-https-errors option Token deletion is now behavior, not just a rule. --- .../public/skills/openbotauth.md | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md index 0d1f133..77d55eb 100644 --- a/apps/registry-portal/public/skills/openbotauth.md +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -12,7 +12,9 @@ Bash ## Instructions -You help users cryptographically sign their browser sessions using OpenBotAuth (OBA) with Ed25519. This skill is **self-contained** — it uses inline Node.js (v18+) for all crypto operations. No external CLI tools are required. +Portable Ed25519 identity for AI agents. Register once, then sign HTTP requests (RFC 9421) anywhere. Optional browser integrations (agent-browser, OpenClaw Browser Relay) via per-request signing proxy. + +This skill is **self-contained** — no npm packages required. Core mode uses Node.js (v18+) + curl; proxy mode additionally needs openssl. ### Compatibility Modes @@ -154,13 +156,15 @@ const os = require('node:os'); const dir = path.join(os.homedir(), '.config', 'openbotauth'); const key = JSON.parse(fs.readFileSync(path.join(dir, 'key.json'), 'utf-8')); -const token = fs.readFileSync(path.join(dir, 'token'), 'utf-8').trim(); +const tokenPath = path.join(dir, 'token'); +const token = fs.readFileSync(tokenPath, 'utf-8').trim(); const AGENT_NAME = process.argv[1] || 'my-agent'; const API = 'https://api.openbotauth.org'; fetch(API + '/agents', { method: 'POST', + redirect: 'error', // Never follow redirects with bearer token headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' @@ -178,18 +182,32 @@ fetch(API + '/agents', { } }) }) -.then(r => r.json()) +.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(async d => { console.log('Agent registered!'); console.log('Agent ID:', d.id); // Fetch session to get username for JWKS URL const session = await fetch(API + '/auth/session', { + redirect: 'error', // Never follow redirects with bearer token headers: { 'Authorization': 'Bearer ' + token } - }).then(r => r.json()); + }).then(r => { if (!r.ok) throw new Error('Session HTTP ' + r.status); return r.json(); }); const username = session.profile?.username || session.user?.github_username; const jwksUrl = API + '/jwks/' + username + '.json'; + // Write config.json for the signing proxy + fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ + agent_id: d.id, + username: username, + jwksUrl: jwksUrl + }, null, 2), { mode: 0o600 }); + console.log('Config written to ~/.config/openbotauth/config.json'); + + // Delete token — no longer needed after registration + fs.unlinkSync(tokenPath); + console.log('Token deleted (no longer needed)'); + + console.log(''); console.log('JWKS URL:', jwksUrl); console.log(''); console.log('Save this to memory:'); @@ -288,24 +306,25 @@ Replace the arguments: ### Step 5: Apply headers to browser session +**For single signed navigation (demo / Radar proof):** +```bash +agent-browser open --headers '' +``` +This uses origin-scoped headers (safer than global). + +**For real browsing (subresources/XHR):** Use the signing proxy (Step A-C below). + **OpenClaw browser:** ``` set headers --json '' ``` -**agent-browser CLI (if installed):** -```bash -agent-browser set headers '' -agent-browser open -``` - **With named session:** ```bash -agent-browser --session myagent set headers '' -agent-browser --session myagent open +agent-browser --session myagent open --headers '' ``` -**Important: re-sign before each navigation.** Because RFC 9421 signatures are bound to `@method`, `@authority`, and `@path`, you must regenerate headers (Step 4) before navigating to a different URL. +**Important: re-sign before each navigation.** Because RFC 9421 signatures are bound to `@method`, `@authority`, and `@path`, you must regenerate headers (Step 4) before navigating to a different URL. For continuous browsing, use the proxy instead. --- @@ -570,9 +589,16 @@ This starts the signing proxy on `127.0.0.1:8421`. Every HTTP and HTTPS request In another terminal (or from agent-browser): ```bash -agent-browser --proxy http://127.0.0.1:8421 open https://example.com +# For demos (ignore cert warnings): +agent-browser --proxy http://127.0.0.1:8421 --ignore-https-errors open https://example.com + +# For production: install ~/.config/openbotauth/ca/ca.crt as trusted CA ``` +**TLS Note:** The proxy MITMs HTTPS by generating per-domain certs signed by a local CA. Either: +- Use `--ignore-https-errors` for demos/testing +- Install `~/.config/openbotauth/ca/ca.crt` as a trusted CA for clean operation + The proxy: - Signs **every** outgoing request with a fresh RFC 9421 signature - Handles both HTTP and HTTPS (generates a local CA for HTTPS MITM) From a9ffcb47b36ee5baa73c1651301fb28262e62023 Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sun, 15 Feb 2026 01:04:31 +0500 Subject: [PATCH 5/8] Final polish: accuracy and expectations - Header: broader framing (not just browser sessions) - Step 3b: add username guard to prevent undefined.json - Step 4: remove unused SESSION_ID, update description - Proxy: add CA security warning and protocol limitations - Enterprise SSO: mark as TBD (endpoint not yet live) --- .../public/skills/openbotauth.md | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md index 77d55eb..dd764e8 100644 --- a/apps/registry-portal/public/skills/openbotauth.md +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -1,6 +1,6 @@ # openbotauth -Cryptographic identity for AI agent browser sessions. Signs HTTP requests with Ed25519 keys using RFC 9421 Message Signatures via OpenBotAuth. +Cryptographic identity for AI agents. Register once, then sign HTTP requests (RFC 9421) anywhere. Optional browser integrations via per-request signing proxy. ## When to trigger @@ -193,6 +193,7 @@ fetch(API + '/agents', { headers: { 'Authorization': 'Bearer ' + token } }).then(r => { if (!r.ok) throw new Error('Session HTTP ' + r.status); return r.json(); }); const username = session.profile?.username || session.user?.github_username; + if (!username) throw new Error('Could not resolve username from /auth/session'); const jwksUrl = API + '/jwks/' + username + '.json'; // Write config.json for the signing proxy @@ -245,9 +246,9 @@ You should see your public key in the `keys` array. This is the URL that verifie --- -### Step 4: Sign a browser session +### Step 4: Sign a request -Generate RFC 9421 signed headers for a target URL. The output is a JSON object for `set headers --json` (OpenClaw) or `agent-browser set headers`. +Generate RFC 9421 signed headers for a target URL. The output is a JSON object for `agent-browser open --headers` or `set headers --json` (OpenClaw). **Required inputs:** - `TARGET_URL` — the URL being browsed @@ -264,9 +265,8 @@ const { homedir } = require('os'); const METHOD = (process.argv[1] || 'GET').toUpperCase(); const TARGET_URL = process.argv[2]; const JWKS_URL = process.argv[3] || ''; -const SESSION_ID = process.argv[4] || 'oba-session-' + randomUUID(); -if (!TARGET_URL) { console.error('Usage: node sign.js METHOD URL JWKS_URL [SESSION_ID]'); process.exit(1); } +if (!TARGET_URL) { console.error('Usage: node sign.js METHOD URL JWKS_URL'); process.exit(1); } const key = JSON.parse(readFileSync(join(homedir(), '.config', 'openbotauth', 'key.json'), 'utf-8')); const url = new URL(TARGET_URL); @@ -296,7 +296,7 @@ if (JWKS_URL) { } console.log(JSON.stringify(headers)); -" "METHOD" "TARGET_URL" "JWKS_URL" "OPTIONAL_SESSION_ID" +" "METHOD" "TARGET_URL" "JWKS_URL" ``` Replace the arguments: @@ -346,7 +346,9 @@ console.log('Created: ' + k.createdAt); --- -### Enterprise SSO Registration (Okta / WorkOS / Descope) +### Enterprise SSO Registration (Okta / WorkOS / Descope) — TBD + +> **Note:** This endpoint (`/enterprise/keys`) is on the roadmap but not yet implemented. The code below is forward-looking. For organizations that want to bind agent identities to their SSO: @@ -606,6 +608,13 @@ The proxy: - Runs on `127.0.0.1:8421` by default (configurable with `--port`) - Requires openssl (pre-installed on macOS/Linux) for HTTPS certificate generation +**Security warning:** `~/.config/openbotauth/ca/ca.key` is a local MITM root key. Treat it as sensitive as a private key — if stolen, an attacker can intercept traffic on that machine. + +**Limitations:** +- HTTP/2, WebSockets, and multiplexed connections are not reliably supported +- Best for demos and basic browsing; not a production-grade proxy +- IP addresses in hostnames use `DNS:` in SAN (some clients may reject this) + **When to use Steps 4-5 instead:** Simple single-page-load scenarios where you control every navigation and can re-sign before each one. --- From 7f2e4c768928aa873caa1b6a0b92180640a75216 Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sun, 15 Feb 2026 01:32:38 +0500 Subject: [PATCH 6/8] Add SDK references, strict verifier note, and IP mitigation to skill --- .../public/skills/openbotauth.md | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md index dd764e8..2e73f08 100644 --- a/apps/registry-portal/public/skills/openbotauth.md +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -304,6 +304,8 @@ Replace the arguments: - `TARGET_URL` — e.g., `https://example.com/page` - `JWKS_URL` — e.g., `https://api.openbotauth.org/jwks/your-username.json` +**For strict verifiers:** If a site rejects signatures from this inline signer, use the reference implementation from `@openbotauth/bot-cli` or the `signing-ts` demo package for exact RFC 9421 canonicalization. + ### Step 5: Apply headers to browser session **For single signed navigation (demo / Radar proof):** @@ -346,46 +348,18 @@ console.log('Created: ' + k.createdAt); --- -### Enterprise SSO Registration (Okta / WorkOS / Descope) — TBD - -> **Note:** This endpoint (`/enterprise/keys`) is on the roadmap but not yet implemented. The code below is forward-looking. - -For organizations that want to bind agent identities to their SSO: - -```bash -node -e " -const { readFileSync } = require('fs'); -const { join } = require('path'); -const { homedir } = require('os'); +### Enterprise SSO Binding — Roadmap -const PROVIDER = process.argv[1]; -const ORG_ID = process.argv[2]; -const TOKEN = process.argv[3]; -const API = process.argv[4] || 'https://api.openbotauth.org'; +> **Status:** Not yet implemented. This describes the planned direction. -const key = JSON.parse(readFileSync(join(homedir(), '.config', 'openbotauth', 'key.json'), 'utf-8')); +For organizations using Okta, WorkOS, or Descope: OBA will support binding agent keys to enterprise subjects issued by your IdP. OBA is **not replacing your IdP directory** — it attaches verifiable agent keys and audit trails to identities you already manage. -fetch(API + '/enterprise/keys', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + TOKEN, - 'X-SSO-Provider': PROVIDER, - 'X-Org-ID': ORG_ID - }, - body: JSON.stringify({ - key: { kty: 'OKP', crv: 'Ed25519', kid: key.kid, x: key.x, use: 'sig', alg: 'EdDSA' }, - sso: { provider: PROVIDER, orgId: ORG_ID }, - metadata: { tool: 'openbotauth', platform: 'openclaw' } - }) -}) -.then(r => r.json()) -.then(d => console.log(JSON.stringify(d, null, 2))) -.catch(e => console.error('Failed:', e.message)); -" "PROVIDER" "ORG_ID" "SSO_TOKEN" -``` +**Planned flow:** +1. Authenticate via your IdP (SAML/OIDC) +2. Bind an agent public key to that enterprise subject +3. Signatures from that agent carry the enterprise identity anchor -Supported providers: `okta`, `workos`, `descope`. +This complements (not competes with) IdP-native agent features — you get portable keys + web verification surface. --- @@ -613,7 +587,7 @@ The proxy: **Limitations:** - HTTP/2, WebSockets, and multiplexed connections are not reliably supported - Best for demos and basic browsing; not a production-grade proxy -- IP addresses in hostnames use `DNS:` in SAN (some clients may reject this) +- **IP-based hostnames:** If the CONNECT target is an IP address, consider rejecting it or use `subjectAltName=IP:` instead of `DNS:` (current code uses DNS, which strict clients may reject) **When to use Steps 4-5 instead:** Simple single-page-load scenarios where you control every navigation and can re-sign before each one. @@ -657,6 +631,16 @@ The proxy: **For browser runtimes:** Complete registration in CLI mode. The signing proxy only needs the private key (local) and JWKS URL (public). No bearer token needed during browsing. +### Official Packages + +For production integrations, prefer the official packages: +- `@openbotauth/verifier-client` — verify signatures +- `@openbotauth/registry-signer` — key generation and JWK utilities +- `@openbotauth/bot-cli` — CLI for signing requests +- `@openbotauth/proxy` — signing proxy + +For strict RFC 9421 signing, use the reference signer from `openbotauth-demos` (`packages/signing-ts`). + ### Links - **Website:** https://openbotauth.org From 3f4eed099aef05c3e831b9fd97391d304287a31e Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sun, 15 Feb 2026 01:38:26 +0500 Subject: [PATCH 7/8] Remove duplicate intro, tighten strict verifier wording --- apps/registry-portal/public/skills/openbotauth.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/openbotauth.md index 2e73f08..680eae6 100644 --- a/apps/registry-portal/public/skills/openbotauth.md +++ b/apps/registry-portal/public/skills/openbotauth.md @@ -12,8 +12,6 @@ Bash ## Instructions -Portable Ed25519 identity for AI agents. Register once, then sign HTTP requests (RFC 9421) anywhere. Optional browser integrations (agent-browser, OpenClaw Browser Relay) via per-request signing proxy. - This skill is **self-contained** — no npm packages required. Core mode uses Node.js (v18+) + curl; proxy mode additionally needs openssl. ### Compatibility Modes @@ -304,7 +302,7 @@ Replace the arguments: - `TARGET_URL` — e.g., `https://example.com/page` - `JWKS_URL` — e.g., `https://api.openbotauth.org/jwks/your-username.json` -**For strict verifiers:** If a site rejects signatures from this inline signer, use the reference implementation from `@openbotauth/bot-cli` or the `signing-ts` demo package for exact RFC 9421 canonicalization. +**For strict verifiers:** If a site rejects signatures from this inline signer, use `@openbotauth/bot-cli` (recommended) or the `openbotauth-demos/packages/signing-ts` reference signer. ### Step 5: Apply headers to browser session From 4d4f874c253d9408408a8814d675660605c68d47 Mon Sep 17 00:00:00 2001 From: Hammad Tariq Date: Sun, 15 Feb 2026 01:46:04 +0500 Subject: [PATCH 8/8] Rename skill to SKILL.md for ClawHub/skills.sh compatibility --- apps/registry-portal/public/robots.txt | 2 +- apps/registry-portal/public/skills/{openbotauth.md => SKILL.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename apps/registry-portal/public/skills/{openbotauth.md => SKILL.md} (100%) diff --git a/apps/registry-portal/public/robots.txt b/apps/registry-portal/public/robots.txt index b432b95..bd18c13 100644 --- a/apps/registry-portal/public/robots.txt +++ b/apps/registry-portal/public/robots.txt @@ -16,4 +16,4 @@ Allow: / Sitemap: https://openbotauth.org/sitemap.xml # AI agents: get a cryptographic identity -# Skill: https://openbotauth.org/skill/openbotauth.md +# Skill: https://openbotauth.org/skills/SKILL.md diff --git a/apps/registry-portal/public/skills/openbotauth.md b/apps/registry-portal/public/skills/SKILL.md similarity index 100% rename from apps/registry-portal/public/skills/openbotauth.md rename to apps/registry-portal/public/skills/SKILL.md