Bundle Signing
Ed25519 signatures for contract bundles. Every deployed bundle is signed server-side. SDK signature verification is planned.
Right page if: you need the details of Ed25519 bundle signing -- keypair generation, private key encryption, the signing/verification flow, and key rotation. Wrong page if: you want the broader security model (see https://docs.edictum.ai/docs/console/concepts/security-model) or how signed bundles reach agents via SSE (see https://docs.edictum.ai/docs/console/concepts/hot-reload). Gotcha: key rotation re-signs all active deployments atomically, but there is a brief window between rotation and SSE delivery where agents hold bundles signed by the old key.
Agents pull contract bundles from the console. Without signatures, a compromised transport layer could swap in a permissive bundle. Ed25519 signing provides server-side integrity verification of deployed bundles.
The Server SDK (edictum[server]) currently receives the signature and public key via SSE but does not verify the signature before applying contract updates. Transport security (TLS) is the current integrity mechanism between the console and agents. End-to-end signature verification in the SDK is planned — see roadmap.
How It Works
Every tenant gets an Ed25519 keypair. When a contract bundle is deployed, the console signs the YAML content and includes the 64-byte signature in the deployment response. The public key is available via SSE so agents can verify.
YAML bytes ──> Ed25519 sign(private_key) ──> 64-byte signature ──> hex-encoded in API responseNo third-party certificate authority is needed. Keys are tenant-managed within the console.
Keypair Generation
A keypair is generated automatically on first bundle deploy. The process:
- Generate an Ed25519 signing key
- Extract the 32-byte public key
- Encrypt the 32-byte private key with NaCl SecretBox using the tenant secret
- Store both in the
signing_keystable
from nacl.signing import SigningKey
from nacl.secret import SecretBox
signing_key = SigningKey.generate()
public_key = signing_key.verify_key.encode() # 32 bytes
private_key_raw = bytes(signing_key) # 32 bytes
box = SecretBox(signing_key_secret) # 32-byte secret
encrypted_private_key = box.encrypt(private_key_raw)The private key is never stored in plaintext. The tenant secret used for encryption is a 32-byte value configured via the EDICTUM_SIGNING_KEY_SECRET environment variable.
Private Key Storage
| Column | Type | Description |
|---|---|---|
public_key | bytes | Raw 32-byte Ed25519 public key |
private_key_encrypted | bytes | NaCl SecretBox-encrypted private key |
active | bool | Whether this key is the current signing key |
tenant_id | UUID | Owning tenant |
Only one key is active per tenant at any time. Previous keys are retained (with active = false) for audit purposes.
Signing Process
When a bundle is deployed, the console:
- Retrieves the tenant's active signing key
- Decrypts the private key using the tenant secret
- Signs the raw YAML bytes with Ed25519
- Stores the 64-byte signature on the bundle record
- Returns
signature_hexin the API response
from nacl.secret import SecretBox
from nacl.signing import SigningKey
box = SecretBox(signing_key_secret)
private_key_raw = box.decrypt(encrypted_private_key)
signing_key = SigningKey(private_key_raw)
signed = signing_key.sign(yaml_bytes)
signature = signed.signature # 64 bytesThe API response includes the signature as a hex string:
{
"id": "bundle-uuid",
"name": "production-contracts",
"version": 3,
"signature_hex": "a1b2c3d4...64-byte-signature-in-hex",
"created_at": "2026-03-08T12:00:00Z"
}Verification
Verification uses only the public key (no secret needed):
from nacl.signing import VerifyKey
verify_key = VerifyKey(public_key_bytes)
try:
verify_key.verify(yaml_bytes, signature_bytes)
print("Signature valid")
except Exception:
print("Signature invalid — bundle may be tampered")The public key is delivered to agents via SSE deployment events alongside the bundle content. Agents that connect via the Server SDK (edictum[server]) receive both automatically. However, the SDK does not currently verify the signature — it stores the public key and signature for future use. Use TLS between the console and agents to protect bundle integrity in transit until SDK-side verification ships.
Key Rotation
Rotate keys when a secret may be compromised or as part of regular key hygiene. Rotation is atomic:
- All currently active keys for the tenant are deactivated
- A new Ed25519 keypair is generated and encrypted
- Every currently-deployed bundle is re-signed with the new key
- All changes are flushed in a single database transaction
POST /api/v1/settings/rotate-signing-keyResponse:
{
"public_key": "new-public-key-hex",
"rotated_at": "2026-03-08T12:00:00Z",
"deployments_re_signed": 4
}Re-signing covers the latest deployment per environment and bundle name. Old deployments retain their original signatures (signed by the now-deactivated key).
Key rotation re-signs all active deployments atomically. Connected agents receive updated bundles via SSE. There is a brief window between rotation and SSE delivery where agents hold bundles signed by the old key.
Security Properties
| Property | Detail |
|---|---|
| Algorithm | Ed25519 (RFC 8032) |
| Signature size | 64 bytes |
| Private key encryption | NaCl SecretBox (XSalsa20-Poly1305) |
| Key storage | Encrypted at rest, decrypted only during sign operations |
| Rotation | Atomic — old key deactivated, all active bundles re-signed in one transaction |
| CA dependency | None — tenant-managed keys |
Next Steps
- Contract Bundles -- uploading and deploying bundles
- Connecting Agents -- how agents receive signed bundles via SSE
- Architecture -- where signing fits in the console
- Roadmap -- planned verification enhancements
Last updated on