A CWT profile for the Digital Product Passport.
A selective-disclosure credential profile over SD-CWT: one signed passport that a buyer can present three ways — to the public, to a legitimate-interest auditor, or to an authority — revealing only what each is meant to see, and nothing more.
v1 pre-draft. Not for implementation. Codepoints 600-610 are placeholders pending IANA registration.
- media type
- application/dpp+sd-cwt
- substrate
- draft-ietf-spice-sd-cwt-07
- codepoints
- 600–699 (placeholder)
A simulated stream of battery passports.
This is a simulation, not a real market — synthetic e-bike traction batteries from various drive-system makers, moving through their lifecycle. Each step issues a Digital Product Passport, logs it to a growing transparency log, and presents, verifies and validates it with the Go issuer compiled to WebAssembly. The data is made up; the cryptography is genuine.
The CDDL, verbatim.
This is the exact grammar the Go test-suite validates issued tokens against on every CI run. Copy it, edit it, and re-run make data-cddl to retest against freshly issued SD-CWT.
; ============================================================================
; dpp-profile.cddl — DPP CWT/SD-CWT profile, REALIZATION grammar
; ============================================================================
; This is the *implementable* grammar for the Digital Product Passport
; profile of SD-CWT. It is the machine-checked companion to the pre-draft
; spec "A CDDL Definition for a DPP Profile for CWT/SD-CWT" (the annotated
; source lives in the IETF notes pad linked from the repo README).
;
; Unlike the notes-pad spec — which is written against the Ruby `cddl` gem
; and documents idealized forms — this file is validated on every CI run by
; the Rust `cddl` crate (v0.10.x) against REAL tokens emitted by
; internal/dpp + internal/sim (see internal/sim/cddl_test.go). If issuance
; and this grammar drift apart, the test goes red. Keep them aligned.
;
; Codepoints 600..611 are PLACEHOLDERS pending IANA registration.
;
; ----------------------------------------------------------------------------
; Realization deviations from the notes-pad spec (each stated transparently):
;
; R1. iat / exp are bare CWT NumericDate (int / float), NOT #6.1(int).
; RFC 8392 §2 defines NumericDate as a bare numeric; the spec's
; `secs = #6.1(int)` is the CBOR epoch-datetime tag, which CWT does
; not use. Issuance emits a bare int, which is the RFC-correct form.
; R2. The `&(name: label) ^=> type` group-to-choice + cut sugar from the
; spec is written here as plain `label => type`. Semantically identical
; for a closed integer-keyed map; the Rust validator handles it directly.
; R3. sd_alg (170) is OPTIONAL and omitted when SHA-256 (-16) is in use, to
; match the substrate (sdcwt.Issue only advertises non-default sd_alg).
; R4. The four spec categories (METADATA / ANCHOR / CORE / EXTENSION) are
; prose section markers, not structure — the payload is one flat map.
; R5. dpp-submodule modes (inline / detached-digest / nested-token) are
; validated structurally as generic maps/arrays here; the three-mode
; discipline and EAT no-inheritance rule (D4) stay prose-enforced.
; R6. Leftover holder-authored text-keyed claims (batch_id, …) are admitted
; via the open `* tstr => any` tail, matching the spec's extension tail.
; ============================================================================
; -----------------------------------------------------------------------------
; Outer envelope: COSE_Sign1 (RFC 9052, tag 18) — ROOT RULE
; -----------------------------------------------------------------------------
dpp-sd-cwt = #6.18([
protected: bstr .cbor dpp-protected-header,
unprotected: dpp-unprotected-header,
payload: bstr .cbor dpp-payload,
signature: bstr
])
; -----------------------------------------------------------------------------
; Protected header
; -----------------------------------------------------------------------------
dpp-protected-header = {
1 => int, ; alg M (RFC 9052)
16 => dpp-media-type, ; typ M (RFC 9596 / V5)
? 4 => bstr, ; kid O
? 170 => int, ; sd_alg O (omitted for SHA-256; R3)
* int => any ; open extension
}
dpp-media-type =
"application/dpp+sd-cwt" ; preferred string form
/ 293 ; SD-CWT integer typ (fallback)
; -----------------------------------------------------------------------------
; Unprotected header — salted disclosures travel here (SD-CWT §5.1)
; -----------------------------------------------------------------------------
dpp-unprotected-header = {
? 17 => [+ bstr], ; sd_claims (salted disclosures)
* int => any
}
; -----------------------------------------------------------------------------
; Payload — flat Claims-Set; categories are prose (R4)
; -----------------------------------------------------------------------------
dpp-payload = {
; ----- METADATA (about the token; never redactable, D6) ------------------
1 => tstr, ; iss M (RFC 8392)
6 => secs, ; iat M (RFC 8392; R1)
? 4 => secs, ; exp M-if freshness policy (R1)
? 7 => bstr, ; cti O
11 => dpp-vct-value, ; vct M (D2 discriminator)
? 275 => uint, ; intuse O (EAT)
? 600 => tstr, ; dpp-schema-version (semver of the claim schema)
? 607 => dpp-lifecycle-phase-value, ; dpp-lifecycle-phase (D5)
? 606 => bstr, ; dpp-multihash
? 611 => dpp-supersedes, ; dpp-supersedes — digest of the prior passport this one replaces
; ----- ANCHOR (binds to a physical instance; never redactable, D6) -------
8 => dpp-cnf, ; cnf M (SD-CWT; D1)
? 2 => tstr, ; sub M-if no on-board key
? 601 => dpp-ueid, ; dpp-product-instance-uid
? 602 => [+ carrier-binding-entry], ; dpp-carrier-binding
? 609 => [+ dpp-trust-anchor], ; dpp-trust-anchors
; ----- CORE (universal product description; redactable) ------------------
? 603 => dpp-economic-operator, ; dpp-economic-operator
? 604 => dpp-life-cycle-stage-value, ; dpp-life-cycle-stage
? 605 => dpp-claim-level-state-value,; dpp-claim-level-state
? 273 => [+ measurement-entry], ; measurements (EAT)
? 274 => [+ dloa-type], ; dloas (EAT)
? 610 => dpp-status-list-pointer, ; dpp-status-list
; ----- EXTENSION (sector submodules; redactable) -------------------------
? 608 => { + tstr => any }, ; dpp-submods (R5)
; ----- SD-CWT redaction marker (hashes of redacted CORE/EXTENSION keys) --
? #7.59 => [* bstr],
; ----- open extension tail (holder-authored text claims, R6) -------------
* tstr => any,
* int => any
}
; -----------------------------------------------------------------------------
; Domain types
; -----------------------------------------------------------------------------
secs = int / float ; RFC 8392 NumericDate (R1)
; vct is a URL that resolves to this profile + version, e.g.
; "https://dpp.space/credentials/ebike-battery/v1". Distinct from
; dpp-schema-version (600), which versions the claim schema independently.
dpp-vct-value = tstr
dpp-ueid = bstr / tstr ; EAT UEID (bstr) or text instance id
; dpp-supersedes: digest (leaf hash) of the prior passport in the lifecycle
; that this one replaces — making the version chain navigable and anchoring
; it to a previous transparency-log entry. Generic; not battery-specific.
dpp-supersedes = bstr
dpp-cnf = { * int => any } ; {1: COSE_Key} or {3: thumbprint}
dpp-lifecycle-phase-value =
"issuance" / "transfer" / "in-service" / "repair" / "recall" / "end-of-life" / int
dpp-life-cycle-stage-value =
"raw-material" / "manufacture" / "distribution" / "use" / "repair"
/ "remanufacture" / "reuse" / "recycle" / "disposal" / int
dpp-claim-level-state-value = "active" / "superseded" / "withdrawn"
dpp-economic-operator = {
1 => tstr, ; eo-type
2 => tstr, ; eo-id
? 3 => tstr ; eo-name
}
carrier-binding-entry = {
1 => tstr, ; carrier-type
2 => tstr, ; carrier-value
? 3 => tstr ; carrier-uri
}
dpp-trust-anchor = {
1 => bstr, ; ta-kid
2 => [+ bstr], ; ta-x5c
? 3 => tstr ; ta-role
}
dpp-status-list-pointer = {
1 => tstr, ; uri
2 => uint, ; index
? 3 => tstr ; format
}
measurement-entry = {
1 => tstr, ; meas-method
2 => any, ; meas-value
? 3 => tstr ; meas-unit
}
; dloa-type: [registrar, platform-label, ?application-label]. Written as an
; explicit two-arity choice because cddl-rs mishandles a trailing `? tstr`
; inside an array group.
dloa-type =
[tstr, tstr, tstr]
/ [tstr, tstr]
; ============================================================================