| Internet-Draft | Digest-Only Evidence Logs | October 2026 |
| Jangra | Expires 11 April 2027 | [Page] |
This document describes an evidence log that records only salted digests of records held by their makers, as leaves of an RFC 9162 Merkle tree, and three constructions over it that answer questions a single inclusion proof cannot: whether a set of records shown is complete (completeness chains), what one field of a sealed record said without revealing the others (selective disclosure), and whether records were kept, checked on a sample nobody could choose (spot checks). It profiles SCITT registration so that the log keeps a digest of each Signed Statement and never the statement. It is published as running code with test vectors.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 11 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
Regulated firms must keep records (advice given, credit decisions, AI system logs, security logs) and, years later, show that a record is the one that existed at the time. A transparency log can make that showable without anyone trusting the firm or the log operator. Where records hold personal data, the log should never receive them: only a digest of a salted record leaves its maker, so the log holds nothing that must later be erased.¶
An inclusion proof answers "was this record sealed unchanged?". It does not answer "is this all of it?", "what did this one field say?", or "were the records kept?". Section 5, Section 6 and Section 7 answer those, over an unchanged RFC 9162 tree. Section 8 profiles SCITT registration for digest-only logs.¶
The key words "MUST", "MUST NOT", "SHOULD" and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.¶
Hashes are SHA-256 [RFC6234]. A digest is written as 64 lowercase hexadecimal characters. Strings are
UTF-8. The fields of a string that is hashed or signed are joined with "|", with no spaces and no escaping;
no field may contain "|". Integers are written in ASCII decimal without leading zeros. Times inside such
strings are UTC, written YYYY-MM-DDTHH:MM:SS.ffffffZ with exactly six fraction digits; a reader that
receives another representation of the same instant MUST normalise it to this form before hashing or
verifying. An account identifier is a UUID [RFC9562] in its lowercase canonical form. A kind matches
^[a-z0-9][a-z0-9.-]{0,39}$.¶
Unless stated otherwise, signatures are ECDSA over P-256 with SHA-256 [FIPS186-5], encoded as the 64-byte concatenation r || s and then base64 [RFC4648].¶
A record is a JSON object whose keys match ^[a-z][a-z0-9_]{0,63}$ at every level and whose values are
strings, integers within plus or minus (2^53 - 1), true, false, null, arrays and objects, nested at most 32
deep. Its canonical form is the JSON Canonicalization Scheme [RFC8785] of the record. Its digest is¶
record_digest = SHA-256(salt || UTF-8(canonical))¶
where salt is 16 to 64 random bytes the maker keeps with the record. A record outside the profile MUST be refused, never coerced.¶
A leaf records leaf_index (assigned by the log, from 0, without gaps), created_at (assigned by the
log, never by the client), the recording account, a kind, and record_digest:¶
leaf = "riskrouter-evidence-leaf|v2|" leaf_index "|" created_at "|" account "|" kind "|" record_digest leaf hash = SHA-256(0x00 || UTF-8(leaf))¶
Leaves form the Merkle tree of [RFC9162], Section 2.1, whose hashing is unchanged from [RFC6962].¶
A maker MAY sign its own claim, riskrouter-evidence-claim|v3|kind|record_digest|key_id|claimed_at, with
a P-256 key it registered with the log; the signature is then part of a v3 leaf string, inside the tree, so
it cannot be replaced afterwards. The log verifies the signature before recording and refuses, recording
nothing, a claim that does not verify. Heads are signed by the log over
riskrouter-evidence-head|v2|tree_size|root_hash|timestamp. The same tree is also published as C2SP
checkpoints [C2SP-CHECKPOINT], signed with a separate Ed25519 key, with tiles [C2SP-TILES], so
independent witnesses can cosign them.¶
A maker MAY place records in numbered chains. The record carries, inside itself:¶
chain_tag: 64 hex characters naming the chain, which SHOULD be HMAC-SHA256 of a secret the maker keeps
and the chain's name;¶
chain_seq: 1, 2, 3, ... without gaps;¶
chain_prev: the record_digest of the record at chain_seq - 1, or 64 zeros at position 1;¶
and the maker sends chain_tag and chain_seq beside the digest, outside the leaf. The log MUST refuse a
chain_seq that is not exactly the last position plus one, and a chain_tag already used by another
account, recording nothing.¶
On request for a tag, the log reads its current signed head first and then the chain's entries below that head's size, and signs:¶
link line = chain_seq "|" leaf_index "|" created_at "|" record_digest LF
links_digest = SHA-256(UTF-8(concatenation of the link lines from `from` to `to`))
chain payload = "riskrouter-evidence-chain|v1|" chain_tag "|" length "|" tree_size "|"
root_hash "|" timestamp "|" from "|" to "|" links_digest
¶
A verifier holding the statement and the records a maker shows checks, for every position from 1 to
length, that a record was shown which produces the digest at that position, names that tag and position,
and names the digest of the position before. A position the statement lists and nobody showed is a record
left out. The maker alone cannot hide a chained record; hiding one requires the log to sign a statement its
own tree contradicts.¶
To seal a record from which single fields can later be disclosed, the maker seals a commitment record:¶
field salt = HMAC-SHA256(secret, UTF-8("riskrouter-disclosable|1|" || name))
commitment = SHA-256(field salt || UTF-8(canonical({name: value})))
sealed record = {"format": "riskrouter-disclosable|1", "kind": kind,
"fields": {name: commitment, ...}}
¶
and records the digest of the sealed record as any record. A disclosure carries the sealed record, its salt, the proof, and, for each disclosed field, its value and field salt. The secret MUST NOT be disclosed: with it, a hidden field of low entropy could be found by trial. Field names are visible; values are not.¶
To check that records were kept without receiving them all, an auditor seals a plan naming a chain, a sample size and a future Bitcoin block height. When the block is mined, its hash is the seed, and the positions drawn are:¶
for counter = 0, 1, 2, ...
h = SHA-256(UTF-8("riskrouter-spot-check|v1|" seed "|" population "|" counter))
x = the first 8 bytes of h, big-endian, unsigned
skip if x >= 2^64 - (2^64 mod population)
position = 1 + (x mod population), kept unless already drawn
until min(sample, population) positions are drawn
¶
where population is the chain's signed length. The maker shows the records at those positions, which are
checked as in Section 5.¶
Registration under policy riskrouter-scitt-profile|1 [I-D.ietf-scitt-architecture] accepts a COSE_Sign1
[RFC9052] signed ES256 with a key the caller registered, with CWT claims iss and sub in the protected
header [RFC9597], and a hash envelope [I-D.ietf-cose-hash-envelope] whose payload is 32 bytes, so the
artifact never reaches the log. The log keeps one leaf of kind
scitt.statement whose digest is SHA-256 of the statement re-encoded with an empty unprotected header, and
returns an RFC 9942 receipt [RFC9942] over the tree root. This departs from the architecture's expectation
that a Transparency Service stores statements, deliberately: the log holds no content.¶
Each new signed head is sent to RFC 3161 [RFC3161] time-stamping authorities and anchored in Bitcoin through OpenTimestamps [OPENTIMESTAMPS]. Neither is a qualified time stamp in the sense of EU law unless its authority is.¶
A digest of a guessable record can be confirmed by anyone who guesses it; makers MUST salt records. A seal shows a record existed unchanged no later than its leaf's time; it does not show the record is true. A chain shows nothing about records that were never chained; whether every record of a kind must be chained is a policy question for the maker's supervisor. A log operator that colludes with a maker could accept a second chain position out of turn; its signed chain statements and heads, saved by holders and cosigned by witnesses, are what make such a fork evidence against the operator. On a log the maker runs itself, a chain is the maker's own word. Spot-check seeds rely on the auditor announcing a block height before it is mined, provable by the plan's leaf time.¶
The log holds digests, short kinds, account identifiers, chain tags and times. Records, salts, chain secrets and disclosure secrets stay with makers. A chain statement reveals how many records a chain holds and when they were sealed, to anyone holding its tag.¶
This document has no IANA actions.¶
(RFC Editor: please remove this section before publication.) This section follows [RFC7942].¶
An implementation is published at https://riskrouter.eu/spec with test vectors (spec-vectors.json), two independent verifiers (JavaScript and Python, standard libraries only) and a conformance suite. Its result files are the implementer's own statements; nobody certifies an implementation.¶