Override Workflow
Two-key override approval for governance gate failures.
Two-key override approval for governance gate failures.
When a proposal fails one or more governance gates but must proceed (e.g., emergency hotfix), AEGIS requires two independent signatures from designated custodians before approving the override. This page explains the full workflow.
What Is an Override?
An override bypasses failed governance gates for a specific proposal. It is a controlled exception, not a workaround -- every override is signed, audited, and time-limited.
Constraints:
- The Complexity gate cannot be overridden (hard floor at 0.5). If complexity fails, the proposal must be redesigned.
- Override requests expire after 24 hours by default.
- Both signatures derive from the same request hash (canonical encoding of
proposal ID, justification, failed gates, and
requested_at), but each custodian signs its own role-bound hash (see Signed Message (Role Binding)).
Who Signs?
Two distinct custodians, each holding a separate private key:
| Role | Responsibility | RBAC Role |
|---|---|---|
| Risk Lead | Assesses whether the risk of proceeding is acceptable | risk_lead |
| Security Lead | Assesses whether security controls remain adequate | security_lead |
Both signatures are required. A single person holding both keys does not satisfy the two-key requirement -- the roles must be assigned to different individuals.
Flow Diagram
Proposal fails gates
|
v
+------+-------+
| Override | state: PENDING
| Request | requested_by, justification, failed_gates
+------+--------+
|
Risk Lead reviews
|
+----v----+
| Sign | Risk Lead signs its role-bound hash
+---------+
|
v
+------+-------+
| Override | state: PARTIAL
| Request | risk_lead_signature populated
+------+--------+
|
Security Lead reviews
|
+----v----+
| Sign | Security Lead signs its own role-bound hash
+---------+
|
v
+------+---------+
| DualSignature | Validates both signatures
| Validator | Checks expiration, roles, hashes
+------+---------+
|
+----+----+
| |
APPROVED REJECTEDKey Generation
Each custodian generates a keypair using the configured signature provider.
BIP-322 (Default)
from crypto.bip322_provider import BIP322Provider
import base64
provider = BIP322Provider()
private_key, public_key = provider.generate_keypair()
# private_key: 32 bytes (BIP-340 secret key)
# public_key: 32 bytes (x-only public key)
print(f"Private key: {base64.b64encode(private_key).decode()}")
print(f"Public key: {base64.b64encode(public_key).decode()}")Hybrid Post-Quantum (Ed25519 + ML-DSA-65)
from crypto import get_hybrid_provider
provider = get_hybrid_provider()
private_key, public_key = provider.generate_keypair()
# private_key: Ed25519 (32B) + ML-DSA-65 (4032B) = 4064B total
# public_key: Ed25519 (32B) + ML-DSA-65 (1952B) = 1984B totalKey Security
Private keys must be stored securely. Never commit private keys to version control or log them. Use environment variables or a secrets manager for distribution.
Code Example: Full Override Flow
Step 1: Create the Override Request
from aegis_governance import OverrideWorkflow
workflow = OverrideWorkflow(
proposal_id="prop-2024-001",
justification="Emergency hotfix for production outage",
failed_gates=["risk"],
requested_by="engineer-1",
)
validator = workflow.validator
print(f"Request hash: {workflow.message_hash}") # bound to requested_at (N2)
print(f"Algorithm: {validator.algorithm_name}")Step 2: Risk Lead Signs Its Role-Bound Hash
Each custodian signs the hash for its own role and signer id, never the bare
message_hash (P5-1 / G133):
import base64
# Keys from "Key Generation" above are raw bytes; the validator takes base64.
risk_lead_private_key_b64 = base64.b64encode(risk_lead_private_key).decode()
risk_lead_public_key_b64 = base64.b64encode(risk_lead_public_key).decode()
risk_hash = workflow.role_message_hash("risk_lead", "risk-lead-1")
risk_sig_b64 = validator.sign_message(risk_hash, risk_lead_private_key_b64)Step 3: Security Lead Signs Its Role-Bound Hash
sec_hash = workflow.role_message_hash("security_lead", "sec-lead-1")
sec_sig_b64 = validator.sign_message(sec_hash, security_lead_private_key_b64)A custodian who signs offline computes the same value with
validator.create_role_message_hash(workflow.message_hash, role, signer_id).
Step 4: Add Both Signatures
ok, msg = workflow.add_signature(
signer_id="risk-lead-1",
role="risk_lead",
signature=risk_sig_b64,
public_key=risk_lead_public_key_b64,
)
ok, msg = workflow.add_signature(
signer_id="sec-lead-1",
role="security_lead",
signature=sec_sig_b64,
public_key=security_lead_public_key_b64,
)
result = workflow.get_result()
if result.approved:
print(f"Override approved (expires: {result.expires_at})")
else:
print(f"Override state: {result.state}")add_signature() rejects a signature that verifies only against the bare
message_hash, against the other role's hash, or against another signer id. It
also rejects a second signature made with the same public key as the first (for
hybrid keys, a shared Ed25519 or ML-DSA-65 half counts as the same key). That
check compares key encodings; it does not prove two different custodians, so
G109 (keys enrolled per signer) remains the real control. signer_id must be
non-empty, NFKC-normalized, free of leading or trailing whitespace, and free of
control, format, unassigned, surrogate, and private-use characters. This
excludes zero-width characters, including the ZWJ/ZWNJ joiners some scripts and
emoji sequences use; use an ASCII or plain-letter identifier for signers.
With no signature provider and no cryptography library installed,
every signature is rejected (fail closed).
Signed Message (Role Binding)
The two custodians do not sign the same bytes. The workflow derives one request hash, then one role-bound hash per signer:
message_hash = create_message_hash(proposal_id, justification,
failed_gates, requested_at)
role_message_hash = SHA-256(canonical JSON {
"binding": "aegis-override-role-binding/v1",
"message_hash": message_hash,
"role": role,
"signer_id": signer_id })The same derivation applies to every signature provider (BIP-322, Ed25519,
hybrid, and the legacy Ed25519 path). The N2 timestamp binding is kept, because
message_hash already includes requested_at. The combined authorization hash
(combine_signatures) is unchanged and still independent of signature order.
Breaking change (P5-1 / G133)
Signatures made over the bare message_hash are no longer accepted.
Signers and tools that sign workflow.message_hash directly must sign
workflow.role_message_hash(role, signer_id) instead. New SignatureRecord
entries carry binding = "aegis-override-role-binding/v1"; records
persisted before this change deserialize with binding = "" (unbound).
A PARTIAL override (one signature recorded) persisted before this change
cannot be completed (add_signature rejects the second signature with
"predates role binding"); re-request it. A PENDING override with no
signatures completes normally once both custodians sign their
role_message_hash.
This change does not bind signer_id to an enrolled key. That gap is
tracked separately as G109.
Using the Approver Actor
The Approver actor wraps override authorization with RBAC enforcement.
Approver.authorize_override is a single-actor check: it does not use the
role binding above and does not complete a two-key override. Without a
signature_verifier and public_key it accepts any non-empty signature string.
Use OverrideWorkflow.add_signature for two-key approval.
from aegis_governance import Approver
from crypto.bip322_provider import BIP322Provider
# Create approver with signature verification
approver = Approver(
actor_id="risk-lead-1",
name="Risk Lead",
is_risk_lead=True,
signature_verifier=BIP322Provider(),
)
result = approver.authorize_override(
proposal_id="prop-2024-001",
failed_gates=["risk"],
justification="Emergency hotfix",
# BIP-322 Simple signature over
# provider.create_message_hash(proposal_id, justification, failed_gates);
# NOT the role-bound OverrideWorkflow signature from Step 2.
signature=approver_sig_simple,
public_key=risk_lead_public_key, # 32 bytes
)
if result.success:
print("Override authorized by Risk Lead")Override Constraints
| Constraint | Value | Configurable? |
|---|---|---|
| Required signatures | 2 (Risk Lead + Security Lead) | No |
| Expiration window | 24 hours | Yes (expiration_hours parameter) |
| Max expiration | 10 years (87,600 hours) | No |
| Complexity gate | Cannot be overridden | No |
| Signature algorithm | BIP-322 (default) or Hybrid | Yes (provider parameter) |
Audit Trail
Every override attempt generates a SignatureRecord with:
| Field | Description |
|---|---|
signer_id | Custodian identifier |
role | risk_lead or security_lead |
signature | Base64-encoded signature |
message_hash | SHA-256 hex of the request message (each signature covers the role-bound hash derived from it) |
timestamp | UTC timestamp of signature |
public_key | Base64-encoded public key |
algorithm | bip322-simple, ed25519, or ed25519+ml-dsa-65 |
binding | Signed-message scheme: aegis-override-role-binding/v1, or empty for pre-P5-1 records |
Both successful and failed override attempts are logged in the telemetry pipeline with full signature records for forensic analysis.
Post-Quantum Option
For defense against quantum computing threats, use the hybrid provider:
from crypto import get_hybrid_provider
from aegis_governance import DualSignatureValidator
hybrid = get_hybrid_provider()
validator = DualSignatureValidator(provider=hybrid)
# Signatures now use Ed25519 + ML-DSA-65 (both must verify)Hybrid signatures are larger (~4,064 bytes vs 64 bytes for BIP-322) but provide post-quantum resistance.
Related Documentation
- Crypto Overview -- Entry point for all crypto in AEGIS
- Key Management Guide -- KEK lifecycle and rotation
- Python SDK: Key Management -- API reference