Security audit

Version 0.2September 2026

AEOS 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).

1

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.

2

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.

Findings by severity and status Findings by severity and status Critical none found High 1 2 3 Medium 4 1 1 1 7 Low 1 5 6 Informational 8 8 Fixed Mitigated Acknowledged Open Mitigated: the risk is reduced by a control that is in place and tested. Acknowledged: unchanged, with the limit stated.
Figure 1. Findings by severity, segmented by status. Counts are computed from the findings list of this document.
SeverityFindingsStatus
Criticalnone found—
High3H-1 mitigated, H-2 acknowledged, H-3 acknowledged
Medium7M-1 fixed, M-2 acknowledged, M-3 fixed, M-4 fixed, M-5 open, M-6 fixed, M-7 mitigated
Low6L-1 acknowledged, L-2 acknowledged, L-3 acknowledged, L-4 acknowledged, L-5 acknowledged, L-6 fixed
Informational8I-7 mitigated, I-8 mitigated, I-9 mitigated, I-10 mitigated, I-11 mitigated, I-12 mitigated, I-13 mitigated, I-14 mitigated
3

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.

ThreatCategorySurfaceControls
Agent identity forgerySpoofingissuance, presentations, delegationthe 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 registrySpoofingevery /v1 route that changes statefour 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 merchantSpoofingRFC 9421 requestsholder-key signatures with a registry-resolved key id, an eight-minute window, a nonce cache and a content digest; inactive keys refused
Sybil admissionSpoofing/v1/ids/apply and the batch routedeny and allow lists, per-controller quotas, attested mode, provisional IDs with sealed zero bounds, the review queue, every decision recorded
Ledger or log tamperingTamperingactivity record, audit trail, ledger, PBFTSHA-256 hash chains and RFC 6962 trees; operator signatures; 3f + 1 replica commits with exported signed bytes; witnesses
Registry rollback or forkTamperingtree headswitnesses refuse a smaller tree and a different history at the same size, verify consistency proofs and cosign; cosignatures published in the anchors
Publication tamperingTamperingthe static files phanes.app servesa signed, sealed manifest over every file; missing, altered and unexpected files named; witness cosignatures; browser-side verification under the anchors
Inspection report tampering in transitTamperingGET /v1/agents/{number}/inspect, the site proxy, the publicationregistry 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-spendElevation/v1/paymandate 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 drainageElevationcontracts, escrow, fulfilmenttwo holder signatures to activate; the risk gate; obligation hash verification before release; circuit breakers
Guard bypass by a hostile runtimeElevationthe enforcement guardthe guard is cooperative by design; the control over any agent is the refusal of venues, merchants and payments that check the registry
Contract obligation replayRepudiationfulfilment proofseach fulfilment carries a unique proof hash; ledger sequence numbers; operator signatures
Unauthorised disclosureInformation disclosuredocuments, contract terms, the public inspectorSD-JWT selective disclosure with decoys; AES-256-GCM envelopes; the public report carries integers and digests only, asserted by tests
Overseer key misuseInformation disclosure/v1/oversightregistered keys with a scope and immediate revocation; a required, recorded purpose; signed, addressed responses; the hash-chained audit log
Profiling by addressInformation disclosurenetwork enrichmentregistration of the range, never a location; unknown reported as unknown; visible only behind the oversight gate
Consensus disruptionDenial of servicePBFT message floodview change recovers from a faulty primary; watermark-bounded log windows; checkpoints keep their own entry
Registry exhaustionDenial of serviceevery public routeper-caller token-bucket rate limits; five-second signed snapshots of the inspector; the static publication absorbs public reads
4

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.
5

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.

AttackExpectedObservedDefended
replay_signed_requestHTTP 401HTTP 401yes
stale_timestamp_-10minHTTP 401HTTP 401yes
future_timestamp_+10minHTTP 401HTTP 401yes
unsigned_holder_routeHTTP 401HTTP 401yes
wrong_key_read_full_idHTTP 403HTTP 403yes
wrong_key_record_eventHTTP 403HTTP 403yes
wrong_key_quoteHTTP 403HTTP 403yes
operator_route_wrong_bearerHTTP 403HTTP 403yes
operator_route_no_bearerHTTP 401HTTP 401yes
signature_over_different_pathHTTP 401HTTP 401yes
signature_over_different_bodyHTTP 401HTTP 401yes
forged_mark_attacker_signed_offlineFalseFalseyes
forged_mark_attacker_signed_onlineFalseFalseyes
pq_downgrade_v2_to_v1_onlineFalseFalseyes
pq_downgrade_v2_to_v1_offlineFalseFalseyes
pq_signature_forged_random_onlineFalseFalseyes
pq_signature_forged_random_offlineFalseFalseyes
pq_signature_by_attacker_key_onlineFalseFalseyes
pq_signature_by_attacker_key_offlineFalseFalseyes
pq_signature_byte_tampers_113_offline113113yes
single_bit_tampers_1280_offline12801280yes
byte_tampers_11_online1111yes
garbage_mark_onlineFalseFalseyes
short_mark_onlineFalseFalseyes
tampered_event_body_commitHTTP 4xxHTTP 403yes
unregistered_witnessHTTP 4xxHTTP 403yes
replay_committed_eventHTTP 4xxHTTP 400yes
presentation_nonce_reuseFalseFalseyes
presentation_wrong_audienceFalseFalseyes
presentation_swapped_disclosureFalseFalseyes
visa_from_unregistered_venueHTTP 403HTTP 403yes
robotics_package_for_software_agentHTTP 403HTTP 403yes
physical_cover_without_hardware_attestationHTTP 4xxHTTP 400yes
policy_beyond_pool_capacity_refusedHTTP 400HTTP 400yes
bind_without_verified_premium_strict_registryHTTP 403HTTP 403yes
suspended_mark_onlineFalseFalseyes
suspended_mark_offline_after_refreshFalseFalseyes
unsuspended_mark_onlineTrueTrueyes
revoked_mark_onlineFalseFalseyes
revoked_mark_offline_after_refreshFalseFalseyes
new_mark_for_revoked_idHTTP 4xxHTTP 400yes
unknown_id_numberHTTP 404HTTP 404yes
path_traversal_numberHTTP 4xxHTTP 404yes
6

Remediation priorities

PriorityFindingActionEffort
P0M-7Run attested or review admission on every relied-on registry; keep open for development onlyConfiguration
P1H-3Second-order drift detection; rate-limited profile updatesMedium
P1M-5Seed the isolation forest from the secrets moduleLow
P1H-2Constant-time backend before distributed secret sharingMedium
P2I-11Witnesses run by independent parties on a scheduleOperational
P2L-4Re-authorise Stripe escrow before day five, or keep long contracts on service creditsLow
P2M-2Revocation tree for the legacy delegation moduleMedium
P3L-1Cached subtree hashes or a Merkle mountain range for large logsHigh
P3L-3View-change timers and state transfer before any multi-host consensusHigh
7

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.

A third-party audit remains the right next step before adversarial, multi-operator deployment; this document is the material such an audit would start from.

Every claim in this document is checked by a test or recorded in the evidence run.