Static read-through audit of crates/dpp-vault. API-key storage/auth itself is sound (high-entropy secrets, SHA-256 hash at rest, constant-time comparison, correct revoked/expired handling); status-list and signature verification are genuinely fail-closed. One verified finding — the most severe in the whole 32-crate audit.
1. Scope enforcement exists but is never called on any mutation handler
Location: src/handlers/create.rs:48, update.rs:17, publish.rs:19, suspend.rs:27, archive.rs:18, eol.rs:37, transfer.rs:35,77
Issue: dpp-types/src/api_key.rs:39-43 defines ApiKeyScope::can_write(), specifically documented to gate passport writes (create/update/publish/suspend/archive) to Write/Admin scope — but a repo-wide grep shows can_write is never called anywhere in dpp-engine. None of the seven mutation handlers check auth.scope at all; they only read auth.user_id for the audit-log actor field. router.rs:118-121 wraps /api/v1/* in auth_middleware, but that middleware only authenticates the bearer token and injects AuthContext — it performs no scope-based authorization. Contrast handlers/api_keys.rs:20, registry_identity.rs:21, operator.rs:36, which correctly call auth.scope.is_admin().
Failure scenario: An operator mints a Read-scoped integration key (documented as intended for GET-only listings, e.g. for a low-trust partner or read-only dashboard). That key can nonetheless POST /api/v1/dpp, publish it (Ed25519-signed, registered with the EU registry), suspend/archive/EOL it, and initiate/accept operator transfers — full passport-lifecycle control despite least-privilege being the entire reason scopes were introduced. This is a real, currently-exploitable privilege escalation for any deployment that has issued a non-Admin key. (See also the related, broader scope-enforcement findings in dpp-types and dpp-dal.)
Static read-through audit of
crates/dpp-vault. API-key storage/auth itself is sound (high-entropy secrets, SHA-256 hash at rest, constant-time comparison, correct revoked/expired handling); status-list and signature verification are genuinely fail-closed. One verified finding — the most severe in the whole 32-crate audit.1. Scope enforcement exists but is never called on any mutation handler
Location:
src/handlers/create.rs:48,update.rs:17,publish.rs:19,suspend.rs:27,archive.rs:18,eol.rs:37,transfer.rs:35,77Issue:
dpp-types/src/api_key.rs:39-43definesApiKeyScope::can_write(), specifically documented to gate passport writes (create/update/publish/suspend/archive) toWrite/Adminscope — but a repo-wide grep showscan_writeis never called anywhere indpp-engine. None of the seven mutation handlers checkauth.scopeat all; they only readauth.user_idfor the audit-log actor field.router.rs:118-121wraps/api/v1/*inauth_middleware, but that middleware only authenticates the bearer token and injectsAuthContext— it performs no scope-based authorization. Contrasthandlers/api_keys.rs:20,registry_identity.rs:21,operator.rs:36, which correctly callauth.scope.is_admin().Failure scenario: An operator mints a
Read-scoped integration key (documented as intended for GET-only listings, e.g. for a low-trust partner or read-only dashboard). That key can nonethelessPOST /api/v1/dpp, publish it (Ed25519-signed, registered with the EU registry), suspend/archive/EOL it, and initiate/accept operator transfers — full passport-lifecycle control despite least-privilege being the entire reason scopes were introduced. This is a real, currently-exploitable privilege escalation for any deployment that has issued a non-Admin key. (See also the related, broader scope-enforcement findings indpp-typesanddpp-dal.)