Protocol version citesig/0.1. Document edition of 24 September 2026.
CiteSig is a format for signing a factual claim together with the sources behind it, so that anyone who receives the claim can verify it without trusting whoever sent it.
A CiteSig attestation is a JSON object carrying a claim, a list of sources, an identifier for the signer, a timestamp, and a signature over all of it. Verification is offline: it requires the attestation and nothing else, and it succeeds or fails years later, without a network, and whether or not the signer still exists.
This specification defines the attestation format, the bytes that are signed, the verification procedure, a compact single-string encoding, and the conformance requirement.
It does not define how claims are authored, how sources are chosen, how signing keys are managed, or any revocation or expiry mechanism. Section 8 says what follows from those omissions.
This is a reconstructed edition. The protocol version it describes, citesig/0.1, is the
version implemented by the published reference implementations and carried in the v field of
every attestation in circulation. The version is unchanged and is not being revised here.
The document itself is new. It was written from the two published reference implementations —
@citesig/core on npm and citesig on PyPI — together with the conformance vectors described
in Section 10. Where this document states a requirement, that requirement is one both
implementations satisfy and at least one vector exercises.
Readers who hold an earlier edition of this document should treat any conflict between the two as a defect in one of them and resolve it against the vectors, which are executable.
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
| Term | Meaning |
|---|---|
| Attestation | The JSON object defined in Section 3. |
| Claim | The assertion being attested, carried in the claim field. |
| Signer | The identifier in the signer field, resolvable to one or more public keys. |
| Signing input | The exact byte sequence covered by the signature, defined in Section 4. |
| Verifier | Any party checking an attestation. |
All byte sequences are octet sequences. All strings are Unicode strings serialized as UTF-8.
An attestation is a JSON object. It has six REQUIRED members and MAY have any number of extension members.
| Field | Type | Requirement |
|---|---|---|
v |
string | MUST be exactly citesig/0.1. |
claim |
string | The assertion being attested. |
signer |
string | An identifier resolvable to one or more Ed25519 public keys. See Section 7. |
sources |
array | The sources supporting the claim. MAY be empty. |
issued_at |
string | The time the attestation was produced. |
sig |
string | The signature, base64url-encoded. See Section 3.3. |
Five of these — v, claim, signer, sources and issued_at — are part of the signed
content. sig carries the signature and is excluded from the signing input, for the obvious
reason that it cannot cover itself.
This specification does not constrain the internal structure of sources. Its elements MAY be
strings, objects or any other JSON value. Whatever they are, they are covered by the signature
exactly as they appear.
issued_at is a string and is not otherwise constrained by this version. Producers SHOULD use
RFC 3339 with an explicit UTC offset. Verifiers MUST NOT reject an attestation on the basis of
issued_at alone; see Section 8.3.
An attestation MAY carry additional members beyond the six above. Producers SHOULD namespace them to avoid collision with future versions of this specification.
Extension fields are part of the signed content. Adding, removing or altering one after signing
invalidates the signature, and verification MUST fail with signature-invalid. This is the
intended behavior: an extension field is not an annotation, it is part of what was attested.
Numbers in extension fields MUST be representable as IEEE 754 double-precision values. This is a consequence of Section 4 and is stated here because it constrains what a producer may put in an attestation. An implementation in a language with wider integers than a double — arbitrary-precision integers, for example — MUST reject such a value rather than rounding it. Rounding would sign a number the producer did not supply.
sig MUST be the base64url encoding, per RFC 4648 §5, of a 64-byte Ed25519 signature over the
signing input defined in Section 4.
Encoders MUST omit the = padding. Decoders SHOULD accept input with or without padding, and
MUST reject input containing characters outside the RFC 4648 §5 alphabet.
The signing input is the JSON Canonicalization Scheme serialization, per RFC 8785, of the
attestation with the sig member removed, encoded as UTF-8.
Canonicalization is what makes the signature portable: two implementations presented with the same attestation must produce the same bytes, or a signature made by one will not verify under the other.
Three consequences of RFC 8785 have caused divergence between implementations in practice and are stated explicitly.
Object members MUST be sorted by the UTF-16 code unit sequence of their names, as RFC 8785 §3.2.3 requires. For names within the Basic Multilingual Plane this coincides with code point order; for names containing supplementary characters it does not, because a surrogate pair sorts below characters in the range U+E000 to U+FFFF.
Number::toStringRFC 8785 §3.2.2.3 defers number serialization to ECMA-262. The rules that most often go wrong:
| Value | Correct serialization | A common error |
|---|---|---|
1.0 |
1 |
1.0 |
-0.0 |
0 |
-0.0 |
1e20 |
100000000000000000000 |
1e+20 |
1e21 |
1e+21 |
1000000000000000000000 |
1e-7 |
1e-7 |
1e-07 |
1e-6 |
0.000001 |
1e-06 |
Exponential notation is used when, and only when, the decimal exponent places the value at or above 10²¹ or below 10⁻⁶. The exponent carries a sign and no leading zeros.
Implementations MUST NOT rely on a general-purpose JSON serializer for numbers unless they have confirmed it matches ECMA-262 on these cases. Several do not.
NaN and the infinities have no JSON representation. An implementation encountering one while
canonicalizing MUST fail rather than emit a substitute.
Verification answers one question: was this exact attestation signed by a key that the signer
field resolves to?
It answers nothing else. In particular a valid signature does not mean the claim is true, that the sources say what the claim says they say, or that the signer is trustworthy. It means the content has not changed since it was signed, and that whoever holds the signer's private key signed it.
Verification is offline for did:key signers, which carry their own public key. For https
signers it requires the verifier to supply a resolver; see Section 7.2.
A verifier that rejects an attestation MUST report one of the following reasons. They exist so a caller can distinguish a malformed input from a tampered one, which are different problems with different responses.
| Reason | Meaning |
|---|---|
version-unknown |
v is absent or is not citesig/0.1. |
field-missing |
A REQUIRED field is absent. |
field-malformed |
A field is present but has the wrong type, or the signature does not decode to 64 bytes. |
signature-invalid |
The attestation is well formed but the signature does not verify. |
signer-unresolvable |
The signer could not be resolved to any public key. |
A verifier MUST perform these steps in order and MUST stop at the first failure. The order is normative: the version check precedes all signature work, so that an attestation of a future version is reported as such rather than as a bad signature.
field-malformed.v is not exactly citesig/0.1, fail version-unknown.v, claim, signer, sources, issued_at is absent, fail
field-missing.claim, signer or issued_at is not a string, or sources is not an array,
fail field-malformed. If sig is not a string, fail field-missing.sig and canonicalize the remainder per Section 4.sig from base64url. If it does not decode, or does not
decode to exactly 64 bytes, fail field-malformed.signer to a set of Ed25519 public keys per Section 7. If
resolution fails, or yields an empty set, fail signer-unresolvable.signature-invalid.Step 7 yielding more than one key is expected rather than exceptional: it is how a signer rotates keys without invalidating attestations made under the previous one.
The compact form encodes an attestation as a single colon-separated string, for contexts where a JSON object is impractical — an HTML attribute, a header, a QR code.
citesig:0.1:<claim>:<signer>:<source-digest>:<issued_at>:<sig>
Seven segments. The first is the literal citesig, the second the literal 0.1. Segments three,
four and six are the base64url encodings of the UTF-8 bytes of claim, signer and issued_at.
Segment five is the base64url encoding of the SHA-256 digest of the canonicalization of the
sources array, per Section 4. Segment seven is the sig value, carried through unchanged —
it is already base64url, and re-encoding it would alter the string.
A producer MUST reject an attestation whose claim, signer or issued_at is not a string,
rather than coercing it.
The compact form is lossy. The source list is reduced to a digest and cannot be recovered from it. A recipient holding only the compact form can confirm that the claim is intact and that a particular source list would match, but cannot enumerate the sources. Recovering them requires the full attestation.
A parser MUST reject a string that does not have exactly seven segments, whose first segment is
not citesig, or whose second is not 0.1.
The signer field identifies who signed. This version defines two schemes and no others; an
identifier in any other scheme MUST fail signer-unresolvable.
did:keyA did:key identifier carries the public key inside itself, so it resolves offline and with no
external dependency.
The form is did:key:z followed by the base58btc encoding of the two-byte multicodec prefix
0xED 0x01, denoting an Ed25519 public key, concatenated with the 32-byte key. A resolver MUST
reject an identifier whose multicodec prefix is not 0xED 0x01, or whose key is not 32 bytes.
This is the scheme to prefer. An attestation signed under it can be verified by anyone, forever, with no network and no trust in any third party.
httpsAn identifier beginning with https:// denotes a key published at a URL. This specification
does not define how the key is published or fetched: a verifier supplies its own resolver, and
if it supplies none, the attestation MUST fail signer-unresolvable rather than being treated
as valid or as malformed.
A resolver MUST return only 32-byte Ed25519 public keys and MUST reject anything else. A
verifier SHOULD treat the transport as the trust boundary it is — an attacker who controls what
the resolver returns controls the verification result, which is not true of did:key.
A valid signature establishes integrity and origin: the content is unchanged, and the holder of the signer's private key signed it. It establishes nothing about the truth of the claim, the adequacy of the sources, or the honesty of the signer. A signed false claim is a false claim with a valid signature.
This version defines no revocation mechanism. An attestation signed with a key that is later
compromised continues to verify. Deployments needing revocation must provide it outside this
specification — for example by publishing a revocation list a verifier consults separately, or
by using https signers and ceasing to publish the compromised key.
issued_at records when the attestation was made and carries no expiry semantics. A verifier
MUST NOT treat an old issued_at as a verification failure. A deployment that needs freshness
must apply its own policy after verification succeeds, and must not conflate the two: an
attestation that is valid but stale is a different condition from one that does not verify.
The compact form binds the source list by digest. A recipient who reconstructs a source list and finds the digest matches has confirmed that list; a recipient who has no list learns only that one was committed to. The signature covers the full attestation, not the compact string, so a compact string is evidence about an attestation rather than a self-contained one.
Any divergence in canonicalization between two implementations is a security defect, not merely an interoperability one: it permits an attestation that verifies under one implementation and fails under another, and in the worst case two different attestations sharing one signature. The requirements in Section 4 are stated in the detail they are for this reason, and the vectors in Section 10 exist to be run rather than read.
A conforming implementation MUST pass every vector in signatures.json and every vector in
canonical.json, as described in Section 10.
An implementation that produces attestations MUST additionally produce, for every accepted input, an attestation that it can itself verify.
There is no partial conformance and no conformance level. An implementation that fails one canonicalization vector produces signatures other implementations reject, which is indis- tinguishable in practice from not implementing the protocol.
Two files carry the vectors.
canonical.json is an array of canonicalization vectors. Each has a name, an input
(the value to canonicalize) and canonical_bytes_utf8 (the expected serialization). An
implementation passes by producing exactly that string.
signatures.json carries a test_key — a seed, its public key and its did:key
identifier — and an array of signing vectors. Each has an id, an expected verdict of
ACCEPT or REJECT, an attestation, and for rejections a reason drawn from the set in
Section 5.2. An implementation passes by returning the expected verdict, and for rejections the
expected reason. The file also carries a compact_form example, giving the compact encoding of
one named attestation.
The test key in signatures.json is published and deliberately trivial. It is a test fixture
and MUST NOT be used to sign anything.
The vectors are the operative definition of correctness. Where this document and the vectors disagree, the vectors govern, and the disagreement is a defect in this document.
This document was reconstructed from the published reference implementations rather than revised from a previous edition.
Every normative statement above was derived from the behavior of both @citesig/core and
citesig, and was checked against the conformance vectors. The section numbering follows the
structure the implementations themselves reference in their source comments, so that a reader
holding code that cites "spec §5.3" finds the verification procedure here.
Section 4.2 is the one place where this edition records a correction rather than a description. The two implementations did not agree on number serialization: whole-valued floats, negative zero, and the exponent thresholds were each handled differently, so an attestation carrying a numeric extension field could be signed by one and rejected by the other. RFC 8785 settles which behavior is correct. The implementations were brought into agreement, vectors were added for each case, and Section 4.2 states the rule they now both follow.
No attestation in circulation was affected: every field in production use is a string, and the divergence could only be reached through a numeric extension field.