1. Protocol Overview
Work References enables employers to issue cryptographically signed employment references that candidates can carry with them throughout their career. Any third party (a recruiter, HR team, or hiring manager) can verify the authenticity of a reference instantly, without contacting the issuing company or creating an account.
Actors
The employer who creates and signs a reference. Publishes their public key in DNS and signs references with their private key.
The employee who receives the signed reference. Carries it as a PDF or shareable link and presents it to future employers.
Any third party (recruiter, HR) who verifies a reference. Checks the signature against the issuer’s public key via DNS.
Protocol Flow
Design Goals
- •Portability: references travel with the candidate, not locked to any platform.
- •Tamper-evidence: any modification to the signed payload invalidates the signature; visible PDF edits are not checked.
- •Decentralised trust: verification relies on DNS, not a central certificate authority.
- •Browser tools: WorkReferences.org generates keys and signs in the browser. RefPassport has different setup and reset behavior.
- •Simplicity: inspired by DKIM, the protocol uses well-understood standards (Ed25519, DNS TXT, JSON).
2. DNS Record Specification
The issuer’s Ed25519 public key is published as a DNS TXT record under a well-known subdomain. This is the anchor that allows anyone to independently verify a reference without trusting Work References itself.
Record Format
Name: {SELECTOR}._workreferences.{DOMAIN}
Type: TXT
Value: v=workreferences1; k=ed25519; p={BASE64_PUBLIC_KEY}Field Definitions
| Field | Type | Description |
|---|---|---|
| SELECTOR | String | Alphanumeric key selector (e.g. REF1, 2025Q1). Max 63 characters. Case-insensitive. Allows key rotation. |
| _workreferences | Fixed | Protocol-reserved subdomain prefix. Must appear exactly as shown. |
| DOMAIN | String | The issuer's domain (e.g. example.com). Must be a domain they control. |
| v | String | Protocol version. Must be "workreferences1" for this version of the standard. |
| k | String | Key algorithm. Must be "ed25519" in v1. Future versions may support additional algorithms. |
| p | Base64 | The Ed25519 public key, base64-encoded. 32 bytes (44 characters in base64). |
Example
REF1._workreferences.acmecorp.com. 3600 IN TXT "v=workreferences1; k=ed25519; p=Ky2hL9xR4bT...44chars...="Registrar Examples
REF1._workreferencesv=workreferences1; k=ed25519; p=...REF1._workreferencesv=workreferences1; k=ed25519; p=...REF2). Keep old records active so previously-issued references remain verifiable.3. Cryptographic Primitives
Ed25519 Digital Signatures
All references are signed using Ed25519 (Edwards-curve Digital Signature Algorithm on Curve25519). Ed25519 provides strong security guarantees with compact keys and fast operations.
| Field | Type | Description |
|---|---|---|
| Algorithm | Ed25519 | Elliptic curve digital signature scheme (RFC 8032) |
| Public Key | 32 bytes | Base64-encoded (44 characters). Published in DNS. |
| Private Key | 64 bytes | Base64-encoded (88 characters). Contains seed + public key. Never leaves the client. |
| Signature | 64 bytes | Base64-encoded (88 characters). Detached signature over the canonical payload. |
| Encoding | Base64 | Standard base64 with padding (tweetnacl-util). |
import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
const keyPair = nacl.sign.keyPair();
const publicKey = encodeBase64(keyPair.publicKey); // 44 chars
const privateKey = encodeBase64(keyPair.secretKey); // 88 charsAES-256-GCM (Private Key Encryption)
Private keys are encrypted at rest using AES-256-GCM with a key derived from the user’s password. Key custody depends on the implementation. The free signing tool uses keys in the browser; RefPassport setup and reset receive the password and generate the key on its server before encryption.
| Field | Type | Description |
|---|---|---|
| Cipher | AES-256-GCM | Authenticated encryption (confidentiality + integrity). |
| Key Derivation | PBKDF2 | SHA-256, 100,000 iterations. |
| Salt | 16 bytes | Random, base64-encoded. Unique per encryption. |
| IV / Nonce | 12 bytes | Random, base64-encoded. Recommended size for GCM. |
SHA-256 Hashing
SHA-256 is used for two purposes: (1) as the hash function within PBKDF2 key derivation, and (2) for computing integrity hashes of reference payloads and PDF documents. Hashes are represented as lowercase hexadecimal strings.
4. Payload Format
The reference payload is a JSON object containing the reference data and a unique nonce. This is the exact data that gets signed.
JSON Structure
{
"name": "James Holloway",
"role": "Baggage Handler",
"dates": "June 2021 – December 2024",
"text": "James was a reliable and hardworking member of our airside baggage team...",
"nonce": "550e8400-e29b-41d4-a716-446655440000",
"domain": "heathrow-gh.com",
"selector": "REF1"
}Field Specifications
| Field | Type | Description |
|---|---|---|
| name | String (max 200) | Full name of the candidate. |
| role | String (max 200) | Job title or position held. |
| dates | String (max 100) | Employment period in free-form text (e.g. "Jan 2020 – Dec 2024"). |
| text | String (max 10,000) | The reference letter content. Newlines are preserved. |
| nonce | UUID v4 | Unique identifier. Prevents replay attacks and ensures signature uniqueness. |
| domain | String | The issuer’s domain (e.g. "acme.com"). Used for DNS public key lookup. |
| selector | String | DNS key selector (e.g. "REF1"). Maps to {selector}._workreferences.{domain}. |
Canonical Serialisation
To ensure that both the signer and verifier produce identical byte representations, the payload is serialised with alphabetically sorted keys:
function normalizeJson(obj) {
return JSON.stringify(obj, Object.keys(obj).sort());
}
// Result (keys sorted alphabetically):
// {"dates":"June 2021...","domain":"heathrow-gh.com","name":"James Holloway","nonce":"550e8400...","role":"Baggage...","selector":"REF1","text":"James was..."}5. Signing Process
Signing happens entirely on the client side. The private key is decrypted in the browser using the user’s password, used to sign, and then discarded from memory. The plaintext private key is never sent to or stored on the server.
Steps
Code Example
import nacl from 'tweetnacl';
import { encodeBase64, decodeBase64 } from 'tweetnacl-util';
function signReference(payload, privateKeyBase64) {
// 1. Canonical serialisation (sorted keys)
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
// 2. Encode to bytes
const payloadBytes = new TextEncoder().encode(canonical);
const privateKeyBytes = decodeBase64(privateKeyBase64);
// 3. Sign (detached: signature does not contain the message)
const signatureBytes = nacl.sign.detached(payloadBytes, privateKeyBytes);
// 4. Return base64-encoded signature
return encodeBase64(signatureBytes); // 88 characters
}6. Verification Process
Verification requires no central server or database. The reference document (PDF) contains everything needed: the signed payload, the signature, and the issuer’s domain and key selector. A verifier extracts this data, looks up the public key via DNS, and checks the signature. The result is Gold or Invalid.
Algorithm
Full Verification (Decentralised)
import nacl from 'tweetnacl';
import { decodeBase64 } from 'tweetnacl-util';
import dns from 'dns/promises';
async function verifyReference(payload, signatureBase64) {
// 1. Extract domain and selector from the payload itself
const { domain, selector } = payload;
// 2. Look up the public key via DNS
const recordName = `${selector}._workreferences.${domain}`;
let publicKeyBase64;
try {
const records = await dns.resolveTxt(recordName);
const flat = records.map(r => r.join(''));
const match = flat.find(r => r.startsWith('v=workreferences1;'));
if (!match) return { status: 'invalid', reason: 'No DNS record found' };
publicKeyBase64 = match.match(/p=([A-Za-z0-9+/=]+)/)?.[1];
if (!publicKeyBase64) return { status: 'invalid', reason: 'Malformed DNS record' };
} catch {
return { status: 'invalid', reason: 'DNS lookup failed' };
}
// 3. Verify the signature against the DNS public key
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
const payloadBytes = new TextEncoder().encode(canonical);
const signatureBytes = decodeBase64(signatureBase64);
const publicKeyBytes = decodeBase64(publicKeyBase64);
const valid = nacl.sign.detached.verify(payloadBytes, signatureBytes, publicKeyBytes);
return valid
? { status: 'gold', reason: 'Signature valid, DNS verified' }
: { status: 'invalid', reason: 'Signature verification failed' };
}7. Trust Levels
The stateless verifier returns Gold only when the payload signature verifies against a DNS key. Otherwise it returns Invalid. It does not check expiry or revocation.
Payload signature valid against the DNS key. This does not establish current reference status or visible PDF integrity.
Silver is not returned by this stateless verifier. Without a DNS key, it cannot establish signature validity.
DNS key missing or signature failed. Authenticity cannot be established. Revocation and expiry are not checked.
| Condition | Trust Level |
|---|---|
| Signature valid + DNS record matches | Gold |
| DNS record not found | Invalid |
| Signature invalid | Invalid |
| Reference revoked by issuer | Not checked |
| Reference past expiry date | Not checked |
8. PDF Proof Format
Work References generates a branded PDF document that serves as a self-contained, portable proof of the reference. The PDF embeds the full signed payload and signature in its metadata — everything a verifier needs to check authenticity using only DNS.
Visual Structure
Embedded Metadata
The following data is embedded in the PDF’s document properties. Together, these fields carry the signed payload. This website uploads the PDF for extraction and uses a live DNS lookup; it does not provide fully offline verification:
| Field | Type | Description |
|---|---|---|
| Title | String | "Reference Letter - {candidateName}" |
| Subject | JSON | The full canonical JSON payload (includes domain and selector). |
| Creator | String | "Work References" |
| Keywords[0] | JSON | Full canonical JSON payload (redundant copy). |
| Keywords[1] | String | "payload-hash:{SHA256_HEX}": SHA-256 hash of the canonical payload. |
| Keywords[2] | String | "signature:{BASE64}": the Ed25519 signature. |
QR Code
Implementations MAY include a QR code linking to an online verification page for convenience. However, the PDF metadata is the authoritative source — the QR code is optional and the signed payload can be extracted without it; this verifier still requires a DNS lookup.
Tamper Detection
The verifier checks the signature of the embedded payload. It does not compare the visible PDF text or layout with that payload, and does not validate the returned payload-hash field. Compare the displayed verified payload with the document; changing the visible PDF while preserving its metadata is not detected by this tool.
9. Security Properties
Guarantees
Threat Model
| Attack | Defence |
|---|---|
| Forged reference | Ed25519 signature verification fails without the correct private key. |
| Modified content | Any change to the payload invalidates the detached signature. |
| Stolen private key from server | Encrypted storage protects keys at rest. RefPassport setup/reset still handle plaintext keys and passwords on the server. |
| DNS hijacking | DNS is a trust dependency. Compromise may substitute a key; DNSSEC validation is not implemented by this verifier. |
| Replay attack | UUID v4 nonce produces unique signatures for identical content. |
| PDF tampering | Payload signature detects changes to signed metadata, not changes to visible PDF contents. |
Non-Goals
- •Anonymity: references are explicitly linked to identifiable domains.
- •Confidentiality: reference content is not encrypted in transit. Once shared, it is readable by any recipient.
- •Guaranteed availability: DNS lookup failures return Invalid. This tool needs DNS and does not offer offline verification.
10. Implementation Notes
For Issuers
- Generate an Ed25519 key pair on the client.
- Encrypt the private key with a strong password (PBKDF2, 100k iterations, AES-256-GCM).
- Publish the public key as a DNS TXT record at
{SELECTOR}._workreferences.{DOMAIN}. - Wait for DNS propagation (typically 15 minutes to 48 hours).
- Sign references client-side. Never send the plaintext private key to the server.
- Generate a PDF with the payload, signature, domain, and selector embedded in metadata.
- Share the PDF with the candidate. The document is self-contained proof.
For Verifiers
- Receive a PDF from the candidate.
- Extract the payload and signature from the PDF metadata.
- Look up the public key via DNS using the domain and selector from the payload.
- Verify the Ed25519 signature. Check the trust level: Gold (signature valid against DNS), Invalid (missing DNS or failed signature). Revocation and expiry are not checked.
Best Practices
Recommended Libraries
| Field | Type | Description |
|---|---|---|
| tweetnacl | JavaScript | Ed25519 key generation, signing, and verification. |
| tweetnacl-util | JavaScript | Base64 encoding/decoding for keys and signatures. |
| pdf-lib | JavaScript | PDF generation with metadata embedding. |
| qrcode | JavaScript | QR code generation for verification URLs. |
| dns.promises | Node.js | DNS TXT record resolution for domain verification. |
| Web Crypto API | Browser/Node | AES-256-GCM encryption and PBKDF2 key derivation. |
Ready to get started?
Try the interactive demo to see the full signing and verification flow in action, or generate your keys to start issuing references.