Skip to content

4.8.9 Network Authentication Foundations

(import :std/ensemble/network/auth) supplies internal authentication utilities for handshake, stream-opening, and renewal owners. It does not implement a transport, monitor admission, timers, credential-retry state, renewal, or connection publication. It borrows its capability context and key wrappers. It does not close contexts or release context-owned keys.

4.8.9.1 Identities And Bundles

proto:/network/connect/v0 is "/network/connect/v0". proto:/network/connect refers to the current connection capability, presently v0. Both are also exported by the public :std/ensemble/network facade for creating UCAN credentials without embedding a protocol string. They replace the internal connection-auth-protocol name; the protocol value and authentication behavior are unchanged. Treat these strings as immutable.

normalize-did(did), exported by :std/ensemble/ucan/did, produces the canonical Base64url Ed25519 DID without allocating a native public key. Context-backed issuance, parent filtering, and selection use CapabilityContext.normalize-did for canonical comparison projections. Equivalent did:key:z and did:key:u spellings identify the same key, including roots configured with z spellings. Signed token issuer, audience, and ancestor fields are never rewritten by these comparisons; signatures and marshaled bytes retain their original spelling. New tokens issued through grant! or delegate! have canonical fields, whereas low-level sign! preserves the supplied fields. The handshake owner must reject noncanonical HELLO identities, bind them to the authenticated key, and reject self-connections before proceeding.

make-auth-tokens(ctx, issuer, audience, protocol, expire, headroom, now, [parent]) returns a fresh list of newly issued Token objects in stable descending expiration order, without marshaling the outgoing credentials. It has the same positional arguments and contracts as make-auth-bundle: a CapabilityContext, three strings, three u64 integers (expire, headroom, now), and an optional Token parent defaulting to #f; the return type is :list. With no parent it uses CapabilityContext.provide!; with a supplied parent it uses delegate! to create a singleton INVOKE list. The requested expiration must cover the fixed headroom and remain live. A supplied parent must cover that entire expiration, so delegation clamping never silently shortens a successful request. The UCAN extension/signing checks validate delegation authority; this module does not duplicate capability inclusion checks. Issuance neither saves tokens nor implies recipient trust.

make-auth-bundle(ctx, issuer, audience, protocol, expire, headroom, now, [parent]) retains its existing signature and handshake behavior: it maps marshal-token over make-auth-tokens and returns opaque blobs. This compatibility entry point does not impose an encoding byte limit. Issuance/signing can still perform its own serialization internally; make-auth-tokens only separates issuance from retained outgoing blob construction.

make-adaptive-auth-tokens(ctx, issuer, audience, protocol, required, headroom, now, window, seed: #f) -> :list is a separate internal issuance path. Context and identity/protocol types match the fixed helper; required, headroom, and now are u64 integers, window is a positive exact integer number of seconds, and seed: is a nullable Token. Zero required means no explicit lower bound, not infinite authority. It does not change either fixed helper’s behavior.

The direct INVOKE grant expires at max(now + window, required). The window sum must fit u64 before issuance; overflow raises IOError. If that finite expiration cannot cover headroom and current validity, the result is empty, without issuing any tokens or silently extending the window to headroom. An explicit required target may extend the window, but is never weakened. No maximum-u64 policy sentinel is minted. Direct issuance does not require the issuer in local roots or input anchors: the recipient alone decides whether to trust it.

After the direct grant, issuance considers the explicit seed and freshly queried applicable output parents, in that order. Context applicability uses DELEGATE type, an audience whose canonical projection matches the issuer or is the literal wildcard "*", and protocol/group inclusion, as provide! does. Normalizing a parent’s audience for comparison does not mutate the parent or its signed bytes. Each delegated target is the minimum expiration over its entire parent chain, not just the supplied leaf, and must be a live u64 covering required/headroom. Exhausted parents, including an expired seed or one with an expired ancestor, are ignored without preventing fresh context alternatives. A lifetime-eligible explicit seed still passes through delegate!’s type/audience/protocol contracts and signing validation. Computing a chain horizon does not repair an invalid chain or bypass any signature or delegation constraint.

Structurally equal parents are deduplicated before issuing randomized children, preserving first occurrence. The final stable descending-expiration sort retains direct, seed, then context order on ties. Context, signing, and encoding exceptions propagate, including a raised #f; they are not candidate refusals. Contexts, parents, and seeds remain borrowed and immutable. Owners retain/replace their seed under their own serialization and call this helper anew for each operation.

An empty result means the local choice cannot supply credentials. Owners try eligible alternatives within their original operation budget before sending reason-no-credentials (12). An empty result here means insufficient lifetime. Delegation, identity, serialization, context, storage, and cryptographic exceptions propagate to the owner for abort. In particular, a missing host principal key is a broken local prerequisite, not a credential alternative to skip; the original keystore error propagates in both construction paths. CapabilityContext cryptographically validates input and output anchors at insertion, checking signatures, delegation chains, and expiration before storage. Invalid anchors are rejected without replacing existing valid alternatives. Admission requires no existing recipient-side trust. Verification uses the recipient context’s implicit principal roots, explicit roots, and input anchors. Output-anchor insertion does not confer recipient-side trust. provide! returns the direct grant followed by all selected anchor delegations in the context’s listing order; it does not silently skip corrupt credentials. Its signing/context exceptions propagate through network construction, as do all exceptions from the supplied-parent path. No exception contents are inspected.

encode-auth-bundle(tokens: :list, limit: :fixnum) -> :list encodes already-issued Tokens without reordering them. Its cumulative byte allowance includes the four-byte bundle count and every four-byte blob-length prefix: 4 + sum(4 + blob-length) <= limit. The returned list contains only the opaque token blobs, not those framing bytes. [] is valid with a limit of at least four; any insufficient limit, including a negative one, raises IOError. Non-Token elements fail the Token contract. Tokens and their parameters come from trusted local sources and must remain immutable during encoding; encoding does not authenticate them or establish recipient trust.

Encoding uses a fresh marshal-context(dag: #t) and BufferedWriter.serialize for each token, preserving the existing binary representation byte for byte, including shared acyclic data. A narrow bounded BufferedWriter checks both single-byte and bulk writes before forwarding to a growable :std/io memory writer. The delegate starts with a small buffer, not a preallocation of the full allowance. Its logical output is bounded while serializing, not checked only after an unbounded marshal. Each memory writer is closed on success and failure; successful output transfers to the caller, and failed output returns its cached buffer. DAG/cycle and other serialization errors propagate unchanged.

These helpers are explicitly exported only by the internal auth module, not the public network facade. An outgoing OPEN owner can issue tokens first, but must reserve connection control space before calling encode-auth-bundle, then refund unused reservation once the actual encoded size is known. The owner must allow separately for frame headers and other OPEN fields, and release its reservation on failure. These helpers do not reserve, wait for, or refund control space themselves. The byte allowance bounds logical serialized output, not token construction, serde graph scanning/metadata, decoder resources, or exact heap usage including memory-writer capacity rounding.

order-auth-tokens(tokens) returns a fresh, stably sorted list of Token objects, longest expiration first. It can order supplied parents before an owner performs failover; it does not deduplicate, authenticate, or mutate them.

decode-auth-bundle(blobs) accepts the ordered opaque byte-vector list from the wire codec. It decodes with unmarshal-token and its existing DAG environment, propagates any decoding failure, and sorts candidates stably by decreasing expiration. A malformed blob aborts decoding of the bundle, even if other blobs are valid. AuthCandidate contains:

Field Meaning
index Original zero-based wire index, independent of sorting
token Decoded Token; not yet authenticated
bytes The original supplied byte vector, retained by identity

Pass this sorted list unchanged to select-auth-candidate(ctx, candidates, issuer, audience, protocol, min-auth-expire, headroom, now). The expected endpoint identities and signed issuer/audience comparison projections are normalized through the context. Selection accepts equivalent z/u spellings in either direction without modifying signed fields or retained wire bytes. It requires an INVOKE leaf, matching canonical issuer/audience projections, exact protocol, u64 expiration, current validity, full requested lifetime, and the fixed headroom. It invokes CapabilityContext.verify, which checks signatures, delegation, and implicit-root, explicit-root, or input-anchor trust. Merely using verify-token would not establish that trust. Unlike delegation-parent applicability, leaf selection rejects wildcard audience "*"; it does not normalize the wildcard or treat it as the expected endpoint.

AuthSelection contains candidate and reason. Success has the selected AuthCandidate and reason-ok (0). Failure has no candidate and reason-auth-failed (3), or reason-lifetime (4) if a matching, currently live, context-verified candidate was found but could not cover the required expiration or headroom. Invalid/untrusted long-lived candidates never mask a valid shorter alternative. Expired tokens are invalid candidates, not evidence of sufficient trust to report the more specific lifetime failure.

Classes use the ordinary generated constructors and checked accessors/setters. They are internal module-transfer objects, not a public network facade. Treat lists, candidates, tokens, and retained byte vectors as immutable while in use. In particular, do not manufacture cyclic candidates or mutate a decoded candidate before selection. Owners validate acknowledgment indices against the original bundle, not the sorted candidate position, and recheck timing before publication.

4.8.9.2 Failure Boundary

Encoding, decoding, normalization, and context exceptions propagate unchanged. Do not inspect exception messages or irritants to infer a recoverable cause, or turn exceptions into rejected candidates. Data must be well formed; an exception requires the owner to abort the operation. The existing token codec enforces DAG structure at encoding/decoding boundaries. Adaptive horizon traversal also rejects cyclic parent chains using identity tracking before structural deduplication or delegation; this prevents malformed local seeds from looping before signing can reach the codec. No new class-loading policy or decoder resource budget is introduced.

Ordinary verification result values remain distinct from exceptions: a well-formed token with a bad signature, insufficient trust, or inadequate lifetime can be rejected while considering other candidates. There is no catch-all logger here. Owners must clean up and report unexpected errors using the package logger without dumping token blobs, private keys, or secret-bearing exception irritants. No exception object or diagnostic string is a wire result.

Reasons 3/4 reject the recipient’s credentials; reason 12 describes the sender’s own inability. Reason 13 describes insufficient old-lease headroom, not a peer credential failure. Only owners know whether an authenticated peer’s reason and the current protocol phase permit retry; these utilities do not initiate retries.

4.8.9.3 Pure Timing

Timing functions read no clock and own no socket, timer, or renewal state.

Procedure Contract
resolve-auth-expiration(start, default-ttl, ttl:, expire:) Integer u64 Unix-second start; positive exact TTLs. Explicit expire wins, otherwise start plus TTL/default. Both overrides are checked. Reject overflow and expiration at/before start.
auth-headroom(start, timeout) U64 Unix-second start plus positive exact integer seconds. Reject thresholds above u64. Capture once per local attempt/round.
auth-lifetime?(expire, min-auth-expire, headroom, now) min-auth-expire, headroom, and now are u64 Unix seconds. Expiration must be u64, cover min-auth-expire and headroom independently, and be strictly later than now.
operation-deadline(start, timeout, deadline:) U64 Unix-second start plus positive exact integer seconds. Optional inherited u64 deadline can only shorten it. Overflow is rejected before applying the cap.
operation-expired?(deadline, now) Macro expanding to (>= now deadline) without type contracts; equality is expired.

Capture operation starts and current time using current-time-seconds. All authentication timestamps and operation deadlines use exact integer Unix seconds, locally and on the wire. Fractional and inexact timestamps/durations are rejected; there is no scaling, real-number conversion, or quantization machinery. The separate IOTimeout API is unchanged.

Both fixed and connection-linked initial OPEN pass zero extra headroom, while requiring full coverage of their captured expiration. Renewal owners supply their captured required expiration and operation headroom; adaptive initial connections pass required zero with fixed handshake headroom. Renewal owners check auth-lifetime? on the old lease before starting a round and map insufficient headroom to reason 13 without changing that lease. A new retry captures a new headroom threshold but retains the original operation deadline. The owners keep no-override connection reuse separate from new-operation lifetime resolution. These functions do not alter the !NoTimeout I/O defaults.

4.8.9.4 Unix Identity Proof

unix-auth-transcript(initiator-hello, responder-hello, role, bundle) accepts raw byte vectors and returns exactly:

ASCII gerbil:ensemble:network:unix-auth:v0
u32be initiator-length | exact initiator HELLO payload
u32be responder-length | exact responder HELLO payload
u8 physical-sender-role (0 initiator, 1 responder)
u32be bundle-length | exact sender bundle encoding

No NUL terminator, frame headers, hash, or signature is included. Length arithmetic is checked before allocation; validated lengths use the standard unchecked big-endian u8vector writer. Physical direction is independent of DID election role.

sign-unix-auth(key, initiator-hello, responder-hello, role, bundle) uses the existing digest-sign! API and an Ed25519 PrivKey, returning 64 signature bytes. verify-unix-auth(key, initiator-hello, responder-hello, role, bundle, signature) uses digest-verify! with the known DID’s PubKey. It rejects non-64-byte signatures and returns false on a signature mismatch; unsupported key types and unexpected crypto failures raise.

The wire codec validates HELLO structure and the exact 32-byte challenge and Unix 64-byte AUTH suffix. The owner generates fresh random challenges, validates version, physical roles, HELLO IDs/required-expiration semantics, and envelope state. It must retain both raw HELLO payloads and the raw AUTH prefix excluding its last 64 bytes. Do not reconstruct the bundle from decoded blobs or tokens for proof verification, even if normal local encoding would produce equal bytes. Only outgoing construction may use the wire AUTH codec without its Unix suffix to obtain the bundle bytes before signing.

This is the agreed plaintext-Unix identity proof, not per-frame integrity or protection against malicious local relays. TLS and renewal add no such proof.

4.8.9.5 Tests

Run from the repository root after make stdlib:

./build.sh test std/ensemble/network/...

auth-test.ss includes integer-second range/deadline/contract tests, actual Ed25519 transcript tests, empty-bundle bounds, and oversized/cyclic token failures that check cached-output return and rejection before memory-buffer growth. With SQLite enabled, it also checks fresh token issuance and bounded encoding against map marshal-token at exact byte boundaries, including count/prefix overhead, original order, shared DAG data, UTF-8, and bulk writes. It uses the real capability context and encrypted memory keystore for output/input/root trust, delegation, malformed blobs, cycles, every proper token truncation, stable ties and wire indices, mixed signed/API DID spellings in both directions, roots configured with aliases, unchanged signed bytes, wildcard-leaf rejection, lifetime rejection, missing keys, and invalid signatures. Small deliberately failing contexts test unexpected-error propagation. None of these tests open a network socket or claim completed transport authentication.

Adaptive cases add finite direct windows without implicit trust, accepted shorter alternatives, explicit-target extension, full-chain horizons, expired-seed context refresh, signed alias-audience output parents without rewriting parent bytes, structural deduplication before signing, stable ties/original indices, bounded encoding, numeric contracts, and unchanged exception identity. These cases are included in the integrated network regression; see the main-owned current checkpoint for revision-scoped build and runtime evidence.