YUME Transport Protocol 1 kernel
This is the normative development contract for the YTP/1 kernel, session engine, native providers and internal native endpoint.
The authority for this contract is src/ytp/,
src/engine/session_engine.*, src/runtime/native_endpoint.*, and the
provider sources openssl_security_provider.*, ytp1_crypto.*,
tls13_secure_channel.*, h2_duplex_carrier.*, h2_web_front_door.*,
ytp1_h2_admission.*, cover_site.* and asio_* under src/providers/,
together with their focused tests and the checked-in kernel vectors.
Source and executable tests outrank this document if they disagree.
The kernel implements canonical encodings, structural validation, bounds, stream rules,
fixed suite metadata, domains, and key-schedule input encoding. The session
engine implements the authenticated record lifecycle and multiplexing over a
carrier. Native providers implement the fixed hybrid handshake, a memory-BIO
TLS 1.3 secure channel, duplex H2 carrier behavior, client TCP ByteChannel, and
direct TCP/connected-UDP routing. A native FrontDoor composes accepted TCP,
TLS, genuine static cover and admission into the H2 carrier. The internal
NativeEndpoint composes both bootstrap roles, protected schema-1 credentials,
per-identity named-handler authorization, bounded automatic server accepts,
session ownership and shutdown drain.
It also composes configured direct TCP/UDP adapters whose schema-1
destinations are authorized before resolution and for every resolved address.
The experimental schema-1 C ABI backend carries named streams, named packets
and routed TCP/UDP requests, while refusing adapter declarations.
Development yumed and yume processes compose direct routes, SOCKS5 CONNECT
and UDP ASSOCIATE, forwards, server modules and managed Linux TUN.
Qualification is tracked in implementation status.
Conventions and compatibility
The key words MUST, MUST NOT, SHOULD, and MAY describe the behavior accepted or emitted by the current kernel.
All multibyte integers are unsigned and big-endian. Lengths count octets. Decoders require exact input consumption: missing bytes are truncated input and extra bytes are trailing data. Reserved values and bits fail closed. Bounds are checked before variable-length data is copied or returned.
YTP/1 is wire version 1. It has no compatibility path, downgrade, alias, or
suite negotiation for YUME transport v2, AUTH v2, relay v2, or their domains.
Product version, configuration schema 1, C ABI v1, provider versions, and
YTP/1 are independent version axes.
H2 admission v1
Admission proves possession of the separate 32-byte admission key before the
server starts YTP AUTH. It does not authenticate a transport identity or grant
a service. providers/ytp1_h2_admission.* owns this construction;
admission/h2_admission.* owns the shared path parser, HMAC primitive and
bounded replay cache. The client carrier builds the proof from its active
TLS channel. providers/h2_web_front_door.* verifies admission on the actual
accepted TLS connection before transferring it to the H2 carrier.
The proof path is / followed by 64 lowercase hexadecimal token characters,
/, and 64 lowercase hexadecimal nonce characters. No query, trailing data,
uppercase hex or alternative encoding is accepted. The nonce is 32 fresh
cryptographically random bytes for each client carrier creation.
The HMAC-SHA256 key is exactly 32 bytes. Its canonical input is:
u16 domain_length
u8 domain[domain_length] = "yume/ytp/1/h2-web/admission/v1"
u16 server_name_length
u8 canonical_server_name[server_name_length]
u16 exporter_length = 32
u8 exporter[32]
u8 nonce[32]
The server name is lowercase ASCII DNS text, at most 253 bytes, with nonempty
labels of at most 63 bytes, no trailing root dot, and no leading or trailing
hyphen in a label. The client uses the same name its TLS provider authenticated;
the server checks HTTP authority against received TLS SNI and its listener port.
Both obtain 32 exporter bytes locally from the completed TLS connection using
EXPORTER-yume/ytp/1/h2-web-admission/v1 and an empty context. The exporter is
never a peer-supplied field. Token comparison is constant time.
Wall-clock buckets and browser evidence-profile IDs are absent. The exporter binds the proof to one TLS connection, avoiding a clock-synchronization requirement. Browser geometry remains independent of this admission encoding. It shares no field or domain with transport v2’s admission token.
A native front door MUST verify the proof and reserve its nonce atomically in one process-shared bounded replay cache before promotion. Cache expiry uses monotonic time; duplicates, saturation, clock regression, overflow and failed allocation refuse admission without evicting a live nonce. At most one successful carrier promotion is allowed for a TLS connection throughout its lifetime. A TTL cache alone cannot enforce that rule after an entry expires. Missing, invalid, replayed or resource-exhausted admission follows the genuine configured cover path and never starts AUTH. The native FrontDoor implements these ingress rules with a configured immutable static website; admission alone does not authenticate a YTP peer.
At the static-site boundary, methods other than GET and HEAD receive the cover reference’s fixed 405 response, including failed CONNECT admission. This does not change proof verification, replay reservation or successful promotion. GET and HEAD retain configured route and not-found handling.
Streams and frames
Stream identifiers
A stream identifier is an unsigned 31-bit integer in the range 0 through
0x7fffffff.
- Stream
0is the session-control stream. - A client owns odd application stream identifiers, beginning with
1. - A server owns even application stream identifiers, beginning with
2. - Each endpoint advances its locally owned identifier by two. Advancing past the 31-bit range is exhaustion and MUST fail.
- OPEN ownership validation rejects stream zero and an application stream not owned by the opener.
Frame decoding enforces control-versus-application stream class. It does not infer the peer role and therefore does not itself enforce OPEN ownership; the caller MUST apply the separate ownership check when admitting an OPEN.
Frame header
Every frame begins with this canonical 12-octet header:
u8 version = 1
u8 record_type
u16 flags = 0
u32 stream_id
u32 payload_length
The stream identifier’s high bit and every flag bit are reserved and MUST be
zero. payload_length MUST NOT exceed the bound supplied by the caller. The
kernel default is 1 MiB (1 << 20), but a runtime MAY impose a smaller bound.
The record decoder accepts exactly 12 + payload_length octets and returns a
borrowed view of the payload.
H2-duplex carrier framing v1
The fixed YTP/1 suite carries each record through a carrier-private framing envelope before WebSocket and HTTP/2 encoding:
u8 magic[4] = "YCR\\0"
u8 envelope_version = 1
u8 flags = 0
u16 reserved = 0
u32 record_length
u8 record[record_length]
record_length is unsigned big-endian, must be nonzero, and must not exceed
the carrier’s immutable record bound. The receiver validates all 12 header
octets before allocating or retaining the record payload and rejects wrong
magic/version, nonzero flags/reserved bytes, oversize lengths, truncated
records, and terminal trailing state. The header’s HTTP/2 receive credit may
be retired after validation; payload credit stays move-owned by the resulting
ReceivedRecord until the engine or application releases it.
This envelope preserves one complete Carrier record across arbitrary secure channel reads and WebSocket message fragmentation/coalescing. WebSocket message boundaries and captured browser geometry are never YTP semantics and may change with an independently qualified profile. This framing is not a cryptographic or KDF domain and does not provide authentication by itself.
The implemented record type and stream-class assignments are:
| Value | Record | Required stream class |
|---|---|---|
| 1 | AUTH |
control (0) |
| 2 | AUTH_RESULT |
control (0) |
| 3 | CAPABILITIES |
control (0) |
| 4 | OPEN |
application (nonzero) |
| 5 | DATA |
application (nonzero) |
| 6 | PACKET |
application (nonzero) |
| 7 | CLOSE |
application (nonzero) |
| 8 | CONNECTION_CREDIT |
control (0) |
| 9 | STREAM_CREDIT |
application (nonzero) |
| 10 | REKEY_INIT |
control (0) |
| 11 | REKEY_ACK |
control (0) |
| 12 | PING |
control (0) |
| 13 | PONG |
control (0) |
Unknown record types are rejected. The kernel defines payload codecs only for OPEN, CAPABILITIES, credit updates, and the standalone AUTH TLV format below. It does not yet define or validate complete payload semantics for every record in this table.
Post-AUTH record protection
AUTH and AUTH_RESULT flights use the canonical frame directly. After AUTH, the carrier record for every frame except REKEY_ACK has this envelope:
u8 envelope_schema = 1
u24 reserved = 0
u32 directional_epoch
u64 directional_sequence
u8 aead_ciphertext_and_tag[]
The AEAD plaintext is exactly one canonical YTP/1 frame. Epoch and sequence form the provider’s record-key token and are bound into its AAD. Sequence is global and strictly increasing for each direction; it does not restart when the epoch advances. A token is accepted exactly once and the provider derives a fresh AES-256-GCM key and nonce for it. A malformed envelope, wrong epoch, replayed or out-of-order sequence, invalid tag, or malformed plaintext is a terminal protocol failure.
REKEY_ACK is the sole post-AUTH exception: it is carried as one bare canonical YTP/1 control frame in Active state. Its fixed provider payload contains a candidate-new-root HMAC described under directional rekey below, and the TLS carrier still protects the outer channel. A protected REKEY_ACK and every other bare post-AUTH frame are rejected. The exception permits opposite directions to rekey simultaneously without retaining a retired root merely to open a crossed acknowledgement.
OPEN payload
An OPEN payload is at most 512 octets:
u8 schema = 1
u8 service_kind
u8 transport
u8 address_kind
u16 service_name_length
u16 destination_length
u8 service_name[service_name_length]
u8 destination[destination_length]
service_kind is 1 for a byte stream or 2 for a packet channel.
transport is 0 for none, 1 for TCP, or 2 for UDP. address_kind is
0 for none, 1 for IPv4, 2 for IPv6, or 3 for DNS.
The service name MUST contain 1 through 128 octets in canonical lowercase
ASCII namespace form. A name is one or more segments separated by .. Every
segment begins and ends with an ASCII lowercase letter or digit; - and _
MAY occur only inside a segment. Uppercase, Unicode, empty segments, control
bytes, /, and : are invalid. Names are compared as exact octets with no
case folding or normalization.
A named service with no destination MUST set both transport and address kind to zero and use a zero-length destination. Otherwise the transport and address kind MUST both be nonzero, the port MUST be nonzero, and:
- a byte-stream destination MUST use TCP;
- a packet-channel destination MUST use UDP;
- IPv4 is
u16 port || 4 address octetsand has destination length 6; - IPv6 is
u16 port || 16 address octetsand has destination length 18; - DNS is
u16 port || u8 dns_length || dns_name, anddns_lengthMUST equaldestination_length - 3.
A DNS name contains 1 through 253 ASCII octets. It MUST be lowercase, consist
only of a through z, 0 through 9, hyphen, and dot, and contain labels
of 1 through 63 octets. A label MUST NOT begin or end with a hyphen. Empty
labels, a leading or trailing dot, uppercase, non-ASCII input, and a trailing
root dot are rejected. The kernel performs no IDNA conversion.
OPEN acceptance
Both per-stream receive windows begin at zero. Sending OPEN does not accept it and the opener MUST NOT send a stream-credit update, DATA, or PACKET until it receives the receiver’s first STREAM_CREDIT. The receiver MUST first complete per-OPEN authorization and handler acceptance; a destination-bearing service MUST establish its authorized destination connection before accepting. The receiver’s first STREAM_CREDIT is the authenticated acceptance barrier. A refusal uses terminal CLOSE and grants no stream credit. Authorization refusal, including a denied resolved destination address, uses the unauthorized close code. Failure to establish an otherwise authorized route uses the handler-failure close code.
After receiving acceptance, the opener publishes its initial STREAM_CREDIT before reporting successful OPEN to its application. The receiver can then write within that reverse-direction grant. Pending OPENs count against the session’s pending-open and stream bounds until accepted or refused. Local cancellation before acceptance sends a terminal CLOSE and completes that pending OPEN once; crossed acceptance cannot revive it. If OPEN is still unpublished behind the outbound rekey barrier, cancellation drops that OPEN without sending CLOSE for a stream the peer has never observed.
CLOSE payload
A CLOSE payload is exactly one octet:
u8 close_code
The codes are:
| Code | Meaning | Effect |
|---|---|---|
| 0 | directional FIN | The sender will transmit no more DATA or PACKET records on this stream. The peer may continue writing in the other direction. |
| 1 | unauthorized | Terminal OPEN rejection or stream abort. |
| 2 | unsupported | Terminal OPEN rejection or stream abort. |
| 3 | handler failure | Terminal service failure and stream abort. |
| 4 | application abort | Terminal abort requested by the application. |
| 5 | settled | Ordered confirmation that the sender will publish no further records for this stream. |
Code 0 closes only the sender’s write side. Already accepted inbound records remain readable. After both directional FINs and draining queued inbound data, each side sends code 5 and retains the stream’s closing state until receiving the peer’s code 5. A crossed terminal CLOSE does not discharge that peer settlement obligation, even if the local code 5 has already been sent. Returning application-held credit after closure restores connection credit only.
Codes 1 through 4 terminate both directions and fail pending OPEN and I/O. The receiver sends code 5 after settling application ownership. The initiator retains the closing entry until that acknowledgement arrives. If terminal CLOSEs cross, each side sends code 5 and waits for the other’s code 5. A side sends at most one settlement acknowledgement for each stream.
A terminal CLOSE or code 5 MUST follow every already-published record for that stream in the ordered record direction; no later DATA, PACKET or STREAM_CREDIT may be published in that direction. While waiting for the peer barrier, a locally closed stream validates crossed records against its service kind and remaining granted credit. Crossed DATA/PACKET is discarded and returns only connection credit; crossed stream credit remains bounded and cannot reopen the stream. Closing entries count against the session stream bound, so a peer that withholds settlement cannot cause unbounded tombstone retention.
An unknown stream ID, a repeated FIN, an unexpected or repeated settlement, an unknown close code, data after the peer’s FIN, or DATA, PACKET or STREAM_CREDIT after the peer’s terminal barrier is a protocol failure. A required crossed settlement acknowledgement remains valid after terminal CLOSE. Refusal does not invalidate other streams or prevent a subsequent authorized OPEN on the same session.
Capability manifest
A canonical capability manifest is at most 64 KiB and contains at most 256 entries:
u8 schema = 2
u8 flags = 0
u16 entry_count
u32 max_epoch_bytes
repeat entry_count:
u8 service_kind
u8 flags = 0
u16 service_name_length
u32 max_concurrent_streams
u8 service_name[service_name_length]
Service kinds and names have the same rules as OPEN.
max_concurrent_streams MUST be in 1..1048576 inclusive.
Entries are ordered first by unsigned service-name octets and then by numeric
service kind. The encoder sorts entries into that order. The decoder rejects
out-of-order entries and duplicate (service_name, service_kind) keys. The
same name MAY appear once for each distinct service kind. An empty manifest
that accepts 1 MiB epochs is the eight-octet value 02 00 00 00 00 10 00 00.
max_epoch_bytes is the most protected payload the sender accepts in one
directional epoch, in either direction. It MUST be a power of two from 1 MiB
(1048576) through 64 MiB (67108864). Each side advertises its own value in
its AUTH flight, and both directions of the session use the smaller of the two
values, so neither side sends or accepts a larger epoch than the other allows.
Schema 1, which had no epoch field, is rejected.
A capability is authenticated advertisement, not authorization. A runtime MUST still apply its resource and per-OPEN authorization policy.
Flow-credit update
Both connection-credit and stream-credit updates use exactly one big-endian
u32 increment. An increment MUST be in 1..1073741824 (1..2^30)
inclusive. Zero, a larger value, a shorter payload, and a longer payload are
rejected.
A receiver MUST NOT let the credit it has granted and the peer has not yet used exceed 2^30 octets on one stream or on the connection. A sender that would hold more treats the update as a protocol failure. Each side sizes its own receive windows, so a sender does not compare a grant with its own configuration.
The kernel encodes individual increments only. Window accounting, overflow handling, backpressure, and when credit is returned are runtime responsibilities and are not specified by this codec.
The native engine starts each stream’s receive window at 256 KiB and the
connection’s at 4 MiB, both capped by the byte budget
(limits.max_queued_bytes). It returns consumed credit in one update once half
a window is consumed, or once the window less one frame is consumed if that
comes first, so a peer holding less than a frame of credit never waits for it.
A packet cannot be split to spend the last of a window, so on a packet
channel the halves are those of the whole packets that fit, measured by the
largest packet the peer has sent. Half the whole window could exceed that by
less than a packet, and a sender left short of one packet would then get a
single return, about half its window, per round trip. When two returns on a window come less than two round trips apart, the window
was limiting the sender, and the engine doubles it, as Chromium does. A stream
window can reach two thirds of the budget and the connection window the whole
budget. The connection window stays at least one and a half times the largest
stream window, and no window shrinks. If the peer has less connection credit
than one maximum frame, the engine returns any already-consumed connection
credit even below the usual return point. It checks both record arrival and
application release, since the last record can arrive on an unread stream
after useful records have been consumed. Application-held bytes keep their
credit until release; an early return neither enlarges the window nor changes
the configured return count. Otherwise, consumed credit below the return point
waits at the receiver, so a sender held back by a window that can no longer
grow moves about half of it per round trip. limits.credit_returns_per_window,
2 by default, 4 or 8, makes such a window return credit every half, quarter or
eighth of itself, so the sender moves about a half, three quarters or seven
eighths of it per round trip and the receiver sends that many more credit
records. The engine times round trips locally,
from its AUTH flight to the peer’s answer and from each REKEY_INIT to its ACK,
keeps the smallest sample of the last ten seconds, and sends no record to take
them.
The native engine does not fail a session because its own records find the
outbound byte budget full. DATA, PACKET, OPEN and pending writes leave room for
one credit or CLOSE record, beside the room kept for directional rekey. When
the budget is full, an acceptance or returned STREAM_CREDIT, a
CONNECTION_CREDIT, a FIN, a terminal CLOSE or a settlement becomes an
obligation the engine still owes. It sends owed records as earlier records
finish sending, under the ordering rules above. A window keeps at most
limits.credit_returns_per_window grants waiting to be sent, and further
returns join the next grant, so each stream waits on a bounded number of
records. A closing stream keeps its entry, which counts against the stream
bound, until everything it owes has been sent. A local OPEN that does not fit
fails alone with ResourceExhausted.
Fixed security composition
YTP/1 exposes one exact suite identifier:
YTP/1:TLS13:H2:ED25519+ML-DSA-87:X25519+ML-KEM-1024:HKDF-SHA256:AES-256-GCM
This is a protocol constant, not a negotiation offer or provider-selection string. The required 24-octet security-parameter value is:
01 01 01 01 01 01 01 01 01 20 0c 10 20 20 20 20
06 20 06 20 00 40 01 00
In order, these octets declare parameter schema 1; composite authentication; Ed25519; ML-DSA-87; hybrid establishment; X25519; ML-KEM-1024; HKDF-SHA256; AES-256-GCM; a 32-octet AEAD key; a 12-octet nonce; a 16-octet tag; 32-octet access PSK, exporter, X25519 shared secret, and ML-KEM shared secret; 1568-octet ML-KEM-1024 public key and ciphertext; at most 64 prepared/in-flight ratchet epochs; mandatory one-use message keys; and a zero reserved octet. Every octet MUST match exactly.
The provider openssl35.ytp1-security enforces this exact composition. It
creates a private
OpenSSL library context, loads only the instance-local default provider,
requires every named algorithm, and has no provider fallback or suite
negotiation. The native programs and C ABI use this provider. Its production
qualification remains open.
Domains
These byte strings are fixed, case-sensitive constants:
yume/ytp/1/auth-signature/v1
yume/ytp/1/composite-identity/v1
yume/ytp/1/auth-role/v1
yume/ytp/1/transcript/v1
yume/ytp/1/root/v1
yume/ytp/1/psk/v1
yume/ytp/1/handshake-confirmation/v1
yume/ytp/1/c2s-root/v1
yume/ytp/1/s2c-root/v1
yume/ytp/1/message/v1
yume/ytp/1/aad/v1
yume/ytp/1/ratchet/v1
EXPORTER-yume/ytp/1/channel-binding/v1
The kernel embeds the root domain in the key-schedule input and checks the
domain constants in its focused test. The security provider uses these domains
for composite identity, transcript, signatures, PSK authentication, hybrid
handshake confirmation, directional roots, per-record material and AAD, and
directional rekey. Implementations MUST NOT substitute formulas or
yume/2.0/... labels from the transport-v2 design.
AUTH TLV encoding
The standalone canonical AUTH encoding is at most 64 KiB:
u8 schema = 1
u8 message_type
u16 field_count
u32 field_bytes
repeat field_count:
u16 field_id
u16 flags
u32 value_length
u8 value[value_length]
field_bytes MUST equal the exact remaining payload size. Field IDs MUST be
nonzero and strictly increasing. Duplicate or out-of-order fields are
rejected. At most 32 fields are permitted.
Flag bit 0 (0x0001) marks a critical field; all other bits are reserved.
Unknown critical fields are rejected. A bounded unknown noncritical field is
retained by decode and survives re-encoding. Every implemented known field is
critical.
The accepted message-type values are:
| Value | Name |
|---|---|
| 1 | CHALLENGE |
| 2 | RESPONSE |
| 3 | ACCEPTED |
| 4 | REKEY_INIT |
| 5 | REKEY_ACK |
The encoder inserts fields 1 through 3; callers MUST NOT supply them:
| ID | Field | Required value |
|---|---|---|
| 1 | suite ID | exact suite string above |
| 2 | security parameters | exact 24-octet value above |
| 3 | sender role | one octet: client 1 or server 2 |
| 4 | transcript hash | 32 octets |
| 5 | identity | 1 through 16384 opaque octets |
| 6 | composite signature | 4691 octets: Ed25519 64 plus ML-DSA-87 4627 |
| 7 | ML-KEM public key | 1568 octets |
| 8 | ML-KEM ciphertext | 1568 octets |
| 9 | X25519 public key | 32 octets |
| 10 | capability manifest | a canonical manifest as specified above |
| 11 | nonce | 32 octets |
| 12 | PSK authenticator | 32 octets |
| 13 | hybrid key confirmation | 32 octets |
The codec validates field widths and canonical capability bytes. It treats an identity as bounded opaque bytes and does not parse a composite identity. It does not itself enforce a message-specific set of required or forbidden fields: an encoding containing only fields 1 through 3 is structurally valid for any known message type.
The OpenSSL provider accepts only these exact critical caller-field sets, in addition to mandatory fields 1 through 3:
| Message | Sender | Exact caller field IDs |
|---|---|---|
| CHALLENGE | server | 4, 5, 6, 7, 9, 10, 11 |
| RESPONSE | client | 4, 5, 6, 8, 9, 10, 12, 13 |
| ACCEPTED | server | 4, 6, 13 |
It rejects missing, additional, noncritical, reordered, noncanonical, wrong-role, and wrong-flight fields. Both Ed25519 and ML-DSA-87 signatures must verify. RESPONSE also requires the authorized key’s access-PSK authenticator, X25519 and ML-KEM-1024 contributions, and hybrid-root confirmation; ACCEPTED requires a distinct server confirmation. The provider uses the fixed binary rekey payload below rather than AUTH TLV message types 4 and 5.
Canonical key-schedule input
The kernel validates and encodes, but does not derive a key from, a canonical key-schedule input. The encoding is at most 128 KiB:
u8 schema = 1
u8 reserved = 0
u16 field_count = 18
repeat 18:
u16 field_id
u32 value_length
u8 value[value_length]
The fields are fixed and emitted in this order:
| ID | Value |
|---|---|
| 1 | yume/ytp/1/root/v1 |
| 2 | exact suite ID |
| 3 | initiator role, one octet |
| 4 | responder role, one octet |
| 5 | 32-octet transcript hash |
| 6 | 32-octet TLS exporter |
| 7 | client identity, 1 through 16384 octets |
| 8 | server identity, 1 through 16384 octets |
| 9 | canonical client capability manifest |
| 10 | canonical server capability manifest |
| 11 | exact required security parameters |
| 12 | 32-octet access PSK |
| 13 | 32-octet client X25519 public key |
| 14 | 32-octet server X25519 public key |
| 15 | 32-octet X25519 shared contribution |
| 16 | 1568-octet ML-KEM-1024 public key |
| 17 | 1568-octet ML-KEM-1024 ciphertext |
| 18 | 32-octet ML-KEM shared contribution |
The initiator and responder roles MUST each be known and MUST differ. Both capability manifests MUST already be canonical. There are no optional security contributors. The encoder writes into caller-owned output, reports the exact number of octets written, rejects undersized output, and rejects overlap between that output and any input field so secret material is not silently aliased or copied through an unsafe buffer.
The kernel does not provide a decoder or perform cryptography for this encoding. The security provider supplies the checked operations: it requires exact key algorithms and canonical public DER, rejects all-zero hybrid shared contributions, compares authentication values with OpenSSL’s constant-time primitive, and runs the fixed HKDF before publishing peer evidence or directional roots.
Directional rekey
REKEY_INIT and REKEY_ACK frame payloads begin with an engine-visible
big-endian u32 next_epoch, followed by a fixed provider message that repeats
and cryptographically binds the epoch. The duplicate is intentional: the
engine can enforce lifecycle and resource policy without parsing provider
private fields, while the provider independently rejects a mismatched epoch.
The 1672-octet INIT provider message is:
u8 schema = 1
u8 kind = 1
u8 direction = initiating sender role (client 1 or server 2)
u8 reserved = 0
u32 next_epoch
u8 ml_kem_1024_public_key[1568]
u8 x25519_public_key[32]
u8 nonce[32]
u8 old_root_hmac_sha256[32]
INIT is a protected YTP record under the old directional epoch. Its HMAC input binds the ratchet domain, exact suite, session/transcript binding, direction, epoch, ML-KEM public key, X25519 public key, and nonce. Acceptance requires the next exact inbound epoch, valid old-root HMAC, nonzero X25519 and ML-KEM contributions, and the fixed message shape.
The 1640-octet ACK provider message is:
u8 schema = 1
u8 kind = 2
u8 direction = original initiating sender role (client 1 or server 2)
u8 reserved = 0
u32 next_epoch
u8 ml_kem_1024_ciphertext[1568]
u8 responder_x25519_public_key[32]
u8 new_root_hmac_sha256[32]
ACK is the bare control-frame exception described above. Its confirmation is computed under the candidate new directional root and binds the ratchet domain, exact suite, direction, epoch, session/transcript binding, the full canonical INIT context excluding its old-root authenticator, ML-KEM ciphertext, and responder X25519 key. The initiator commits its outbound root only after that confirmation succeeds. The responder commits its inbound root only after INIT succeeds. INIT consumes the next old-epoch record sequence and ACK consumes none.
Until the initiator has processed ACK, it MAY keep sending protected records in the old epoch, within that epoch’s limits, each with the next global directional sequence. Its first protected record after ACK uses the new epoch. The responder keeps the old inbound root after accepting INIT and opens old-epoch records with it until the first record of the new epoch opens. It then wipes the old root and MUST treat any later old-epoch record as a protocol failure. INIT MUST travel in the current epoch: an INIT in the epoch it replaces terminates the session. A direction therefore retains at most one old inbound root, and only until the peer’s first record of the new epoch.
At most the configured bounded number of directional rekeys may be in flight. Skipped epochs, repeated INIT or ACK, an ACK without a pending outbound rekey, a protected ACK, a bare non-ACK post-AUTH frame, component mutation, or post-failure reuse terminates the session. Crossed opposite-direction rekeys need no further roots.
A directional epoch carries at most the session’s epoch size in protected
payload (the smaller max_epoch_bytes of the two manifests) and at most one
record per 2 KiB of it, 512 records for 1 MiB. A receiver MUST treat a peer
epoch that exceeds either limit as a protocol failure, counting the old
epoch’s records after INIT. The native engine starts a rotation once an epoch
has used half its payload, half its records or half its fixed 500 ms send
lifetime. It keeps sending in that epoch while REKEY_ACK is on its way and the
epoch has room, so a path on which half an epoch lasts longer than a round trip
never pauses for the ACK. Once a limit or the 500 ms lifetime is reached,
later records wait for the new epoch and keep their order. INIT does not count
toward these application thresholds.
When the peer’s REKEY_INIT arrives and this direction’s epoch has carried a
record and used half its send lifetime, the native engine starts this
direction’s rotation right after its REKEY_ACK. A peer that rekeys after a
quiet spell is about to send, and the answer then needs no round trip of
its own for a new key. This adds no exchange: the answer’s send would have
started the same rotation one round trip later.
With limits.idle_epoch_rotation, the native endpoint also starts the
rotation of an epoch that has carried a record once it is 250 ms old, without
waiting for a send. It is off by default: that exchange follows every burst of
traffic, which a browser session does not show, and the next send then
rotates the epoch itself. Either way an epoch that carried nothing is not
rotated, so an idle session’s root does not expire on its own.
A pending outbound rekey has a separate local ACK deadline. The default is
30 seconds; callers may select a positive duration up to 30 seconds. The deadline
starts before provider work and INIT queueing. Late traffic or ACK verification
fails the session, clears deferred records, and wipes provider state. An engine
owner services rekey_deadline() and expire_rekey(now); NativeEndpoint does
so with its session timer. Expiry delivery depends on the execution context
running. This policy neither changes the wire format nor establishes a high-RTT
or delivery-time guarantee.
Canonical vectors
src/ytp/testdata/ytp1_vectors.txt contains public synthetic encoding
fixtures, not credentials or cryptographic test keys. The focused executable
test checks:
- frame header
010500000000000100000003; - named OPEN
01010000000400006563686f; - TCP/DNS OPEN
01010103000a000e6469726563742e74637001bb0b6578616d706c652e636f6d; - capability manifest
020000020010000001000004000000086563686f0200000300000004756470; - the mandatory-only accepted AUTH encoding recorded in the vector file; and
- a 3665-octet synthetic key-schedule input whose FNV-1a-64 encoding checksum
is
e5f96821b0256972.
FNV-1a is used only as an encoding-regression checksum and makes no cryptographic claim.
src/ytp/testdata/ytp1_crypto_vectors.txt records deterministic cryptographic
construction vectors. Its independent Python generator uses hashlib, hmac
and cryptography AESGCM, with primitive known-answer self-checks. The native
security-provider test compares transcript and signature-input hashes, the
canonical key schedule, directional roots, PSK and confirmation proofs,
record AAD/key/nonce/ciphertext, and both directions of rekey INIT, ACK and
new-root derivation. Record cases cover empty plaintext, wide counters and
the next epoch with a continuing sequence.
All contributions are public synthetic octet strings. The corpus does not
contain provisionable identities or exercise deterministic asymmetric key
generation, signatures or encapsulation. Those primitives retain real-key
handshake and mutation tests; complete independent peer interoperability and
security review remain acceptance gates. The internal
providers/ytp1_crypto.* module owns the tested deterministic constructions;
the security provider owns authentication state, key lifetimes and ratchets.
Implemented and unfinished boundary
Implemented in the dependency-pure yume_ytp1 static library and exercised by
yume_ytp1_kernel_test:
- stream identifier construction, ownership helpers, and exhaustion checks;
- exact frame-header and whole-record length validation;
- bounded OPEN, destination, capability-manifest, and credit codecs;
- bounded canonical AUTH TLV encoding and decoding;
- exact suite, security-parameter, width, and domain constants;
- key-schedule input validation, sizing, and canonical encoding; and
- canonical encoding vectors and malformed-input tests.
Implemented in the dependency-pure session engine and its in-memory tests:
- strict AUTH flight ordering, authenticated capability confirmation, and fail-closed provider/suite/exporter checks;
- the protected-record envelope, exact directional epoch and global sequence, one-use provider-token handoff, and the sole raw REKEY_ACK admission rule;
- 31-bit odd/even multiplexing, named stream and packet services, destination policy handoff, bounded connection/stream credit, queues, control work, opens, packets, and rekeys; and
- crossed and sequential directional rekey lifecycle, cancellation, teardown, half-close, backpressure, and allocation-failure contracts.
Implemented in yume_openssl_security_provider and exercised by
yume_openssl_security_provider_test with freshly generated real keys:
- private OpenSSL context and exact Ed25519, ML-DSA-87, X25519, ML-KEM-1024, SHA-256, HMAC, HKDF, and AES-256-GCM algorithm requirements, refusing to construct over a provider that lacks them;
- canonical unencrypted PKCS#8 private keys, canonical SubjectPublicKeyInfo public keys, per-identity access PSKs, duplicate-identity rejection, exact role factories, and bounded public peer labels;
- exact three-flight hybrid authentication, transcript/exporter/capability binding, both signature components, PSK precheck, and distinct response and accepted key confirmations;
- refusal of every AUTH flight with any one field removed or shortened by an octet, or with either signature component stripped, and of each flight replayed into a fresh session or into an established one;
- directional roots, fresh per-record keys and nonces, exact token ordering, constant-time authentication comparisons, secret wiping, and cancellation; and
- fixed hybrid rekey INIT/ACK processing, crossed-direction operation, and mutation, mismatch, component-stripping, replay, epoch, and post-failure negative tests.
Implemented as separate native providers with focused tests:
yume_tls13_secure_channelwraps an engine ByteChannel with OpenSSL memory BIOs, enforces TLS 1.3 and ALPNh2, verifies client-side trust and hostname, bounds peer evidence, and exports channel binding. Its client applies the shared browser TLS profile and refuses negotiated TLS 1.2 or HTTP/1.1 before publishing a channel. Its separate server-cover entry keeps ordinary TLS 1.2/1.3 H2 traffic in a ByteChannel and rejects HTTP/1.1-only ALPN offers. A no-ALPN connection reaches the front door only to receive the reference’s fixed 239-byte refusal and close, with no H2 state, cover channel or promotion. It promotes only a settled TLS 1.3/H2 connection to the strict SecureChannel type;yume_h2_duplex_carrierimplements client priming and exporter-bound admission in extended CONNECT, bounded private record framing, flow-credit ownership, and a typed server promotion seam for an already-admitted live H2 connection;yume_asio_tcp_byte_channel_providersupplies bounded client DNS/connect, socket protection, ordered operations, cancellation, and TCP half-close.AsioTcpAcceptedChannelOwneradopts connected TCP or UNIX stream sockets through the same bounded channel implementation; it does not own a listener; andyume_asio_direct_route_providersupplies bounded TCP and connected-UDP egress behind the dependency-pure route-handler contract, with mandatory numeric-destination policy before socket creation and using the same single-runner execution context and reserved controls as native ingress.
Implemented in yume_h2_web_front_door: a native listener using
the accepted-channel owner, a configured static site loaded through confined
FileRoot reads, received-SNI/exporter verification, shared replay reservations,
bounded admission capacity and one promotion per TLS lifetime. Promotion
preserves TLS/H2 state, provider provenance and executor affinity after cover
output and timer drain. Ordinary H2 cover handling remains with the carrier.
The caller owns execution and completion drain; this ingress performs no DNS.
The internal yume_native_endpoint target composes native ingress, security,
session bootstrap and per-identity handlers. An optional route provider enables
explicitly authorized destination handlers. Schema-1 direct_tcp and
direct_udp declarations create those handlers when the provider is supplied.
Their required destinations are enforced before DNS or socket creation, an
optional application callback can only refuse more, and resolved-address policy
remains mandatory before socket creation. The engine supplies its selected provider to each routed
OPEN, and only successful endpoint creation adopts provider cancellation.
Other services require explicit bindings. The standalone runtimes compose
SOCKS5 and managed Linux packet/TUN declarations. The schema-1 ABI backend
exposes named stream/packet and routed TCP/UDP operations, while refusing all adapter
declarations. Focused integration exercises configured authenticated TCP/UDP
routes, packet boundaries, policy refusal and retained credit after drain, plus
real TCP/TLS/H2, AUTH, OPEN refusal followed by acceptance, data, rekey,
half-close and final ownership drain. This is an internal runtime seam; its
tests do not establish production qualification.
The native endpoint and its providers are internal C++ components. The native executables are installed by default; the experimental C SDK has a separate installation opt-in. Enabling the native programs or the shared ABI builds the complete native provider graph. The relay module, the GUI and other product-specific surfaces are not part of YTP/1.
Published construction vectors cover deterministic transcript, key-schedule, record and rekey calculations. Complete AUTH/rekey interoperability with an independent peer remains open. The status page owns the remaining runtime and release qualification.