Skip to content

Credential product-category scope is accepted but never enforced #64

Description

@LKSNDRTMLKV

DppCredentialSubject carries product_categories: Vec<String>, and core will enforce it — verify_credential_claims_with_trust takes a required_product_category and applies the same "empty means unscoped, populated must contain" rule it applies to sectors.

The engine passes None. So a credential scoped to one product category grants its full audience over every product in a sector it covers.

Why it was left unenforced

This is a deliberate non-implementation, not an oversight, and it should not be "fixed" by wiring the obvious thing.

There is no defined mapping between the credential's free-string product_categories and the typed dpp_domain::ProductCategory a passport carries. ProductCategory is a #[non_exhaustive] enum with rename_all = "snake_case" plus an Other(String) variant, so:

  • unit variants serialise as "ev_battery", "apparel", "smartphone"
  • Other(s) serialises as {"other": s} — an object, not a string

There is no round-trippable &str form today. Enforcing scope would mean inventing a correspondence and then relying on it as a security control: get it wrong in the permissive direction and the check silently passes everything; wrong in the restrictive direction and lawful holders are denied. Neither failure is visible without a spec to test against.

Sector scope is enforced (VerifiedCredential::audience_for_sector), and sector is the dispatch key that selects schema and plugin. Product category is a data attribute within a sector, so the unenforced axis is the narrower one.

What closing it needs

  1. A core decision on the canonical wire form of a product category — most likely a ProductCategory::as_wire_str()/from_wire_str() pair with Other(s) mapping to s itself, and a documented rule for collisions between an Other string and a future unit variant. This is the actual blocker and it belongs in dpp-core.
  2. Thread it through VerifiedCredential::audience_for_sector → something like audience_for(sector, product_category), passing required_product_category to core.
  3. Decide the same question sector scope already answered: out-of-scope is a downgrade to Audience::Public, not a denial. A credential that does not cover this product is valid, it simply unlocks nothing here — and denying would make the route a probe for a passport's category.
  4. Tests mirroring the sector ones in crates/dpp-vault/src/middleware/credential.rs: in-scope grants, out-of-scope downgrades, empty list stays unscoped, and an Other(_) category round-trips.

Where it is recorded

Named in the module docs at crates/dpp-vault/src/handlers/audience_read.rs and on VerifiedCredential::audience_for_sector, both of which record it as deliberately not built.

Introduced alongside #63.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecuritySecurity-relevant issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions