Aller au contenu principal

Signing and integrity

Every event stored by QAudit carries a cryptographic signature. This page explains what the signature covers, how keys are managed, and how to use the signature to verify an event independently.

What gets signed

For each event, the Audit Gateway computes a signature over the following input:

signature = Ed25519.Sign(
signing_key,
SHA-256(canonical_payload ‖ receipt_ts ‖ chain_link_hash)
)
  • canonical_payload — the event's payload bytes, serialised in RFC 8785 JCS deterministic form. This is exactly what is stored in the event record and what appears in the raw JSON inspector.
  • receipt_ts — the Gateway's receipt timestamp, assigned at the moment the event was stored. This is the authoritative "received at" time; it is independent of any timestamp the emitter may have included in the payload.
  • chain_link_hash — the event's position on the tenant's hash chain (see The event chain). Including this in the signature input binds the signature to the event's position — an attacker cannot move the event to a different position in the chain without invalidating the signature.

The three inputs are what the signature proves. If the canonical payload bytes match, the receipt timestamp matches, and the chain link hash matches, the signature verifies — the event is proven to be exactly what the Gateway stored, in the position it was stored.

The key model

QAudit uses Ed25519 — a modern elliptic-curve signature scheme — for all event signing. Keys are managed by a dedicated key management system (KMS) that runs inside the same sovereign cloud as the event store.

Serensia operates its own Public Key Infrastructure (PKI) and acts as the certificate authority for all keys and certificates used by the platform. This means the root of trust is entirely under Serensia's control, within the SecNumCloud perimeter — no dependency on external certificate authorities for event signing.

Key hierarchy:

  • One root signing key held by the platform. It certifies per-organisation keys.
  • One per-organisation signing key for each organisation. All tenants under a given organisation share that organisation's key. (See Organisations and tenants for the distinction between organisations and tenants.)

Private key material never leaves the KMS. The Gateway calls the KMS signing API for each event; the key itself is never exposed to the Gateway process or to any other service. There is no on-disk copy, no in-memory cached key — only signed outputs.

The public key

At onboarding, the organisation receives the public key corresponding to the organisation's signing key. This is the key used to verify signatures independently. It is a standard Ed25519 public key (32 bytes) and can be used with any Ed25519 implementation.

The public key is also available at any time from the dashboard (account settings page). In addition, the initial signing public key is embedded in the qaudit.tenant.created event at the head of the tenant's chain — so a full chain export is self-describing: the verifying public key travels with the events.

When the platform rotates a signing key (at the end of a key's configured lifetime), the old public key remains valid for verifying events signed under it. Every event record carries a reference to the key version it was signed with. Each rotation is also recorded as a qaudit.tenant.signing-key.rotated event on the chain, making the chain self-describing: a verifier can read the rotation event to determine which key applies to each range of events without consulting an external registry. See Platform events.

Verifying an event yourself

To verify an event's signature outside the dashboard:

  1. Obtain the canonical payload from the single-event view (raw JSON inspector, "Download canonical bytes" button).
  2. Read the receipt timestamp and chain link hash from the event's detail view.
  3. Construct the signed bytes: SHA-256(canonical_payload ‖ receipt_ts ‖ chain_link_hash) — the hash is over the three fields concatenated in that order.
  4. Verify the Ed25519 signature in the event detail against the signed bytes, using the organisation's public key.

Any standard Ed25519 library can perform step 4. If the verification passes, the event is byte-for-byte what the Gateway stored at the stated time in the stated chain position.

Worked example — full hash derivation

The following is a reproducible test vector. Every byte sequence and SHA-256 digest is exact; SDK implementers can use it to validate their canonicalization and hashing pipeline.

Input. An invoice-received event submitted as a single JSON object — mandatory fields and payload fields together, with keys out of order and a trailing zero on the amount:

{
"tenant_id": "acme-corp",
"event_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"event_name": "qaudit.invoice.received.v1",
"date": "2026-05-24T10:15:30.000Z",
"invoice_id": "INV-2026-0042",
"amount": 1234.50,
"currency": "EUR"
}

After Gateway processing. The Gateway re-canonicalizes the full object to RFC 8785 JCS — all keys sorted lexicographically, 1234.50 normalized to 1234.5 — and computes the chain link hash and signature:

canonical_payload = {"amount":1234.5,"currency":"EUR","date":"2026-05-24T10:15:30.000Z","event_id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","event_name":"qaudit.invoice.received.v1","invoice_id":"INV-2026-0042","tenant_id":"acme-corp"}
receipt_ts = "2026-05-24T10:15:30.527198341Z"
prev_event_id = "8e5d4c3b-2a1f-4f6e-9d8c-7b6a5f4e3d2c"
prev_signature = 633317e9 e5ed5b87 41ec96b6 ddbd935e
8e9687d5 da8d9fb9 04e0b9d8 587ccb8f
aa67a2dd 88e3be92 a3568ca5 11fc122f
aa5f4bf6 1fe1fec6 f0707e6c 4c1eec23

Chain-link hash derivation. chain_link_hash = SHA-256(prev_signature ‖ utf8(prev_event_id) ‖ utf8(event_id)) — fields concatenated with no delimiter, raw bytes.

[1] prev_signature 64 bytes (raw Ed25519 signature)

633317e9 e5ed5b87 41ec96b6 ddbd935e
8e9687d5 da8d9fb9 04e0b9d8 587ccb8f
aa67a2dd 88e3be92 a3568ca5 11fc122f
aa5f4bf6 1fe1fec6 f0707e6c 4c1eec23


[2] utf8(prev_event_id) 36 bytes
"8e5d4c3b-2a1f-4f6e-9d8c-7b6a5f4e3d2c"

38653564 34633362 2d326131 662d3466
36652d39 6438632d 37623661 35663465
33643263


[3] utf8(event_id) 36 bytes

"f47ac10b-58cc-4372-a567-0e02b2c3d479"
66343761 63313062 2d353863 632d3433
37322d61 3536372d 30653032 62326333
64343739


[1] ‖ [2] ‖ [3] → concatenation 136 bytes

633317e9 e5ed5b87 41ec96b6 ddbd935e
8e9687d5 da8d9fb9 04e0b9d8 587ccb8f
aa67a2dd 88e3be92 a3568ca5 11fc122f
aa5f4bf6 1fe1fec6 f0707e6c 4c1eec23
38653564 34633362 2d326131 662d3466
36652d39 6438632d 37623661 35663465
33643263 66343761 63313062 2d353863
632d3433 37322d61 3536372d 30653032
62326333 64343739


SHA-256(concatenation) → chain_link_hash 32 bytes

dd301904 e6c2aa6c 8e4c2b52 993dcc69
946fcd06 77e808c1 d661add3 177bb156

Signed-bytes derivation. signed_hash = SHA-256(canonical_payload ‖ utf8(receipt_ts) ‖ chain_link_hash) — same pattern: concatenate in order, no delimiter, raw bytes.

[1] canonical_payload 213 bytes
{"amount":1234.5,"currency":"EUR","date":"2026-05-24T10:15:30.000Z","event_id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","event_name":"qaudit.invoice.received.v1","invoice_id":"INV-2026-0042","tenant_id":"acme-corp"}

7b22616d 6f756e74 223a3132 33342e35
2c226375 7272656e 6379223a 22455552
222c2264 61746522 3a223230 32362d30
352d3234 5431303a 31353a33 302e3030
305a222c 22657665 6e745f69 64223a22
66343761 63313062 2d353863 632d3433
37322d61 3536372d 30653032 62326333
64343739 222c2265 76656e74 5f6e616d
65223a22 71617564 69742e69 6e766f69
63652e72 65636569 7665642e 7631222c
22696e76 6f696365 5f696422 3a22494e
562d3230 32362d30 30343222 2c227465
6e616e74 5f696422 3a226163 6d652d63
6f727022 7d


[2] utf8(receipt_ts) 30 bytes
"2026-05-24T10:15:30.527198341Z"

32303236 2d30352d 32345431 303a3135
3a33302e 35323731 39383334 315a


[3] chain_link_hash 32 bytes

dd301904 e6c2aa6c 8e4c2b52 993dcc69
946fcd06 77e808c1 d661add3 177bb156


[1] ‖ [2] ‖ [3] → concatenation 275 bytes

7b22616d 6f756e74 223a3132 33342e35
2c226375 7272656e 6379223a 22455552
222c2264 61746522 3a223230 32362d30
352d3234 5431303a 31353a33 302e3030
305a222c 22657665 6e745f69 64223a22
66343761 63313062 2d353863 632d3433
37322d61 3536372d 30653032 62326333
64343739 222c2265 76656e74 5f6e616d
65223a22 71617564 69742e69 6e766f69
63652e72 65636569 7665642e 7631222c
22696e76 6f696365 5f696422 3a22494e
562d3230 32362d30 30343222 2c227465
6e616e74 5f696422 3a226163 6d652d63
6f727022 7d323032 362d3035 2d323454
31303a31 353a3330 2e353237 31393833
34315add 301904e6 c2aa6c8e 4c2b5299
3dcc6994 6fcd0677 e808c1d6 61add317
7bb156


SHA-256(concatenation) → signed_hash 32 bytes

049a02aa df05a394 bf5e60ba 6a371f94
fdb032ac 993ac084 646027eb ca6925f3

Ed25519.Sign(organisation_private_key, signed_hash) → signature 64 bytes
(omitted — depends on the organisation's private key)

Verification. Call Ed25519.Verify(organisation_public_key, signed_hash, signature). signed_hash (049a02aa…ca6925f3) is deterministic for this input and can be used to validate the hashing pipeline end-to-end.

Note: prev_signature here is a contrived deterministic value chosen so the example is fully reproducible. In a live chain it is the previous event's actual Ed25519 signature as returned by the Gateway.