Protocol Specification¶
This document describes the Diogenes protocol in detail: the key lifecycle, signing flow, verification layers, transparency log structure, and data formats.
Key Registration Protocol¶
Key Pair Generation¶
Participants generate key pairs locally. Diogenes supports three algorithms:
| Algorithm | Identifier | Notes |
|---|---|---|
| Ed25519 | ed25519 |
Recommended. EdDSA over Curve25519. |
| ECDSA P-256 | ecdsa-p256 |
Compatible with Web Crypto API for browser-based signing. |
| RSA-2048 | rsa-2048 |
Legacy compatibility only. |
The fingerprint is computed as the SHA-256 hash of the public key's DER-encoded SubjectPublicKeyInfo bytes, hex-encoded with a sha256: prefix.
Registration Event¶
When a key is registered, the following event is appended to the transparency log:
{
"event_type": "key_registration",
"payload": {
"public_key": "-----BEGIN PUBLIC KEY-----\nMC...\n-----END PUBLIC KEY-----",
"pseudonym": "Alice Scholar",
"key_algorithm": "ed25519",
"expiration_date": "2027-01-15T00:00:00Z"
}
}
The server computes the fingerprint from the public key and creates both a log entry (with hash chain linkage) and a key record.
Key Succession Protocol¶
Key succession allows a key holder to replace their active key with a new one, maintaining identity continuity.
Flow:
- The holder generates a new key pair.
- The holder authenticates as the old key (challenge-response or JWT).
- The holder submits a succession request with the old fingerprint and new public key.
- The server verifies authentication, creates a log entry, registers the new key, and marks the old key as "succeeded" with a pointer to the new fingerprint.
Recovery succession allows succession of expired keys. The holder must prove knowledge of the old key through a recovery-eligible challenge.
Key Revocation Protocol¶
Revocation permanently disables a key. Signatures made before revocation remain verifiable.
Flow:
- The holder authenticates as the key to be revoked.
- The server verifies the key is active.
- A revocation event is appended to the log.
- The key status is set to "revoked".
Recovery tokens cannot be used for revocation (security constraint).
Attestation Signing Protocol¶
Diogenes uses client-side signing. The private key never leaves the client. The server only receives the public key and the signature.
Signing Flow¶
sequenceDiagram
participant C as Client
participant S as Server
Note over C: 1. Compute document SHA-256
Note over C: 2. Build manifest JSON
Note over C: 3. Deterministic serialization
Note over C: 4. Sign with private key
C->>S: POST /api/v1/attestations
{manifest, public_key_pem,
signature, attestation_type}
Note over S: 5. Parse public key
Note over S: 6. Verify signature
Note over S: 7. Register key (if new)
Note over S: 8. Record attestation event
S->>C: {attestation, fingerprint,
pseudonym, log_entry_id}
Deterministic Payload¶
The signing payload is the deterministic JSON serialization of the manifest:
This ensures that the same manifest always produces the same byte sequence for signing, regardless of JSON formatting differences.
Attestation Types¶
| Type | Value | Description |
|---|---|---|
| Authorship | authorship |
The signer authored or co-authored the document. |
| Editorial Review | editorial_review |
The signer reviewed and approved editorially. |
| Peer Review | peer_review |
The signer reviewed for scholarly/technical accuracy. |
| Publication | publication |
The signer (institution) published the document. |
Reserved predicate prefix and operator identity¶
Under Phase 11 Decision 10 an attestation's event_type is its predicate
URL. Who may publish under a given predicate URL is therefore an identity
question, and one prefix is reserved:
This prefix is reserved to the Diogenes operator key. The rule is enforced in two places, and they behave differently on purpose:
| Layer | Behavior |
|---|---|
| Client / SDK schema layer | Does not enforce. Attestation.type calls validate_predicate_uri(..., is_operator=True) unconditionally, deliberately deferring the identity check to the server. A reserved-prefix attestation therefore constructs fine locally. |
Server POST /api/v1/attestations |
Enforces. A novel predicate under the reserved prefix is rejected with HTTP 403 unless the submitting key is the operator key. |
Core-URL carve-out. The canonical AttestationType predicate URLs are
exempt: they live under the reserved prefix but are accepted from any registered
key. The operator restriction targets novel URLs minted under the reserved
prefix, not the core vocabulary.
Consequence for vault-publication. The Diogenes Vault predicate
https://schemas.trustdiogenes.com/predicates/vault-publication/v1 is not
currently an AttestationType member, so it does not receive the core-URL
carve-out. As the code stands, a Vault deployment publishing under this
predicate receives HTTP 403 unless it signs with the Diogenes operator key.
Two resolutions are possible, and the choice is not yet made — see #442:
- Add
vault-publicationtoAttestationType, so it inherits the core-URL carve-out and any registered key may publish it. - Move the predicate to a non-reserved prefix that Vault deployments control.
Until one is chosen, treat operator-key signing as a prerequisite for Vault publication.
Password Protection¶
Keys can optionally have a password registered. When a password is set, attestation submissions must include the password. This provides a second factor beyond key possession.
Manifest Format¶
A manifest is a JSON document that binds a document's metadata to its attestation graph:
{
"document": {
"title": "Example Document",
"content_hash": "sha256:abc123..."
},
"attestations": [
{
"id": "att-001",
"type": "authorship",
"scope": { "content_hash": "sha256:abc123..." },
"signer_key_fingerprint": "sha256:def456...",
"signature_algorithm": "ed25519",
"signature": "base64-encoded-signature",
"timestamp": "2026-01-15T10:30:00Z",
"intent_statement": "I authored this document."
}
]
}
The content_hash is the SHA-256 hex digest of the source document's bytes. The manifest hash is computed separately from the content hash and used for log references.
Verification Protocol¶
Layer 1: Cryptographic Verification¶
- Parse and validate the manifest structure.
- For each attestation: a. Look up the signer's public key by fingerprint. b. Reconstruct the deterministic signing payload. c. Verify the signature using the public key and stated algorithm.
- Verify the source hash matches the manifest's content hash.
Layer 2: Key Status Verification¶
- For each signer key: a. Check that the key was registered before the attestation timestamp. b. Check the current key status (active, succeeded, revoked, expired). c. For succeeded keys, verify the succession chain is valid.
- Flag any attestations signed by revoked or unregistered keys.
Layer 3: Subjective Trust Assessment¶
- Load the verifier's trust configuration (trust anchors, depth limits, decay parameters).
- For each signer key: a. Traverse the endorsement graph from the signer to the verifier's trust anchors. b. Compute trust score based on path length, endorsement categories, and decay. c. Apply the verifier's minimum trust threshold.
- Report trust assessment per attestation.
Layer 3 is optional and verifier-defined. Different verifiers may reach different trust conclusions for the same document.
Transparency Log Structure¶
Hash Chain¶
Each log entry links to its predecessor via the previous_hash field:
Entry 1: previous_hash=null, entry_hash=H(...)
Entry 2: previous_hash=H1, entry_hash=H(...)
Entry 3: previous_hash=H2, entry_hash=H(...)
There are two entry-hash recipes. Every entry carries a
chain_version field (log_entries.chain_version, also on the wire and
in federation replication) naming the one its digest commits to. A
verifier dispatches on that field; an entry served without it is
version 1.
Version 1 (pre-#514):
This recipe does not cover log_entries.timestamp. A single
UPDATE log_entries SET timestamp = ... WHERE id = ... therefore left
every digest, every previous_hash link, every Merkle root, every OTS
proof and every STH signature intact — silent, undetectable backdating,
against the log's temporal-anchoring claim (finding M-12). It is
preserved unchanged as a first-class verification lens because every
entry written before the transition must keep verifying; rewriting or
re-signing published history is exactly what a transparency log must
not do.
Version 2 (#514): the preimage is a domain-separated canonical envelope that binds the timestamp:
_DOMAIN = b"diogenes.log.entry.v2\x00"
entry_hash = SHA256(
_DOMAIN + canonical_json({
"payload": payload,
"previous_hash": previous_hash, # explicit JSON null on genesis
"timestamp": canonical_timestamp, # "YYYY-MM-DDTHH:MM:SS.ffffffZ", UTC
})
)
RFC 8785 output for a dict always begins with {, so no v1 preimage can
equal a v2 preimage — the two live in disjoint byte spaces by
construction. All three inputs go through the canonicalizer inside one
object, so field boundaries are unambiguous. The stored and wire
spelling of timestamp is unchanged (isoformat()); only the hashed
form is normalised.
chain_version is a dispatch hint, not a trust anchor: the stored
digest already commits to a recipe, so relabelling a row makes
verification fail — it cannot make a forged row pass.
Timestamp ordering. Version-2 entries are timestamp-monotonic by
construction: the writer clamps forward against the previous entry
inside the append advisory lock and logs any backwards clock step.
Verifiers treat a decrease as an integrity error only when the later
entry is v2 — v1 timestamps came from a now() column default evaluated
at transaction start, so a transaction that began earlier but acquired
the append lock later legitimately holds a higher id with a lower
timestamp. A v1 entry appearing after a v2 entry is a warning, not an
error: rolling deploys interleave old and new writers.
Transition boundary. The first version-2 entry is
min(id) WHERE chain_version = 2; everything below it is version 1 and
its timestamp remains malleable — a documented residual, not a
regression. Storage-layer WORM controls remain the real defence against
an attacker holding UPDATE/DELETE on log_entries; timestamp
binding makes single-row backdating detectable, not impossible.
Event Types¶
| Event Type | Description |
|---|---|
key_registration |
New key registered |
key_succession |
Key replaced by successor |
key_revocation |
Key revoked |
attestation |
Document attestation recorded |
endorsement_offer |
Endorsement offered |
endorsement_acceptance |
Endorsement accepted |
endorsement_withdrawal |
Endorsement withdrawn |
acceptance_revocation |
Endorsement acceptance revoked |
endorser_revocation_alert |
Alert about a compromised key |
Temporal Anchoring¶
Log entries can be anchored to Bitcoin via OpenTimestamps:
- The server submits the latest entry hash to the OTS service.
- The OTS service returns a proof that can be verified against the Bitcoin blockchain.
- The proof is stored alongside the log entry.
This provides a timestamp that does not depend on server-controlled clocks.
Merkle Tree Heads¶
The log supports signed tree heads for efficient audit:
- All entry hashes are arranged in a Merkle tree.
- The tree root is signed by the operator key.
- Auditors can verify entry inclusion via Merkle proofs without downloading the full log.
Leaf and node hashing are versioned (finding L-6,
#514). Version 1
hashes both the same way, which is the RFC 6962 §2.1 exposure: leaf(x)
and node(l, r) coincide whenever x == l || r. Version 2 applies the
RFC 6962 prefixes:
v1: leaf(d_hex) = SHA256(d_hex)
node(l_hex,r_hex) = SHA256(l_hex || r_hex)
v2: leaf(d_hex) = SHA256(0x00 || d_hex)
node(l_hex,r_hex) = SHA256(0x01 || l_hex || r_hex)
Operands stay hex strings and both outputs are 64 hex characters, so every wire shape, audit-path format and stored column width is unchanged.
Severity, stated plainly so it is not misread: every real leaf preimage is a 64-character digest while an interior-node preimage is 128, and the digest function cannot emit a 128-hex value — so the second preimage is not constructible against real leaves. This is spec conformance and defence-in-depth, which is why the lens flip is sequenced as a separate release behind the M-12 fix.
Unlike the entry hash, the Merkle lens is not a property of a log
row — leaves are just entry digests, so any tree size is computable
under either lens. It is therefore a parameter, recorded on the
artefacts that persist a computed result: merkle_tree_state.tree_version
and signed_tree_heads.tree_version, plus tree_version on inclusion
proofs and an optional ?tree_version= on GET /log/consistency so an
auditor can reproduce a root they pinned before the transition. The
STH's signed message (f"{root_hash}:{tree_size}:{timestamp}") is
deliberately unchanged, so tree_version on an STH is an unsigned hint.
Duplicate-last padding is retained. Diogenes adopts RFC 6962 domain separation and deliberately not RFC 6962 tree shape: the real unbalanced-tree shape would invalidate every existing audit path, consistency proof and pinned STH for a property L-6 does not ask for.
Endorsement Protocol¶
Offer¶
{
"event_type": "endorsement_offer",
"payload": {
"endorser_fingerprint": "sha256:...",
"endorsed_fingerprint": "sha256:...",
"category": "x-diogenes:human_attestation",
"valid_until": "2027-01-15"
}
}
Acceptance¶
The endorsed party accepts, activating the endorsement after the activation delay period.
Categories¶
| Category | Description |
|---|---|
x-diogenes:human_attestation |
Personal knowledge of the key holder's identity. |
x-diogenes:institutional_endorsement |
Organizational/institutional verification of identity. |
Sybil Defense Mechanisms¶
- Endorsement capacity: Each key has a limited number of endorsements it can issue, proportional to its own trust level.
- Activation delay: Endorsements do not become active immediately, providing time to detect compromises.
- Over-capacity discounting: When a key exceeds its capacity, all its endorsements are discounted.
- Privilege threshold: Keys must reach a minimum trust threshold before they can endorse others.