Skip to content

Commit 5c613cc

Browse files
committed
feat: argon2id memory-hard PoW, key rotation with chain continuity, pre-rotation commitments
Add Argon2id proof-of-work with SHA-256 pre-filter (12-bit) for GPU-farm resistance. Three server-assigned presets (light/standard/ hardened) with bounded verification concurrency via semaphore. Implement rotation_chain_id (SHA-256 of first public key) linking successive DIDs across key rotations. Trust scores, rate limits, and fingerprints follow the chain, not individual DIDs. First-write- wins prevents fork attacks on the rotation endpoint. Add KERI-inspired pre-rotation commitment: agents commit SHA-256 of their next public key before rotation. Mandatory from Tier 1, with 72-hour update lockout to prevent attacker overwrite. New endpoints: POST /rotate-key, POST /pre-commit-key. New package: airlock/rotation/ (chain registry + precommit). 68 new tests (601 → 669 total).
1 parent 28414ad commit 5c613cc

18 files changed

Lines changed: 2440 additions & 44 deletions

airlock/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ class AirlockConfig(BaseSettings):
130130
pow_ttl_seconds: int = Field(default=120, ge=30, le=600)
131131
pow_difficulty_new_did: int = Field(default=22, ge=1, le=32)
132132

133+
# Argon2id memory-hard PoW (replaces SHA-256 when enabled)
134+
pow_algorithm: str = "sha256" # "sha256" or "argon2id"
135+
pow_argon2id_preset: str = "standard" # "light", "standard", "hardened"
136+
pow_argon2id_pre_filter_bits: int = Field(default=12, ge=4, le=24)
137+
pow_argon2id_max_concurrent: int = Field(default=8, ge=1, le=64)
138+
pow_argon2id_verify_timeout_seconds: float = Field(default=10.0, ge=1.0, le=60.0)
139+
133140
# -----------------------------------------------------------------------
134141
# Privacy mode
135142
# -----------------------------------------------------------------------
@@ -163,6 +170,16 @@ class AirlockConfig(BaseSettings):
163170
# Falls back to gateway_seed_hex if empty.
164171
crl_signing_key_hex: str = ""
165172

173+
# -----------------------------------------------------------------------
174+
# Key Rotation
175+
# -----------------------------------------------------------------------
176+
key_rotation_enabled: bool = False
177+
key_rotation_grace_seconds: int = Field(default=60, ge=0, le=300)
178+
key_rotation_max_per_24h: int = Field(default=3, ge=1, le=10)
179+
key_rotation_trust_penalty: float = Field(default=0.02, ge=0.0, le=0.1)
180+
pre_rotation_required_tier: int = Field(default=1, ge=0, le=3) # TrustTier value
181+
pre_rotation_update_lockout_hours: int = Field(default=72, ge=1, le=720)
182+
166183
# Event bus drain timeout during shutdown (seconds).
167184
event_bus_drain_timeout_seconds: float = Field(default=30.0, ge=1.0, le=600.0)
168185

airlock/gateway/app.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from airlock.gateway.startup_validate import AirlockStartupError, validate_startup_config
3030
from airlock.registry.agent_store import AgentRegistryStore
3131
from airlock.reputation.store import ReputationStore
32+
from airlock.rotation.chain import RotationChainRegistry
33+
from airlock.rotation.precommit import PreRotationCommitment
3234

3335
logger = logging.getLogger(__name__)
3436

@@ -49,6 +51,17 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
4951
except AirlockStartupError as exc:
5052
logger.error("Startup aborted: %s", exc)
5153
raise
54+
55+
# Argon2id fail-fast: production + argon2id enabled requires argon2-cffi
56+
if cfg.is_production and cfg.pow_algorithm == "argon2id":
57+
from airlock.pow import argon2_available
58+
59+
if not argon2_available():
60+
raise RuntimeError(
61+
"pow_algorithm is 'argon2id' but argon2-cffi is not installed. "
62+
"Install with: pip install argon2-cffi"
63+
)
64+
5265
configure_airlock_logging(log_json=cfg.log_json, log_level=cfg.log_level)
5366
app.state.started_at_monotonic = time.monotonic()
5467
app.state.shutting_down = False
@@ -189,6 +202,19 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
189202
app.state.pow_challenges: dict[str, Any] = {}
190203
app.state.redis_client = redis_client
191204

205+
# Key rotation (behind feature flag)
206+
if cfg.key_rotation_enabled:
207+
app.state.chain_registry = RotationChainRegistry()
208+
app.state.precommit_store: dict[str, PreRotationCommitment] = {}
209+
else:
210+
app.state.chain_registry = None
211+
app.state.precommit_store = {}
212+
213+
# Argon2id bounded verification worker pool
214+
import asyncio as _asyncio
215+
216+
app.state.argon2id_semaphore = _asyncio.Semaphore(cfg.pow_argon2id_max_concurrent)
217+
192218
registry_url = (cfg.default_registry_url or "").strip().rstrip("/")
193219
if registry_url:
194220
app.state.registry_http_client = httpx.AsyncClient(

0 commit comments

Comments
 (0)