Signing
Every Telium signature is Ed25519 (RFC 8032). There is no algorithm negotiation and no fallback. This page defines how each kind of signed object is serialised and verified.
General rules
Section titled “General rules”- Verify strictly (
verify_strict): a verifier MUST reject non-canonicalS(S + L) and small-order public keys, identity keys included. - A signer MUST NOT sign JSON containing a non-integer number at any depth. It returns the document unchanged, without
signature/key_id. A verifier MUST reject an unsigned document.i64/u64integers are fine. - Base64 is base64url unpadded (RFC 4648 §5) everywhere except MLS frames (MLS).
decode_any_b64(identity input) accepts all variants and re-encodes to base64url unpadded before signing or comparing.
Family A: signed JSON documents
Section titled “Family A: signed JSON documents”sign_json/verify_json. Used for key documents, media vouchers, relocation records, promotion proofs and migration tombstones.
- Remove
signatureandkey_id. - Serialise with no whitespace, keys sorted by UTF-8 bytes at every depth, escaping only
",\and C0 controls (not/, not non-ASCII). - Sign, then insert
signature(base64url unpadded) andkey_id.
To verify, repeat steps 1 and 2 on the document as received. Transmitted key order is irrelevant, but a serialiser MUST emit sorted order.
key_idis not signed.{"a":null}and{}are different documents.- Unknown members are covered by the signature.
- A valid signature is not authorisation. A voucher’s
audience,expandmax_bytesare checked separately.
Family B: identity assertions
Section titled “Family B: identity assertions”base64url(payload) "." base64url(signature) "." key_idThe payload JSON is in declaration order, not sorted:
user_id, device_id, device_pubkey, audience, origin, profile_version, display_name, avatar_ref, iat, expavatar_ref is omitted when absent. Verify the received payload segment verbatim, then parse it. Never re-serialise. Unknown claims are ignored.
The verifier checks, in order:
- the signature, against the key found from
origin+key_id; audienceequals this server’s name exactly;exp(ms) > now;iat≤ now + 60 000 ms.
Family C: federation requests
Section titled “Family C: federation requests”federation::signing_payload is a Family A canonical object with exactly six members:
{"body_hash":"…","destination":"…","method":"…","origin":"…","path":"…","timestamp":0}methodis upper-case.pathexcludes the query string.timestampis in ms.body_hashis the lowercase hex BLAKE3 of the body. For an empty body:af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262.
Authorization: X-Telium-Federation origin=…,destination=…,key=…,ts=…,sig=…Optional parameters signer= and authority= mark delegated signing.
The query string is unsigned, so a federation endpoint MUST NOT accept a query argument that changes what the request does.
The receiver, before using the body:
- rejects a malformed header (no defaults);
- checks that
destinationis itself; - checks
|now - timestamp|≤ the skew window ([federation] max_clock_skew_secs, default 300); - recomputes
body_hashfrom the received bytes; - resolves the key and verifies the signature.
Family D: domain-separated strings
Section titled “Family D: domain-separated strings”The tag and fields are joined with | (U+007C), UTF-8, no length prefixes.
| Tag | Fields after the tag | Signed by |
|---|---|---|
telium-identity-anchor:v1 |
user_id, immutable_id, identity_key |
identity key |
telium-identity-rotate:v1 |
immutable_id, sequence, previous_key, new_key |
new identity key |
telium-identity-recovery-policy:v1 |
immutable_id, version, identity_sequence, guardians |
identity key |
telium-device-sig:v1 |
user_id, device_id, mls_key |
identity key |
telium-device-revoke:v1 |
user_id, immutable_id, device_id, mls_key |
identity key |
telium-password-reset:v1 |
user_id, reset_id, challenge, registration_record |
recovery key |
telium-recovery-k1-restore:v1 |
user_id, attempt_id, challenge, ciphertext |
recovery key |
Guardians are serialised as JSON [threshold, [[user_id, immutable_id, identity_key], …]], with the rows sorted by immutable_id, then user_id, then identity_key.
The immutable id hashes a domain tag that ends in a NUL byte:
"tid:" || lowercase_hex(SHA-256("telium-identity-id:v1\0" || raw_32_byte_pubkey))Migration grants (grant_payload) are Family A JSON signed by a device key, with the signature sent separately and no signature member in the object.
Family E: relocation record
Section titled “Family E: relocation record”- The successor signs the body (Family A). Rename
signaturetonew_signatureandkey_idtonew_key_id. - The old key signs the body including
new_signature(Family A).
To verify, check the outer signature, then rename the fields back and check the inner one. Both MUST pass (relocation::check_signatures).
Key validity
Section titled “Key validity”valid_from and valid_until are in ms; valid_until is exclusive.
| Object | Key must be valid |
|---|---|
| Live request | now (verify_key) |
| Durable record (tombstone, promotion proof, relocation record) | at the record’s timestamp (verify_key_at) |
Vectors for every family are listed in Conformance.