Technical deep-dive · 2026-05-25

Ed25519, TOFU pinning and offline verification: how monsys evidence packs work

Dashboards are for operators. Auditors want an artifact they can verify offline, without trusting your system. How the monsys signing chain works.

An auditor asks for proof that your server had no unauthorised changes on March 14. You send a dashboard screenshot. The auditor asks: how do I know this wasn't edited?

That's the core problem of any monitoring tool that wants to deliver compliance evidence. Dashboards are great for operators. Auditors want something else: an artifact they can verify offline, without having to trust your system.

This is how the monsys signing chain works.

The architecture in one picture

Every payload the agent sends to the hub is signed with an Ed25519 keypair. The keypair is unique per agent — the private key is generated on the host at install time and never leaves it. The hub only knows the public key.

Agent (host)                    Hub (monsys.ai BE)
─────────────────               ──────────────────────────────
Ed25519 private key             Ed25519 public key (TOFU pinned)
    │                               │
    ▼                               ▼
payload → sign(payload, privkey) → verify(sig, pubkey) → store

The hub pins the public key on first connection (TOFU). Every payload after that is verified. An attacker who steals the hub token can't inject fake telemetry — they would need the private key on the host to produce a valid signature.

How an evidence pack is built

When you click "Generate evidence pack" for a period (e.g. April 2026):

Step 1: Query The hub fetches every relevant event for your tenant in that period:

Step 2: Normalisation Each event row is serialised to canonical JSON (sorted keys, no whitespace variation). This matters: two identical datasets must produce identical bytes, otherwise the signature won't match.

Step 3: Manifest The manifest is a JSON file that lists every file in the tarball with its SHA256 hash:

{
  "tenant_id": "acme-bv",
  "period_start": "2026-04-01T00:00:00Z",
  "period_end": "2026-04-30T23:59:59Z",
  "generated_at": "2026-05-01T08:03:14Z",
  "signing_key_id": "sk_2024_q1",
  "files": [
    {
      "path": "agents/web-edge-01/inventory_snapshots.ndjson",
      "sha256": "b4c2f1a3...",
      "record_count": 2880
    },
    {
      "path": "alerts/april_2026.ndjson",
      "sha256": "e7d9c2b1...",
      "record_count": 47
    }
  ],
  "inputs_hash": "sha256:a1b2c3d4..."
}

The inputs_hash is the SHA256 of all sha256 values concatenated — a fingerprint of the whole bundle.

Step 4: Signing The hub signs the manifest with its tenant Ed25519 signing key:

signature = Ed25519.sign(manifest_bytes, tenant_signing_privkey)

The signature and the public key both go into the tarball.

Step 5: Packaging

evidence_acme-bv_april-2026.tar.gz
├── manifest.json
├── manifest.sig          ← Ed25519 signature (hex)
├── signing_key.pub       ← public key (PEM)
├── verify.py             ← standalone verification script
└── data/
    ├── agents/
    ├── alerts/
    ├── detections/
    ├── compliance/
    └── audit_log/

Offline verification: verify.py

The verify.py script inside the tarball has no external dependencies beyond the Python standard library (3.8+) and cryptography (pip installable). An auditor who's never heard of monsys can run it:

pip install cryptography
python verify.py evidence_acme-bv_april-2026.tar.gz

Output on success:

✓ Signature valid (Ed25519)
✓ Signing key matches expected fingerprint: 4a:b3:c2:...
✓ All 12 files intact (SHA256 match)
✓ inputs_hash consistent
✓ Period: 2026-04-01 → 2026-04-30
✓ Tenant: acme-bv

exit 0

Output on tampering:

✗ File tampered: data/alerts/april_2026.ndjson
  Expected SHA256: e7d9c2b1...
  Actual SHA256:   f8e0d3c2...

exit 1

The auditor doesn't need a monsys account. The script downloads nothing from the internet. Verification is fully offline.

Key rotation: the signing-key lineage

Signing keys are rotated periodically (recommended: yearly, or on suspicion of compromise). But what happens to evidence packs signed under an old key?

monsys maintains a key lineage. Each key has:

On verification, verify.py checks not only whether the signature is valid, but also whether the key used was active at generation time (generated_at in the manifest). A key rotated on January 1 2025 is still valid for evidence packs generated before that date.

# Simplified from verify.py
def verify_key_was_active(key_id: str, generated_at: datetime, lineage: list) -> bool:
    for key in lineage:
        if key["key_id"] == key_id:
            active_from = datetime.fromisoformat(key["active_from"])
            active_until = datetime.fromisoformat(key["active_until"]) if key.get("active_until") else datetime.max
            return active_from <= generated_at <= active_until
    return False

The lineage snapshot is in the tarball too, so verification works without network access.

Bearer-token theft: what it can and can't do

A frequent question: if an attacker steals the agent token (the token the agent uses to push telemetry to the hub), what can they do?

Can the attacker:

Cannot the attacker:

This is why periodic token rotation is still recommended — but the signing chain guarantees that stolen tokens can't poison historical evidence.

mTLS: the second security layer

On top of Ed25519 signing, the hub-agent connection uses mTLS. The agent receives a client certificate at first connection that is then pinned. This means:

The combination: mTLS authenticates who makes the connection; Ed25519 authenticates the contents of each payload.

NIS2 and AI Act: what this concretely means

Article 21 of NIS2 requires "appropriate technical measures" for logging and incident response. "Appropriate" is vague, but a signed, offline-verifiable audit trail scores noticeably better with a regulator than screenshots or CSV exports.

For AI Act Article 12 (logging of AI-system events): the AI observability module uses the same signing chain as the rest of monsys. Every trace — prompt, completion, tokens, PII hits — is in the tarball, signed, verifiable by a DPA inspector without a monsys account.

DPA inspector:  "Prove that your AI system did not pass any IBAN
                 numbers to OpenAI on April 3."

Operator:       python verify.py evidence_april-2026.tar.gz
                ✓ Signature valid
                grep -r "pii_hits_count" data/ai_traces/ | grep "2026-04-03"
                → 0 results for IBAN hits on that date

Verifiable. Reproducible. Without asking for your trust.


The signing chain is documented at docs.monsys.ai/en/security/agent-signing and docs.monsys.ai/en/security/signing-keys-rotation. First five servers free: monsys.ai/en/signup.

Back to blog