Wire protocol
How the request and response travel through the W3C Digital Credentials API as an ISO mdoc presentation: who builds each structure, what gets hashed, signed, and encrypted, and what each side checks. The steps follow spec §8, and each links its requirement. For what the JSON inside means, see Request and response.
- Protocol
org-iso-mdoc - docType
org.smarthealthit.checkin.1 - Namespace
org.smarthealthit.checkin - Element
smart_health_checkin_response - All fixed values §8.1
Strict to build, lenient to receive. Whoever builds a message builds exactly what the spec describes RCV-0. Whoever receives one stops only at a step marked fail; any other problem is a warn: it keeps going and reports it RCV-1 RCV-2. A bad signature is a warning, not a failure. See Security and timeouts for why.
How a request and response flow
navigator.credentials.get.
1. Build the request verifier
Spec §8.2. The Verifier builds, in order:
The SMART request, inside an ItemsRequest VRQ-1 VRQ-2 VRQ-3
The SMART request JSON rides as a text string in requestInfo. The mdoc part asks for the one
element, and its boolean is intentToRetain: true only if the Verifier may keep
the response after the session. The whole ItemsRequest is CBOR-encoded and wrapped in tag 24,
which fixes the exact bytes that reader authentication later signs.
ItemsRequest = {
docType: "org.smarthealthit.checkin.1",
nameSpaces: { "org.smarthealthit.checkin": { smart_health_checkin_response: intentToRetain } },
requestInfo: { "org.smarthealthit.checkin.request": "<SMART request JSON text>" }
}
ItemsRequestBytes = tag24(CBOR(ItemsRequest))
A fresh HPKE key and encryptionInfo VRQ-4 VRQ-5
A new P-256 key pair and a random nonce (16 bytes or more) for this request. The public key goes to the
wallet inside encryptionInfo; the private key never leaves the page. The Verifier keeps the
exact base64url string it sends, because the transcript is computed from that string VRQ-9.
EncryptionInfo = ["dcapi", {
nonce: bstr,
recipientPublicKey: { 1: 2, -1: 1, -2: x, -3: y } // COSE_Key, EC2 P-256
}]
Reader authentication, if used VRQ-6 RA-1
Optional. The Verifier signs ReaderAuthenticationBytes, which covers this session's
SessionTranscript and the exact ItemsRequestBytes, with a key whose certificate goes in
x5chain. A wallet that checks it puts the result in one of five states (absent, malformed,
invalid, valid but untrusted, trusted) and shows a verifier identity only when trusted TRUST-2.
It never stops the exchange WRQ-9. Without a deployment trust list, "valid but untrusted" is the
normal result.
DeviceRequest, then the browser call VRQ-7 VRQ-8
DeviceRequest = { version: "1.0", docRequests: [{ itemsRequest: ItemsRequestBytes, readerAuth? }] }
navigator.credentials.get({
mediation: "required",
digital: { requests: [{ protocol: "org-iso-mdoc",
data: { deviceRequest: base64url(DeviceRequest), encryptionInfo: base64url(EncryptionInfo) } }] }
})
Set a timeout on the call VRQ-10. On some platforms a failed delivery never settles the call (timeouts).
2. SessionTranscript verifier wallet
Spec §8.3. Both sides compute it separately, and it must come out
byte for byte the same: it is the HPKE info, and it is inside what reader authentication and the
device signature cover TR-5.
dcapiInfo = CBOR([encryptionInfoBase64url, origin]) Handover = ["dcapi", SHA-256(dcapiInfo)] SessionTranscript = [null, null, Handover] // HPKE info = CBOR(SessionTranscript)
encryptionInfoBase64urlis the exact string the Verifier sent, not a re-encoding TR-1.originfor a web page is its ASCII origin, such ashttps://clinic.example: no path, no trailing slash TR-2. For a native app, it is exactly what the platform reports; see Platform notes.- The wallet takes the origin only from the browser or platform, never from the request TR-3. With no origin it cannot respond TR-4.
- The two
nullslots are ISO 18013-5'sDeviceEngagementBytesandEReaderKeyBytes, which the Digital Credentials API flow doesn't use.
3. Check the request wallet
Spec §8.4. On a fail, the wallet does not respond, and the platform ends the call with an error WRQ-1.
| Step | Check | On a problem |
|---|---|---|
| WRQ-2 | Decode deviceRequest (base64url, CBOR) | fail if it can't be decoded; warn for another protocol name or padded base64url |
| WRQ-3 | DeviceRequest.version is "1.0" | warn |
| WRQ-4 | Find the DocRequest with this profile's docType | fail if none; warn if several (use the first) |
| WRQ-5 | Take the request text from requestInfo | fail if missing; warn for other nameSpaces or intentToRetain problems |
| WRQ-6 | Validate the SMART request (spec §5) | fail where §5 rejects it; problems inside one item make that item unsupported |
| WRQ-7 | Decode encryptionInfo and its P-256 key | fail if no usable key; warn for other shape problems |
| WRQ-8 | Compute the SessionTranscript | fail if there is no origin |
| WRQ-9 | Check readerAuth, if present | Never fails; classified as above |
Then the patient chooses, item by item, and the wallet builds the SMART response WRQ-10.
4. Build the response wallet
Spec §8.4. The wallet signs its own mdoc: it acts as the issuer and as the device, with keys it controls. The signatures make the mdoc complete and verifiable; they don't say who issued the content (§7).
The issuer-signed item and its digest WRS-1
IssuerSignedItem = { digestID: 0, random: 16+ random bytes,
elementIdentifier: "smart_health_checkin_response", elementValue: "<SMART response JSON text>" }
IssuerSignedItemBytes = tag24(CBOR(IssuerSignedItem))
digest = SHA-256(IssuerSignedItemBytes) // tag included
Receivers hash these bytes exactly as received and never re-encode them ENC-2.
The MSO and issuerAuth WRS-2 WRS-3 WRS-4
MSO = { version: "1.0", digestAlgorithm: "SHA-256", docType: "org.smarthealthit.checkin.1",
valueDigests: { "org.smarthealthit.checkin": { digestID: digest } },
deviceKeyInfo: { deviceKey: COSE_Key },
validityInfo: { signed, validFrom, validUntil } } // tag-0 UTC date-times, no fractions
issuerAuth = COSE_Sign1(protected {1: -7}, unprotected {33: x5chain}, payload tag24(CBOR(MSO)))
The certificate in x5chain may be self-signed.
The device signature, detached WRS-5 WRS-6
DeviceAuthentication = ["DeviceAuthentication", SessionTranscript, "org.smarthealthit.checkin.1",
DeviceNameSpacesBytes] // SessionTranscript as the array, not bytes
deviceSignature = COSE_Sign1(protected {1: -7}, payload null)
signed over tag24(CBOR(DeviceAuthentication)) with the MSO's deviceKey
The payload is null; the verifier rebuilds the signed bytes from its own transcript. DeviceNameSpacesBytes is tag24(CBOR({})).
The DeviceResponse WRS-7
DeviceResponse = { version: "1.0", status: 0, documents: [{
docType: "org.smarthealthit.checkin.1",
issuerSigned: { nameSpaces: { "org.smarthealthit.checkin": [IssuerSignedItemBytes] }, issuerAuth },
deviceSigned: { nameSpaces: DeviceNameSpacesBytes, deviceAuth: { deviceSignature } } }] }
Every structure is defined in CDDL in §8.7; which bytes each hash and signature covers is in the table in §8.6.
5. Encrypt wallet
Spec §8.5 HPKE-1 HPKE-2. One fixed suite; nothing on the wire names another ALG-1.
| Parameter | Value |
|---|---|
| Mode | HPKE base mode |
| KEM | DHKEM(P-256, HKDF-SHA256), 0x0010 |
| KDF | HKDF-SHA256, 0x0001 |
| AEAD | AES-128-GCM, 0x0001 |
| info | CBOR(SessionTranscript) |
| aad | empty |
dcapiResponse = ["dcapi", { enc: 65-byte P-256 point, cipherText }]
result = { protocol: "org-iso-mdoc", data: { response: base64url(CBOR(dcapiResponse)) } }
6. Check the response verifier
Spec §8.5. On a fail, the Verifier rejects the whole response VRS-1.
| Step | Check | On a problem |
|---|---|---|
| VRS-2 | Decode data.response as ["dcapi", {enc, cipherText}] | fail if it can't be decoded; warn for another protocol name or padding |
| VRS-3 | Recompute the transcript from its own origin and encryptionInfo string, and decrypt | fail if it doesn't open. HPKE is authenticated, so this also catches tampering in transit. |
| VRS-4 | Decode the DeviceResponse, find the document with this docType | fail if none; warn for version, status, or extra documents |
| VRS-5 | Verify issuerAuth with the first x5chain certificate; check MSO fields | warn |
| VRS-6 | SHA-256 of IssuerSignedItemBytes as received equals the MSO digest for its digestID | warn |
| VRS-7 | Rebuild DeviceAuthenticationBytes, verify the device signature | warn, also for an attached payload that differs from the rebuilt bytes |
| VRS-8 | Find smart_health_checkin_response with a text value | fail if missing |
| VRS-9 | Parse and validate the SMART response against the request | fail only for the whole-response problems in §6.4; the rest affects one item or record (Request and response) |
| VRS-10 | MSO validityInfo includes now, give or take a few minutes | warn |
Real bytes
Spec Appendix A walks one real Chrome-to-Android capture step by step: the transcript hash, the decryption, the digest, and all three signatures, with every value recomputed from the fixture on each build. The Capture inspector shows the same capture byte by byte, and can load a run from the Testing EHR.