Skip to content

v0.13.0 — Security fail-loud + strict proof-enforced mint mode

Latest

Choose a tag to compare

@craigamcw craigamcw released this 28 May 12:51

Security minor release. Two P1 fixes addressing structural-correctness gaps surfaced by an internal security review. No public-API breaks — every new behaviour is opt-in. Existing deployments see no surprise.

FIX 1 — Fail-loud cryptographic configuration

Six places in the codebase used silent try / except: pass patterns around signer initialisation. The behaviour is now:

  • Explicit config + broken = REFUSE. Operator misconfigured; raise ConfigurationError, exit non-zero.
  • No config + safe default exists = WARN loudly. Continue in explicitly-marked unsigned mode.
  • Never silent.

Affected paths:

  • raucle_detect/errors.py — new module: ConfigurationError, PolicyUnproven.
  • raucle_detect.server._init_compliance() — extracted from import-time init; RAUCLE_DETECT_VERDICT_KEY_PEM or RAUCLE_DETECT_AUDIT_PRIVATE_KEY_PEM set but unparseable now refuses to start.
  • VerdictSigner.__init__ / Ed25519Signer.__init__ — a signer whose public key cannot be extracted raises ConfigurationError at construction (was: silently produced unverifiable receipts).
  • audit.sink_from_env() — invalid RAUCLE_DETECT_AUDIT_PRIVATE_KEY_PEM raises instead of silently producing an unsigned sink.
  • HashChainSink — every newly-created chain emits a self-describing chain_meta header on line 1, signed when a signer is supplied. AuditVerifier surfaces signed_mode ∈ {signed, unsigned, unknown} and rejects splice attempts (signed checkpoint in unsigned chain, mismatched key_id between header and checkpoint).

FIX 2 — Strict proof-enforced mint mode

Previously, cap mint --policy-proof-hash was advisory: any string could be passed. Tokens cited proofs they had no structural relationship to. The new strict mode makes the relationship enforceable.

from raucle_detect.capability import CapabilityIssuer
from raucle_detect.prove import JSONSchemaProver

# 1. Prove the policy
result = JSONSchemaProver().prove(schema=tool_schema, policy=tool_policy)
assert result.status == "PROVEN"

# 2. Strict-mint binds proof + grammar + policy hashes into the token
issuer = CapabilityIssuer.generate(issuer="acme.bank.kyc", require_proof=True)
token = issuer.mint(
    agent_id="agent:kyc-prod",
    tool="lookup_customer",
    proof_result=result,  # required
)

# 3. The token now carries:
#   token.policy_proof_hash == result.hash
#   token.grammar_hash      == result.grammar_hash
#   token.policy_hash       == result.policy_hash
# All three covered by token_id and the Ed25519 signature.

Defence-in-depth at the gate too:

from raucle_detect.capability import CapabilityGate

gate = CapabilityGate(
    trusted_issuers={...},
    proof_enforcement_mode=\"strict\",
    trusted_proofs={result.hash: result},  # pre-loaded cache
)
# Every call now denied unless:
#   * token has policy_proof_hash, AND
#   * proof in trusted_proofs cache, AND
#   * proof.status == 'PROVEN', AND
#   * token.grammar_hash matches proof.grammar_hash, AND
#   * token.policy_hash matches proof.policy_hash

CLI:

# Strict mint:
raucle-detect cap mint \\
  --key issuer.key.pem --issuer acme.bank.kyc \\
  --agent-id agent:kyc-prod --tool lookup_customer \\
  --require-proof --proof-result proof.json \\
  --out token.json

raucle-detect prove json|url|sql already returned exit-code 2 on REFUTED and 1 on UNDECIDED — both pipeline-failing under set -e. Now covered by tests so CI never silently regresses.

Backward compatibility

Default Opt-in via
Strict mint mode OFF CapabilityIssuer(require_proof=True) or RAUCLE_REQUIRE_PROOF=1
Gate-time proof enforcement OFF `CapabilityGate(proof_enforcement_mode='lenient'
Chain chain_meta header always emitted on new chains n/a — additive on-wire format; legacy chains remain verifiable
Fail-loud crypto errors always loud n/a — replaces silent except-pass with ConfigurationError

The two Capability field additions (grammar_hash, policy_hash) are nullable; absent in pre-0.13.0 tokens. Round-tripped through to_dict() / from_dict() cleanly.

Tests

70 new tests across tests/test_audit.py, tests/test_verdicts.py, tests/test_capability.py, tests/test_server_init.py, tests/test_cli_exit_codes.py. Full suite: 429 passed, 4 skipped. No regressions.

What's not in this release

  • Hosted policy-registry fetcher (gate consults a published registry at runtime with TTL caching). [DECIDE: gate-time proof enforcement] comment in CapabilityGate.__init__ flags this as future scope; current gate-time enforcement uses a caller-supplied in-memory cache.
  • Argon2id password hashes server-side. Out of scope for this fix.

🤖 Generated with Claude Code