korium.io Docs
v0.8.33 Rust source ↗

Consumer guide · Korium 0.8.33

Build the application.
Let the fabric carry trust.

Korium gives Rust applications self-certifying identity, encrypted node transport, discovery, messaging, streams, pub-sub, tunnels, and optional service authorization through one bounded runtime. Start with the interaction your application needs.

Cargo.toml
[dependencies]
korium = "0.8"
tokio = { version = "1", features = ["full"] }
anyhow = "1"

Requires stable Rust 1.97.1+ · Edition 2024 · default feature: authz

01

Decision point

Choose the runtime posture first

There is no hidden protected-mode flag. The identity evidence supplied at build time determines admission behavior.

Default

DID-only node

Use for chat, collaboration, agent meshes, device networks, and other decentralized applications.

  • mTLS did:korium identity and key-bound PoW
  • Encrypted, bounded protocol handling
  • Your app owns contacts, invitations, membership, spam controls, and action policy
Node::builder().build().await?

Explicit evidence

Verified service

Use for APIs, gateways, CI/CD services, edge ingestion, and cross-organization service calls.

  • Everything in a DID-only node
  • Cert-borne service identity and source allowlist
  • Fail-closed bi-stream admission plus optional L7 invocation grants
.with_service_identity(root, bundle)
If you need…Start withPrimary API
Small RPC callsDID-only or verified servicesend / incoming_requests
Custom or long-lived protocolsDID-only or verified serviceopen_stream / incoming_streams
Many-to-many eventsDID-only nodesubscribe / publish
Stable service or resource namesEither postureannounce_* / lookup_*
Legacy TCP connectivityTunnel-enabled nodestart_tunnel_entry
DNS-native service lookupDNS-enabled node or gatewaystart_dns_listener_at
Service-scoped authorizationVerified serviceverify_action

02

Foundation

Configure one node

Construction starts the fabric and public DNS bootstrap by default. Persist both the keypair and its mined identity proof when identity must survive restarts; restoring a key without its matching proof fails the build.

src/main.rs
use anyhow::Result;
use korium::Node;

#[tokio::main]
async fn main() -> Result<()> {
    let node = Node::builder()
        .bind_port(4433)
        .bootstrap(true)
        .build()
        .await?;

    println!("DID: {}", node.self_did());
    println!("identity hex: {}", node.identity());
    println!("listening: {}", node.local_addr()?);

    tokio::signal::ctrl_c().await?;
    node.shutdown().await;
    Ok(())
}
DID and identity hex are different.

self_did() is the public did:korium URI. identity() is the 64-character routing identity expected by the node APIs.

bind_addr exact socket contact restore identity + proof bootstrap(false) private/manual network bootstrap_route_validation_timeout bound the join wait tunnel enable tunnel exit

03

Scenario

Build a private mesh

Disable public bootstrap when your deployment distributes explicit seed identities and socket addresses. A join address is an untrusted reachability hint; the connection still authenticates the remote node with mTLS, DID binding, and PoW.

Manual bootstrap
let node = Node::builder()
    .bootstrap(false)
    .build()
    .await?;

node.join(
    "0123...64-hex-characters...cdef",
    &["203.0.113.10:4433".to_string()],
).await?;

node.connect("0123...64-hex-characters...cdef").await?;
Korium handles

Identity verification, path validation, encrypted transport, bounded routing, and discovery records.

Your app handles

Who should be contacted, invitations and blocklists, node roles, and user-facing trust decisions.

04

Scenario

Request / response

Use PLAIN request streams for bounded request/response work. Requests are capped at 64 KiB; callers always provide their own end-to-end timeout.

Caller
use std::time::Duration;

let reply = node.send(
    &remote_identity_hex,
    br#"{"op":"status"}"#.to_vec(),
    Duration::from_secs(5),
).await?;
Async server
let mut requests = node.incoming_requests().await?;

while let Some((from, pubkey, body, ctx, reply)) = requests.recv().await {
    // Inspect ctx and authorize before changing state.
    let response = handle(from, pubkey, body, ctx).await;
    let _ = reply.send(response);
}

incoming_requests is take-once and is the only request-serving API; the synchronous set_request_handler shortcut was removed in 0.8.28. Own the receiving task, and keep draining it.

05

Scenario

Raw bidirectional streams

Use RAW streams for custom framing, file transfer, interactive sessions, or protocols that need full duplex. Korium authenticates and bounds stream admission; your protocol owns framing and payload limits.

Open and accept
use tokio::io::{AsyncReadExt, AsyncWriteExt};

// Caller
let (mut send, mut recv) = node.open_stream(&remote_identity_hex).await?;
send.write_all(b"hello").await?;
send.finish()?;
let reply = recv.read_to_end(64 * 1024).await?;

// Receiver: take this once, then keep draining it.
let mut incoming = node.incoming_streams().await?;
while let Some((from, mut stream)) = incoming.recv().await {
    if stream.kind() != IncomingStreamKind::Raw { continue; }
    let body = stream.recv.read_to_end(64 * 1024).await?;
    stream.send.write_all(&process(from, body)).await?;
    stream.send.shutdown().await?;
}
Keep the guard alive.

GuardedRawStream owns the per-node admission permit. Do not discard it while retaining only detached I/O handles unless you also retain the guard returned by into_parts().

06

Scenario

Signed pub-sub

Use GossipSub for many-to-many events where duplicate, delayed, or reordered delivery is acceptable. Messages are signed and replay-bounded; publication does not grant application membership.

Subscribe and publish
node.subscribe("events/alerts").await?;
let mut messages = node.messages().await?; // take once

node.publish(
    "events/alerts",
    b"system update available".to_vec(),
).await?;

while let Some(message) = messages.recv().await {
    consume(message).await?;
}
Good fit

Presence, invalidation, event fan-out, replicated notifications, and eventually consistent signals.

Use RPC instead

Commands that require a direct result, request-specific authorization, or business-level success semantics.

07

Scenario

Discover stable services

Service URNs stay stable while node identities rotate. Announcements publish signed, expiring liveness; lookups return candidates, never authority.

Service liveness
let service = "urn:korium:acme/payments";

// Serving node
node.announce_service(service).await?;

// Consumer
for (identity, addresses) in node.lookup_service_endpoints(service).await? {
    println!("{} at {:?}", identity.to_hex(), addresses);
}
Discovery is not authorization.

A fresh signed record proves which node published the hint. It does not prove permission, service authority, health, or membership. Connect and enforce the appropriate application or verified-service policy.

08

Scenario

Locate resources without a registry

Publish exact resources or bounded parent prefixes under a namespace or service URN. Consumers always query an exact resource and receive bounded fresh candidates.

Resource discovery
use korium::ResourceSelector;

let context = "urn:korium:acme/storage";

node.announce_resource(
    context,
    ResourceSelector::prefix("repos/project"),
).await?;

if let Some(found) = node
    .lookup_resource(context, "repos/project/readme")
    .await?
{
    for candidate in found.candidates {
        node.connect(&candidate.identity.to_hex()).await?;
    }
}
  • Resource strings are canonical printable ASCII and capped at 1024 bytes.
  • No list-all, wildcard, regex, glob, or prefix-enumeration lookup exists.
  • Use opaque, keyed, or tenant-scoped names when discovery subjects are sensitive.
  • unannounce_resource stops republishing; old records expire naturally.

09

Scenario

Bridge a TCP application

TUNNEL lets a local TCP client reach an in-process stream handler on a destination node. The entry tries native TLS 1.3 mTLS/TCP first and falls back to a QUIC TUNNEL stream through the fabric.

Destination and entry
use std::net::SocketAddr;
use korium::{IncomingStreamKind, Node};

// Destination: enable the exit and consume Tunnel streams.
let destination = Node::builder().tunnel().build().await?;
let mut incoming = destination.incoming_streams().await?;

// Entry: bind a local port for the legacy TCP client.
let entry = Node::new().await?;
let listen: SocketAddr = "127.0.0.1:13306".parse()?;
let (handle, shutdown) = entry
    .start_tunnel_entry(listen, &destination_identity_hex)
    .await?;

// Later: stop accepting and await clean termination.
shutdown.notify_one();
handle.await??;
Destination contract

Drain incoming_streams, select IncomingStreamKind::Tunnel, and map each accepted stream to local application state.

Operational bounds

256 concurrent sessions globally, 16 streams per remote node, 5-minute per-direction idle timeout, and a 1-hour session cap.

10

Kubernetes scenario

Expose Korium discovery through DNS

The DNS shim lets DNS-native workloads consume Korium identity and service discovery without embedding the Rust API. It is an authoritative, non-recursive UDP listener backed by the node's bounded DHT lookups—not a replacement for CoreDNS or a general-purpose resolver.

Pod-local

Sidecar resolver

Bind to loopback and configure one application or a local forwarding proxy to query port 5353.

  • Smallest network exposure
  • Korium identity and lifecycle stay with the workload
  • Best when the client supports a custom DNS server and port
--dns 127.0.0.1:5353

Namespace or cluster

DNS gateway

Run a persistent Korium node behind an internal UDP Service and query it explicitly.

  • One stable Kubernetes service address
  • Shared DHT view and bounded lookup concurrency
  • Keep access internal and enforce network-level source controls
--dns 0.0.0.0:5353
Build and run
# The standalone binary requires both features.
cargo build --release --bin korium --features cli,dns

# CLI bootstrap is opt-in. The DNS listener defaults to loopback:5353
# when --dns is omitted.
korium \
  --port 4433 \
  --dns 0.0.0.0:5353 \
  --bootstrap \
  --identity-file /var/lib/korium/identity
Library embedding
use std::net::SocketAddr;

let node = Node::builder().build().await?;

// Default: loopback on UDP 5353.
let local_dns = node.start_default_dns_listener().await?;

// Or expose an explicit address for a controlled gateway.
let bind: SocketAddr = "0.0.0.0:5353".parse()?;
let gateway_dns = node.start_dns_listener_at(bind).await?;

Query service URNs

A single-segment service URN maps deterministically to a DNS owner. For example, urn:korium:acme.example/payments becomes _korium._udp.u1.acme.example._svc.payments.svc.

DNS client
# Ask the Kubernetes Service explicitly; do not replace cluster DNS.
dig @korium-dns.platform.svc.cluster.local \
  _korium._udp.u1.acme.example._svc.payments.svc. SRV

dig @korium-dns.platform.svc.cluster.local \
  _korium._udp.u1.acme.example._svc.payments.svc. SVCB
RecordConsumer useReturned evidence
SRVConnect to a node or stable servicePort, generated target, and A/AAAA additionals
SVCBDiscover Korium transport parametersALPN, port, IPv4 and IPv6 hints
TXT …contactFetch a node contact recordBounded postcard + base64url signed contact
TXT …didRead the self-certifying node URIdid=did:korium:…

Kubernetes gateway

The following skeleton keeps the Korium identity on persistent storage and publishes DNS only through an internal UDP Service. Replace the image and storage class for your environment.

korium-dns.yaml
apiVersion: v1
kind: Service
metadata:
  name: korium-dns
  namespace: platform
spec:
  selector:
    app: korium-dns
  ports:
    - name: dns
      port: 53
      targetPort: dns
      protocol: UDP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: korium-dns
  namespace: platform
spec:
  serviceName: korium-dns
  replicas: 1
  selector:
    matchLabels:
      app: korium-dns
  template:
    metadata:
      labels:
        app: korium-dns
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        fsGroupChangePolicy: OnRootMismatch
      containers:
        - name: korium
          image: registry.example/korium:0.8.33
          args:
            - --port
            - "4433"
            - --dns
            - 0.0.0.0:5353
            - --bootstrap
            - --identity-file
            - /var/lib/korium/identity
          ports:
            - name: fabric
              containerPort: 4433
              protocol: UDP
            - name: dns
              containerPort: 5353
              protocol: UDP
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: identity
              mountPath: /var/lib/korium
  volumeClaimTemplates:
    - metadata:
        name: identity
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi
Keep Kubernetes DNS and Korium DNS separate.

Query the gateway explicitly or configure a narrowly scoped local forwarder. Do not replace the Pod's normal nameserver or forward the broad .svc suffix: Kubernetes already owns *.svc.<cluster-domain>.

  • External DNS exposure requires an explicit non-loopback bind plus source-spoofing and rate controls.
  • The listener is UDP-only, authoritative, non-recursive, and accepts exactly one IN-class question per packet.
  • Responses are capped at 1232 bytes, in-flight queries at 64, and query work at 2 seconds.
  • Records use a 15-second TTL; service lookup gets a bounded 1.5-second DHT budget.
  • DNS output remains discovery evidence, not service authority or authorization. Korium mTLS and application policy still apply.
  • Persist the identity file with mode 0600. A new identity changes the gateway's node identity and requires new PoW.

11

Scenario

Run a verified service

A verified service presents an offline-authored authority bundle bound to its node key. Build fails if the proof is malformed, expired, rooted in another namespace, or bound to a different keypair.

Service admission + L7 grant
let service = Node::builder()
    .contact(persisted_keypair, persisted_pow)
    .with_service_identity(namespace_root, signed_bundle)
    .build()
    .await?;

assert_eq!(service.service_urn(), Some("urn:korium:acme/payments"));

let mut requests = service.incoming_requests().await?;
while let Some((_from, _pubkey, body, ctx, reply)) = requests.recv().await {
    let request = parse_request(body)?;
    let grant = service.verify_action(
        &ctx,
        request.bearer_token.as_deref(),
        "payment.capture",
        &format!("payments/{}", request.payment_id),
    )?;

    let _ = reply.send(capture(request, grant).await?);
}
1mTLSCaller controls its DID key
2Service identityInstance may speak as the URN
3L4 admissionSource and stream are allowed
4L7 grantIssuer, audience, service, action, resource, expiry
The consumer defines authorization semantics.

Korium defines no action vocabulary. Canonicalize the resource before calling verify_action. L7 Biscuit grants are single-issuer, single-audience, service-scoped, terminal, non-delegating, and capped at 300 seconds; there is no fabric-wide instant revocation. That L7 rule is separate from the v2 service-instance delegation below, which authorizes a child key to speak for the same exact service URN.

Scale out a verified service without the root secret

This workflow is for the service-instance holder or its control plane, not an ordinary calling client. The namespace root grants a bounded delegation capability before its private key is destroyed; the authorized instance can then issue exact-URN child credentials without choosing a new service name or policy. Enable Korium's authoring feature in the binary or control-plane tool that performs the first two phases.

1. While the namespace-root secret still exists

The root must issue the parent a v2 bundle with explicit delegation permission. Keep the root public key before destroying the private key.

Root-authorized v2 parent bundle
use korium::authoring::{
    DelegableUrnAuthorityBundleSpec,
    ExactUrnDelegationPermission,
    LeaseTerms,
    UrnBindingProofSpec,
};

let parent_proof = UrnBindingProofSpec::direct(
    &namespace_root,
    &instance_issuer,
    "urn:korium:acme/payments",
    parent_key.public_key_bytes(),
    parent_terms,
)
.sign()?;

let parent_bundle = DelegableUrnAuthorityBundleSpec {
    urn_binding_proof: parent_proof,
    allow_sources,
    grant_issuers,
    delegation: ExactUrnDelegationPermission {
        max_depth: 1,
        nonce: root_authorization_nonce,
    },
}
.sign(&namespace_root)?;

let namespace_root_public = namespace_root.public_bytes();

max_depth: 1 is the normal scale-out posture: the original instance may authorize children, but those children cannot delegate again. The value limits ancestry depth, not the number of sibling replicas the parent may authorize.

Delegation permission cannot be added later.

An existing v1 authority bundle cannot delegate, and it cannot be upgraded after the namespace-root private key has been destroyed.

2. After the namespace-root secret has been destroyed

The authorized parent generates or receives a child key, then signs a child bundle. authorize() verifies the supplied parent bundle before signing and requires the child validity window to stay within the parent's.

Exact-URN child authorization
use korium::{
    authoring::{ExactUrnInstanceDelegationSpec, LeaseTerms},
    Keypair,
};

let (child_key, child_pow) = Keypair::generate()?;

let child_bundle = ExactUrnInstanceDelegationSpec {
    namespace_root: namespace_root_public,
    parent_key: &parent_key,
    verified_parent_bundle: &parent_bundle,
    child_public_key: child_key.public_key_bytes(),
    terms: LeaseTerms {
        not_before_unix_seconds: now,
        expires_unix_seconds: now + 300,
    },
    nonce: child_delegation_nonce,
}
.authorize()?;

The child authoring API deliberately accepts no URN, allow_sources, or grant_issuers; all three are inherited from the root-signed parent bundle. Persist the child key, matching PoW proof, child bundle, root public key, and the authorization nonces according to your control-plane durability and audit requirements. The namespace-root private key is not required.

3. Start the child normally

Existing NodeBuilder integration
let child = korium::Node::builder()
    .contact(child_key, child_pow)
    .with_service_identity(namespace_root_public, child_bundle)
    .build()
    .await?;

This is the entire runtime integration. NodeBuilder checks that the bundle terminates at the supplied child key. Korium then embeds and verifies the delegated proof through its existing TLS, connection, destination, and stream paths; no transport-specific delegation API is needed.

Rotate the namespace root

Removing a node from a namespace means redeploying that namespace under a new root. A node pins its acceptance policy separately from the root its own credential chains to, so the rollout can proceed as a three-wave rolling redeployment instead of a flag day.

Rolling root transition
// Waves 1 and 2 carry the identical acceptance policy, so no wave
// ever presents a credential the rest of the fleet cannot root.
let node = Node::builder()
    .contact(persisted_keypair, persisted_pow)
    .with_service_identity(own_credential_root, signed_bundle)
    .accepting_namespace_root_transition(
        new_primary_root,
        old_retiring_root,
        retire_after_unix_seconds,
    )
    .build()
    .await?;
WaveOwn credentialAccepts credentials rooted at
1oldnew primary + old retiring until T
2newnew primary + old retiring until T
3newnew only
  • A chain rooted at the retiring root verifies only while now < retire_after; its verified expiry becomes min(chain_expiry, retire_after), so admitted connections close at retirement.
  • Exposure is bounded only once wave 1 has reached every node — a node never reached has no knowledge of T. Treat completion of wave 1, not its start, as the start of the bound.
  • Retirement is fail-closed at the deadline: now == retire_after is already refused.
  • The build rejects equal primary and retiring roots, a deadline not in the future, and a node whose own credential root neither entry accepts.
  • Persist keypairs and PoW proofs. Regenerating them changes every DID, invalidating grant_issuers entries and forcing PoW re-mining fleet-wide.

12

Consumer contract

Know what the proof means

Authenticated node

Key possession

The live node controls the private key bound to its did:korium identity. This says nothing about permission or behavior.

Discovery record

Signed hint

The named node published fresh, bounded reachability or liveness data. The data is not membership or authority.

Service identity

Right to speak as a URN

A signed lease chain rooted in the pinned namespace root binds this node key to this service URN until the earliest expiry in the chain.

Invocation grant

Exact authorized action

An approved issuer granted one audience an action on a canonical resource for one service until a short deadline. The terminal L7 Biscuit does not authorize further grants.

Design for these realities

  • Delivery can be duplicated, delayed, replayed, reordered, or lost. Make application operations idempotent where needed.
  • Live transport does not depend on remote clock claims. Signed records and credentials use bounded local Unix-UTC validity checks.
  • There is no per-node revocation. Removing a node means redeploying the namespace under a new root; lease expiry is a backstop, not the removal path. Author short lease windows if you cannot redeploy quickly.
  • Confidentiality uses X25519 + ML-KEM-768 hybrid key exchange. Identity, authority, and capability signatures remain Ed25519.
  • Audit events are structured where verified context exists; collection, retention, timestamps, integrity, and complete denial coverage are deployment responsibilities.

Normative detail: Security invariants ↗

13

Operate

Own lifecycle and observe pressure

Keep the node alive for as long as its receivers and sessions should run. Prefer explicit shutdown().await; dropping a node aborts remaining work as a last-resort safety boundary.

Telemetry
let t = node.telemetry().await?;

metrics.gauge("korium.connected_peers", t.connected_peers);
metrics.gauge("korium.dht.pressure", t.pressure);
metrics.count("korium.transport.errors", t.transport_errors);
metrics.count("korium.gossip.delivery_drops", t.gossipsub_delivery_drops);
metrics.gauge("korium.tunnel.active", t.tunnel_active_sessions);

node.shutdown().await;
01

Drain take-once receivers continuously; backpressure and channel limits are intentional.

02

Persist the keypair and matching PoW proof together when identity continuity matters.

03

Use application deadlines around work beyond Korium’s built-in transport bounds.

04

Renew service authority and grants before expiry; never treat timeouts as consensus.

05

Alert on pressure, replay-capacity drops, transport errors, and sustained tunnel saturation.

14

Reference

Public API map

Identity & node

Node NodeBuilder Identity Keypair IdentityProof Contact

Requests & streams

IncomingRequests RequestContext GuardedRawStream IncomingStreamKind

Discovery

ResourceSelector ResourceLookup ResourceCandidate ResourceMatchKind

Authorization

PeerDid VerifiedGrant VerifiedToken VerifiedAppClaims VerifiedUrnBinding

Operations

TelemetrySnapshot TopicPeerInfo Message

Offline authoring

authoring::AuthorityKey UrnAuthorityBundleSpec UrnBindingProofSpec LeaseTerms DelegableUrnAuthorityBundleSpec ExactUrnDelegationPermission ExactUrnInstanceDelegationSpec — requires authoring

A v0.8.33 docs.rs build is not available, so these references point to the immutable v0.8.33 tagged source instead of a missing API page.