Skip to content

Credential holder binding defaults to none (unbound) — secure-by-default should bind via did:jwk #172

Description

@jeremi

Use Case

An operator issuing SD-JWT VC credentials from Registry Notary. A credential's holder binding is governed by the credential profile's holder_binding.mode, and that field defaults to "none" (unbound):

  • HolderBindingConfig::default sets mode from default_holder_binding_mode(), which returns "none" (crates/registry-notary-core/src/config.rs).
  • Issuance only attaches a holder confirmation (cnf) when mode != "none" (crates/registry-notary-core/src/sd_jwt.rs); the test credential_without_holder_uses_registry_subject_ref confirms an unbound credential is issued (registry subject_ref as sub) when no holder is supplied.
  • Binding is forced only on the self-attestation / OID4VCI (wallet) path — validate_self_attestation_profile requires mode == "did", proof_of_possession == "required", and allowed_did_methods = ["did:jwk"].

An unbound SD-JWT VC is effectively bearer-style: anyone who comes into possession of it can present it. So the out-of-the-box default for a credential-issuing profile is the less secure posture, and an operator has to know to opt in to binding.

This also diverges from the spec: RS-PR-NOTARY (REQ-PR-NOTARY-013 / -015) states an issued credential MUST bind its holder via did:jwk. So the default profile can issue credentials that don't meet the spec's binding requirement.

Proposed Behavior

Make holder binding the default (secure-by-default), so an operator must consciously opt out rather than opt in:

  • Change default_holder_binding_mode() to "did" (with did:jwk as the default allowed_did_methods), or
  • If a profile legitimately needs to issue unbound credentials, keep "none" available but require it to be set explicitly and surface it loudly (e.g. a registryctl doctor warning when a credential-issuing profile runs unbound).

Either way, the bare/default issuance path should produce a holder-bound credential, matching the spec's MUST.

Boundaries

  • The wallet / self-attestation (OID4VCI) path already binds correctly — this is specifically about the default credential profile and any backend/direct issuance that leaves holder_binding at its default.
  • Flipping the default is a behaviour change for any existing deployment relying on unbound issuance, so it needs a migration note and a clear way to keep the old behaviour deliberately.
  • did:jwk is currently the only supported binding method, so binding-by-default does not need new method support.

Surfaced while reviewing the Stage-1 trust/credential documentation against the implementation: the docs had described credentials as holder-bound, but the default issuance path is unbound. Filed per the secure-by-default principle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:notaryRegistry Notary ownership.enhancementNew feature or request

    Type

    No type
    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