Skip to main content

Compact codes

The phone wallet and the in-person check (see Phone wallet and in-person checks) move data through QR codes. JSON files are too large for that, so the same facts are packed into short binary codes.

PrefixNameCarriesMade byRead byMax length
v1.Static codeA credential's signed facts, optionally the holder's details and selective disclosure dataIssued card (Show a QR code for the holder's phone), wallet (Show the code), device store/wallet, /present4,096 characters
c1.Challenge codeThe verifier's one-time 32-byte challenge and an optional label/present/wallet200 characters
d1.Disclosure codeA holder-signed disclosure for that challenge (a disclosure file in bytes)/wallet (Sign with my wallet)/present8,192 characters

Common rules​

  • Text form: <prefix>.<base64url>, using the URL-safe alphabet without padding. Decoding is strict: the text must be the canonical encoding of its bytes, so one set of bytes has one spelling.
  • Numbers are unsigned and big-endian: u8, u16, u32, u64.
  • Fixed values (addresses, hashes, salts) are raw bytes of their exact size: 20 bytes for an address, 32 for a hash or salt.
  • Blobs (signatures, text) are a u16 length followed by that many bytes, each with its own cap.
  • The first byte of every code is the version, 1. Another value gives "This code was made by another version of the app."
  • A reader must reach the end exactly. Leftover bytes give "This code has extra data at the end."; missing bytes give "This code is cut short."
  • Every code assumes the Certichain signing domain (Certichain, version 1), so the domain name and version are not stored.

A code travels after the # of a link, which browsers never send to the server:

  • /wallet#v1.... adds a credential; /wallet#c1.... opens a challenge to sign.
  • /present#v1.... checks a credential; /present#d1.... checks a signed answer.

parseFragment in src/lib/present/url.ts accepts at most 16,384 characters of the form ^([a-z]\d)\.[A-Za-z0-9_-]+$ and only the three known prefixes. Anything else gives "This link does not hold a code this app knows."

v1. static code​

OrderFieldEncodingNotes
1versionu81
2flagsu8bit 0 (1): details follow. Bit 1 (2): selective disclosure data follows (only with bit 0). Other bits refused.
3chainIdu64
4verifyingContract20 bytesThe CredentialRegistry
5issuer20 bytes
6subjectCommitment32 bytes
7documentHash32 bytes
8metadataHash32 bytes
9recordTypeu32
10issuedAtu64Unix seconds (must fit a safe JavaScript integer)
11validUntilu640 for no expiry
12nonce32 bytesThe uint256 nonce, big-endian
13signatureblob, up to 512 bytesThe issuer's signature (65 bytes for a plain account, longer for ERC-1271)

The credential id is not stored. The reader recomputes it as the EIP-712 digest of fields 3 to 12, so a code cannot claim one id and carry another credential's fields.

With flag bit 0:

OrderFieldEncodingNotes
14subjectSalt32 bytes
15metadataSalt32 bytes
16displayblob, up to 1,024 bytesUTF-8 JSON of the display block (version, holderName, studentId, optional programme, award, awardedOn), validated on read

With flag bit 1 as well:

OrderFieldEncodingNotes
17attributes versionu32
18salts8 times 32 bytesOne per attribute slot
19root32 bytesMust equal the root recomputed from the display and the salts
20attestationblob, up to 512 bytesThe issuer's AttributeSet signature

The wallet stores codes with selective disclosure data so it can sign later. The code it shows to a verifier never includes that data.

c1. challenge code​

OrderFieldEncodingNotes
1versionu81
2challenge32 bytesRandom, made by DisclosureService.newChallenge
3labelthe remaining bytes, 0 to 60Optional UTF-8 name of the verifier. Control characters, zero-width characters, line separators and bidirectional marks are refused.

The check code shown on both screens is challengeFingerprint: the first four bytes of the challenge as lowercase hex in two groups, for example 7777 7777. A holder who sees a different check code than the verifier's screen is looking at a relayed challenge and should not sign.

d1. disclosure code​

OrderFieldEncodingNotes
1versionu81
2chainIdu64
3verifyingContract20 bytes
4credentialId32 bytes
5issuer20 bytes
6attributesRoot32 bytes
7attributesVersionu32
8attestationblob, up to 512 bytesIssuer's AttributeSet signature
9challenge32 bytesThe verifier's challenge
10holder address20 bytes
11holder signatureblob, up to 512 bytesDisclosureRequest signature
12field countu81 to 8
13each fieldu8 index, blob value (1 to 1,500 bytes), 32-byte salt, three 32-byte siblingsIn slot order, each slot once

The field names are not stored; they come from the slot index. The decoder rebuilds a disclosure file and runs it through parseDisclosure, so every rule of the disclosure file applies. A code over 8,192 characters is refused at creation with "The chosen details are too large for one code. Choose fewer."

Examples​

These are the frozen version 1 vectors from src/lib/present/vectors.test.ts. They must keep decoding to the same facts and encoding back to the same text.

Challenge code with challenge 0x7777...77 and label "Admissions desk":

c1.AXd3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3QWRtaXNzaW9ucyBkZXNr

Static code without details (chain id 0xaa36a7, 11155111; record type 4; nonce 7; issued by a Hardhat test account):

v1.AQAAAAAAAKo2p1P7VoIuUHuvXAIh2ZMQhJXOGTHqGefjdufCE7fn5-Rsxwpd0Iba_yrsCtb6VM_dtoraOIYR0ZIJ0SPpM-TpG1X4qWTEDP5ixwICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICvLEaExrkiUGmvx-KeIFW7bQooyx6DIpASwhhpMgpgs8AAAAEAAAAAGtJ0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAEFZbdtiGQBgymUAXIba_ZYrO94laaNYDTeL20Xg9TjwXiADLyQGTCtR-_xgrW9ppU6fn8kEMcUrAurD6LRjD3JqGw

Reading the first bytes: 01 (version), 00 (no flags), 0000000000aa36a7 (chain id), then the 20-byte registry.

The same vector file also holds a static code with the details of "Asha Rai" (S-1001, Computer Science, BSc) and a disclosure code that shows only the programme.

How it is checked​

  1. openCode: size cap, prefix, strict base64url.
  2. The decoder reads every field with its size and cap, refuses unknown flags and leftover bytes, and checks addresses with getAddress.
  3. Static codes: readSignedFields validates the signed fields, the credential id is recomputed, details are validated with validateDisplay, the attribute root is recomputed.
  4. Disclosure codes: rebuilt into a disclosure file and parsed with parseDisclosure.
  5. Then the normal checks run: PresentationService.checkStatic for v1., PresentationService.checkDisclosureCode (which uses DisclosureService.verify) for d1..

Source files​

  • src/lib/present/bytes.ts (ByteWriter, ByteReader, openCode, closeCode)
  • src/lib/present/static-code.ts, src/lib/present/challenge-code.ts, src/lib/present/disclosure-code.ts
  • src/lib/present/url.ts (walletUrl, presentUrl, parseFragment)
  • src/lib/doc-crypto/encoding.ts (toBase64Url, fromBase64Url)

Tests​

  • src/lib/present/vectors.test.ts (frozen version 1 codes)
  • src/lib/present/static-code.test.ts, challenge-code.test.ts, disclosure-code.test.ts, url.test.ts