QR Code
Veylent embeds a QR code alongside each signature block. The QR encodes a verification URL that links the physical document to its blockchain anchor and carries the key to its sealed signing record.
URL Format
HTTPS://VERIFY.GETVEYLENT.COM/V/<anchor ID, 40 digits>#<key, 40 digits>
└──── path param ────┘ └── fragment ──┘
| Segment | Encoding | Size | Purpose |
|---|---|---|---|
anchorId | 40 decimal digits | 16 bytes | Identifies the signature on the blockchain |
key | 40 decimal digits | 16 bytes | AES-128-GCM key that opens the sealed signing record |
The #fragment is never sent to any server (RFC 3986). The decryption key stays entirely client-side.
The anchor ID and key are random values generated before anything is stamped. That is what allows the QR code to be placed on the page in the same layer as the signature, and to be covered by the document hash. The base URL is set at build time with VEYLENT_QR_BASE_URL, so self-hosted deployments encode their own verification site.
Digit Encoding
Each 16-byte value is split into two big-endian 8-byte halves, and each half is written as a zero-padded 20-digit decimal number (u64::MAX is 20 digits). Fixed width keeps leading zero bytes, and a verifier rejects any part that isn't exactly 40 digits or whose halves don't fit in 64 bits.
The whole link is upper case. Scheme and host are case-insensitive, and every verifier accepts /V/ and /v/.
Why Digits and Upper Case
A QR code can switch between encoding modes inside one code, and two of them are much denser than plain bytes:
| Mode | Characters | Bits per character |
|---|---|---|
| Numeric | 0-9 | 3.33 |
| Alphanumeric | 0-9 A-Z $%*+-./: and space | 5.5 |
| Byte | anything | 8 |
The digits go in numeric mode, and the upper-case HTTPS://VERIFY.GETVEYLENT.COM/V/ goes in alphanumeric mode. Only the # needs byte mode. Numeric mode stores the 32 bytes of anchor ID and key in about 267 bits, close to the 256-bit minimum, so tighter packing gains nothing. What remains is the base URL.
Example URL Breakdown
HTTPS://VERIFY.GETVEYLENT.COM/V/0008198552921648689500081985529216486895#0108510259257115009501085102592571150095
├── base URL (29) ──────────┤ ├── anchor ID (40) ───────────────────┤ ├── key (40) ─────────────────────────┤
Total URL length: 113 characters, which the QR code stores in about 505 bits.
Earlier Links
Links from earlier builds used lower case and base64url parts: https://verify.getveylent.com/v/<base64url(anchorId)>#<base64url(key)>. Verifiers still accept them. A 40-digit part and a 22-character base64url part can't be confused.
Version & Error Correction
QR codes have 40 versions (1--40) and 4 error correction levels. Higher versions hold more data but produce denser codes. Higher EC levels tolerate more damage but reduce capacity.
Veylent uses EC level M (15% damage tolerance, suitable for printed documents) and lets the qrcode crate pick the smallest version that fits:
QrCode::with_error_correction_level(url.as_bytes(), EcLevel::M)
| Link | EC level | Version | Modules |
|---|---|---|---|
| Digits, upper case (current) | M | V4 | 33x33 |
| Digits, lower-case URL | M | V5 | 37x37 |
| Base64url, lower case (earlier) | M | V6 | 41x41 |
V4/M holds 64 data codewords (512 bits), so the default link has a few bits to spare. A longer self-hosted base URL moves up to V5 (37x37) automatically.
Quiet Zone
The QR specification requires a 4-module quiet zone (blank padding) around the QR pattern. The qrcode crate includes this in the rendered image, so the fraction depends on the version:
let quiet_zone_frac = 4.0 / (code.width() + 8) as f32; // V4: 4 / 41 ≈ 9.8%
let quiet_zone = qr_size * quiet_zone_frac;
let inner_qr = qr_size - 2.0 * quiet_zone;
This positions the branding text against the QR pattern itself. The signature placer's preview uses the V4 fraction, 4/41.
Minimum Size
The QR code, quiet zone included, is rendered at a minimum of 34 PDF points (~12mm).
| Image modules | Min size (pt) | Pattern size | Module size | Pixels at 300 DPI |
|---|---|---|---|---|
| 41 (V4 + quiet zone) | 34 | ~9.7mm | 0.83 pt (~0.29mm) | ~3.5 px/module |
Phone cameras read modules of about 0.33mm reliably, and printed copies that may be photocopied are safer at 0.4mm or more. The 34pt minimum is below both, so short signature blocks give the smallest QR codes the least margin.
Rendering
The QR image is rendered as a grayscale (DeviceGray) PDF XObject:
- Generate QR using the
qrcodecrate at 570x570 pixels (high resolution for clean scaling) - Flip horizontally (compensates for PDF coordinate system interactions)
- Compress with zlib (FlateDecode)
- Add as a single XObject, referenced by all placement content streams
Branding Text
Below the QR code, "Signed with Veylent" is rendered in light gray Helvetica, right-aligned with the QR pattern edge (accounting for quiet zone offset).