Internet-Draft KSG-Core October 2026
Nam Expires 14 April 2027 [Page]
Workgroup:
Network Working Group
Internet-Draft:
draft-nam-ksg-core-00
Published:
Intended Status:
Informational
Expires:
Author:
Y. Nam
Keysingate

Keysingate Core: Channel Documents, Status Records and Anchored Time

Abstract

This document specifies the core of Keysingate: the canonical form and signature envelope of its documents, the time format and the rule that gives a signed object an upper time bound from public-network anchors, and the documents of the issuance channel -- release, delegation, block allocation, block closure, agent binding, container initiation, key inclusion, class promotion -- together with the status record that tracks a container's life. It is written so that two independent implementations produce the same bytes and reach the same verdict. The work journal kept inside a container, and its export, are specified in a companion document.

Status of This Memo

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 14 April 2027.

▲

Table of Contents

1. Introduction

The work journal and its export are specified in [KSG-JOURNAL].

An issuer hands out ranges of serial numbers; distributors pass parts of them down a channel; a holder turns a serial into a container by binding an agent's key and an artifact to it at one moment. Every step is a signed document, and a third party verifies the whole chain -- from the issuer's release to the container's identifier -- without contacting the issuer and without trusting any operator's word about time.

This document fixes what such verification needs to agree on: bytes, time, anchors, channel documents and status records. It does not specify how a container is built as a program, how keys are split or stored, which networks anchor, how they are read, or what the content of a journal page means.

2. Conventions and Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Release:
A signed declaration of a range of serial numbers by an issuer (Emission).
Block:
A range of serials sold to one holder, with a class and a term (BlockAllocation).
Container:
The object created on one serial at initiation; its identifier is derived (Section "Identifiers").
Anchor:
Evidence from a public network that a subject existed no later than a moment.
Network reader:
A component outside the core that, given an anchor of a kind it knows, confirms the anchor and returns the moment the network proves.
Offset:
Milliseconds since a container's birth (offset_ms); not a date.

3. Canonical Form and Envelope

3.1. Canonicalization

Signed structures are JSON canonicalized with JCS [RFC8785]. A signature is computed over the UTF-8 bytes of the canonical form of the document without its signatures member. An absent optional member is omitted; null and absence are different documents.

3.2. Numbers

Every number in a signed document MUST be an integer in 0 ... 2^53 - 1. A document containing a fraction, a negative number or a larger integer MUST be rejected before any signature is checked: only such numbers serialize to the same bytes in every JCS implementation.

3.3. Required Members

Every signed document of this core MUST carry "@context": "urn:keysingate:core:v3", "type" naming the document and "v": 3. A member not defined for the document type MUST cause rejection: a document two implementations would read differently is accepted by neither.

3.4. Document Hash

hash(doc) = SHA-256(JCS(doc without signatures)), written as a multihash: the string 1220 followed by 64 lowercase hexadecimal digits. Uppercase digits MUST be rejected.

4. Signatures

signatures is an array of objects {kid, alg, value}; value is base64url without padding [RFC4648].

Table 1
alg Use
Ed25519 required; strict verification [RFC8032]: non-canonical encodings and small-order keys are rejected
ML-DSA-65 optional [FIPS204]; only as a further element, never instead of Ed25519

A verification profile names required_algs (default [Ed25519]), min_anchors (an integer >= 1, default 1) and optionally now. A document passes only if every required algorithm is covered by a valid signature of a key from the set given for that document. An unknown alg, a bad encoding, a kid outside the set, or a signature that does not verify MUST cause rejection -- a failing signature is a refusal, never a gap in coverage.

5. Time

5.1. Form

A timestamp is exactly YYYY-MM-DDTHH:MM:SS.mmmZ: UTC, three digits of milliseconds, 24 characters [RFC3339]. Any other form -- no fraction, another fraction length, an offset, a leap second :60 -- MUST be rejected. With one fixed form, the order of the strings is the order of the moments.

5.2. Comparison

Moments are compared as milliseconds since 1970-01-01T00:00:00Z, not as strings. The calendar is checked: month 1-12, day within the month, leap years.

5.3. Offsets

Inside a container, time is offset_ms. It MUST NOT be presented as a date. Only the bounds carry evidential weight: below, the release's issued_at; above, an anchor (Section "Anchors and the Upper Bound").

6. Identifiers

A party is identified by an opaque URI; the core does not interpret it.

The container identifier is derived at initiation and re-derived at verification:

container = SHA-256( F("ksg:container-id:v1") || F(emission)
    || F(issued_at) || F(serial, 8-byte big-endian)
    || F(agent key) || F(artifact)
    || F(offset_ms, 8-byte big-endian) )
F(x) = (length of x, 4-byte big-endian) || x

where emission is the release id, issued_at the release's timestamp string, agent key the base64url key string of the client agent, artifact the artifact URI. The result is written as a multihash. A ContainerInit whose container differs from the derived value MUST be rejected: the identifier is evidence, not a claim.

7. Anchors and the Upper Bound

7.1. Anchor

{ "type": "<network URI>", "subject": "1220...",
  "proof": "<base64url>", "anchored_at": "..." }

The core does not parse proof. A network reader, selected by type, confirms that subject is recorded in the network and returns the moment the network proves. anchored_at is a statement, not evidence.

7.2. The Bound Rule

One procedure applies to everything that is anchored -- a journal seal, a status record, a release:

  1. every anchor MUST have subject equal to the object's hash; otherwise reject;
  2. an anchor of a kind for which the verifier has a reader MUST be confirmed; an unconfirmed one MUST cause rejection (a forgery is a refusal, not "not found");
  3. an anchor of a kind without a reader is skipped;
  4. at least min_anchors (>= 1) anchors MUST be confirmed; with fewer, the object has no upper bound -- there is no bound "by an operator's word";
  5. the bound is the earliest moment among the confirmed anchors.

7.3. Networks

Which network anchors and how it is read is outside this document. A network profile MAY make an anchor blind -- visible to outsiders only as the fact of a write; the core sees only subject and proof.

8. Channel Documents

All channel documents are major version 3 with the envelope of Section "Canonical Form and Envelope". Their JSON Schemas are published at https://schema.keysingate.com/core/v2/ (Appendix "Schemas").

8.1. Release (Emission)

Members: id (ksg:em: followed by digits), range {from, to}, block_ttl_days (>= 1), issuer, issued_at, keys, signatures. Self-signed. Trust goes only to keys present both in the release and in the verifier's own set of trusted issuer keys. issued_at is the lower time bound of everything under the release.

Lifetime. A block may be opened for base * (10 + d) / 10 milliseconds, where base = block_ttl_days * 86 400 000 and d is the depth of the channel the block passed through (0 for a block sold by the issuer). Integer arithmetic, saturating. The result MUST NOT exceed 730 days (63 072 000 000 ms) whatever the release declares. The allowance per level compensates the time a block spends travelling down the channel; the ceiling bounds how long unopened stock may wait.

8.2. Delegation

The right to allocate blocks inside a sub-range. Members: emission, range, delegate, keys, depth (0-4), parent (hash of the delegation above; absent at depth 0), delegated_at, term_ms, expires_at, signatures.

  • term_ms MUST NOT exceed 180 days; expires_at MUST be later than delegated_at and MUST NOT be later than the parent's;
  • the range MUST lie within the granting party's range;
  • one invalid link invalidates the whole chain;
  • with now in the profile, an expired or not yet started link MUST cause rejection; without now, a report MUST NOT state that the authority was in force.

8.3. Block Allocation

Members: emission, block {from, to}, class, holder, allocated_at, expires_at, prev_closure (hash of the previous block's closure; REQUIRED from the holder's second block), signatures.

  • size by class: heavy 1-1 000; light 10 000-100 000; bare is not allocated in blocks;
  • a holder's blocks are sequential and do not overlap;
  • a new block is allowed only once the previous one is used to at least 80 %, computed in integers (used * 5 >= size * 4);
  • signed by a release key or by the key at the end of a delegation chain;
  • the term belongs to the block: initiation before expires_at takes a container out of the term for good; after it, the container's status is 20; serials never initiated reach status 10.

8.4. Block Closure

Members: emission, block, used_submitted, used_not_submitted, cancelled, closed_at, signatures (the holder's). The three sets MUST cover the block exactly, without overlap; for heavy, used_not_submitted MUST be empty.

8.5. Agent Binding

Members: emission, block, agent, bound_at, signatures. A one-time binding of a block to the holder's agent key; a second binding of the same block is invalid.

8.6. Container Initiation

Members: emission, serial, binding (hash of the binding), agent, artifact, offset_ms, container, signatures. The client agent's key and the artifact are bound at one and the same moment. Signatures of the agent and of a second party (the owner or an orchestrator) -- at least two distinct parties. The serial MUST lie in the binding's block; the offset MUST NOT run past the block's lifetime; container MUST equal the derived identifier.

8.7. Key Inclusion

A key is never replaced; a new one is laid on top. Members: emission, block, predecessor (hash of the binding for the first, of the previous link after that), key, offset_ms, signatures (by the previous link's key). The valid set is the binding's key and every key of the unbroken chain; all stay valid. At most 1 024 links.

8.8. Class Promotion

Members: container, from, to, offset_ms, signatures (owner and issuer, at least two). Only upward: bare < light < heavy.

9. Classes

A class is a tariff and the form in which a container's journal opens, not an anchoring mode for life:

Table 2
Class Journal opens as May turn to anchored form Anchors
heavy anchored ("Pro") -- every seal
light unanchored ("lite") yes, by a form-change page only while anchored
bare unanchored no never

10. Status Records

A container's life is a sequence of statuses:

Table 3
Status Meaning Status Meaning
0 printed 12 opened voluntarily
1 block applied 20 initiated after the term
2 buyer's key applied 21 prescription passed
3 sub-agent's key applied 22 key lost
4 initiated 30 in dispute
5 in work 31 dispute proven
6 frozen 32 dispute unproven
7 exported 34 appealed
9 lost 35 settled
10 never activated 8, 33 reserved, no transitions

Each change is a StatusRecord: number (0 for the printing, then +1), status (after the change), event (absent in record 0), offset_ms, prev, signatures. Record 0's prev is the section root:

root = SHA-256( F("ksg:status-section:v1") || F(emission)
    || F(serial, 8-byte big-endian) )

-- the serial's address, known before the container exists. Every later record names its predecessor's hash. A change is valid only if the transition table allows it from the previous status (Appendix "Transition Table"); the table is normative and is also published as test vector 08_status_table.

A status record is anchored, and its anchor is read by the Bound Rule; without a confirmed anchor a status has no time, and a report MUST say so.

Status section export. The release, the serial and every record with its anchors, in order. Not signed as a whole: each record is signed and anchored, and the chain from the root holds the order. Verification repeats acceptance: record 0 from the address, every next record through the transition table, linked, signed, anchored; then every anchor by the Bound Rule.

11. Verification

Input: the object (a journal export, per the companion document, or a status section), the container identifier, the class or the channel it derives from, the keys, a profile, network readers.

  1. limits (Section "Limits") -- before parsing;
  2. parsing: version 3, strict members, numbers, time;
  3. channel, if presented: release -> delegations -> block -> binding -> initiation -> container identifier; a closure of the block, if attached, MUST be of that block, signed by the holder, cover it without gaps or repeats and MUST NOT cancel this container's serial; without it, the report states that the completeness of the block is not established;
  4. the object itself (companion document, or Section "Status Records");
  5. anchors by the Bound Rule;
  6. the report.

The report states accepted or rejected with a reason, and lists explicitly what is not established. A rejection is never replaced by a weakened result.

12. Versions

Documents of this core are major version 3; an implementation of this core issues and verifies only that version. Adding an optional member is a minor change; changing the meaning or requiredness of a member is a new major version.

13. Limits

Table 4
What Limit
core document, bytes 64 KiB
journal export, bytes 8 MiB
signatures per document 8
anchors per object 16
Merkle path, links 64
delegation links 5 (depths 0-4)
key inclusion links 1 024
status section records 1 024

Exceeding a limit MUST cause rejection before any signature is checked: a verifier does not take on unbounded work.

14. Extension Rule

Where a set can be accepted, do not choose: signatures, anchors and identifiers are arrays and opaque URIs. New things are added as elements, not by replacing a member. An extension MUST NOT change the meaning or requiredness of a core member.

15. Security Considerations

Time. The only upper bound is a confirmed anchor. An implementation that falls back to anchored_at, to a server clock or to any operator's statement when no anchor is confirmed violates this document; Rule 4 exists to make that fallback impossible to express.

Fail-closed. Anything unread, unknown or over a limit is a rejection. Strict member parsing and the integer rule exist because a document that two implementations read differently is an attack surface, not a compatibility issue.

Channel trust. A release is self-signed; trust comes from the verifier's own set of issuer keys, never from the document. A delegation chain is only as valid as its weakest link, and without now no statement about authority in force is made.

Post-quantum. ML-DSA-65 is added as a further signature, never as a replacement: an implementation without it still verifies Ed25519, and one requiring it states so in required_algs.

Implementation. Verification keeps no state between calls, and library code of the core returns every error as a value rather than aborting.

16. Privacy Considerations

Party identifiers are opaque URIs; the core neither resolves nor requires personal data. A container identifier is a hash and reveals the artifact only to someone who already holds it. Anchors may be blind (Section "Networks"): the network then shows only that something was written.

17. IANA Considerations

This document has no IANA actions. urn:keysingate:core:v3 is used as an opaque, byte-compared string; no URN namespace is registered by this document.

18. Normative References

[FIPS204]
National Institute of Standards and Technology, "Module-Lattice-Based Digital Signature Standard", FIPS PUB 204, , <https://doi.org/10.6028/NIST.FIPS.204>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC4648]
Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/info/rfc4648>.
[RFC8032]
Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, , <https://www.rfc-editor.org/info/rfc8032>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC8785]
Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, , <https://www.rfc-editor.org/info/rfc8785>.

19. Informative References

[KSG-JOURNAL]
Nam, Y., "Keysingate Journal Export: Pages under a Portable Seal", Work in Progress, Internet-Draft, draft-nam-ksg-journal-export-00, , <https://datatracker.ietf.org/doc/draft-nam-ksg-journal-export/>.
[RFC3339]
Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, , <https://www.rfc-editor.org/info/rfc3339>.
[RFC7942]
Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", RFC 7942, DOI 10.17487/RFC7942, , <https://www.rfc-editor.org/info/rfc7942>.

Appendix A. Schemas

JSON Schemas of every document in Section "Channel Documents" and of the status record are published at https://schema.keysingate.com/core/v2/ (common, emission, delegation, allocation, closure, binding, container-init, key-inclusion, class-promotion, status-record). A schema constrains shape, not rules: coverage, uniqueness, time bounds and signatures lie outside it. An implementation that has checked only the schema has not checked the document.

Appendix B. Transition Table

Generated from the test vector 08_status_table.json, which is generated from the reference implementation. Every pair not listed is refused. Events are those of the StatusRecord schema.

Table 5
From Event Outcome
0 apply_packet -> 1
1 apply_buyer_key -> 2
1 initiate_too_late -> 20
1 packet_term_expired -> 10
2 apply_sub_agent_key -> 3
2 initiate -> 4
2 initiate_too_late -> 20
2 packet_term_expired -> 10
3 initiate -> 4
3 initiate_too_late -> 20
3 packet_term_expired -> 10
4 open_journal -> 5
4 declare_key_lost -> 22
4 declare_container_lost -> 9
4 serve_dispute -> 30
5 record_work stays
5 record_artifact_or_rights stays
5 transfer_ownership stays
5 raise_class stays
5 full_export -> 7
5 finalize -> 6
5 open_voluntarily -> 12
5 freeze_on_owner_application -> 6
5 succeed -> 6
5 declare_key_lost -> 22
5 declare_container_lost -> 9
5 serve_dispute -> 30
6 full_export -> 7
6 open_voluntarily -> 12
6 succeed stays
6 declare_key_lost -> 22
6 declare_container_lost -> 9
6 serve_dispute -> 30
12 record_work stays
12 record_artifact_or_rights stays
12 transfer_ownership stays
12 raise_class stays
12 full_export -> 7
12 finalize -> 6
12 freeze_on_owner_application -> 6
12 succeed -> 6
12 declare_key_lost -> 22
12 declare_container_lost -> 9
12 serve_dispute -> 30
30 record_work stays
30 record_artifact_or_rights stays
30 transfer_ownership stays
30 raise_class stays
30 full_export -> 7
30 open_voluntarily -> 12
30 freeze_on_owner_application -> 6
30 succeed -> 6
30 dispute_proven -> 31
30 dispute_unproven -> 32
30 extend_review stays
30 review_deadline_passed blocks
30 settle -> 35
30 prescription_passed -> 21
31 record_work stays
31 record_artifact_or_rights stays
31 transfer_ownership stays
31 raise_class stays
31 full_export -> 7
31 open_voluntarily -> 12
31 freeze_on_owner_application -> 6
31 succeed -> 6
31 appeal -> 34
32 record_work stays
32 record_artifact_or_rights stays
32 transfer_ownership stays
32 raise_class stays
32 full_export -> 7
32 open_voluntarily -> 12
32 freeze_on_owner_application -> 6
32 succeed -> 6
34 record_work stays
34 record_artifact_or_rights stays
34 transfer_ownership stays
34 raise_class stays
34 full_export -> 7
34 open_voluntarily -> 12
34 freeze_on_owner_application -> 6
34 succeed -> 6
34 appeal_won stays
34 appeal_lost -> 31
34 extend_review stays
34 review_deadline_passed blocks
34 settle -> 35
34 prescription_passed -> 21
35 record_work stays
35 record_artifact_or_rights stays
35 transfer_ownership stays
35 raise_class stays
35 full_export -> 7
35 open_voluntarily -> 12
35 freeze_on_owner_application -> 6
35 succeed -> 6

While a container is blocked awaiting a decision, whatever its status:

Table 6
Event Outcome
dispute_proven -> 31
dispute_unproven -> 32
settle -> 35
prescription_passed -> 21

Terminal statuses: 7, 9, 10, 20, 22, 21. Working statuses: 5, 12, 30, 31, 32, 34, 35.

Appendix C. Implementation Status

Per [RFC7942]. ksg-core-v2 (Rust) implements this document; with the crates around it the workspace has over 750 tests; 13 test-vector sets are regenerated and compared byte for byte by the reference build. The verifier ksg-verify-v2 checks the channel from release to container identifier and status sections with anchors read through a Solana network reader.

Author's Address

Yevhenii Nam
Keysingate