SecureQR v1.0
Terminology
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.
What SecureQR is
dewa has designed the protocol and format Secure-QR to allow securely sharing and verifying short lived presentations of Verifiable Credentials (VC) in close proximity between two devices. It builds upon the use case of verifying Credentials without a full OpenID4VP flow, e.g. proving your age at the club or proving your identity during train ticket inspection.
SecureQR is a streaming QR transport for a presentation token (VP-wrapped JWS containing an SD-JWT), optimized for:
- Offline / proximity verification (camera scan of animated QR sequence)
- Constant-sized frames (fixed bytes per QR, variable number of frames)
- Optional compression (zlib) to reduce frames
- Robust assembly (ordering, padding, timeouts)
Wire format
Frame header
Each frame is a UTF-8 string with a fixed header + a chunk of the payload.
Header format:
<versionTag>|<sessionId>|<idx>/<total>|pc=<padCount>|<chunk>
Default values:
versionTag = mDLQR:v1sessionId = first 8 chars of a UUIDidx/total = 1-based frame index and total frames, zero-padded to digits(total)pc = pad count for the last chunk (zero-padded width, default 4)<chunk> = a slice of the payload
Example:
mDLQR:v1|A1B2C3D4|01/05|pc=0000|<bytes…>
Payload encoding & compression
The pseudocode below should give a sense of how the format is structured.
// Inner presentation (holder-selected disclosure + signed by issuer)
type vp = {
"sdjwt": object, // Verifiable Credential (SD-JWT)
"iat": number, // seconds since Unix epoch
"exp": number // seconds since Unix epoch
}
// Wrapper JWT claims (signed by holder)
type payload = {
"iss": string, // holder DID
"iat": number, // mirrors vp.iat
"exp": number, // mirrors vp.exp
"jti": string, // unique per presentation
"vp": vp // embedded inner presentation
}
// JWS header for payload
type header = {
"alg": string,
"typ": string,
"kid": string // holder did:key
}
// dewa Secure-QR creation pseudocode (v1)
// Sign presentation
const jwt = Wallet.SignPresentation(payload, header);
// Compress JWT using zlib
const jwtBytes = jwt.toBytes();
const jwtDeflated = zlib.deflate(jwtBytes);
let qrData: string;
if (jwtDeflated.length < jwtBytes.length) {
qrData = jwtDeflated.toBase64UrlWithoutPadding();
} else {
qrData = jwt;
}
// Split into equal size multipart QR codes
const sessionId = uuidv4().slice(0, 8);
const chunks = chunkPayload(sessionId, qrData);
// Each chunk follows this format:
// mDLQR:v1|<sessionId>|<idx>/<total>|pc=<padding>|<chunk>
// mDLQR:v1 - format/protocol identifier v1
// <sessionId> - e.g. 789abcde
// <idx/total> - frame nr. idx of total frames
// pc=<padding> - padding to ensure same size QR frames
// <chunk> - chunk of session ID
// © (Copyright) 2026 dewa ApS, licensed under CC-BY 4.0
Framing & chunking algorithm
Stable bytes per QR
Compression is optional. If zlib deflate makes the token smaller, the e-Wallet emits base64-encoded deflated bytes; otherwise the e-Wallet emits the plain JWT. Verifiers must accept both forms and attempt inflate when the input is deflated. Large payloads are split into equal-size QR frames linked by a short session id. Constant frame size and explicit padding keep QR density stable while scanning, and the session id only groups frames for assembly and has no security value.
Deterministic padding
To keep the last frame body exactly chunkSize bytes, the last chunk is padded with a deterministic alphabet.
- Applied only to the final frame if needed.
- Pad count encoded as
pc=<padCount>. - Padding alphabet:
A–Z a–z 0–9 - _ - Padding generated via deterministic PRNG (xorshift32) seeded by
(sessionId | idx).
QR code rendering
Security Considerations
Because Secure-QR codes are targeted towards offline device-to-device, the wallet and verifier
are not able to exchange a nonce. Instead the verifier relies on short lived presentations,
which require the phone and verifier time to be sufficiently synchronized. To prevent misuse
the lifetime of a presentation must be less than 15 minutes (exp - iat <= 900).
The verifier may optionally temporary store the wallet key ID, that is bound to the Verifiable Credential to identify copied credentials. The verifier must verify both presentation- and VC signature as well as the revocation list status, as specified by OpenID4VP 1.0 and W3C VCDM 2.0.
Example





Scanning & reassembly
Figure 1: Sequence diagram of SecureQR verification between User (Wallet) and Verifier.
Frame parsing
A SecureQR receiver MUST accept decoded QR frame text as input.
For each frame, the receiver MUST verify that:
-
The frame begins with the expected version prefix (e.g. mDLQR:v1|)
-
The frame is composed of fields separated by the | character
-
The frame header conforms to the SecureQR frame format
-
The receiver MUST parse the following components from each frame:
| Component | Description |
|---|---|
| sessionId | Identifier used to group frames belonging to the same presentation |
| idx/total | 1-based frame index and total frame count |
| pc | Optional pad count (only meaningful for the final frame) |
| chunk | Payload fragment carried by the frame |
Table 1: SecureQR frame header fields and their descriptions.
The receiver MUST verify:
- Version prefix matches
mDLQR:v1| - Field separator is
| - Header fields are structurally valid
Parsed fields:
sessionIdidxtotalpc(optional)chunk
Invalid frames MUST be rejected.
Reassembly state
Per sessionId, the receiver MUST track:
- Expected total frame count
- Map of received chunks by index
- Pad count of final frame
- Timing metadata
Frames MAY arrive out of order.
Duplicate frames MAY be ignored.
Completion
A session is complete when all indices 1..total are present.
Upon completion:
- Order chunks by index.
- Remove padding from final chunk if
pcpresent. - Concatenate chunks.
- Decode and decompress payload.
Failures invalidate the session.
Timeouts
Implementations SHOULD discard sessions that:
- Are inactive beyond a configured timeout, or
- Exceed a maximum session lifetime
Payload decode and verification
Decoding
Steps:
- Attempt Base64URL decode.
- If successful, attempt zlib decompression.
- Interpret result as UTF-8.
- If Base64URL fails, interpret original payload as UTF-8.
Invalid UTF-8 results invalidate the payload.
Presentation verification
The decoded payload MUST be a compact JWS representing a Verifiable Presentation.
Outer presentation validation
- Parse JWS.
- Extract JSON payload.
- Verify presence of
vp.sdjwt. - Verify
iatandexp. - Enforce:
iat ≤ now ≤ exp
Failure rejects the presentation.
Holder binding
- Extract holder public key from
cnf.jwkinside SD-JWT. - Verify outer JWS signature.
Failure rejects the presentation.
SD-JWT verification
- Verify issuer signature.
- Resolve issuer key.
- Apply disclosures.
- Reconstruct credential subject.
Failure rejects the presentation.
Result construction
A verifier MAY extract:
- Displayable claims (excluding
_sdandid) - Credential type(s)
- Issuer identifier
- Holder identifier
vct
Security Considerations
Because Secure-QR codes are targeted towards offline device-to-device, the wallet and verifier
are not able to exchange a nonce. Instead the verifier relies on short lived presentations,
which require the phone and verifier time to be sufficiently synchronized. To prevent misuse
the lifetime of a presentation must be less than 15 minutes (exp - iat <= 900).
The verifier may optionally temporary store the wallet key ID, that is bound to the Verifiable Credential to identify copied credentials. The verifier must verify both presentation- and VC signature as well as the revocation list status, as specified by OpenID4VP 1.0 and W3C VCDM 2.0.
A full OpenID4VP flow is RECOMMENDED for all use-cases with strict security requirements.
DKTB comparison
| Topic | DKTB Signed QR | SecureQR |
|---|---|---|
| Transport | Multipart QR | Multipart QR |
| Payload parts | CBOR | Text frames |
| Assembled payload | CBOR map | Token string |
| Verification | ISO mdoc + COSE | JWS + SD-JWT |
| Freshness | validFrom/validTo | iat/exp |
Table 2: Comparison between DKTB Signed QR and dewa SecureQR.
Appendix A: Payload structure
After assembly, SecureQR yields a compact JWS:
BASE64URL(header) . BASE64URL(payload) . BASE64URL(signature)
This JWS embeds an SD-JWT presentation.
Appendix B: Key constants
versionTag: mDLQR:v1targetBytesPerQR: 400minChunk: 50padCountFieldWidth: 4defaultCorrection: QdefaultTargetSide: 260assemblyTimeout: 3.0smaxSessionAge: 10.0s