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}._refpassport.{DOMAIN}
Type: TXT
Value: v=refpassport1; 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. |
| _refpassport | 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._refpassport.acmecorp.com. 3600 IN TXT "v=refpassport1; k=ed25519; p=Ky2hL9xR4bT...44chars...="Registrar Examples
REF1._refpassportv=refpassport1; k=ed25519; p=...REF1._refpassportv=refpassport1; 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 and a unique nonce. This is the exact data that gets signed.
JSON Structure
{
"name": "Sarah Chen",
"role": "Senior Software Engineer",
"dates": "March 2021 – January 2025",
"text": "Sarah was an outstanding member of our engineering team...",
"nonce": "550e8400-e29b-41d4-a716-446655440000"
}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. |
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":"March 2021...","name":"Sarah Chen","nonce":"550e8400...","role":"Senior...","text":"Sarah 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 is a multi-step process that checks the reference’s integrity, authenticity, and current status. The result is one of three trust levels.
Algorithm
Signature Verification
import nacl from 'tweetnacl';
import { decodeBase64 } from 'tweetnacl-util';
function verifyReference(payload, signatureBase64, publicKeyBase64) {
// 1. Reconstruct canonical form
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
// 2. Decode inputs
const payloadBytes = new TextEncoder().encode(canonical);
const signatureBytes = decodeBase64(signatureBase64);
const publicKeyBytes = decodeBase64(publicKeyBase64);
// 3. Verify
return nacl.sign.detached.verify(payloadBytes, signatureBytes, publicKeyBytes);
}DNS Verification
import dns from 'dns/promises';
async function verifyDns(domain, selector, expectedPublicKey) {
const recordName = `${selector}._refpassport.${domain}`;
const expectedValue = `v=refpassport1; k=ed25519; p=${expectedPublicKey}`;
const records = await dns.resolveTxt(recordName);
const flat = records.map(r => r.join('')); // TXT records may be multi-string
return flat.some(record => record === expectedValue);
}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 for machine-readable verification:
| Field | Type | Description |
|---|---|---|
| Title | String | "Reference Letter - {candidateName}" |
| Subject | JSON | The full canonical JSON payload. |
| 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. |
| Keywords[3] | String | "pdf-hash:{SHA256_HEX}": SHA-256 hash of the final PDF bytes. |
QR Code
The QR code encodes the URL https://refpassport.com/verify/{REFERENCE_ID}. Scanning the QR code takes the verifier directly to the verification page.
Tamper Detection
The SHA-256 hash of the PDF bytes is stored in the database. A verifier can upload the PDF to check whether SHA256(uploadedPdf) === storedPdfHash. If the hashes differ, the PDF has been modified after issuance.
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}._refpassport.{DOMAIN}. - Wait for DNS propagation (typically 15 minutes to 48 hours).
- Sign references client-side. Never send the plaintext private key to the server.
- Store the signature and payload in the database, generate a PDF with a QR code.
- Share the verification link and/or PDF with the candidate.
For Verifiers
- Receive a verification link or PDF from the candidate.
- Navigate to the verification page or perform programmatic verification.
- Check the trust level: Gold (full trust), Silver (partial), Invalid (reject).
- Optionally upload the PDF to verify its SHA-256 hash against the stored value.
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.