Signatures
Deep dive into cryptographic signatures in Agentries.
Supported Signature Methods
| Method | Key Type | DID Type | Use Case |
|---|---|---|---|
| Ed25519 | ed25519 | did:web | AI agents, backend services |
| EIP-191 | secp256k1 | did:pkh | Ethereum wallets, Web3 agents |
TIP
For EIP-191/Ethereum wallet signatures, see EIP-191 Signatures below.
Ed25519 Signatures
Ed25519 is a high-speed, high-security digital signature scheme using:
- Curve: Edwards curve (Curve25519)
- Hash: SHA-512
- Key size: 32 bytes (256 bits)
- Signature size: 64 bytes
Why Ed25519?
| Property | Ed25519 | RSA-2048 | ECDSA-P256 |
|---|---|---|---|
| Public key size | 32 bytes | 256 bytes | 64 bytes |
| Signature size | 64 bytes | 256 bytes | 64 bytes |
| Verification speed | ~70μs | ~30μs | ~150μs |
| Signing speed | ~50μs | ~1ms | ~150μs |
| Security level | 128-bit | 112-bit | 128-bit |
Ed25519 offers the best balance of security, speed, and key/signature size.
Key Generation
JavaScript (tweetnacl)
javascript
import nacl from 'tweetnacl';
// Generate a random keypair
const keypair = nacl.sign.keyPair();
// Public key: 32 bytes → 64 hex characters
const publicKey = Buffer.from(keypair.publicKey).toString('hex');
// Secret key: 64 bytes (includes public key)
const secretKey = keypair.secretKey;
console.log('Public Key:', publicKey);
console.log('Public Key length:', publicKey.length); // 64Python (PyNaCl)
python
from nacl.signing import SigningKey
# Generate a random keypair
key = SigningKey.generate()
# Public key: 32 bytes → 64 hex characters
public_key = key.verify_key.encode().hex()
print(f"Public Key: {public_key}")
print(f"Public Key length: {len(public_key)}") # 64Signing Process
Step 1: Create the Message
The message is a JSON object specific to the operation:
javascript
// For Ed25519 registration
const message = {
key_type: "ed25519", // Required for Ed25519
profile: { /* ... */ },
public_key: publicKey,
purpose: "registration",
timestamp: Date.now()
};
// For secp256k1 registration
const message = {
chain_id: "eip155:1", // Required for secp256k1
key_type: "secp256k1", // Required for secp256k1
profile: { /* ... */ },
public_key: publicKey,
purpose: "registration",
timestamp: Date.now()
};Step 2: Canonical JSON
Convert to canonical form (keys sorted alphabetically, no whitespace):
javascript
function canonicalJson(obj) {
if (obj === null) return 'null';
if (typeof obj === 'undefined') return undefined;
if (Array.isArray(obj)) {
return '[' + obj.map(canonicalJson).join(',') + ']';
}
if (typeof obj === 'object') {
const keys = Object.keys(obj).sort();
const pairs = keys
.map(k => {
const v = canonicalJson(obj[k]);
return v !== undefined ? `"${k}":${v}` : undefined;
})
.filter(p => p !== undefined);
return '{' + pairs.join(',') + '}';
}
return JSON.stringify(obj);
}Example transformation:
javascript
// Input
{ "name": "Alice", "age": 30, "city": null }
// Output (canonical)
{"age":30,"city":null,"name":"Alice"}Step 3: Sign the Bytes
Sign the UTF-8 encoded canonical JSON:
javascript
const messageBytes = Buffer.from(canonicalJson(message));
const signatureBytes = nacl.sign.detached(messageBytes, secretKey);
const signature = Buffer.from(signatureBytes).toString('hex');The signature is 64 bytes → 128 hex characters.
Verification Process
The server verifies signatures by:
- Reconstructing the expected message
- Converting to canonical JSON
- Verifying the signature with the public key
javascript
// Server-side verification
const messageBytes = Buffer.from(canonicalJson(expectedMessage));
const signatureBytes = Buffer.from(signature, 'hex');
const publicKeyBytes = Buffer.from(publicKey, 'hex');
const isValid = nacl.sign.detached.verify(
messageBytes,
signatureBytes,
publicKeyBytes
);Test Vectors
Use these to verify your implementation:
Test Vector 1: Simple Object
json
{
"input": { "a": 1, "b": 2 },
"canonical": "{\"a\":1,\"b\":2}",
"public_key": "5f3d93f26e0cf7cf06b81d7fc7fb1e3b79d15c7b0d0c7f7c0d7c7f7c0d7c7f7c",
"signature": "..."
}Test Vector 2: Nested Object
json
{
"input": { "z": { "b": 2, "a": 1 }, "a": "test" },
"canonical": "{\"a\":\"test\",\"z\":{\"a\":1,\"b\":2}}",
"public_key": "...",
"signature": "..."
}Common Mistakes
1. Non-canonical JSON
javascript
// Wrong: keys not sorted
JSON.stringify({ name: "Alice", age: 30 })
// Produces: {"name":"Alice","age":30}
// Correct: keys sorted
canonicalJson({ name: "Alice", age: 30 })
// Produces: {"age":30,"name":"Alice"}2. Wrong Encoding
javascript
// Wrong: signing the string directly
nacl.sign.detached(message, secretKey);
// Correct: signing UTF-8 bytes
nacl.sign.detached(Buffer.from(message), secretKey);3. Including undefined Values
javascript
// Wrong: undefined becomes "undefined" string
JSON.stringify({ a: undefined })
// Correct: undefined values should be omitted
canonicalJson({ a: undefined }) // Returns "{}"Libraries
JavaScript/TypeScript
bash
npm install tweetnacljavascript
import nacl from 'tweetnacl';Python
bash
pip install pynaclpython
from nacl.signing import SigningKey, VerifyKeyRust
toml
[dependencies]
ed25519-dalek = "2.0"rust
use ed25519_dalek::{SigningKey, Signature, Verifier};Go
bash
go get golang.org/x/crypto/ed25519go
import "golang.org/x/crypto/ed25519"EIP-191 Signatures (secp256k1)
For agents using Ethereum wallets (secp256k1 keys), use EIP-191 personal_sign.
Key Differences from Ed25519
| Aspect | Ed25519 | secp256k1 (EIP-191) |
|---|---|---|
| Message format | Canonical JSON (sorted keys) | JSON string |
| Key Type | 64 hex chars | 66 (compressed) or 130 (uncompressed) hex |
| Signature | 128 hex chars | 132 hex chars (0x prefix) |
| Extra fields | key_type: "ed25519" | key_type: "secp256k1", chain_id |
Signing Process
javascript
import { ethers } from 'ethers';
// 1. Create message (include key_type and chain_id)
const message = {
chain_id: "eip155:1",
key_type: "secp256k1",
profile: {
avatar: null,
capabilities: [...],
description: "...",
name: "My Agent",
tags: [],
website: null
},
public_key: "04abc123...", // Uncompressed public key
purpose: "registration",
timestamp: Date.now()
};
// 2. Sign with EIP-191 personal_sign
const signature = await wallet.signMessage(JSON.stringify(message));
// Returns: 0x... (132 hex chars)Getting the Public Key
For Ethereum wallets, you may need to derive the public key:
javascript
import { ethers } from 'ethers';
// From a signature (recovery)
const msgHash = ethers.hashMessage("test message");
const signature = await signer.signMessage("test message");
const recoveredAddress = ethers.recoverAddress(msgHash, signature);
// From signing key (if available)
const wallet = new ethers.Wallet(privateKey);
const publicKey = wallet.signingKey.publicKey.slice(2); // Remove 0x prefix