Skip to main content

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:v1
  • sessionId = first 8 chars of a UUID
  • idx/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

Frame1
Frame 1
Frame2
Frame 2
Frame3
Frame 3
Frame4
Frame 4
Frame5

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:

ComponentDescription
sessionIdIdentifier used to group frames belonging to the same presentation
idx/total1-based frame index and total frame count
pcOptional pad count (only meaningful for the final frame)
chunkPayload 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:

  • sessionId
  • idx
  • total
  • pc (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:

  1. Order chunks by index.
  2. Remove padding from final chunk if pc present.
  3. Concatenate chunks.
  4. 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:

  1. Attempt Base64URL decode.
  2. If successful, attempt zlib decompression.
  3. Interpret result as UTF-8.
  4. 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 iat and exp.
  • Enforce:

iat ≤ now ≤ exp

Failure rejects the presentation.

Holder binding

  • Extract holder public key from cnf.jwk inside 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 _sd and id)
  • 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

TopicDKTB Signed QRSecureQR
TransportMultipart QRMultipart QR
Payload partsCBORText frames
Assembled payloadCBOR mapToken string
VerificationISO mdoc + COSEJWS + SD-JWT
FreshnessvalidFrom/validToiat/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:v1
  • targetBytesPerQR: 400
  • minChunk: 50
  • padCountFieldWidth: 4
  • defaultCorrection: Q
  • defaultTargetSide: 260
  • assemblyTimeout: 3.0s
  • maxSessionAge: 10.0s