Skip to content

dpp-identity: mTLS authorization trusts client-supplied headers with no session binding; key rotation fails open #14

Description

@LKSNDRTMLKV

Static read-through audit of crates/dpp-identity. Four verified findings.

1. mTLS authorization is spoofable via plain HTTP headers

Location: src/middleware/mtls.rs:85-158
Issue: Authorization is based entirely on trusting X-Client-Cert-Subject/X-Client-Cert-Issuer header values at face value — nothing in-process binds them to an actual verified TLS session (no shared secret, no localhost/IP restriction, no in-process cert inspection).
Failure scenario: Any caller that can reach this listener directly — bypassing the terminating proxy (misconfiguration, network path error) — can set both headers itself (CN=odal-vault, issuer CN=Odal Internal CA) and obtain full sign/rotate authority. This is a gap in the code as written, not merely a deployment nit: no defense-in-depth exists beyond string equality on attacker-reachable header content.

2. Key rotation destroys the old key even when archival fails

Location: src/handlers/rotate_key.rs:44-47
Issue: archive_key() failure is only logged (tracing::warn!), then generate_key() proceeds anyway, unconditionally overwriting the current record (dpp-crypto/src/keystore/store.rs:250).
Failure scenario: If archiving fails (disk error, lock contention) right before rotation, the old key is destroyed with no backup, the endpoint still returns 200 OK {"rotated": true}, and every JWS previously signed by that operator becomes permanently unverifiable — contradicting the handler's own docstring promise that "historical passport signatures remain verifiable."

3. Per-operator DID document route is dead code

Location: src/handlers/did_document.rs:18-21 vs src/router.rs:54-66, dpp-node/src/router.rs:48
Issue: The handler has an Option<Path<String>> branch documented as reachable at /operators/{id}/did.json, but no such route is ever registered — only /.well-known/did.json is mounted, and the fused dpp-node only mounts the public build.
Failure scenario: Only the "root" operator's DID document can ever be served over HTTP, regardless of how many operators actually have keys.

4. Unbounded key auto-provisioning

Location: src/handlers/sign.rs:54-60
Issue: operator_id is only checked for non-emptiness, then used to auto-provision a brand-new signing key with no format/length/allow-list validation.
Failure scenario: Any distinct string reaching this endpoint (typo, retry, or an attacker reaching it via the mTLS gap above) permanently grows the on-disk encrypted key store — keys are never pruned — with no rate limit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity-relevant issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions