Skip to content

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:

  1. The holder generates a new key pair.
  2. The holder authenticates as the old key (challenge-response or JWT).
  3. The holder submits a succession request with the old fingerprint and new public key.
  4. 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:

  1. The holder authenticates as the key to be revoked.
  2. The server verifies the key is active.
  3. A revocation event is appended to the log.
  4. 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:

signing_payload = json.dumps(manifest, sort_keys=True, separators=(",", ":"))

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:

https://schemas.trustdiogenes.com/predicates/

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:

  1. Add vault-publication to AttestationType, so it inherits the core-URL carve-out and any registered key may publish it.
  2. 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

  1. Parse and validate the manifest structure.
  2. 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.
  3. Verify the source hash matches the manifest's content hash.

Layer 2: Key Status Verification

  1. 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.
  2. Flag any attestations signed by revoked or unregistered keys.

Layer 3: Subjective Trust Assessment

  1. Load the verifier's trust configuration (trust anchors, depth limits, decay parameters).
  2. 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.
  3. 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):

entry_hash = SHA256(canonical_json(payload) + (previous_hash or ""))

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:

  1. The server submits the latest entry hash to the OTS service.
  2. The OTS service returns a proof that can be verified against the Bitcoin blockchain.
  3. 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:

  1. All entry hashes are arranged in a Merkle tree.
  2. The tree root is signed by the operator key.
  3. 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.