diff --git a/backend/tests/test_openclaw_skill_env.py b/backend/tests/test_openclaw_skill_env.py new file mode 100644 index 0000000..9318c44 --- /dev/null +++ b/backend/tests/test_openclaw_skill_env.py @@ -0,0 +1,38 @@ +"""Regression coverage for OpenClaw skill HMAC environment names.""" + +import importlib.util +from pathlib import Path + + +def _load_sb_query(monkeypatch): + module_path = Path(__file__).resolve().parents[2] / "openclaw-skills" / "shadowbroker" / "sb_query.py" + spec = importlib.util.spec_from_file_location("shadowbroker_skill_sb_query_test", module_path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_openclaw_skill_prefers_hmac_secret_env(monkeypatch): + monkeypatch.setenv("SHADOWBROKER_HMAC_SECRET", "preferred-hmac-secret") + monkeypatch.setenv("SHADOWBROKER_KEY", "legacy-hmac-secret") + + module = _load_sb_query(monkeypatch) + + assert module.ShadowBrokerClient()._hmac_secret == "preferred-hmac-secret" + + +def test_openclaw_skill_accepts_legacy_key_as_hmac_secret_alias(monkeypatch): + monkeypatch.delenv("SHADOWBROKER_HMAC_SECRET", raising=False) + monkeypatch.setenv("SHADOWBROKER_KEY", "legacy-hmac-secret") + + module = _load_sb_query(monkeypatch) + client = module.ShadowBrokerClient() + headers = client._sign_headers("GET", "/api/ai/tools") + + assert client._hmac_secret == "legacy-hmac-secret" + assert "X-SB-Timestamp" in headers + assert "X-SB-Nonce" in headers + assert "X-SB-Signature" in headers + assert "Authorization" not in headers + assert "X-Admin-Key" not in headers diff --git a/frontend/src/components/AIIntelPanel.tsx b/frontend/src/components/AIIntelPanel.tsx index 14541ed..b1dc3df 100644 --- a/frontend/src/components/AIIntelPanel.tsx +++ b/frontend/src/components/AIIntelPanel.tsx @@ -632,10 +632,18 @@ function ConnectModalBody({ apiEndpoint, handleCopy, copied }: ConnectModalBodyP const buildSnippet = (key: string) => { const lines: string[] = []; if (resolvedUrl) lines.push(`SHADOWBROKER_URL=${resolvedUrl}`); - lines.push(`SHADOWBROKER_KEY=${key}`); + lines.push(`SHADOWBROKER_HMAC_SECRET=${key}`); lines.push(`SHADOWBROKER_ACCESS=${accessTier}`); if (connectionMode === 'remote' && nodeId) lines.push(`SHADOWBROKER_NODE_ID=${nodeId}`); lines.push(''); + lines.push('# AUTH DIRECTIVE:'); + lines.push('# SHADOWBROKER_HMAC_SECRET is a shared HMAC signing secret, not a raw API key.'); + lines.push('# Never send it as X-Admin-Key, Authorization: Bearer, a query parameter, or a plain request header.'); + lines.push('# Every direct ShadowBroker API request must be HMAC-SHA256 signed with:'); + lines.push('# X-SB-Timestamp, X-SB-Nonce, X-SB-Signature'); + lines.push('# Signature input: METHOD|path|timestamp|nonce|sha256(body)'); + lines.push('# Use the ShadowBrokerClient/openclaw skill helper so requests are signed automatically.'); + lines.push(''); lines.push('# OPERATING DIRECTIVE:'); lines.push('# You are a remote OpenClaw agent connected to ShadowBroker.'); lines.push('# ShadowBroker is a live intelligence and telemetry platform, not a narrow single-purpose API.'); diff --git a/frontend/src/components/OnboardingModal.tsx b/frontend/src/components/OnboardingModal.tsx index 7c3d49a..fe5633f 100644 --- a/frontend/src/components/OnboardingModal.tsx +++ b/frontend/src/components/OnboardingModal.tsx @@ -129,13 +129,16 @@ const OnboardingModal = React.memo(function OnboardingModal({ const agentSnippet = [ `SHADOWBROKER_URL=${agentEndpoint}`, - agentSecret ? `SHADOWBROKER_KEY=${agentSecret}` : 'SHADOWBROKER_KEY=', + agentSecret ? `SHADOWBROKER_HMAC_SECRET=${agentSecret}` : 'SHADOWBROKER_HMAC_SECRET=', `SHADOWBROKER_ACCESS=${agentTier}`, '', '# FIRST: load available tools', `GET ${agentEndpoint}/api/ai/tools`, '', - '# Auth: HMAC-SHA256 signed requests.', + '# Auth: SHADOWBROKER_HMAC_SECRET is not a raw API key.', + '# Sign every direct request with X-SB-Timestamp, X-SB-Nonce, and X-SB-Signature.', + '# Signature input: METHOD|path|timestamp|nonce|sha256(body).', + '# Do not send the secret as X-Admin-Key, Authorization, or a query parameter.', '# Restricted = read-only telemetry. Full = can write when asked.', ].join('\n'); const remoteAgentNeedsTor = agentMode === 'remote' && !torAddress; diff --git a/openclaw-skills/shadowbroker/SKILL.md b/openclaw-skills/shadowbroker/SKILL.md index 531735c..cadd114 100644 --- a/openclaw-skills/shadowbroker/SKILL.md +++ b/openclaw-skills/shadowbroker/SKILL.md @@ -37,7 +37,18 @@ SHADOWBROKER_HMAC_SECRET=your-hmac-secret-here ``` The HMAC secret is found in ShadowBroker's **Connect OpenClaw** modal (AI Intel panel). -All requests are automatically signed with HMAC-SHA256 (timestamp + nonce + body digest) for replay protection and request-body integrity binding. +`SHADOWBROKER_HMAC_SECRET` is a shared signing secret, not a raw API key. Do not +send it as `X-Admin-Key`, `Authorization: Bearer`, a query parameter, or any +plain request header. The `ShadowBrokerClient` signs every direct request with +`X-SB-Timestamp`, `X-SB-Nonce`, and `X-SB-Signature` using: + +```text +HMAC-SHA256(secret, METHOD|path|timestamp|nonce|sha256(body)) +``` + +For compatibility with older snippets, `SHADOWBROKER_KEY` is also accepted by +the client as the same HMAC signing secret. Prefer `SHADOWBROKER_HMAC_SECRET` +for new setups. ### SSE Stream (Preferred — Low-Latency Push) diff --git a/openclaw-skills/shadowbroker/sb_query.py b/openclaw-skills/shadowbroker/sb_query.py index b85a080..9e37704 100644 --- a/openclaw-skills/shadowbroker/sb_query.py +++ b/openclaw-skills/shadowbroker/sb_query.py @@ -5,6 +5,9 @@ the ShadowBroker OSINT platform. For local access (same machine), no authentication is needed. For remote access, set SHADOWBROKER_HMAC_SECRET to enable HMAC-signed requests. +Older ShadowBroker UI snippets used SHADOWBROKER_KEY; this client still accepts +that value as an HMAC signing secret for compatibility. Never send either value +as a raw bearer token, X-Admin-Key, query parameter, or unsigned header. Usage (inside an OpenClaw skill): from sb_query import ShadowBrokerClient @@ -43,11 +46,17 @@ class ShadowBrokerClient: Supports both local (no auth) and remote (HMAC-signed) connections. Set SHADOWBROKER_HMAC_SECRET env var to enable remote authentication. + SHADOWBROKER_KEY is accepted only as a backwards-compatible HMAC-secret + alias for older copy snippets. """ def __init__(self, base_url: str = SB_BASE, hmac_secret: str = ""): self.base = base_url.rstrip("/") - self._hmac_secret = hmac_secret or os.environ.get("SHADOWBROKER_HMAC_SECRET", "") + self._hmac_secret = ( + hmac_secret + or os.environ.get("SHADOWBROKER_HMAC_SECRET", "") + or os.environ.get("SHADOWBROKER_KEY", "") + ) self._client = None # Version tracking for incremental updates self._last_data_version: int | None = None