-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
This document describes the cryptographic and operational security properties of A1 v2.8. It is written for security architects, compliance engineers, and senior engineers evaluating the system for enterprise adoption.
A1 protects against the following threat classes:
| Threat | Mitigation |
|---|---|
| Recursive delegation gap (agent exceeds granted scope) | NarrowingMatrix bitwise enforcement at issuance and guard time |
| Forged authorization credentials | Ed25519 signature verification on every cert in the chain |
| Replay of a previously authorized intent | Per-intent nonce consumption (NonceStore) |
| Escalation attack (sub-cert claims more than parent) | SubScopeProof Merkle containment proof required at issuance |
| Compromised agent credential | Revocation by cert fingerprint (RevocationStore) |
| Cross-tenant authorization (tenant isolation bypass) | Namespace binding enforced before any signature verification |
| Timing side-channel in signature verification |
subtle crate constant-time comparison |
| Private key material leakage | ZeroizeOnDrop on DyoloIdentity; KMS signing patterns eliminate in-process key material |
| Tampered audit receipt | Blake3 commitment in ProvableReceipt; independent verification requires no secrets |
| Chain fingerprint collision | Blake3 over all cert fields; preimage resistance is 256-bit |
Every DelegationCert is signed with Ed25519 (using ed25519-dalek 2.x). Key properties:
- Security level: 128-bit (equivalent to RSA-3072)
- Key size: 32-byte private scalar, 32-byte verifying key
- Signature size: 64 bytes
- Verification speed: ~20 µs on modern hardware
- No weak parameters: Unlike ECDSA or RSA, Ed25519 has no per-signature randomness requirement. There is no parameter space for a weak parameter attack.
-
Batch verification:
ed25519-daleksupports batch verification (used inauthorize_batch) which is ~2× faster than individual verification for large batches.
All internal hashing uses Blake3 with domain separation. Specific usages:
| Usage | Domain prefix |
|---|---|
| Capability name → bit position | dyolo::narrowing::v1 |
| NarrowingMatrix commitment | dyolo::narrowing::v1 |
| Intent hash | dyolo::intent::v1 |
| Chain fingerprint | Blake3 over all cert fingerprints |
| SubScopeProof Merkle nodes | dyolo::merkle::v1 |
Domain separation ensures that an output in one context cannot be repurposed in another, even if the inputs collide.
All equality comparisons on sensitive byte arrays (cert fingerprints, nonces, public keys) use subtle::ConstantTimeEq to prevent timing side-channel attacks. This is enforced by the workspace dependency pinning to subtle = { version = "2.5", default-features = false }.
When DyoloChain::authorize is called:
-
Namespace check — If a namespace is set on the chain, it must match the context namespace. Mismatch returns
A1Error::NamespaceMismatchbefore any cryptographic work. -
Revocation check (fast path) — The principal cert's fingerprint is checked against
RevocationStore. If revoked, authorization fails immediately. -
Chain traversal — For each cert
C_iin the chain: a. Verify Ed25519 signature:C_i.signatureoverC_i.signable_bytes()usingC_{i-1}.delegate_pk(orprincipal_pkfor the first cert). b. Verify expiry:C_i.issued_at ≤ now < C_i.expires_at. c. Verify depth budget:C_i.max_depth ≥ remaining chain length. d. IfC_i.scope_proofis present, verify theSubScopeProof: the Merkle inclusion proof thatC_i.scope_rootis a subset ofC_{i-1}.scope_root. e. Check revocation:C_i.fingerprint()againstRevocationStore. -
Intent authorization — Verify that
intent_hashis in the terminal cert's scope via the providedMerkleProof. -
Nonce consumption — Call
NonceStore::consume(intent_nonce). If the nonce was already consumed (replay), authorization fails. -
Audit emission — Emit
AuditEventto all registeredAuditSinkinstances. -
Receipt production — Return
AuthorizedActioncontaining aVerificationReceipt.
For DyoloPassport::guard, steps 0 and 6.5 are prepended/appended:
-
NarrowingMatrix check (O(1)) —
(passport.capability_mask & intent_mask) == intent_mask. Fails fast before any chain traversal. 6.5. ProvableReceipt construction — Wrap theVerificationReceiptwith the passport namespace and Blake3 commitment over the enforced mask.
The NarrowingMatrix is a 256-bit bitmask. Each capability name maps to a (byte_index, bit_index) pair:
byte_index = blake3(DOMAIN || name)[0] % 32
bit_index = blake3(DOMAIN || name)[1] % 8
The narrowing invariant is:
child.mask & parent.mask == child.mask
This is equivalent to child ⊆ parent. It is computed as eight 64-bit AND operations (the 256-bit mask split into four u64 words processed in parallel on modern CPUs).
Collision analysis: Two distinct capability names may map to the same bit. This is intentional and conservative — if names A and B share a bit, authorizing A also authorizes B from the narrowing check's perspective. This is acceptable because:
- The
IntentTreeMerkle proof separately verifies the exact intent hash. - The narrowing check is an additional defense layer, not the sole gate.
- With 256 bits and typical capability sets of 5–20 names, collision probability is negligible (< 0.1% for 20 names against 256 slots).
Upgrade path: The domain prefix dyolo::narrowing::v1 allows a future v2 to change the bit assignment algorithm without breaking existing certs.
When a sub-cert is issued with a subset of the parent's capabilities, it carries a SubScopeProof. This is a Merkle inclusion proof proving that each hash in the sub-cert's IntentTree is present in the parent cert's IntentTree.
The chain verifier checks this proof at step 3d. A sub-cert without a SubScopeProof when one is required is rejected. A sub-cert whose proof fails Merkle verification is rejected. There is no way to produce a valid sub-cert with capabilities outside the parent's scope without either:
- Forging an Ed25519 signature (infeasible)
- Finding a Blake3 collision (infeasible)
Every DelegationCert carries a random 32-byte nonce. NonceStore::consume uses a compare-and-insert operation:
-
MemoryNonceStore:
HashMapwith anOnceLock-based single-writer pattern. -
a1-pg:
INSERT INTO nonces (nonce) VALUES ($1) ON CONFLICT DO NOTHING. TheON CONFLICT DO NOTHINGensures exactly-once semantics at the database level, even under concurrent requests. -
a1-redis:
SET nonce:$1 1 NX EX $ttl. TheNXflag provides atomic compare-and-set.
Nonces are generated via rand::rngs::OsRng, providing 256 bits of entropy. Collision probability for a 32-byte nonce is negligible.
Revocation is a fingerprint deny-list, not a revocation certificate. This design choice has tradeoffs:
Advantages:
- O(1) lookup (hash map or database index).
- No online requirement for issuers — revocation is stored at the verifier, not the issuer.
- Revocation takes effect instantly at the next authorization attempt.
Limitation:
- Revocation is not propagated passively. It must be written to the same
RevocationStorethat the verifier reads. For multi-instance deployments, use a shared Redis or Postgres backend.
Fingerprint: blake3(cert.signable_bytes())[0..32]. Because it is a hash of all cert fields including the signature, fingerprint collision requires forging the signature.
DyoloIdentity uses ed25519-dalek::SigningKey wrapped with ZeroizeOnDrop (from the zeroize crate). When the struct is dropped, the 32-byte key scalar is overwritten with zeros in memory before the memory is freed. This limits the window for cold-boot or memory-dump attacks.
DyoloIdentity does not implement Clone. To share across threads, use SharedIdentity(Arc::new(identity)), which provides a reference count without duplicating key material.
For production, implement the Signer trait over your KMS so the private key never touches application memory:
-
AWS KMS: The HMAC-KDF pattern (see
AwsKmsSigner) derives the Ed25519 scalar from a KMS HMAC operation. The scalar is used to sign and immediately dropped. - HashiCorp Vault Transit: Vault signs the payload server-side. The private key never leaves Vault.
- GCP KMS / Azure Key Vault: Asymmetric sign operations are performed server-side.
In all cases, verifying_key_bytes() returns the 32-byte public key, which is embedded in the cert. At verification time, no KMS call is required.
The ProvableReceipt is designed so that an auditor can independently verify it without any secrets:
- Load the
DyoloPassportfile (public cert, no private key needed). - Recompute
NarrowingMatrix::from_hex(receipt.capability_mask_hex).commitment(). - Compare with
receipt.narrowing_commitment. - Check
receipt.inner.chain_fingerprintagainst your audit log.
No private key, no KMS access, no network call is required for this verification.
| Feature | Additional dependencies | Notes |
|---|---|---|
serde |
serde, ed25519-dalek/serde
|
Serialization only |
wire |
serde_json |
JSON encoding of all wire types |
async |
async-trait, tokio
|
Async trait wrappers |
ffi |
None beyond wire
|
C ABI exports; unsafe is gated behind this flag |
policy-yaml |
serde_yaml |
YAML parsing only |
otel |
opentelemetry |
Tracing integration |
cbor |
ciborium |
Binary encoding |
The ffi feature is the only one that uses unsafe code, and only because C ABI export requires it. The unsafe blocks are narrowly scoped to FFI boundary marshaling.
The core a1 crate enforces #![deny(unsafe_code)] at the crate level. The compiler will reject any unsafe block introduced in the future unless the ffi feature and #[allow(unsafe_code)] are explicitly present on the specific module.
If you discover a security vulnerability in A1, see SECURITY.md for the responsible disclosure policy and contact information.