Hosted service offline; see undercurrentholdings.com/status
Guides

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:

RoleResponsibilityRBAC Role
Risk LeadAssesses whether the risk of proceeding is acceptablerisk_lead
Security LeadAssesses whether security controls remain adequatesecurity_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  REJECTED

Key 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 total

Key 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

ConstraintValueConfigurable?
Required signatures2 (Risk Lead + Security Lead)No
Expiration window24 hoursYes (expiration_hours parameter)
Max expiration10 years (87,600 hours)No
Complexity gateCannot be overriddenNo
Signature algorithmBIP-322 (default) or HybridYes (provider parameter)

Audit Trail

Every override attempt generates a SignatureRecord with:

FieldDescription
signer_idCustodian identifier
rolerisk_lead or security_lead
signatureBase64-encoded signature
message_hashSHA-256 hex of the request message (each signature covers the role-bound hash derived from it)
timestampUTC timestamp of signature
public_keyBase64-encoded public key
algorithmbip322-simple, ed25519, or ed25519+ml-dsa-65
bindingSigned-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.


On this page