1. Protocol Overview
RefPassport 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 reference content invalidates the signature.
- •Decentralised trust: verification relies on DNS, not a central certificate authority.
- •Zero-knowledge: the server never has access to plaintext signing keys.
- •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 RefPassport 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 "refpassport1" 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. The server stores only the ciphertext and can never decrypt the private key without the password (zero-knowledge architecture).
| 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, a unique nonce, and the issuer’s domain and key selector. This makes each reference self-contained — a verifier can extract the payload and know exactly where to look up the public key via DNS.
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 one of three trust levels.
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: 'silver', reason: 'No DNS record found' };
publicKeyBase64 = match.match(/p=([A-Za-z0-9+/=]+)/)?.[1];
if (!publicKeyBase64) return { status: 'silver', reason: 'Malformed DNS record' };
} catch {
return { status: 'silver', 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
Verification produces one of three trust levels, determined by the combination of signature validity and DNS record presence.
Signature valid and DNS record found. Highest trust.
Signature valid, DNS record not found. May indicate DNS misconfiguration or propagation delay.
Signature failed, or reference is revoked or expired. Do not trust.
| Condition | Trust Level |
|---|---|
| Signature valid + DNS record matches | Gold |
| Signature valid + DNS record not found | Silver |
| Signature invalid | Invalid |
| Reference revoked by issuer | Invalid |
| Reference past expiry date | Invalid |
8. PDF Proof Format
RefPassport generates a branded PDF document that serves as a portable proof of the reference. The PDF embeds all the data needed for verification in its metadata, and includes a QR code linking to the online verification page.
Visual Structure
Embedded Metadata
The following data is embedded in the PDF’s document properties. Together, these fields contain everything needed for fully offline verification — no server or database required:
| Field | Type | Description |
|---|---|---|
| Title | String | "Reference Letter - {candidateName}" |
| Subject | JSON | The full canonical JSON payload (includes domain and selector). |
| Creator | String | "RefPassport" |
| 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 reference is verifiable without it.
Tamper Detection
The SHA-256 hash of the canonical payload is embedded in the PDF metadata as a keyword. A verifier can recompute the hash from the extracted payload and compare it against the embedded value to detect any metadata corruption.
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 | Private key is AES-256-GCM encrypted with user password. Server cannot decrypt. |
| DNS hijacking | Degrades to Silver trust level. DNSSEC recommended for additional protection. |
| Replay attack | UUID v4 nonce produces unique signatures for identical content. |
| PDF tampering | SHA-256 hash comparison detects any modification to the document. |
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 degrade to Silver, not Gold. Offline signature verification is possible.
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 verification link and/or PDF with the candidate.
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 (full trust), Silver (DNS not found), Invalid (signature failed).
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 implement?
Try the interactive demo to see the full signing and verification flow in action, or register your domain to start issuing references.