QERYX COMING SOON
OBSERVATORY

Research Messenger

protocol-spec

Identity and Key Transparency: Serpent ID, Profile ID, and the KTS Log

Version
v1.0
Category
protocol-spec
Published
Last updated
Authors
QERYX Research
Primitives SHA-256 ML-DSA-87 SHA-384 HMAC-SHA-256
What changed v1.0
  1. v1.0 First publication of the identity layer and KTS chapters.
Contents
  1. §1 Two identity layers
  2. §2 Blinding the social graph
  3. §3 The Key Transparency System
  4. §4 Distrust of the operator
  5. §5 Wire-stable byte recipes
  6. §6 References

QERYX identity is two layers: a Serpent ID that never leaves the device, and a public Profile ID derived from it one-way as SHA-256("qeryx.identity.profile.v1:" ‖ serpent), truncated to its first eight bytes. Both identifiers are 64-bit handles; the authentication factor is a separate 256-bit device-held secret under Argon2id, never the handle. Public keys bind to Profile IDs through the Key Transparency System — an append-only SHA-384 Merkle log whose roots are signed under QERYX-KTS-ROOT-v1 — so a peer's real key is verifiable by anyone, from public endpoints, against an operator key the app pins at build time.

Two identity layers

The Serpent ID is drawn from the OS secure random generator and kept in the Keychain as an after-first-unlock, this-device-only item, so a locked device can still route while a backup or a bare filesystem read cannot open it. It appears on no wire, ever: the transport boundary derives the Profile ID and refuses to forward a SERPENT- value even when a caller passes one.

One public handle derives from it, under one domain string. The identity, key-agreement and signing keypairs are drawn independently from the OS generator at registration rather than derived from the handle, so a handle compromise reaches no key material:

DomainSubkeyUse
qeryx.identity.profile.v1Profile IDpublic messaging handle

The derivation is one-way, and every identifier passes one normaliser before any derivation, conversation id or tombstone key uses it: PROFILE- is canonicalised, SERPENT- is derived rather than forwarded, a routing UUID is refused rather than coerced, and SYSTEM- and SHADOW- live in namespaces that never map into PROFILE-.

Blinding the social graph

Each conversation carries a random 32-byte salt and stores membership twice: as SHA-256(salt || profile_id), and as the array of account identifiers the relay must deliver to. The hash is per-conversation, so the same Profile ID takes a different value in every room — but the salt sits on the same row and the profile identifiers sit in the same database, so an operator holding a snapshot can test membership directly. The roster is the authorization spine; a delivery capability that removes it from the relay's view is designed and not yet built into the app.

Presence uses a second, disjoint namespace: a truncated HMAC-SHA-256 token over "pair:" || presenter || "|" || peer, asymmetric by construction so a recipient cannot replay an inbound token to spoof the presenter. Wire-stable test vectors pin both recipes; if either vector ever fails, clients and database desync — the tests are tripwires.

The Key Transparency System

KTS is an append-only Merkle log of (profile_id, ml_dsa_87_pubkey) bindings, modeled on RFC 6962 and RFC 9162 and adapted to ML-DSA-87 so the operator's commitment survives a quantum adversary.

Leaf:  H = SHA-384( 0x00 || profile_id || identity_pub_key || u64_be(bundle_epoch) )
Node:  H = SHA-384( 0x01 || left || right )

The domain-separator byte between leaf and node hashing forecloses second-preimage confusion between the two levels (RFC 6962 §2.1). Every root is signed under the FIPS 204 context QERYX-KTS-ROOT-v1 over u64_be(seq) || root_hash, where seq is the tree size at that append — so the signature is itself a signed tree head. Clients verify the inclusion path to that root, refuse any sequence lower than the one they last accepted for that peer, and refuse a leaf that does not hash from the exact bundle they are about to encrypt to. Consistency proofs between two roots are the phase-2 item: until then the log serves inclusion proofs only.

An earlier design signed the domain string inside the message body; an internal review finding (F-002, closed 2026-04-30) moved domain separation into the FIPS 204 §5.2 context framing the signer uses, eliminating a cross-protocol replay class.

Distrust of the operator

The operator is assumed hostile. Three attacks, three answers:

Operator attackDefense
Equivocate — show different logs to different clients two roots signed at the same sequence conflict, and clients gossip the signed root they were served to a random subset of their peers, so an operator serving two histories has to keep every pair of devices apart to stay hidden
Insert silently — swap a key without the owner noticing clients verify every root against an operator key whose SHA-384 is compiled into the app, and hard-fail any root that does not verify against it
Erase history — remove a compromising binding erasure below a root a device already accepted changes that root; the device refuses a lower sequence and gossips the root it was served, so the erased history has to be shown consistently to every client at once

Operator-key rotation is deliberately not silent. The client refuses any operator key whose SHA-384 does not match the anchor compiled into the binary, and refuses a live key that differs from the one it pinned — so replacing the operator key requires a new app version. An adversary cannot induce re-pinning at all.

Wire-stable byte recipes

A reviewer can re-derive any KTS proof from these alone:

canonical(leaf) = profile_id_utf8 || identity_pub_key || u64_be(bundle_epoch)
leaf_hash    = SHA-384(0x00 || canonical(leaf))
node         = SHA-384(0x01 || left || right)
root_payload = u64_be(seq) || root_hash   // 56 B
root_sig     = ML-DSA-87.SignWithCtx(sk_op, ctx = "QERYX-KTS-ROOT-v1", msg = root_payload)

Fetch GET /kts/proof/{profile_id} and GET /kts/root and check them yourself.

References

  1. QERYX Protocol Specification §3–§4 — docs/SPEC/02-identity-and-kts.md; implementation crypto-core/src/identity.rs; the log is backend/src/services/kts/merkle.rs and backend/src/services/kts/signer.rs, with the client verifier in ContactKeyVerificationService.
  2. RFC 6962 — Certificate Transparency; RFC 9162 — Certificate Transparency v2.
  3. NIST FIPS 204 — ML-DSA, §5.2 context framing.
  4. Verify Your Safety Number — the user-facing check this log underwrites.