<?php if(!defined('PRIVATE_DIR'))exit; class RequestSigner{ private static ?string $key=null; private static function loadKey():string{ if(self::$key!==null)return self::$key; require_once __DIR__ . '/credentials.php'; $key = CredentialStore::get('bridge_hmac'); if(!$key)throw new RuntimeException('HMAC signing key not found'); self::$key=trim($key); return self::$key; } public static function sign(string $body):array{ $key=self::loadKey(); $timestamp=(string)time(); $nonce=bin2hex(random_bytes(16)); $message=$timestamp.'.'.$nonce.'.'.$body; $signature=hash_hmac('sha256',$message,$key); return['signature'=>$signature,'timestamp'=>$timestamp,'nonce'=>$nonce]; } public static function verify(string $body,string $signature,string $timestamp,string $nonce):bool{ $key=self::loadKey(); if(abs(time()-(int)$timestamp)>300)return false; $message=$timestamp.'.'.$nonce.'.'.$body; $expected=hash_hmac('sha256',$message,$key); return hash_equals($expected,$signature); } public static function buildHeaders(string $body):array{ $signed=self::sign($body); return['Content-Type: application/json','X-Bridge-Sig: '.$signed['signature'],'X-Bridge-Ts: '.$signed['timestamp'],'X-Bridge-Nonce: '.$signed['nonce'],'X-Bridge-Node: origin-01']; } public static function enforceIncoming():void{ $signature=$_SERVER['HTTP_X_BRIDGE_SIG']??''; $timestamp=$_SERVER['HTTP_X_BRIDGE_TS']??''; $nonce=$_SERVER['HTTP_X_BRIDGE_NONCE']??''; $body=file_get_contents('php://input'); if(!self::verify($body,$signature,$timestamp,$nonce)){ if(class_exists('Events')&&method_exists('Events','record')){Events::record(Events::CAT_SECURITY,'hmac.rejected','bridge',null,['ip'=>$_SERVER['REMOTE_ADDR']??'']);} http_response_code(401); exit(json_encode(['ok'=>false,'error'=>'Invalid request signature.'])); } } } 