Edictum
Edictum Console

Bundle Signing

Ed25519 signatures for contract bundles. Every deployed bundle is signed server-side. SDK signature verification is planned.

AI Assistance

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 response

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

  1. Generate an Ed25519 signing key
  2. Extract the 32-byte public key
  3. Encrypt the 32-byte private key with NaCl SecretBox using the tenant secret
  4. Store both in the signing_keys table
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

ColumnTypeDescription
public_keybytesRaw 32-byte Ed25519 public key
private_key_encryptedbytesNaCl SecretBox-encrypted private key
activeboolWhether this key is the current signing key
tenant_idUUIDOwning 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:

  1. Retrieves the tenant's active signing key
  2. Decrypts the private key using the tenant secret
  3. Signs the raw YAML bytes with Ed25519
  4. Stores the 64-byte signature on the bundle record
  5. Returns signature_hex in 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 bytes

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

  1. All currently active keys for the tenant are deactivated
  2. A new Ed25519 keypair is generated and encrypted
  3. Every currently-deployed bundle is re-signed with the new key
  4. All changes are flushed in a single database transaction
POST /api/v1/settings/rotate-signing-key

Response:

{
  "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

PropertyDetail
AlgorithmEd25519 (RFC 8032)
Signature size64 bytes
Private key encryptionNaCl SecretBox (XSalsa20-Poly1305)
Key storageEncrypted at rest, decrypted only during sign operations
RotationAtomic — old key deactivated, all active bundles re-signed in one transaction
CA dependencyNone — tenant-managed keys

Next Steps

Last updated on

On this page