Appearance
Cryptographic Signing with Ed25519
FreshARP v1.2 introduced optional cryptographic signing using the Ed25519 algorithm. Signing your reasoning.json allows AI systems to verify that the file was genuinely published by the domain owner - the same trust model used by DKIM for email authentication.
This SOP walks through the complete signing procedure.
Prerequisites
- A complete, valid
reasoning.jsonfile ready to sign - Node.js 18+ or a browser that supports the Web Crypto API
- Access to your domain's DNS management
When to Sign
Cryptographic signing is optional but recommended if:
- Your entity has been a target of misinformation or impersonation
- You want AI systems to give your corrections higher trust weight
- You are building AI integrations where provenance matters
- You want to demonstrate proactive security posture
Step 1 - Generate an Ed25519 Keypair
Ed25519 is a modern elliptic curve signature algorithm. It produces compact keys and signatures and is well-supported across platforms.
Option A - Using Node.js
javascript
// generate-arp-keypair.js
const { generateKeyPairSync } = require('crypto');
const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
publicKeyEncoding: { type: 'spki', format: 'der' }
});
const privateKeyHex = privateKey.toString('hex');
const publicKeyHex = publicKey.toString('hex');
console.log('Public key (hex):', publicKeyHex);
console.log('Private key (hex):', privateKeyHex);
console.log('');
console.log('SAVE THE PRIVATE KEY SECURELY. Do not commit it to version control.');
console.log('Publish the PUBLIC key in DNS. Keep the private key secret.');Run with: node generate-arp-keypair.js
Option B - Using @noble/ed25519 (browser or Node)
javascript
import * as ed from '@noble/ed25519';
const privateKey = ed.utils.randomPrivateKey();
const publicKey = await ed.getPublicKeyAsync(privateKey);
const privateKeyHex = Buffer.from(privateKey).toString('hex');
const publicKeyHex = Buffer.from(publicKey).toString('hex');
console.log('Public key (hex):', publicKeyHex);
console.log('Private key (hex):', privateKeyHex);Step 2 - Canonicalize the JSON with JCS
Before signing, the JSON must be converted to a canonical byte representation. This ensures that two different serializations of the same JSON object (different key ordering, different whitespace) produce the same bytes to sign.
ARP uses JCS (JSON Canonicalization Scheme, RFC 8785) for canonicalization.
javascript
// Using the 'canonicalize' package (npm install canonicalize)
const canonicalize = require('canonicalize');
// Load your reasoning.json WITHOUT the _arp_signature field
const reasoningJson = require('./reasoning.json');
// Remove _arp_signature if present from a previous signing
delete reasoningJson._arp_signature;
// Produce canonical JSON bytes
const canonical = canonicalize(reasoningJson);
const canonicalBytes = Buffer.from(canonical, 'utf8');The canonical form has no extra whitespace, sorted object keys, and deterministic number encoding. It is not human-readable, but it is byte-for-byte identical regardless of how the original JSON was formatted.
Step 3 - Sign the Canonical Bytes
javascript
const { createPrivateKey, sign } = require('crypto');
// Load your hex-encoded private key
const privateKeyHex = process.env.ARP_PRIVATE_KEY;
const privateKeyDer = Buffer.from(privateKeyHex, 'hex');
const privateKeyObj = createPrivateKey({
key: privateKeyDer,
format: 'der',
type: 'pkcs8'
});
const signature = sign(null, canonicalBytes, privateKeyObj);
const signatureHex = signature.toString('hex');
console.log('Signature (hex):', signatureHex);Step 4 - Append the _arp_signature Object
Add the _arp_signature block to your reasoning.json. This must be the last property in the root object.
json
"_arp_signature": {
"algorithm": "Ed25519",
"publicKey": "302a300506032b6570032100...",
"signature": "a1b2c3d4e5f6...",
"signedAt": "2026-04-15T10:30:00Z",
"canonicalization": "JCS-RFC8785",
"dns_selector": "arp2026"
}| Field | Description |
|---|---|
algorithm | Always "Ed25519" for v1.2 |
publicKey | Hex-encoded public key (SPKI DER format) |
signature | Hex-encoded signature of the canonical JSON bytes |
signedAt | ISO 8601 timestamp of when the file was signed |
canonicalization | Always "JCS-RFC8785" for v1.2 |
dns_selector | The DNS selector name you will use for the TXT record |
Signing Sequence Diagram
sequenceDiagram
participant O as Domain Owner
participant G as Key Generator
participant C as Canonicalizer (JCS)
participant S as Signer (Ed25519)
participant DNS as DNS Provider
O->>G: Generate Ed25519 keypair
G-->>O: privateKey (hex), publicKey (hex)
O->>O: Write reasoning.json (no _arp_signature yet)
O->>C: Canonicalize JSON bytes (RFC 8785)
C-->>O: canonicalBytes
O->>S: sign(canonicalBytes, privateKey)
S-->>O: signatureHex
O->>O: Append _arp_signature block with publicKey + signature
O->>O: Deploy updated reasoning.json to /.well-known/
O->>DNS: Publish TXT record at selector._arp.domain
DNS-->>O: TXT record live
Note over O,DNS: AI agents can now verify domain ownershipStep 5 - Publish the DNS TXT Record
See the DNS Verification SOP for the exact DNS TXT record format. The short version: publish your public key as a TXT record at `{selector}._arp.{domain}`.
Step 6 - Keep the Private Key Secure
The private key signs your reasoning.json content. If it is compromised, an attacker can sign modified versions of your file. Treat it like any other signing credential:
- Store in a secrets manager (AWS Secrets Manager, Doppler, 1Password Secrets Automation), not in your codebase
- Pass as an environment variable during the signing step
- Rotate annually or whenever you suspect compromise
- Rotation means generating a new keypair, updating the DNS record, and re-signing your file
Updating a Signed File
When you update your reasoning.json content:
- Make your content changes
- Remove the existing
_arp_signatureblock - Canonicalize the updated content
- Sign with your private key
- Append the new
_arp_signaturewith the updatedsignedAttimestamp - Deploy the updated file
The public key and DNS record do not change unless you rotate the keypair.
Complete Signing Script
javascript
// sign-reasoning.js
// Usage: ARP_PRIVATE_KEY=<hex> node sign-reasoning.js
const { createPrivateKey, sign } = require('crypto');
const canonicalize = require('canonicalize');
const fs = require('fs');
const privateKeyHex = process.env.ARP_PRIVATE_KEY;
if (!privateKeyHex) {
console.error('ARP_PRIVATE_KEY environment variable required');
process.exit(1);
}
const reasoningJson = JSON.parse(fs.readFileSync('./reasoning.json', 'utf8'));
// Remove existing signature if present
delete reasoningJson._arp_signature;
// Canonicalize
const canonical = canonicalize(reasoningJson);
const canonicalBytes = Buffer.from(canonical, 'utf8');
// Sign
const privateKeyDer = Buffer.from(privateKeyHex, 'hex');
const privateKeyObj = createPrivateKey({ key: privateKeyDer, format: 'der', type: 'pkcs8' });
const signature = sign(null, canonicalBytes, privateKeyObj);
// Get public key from private key
const { createPublicKey } = require('crypto');
const publicKeyObj = createPublicKey(privateKeyObj);
const publicKeyDer = publicKeyObj.export({ type: 'spki', format: 'der' });
// Append signature block
reasoningJson._arp_signature = {
algorithm: 'Ed25519',
publicKey: publicKeyDer.toString('hex'),
signature: signature.toString('hex'),
signedAt: new Date().toISOString(),
canonicalization: 'JCS-RFC8785',
dns_selector: process.env.ARP_DNS_SELECTOR || 'arp2026'
};
fs.writeFileSync('./reasoning.json', JSON.stringify(reasoningJson, null, 2), 'utf8');
console.log('Signed successfully. Deploy reasoning.json to /.well-known/');