PRE-DRAFT · not for implementation

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)
Simulation · runs in your browser

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.

passports0validated0/0log entries0vendors0
Compiling the issuer to run in your browser…
Grammar

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-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]
; ============================================================================