QERYX identity is two layers: a 256-bit Serpent ID
that never leaves the device, and a public Profile ID
derived from it one-way through BLAKE3 keyed hashing.
Public keys bind to Profile IDs through the Key Transparency System — an
append-only Merkle log with ML-DSA-87-signed tree heads —
so a peer's real key is auditor-verifiable without trusting the
operator.
Two identity layers
The Serpent ID is 256 bits of seed entropy: an OS secure-random draw XORed with 256 bits from the certified-QRNG pool, so an attacker controlling one entropy source cannot bias the result. It is stored device-only under biometric protection and appears on no wire, ever.
Three subkeys derive from it through BLAKE3 keyed-derive,
each under its own domain string — a break of one derivation stream does
not touch another:
| Domain | Subkey | Use |
|---|---|---|
qeryx.identity.profile.v1 | Profile ID | public messaging handle |
qeryx.identity.vault.v1 | vault key | QVault encryption |
qeryx.identity.signing.v1 | signing seed | derives the ML-DSA-87 identity keypair |
The derivation is one-way: the Profile ID leaks nothing about the Serpent
ID, and the parser hard-rejects any identifier that does not carry the
exact PROFILE- prefix discipline — test vectors in the
source lock this at the byte level.
Blinding the social graph
The relay locates conversations without ever seeing a plaintext Profile
ID. Each conversation carries a random 32-byte salt, and membership is
stored as SHA-256(salt || profile_id) — the same Profile ID
hashes to a different value in every conversation, so an operator with
database access cannot correlate hashes across conversations to
enumerate who talks to whom.
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-512[..32]( 0x00 || canonical(entry) )
Node: H = SHA-512[..32]( 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
Signed Tree Head is signed under the FIPS 204 context
QERYX-KTS-STH-v1; inclusion proofs authenticate a single
binding, and consistency proofs force every new tree head to be an
append-only extension of every prior one — truncation or rewrite
produces a proof failure, treated as operator equivocation, a hard
error.
An earlier design signed the domain string inside the message body; an external review finding (F-002, closed 2026-04-30) moved domain separation into the FIPS 204 context framing, eliminating a cross-protocol replay class.
Distrust of the operator
The operator is assumed hostile. Three attacks, three answers:
| Operator attack | Defense |
|---|---|
| Equivocate — show different logs to different clients | two conflicting signed tree heads are dispositive evidence; clients gossip STHs out-of-band (mesh, paired devices, QR) so a split view surfaces even offline |
| Insert silently — swap a key without the owner noticing | clients pin the operator key on first contact and hard-fail any tree head that does not verify against the pin |
| Erase history — remove a compromising binding | consistency proofs bind every new head to every old one; erasure cannot produce a valid proof |
Operator key rotation is deliberately manual: a legitimate rotation is co-signed by the currently-pinned key, and accepting a new key after a compromise requires an explicit, biometric-gated "Reset Operator Trust" action with confirmation from at least two independent channels. An adversary cannot induce silent re-pinning.
Wire-stable byte recipes
A reviewer can re-derive any KTS proof from these alone:
canonical(entry) = u64_be(index) || u64_be(timestamp_secs)
|| u32_be(len(profile_id)) || profile_id
|| u32_be(len(public_key)) || public_key
sth_payload = u64_be(tree_size) || root_hash || u64_be(ts) || log_id // 80 B
sth_sig = ML-DSA-87.SignWithCtx(sk_op, ctx = "QERYX-KTS-STH-v1", msg = sth_payload)
Big-endian length prefixes make the parse unambiguous; the entry's own internal prefixes are the envelope.
References
-
QERYX Protocol Specification §3–§4 —
docs/SPEC/02-identity-and-kts.md; implementationcrypto-core/src/identity.rs,crypto-core/src/merkle_kts.rs. - RFC 6962 — Certificate Transparency; RFC 9162 — Certificate Transparency v2.
- NIST FIPS 204 — ML-DSA, §5.2 context framing.
- Verify Your Safety Number — the user-facing check this log underwrites.