Skip to content

dpp-node: EU registry payload validation failure sends anyway; transfer notifications always omit the prior operator #19

Description

@LKSNDRTMLKV

Static read-through audit of crates/dpp-node. No auth/authz logic lives in this crate (routes are mounted from other services); /health//metrics are its only own handlers. Four verified findings.

1. Invalid EU registry payloads are submitted anyway

Location: src/infra/registry/mapping.rs:157-163
Issue: envelope.payload.validate() failing only logs tracing::warn!("... failed B1 validation — sending anyway (pre-go-live)"), and the invalid payload is submitted to the EU registry regardless.
Failure scenario: Any malformed/incomplete registration payload (e.g. missing required Annex III fields) is sent as-is rather than being rejected before it leaves the node — the opposite of fail-closed for a regulatory submission.

2. Transfer notifications always omit the previous operator

Location: src/infra/registry/mapping.rs:299-320 (notify_transfer)
Issue: from_operator is always built with value/name/country hardcoded to empty strings ("would come from context"). The RegistrySyncPort::notify_transfer signature itself never receives a "from" operator — this is structural, not an edge case.
Failure scenario: Every EU-registry transfer notification this node ever sends permanently omits who the previous operator was.

3. Token-expiry check can panic on underflow

Location: src/infra/registry/token.rs:22-25 (CachedToken::is_expired)
Issue: self.expires_at - Duration::from_secs(30) on a std::time::Instant panics on underflow rather than saturating.
Failure scenario: If the EU registry's OAuth endpoint ever returns expires_in under ~30s while the node has been up for under ~20-30s (fresh restart racing a token refresh), this panics inside get_token() — on the hot path of every register/check_status/notify_transfer call.

4. Silent fallback on boot-time DB lookup failure

Location: src/main.rs:197-204
Issue: db.operator_repo.get(...).await.ok().flatten().map(|c| c.country).unwrap_or_default() discards any DB error with no log line — every other fallible boot step in this file logs on failure.
Failure scenario: A transient Postgres hiccup during boot silently sets operator_country to "", baked into every passport's facility/operator registry payload for the life of the process with zero signal anything went wrong.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcomplianceRegulatory/compliance correctness 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