Security audit
Version 0.2September 2026AEOS Security Audit
The protocol's own review: STRIDE threat model, invariant analysis, every finding's status checked against the code, and a recorded adversary against a live registry.
Conducted by the project against its own code. Every finding names where its status can be checked; the adversary's matrix is read from the recorded evidence run.
Abstract
This document reviews the AEOS protocol as implemented in the repository at the time of writing: the cryptographic primitives, Phanes ID, admission, the activity record, the Agent Stack (contracts, escrow, risk, consensus, disputes, inspection), agent payments, recognition by merchants, authorised oversight, witnesses, static publication, the enforcement guard, network enrichment and the evidence pipeline. It is conducted by the project against its own code and is not a third-party audit. Its empirical part is the adversary of the recorded evidence run 20260915T060329Z-evidence: 43 of 43 attacks against a live registry were defended, every one with the expected status code.
Of the 24 findings, 5 are fixed, 10 mitigated by a control that is in place and tested, 8 acknowledged with their limit stated, and 1 open. No critical finding. The one new finding of this version that deserves an operator's attention is the default admission mode (M-7).
Scope and method
- Document
- AEOS-SEC-AUDIT-002 · version 0.2 · September 2026
- Scope
- aeos/ (the protocol, the registry, the surfaces), phanes-core/ (the Rust verifier), ts-sdk/, web/ and phanes.app, the evidence pipeline
- Method
- STRIDE threat model over every surface; invariant analysis of the signed objects; review of each v0.1 finding against the current code; the recorded adversary and the test suites as evidence
- Conducted by
- the project, against its own code; not a third-party audit
- Evidence run
- 20260915T060329Z-evidence: 80 recorded sessions; every one at exit 0
A finding keeps its identifier across versions. Its status is one of four words: fixed (the defect is gone and a test guards it), mitigated (a control in place and tested reduces the risk, and the residual is stated), acknowledged (unchanged, with the limit stated so an operator can decide), open (a defect with a known remedy that is not yet applied). Informational findings describe a surface and the controls on it.
Executive summary
The protocol implements a zero-trust model in which every operation carries a verifiable signature by the party that made it, every state transition is written to a hash-chained, Merkle-anchored log, and every agent acts within bounds sealed into its ID. The registry seals what it issues and never holds a holder key; witnesses and the static publication put its history in front of parties who do not depend on it.
| Severity | Findings | Status |
|---|---|---|
| Critical | none found | — |
| High | 3 | H-1 mitigated, H-2 acknowledged, H-3 acknowledged |
| Medium | 7 | M-1 fixed, M-2 acknowledged, M-3 fixed, M-4 fixed, M-5 open, M-6 fixed, M-7 mitigated |
| Low | 6 | L-1 acknowledged, L-2 acknowledged, L-3 acknowledged, L-4 acknowledged, L-5 acknowledged, L-6 fixed |
| Informational | 8 | I-7 mitigated, I-8 mitigated, I-9 mitigated, I-10 mitigated, I-11 mitigated, I-12 mitigated, I-13 mitigated, I-14 mitigated |
Threat model
The protocol operates in a multi-agent environment where any participant may be adversarial: agents, venues, merchants, attesters, witnesses, overseers and the network between them. The registry's operator is trusted to run the code as published and is checked by witnesses and the publication; the holders' keys are never in the operator's hands.
| Threat | Category | Surface | Controls |
|---|---|---|---|
| Agent identity forgery | Spoofing | issuance, presentations, delegation | the holder key in cnf; every presentation signed fresh against a nonce; attestations over the key fingerprint; the registry seals with Ed25519 and ML-DSA-87 |
| Holder impersonation on the registry | Spoofing | every /v1 route that changes state | four Phanes-* headers sign method, path, timestamp, nonce and body hash; nonce replay cache; purpose-bound second signatures on terms, evidence and votes |
| Impersonation of an agent at a merchant | Spoofing | RFC 9421 requests | holder-key signatures with a registry-resolved key id, an eight-minute window, a nonce cache and a content digest; inactive keys refused |
| Sybil admission | Spoofing | /v1/ids/apply and the batch route | deny and allow lists, per-controller quotas, attested mode, provisional IDs with sealed zero bounds, the review queue, every decision recorded |
| Ledger or log tampering | Tampering | activity record, audit trail, ledger, PBFT | SHA-256 hash chains and RFC 6962 trees; operator signatures; 3f + 1 replica commits with exported signed bytes; witnesses |
| Registry rollback or fork | Tampering | tree heads | witnesses refuse a smaller tree and a different history at the same size, verify consistency proofs and cosign; cosignatures published in the anchors |
| Publication tampering | Tampering | the static files phanes.app serves | a signed, sealed manifest over every file; missing, altered and unexpected files named; witness cosignatures; browser-side verification under the anchors |
| Inspection report tampering in transit | Tampering | GET /v1/agents/{number}/inspect, the site proxy, the publication | registry EdDSA JWS with an ML-DSA-87 seal; the browser verifies under the issuer key from its own origin and renders only the verified payload |
| Payment replay or over-spend | Elevation | /v1/pay | mandate hash and nonce recorded; token bound to one ID, merchant, currency, ceiling, uses and expiry; cart bound to its merchant, total and ID; sealed bounds, daily volume, circuit breaker, risk engine; refusals recorded |
| Escrow drainage | Elevation | contracts, escrow, fulfilment | two holder signatures to activate; the risk gate; obligation hash verification before release; circuit breakers |
| Guard bypass by a hostile runtime | Elevation | the enforcement guard | the guard is cooperative by design; the control over any agent is the refusal of venues, merchants and payments that check the registry |
| Contract obligation replay | Repudiation | fulfilment proofs | each fulfilment carries a unique proof hash; ledger sequence numbers; operator signatures |
| Unauthorised disclosure | Information disclosure | documents, contract terms, the public inspector | SD-JWT selective disclosure with decoys; AES-256-GCM envelopes; the public report carries integers and digests only, asserted by tests |
| Overseer key misuse | Information disclosure | /v1/oversight | registered keys with a scope and immediate revocation; a required, recorded purpose; signed, addressed responses; the hash-chained audit log |
| Profiling by address | Information disclosure | network enrichment | registration of the range, never a location; unknown reported as unknown; visible only behind the oversight gate |
| Consensus disruption | Denial of service | PBFT message flood | view change recovers from a faulty primary; watermark-bounded log windows; checkpoints keep their own entry |
| Registry exhaustion | Denial of service | every public route | per-caller token-bucket rate limits; five-second signed snapshots of the inspector; the static publication absorbs public reads |
Findings
Findings are ordered by severity. The evidence column names where the status can be checked in the repository.
H-1 Two families of domain-separation tags
- Severity · status
- High · Mitigated
- Area
- Cryptographic primitives
- Finding
- The v0.1 review found signing contexts under 'AEOS/', 'AEOS/pbft/' and 'AEOS/checkpoint/' with no unified scheme. Since v0.4 every Phanes object (documents, Marks, status lists, events, presentations, mandates, tokens, receipts, tree heads, manifests, oversight responses, RFC 9421 requests) signs under one frame, PHANES/v1/<domain>\0<length><payload>, and the legacy protocol modules keep their AEOS/ tags, which are distinct from each other and from the frame. No two purposes sign identical bytes.
- Evidence
- aeos/canonical.py (PROTOCOL_TAG, domain_sign); the tags listed by grep over aeos/*.py
- Recommendation
- Keep every new object under the PHANES/v1 frame. The legacy tags are not renamed, so earlier signatures stay verifiable.
H-2 Non-constant-time Shamir reconstruction
- Severity · status
- High · Acknowledged
- Area
- Threshold cryptography
- Finding
- Python integer arithmetic is not constant-time; share values could leak through timing on shared hardware. Unchanged.
- Evidence
- aeos/threshold_crypto.py
- Recommendation
- Acceptable for the in-process deployment model. Distributed secret sharing across a network boundary needs a constant-time backend.
H-3 Gradual behavioural drift
- Severity · status
- High · Acknowledged
- Area
- Risk engine and ML
- Finding
- An agent can shift its profile slowly so that anomalous behaviour reads as normal. The entropy drift detector (KL divergence over feature distributions) mitigates in part; a second-order detector over the rate of profile change is not implemented.
- Evidence
- aeos/ml_engine.py (EntropyDriftDetector)
- Recommendation
- Add a second-order drift detector and rate-limit profile updates.
M-1 Python range proofs were forgeable
- Severity · status
- Medium · Fixed
- Area
- Cryptographic primitives
- Finding
- The v0.1 Python range proof was a hash-only construction any party could forge. It was replaced by a sound, zero-knowledge Σ-protocol (Pedersen bit decomposition with Cramer–Damgård–Schoenmakers OR-proofs over Ed25519); the Rust Bulletproofs module remains the production path. The old construction survives only as a regression test that proves it was forgeable.
- Evidence
- aeos/zkrange.py; tests/test_id_stack.py::test_legacy_rangeproof_was_forgeable
- Recommendation
- Done. Keep the regression test in the release gate.
M-2 Delegation revocation scans all agents
- Severity · status
- Medium · Acknowledged
- Area
- Identity system
- Finding
- Revoking a delegation in the legacy identity module scans the registry for sub-delegations. Correct, O(n), unchanged.
- Evidence
- aeos/identity.py
- Recommendation
- A revocation tree or CRL-like structure for O(log n) checks when the legacy delegation module is used at scale.
M-3 Escrow had no persistence guarantee
- Severity · status
- Medium · Fixed
- Area
- Contract engine
- Finding
- A crash between activation and fulfilment could lose escrowed amounts. The agent stack persists every contract, escrow token, dispute, anchor and ledger entry in SQLite (WAL) and restores all of it on start, re-checking every hash chain and Merkle tree; the restart test exercises it.
- Evidence
- aeos/persistence.py; tests/test_agent_stack.py (restart)
- Recommendation
- Done. Keep the restart test in the release gate.
M-4 Arbitrator pool bootstrapping
- Severity · status
- Medium · Fixed
- Area
- Dispute resolution
- Finding
- In a small network the pool may not have enough independent parties. Arbitrators are selected by VRF from the certified, reputable population, never from the parties; each vote is signed with the arbitrator's own holder key; three are selected per dispute, and a dispute waits, visibly, when the pool cannot supply them.
- Evidence
- aeos/agent_stack.py (resolve, select_arbitrators)
- Recommendation
- Done. Collusion among a large pool remains an economic question the reputation rule narrows but does not close.
M-5 Isolation forest splits use the Mersenne Twister
- Severity · status
- Medium · Open
- Area
- Risk engine and ML
- Finding
- Feature and split-value selection use Python's random module, which is not cryptographically secure; an attacker who can predict the generator's state could craft inputs that evade detection. Unchanged.
- Evidence
- aeos/ml_engine.py (random.randint, random.uniform in the isolation forest)
- Recommendation
- Seed the forest from the secrets module in adversarial deployments.
M-6 PBFT checkpoint garbage collection dropped the checkpoint entry
- Severity · status
- Medium · Fixed
- Area
- BFT consensus
- Finding
- Found by the 112-agent campaign: replicas garbage-collected the log entry at each stable checkpoint before the submitting client read its commit certificate, so every hundredth operation was reported as uncommitted. The checkpoint's own entry now stays until the next checkpoint; a regression test commits 305 operations across three checkpoints.
- Evidence
- aeos/bft_ledger.py; tests/test_agent_stack.py
- Recommendation
- Done. The campaign's third phase runs at every evidence run and would surface a regression.
M-7 The default admission mode is open
- Severity · status
- Medium · Mitigated
- Area
- Admission
- Finding
- A registry started without AEOS_ADMISSION_MODE issues every well-formed application that the deny and allow lists and the per-controller quota let through; nothing checks that the controller exists. On a registry that authorities rely on this is the wrong default, because the lists and quotas are all that stands between a hostile party and a thousand IDs with the bounds they asked for. Since this version the registry warns at start when the mode is open; the deployment kit documents attested and review, provisional IDs carry sealed zero bounds, and every decision is on the admission log and in oversight.
- Evidence
- aeos/admission.py (AdmissionPolicy.mode default); aeos/id_service.py (warning at start); tests/test_admission.py; deploy/registry/README.md
- Recommendation
- Run attested or review on any registry that is relied on; keep open for development registries only.
L-1 Merkle roots are recomputed over the leaves
- Severity · status
- Low · Acknowledged
- Area
- Cryptographic primitives
- Finding
- The activity record computes tree hashes recursively over the leaf hashes and the ledger's accumulator rebuilds on append: O(n) per root for large logs. Correct at the sizes recorded so far; unchanged.
- Evidence
- aeos/provenance.py (MerkleTree._mth); aeos/crypto_primitives.py (MerkleAccumulator._rebuild)
- Recommendation
- A Merkle mountain range or cached subtree hashes for O(log n) appends at scale.
L-2 Evidence cannot be retracted
- Severity · status
- Low · Acknowledged
- Area
- Dispute resolution
- Finding
- Once committed, evidence stays in the tree. This is by design: it prevents evidence tampering.
- Evidence
- aeos/agent_stack.py
- Recommendation
- Documented as intentional.
L-3 No network partition handling
- Severity · status
- Low · Acknowledged
- Area
- BFT consensus
- Finding
- The replicas run in one process and assume reliable delivery; a minority partition of a distributed deployment would halt. Unchanged.
- Evidence
- aeos/bft_ledger.py
- Recommendation
- Exponential-backoff view-change timers and state transfer before any multi-host deployment.
L-4 Stripe authorisations expire
- Severity · status
- Low · Acknowledged
- Area
- Settlement
- Finding
- Stripe authorisations expire after seven days (up to thirty-one with extended authorisation); a contract that holds escrow on the Stripe rail longer than that loses its authorisation. The settlement engine refreshes status from Stripe but does not re-authorise. Unchanged.
- Evidence
- aeos/settlement.py
- Recommendation
- Re-authorise before day five for contracts that exceed the window, or keep long contracts on service credits.
L-5 The guard is cooperative
- Severity · status
- Low · Acknowledged
- Area
- Enforcement guard
- Finding
- The guard runs inside the agent's own process; a hostile runtime will not run it. It turns the registry's decisions into the behaviour of the runtimes a controller owns. The control that reaches every agent is the refusal of the venues, merchants and payments that check the registry.
- Evidence
- aeos/enforce.py; docs-portal/protocol/enforcement-guard.md; tests/test_enforce.py
- Recommendation
- Documented. Never present the guard as a control over third-party agents.
L-6 An ephemeral operator key was embedded in a recorded command
- Severity · status
- Low · Fixed
- Area
- Evidence pipeline
- Finding
- The walkthrough's robot-attestation step passed the walkthrough registry's bearer key inside the recorded command text, so an earlier evidence run in the repository's history shows the key of a throwaway registry that no longer exists. Every operator call in the walkthrough now takes the key from the environment, and no recorded command carries it.
- Evidence
- campaign/evidence_run.py; the recorded sessions of the current run
- Recommendation
- Done. Recorded commands are reviewed for secrets before an evidence run is delivered.
I-7 The public inspection route
- Severity · status
- Informational · Mitigated
- Area
- Agent inspector
- Finding
- GET /v1/agents/{number}/inspect is public and signs each report twice (Ed25519 and ML-DSA-87). Signed snapshots are cached for five seconds and every caller is under the registry's token-bucket rate limit, so a burst costs one signing pass. The report's redaction (no disclosable claims, keys, balances, prices, payloads, policy numbers) is asserted by tests on real output.
- Evidence
- aeos/id_service.py; tests
- Recommendation
- Keep the redaction test in the release gate; add per-route limits if the inspector is exposed without the site proxy or the static publication.
I-8 Authorised oversight routes
- Severity · status
- Informational · Mitigated
- Area
- Oversight
- Finding
- The /v1/oversight routes return the unredacted record of any agent, names and amounts included, to keys the operator registered as overseers. The surface is gated four ways: holder-style request signatures (timestamp window, single-use nonce), registry-side membership and scope of the overseer key with immediate revocation, a required and recorded purpose for every dossier, and the hash-chained audit log under the overseer's key fingerprint. Responses are signed, sealed and addressed to the overseer. Nothing on this surface changes an agent's state.
- Evidence
- aeos/id_api.py; aeos/id_service.py; tests/test_oversight.py
- Recommendation
- Register overseer keys from hardware-backed stores where the authority has them; review the oversight log against the authority's own case numbers.
I-9 Mandate replay, token scope and cart binding
- Severity · status
- Informational · Mitigated
- Area
- Agent payments
- Finding
- A payment mandate is accepted once: its hash and nonce are recorded and a second presentation is refused. A token is bound to one ID, one merchant, one currency, a ceiling, a number of uses and an expiry, and the intent it serves; a cart is bound to its merchant, total and currency and, when it names one, to one ID. The sealed per-transaction and daily-volume bounds, the circuit breaker and the risk engine apply before settlement. Every refusal is recorded with its reason; three in a day alert oversight.
- Evidence
- aeos/payments.py (authorise); tests/test_payments.py
- Recommendation
- Keep the refusal tests in the release gate; when a Stripe rail is configured, reconcile receipts against Stripe events.
I-10 RFC 9421 verification and the key directory
- Severity · status
- Informational · Mitigated
- Area
- Recognition by merchants
- Finding
- Merchant-side verification rebuilds the signature base, requires the covered components, enforces the window (eight minutes for the Visa TAP tags, twenty-four hours for Web Bot Auth), records nonces, checks the content digest, and resolves the key id through the registry, which reports whether the key is active. A revoked or suspended ID's key is not active. The TypeScript SDK's signatures verify under the Python verifier.
- Evidence
- aeos/httpsig.py; tests/test_httpsig.py; ts-sdk/src/httpsig.ts
- Recommendation
- A static publication cannot sign a directory response with a fresh timestamp; merchants that require one use the live registry.
I-11 Rollback and fork refusal
- Severity · status
- Informational · Mitigated
- Area
- Witnesses
- Finding
- A witness refuses a tree head smaller than the last it saw for the log (rollback) and a different head at the same size (fork), verifies the consistency proof between the two, and only then cosigns. Cosignatures are listed in the anchors and the inspection report; the count on the current head is shown, and zero as zero.
- Evidence
- aeos/witness.py; tests/test_witness.py
- Recommendation
- A registry that never shows a witness a head cannot be caught by that witness: run witnesses on a schedule from independent parties.
I-12 Publication integrity
- Severity · status
- Informational · Mitigated
- Area
- Static publication
- Finding
- The manifest carries the SHA-256 of every published file and is signed (phanes-publication+jwt) and sealed. Verification names missing, altered and unexpected files and checks the witnesses' cosignatures; the browser verifies each document under the anchors before showing it. The publication carries public documents only and refuses IDs of a foreign authority.
- Evidence
- aeos/publish.py; tests/test_publish.py; web/site_e2e.py
- Recommendation
- Publish from the registry's own machine only; keep the publication verifier in the site's deployment step.
I-13 Addresses are registrations, not locations
- Severity · status
- Informational · Mitigated
- Area
- Network enrichment
- Finding
- Every recorded address is enriched from the RIR delegation files and RouteViews data and labelled as a registration of the range, never as a location; addresses the data does not cover are reported as unknown. The enrichment appears only in the roster and the dossier, behind the oversight gate.
- Evidence
- aeos/netinfo.py; tests/test_netinfo.py
- Recommendation
- Refresh the delegation files daily; never present the country as the agent's whereabouts.
I-14 Build provenance and SBOM
- Severity · status
- Informational · Mitigated
- Area
- Supply chain
- Finding
- On every release tag the provenance workflow attests build provenance for the wheel and the container image and attaches CycloneDX SBOMs.
- Evidence
- .github/workflows/provenance.yml
- Recommendation
- Verify attestations before installing a release; pin dependencies in the image.
The recorded adversary
During the evidence run 20260915T060329Z-evidence an adversary process ran 43 attacks against the live campaign registry while 112 agents and robots used it: replayed and unsigned requests, stale and future timestamps, wrong keys, forged and bit-flipped Marks, tampered events, nonce reuse, revoked IDs, pool capacity, a strict registry refusing unverified premiums, and the rest. Each attack names the status it expected and the status it observed; a defence is a match. The matrix is read from the run's summary file.
| Attack | Expected | Observed | Defended |
|---|---|---|---|
| replay_signed_request | HTTP 401 | HTTP 401 | yes |
| stale_timestamp_-10min | HTTP 401 | HTTP 401 | yes |
| future_timestamp_+10min | HTTP 401 | HTTP 401 | yes |
| unsigned_holder_route | HTTP 401 | HTTP 401 | yes |
| wrong_key_read_full_id | HTTP 403 | HTTP 403 | yes |
| wrong_key_record_event | HTTP 403 | HTTP 403 | yes |
| wrong_key_quote | HTTP 403 | HTTP 403 | yes |
| operator_route_wrong_bearer | HTTP 403 | HTTP 403 | yes |
| operator_route_no_bearer | HTTP 401 | HTTP 401 | yes |
| signature_over_different_path | HTTP 401 | HTTP 401 | yes |
| signature_over_different_body | HTTP 401 | HTTP 401 | yes |
| forged_mark_attacker_signed_offline | False | False | yes |
| forged_mark_attacker_signed_online | False | False | yes |
| pq_downgrade_v2_to_v1_online | False | False | yes |
| pq_downgrade_v2_to_v1_offline | False | False | yes |
| pq_signature_forged_random_online | False | False | yes |
| pq_signature_forged_random_offline | False | False | yes |
| pq_signature_by_attacker_key_online | False | False | yes |
| pq_signature_by_attacker_key_offline | False | False | yes |
| pq_signature_byte_tampers_113_offline | 113 | 113 | yes |
| single_bit_tampers_1280_offline | 1280 | 1280 | yes |
| byte_tampers_11_online | 11 | 11 | yes |
| garbage_mark_online | False | False | yes |
| short_mark_online | False | False | yes |
| tampered_event_body_commit | HTTP 4xx | HTTP 403 | yes |
| unregistered_witness | HTTP 4xx | HTTP 403 | yes |
| replay_committed_event | HTTP 4xx | HTTP 400 | yes |
| presentation_nonce_reuse | False | False | yes |
| presentation_wrong_audience | False | False | yes |
| presentation_swapped_disclosure | False | False | yes |
| visa_from_unregistered_venue | HTTP 403 | HTTP 403 | yes |
| robotics_package_for_software_agent | HTTP 403 | HTTP 403 | yes |
| physical_cover_without_hardware_attestation | HTTP 4xx | HTTP 400 | yes |
| policy_beyond_pool_capacity_refused | HTTP 400 | HTTP 400 | yes |
| bind_without_verified_premium_strict_registry | HTTP 403 | HTTP 403 | yes |
| suspended_mark_online | False | False | yes |
| suspended_mark_offline_after_refresh | False | False | yes |
| unsuspended_mark_online | True | True | yes |
| revoked_mark_online | False | False | yes |
| revoked_mark_offline_after_refresh | False | False | yes |
| new_mark_for_revoked_id | HTTP 4xx | HTTP 400 | yes |
| unknown_id_number | HTTP 404 | HTTP 404 | yes |
| path_traversal_number | HTTP 4xx | HTTP 404 | yes |
Remediation priorities
| Priority | Finding | Action | Effort |
|---|---|---|---|
| P0 | M-7 | Run attested or review admission on every relied-on registry; keep open for development only | Configuration |
| P1 | H-3 | Second-order drift detection; rate-limited profile updates | Medium |
| P1 | M-5 | Seed the isolation forest from the secrets module | Low |
| P1 | H-2 | Constant-time backend before distributed secret sharing | Medium |
| P2 | I-11 | Witnesses run by independent parties on a schedule | Operational |
| P2 | L-4 | Re-authorise Stripe escrow before day five, or keep long contracts on service credits | Low |
| P2 | M-2 | Revocation tree for the legacy delegation module | Medium |
| P3 | L-1 | Cached subtree hashes or a Merkle mountain range for large logs | High |
| P3 | L-3 | View-change timers and state transfer before any multi-host consensus | High |
Conclusion
The protocol's fundamentals hold: strict Ed25519 with post-quantum seals on every issued object, signatures by the party that acts, hash-chained and Merkle-anchored records with independent witnesses, Byzantine fault tolerant commits with exported quorum certificates, and a public surface that carries digests and integers only. The v0.1 findings that could be closed in code are closed and guarded by tests; the ones that remain are stated with their limits.
The residual risks are operational. A relied-on registry must not run with open admission; witnesses must be run by parties independent of the operator; authority keys belong in hardware-backed stores; a multi-host consensus deployment needs partition handling before it exists. Within those conditions the protocol is suitable for controlled deployment with trusted operators, and the recorded adversary shows the request surface holding under the attacks it was given.
Every claim in this document is checked by a test or recorded in the evidence run.