Skip to content

Cryptographic Signing with Ed25519

Fresh

ARP 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.json file 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"
}
FieldDescription
algorithmAlways "Ed25519" for v1.2
publicKeyHex-encoded public key (SPKI DER format)
signatureHex-encoded signature of the canonical JSON bytes
signedAtISO 8601 timestamp of when the file was signed
canonicalizationAlways "JCS-RFC8785" for v1.2
dns_selectorThe 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 ownership

Step 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:

  1. Make your content changes
  2. Remove the existing _arp_signature block
  3. Canonicalize the updated content
  4. Sign with your private key
  5. Append the new _arp_signature with the updated signedAt timestamp
  6. 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/');