openclaw_oauth_sync/scripts/webhook-security/gitea-hmac-verify.js
sol 2db7d7d90a feat: merge Gitea webhook security into setup wizard (issue #2)
Integrates the 5-layer Gitea webhook security system from
sol/clawgravity-hook-security (v2.0) into the setup wizard.

## What's added

### New files (from clawgravity-hook-security v2.0)
- scripts/webhook-security/gitea-hmac-verify.js    -- njs HMAC-SHA256 module
- scripts/webhook-security/gitea-approve-repo       -- allowlist helper
- scripts/webhook-security/rotate-webhook-secret.sh -- monthly secret rotation (templated)
- scripts/webhook-security/webhook-audit-alert.sh   -- daily audit summaries (templated)
- scripts/webhook-security/ntfy-blocked-pickup.sh   -- blocked webhook alerts (templated)
- templates/webhook-security/nginx-site.conf.example
- templates/webhook-security/nginx.conf.example
- templates/webhook-security/gitea-repo-allowlist.json.example
- docs/WEBHOOK-SECURITY.md   -- full documentation
- docs/SECURITY-AUDIT.md     -- 35-case test matrix
- tests/test-webhook-security.sh  -- 48 offline tests

### Modified files
- setup.sh: Step 11 (webhook security wizard with 6 sub-sections)
- scripts/uninstall.sh: webhook security cleanup section
- README.md: Webhook Security section after Quick Start
- Makefile: test target now runs test-webhook-security.sh
- .secret-scan-allowlist: allowlist docs/SECURITY-AUDIT.md (test fixture)

## Security layers
1. IP allowlisting (nginx)
2. Rate limiting 10 req/s burst 20 (nginx)
3. Payload size 1MB (nginx)
4. HMAC-SHA256 signature verification (njs)
5. Per-repository allowlist (njs)

## make check
- prettier: PASS
- secret-scan: PASS
- tests: 48/48 PASS

Closes #2
2026-03-01 08:43:02 +00:00

65 lines
2.4 KiB
JavaScript

import fs from 'fs';
import crypto from 'crypto';
var ALLOWLIST_PATH = '/etc/nginx/gitea-repo-allowlist.json';
var SECRET_PATH = '/etc/nginx/gitea-webhook-secret';
function getSecret() {
try { return fs.readFileSync(SECRET_PATH).toString().trim(); }
catch (e) { return null; }
}
function loadAllowlist() {
try { return JSON.parse(fs.readFileSync(ALLOWLIST_PATH).toString()); }
catch (e) { return null; }
}
function isRepoAllowed(repoFullName, allowlist) {
if (!allowlist || !repoFullName) return false;
if ((allowlist.repos || []).indexOf(repoFullName) !== -1) return true;
var owners = allowlist.trusted_owners || [];
for (var i = 0; i < owners.length; i++) {
if (repoFullName.startsWith(owners[i] + '/')) return true;
}
return false;
}
function constantTimeEqual(a, b) {
if (a.length !== b.length) return false;
var result = 0;
for (var i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
return result === 0;
}
async function verifyAndProxy(r) {
var secret = getSecret();
if (!secret) { r.error('gitea-hmac: failed to read secret'); r.return(500, 'Config error'); return; }
var giteaSig = r.headersIn['X-Gitea-Signature'];
if (!giteaSig) { r.error('gitea-hmac: missing X-Gitea-Signature'); r.return(403, 'Missing signature'); return; }
var body = r.requestText || '';
var hmac = crypto.createHmac('sha256', secret); hmac.update(body);
if (!constantTimeEqual(hmac.digest('hex'), giteaSig)) {
r.error('gitea-hmac: signature mismatch'); r.return(403, 'Invalid signature'); return;
}
var allowlist = loadAllowlist();
if (!allowlist) { r.error('gitea-hmac: cannot read allowlist'); r.return(403, 'Authorization unavailable'); return; }
var repoFullName = '';
try { repoFullName = (JSON.parse(body).repository || {}).full_name || ''; }
catch (e) { r.error('gitea-hmac: invalid JSON'); r.return(403, 'Invalid body'); return; }
if (!repoFullName) { r.error('gitea-hmac: no repo name'); r.return(403, 'Missing repo'); return; }
if (!isRepoAllowed(repoFullName, allowlist)) {
r.error('gitea-hmac: BLOCKED ' + repoFullName); r.return(403, 'Not authorized'); return;
}
var res = await r.subrequest('/hooks/gitea-upstream', { method: r.method, body: body });
var ct = res.headersOut['Content-Type']; if (ct) r.headersOut['Content-Type'] = ct;
r.return(res.status, res.responseBody);
}
export default { verifyAndProxy };