{
  "schemaVersion": "1.0",
  "item": {
    "slug": "openbotauth",
    "name": "OpenBotAuth",
    "source": "tencent",
    "type": "skill",
    "category": "AI 智能",
    "sourceUrl": "https://clawhub.ai/hammadtq/openbotauth",
    "canonicalUrl": "https://clawhub.ai/hammadtq/openbotauth",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/openbotauth",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=openbotauth",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/openbotauth"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/openbotauth",
    "agentPageUrl": "https://openagent3.xyz/skills/openbotauth/agent",
    "manifestUrl": "https://openagent3.xyz/skills/openbotauth/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/openbotauth/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "openbotauth",
        "body": "Cryptographic identity for AI agents. Register once, then sign HTTP requests (RFC 9421) anywhere. Optional browser integrations via per-request signing proxy."
      },
      {
        "title": "When to trigger",
        "body": "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."
      },
      {
        "title": "Tools",
        "body": "Bash"
      },
      {
        "title": "Instructions",
        "body": "This skill is self-contained — no npm packages required. Core mode uses Node.js (v18+) + curl; proxy mode additionally needs openssl."
      },
      {
        "title": "Compatibility Modes",
        "body": "Core Mode (portable, recommended):\n\nWorks with: Claude Code, Cursor, Codex CLI, Goose, any shell-capable agent\nUses: Node.js crypto + curl for registration\nToken needed only briefly for POST /agents\n\nBrowser Mode (optional, runtime-dependent):\n\nFor: agent-browser, OpenClaw Browser Relay, CUA tooling\nBearer token must NOT live inside the browsing runtime\nDo registration in CLI mode first, then browse with signatures only"
      },
      {
        "title": "Key Storage",
        "body": "Keys are stored at ~/.config/openbotauth/key.json in OBA's canonical format:\n\n{\n  \"kid\": \"<thumbprint-based-id>\",\n  \"x\": \"<base64url-raw-public-key>\",\n  \"publicKeyPem\": \"-----BEGIN PUBLIC KEY-----\\n...\",\n  \"privateKeyPem\": \"-----BEGIN PRIVATE KEY-----\\n...\",\n  \"createdAt\": \"...\"\n}\n\nThe OBA token lives at ~/.config/openbotauth/token (chmod 600).\n\nAgent registration info (agent_id, JWKS URL) should be saved in agent memory/notes after Step 3."
      },
      {
        "title": "Token Handling Contract",
        "body": "The bearer token is for registration only:\n\nUse it ONLY for POST /agents (and key rotation)\nDelete ~/.config/openbotauth/token after registration completes\nNever attach bearer tokens to browsing sessions\n\nMinimum scopes: agents:write + profile:read\n\nOnly add keys:write if you need /keys endpoint\n\nNever use global headers with OBA token:\n\nagent-browser's set headers command applies headers globally\nUse origin-scoped headers only (via open --headers)"
      },
      {
        "title": "Step 1: Check for existing identity",
        "body": "cat ~/.config/openbotauth/key.json 2>/dev/null && echo \"---KEY EXISTS---\" || echo \"---NO KEY FOUND---\"\n\nIf 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).\n\nIf no key exists: proceed to Step 2."
      },
      {
        "title": "Step 2: Generate Ed25519 keypair (if no key exists)",
        "body": "Run this locally. Nothing leaves the machine.\n\nnode -e \"\nconst crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst os = require('node:os');\nconst path = require('node:path');\n\nconst { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');\nconst publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();\nconst privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();\n\n// Derive kid from JWK thumbprint (matches OBA's format)\nconst spki = publicKey.export({ type: 'spki', format: 'der' });\nif (spki.length !== 44) throw new Error('Unexpected SPKI length: ' + spki.length);\nconst rawPub = spki.subarray(12, 44);\nconst x = rawPub.toString('base64url');\nconst thumbprint = JSON.stringify({ kty: 'OKP', crv: 'Ed25519', x });\nconst hash = crypto.createHash('sha256').update(thumbprint).digest();\nconst kid = hash.toString('base64url').slice(0, 16);\n\nconst dir = path.join(os.homedir(), '.config', 'openbotauth');\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nfs.writeFileSync(path.join(dir, 'key.json'), JSON.stringify({\n  kid, x, publicKeyPem, privateKeyPem,\n  createdAt: new Date().toISOString()\n}, null, 2), { mode: 0o600 });\n\nconsole.log('Key generated!');\nconsole.log('kid:', kid);\nconsole.log('x:', x);\n\"\n\nSave the kid and x values — needed for registration."
      },
      {
        "title": "Step 3: Register with OpenBotAuth (if not yet registered)",
        "body": "This is a one-time setup that gives your agent a public JWKS endpoint for signature verification.\n\n3a. Get a token from the user\n\nAsk the user:\n\nI need an OpenBotAuth token to register my cryptographic identity. Takes 30 seconds:\n\nGo to https://openbotauth.org/token\nClick \"Login with GitHub\"\nCopy the token and paste it back to me\n\nThe token looks like oba_ followed by 64 hex characters.\n\nWhen they provide it, save it:\n\nnode -e \"\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst os = require('node:os');\nconst dir = path.join(os.homedir(), '.config', 'openbotauth');\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nconst token = process.argv[1].trim();\nfs.writeFileSync(path.join(dir, 'token'), token, { mode: 0o600 });\nconsole.log('Token saved.');\n\" \"THE_TOKEN_HERE\"\n\n3b. Register the agent\n\nnode -e \"\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst os = require('node:os');\n\nconst dir = path.join(os.homedir(), '.config', 'openbotauth');\nconst key = JSON.parse(fs.readFileSync(path.join(dir, 'key.json'), 'utf-8'));\nconst tokenPath = path.join(dir, 'token');\nconst token = fs.readFileSync(tokenPath, 'utf-8').trim();\n\nconst AGENT_NAME = process.argv[1] || 'my-agent';\nconst API = 'https://api.openbotauth.org';\n\nfetch(API + '/agents', {\n  method: 'POST',\n  redirect: 'error',  // Never follow redirects with bearer token\n  headers: {\n    'Authorization': 'Bearer ' + token,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    name: AGENT_NAME,\n    agent_type: 'agent',\n    public_key: {\n      kty: 'OKP',\n      crv: 'Ed25519',\n      kid: key.kid,\n      x: key.x,\n      use: 'sig',\n      alg: 'EdDSA'\n    }\n  })\n})\n.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })\n.then(async d => {\n  console.log('Agent registered!');\n  console.log('Agent ID:', d.id);\n\n  // Fetch session to get username for JWKS URL\n  const session = await fetch(API + '/auth/session', {\n    redirect: 'error',  // Never follow redirects with bearer token\n    headers: { 'Authorization': 'Bearer ' + token }\n  }).then(r => { if (!r.ok) throw new Error('Session HTTP ' + r.status); return r.json(); });\n  const username = session.profile?.username || session.user?.github_username;\n  if (!username) throw new Error('Could not resolve username from /auth/session');\n  const jwksUrl = API + '/jwks/' + username + '.json';\n\n  // Write config.json for the signing proxy\n  fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({\n    agent_id: d.id,\n    username: username,\n    jwksUrl: jwksUrl\n  }, null, 2), { mode: 0o600 });\n  console.log('Config written to ~/.config/openbotauth/config.json');\n\n  // Delete token — no longer needed after registration\n  fs.unlinkSync(tokenPath);\n  console.log('Token deleted (no longer needed)');\n\n  console.log('');\n  console.log('JWKS URL:', jwksUrl);\n  console.log('');\n  console.log('Save this to memory:');\n  console.log(JSON.stringify({\n    openbotauth: {\n      agent_id: d.id,\n      kid: key.kid,\n      username: username,\n      jwks_url: jwksUrl\n    }\n  }, null, 2));\n})\n.catch(e => console.error('Registration failed:', e.message));\n\" \"AGENT_NAME_HERE\"\n\n3c. Verify registration\n\ncurl https://api.openbotauth.org/jwks/YOUR_USERNAME.json\n\nYou should see your public key in the keys array. This is the URL that verifiers will use to check your signatures.\n\nSave 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."
      },
      {
        "title": "Token Safety Rules",
        "body": "DoDon'tcurl -H \"Authorization: Bearer ...\" https://api.openbotauth.org/agentsSet bearer token as global browser headerDelete token after registrationKeep token in browsing sessionUse origin-scoped headers for signingUse set headers with bearer tokensStore token at ~/.config/openbotauth/token (chmod 600)Paste token into chat logs"
      },
      {
        "title": "Step 4: Sign a request",
        "body": "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).\n\nRequired inputs:\n\nTARGET_URL — the URL being browsed\nMETHOD — HTTP method (GET, POST, etc.)\nJWKS_URL — your JWKS endpoint from Step 3 (the Signature-Agent value)\n\nnode -e \"\nconst { createPrivateKey, sign, randomUUID } = require('crypto');\nconst { readFileSync } = require('fs');\nconst { join } = require('path');\nconst { homedir } = require('os');\n\nconst METHOD = (process.argv[1] || 'GET').toUpperCase();\nconst TARGET_URL = process.argv[2];\nconst JWKS_URL = process.argv[3] || '';\n\nif (!TARGET_URL) { console.error('Usage: node sign.js METHOD URL JWKS_URL'); process.exit(1); }\n\nconst key = JSON.parse(readFileSync(join(homedir(), '.config', 'openbotauth', 'key.json'), 'utf-8'));\nconst url = new URL(TARGET_URL);\nconst created = Math.floor(Date.now() / 1000);\nconst expires = created + 300;\nconst nonce = randomUUID();\n\n// RFC 9421 signature base\nconst lines = [\n  '\\\"@method\\\": ' + METHOD,\n  '\\\"@authority\\\": ' + url.host,\n  '\\\"@path\\\": ' + url.pathname + url.search\n];\nconst sigInput = '(\\\"@method\\\" \\\"@authority\\\" \\\"@path\\\");created=' + created + ';expires=' + expires + ';nonce=\\\"' + nonce + '\\\";keyid=\\\"' + key.kid + '\\\";alg=\\\"ed25519\\\"';\nlines.push('\\\"@signature-params\\\": ' + sigInput);\n\nconst base = lines.join('\\n');\nconst pk = createPrivateKey(key.privateKeyPem);\nconst sig = sign(null, Buffer.from(base), pk).toString('base64');\n\nconst headers = {\n  'Signature': 'sig1=:' + sig + ':',\n  'Signature-Input': 'sig1=' + sigInput\n};\nif (JWKS_URL) {\n  headers['Signature-Agent'] = JWKS_URL;\n}\n\nconsole.log(JSON.stringify(headers));\n\" \"METHOD\" \"TARGET_URL\" \"JWKS_URL\"\n\nReplace the arguments:\n\nMETHOD — e.g., GET\nTARGET_URL — e.g., https://example.com/page\nJWKS_URL — e.g., https://api.openbotauth.org/jwks/your-username.json\n\nFor 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."
      },
      {
        "title": "Step 5: Apply headers to browser session",
        "body": "For single signed navigation (demo / Radar proof):\n\nagent-browser open <url> --headers '<OUTPUT_FROM_STEP_4>'\n\nThis uses origin-scoped headers (safer than global).\n\nFor real browsing (subresources/XHR): Use the signing proxy (Step A-C below).\n\nOpenClaw browser:\n\nset headers --json '<OUTPUT_FROM_STEP_4>'\n\nWith named session:\n\nagent-browser --session myagent open <url> --headers '<OUTPUT_FROM_STEP_4>'\n\nImportant: 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."
      },
      {
        "title": "Step 6: Show current identity",
        "body": "node -e \"\nconst { readFileSync, existsSync } = require('fs');\nconst { join } = require('path');\nconst { homedir } = require('os');\nconst f = join(homedir(), '.config', 'openbotauth', 'key.json');\nif (!existsSync(f)) { console.log('No identity found. Run Step 2 first.'); process.exit(0); }\nconst k = JSON.parse(readFileSync(f, 'utf-8'));\nconsole.log('kid:        ' + k.kid);\nconsole.log('Public (x): ' + k.x);\nconsole.log('Created:    ' + k.createdAt);\n\""
      },
      {
        "title": "Enterprise SSO Binding — Roadmap",
        "body": "Status: Not yet implemented. This describes the planned direction.\n\nFor 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.\n\nPlanned flow:\n\nAuthenticate via your IdP (SAML/OIDC)\nBind an agent public key to that enterprise subject\nSignatures from that agent carry the enterprise identity anchor\n\nThis complements (not competes with) IdP-native agent features — you get portable keys + web verification surface."
      },
      {
        "title": "Signed Headers Reference",
        "body": "Every signed request produces these RFC 9421-compliant headers:\n\nHeaderPurposeSignaturesig1=:<base64-ed25519-signature>:Signature-InputCovered components (@method @authority @path), created, expires, nonce, keyid, algSignature-AgentJWKS URL for public key resolution (from OBA Registry)\n\nThe Signature-Input encodes everything a verifier needs: which components were signed, when, by whom (keyid), and when it expires."
      },
      {
        "title": "OpenClaw Session Binding",
        "body": "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:\n\nagent:main:main                              # Main chat session\nagent:main:discord:channel:123456789         # Discord channel\nagent:main:subagent:<uuid>                   # Spawned sub-agent\n\nThis lets publishers trace whether a request came from the main agent or a sub-agent."
      },
      {
        "title": "Sub-Agent Identity (Tier 2 — TBD)",
        "body": "Sub-agent key derivation (HKDF from parent key) is planned but not yet implemented in a cryptographically sound way. For now, sub-agents should:\n\nGenerate their own independent keypair (Step 2)\nRegister separately with OBA (Step 3)\nOptionally, the parent agent can publish a signed attestation linking the sub-agent's kid to its own\n\nA proper delegation/attestation protocol is being designed."
      },
      {
        "title": "Per-Request Signing via Proxy (Recommended for Real Browsing)",
        "body": "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.\n\nSolution: 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.\n\nStep A: Write the proxy to a temp file\n\ncat > /tmp/openbotauth-proxy.mjs << 'PROXY_EOF'\nimport { createServer as createHttpServer, request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { createServer as createTlsServer } from \"node:tls\";\nimport { connect, isIP } from \"node:net\";\nimport { createPrivateKey, sign as cryptoSign, randomUUID, createHash } from \"node:crypto\";\nimport { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { execFileSync } from \"node:child_process\";\n\nconst OBA_DIR = join(homedir(), \".config\", \"openbotauth\");\nconst KEY_FILE = join(OBA_DIR, \"key.json\");\nconst CONFIG_FILE = join(OBA_DIR, \"config.json\");\nconst CA_DIR = join(OBA_DIR, \"ca\");\nconst CA_KEY = join(CA_DIR, \"ca.key\");\nconst CA_CRT = join(CA_DIR, \"ca.crt\");\n\n// Load credentials\nif (!existsSync(KEY_FILE)) { console.error(\"No key found. Run keygen first.\"); process.exit(1); }\nconst obaKey = JSON.parse(readFileSync(KEY_FILE, \"utf-8\"));\nlet jwksUrl = null;\nif (existsSync(CONFIG_FILE)) { const c = JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")); jwksUrl = c.jwksUrl || null; }\n\n// Strict hostname validation (blocks shell injection & path traversal)\nconst 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])?)*$/;\nfunction isValidHostname(h) {\n  return typeof h === \"string\" && h.length > 0 && h.length <= 253 && (HOSTNAME_RE.test(h) || isIP(h) > 0);\n}\n\n// Ensure CA exists\nmkdirSync(CA_DIR, { recursive: true, mode: 0o700 });\nif (!existsSync(CA_KEY) || !existsSync(CA_CRT)) {\n  console.log(\"Generating proxy CA certificate (one-time)...\");\n  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\" });\n  execFileSync(\"chmod\", [\"600\", CA_KEY], { stdio: \"pipe\" });\n}\n\n// Per-domain cert cache\nconst certCache = new Map();\nfunction getDomainCert(hostname) {\n  if (!isValidHostname(hostname)) throw new Error(\"Invalid hostname: \" + hostname.slice(0, 50));\n  if (certCache.has(hostname)) return certCache.get(hostname);\n  // Use hash for filenames to prevent path traversal\n  const hHash = createHash(\"sha256\").update(hostname).digest(\"hex\").slice(0, 16);\n  const tk = join(CA_DIR, `_t_${hHash}.key`), tc = join(CA_DIR, `_t_${hHash}.csr`);\n  const to = join(CA_DIR, `_t_${hHash}.crt`), te = join(CA_DIR, `_t_${hHash}.ext`);\n  try {\n    execFileSync(\"openssl\", [\"ecparam\", \"-genkey\", \"-name\", \"prime256v1\", \"-noout\", \"-out\", tk], { stdio: \"pipe\" });\n    execFileSync(\"openssl\", [\"req\", \"-new\", \"-key\", tk, \"-out\", tc, \"-subj\", `/CN=${hostname}`], { stdio: \"pipe\" });\n    writeFileSync(te, `subjectAltName=DNS:${hostname}\\nbasicConstraints=CA:FALSE\\nkeyUsage=digitalSignature,keyEncipherment\\nextendedKeyUsage=serverAuth`);\n    execFileSync(\"openssl\", [\"x509\", \"-req\", \"-sha256\", \"-in\", tc, \"-CA\", CA_CRT, \"-CAkey\", CA_KEY, \"-CAcreateserial\", \"-out\", to, \"-days\", \"365\", \"-extfile\", te], { stdio: \"pipe\" });\n    const r = { key: readFileSync(tk, \"utf-8\"), cert: readFileSync(to, \"utf-8\") };\n    certCache.set(hostname, r);\n    return r;\n  } finally { for (const f of [tk, tc, to, te]) try { unlinkSync(f); } catch {} }\n}\n\n// RFC 9421 signing\nfunction signReq(method, authority, path) {\n  const created = Math.floor(Date.now() / 1000), expires = created + 300, nonce = randomUUID();\n  const lines = [`\"@method\": ${method.toUpperCase()}`, `\"@authority\": ${authority}`, `\"@path\": ${path}`];\n  const sigInput = `(\"@method\" \"@authority\" \"@path\");created=${created};expires=${expires};nonce=\"${nonce}\";keyid=\"${obaKey.kid}\";alg=\"ed25519\"`;\n  lines.push(`\"@signature-params\": ${sigInput}`);\n  const sig = cryptoSign(null, Buffer.from(lines.join(\"\\n\")), createPrivateKey(obaKey.privateKeyPem)).toString(\"base64\");\n  const h = { signature: `sig1=:${sig}:`, \"signature-input\": `sig1=${sigInput}` };\n  if (jwksUrl) h[\"signature-agent\"] = jwksUrl;\n  return h;\n}\n\nconst verbose = process.argv.includes(\"--verbose\") || process.argv.includes(\"-v\");\nconst port = parseInt(process.argv.find((a,i) => process.argv[i-1] === \"--port\")) || 8421;\nlet rc = 0;\nfunction log(id, msg) { if (verbose) console.log(`[${id}] ${msg}`); }\n\nconst server = createHttpServer((cReq, cRes) => {\n  const id = ++rc, url = new URL(cReq.url), auth = url.host, p = url.pathname + url.search;\n  const sig = signReq(cReq.method, auth, p);\n  log(id, `HTTP ${cReq.method} ${auth}${p} → signed`);\n  const h = { ...cReq.headers }; delete h[\"proxy-connection\"]; delete h[\"proxy-authorization\"];\n  Object.assign(h, sig); h.host = auth;\n  const fn = url.protocol === \"https:\" ? httpsRequest : httpRequest;\n  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); });\n  pr.on(\"error\", (e) => { log(id, `Error: ${e.message}`); cRes.writeHead(502); cRes.end(\"Proxy error\"); });\n  cReq.pipe(pr);\n});\n\nserver.on(\"connect\", (req, cSock, head) => {\n  const id = ++rc, [host, ps] = req.url.split(\":\"), tp = parseInt(ps) || 443;\n  // Validate host and port before processing\n  if (!isValidHostname(host) || tp < 1 || tp > 65535) {\n    log(id, `CONNECT rejected: invalid ${host}:${tp}`);\n    cSock.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\"); cSock.end(); return;\n  }\n  log(id, `CONNECT ${host}:${tp} → MITM`);\n  cSock.write(\"HTTP/1.1 200 Connection Established\\r\\nProxy-Agent: openbotauth-proxy\\r\\n\\r\\n\");\n  const dc = getDomainCert(host);\n  const tls = createTlsServer({ key: dc.key, cert: dc.cert }, (ts) => {\n    let data = Buffer.alloc(0);\n    ts.on(\"data\", (chunk) => {\n      data = Buffer.concat([data, chunk]);\n      const he = data.indexOf(\"\\r\\n\\r\\n\");\n      if (he === -1) return;\n      const hs = data.subarray(0, he).toString(), body = data.subarray(he + 4);\n      const ls = hs.split(\"\\r\\n\"), [method, path] = ls[0].split(\" \");\n      const rh = {};\n      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(); }\n      const cl = parseInt(rh[\"content-length\"]) || 0, fp = path || \"/\";\n      const sig = signReq(method, host + (tp !== 443 ? `:${tp}` : \"\"), fp);\n      log(id, `HTTPS ${method} ${host}${fp} → signed`);\n      Object.assign(rh, sig);\n      const pr = httpsRequest({ hostname: host, port: tp, path: fp, method, headers: rh, rejectUnauthorized: true }, (r) => {\n        let resp = `HTTP/1.1 ${r.statusCode} ${r.statusMessage}\\r\\n`;\n        const rw = r.rawHeaders; for (let i = 0; i < rw.length; i += 2) resp += `${rw[i]}: ${rw[i+1]}\\r\\n`;\n        resp += \"\\r\\n\"; ts.write(resp); r.pipe(ts);\n      });\n      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\"); });\n      if (body.length > 0) pr.write(body);\n      if (cl <= body.length) { pr.end(); } else {\n        let recv = body.length;\n        const bh = (d) => { recv += d.length; pr.write(d); if (recv >= cl) { pr.end(); ts.removeListener(\"data\", bh); } };\n        ts.on(\"data\", bh);\n      }\n    });\n  });\n  tls.listen(0, \"127.0.0.1\", () => {\n    const lc = connect(tls.address().port, \"127.0.0.1\", () => { lc.write(head); lc.pipe(cSock); cSock.pipe(lc); });\n    lc.on(\"error\", () => cSock.end()); cSock.on(\"error\", () => lc.end());\n    cSock.on(\"close\", () => { tls.close(); lc.end(); });\n  });\n});\n\nserver.listen(port, \"127.0.0.1\", () => {\n  console.log(`openbotauth signing proxy on http://127.0.0.1:${port}`);\n  console.log(`  kid: ${obaKey.kid}`);\n  if (jwksUrl) console.log(`  Signature-Agent: ${jwksUrl}`);\n  console.log(\"Every request gets a fresh RFC 9421 signature.\");\n});\nPROXY_EOF\necho \"Proxy written to /tmp/openbotauth-proxy.mjs\"\n\nStep B: Start the proxy\n\nnode /tmp/openbotauth-proxy.mjs --verbose\n\nThis 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.\n\nStep C: Browse through the proxy\n\nIn another terminal (or from agent-browser):\n\n# For demos (ignore cert warnings):\nagent-browser --proxy http://127.0.0.1:8421 --ignore-https-errors open https://example.com\n\n# For production: install ~/.config/openbotauth/ca/ca.crt as trusted CA\n\nTLS Note: The proxy MITMs HTTPS by generating per-domain certs signed by a local CA. Either:\n\nUse --ignore-https-errors for demos/testing\nInstall ~/.config/openbotauth/ca/ca.crt as a trusted CA for clean operation\n\nThe proxy:\n\nSigns every outgoing request with a fresh RFC 9421 signature\nHandles both HTTP and HTTPS (generates a local CA for HTTPS MITM)\nIncludes the Signature-Agent header (JWKS URL) on every request\nRuns on 127.0.0.1:8421 by default (configurable with --port)\nRequires openssl (pre-installed on macOS/Linux) for HTTPS certificate generation\n\nSecurity 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.\n\nLimitations:\n\nHTTP/2, WebSockets, and multiplexed connections are not reliably supported\nBest for demos and basic browsing; not a production-grade proxy\nIP-based hostnames: If the CONNECT target is an IP address, consider rejecting it or use subjectAltName=IP:<ip> instead of DNS: (current code uses DNS, which strict clients may reject)\n\nWhen to use Steps 4-5 instead: Simple single-page-load scenarios where you control every navigation and can re-sign before each one."
      },
      {
        "title": "Important Notes",
        "body": "Private keys live at ~/.config/openbotauth/key.json with 0600 permissions — never expose them\nThe OBA token at ~/.config/openbotauth/token is also sensitive — never log or share it\nSignature-Agent must point to a publicly reachable JWKS URL for verification to work\nAll crypto uses Node.js built-in crypto module — no npm dependencies required\nSecurity: Never send private keys or OBA tokens to any domain other than api.openbotauth.org\nToken lifecycle: Delete ~/.config/openbotauth/token after registration. You won't need it for signing.\nBrowser sessions: After registration, only signatures travel over the wire. The token stays local and should be deleted.\nGlobal headers warning: Never use set headers with bearer tokens in agent-browser. Use open --headers for origin-scoped injection."
      },
      {
        "title": "File Layout",
        "body": "~/.config/openbotauth/\n├── key.json       # kid, x, publicKeyPem, privateKeyPem (chmod 600)\n├── key.pub.json   # Public JWK for sharing (chmod 644)\n├── config.json    # Agent ID, JWKS URL, registration info\n├── token          # oba_xxx bearer token (chmod 600)\n└── ca/            # Proxy CA certificate (auto-generated)\n    ├── ca.key     # CA private key\n    └── ca.crt     # CA certificate"
      },
      {
        "title": "Runtime Compatibility",
        "body": "RuntimeSupportNotesClaude Code / Cursor / Codex✅ FullRecommended path - CLI registrationagent-browser✅ FullUse scoped headers, not globalOpenClaw Browser Relay✅ After registrationRegister via CLI firstCUA / Browser Control⚠️ CautionTreat control plane as hostileskills.sh✅ Fullcurl-based registration is safe\n\nFor 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."
      },
      {
        "title": "Official Packages",
        "body": "For production integrations, prefer the official packages:\n\n@openbotauth/verifier-client — verify signatures\n@openbotauth/registry-signer — key generation and JWK utilities\n@openbotauth/bot-cli — CLI for signing requests\n@openbotauth/proxy — signing proxy\n\nFor strict RFC 9421 signing, use the reference signer from openbotauth-demos (packages/signing-ts)."
      },
      {
        "title": "Links",
        "body": "Website: https://openbotauth.org\nAPI: https://api.openbotauth.org\nSpec: https://github.com/OpenBotAuth/openbotauth\nIETF: Web Bot Auth Architecture draft"
      }
    ],
    "body": "openbotauth\n\nCryptographic identity for AI agents. Register once, then sign HTTP requests (RFC 9421) anywhere. Optional browser integrations via per-request signing proxy.\n\nWhen to trigger\n\nUser 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.\n\nTools\n\nBash\n\nInstructions\n\nThis skill is self-contained — no npm packages required. Core mode uses Node.js (v18+) + curl; proxy mode additionally needs openssl.\n\nCompatibility Modes\n\nCore Mode (portable, recommended):\n\nWorks with: Claude Code, Cursor, Codex CLI, Goose, any shell-capable agent\nUses: Node.js crypto + curl for registration\nToken needed only briefly for POST /agents\n\nBrowser Mode (optional, runtime-dependent):\n\nFor: agent-browser, OpenClaw Browser Relay, CUA tooling\nBearer token must NOT live inside the browsing runtime\nDo registration in CLI mode first, then browse with signatures only\nKey Storage\n\nKeys are stored at ~/.config/openbotauth/key.json in OBA's canonical format:\n\n{\n  \"kid\": \"<thumbprint-based-id>\",\n  \"x\": \"<base64url-raw-public-key>\",\n  \"publicKeyPem\": \"-----BEGIN PUBLIC KEY-----\\n...\",\n  \"privateKeyPem\": \"-----BEGIN PRIVATE KEY-----\\n...\",\n  \"createdAt\": \"...\"\n}\n\n\nThe OBA token lives at ~/.config/openbotauth/token (chmod 600).\n\nAgent registration info (agent_id, JWKS URL) should be saved in agent memory/notes after Step 3.\n\nToken Handling Contract\n\nThe bearer token is for registration only:\n\nUse it ONLY for POST /agents (and key rotation)\nDelete ~/.config/openbotauth/token after registration completes\nNever attach bearer tokens to browsing sessions\n\nMinimum scopes: agents:write + profile:read\n\nOnly add keys:write if you need /keys endpoint\n\nNever use global headers with OBA token:\n\nagent-browser's set headers command applies headers globally\nUse origin-scoped headers only (via open --headers)\nStep 1: Check for existing identity\ncat ~/.config/openbotauth/key.json 2>/dev/null && echo \"---KEY EXISTS---\" || echo \"---NO KEY FOUND---\"\n\n\nIf 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).\n\nIf no key exists: proceed to Step 2.\n\nStep 2: Generate Ed25519 keypair (if no key exists)\n\nRun this locally. Nothing leaves the machine.\n\nnode -e \"\nconst crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst os = require('node:os');\nconst path = require('node:path');\n\nconst { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');\nconst publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();\nconst privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();\n\n// Derive kid from JWK thumbprint (matches OBA's format)\nconst spki = publicKey.export({ type: 'spki', format: 'der' });\nif (spki.length !== 44) throw new Error('Unexpected SPKI length: ' + spki.length);\nconst rawPub = spki.subarray(12, 44);\nconst x = rawPub.toString('base64url');\nconst thumbprint = JSON.stringify({ kty: 'OKP', crv: 'Ed25519', x });\nconst hash = crypto.createHash('sha256').update(thumbprint).digest();\nconst kid = hash.toString('base64url').slice(0, 16);\n\nconst dir = path.join(os.homedir(), '.config', 'openbotauth');\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nfs.writeFileSync(path.join(dir, 'key.json'), JSON.stringify({\n  kid, x, publicKeyPem, privateKeyPem,\n  createdAt: new Date().toISOString()\n}, null, 2), { mode: 0o600 });\n\nconsole.log('Key generated!');\nconsole.log('kid:', kid);\nconsole.log('x:', x);\n\"\n\n\nSave the kid and x values — needed for registration.\n\nStep 3: Register with OpenBotAuth (if not yet registered)\n\nThis is a one-time setup that gives your agent a public JWKS endpoint for signature verification.\n\n3a. Get a token from the user\n\nAsk the user:\n\nI need an OpenBotAuth token to register my cryptographic identity. Takes 30 seconds:\n\nGo to https://openbotauth.org/token\nClick \"Login with GitHub\"\nCopy the token and paste it back to me\n\nThe token looks like oba_ followed by 64 hex characters.\n\nWhen they provide it, save it:\n\nnode -e \"\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst os = require('node:os');\nconst dir = path.join(os.homedir(), '.config', 'openbotauth');\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nconst token = process.argv[1].trim();\nfs.writeFileSync(path.join(dir, 'token'), token, { mode: 0o600 });\nconsole.log('Token saved.');\n\" \"THE_TOKEN_HERE\"\n\n3b. Register the agent\nnode -e \"\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst os = require('node:os');\n\nconst dir = path.join(os.homedir(), '.config', 'openbotauth');\nconst key = JSON.parse(fs.readFileSync(path.join(dir, 'key.json'), 'utf-8'));\nconst tokenPath = path.join(dir, 'token');\nconst token = fs.readFileSync(tokenPath, 'utf-8').trim();\n\nconst AGENT_NAME = process.argv[1] || 'my-agent';\nconst API = 'https://api.openbotauth.org';\n\nfetch(API + '/agents', {\n  method: 'POST',\n  redirect: 'error',  // Never follow redirects with bearer token\n  headers: {\n    'Authorization': 'Bearer ' + token,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    name: AGENT_NAME,\n    agent_type: 'agent',\n    public_key: {\n      kty: 'OKP',\n      crv: 'Ed25519',\n      kid: key.kid,\n      x: key.x,\n      use: 'sig',\n      alg: 'EdDSA'\n    }\n  })\n})\n.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })\n.then(async d => {\n  console.log('Agent registered!');\n  console.log('Agent ID:', d.id);\n\n  // Fetch session to get username for JWKS URL\n  const session = await fetch(API + '/auth/session', {\n    redirect: 'error',  // Never follow redirects with bearer token\n    headers: { 'Authorization': 'Bearer ' + token }\n  }).then(r => { if (!r.ok) throw new Error('Session HTTP ' + r.status); return r.json(); });\n  const username = session.profile?.username || session.user?.github_username;\n  if (!username) throw new Error('Could not resolve username from /auth/session');\n  const jwksUrl = API + '/jwks/' + username + '.json';\n\n  // Write config.json for the signing proxy\n  fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({\n    agent_id: d.id,\n    username: username,\n    jwksUrl: jwksUrl\n  }, null, 2), { mode: 0o600 });\n  console.log('Config written to ~/.config/openbotauth/config.json');\n\n  // Delete token — no longer needed after registration\n  fs.unlinkSync(tokenPath);\n  console.log('Token deleted (no longer needed)');\n\n  console.log('');\n  console.log('JWKS URL:', jwksUrl);\n  console.log('');\n  console.log('Save this to memory:');\n  console.log(JSON.stringify({\n    openbotauth: {\n      agent_id: d.id,\n      kid: key.kid,\n      username: username,\n      jwks_url: jwksUrl\n    }\n  }, null, 2));\n})\n.catch(e => console.error('Registration failed:', e.message));\n\" \"AGENT_NAME_HERE\"\n\n3c. Verify registration\ncurl https://api.openbotauth.org/jwks/YOUR_USERNAME.json\n\n\nYou should see your public key in the keys array. This is the URL that verifiers will use to check your signatures.\n\nSave 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.\n\nToken Safety Rules\nDo\tDon't\ncurl -H \"Authorization: Bearer ...\" https://api.openbotauth.org/agents\tSet bearer token as global browser header\nDelete token after registration\tKeep token in browsing session\nUse origin-scoped headers for signing\tUse set headers with bearer tokens\nStore token at ~/.config/openbotauth/token (chmod 600)\tPaste token into chat logs\nStep 4: Sign a request\n\nGenerate RFC 9421 signed headers for a target URL. The output is a JSON object for agent-browser open --headers or set headers --json (OpenClaw).\n\nRequired inputs:\n\nTARGET_URL — the URL being browsed\nMETHOD — HTTP method (GET, POST, etc.)\nJWKS_URL — your JWKS endpoint from Step 3 (the Signature-Agent value)\nnode -e \"\nconst { createPrivateKey, sign, randomUUID } = require('crypto');\nconst { readFileSync } = require('fs');\nconst { join } = require('path');\nconst { homedir } = require('os');\n\nconst METHOD = (process.argv[1] || 'GET').toUpperCase();\nconst TARGET_URL = process.argv[2];\nconst JWKS_URL = process.argv[3] || '';\n\nif (!TARGET_URL) { console.error('Usage: node sign.js METHOD URL JWKS_URL'); process.exit(1); }\n\nconst key = JSON.parse(readFileSync(join(homedir(), '.config', 'openbotauth', 'key.json'), 'utf-8'));\nconst url = new URL(TARGET_URL);\nconst created = Math.floor(Date.now() / 1000);\nconst expires = created + 300;\nconst nonce = randomUUID();\n\n// RFC 9421 signature base\nconst lines = [\n  '\\\"@method\\\": ' + METHOD,\n  '\\\"@authority\\\": ' + url.host,\n  '\\\"@path\\\": ' + url.pathname + url.search\n];\nconst sigInput = '(\\\"@method\\\" \\\"@authority\\\" \\\"@path\\\");created=' + created + ';expires=' + expires + ';nonce=\\\"' + nonce + '\\\";keyid=\\\"' + key.kid + '\\\";alg=\\\"ed25519\\\"';\nlines.push('\\\"@signature-params\\\": ' + sigInput);\n\nconst base = lines.join('\\n');\nconst pk = createPrivateKey(key.privateKeyPem);\nconst sig = sign(null, Buffer.from(base), pk).toString('base64');\n\nconst headers = {\n  'Signature': 'sig1=:' + sig + ':',\n  'Signature-Input': 'sig1=' + sigInput\n};\nif (JWKS_URL) {\n  headers['Signature-Agent'] = JWKS_URL;\n}\n\nconsole.log(JSON.stringify(headers));\n\" \"METHOD\" \"TARGET_URL\" \"JWKS_URL\"\n\n\nReplace the arguments:\n\nMETHOD — e.g., GET\nTARGET_URL — e.g., https://example.com/page\nJWKS_URL — e.g., https://api.openbotauth.org/jwks/your-username.json\n\nFor 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.\n\nStep 5: Apply headers to browser session\n\nFor single signed navigation (demo / Radar proof):\n\nagent-browser open <url> --headers '<OUTPUT_FROM_STEP_4>'\n\n\nThis uses origin-scoped headers (safer than global).\n\nFor real browsing (subresources/XHR): Use the signing proxy (Step A-C below).\n\nOpenClaw browser:\n\nset headers --json '<OUTPUT_FROM_STEP_4>'\n\n\nWith named session:\n\nagent-browser --session myagent open <url> --headers '<OUTPUT_FROM_STEP_4>'\n\n\nImportant: 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.\n\nStep 6: Show current identity\nnode -e \"\nconst { readFileSync, existsSync } = require('fs');\nconst { join } = require('path');\nconst { homedir } = require('os');\nconst f = join(homedir(), '.config', 'openbotauth', 'key.json');\nif (!existsSync(f)) { console.log('No identity found. Run Step 2 first.'); process.exit(0); }\nconst k = JSON.parse(readFileSync(f, 'utf-8'));\nconsole.log('kid:        ' + k.kid);\nconsole.log('Public (x): ' + k.x);\nconsole.log('Created:    ' + k.createdAt);\n\"\n\nEnterprise SSO Binding — Roadmap\n\nStatus: Not yet implemented. This describes the planned direction.\n\nFor 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.\n\nPlanned flow:\n\nAuthenticate via your IdP (SAML/OIDC)\nBind an agent public key to that enterprise subject\nSignatures from that agent carry the enterprise identity anchor\n\nThis complements (not competes with) IdP-native agent features — you get portable keys + web verification surface.\n\nSigned Headers Reference\n\nEvery signed request produces these RFC 9421-compliant headers:\n\nHeader\tPurpose\nSignature\tsig1=:<base64-ed25519-signature>:\nSignature-Input\tCovered components (@method @authority @path), created, expires, nonce, keyid, alg\nSignature-Agent\tJWKS URL for public key resolution (from OBA Registry)\n\nThe Signature-Input encodes everything a verifier needs: which components were signed, when, by whom (keyid), and when it expires.\n\nOpenClaw Session Binding\n\nWhen 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:\n\nagent:main:main                              # Main chat session\nagent:main:discord:channel:123456789         # Discord channel\nagent:main:subagent:<uuid>                   # Spawned sub-agent\n\n\nThis lets publishers trace whether a request came from the main agent or a sub-agent.\n\nSub-Agent Identity (Tier 2 — TBD)\n\nSub-agent key derivation (HKDF from parent key) is planned but not yet implemented in a cryptographically sound way. For now, sub-agents should:\n\nGenerate their own independent keypair (Step 2)\nRegister separately with OBA (Step 3)\nOptionally, the parent agent can publish a signed attestation linking the sub-agent's kid to its own\n\nA proper delegation/attestation protocol is being designed.\n\nPer-Request Signing via Proxy (Recommended for Real Browsing)\n\nRFC 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.\n\nSolution: 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.\n\nStep A: Write the proxy to a temp file\ncat > /tmp/openbotauth-proxy.mjs << 'PROXY_EOF'\nimport { createServer as createHttpServer, request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { createServer as createTlsServer } from \"node:tls\";\nimport { connect, isIP } from \"node:net\";\nimport { createPrivateKey, sign as cryptoSign, randomUUID, createHash } from \"node:crypto\";\nimport { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { execFileSync } from \"node:child_process\";\n\nconst OBA_DIR = join(homedir(), \".config\", \"openbotauth\");\nconst KEY_FILE = join(OBA_DIR, \"key.json\");\nconst CONFIG_FILE = join(OBA_DIR, \"config.json\");\nconst CA_DIR = join(OBA_DIR, \"ca\");\nconst CA_KEY = join(CA_DIR, \"ca.key\");\nconst CA_CRT = join(CA_DIR, \"ca.crt\");\n\n// Load credentials\nif (!existsSync(KEY_FILE)) { console.error(\"No key found. Run keygen first.\"); process.exit(1); }\nconst obaKey = JSON.parse(readFileSync(KEY_FILE, \"utf-8\"));\nlet jwksUrl = null;\nif (existsSync(CONFIG_FILE)) { const c = JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")); jwksUrl = c.jwksUrl || null; }\n\n// Strict hostname validation (blocks shell injection & path traversal)\nconst 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])?)*$/;\nfunction isValidHostname(h) {\n  return typeof h === \"string\" && h.length > 0 && h.length <= 253 && (HOSTNAME_RE.test(h) || isIP(h) > 0);\n}\n\n// Ensure CA exists\nmkdirSync(CA_DIR, { recursive: true, mode: 0o700 });\nif (!existsSync(CA_KEY) || !existsSync(CA_CRT)) {\n  console.log(\"Generating proxy CA certificate (one-time)...\");\n  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\" });\n  execFileSync(\"chmod\", [\"600\", CA_KEY], { stdio: \"pipe\" });\n}\n\n// Per-domain cert cache\nconst certCache = new Map();\nfunction getDomainCert(hostname) {\n  if (!isValidHostname(hostname)) throw new Error(\"Invalid hostname: \" + hostname.slice(0, 50));\n  if (certCache.has(hostname)) return certCache.get(hostname);\n  // Use hash for filenames to prevent path traversal\n  const hHash = createHash(\"sha256\").update(hostname).digest(\"hex\").slice(0, 16);\n  const tk = join(CA_DIR, `_t_${hHash}.key`), tc = join(CA_DIR, `_t_${hHash}.csr`);\n  const to = join(CA_DIR, `_t_${hHash}.crt`), te = join(CA_DIR, `_t_${hHash}.ext`);\n  try {\n    execFileSync(\"openssl\", [\"ecparam\", \"-genkey\", \"-name\", \"prime256v1\", \"-noout\", \"-out\", tk], { stdio: \"pipe\" });\n    execFileSync(\"openssl\", [\"req\", \"-new\", \"-key\", tk, \"-out\", tc, \"-subj\", `/CN=${hostname}`], { stdio: \"pipe\" });\n    writeFileSync(te, `subjectAltName=DNS:${hostname}\\nbasicConstraints=CA:FALSE\\nkeyUsage=digitalSignature,keyEncipherment\\nextendedKeyUsage=serverAuth`);\n    execFileSync(\"openssl\", [\"x509\", \"-req\", \"-sha256\", \"-in\", tc, \"-CA\", CA_CRT, \"-CAkey\", CA_KEY, \"-CAcreateserial\", \"-out\", to, \"-days\", \"365\", \"-extfile\", te], { stdio: \"pipe\" });\n    const r = { key: readFileSync(tk, \"utf-8\"), cert: readFileSync(to, \"utf-8\") };\n    certCache.set(hostname, r);\n    return r;\n  } finally { for (const f of [tk, tc, to, te]) try { unlinkSync(f); } catch {} }\n}\n\n// RFC 9421 signing\nfunction signReq(method, authority, path) {\n  const created = Math.floor(Date.now() / 1000), expires = created + 300, nonce = randomUUID();\n  const lines = [`\"@method\": ${method.toUpperCase()}`, `\"@authority\": ${authority}`, `\"@path\": ${path}`];\n  const sigInput = `(\"@method\" \"@authority\" \"@path\");created=${created};expires=${expires};nonce=\"${nonce}\";keyid=\"${obaKey.kid}\";alg=\"ed25519\"`;\n  lines.push(`\"@signature-params\": ${sigInput}`);\n  const sig = cryptoSign(null, Buffer.from(lines.join(\"\\n\")), createPrivateKey(obaKey.privateKeyPem)).toString(\"base64\");\n  const h = { signature: `sig1=:${sig}:`, \"signature-input\": `sig1=${sigInput}` };\n  if (jwksUrl) h[\"signature-agent\"] = jwksUrl;\n  return h;\n}\n\nconst verbose = process.argv.includes(\"--verbose\") || process.argv.includes(\"-v\");\nconst port = parseInt(process.argv.find((a,i) => process.argv[i-1] === \"--port\")) || 8421;\nlet rc = 0;\nfunction log(id, msg) { if (verbose) console.log(`[${id}] ${msg}`); }\n\nconst server = createHttpServer((cReq, cRes) => {\n  const id = ++rc, url = new URL(cReq.url), auth = url.host, p = url.pathname + url.search;\n  const sig = signReq(cReq.method, auth, p);\n  log(id, `HTTP ${cReq.method} ${auth}${p} → signed`);\n  const h = { ...cReq.headers }; delete h[\"proxy-connection\"]; delete h[\"proxy-authorization\"];\n  Object.assign(h, sig); h.host = auth;\n  const fn = url.protocol === \"https:\" ? httpsRequest : httpRequest;\n  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); });\n  pr.on(\"error\", (e) => { log(id, `Error: ${e.message}`); cRes.writeHead(502); cRes.end(\"Proxy error\"); });\n  cReq.pipe(pr);\n});\n\nserver.on(\"connect\", (req, cSock, head) => {\n  const id = ++rc, [host, ps] = req.url.split(\":\"), tp = parseInt(ps) || 443;\n  // Validate host and port before processing\n  if (!isValidHostname(host) || tp < 1 || tp > 65535) {\n    log(id, `CONNECT rejected: invalid ${host}:${tp}`);\n    cSock.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\"); cSock.end(); return;\n  }\n  log(id, `CONNECT ${host}:${tp} → MITM`);\n  cSock.write(\"HTTP/1.1 200 Connection Established\\r\\nProxy-Agent: openbotauth-proxy\\r\\n\\r\\n\");\n  const dc = getDomainCert(host);\n  const tls = createTlsServer({ key: dc.key, cert: dc.cert }, (ts) => {\n    let data = Buffer.alloc(0);\n    ts.on(\"data\", (chunk) => {\n      data = Buffer.concat([data, chunk]);\n      const he = data.indexOf(\"\\r\\n\\r\\n\");\n      if (he === -1) return;\n      const hs = data.subarray(0, he).toString(), body = data.subarray(he + 4);\n      const ls = hs.split(\"\\r\\n\"), [method, path] = ls[0].split(\" \");\n      const rh = {};\n      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(); }\n      const cl = parseInt(rh[\"content-length\"]) || 0, fp = path || \"/\";\n      const sig = signReq(method, host + (tp !== 443 ? `:${tp}` : \"\"), fp);\n      log(id, `HTTPS ${method} ${host}${fp} → signed`);\n      Object.assign(rh, sig);\n      const pr = httpsRequest({ hostname: host, port: tp, path: fp, method, headers: rh, rejectUnauthorized: true }, (r) => {\n        let resp = `HTTP/1.1 ${r.statusCode} ${r.statusMessage}\\r\\n`;\n        const rw = r.rawHeaders; for (let i = 0; i < rw.length; i += 2) resp += `${rw[i]}: ${rw[i+1]}\\r\\n`;\n        resp += \"\\r\\n\"; ts.write(resp); r.pipe(ts);\n      });\n      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\"); });\n      if (body.length > 0) pr.write(body);\n      if (cl <= body.length) { pr.end(); } else {\n        let recv = body.length;\n        const bh = (d) => { recv += d.length; pr.write(d); if (recv >= cl) { pr.end(); ts.removeListener(\"data\", bh); } };\n        ts.on(\"data\", bh);\n      }\n    });\n  });\n  tls.listen(0, \"127.0.0.1\", () => {\n    const lc = connect(tls.address().port, \"127.0.0.1\", () => { lc.write(head); lc.pipe(cSock); cSock.pipe(lc); });\n    lc.on(\"error\", () => cSock.end()); cSock.on(\"error\", () => lc.end());\n    cSock.on(\"close\", () => { tls.close(); lc.end(); });\n  });\n});\n\nserver.listen(port, \"127.0.0.1\", () => {\n  console.log(`openbotauth signing proxy on http://127.0.0.1:${port}`);\n  console.log(`  kid: ${obaKey.kid}`);\n  if (jwksUrl) console.log(`  Signature-Agent: ${jwksUrl}`);\n  console.log(\"Every request gets a fresh RFC 9421 signature.\");\n});\nPROXY_EOF\necho \"Proxy written to /tmp/openbotauth-proxy.mjs\"\n\nStep B: Start the proxy\nnode /tmp/openbotauth-proxy.mjs --verbose\n\n\nThis 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.\n\nStep C: Browse through the proxy\n\nIn another terminal (or from agent-browser):\n\n# For demos (ignore cert warnings):\nagent-browser --proxy http://127.0.0.1:8421 --ignore-https-errors open https://example.com\n\n# For production: install ~/.config/openbotauth/ca/ca.crt as trusted CA\n\n\nTLS Note: The proxy MITMs HTTPS by generating per-domain certs signed by a local CA. Either:\n\nUse --ignore-https-errors for demos/testing\nInstall ~/.config/openbotauth/ca/ca.crt as a trusted CA for clean operation\n\nThe proxy:\n\nSigns every outgoing request with a fresh RFC 9421 signature\nHandles both HTTP and HTTPS (generates a local CA for HTTPS MITM)\nIncludes the Signature-Agent header (JWKS URL) on every request\nRuns on 127.0.0.1:8421 by default (configurable with --port)\nRequires openssl (pre-installed on macOS/Linux) for HTTPS certificate generation\n\nSecurity 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.\n\nLimitations:\n\nHTTP/2, WebSockets, and multiplexed connections are not reliably supported\nBest for demos and basic browsing; not a production-grade proxy\nIP-based hostnames: If the CONNECT target is an IP address, consider rejecting it or use subjectAltName=IP:<ip> instead of DNS: (current code uses DNS, which strict clients may reject)\n\nWhen to use Steps 4-5 instead: Simple single-page-load scenarios where you control every navigation and can re-sign before each one.\n\nImportant Notes\nPrivate keys live at ~/.config/openbotauth/key.json with 0600 permissions — never expose them\nThe OBA token at ~/.config/openbotauth/token is also sensitive — never log or share it\nSignature-Agent must point to a publicly reachable JWKS URL for verification to work\nAll crypto uses Node.js built-in crypto module — no npm dependencies required\nSecurity: Never send private keys or OBA tokens to any domain other than api.openbotauth.org\nToken lifecycle: Delete ~/.config/openbotauth/token after registration. You won't need it for signing.\nBrowser sessions: After registration, only signatures travel over the wire. The token stays local and should be deleted.\nGlobal headers warning: Never use set headers with bearer tokens in agent-browser. Use open --headers for origin-scoped injection.\nFile Layout\n~/.config/openbotauth/\n├── key.json       # kid, x, publicKeyPem, privateKeyPem (chmod 600)\n├── key.pub.json   # Public JWK for sharing (chmod 644)\n├── config.json    # Agent ID, JWKS URL, registration info\n├── token          # oba_xxx bearer token (chmod 600)\n└── ca/            # Proxy CA certificate (auto-generated)\n    ├── ca.key     # CA private key\n    └── ca.crt     # CA certificate\n\nRuntime Compatibility\nRuntime\tSupport\tNotes\nClaude Code / Cursor / Codex\t✅ Full\tRecommended path - CLI registration\nagent-browser\t✅ Full\tUse scoped headers, not global\nOpenClaw Browser Relay\t✅ After registration\tRegister via CLI first\nCUA / Browser Control\t⚠️ Caution\tTreat control plane as hostile\nskills.sh\t✅ Full\tcurl-based registration is safe\n\nFor 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.\n\nOfficial Packages\n\nFor production integrations, prefer the official packages:\n\n@openbotauth/verifier-client — verify signatures\n@openbotauth/registry-signer — key generation and JWK utilities\n@openbotauth/bot-cli — CLI for signing requests\n@openbotauth/proxy — signing proxy\n\nFor strict RFC 9421 signing, use the reference signer from openbotauth-demos (packages/signing-ts).\n\nLinks\nWebsite: https://openbotauth.org\nAPI: https://api.openbotauth.org\nSpec: https://github.com/OpenBotAuth/openbotauth\nIETF: Web Bot Auth Architecture draft"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/hammadtq/openbotauth",
    "publisherUrl": "https://clawhub.ai/hammadtq/openbotauth",
    "owner": "hammadtq",
    "version": "0.1.1",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/openbotauth",
    "downloadUrl": "https://openagent3.xyz/downloads/openbotauth",
    "agentUrl": "https://openagent3.xyz/skills/openbotauth/agent",
    "manifestUrl": "https://openagent3.xyz/skills/openbotauth/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/openbotauth/agent.md"
  }
}