N4-D — binding a transport connection to a device Principal, by composition
Companion to: N4A_DEVICE_AUTHORITY_EVALUATION.md (the verifier), N4B_PORTABLE_EVIDENCE_BUNDLE.md
(carrying facts), N4C_DEVICE_ENROLLMENT_REQUEST.md (how the device got its grant),
IDENTITY_SEMANTICS.md §9 (node transport identity), HOME_RUNTIME_IDENTITY_PROFILE.md §6.5 ·
Issue: #2599 (N4) · Code: icn/crates/icn-identity/src/device_channel_binding.rs,
tests/device_channel_binding.rs
Truth status. Normative for the payload bytes and the verification order; LIB-TESTED (16 tests, observed 2026-10-27 after admission was routed through N4-E). No runtime, route or transport profile calls it; it is not CLI-reachable and not PRODUCTION. It adds no kernel primitive: everything below is a
DeviceActV1evaluated by N4-A unchanged, plus one equality check. Fixture identities only.
1. The question
A Home runtime that terminates a connection — a hosted workstation receiving a desk computer,
a phone, a PC — must be able to conclude "this connection is acting as device Principal K for
Subject S" without treating any of the following as identity or authority: an IP address,
a hostname, an operating-system login, a remote-desktop login, ownership of a TLS certificate,
or possession of an enrollment request. All of those gate a transport. None of them is a device
grant.
2. Why node BindingInfo is not reused
ICN already binds a Principal to a TLS certificate: BindingInfo in the network Hello
(icn/crates/icn-identity/src/bundle.rs, handlers/hello.rs; IDENTITY_SEMANTICS.md §9;
network-session-identity-binding.md). That object binds a node Principal, per connection,
inside the node handshake, and the profile already rules that it "must not be repurposed for
humans or devices" (HOME_RUNTIME_IDENTITY_PROFILE.md §6.5). Three reasons hold:
- A node Principal is a durable infrastructure identity; a device Principal is a bounded, revocable grant in a human's log. Binding the latter through the node object would make a device look like infrastructure to every relying party.
BindingInfois verified against node state; a device binding must be verified against the Subject's retained N1 facts at a relying-party position (N4-A), or it says nothing about authority.- The node object carries no Subject and no capability. A device binding is meaningful only as
"device
K, for SubjectS, with capabilityPresent, at positionE".
So the device binding is an act, not a handshake field.
3. The object
A channel binding is a DeviceActV1 (N4-A) with:
capability = Present;payload = channel_binding_v1, a domain-separated canonical object:
channel_binding_v1 :=
LP("icn.n4.channel-binding") -- distinct from every N1, GEN, N4-A, N4-B, N4-C separator (pinned)
|| u16be(1)
|| b32(channel) -- the digest of the channel the device believes it is on
|| b32(nonce) -- device-chosen or relying-party challenge; see §6
Strict decoding refuses a wrong domain or version, short input and trailing bytes. The act's own
canonical bytes, signature, act_id and bundle carriage are exactly N4-A/N4-B: nothing new is
signed, hashed or framed.
3.1 What channel means — and what N4-D does not define
channel is 32 bytes both ends compute for the same channel and different bytes for different
channels. N4-D does not say what it is the digest of. That belongs to the transport profile
that adopts this binding (for TLS: the server certificate the client saw, or a tls-exporter
value; for a pairing transport: the pairing transcript). N4-D requires only the two properties
above. It says nothing about ciphers, certificate chains, trust roots, hostnames, ports or
reconnection; those are the transport's, and none of them becomes authority.
3.2 Why Present
Of N1's four device capabilities, Present is "the device may present credentials on the
subject's behalf" (body.rs); presenting itself on a channel is the smallest such act. It is
the capability the Alpha genesis profile grants an initial device and the one an enrollment
typically asks for, so a binding needs no new grant. Sign is reserved for acts that do
something; a connection asserting its own identity does nothing yet. The act class (N4-A §7.2)
is 2, immediately settled: the relying party evaluates once, at admission, at its own position.
4. Verification, in order, each fail-closed
There are two operations, and they answer different questions:
admit_channel_binding(signed, subject, store, expected_channel)— admitting a connection now. This is the operation a live connection is admitted under. The position is not the caller's: it is the relying party's own retained clean tip, through N4-E (verify_device_act_at_admission), and the act must bind exactly it.verify_channel_binding_at(signed, subject, store, position, expected_channel)— the historical question: did this device, authorized at a position the relying party chooses, bind this channel? A valid historical answer never admits a connection now.
Both return the VerifiedDeviceAct (the act, its act_id, the evidence), never bare evidence,
and both answer relative to the facts the relying party retains (N4-A §2): neither is finality.
| step | check | refusal |
|---|---|---|
| 1 | the act is within its bounds (DeviceActV1::validate) |
Act(MalformedAct(…)) |
| 2 | the act's capability is Present |
NotAPresentation — a Sign act carrying binding bytes is a signed act, not a binding; it must not be accepted as one by accident |
| 3 | the payload decodes as channel_binding_v1 |
Payload(codec) — an arbitrary Present payload is not automatically a binding |
| 4 (admit) | N4-E current admission: the act binds exactly the relying party's retained clean tip, then N4-A authorship and authorization there | Admission(…): SubjectUnknown, AuthorityHalted, NoDelegationYet, StaleAct, RelyingPartyBehind, or `Act(BadSignature |
| 4 (historical) | N4-A verify_device_act at the position the relying party chose |
Act(…): BadSignature, ActSubjectMismatch, EvaluationPositionMismatch, or `Refused(DeviceNotAuthorized |
| 5 | the bound channel equals the one this relying party sees |
ChannelMismatch |
Pinned by the tests:
- an authorized device binding the channel it is actually on → the verified act;
- admission only at the current tip: a binding at the tip is admitted; the same device's
binding at an earlier position is a valid historical answer and is refused for admission
(
StaleAct); one past the tip isRelyingPartyBehind; - a revoked device's valid historical binding never admits a connection;
- a later retained fact (a revoke) withdraws an earlier admission;
- missing facts hold the tip back and never admit past it;
- a fork in the retained facts admits no connection (
AuthorityHalted); - a binding for one Subject never admits a connection for another, even where the device also
holds a grant (
ActSubjectMismatch); - replay is the transport profile's to refuse; the same binding keeps one
act_id, a fresh nonce gives a new one; - an act moved outside its bounds is refused as malformed by both operations;
- an authorized device binding another channel → refused (the man-in-the-middle case: the signature is genuine, the grant is in force, and the connection is still refused — a transport fact, not an authority fact);
- a device that was never authorized → refused by N4-A before the channel is compared;
- a device authorized and then revoked →
DeviceNotAuthorizedat the post-revocation position; - a
Signact with binding bytes →NotAPresentation; - a
Presentact with a non-binding payload →Payload; - the payload bytes are strict and round-trip, hand-framed from §3;
- the domain is distinct from every other separator.
5. What this keeps apart
| layer | answers | never answers |
|---|---|---|
| transport (TLS, pairing, local network) | is this channel private, and the same channel at both ends? | who is acting, under what authority |
| N4-D binding | the device Principal that signed this is on this channel | whether that device is authorized |
| N4-A over retained facts | is that device authorized for Present by S at E? |
whether the human approved anything today |
| N4-C, at the edge | the human's approvals | — |
No layer may stand in for another. "The TLS handshake succeeded" never means "this is device
K"; "this is device K" never means "K is authorized"; "K is authorized" never means "the
human is present".
6. Replay and the nonce
N4-D fixes no replay rule, because N1 has no clock and N4-A §9 assigns replay slots to the action family. A transport profile that adopts the binding chooses one of two sound rules and states it:
- challenge: the relying party issues the
nonceand accepts a binding only against the challenge it issued for this connection; or - channel-unique digest:
channelitself is unique per connection (atls-exportervalue), so a replayed binding names a channel that no longer exists.
Either way, replaying a binding onto a different channel is already refused by step 5; the nonce decides only whether the same channel may be re-bound.
7. Evaluation position
For a live connection the position is not the caller's: admit_channel_binding takes it from
the relying party's own retained clean tip (N4-E, N4E_CURRENT_ADMISSION.md), and a binding below
it is StaleAct, above it RelyingPartyBehind, never "probably fine". Only the historical
operation, verify_channel_binding_at, takes a position from the relying party, as for any N4-A
act (N4-A §7.1); there a position the relying party does not hold is PrefixIncomplete, and an
act bound elsewhere is EvaluationPositionMismatch.
8. Scope boundaries
Not defined here: the transport profile (what channel digests; TLS trust roots; pairing);
replay policy beyond §6; session lifetime or re-binding cadence; a gateway or runtime that calls
this (none exists); any binding for humans or Subjects (a Subject has no key and binds nothing);
any reuse of node BindingInfo. ChannelBindingV1 has no serde impl and no wire encoding
beyond its canonical bytes.