valis / Reference / API reference

Identity - API reference

Exported surface for the identity subsystem. Part of the API reference.

Package valis/src/identity/authenticator

Classes

anonymous-authenticator

Authenticator that resolves immediately to the anonymous principal. The keyless default: a peer that cannot speak Noise (an HTTP client on the eventual edge, a no-credential local module) gets the anonymous sentinel without prompt or error.

authenticator

Abstract protocol anchor for pluggable identity authenticators. Concrete subclasses specialise AUTHENTICATE to mint a PRINCIPAL (anonymous case) or return a stateful conversation object the caller drives to a principal (Noise case). The protocol's purpose is to keep PRINCIPAL decoupled from the specific proof mechanism — Noise today, an HTTP-edge proof later.

http-signature-authenticator

RFC 9421 HTTP Message Signature authenticator — the third authenticate method. Verifies a relayed signed request against the owner public key and resolves a valid live signature to the owner principal; every failure resolves to anonymous. Constructed by the fabric (which holds the custody store) and reached by the adapter only through the authenticate seam.

noise-authenticator

Authenticator that drives NoiseXX25519ChaChaPolySHA256 to a keyed transport session. AUTHENTICATE returns a NOISE-AUTH-NODE; the caller drives it to completion, then node-attach-identity builds the principal from the recovered peer static key.

Generic functions

authenticate

(authenticate authenticator transport-ctx)

Begin authentication for TRANSPORT-CTX using AUTHENTICATOR.

For an anonymous authenticator the return value is a fully-resolved PRINCIPAL (the anonymous sentinel) — there is no conversation to drive.

For a Noise authenticator the return value is a NOISE-AUTH-NODE the caller drives via node-write / node-read until node-auth-ok-p is true, then reads the recovered peer static key; the did:key principal is built from that key in node-attach-identity (mercer is identity-blind). This two-step shape is what allows the same generic to multiplex single-shot proofs (anonymous, future signature-based HTTP-edge) and multi-message handshakes (Noise) without forcing every caller to spin up a handshake just to learn there is none.

TRANSPORT-CTX is opaque to this seam — it is passed through to allow future authenticators to inspect the carrying transport (the afid, an HTTP request, etc.); the current implementations ignore it.

noise-authenticator-static-key

(noise-authenticator-static-key object)

Undocumented: this exported symbol needs a docstring.

Functions

make-anonymous-authenticator

(make-anonymous-authenticator)

Construct an anonymous authenticator.

make-http-signature-authenticator

(make-http-signature-authenticator &key custody)

Construct an HTTP-signature authenticator over the owner CUSTODY store.

make-noise-authenticator

(make-noise-authenticator &key static-key)

Construct a Noise authenticator wrapping STATIC-KEY (an X25519 private key, usually owned by the factotum custody store).

resolve-http-principal

(resolve-http-principal material)

Resolve relayed RFC 9421 request MATERIAL to a principal through the live HTTP-SIGNATURE-AUTHENTICATOR. Returns the owner principal for a valid live signature and the anonymous principal for every failure — including the case where the authenticator is not installed (fabric not up) or MATERIAL is NIL.

This is the adapter's sole entry into the owner-proof path: it imports this function and the seam variable, never custody or ironclad, so verification stays behind the authenticate seam.

Variables

*http-signature-authenticator*

The live HTTP-signature authenticator, or NIL before the fabric is up. Owned here; START-FABRIC constructs one over the owner custody store and SETFs it, STOP-FABRIC clears it. RESOLVE-HTTP-PRINCIPAL reads it; the adapter never touches the custody store the authenticator holds.

Package valis/src/identity/client-trust-store

Classes

client-trust-store

Tier-1 client-trust material held beside the owner root seed: the single pinned Ed25519 operator public key (32 raw octets), the fixed provisioning namespace every proof-of-reality is checked against, and the enrolled-client records. DIRECTORY is the backing cache directory, or NIL for an in-memory store used by tests and the in-image seam.

enrolled-client

One enrolled-client record: the AUDIENCE DID that received a minted grant and the 32-byte GRANT-HASH of that grant, retained for revocation lookup.

Functions

client-trust-enrolled-records

(client-trust-enrolled-records store)

The enrolled-client records STORE currently holds, most-recent first.

client-trust-pinned-operator-key

(client-trust-pinned-operator-key instance)

Undocumented: this exported symbol needs a docstring.

client-trust-provisioning-namespace

(client-trust-provisioning-namespace instance)

Undocumented: this exported symbol needs a docstring.

client-trust-store-directory

(client-trust-store-directory &optional (data-root (%valis-data-root)))

The client-trust cache directory: <DATA-ROOT>/client-trust/, a sibling of the owner root seed with its own lifecycle. DATA-ROOT defaults to valis's XDG data root; the fabric bring-up passes the same root it derives the keyfile from, so the two directories are guaranteed to sit side by side. Co-located with the root authority for locality — never merged into the owner keyfile.

client-trust-store-p

(client-trust-store-p object)

Undocumented: this exported symbol needs a docstring.

enrolled-client-audience

(enrolled-client-audience instance)

Undocumented: this exported symbol needs a docstring.

enrolled-client-grant-hash

(enrolled-client-grant-hash instance)

Undocumented: this exported symbol needs a docstring.

make-client-trust-store

(make-client-trust-store &key directory pinned-operator-key provisioning-namespace enrolled)

In-memory constructor for a client-trust store. Tests and the in-image seam use it with an explicit pinned key + namespace; the production path uses OPEN-CLIENT-TRUST-STORE, which reads them from the cache directory. PINNED-OPERATOR-KEY is the 32 raw octets of the operator's Ed25519 public key.

open-client-trust-store

(open-client-trust-store &optional (directory (client-trust-store-directory)))

Load-or-create the file-backed client-trust store rooted at DIRECTORY (created if absent), reading any persisted pinned operator key, namespace, and enrolled-client records. The directory sits beside the owner root seed with its own lifecycle.

pin-operator-key

(pin-operator-key store key-octets namespace)

Set STORE's single pinned Ed25519 operator public key (KEY-OCTETS, 32 raw bytes) and the fixed provisioning NAMESPACE, persisting them when the store is file-backed. This is host-local config, set out of band by the operator and never mutated over the network.

record-enrolled-client

(record-enrolled-client store audience grant-hash)

Record that AUDIENCE (a DID string) received a grant whose 32-byte hash is GRANT-HASH, for later revocation lookup. Persists to the store directory when the store is file-backed. Returns the new record.

Variables

*nonce-ttl-seconds*

Lifetime, in seconds, of a single-use proof-of-reality challenge nonce. A challenge is consumable exactly once and only before this window elapses. Dev-generous default (15 minutes) so an operator has ample time to sign the challenge by hand during development and the initial go-live.

*tier-1-token-ttl-seconds*

Lifetime, in seconds, of an operator-equivalent (Tier-1) grant. Moderate and revocation-primary: expiry is the backstop, revoking the grant hash is the primary containment. Dev-generous default (30 days); tightened later.

*tier-2-token-ttl-seconds*

Lifetime, in seconds, of a scoped-client (Tier-2) grant. Short and expiry-primary: re-issue is cheap from an operator-equivalent client or the owner, so a brief lifetime is the primary containment. Dev-generous default (1 day); tightened later.

Package valis/src/identity/custody

Classes

custody-store

Sole holder of private key material in the image. Constructed by LOAD-OR-CREATE-KEYFILE (production path) or MAKE-CUSTODY-STORE (in-memory constructor for tests). The Ed25519 master key is the signing root; the X25519 transport key is derived and held as a separate slot so Noise code and token-signing code each receive only the key they need.

Generic functions

custody-owner-did

(custody-owner-did object)

Undocumented: this exported symbol needs a docstring.

custody-owner-ed25519-public-key

(custody-owner-ed25519-public-key object)

Undocumented: this exported symbol needs a docstring.

custody-owner-key

(custody-owner-key object)

Undocumented: this exported symbol needs a docstring.

custody-owner-x25519-did

(custody-owner-x25519-did object)

Undocumented: this exported symbol needs a docstring.

custody-owner-x25519-key

(custody-owner-x25519-key object)

Undocumented: this exported symbol needs a docstring.

custody-owner-x25519-public-key

(custody-owner-x25519-public-key object)

Undocumented: this exported symbol needs a docstring.

Functions

custody-key-list

(custody-key-list store)

Return a list of factotum(4)-style key descriptor strings for STORE. Each string is a single line minus the trailing newline; id.lisp joins them with newlines when rendering for /id/ctl.

custody-sign-bytes

(custody-sign-bytes store octets)

Sign OCTETS (an octet vector) with STORE's Ed25519 master key. Returns a 64-byte Ed25519 signature vector.

This is the sole call site for ironclad:sign-message in the image. Module code (the /cap service, token minting) must route all signing through the custody store; the raw Ed25519 private key is never passed outside this function.

load-or-create-keyfile

(load-or-create-keyfile path)

Return a CUSTODY-STORE rooted at PATH.

If PATH exists: read the 32-byte Ed25519 secret scalar, reconstruct the Ed25519 key pair, derive the X25519 transport key, derive the owner DID from the Ed25519 public key — the DID is therefore stable across image restarts from the same keyfile.

If PATH does not exist: generate a fresh Ed25519 key pair, write the secret scalar to PATH and chmod it 0600, derive the X25519 transport key and DID.

The returned store is the SOLE in-image holder of the private key.

Note: if PATH contains an old X25519 keyfile (from before the Ed25519 re-root), delete it before starting — the 32-byte scalar format is the same on disk, but the key type differs and the DID will change.

make-custody-store

(make-custody-store &key owner-private-key owner-ed25519-public-key owner-x25519-key owner-did owner-public-key)

Direct in-memory constructor. Used by tests and the in-image authenticator seam; production callers use LOAD-OR-CREATE-KEYFILE so the owner DID is stable across restarts.

OWNER-PRIVATE-KEY — Ed25519 ironclad private key object. OWNER-ED25519-PUBLIC-KEY — 32-byte Ed25519 compressed public key. OWNER-X25519-KEY — X25519 ironclad private key (derived from Ed25519 pub). OWNER-DID — did:key DID string derived from the Ed25519 public key. OWNER-PUBLIC-KEY — accepted for backward compatibility; ignored.

Package valis/src/identity/http-signature

Classes

http-signature-authenticator

RFC 9421 HTTP Message Signature authenticator — the third authenticate method. Verifies a relayed signed request against the owner public key and resolves a valid live signature to the owner principal; every failure resolves to anonymous. Constructed by the fabric (which holds the custody store) and reached by the adapter only through the authenticate seam.

Conditions

base64-decode-error

Signalled by BASE64-DECODE on malformed structure — a non-alphabet character, an input length that is not a multiple of four, '=' padding in an illegal position, or non-canonical trailing bits in the final padded group (RFC 4648 §3.5). The fail-closed contract: a structurally malformed Signature value never decodes to garbage, it signals, and the verifier treats it as no valid signature.

Generic functions

http-signature-authenticator-custody

(http-signature-authenticator-custody object)

Undocumented: this exported symbol needs a docstring.

Functions

base64-decode

(base64-decode string)

Decode a standard RFC 4648 base64 STRING to an octet vector.

Fails closed on malformed structure: signals BASE64-DECODE-ERROR when the input length is not a multiple of four, when '' padding appears anywhere but the last one or two positions, when any non-padding character is outside the base64 alphabet, or when the final padded group carries non-zero bits below the last decoded byte (RFC 4648 §3.5 — so a non-canonical encoding such as "TR=" is rejected rather than silently decoding to the same byte as canonical "TQ=="). The decoder enforces structural validity, not full canonical-form of every group; what it does reject, it rejects rather than returning garbage.

base64-encode

(base64-encode bytes)

Encode BYTES (an octet vector) to a standard RFC 4648 base64 string with '=' padding. Exported so the signer encodes the 64-byte signature here and the verifier shares the same codec.

build-signature-base

(build-signature-base &key method authority path created expires nonce keyid)

Construct the RFC 9421 §2.5 signature base over @method/@authority/@path plus the signature params, returning the ASCII base STRING.

METHOD is the case-sensitive method name (e.g. "GET"). AUTHORITY is the Host value (lowercased, default :80 omitted). PATH is the absolute path (query stripped at '?'). CREATED/EXPIRES are integer Unix times; NONCE and KEYID are strings (KEYID is the owner did:key). ALG is fixed at "ed25519".

This is the SINGLE signature-base builder: the in-image signer and the edge verifier both call it, so the bytes signed and the bytes reconstructed are identical by construction. The returned PARAMS value (second return value) is the Signature-Input inner-list-plus-params the signer emits and the verifier echoes back into the base.

commit-nonce

(commit-nonce nonce expires &optional (now (unix-now)))

Record a VERIFIED signature's NONCE under EXPIRES, bounded by the cap. Returns T on commit, NIL when the nonce is already present (a concurrent commit) or the cap is full — the caller treats a refused commit as verification failure for this request, so the request fails closed to anonymous rather than elevating.

Only the verifier calls this, and only after the Ed25519 check succeeds, so the cache remembers exclusively the nonces of genuine owner signatures: an unauthenticated peer can never grow the cache and so can never exhaust it to lock the owner out. Expired entries are pruned first so the cache stays bounded under honest traffic.

The stored EXPIRES is capped at NOW + MAX-SIGNATURE-LIFETIME-SECONDS so a far-future EXPIRES cannot pin a slot past the maximum lifetime and defeat expiry-based pruning.

http-signature-material-authority

(http-signature-material-authority instance)

Undocumented: this exported symbol needs a docstring.

http-signature-material-method

(http-signature-material-method instance)

Undocumented: this exported symbol needs a docstring.

http-signature-material-p

(http-signature-material-p object)

Undocumented: this exported symbol needs a docstring.

http-signature-material-path

(http-signature-material-path instance)

Undocumented: this exported symbol needs a docstring.

http-signature-material-signature

(http-signature-material-signature instance)

Undocumented: this exported symbol needs a docstring.

http-signature-material-signature-input

(http-signature-material-signature-input instance)

Undocumented: this exported symbol needs a docstring.

make-http-signature-authenticator

(make-http-signature-authenticator &key custody)

Construct an HTTP-signature authenticator over the owner CUSTODY store.

make-http-signature-material

(make-http-signature-material &key ((:method method) nil) ((:authority authority) nil) ((:path path) nil) ((:signature-input signature-input) nil) ((:signature signature) nil))

Undocumented: this exported symbol needs a docstring.

parse-signature

(parse-signature value &optional label)

Parse a Signature header VALUE — `label=:<base64>:` — and return the decoded signature octets, or NIL on malformed input. When LABEL is supplied the parsed label must match it (the Signature-Input label binds the Signature line).

The base64 byte sequence is decoded through the file's own fail-closed BASE64-DECODE, so a corrupted Signature value decodes to NIL (BASE64-DECODE-ERROR caught here), never to garbage.

parse-signature-input

(parse-signature-input value)

Parse a Signature-Input header VALUE — `label=("@method" …);created=…` — into a PARSED-SIGNATURE-INPUT, or NIL on malformed input.

The verbatim params substring (everything after `label=`) is retained so the verifier echoes the bytes the signer committed to rather than re-canonicalizing from its own clock (RFC 9421). CREATED/EXPIRES/KEYID/ALG/NONCE are additionally parsed out for the replay-window and keyid checks.

reset-nonce-cache

(reset-nonce-cache)

Clear the replay nonce cache. Exported for tests that drive replay scenarios across fixtures without cross-contaminating the live window.

signature-window-fresh-p

(signature-window-fresh-p created expires nonce &optional (now (unix-now)))

Return T when a signature's replay params place it inside the live window and its NONCE has NOT already been recorded — a side-effect-free predicate that does not commit the nonce.

Rejects (returns NIL) when CREATED or EXPIRES is missing, when NOW is past EXPIRES, when CREATED is further in the past than SIGNATURE-SKEW-SECONDS, when the validity span EXPIRES - CREATED exceeds MAX-SIGNATURE-LIFETIME-SECONDS, or when NONCE is already in the cache (a replay of a previously-verified signature). Recording the nonce is the caller's responsibility through COMMIT-NONCE, run only after the signature verifies — so an unverified request can never burn a cache slot (RFC 9421 §7.2.2).

verify-http-signature

(verify-http-signature custody material &optional (now (unix-now)))

Verify the relayed RFC 9421 MATERIAL against the owner key in CUSTODY and return the owner principal on success, or NIL on ANY failure.

The pipeline, each step failing closed to NIL:

  1. Parse Signature-Input (RFC 8941) — malformed → NIL.
  2. Parse Signature, decoding the base64 byte sequence through the file's own fail-closed BASE64-DECODE — corrupt → NIL.
  3. Require the signed keyid to equal the owner DID.
  4. Check the replay window WITHOUT recording the nonce (SIGNATURE-WINDOW-FRESH-P) — a side-effect-free gate.
  5. Rebuild the signature base through the SINGLE shared BUILD-SIGNATURE-BASE, echoing the parsed params so the rebuilt base is byte-identical to the signer's, and verify the Ed25519 signature against the owner public key.
  6. Only on a verified signature, commit the nonce to the replay cache (COMMIT-NONCE). The cache therefore remembers exclusively the nonces of genuine owner signatures, so an unauthenticated peer can neither replay an owner nor exhaust the cache to lock the owner out.

Only when all five hold is the owner principal minted (the same type Noise mints). NOW is injectable for deterministic replay-window tests.

Variables

+nonce-cache-cap+

Hard ceiling on live nonce-cache entries. Pruning by expiry keeps the cache small under honest traffic; this cap bounds it even under a flood of distinct short-lived nonces.

Package valis/src/identity/principal

Classes

principal

Immutable value naming a keyed peer, produced by an authenticator after a completed handshake. Decoupled from Noise — any authenticator (a future HTTP-edge proof, etc.) that recovers an X25519 key and derives the same DID mints the same type. Use ANONYMOUS-P to test for the anonymous sentinel.

Generic functions

principal-did

(principal-did object)

Undocumented: this exported symbol needs a docstring.

principal-public-key

(principal-public-key object)

Undocumented: this exported symbol needs a docstring.

Functions

anonymous

(anonymous)

Return the anonymous principal singleton.

anonymous-p

(anonymous-p x)

Return true if X is the anonymous principal sentinel.

make-principal

(make-principal did-string raw-key-bytes)

Construct a keyed principal from DID-STRING and RAW-KEY-BYTES (32 octets). DID-STRING should be a valid did:key string ("did:key:z…"). RAW-KEY-BYTES is the corresponding X25519 public key — stored for use by downstream layers without requiring them to decode the DID.

principal-p

(principal-p x)

Return true if X is a PRINCIPAL (including the anonymous sentinel).

Package valis/src/identity/ssh-reality

Classes

challenge

A single-use, time-bounded proof-of-reality challenge: the NONCE the operator signs and the EXPIRES-AT universal-time deadline past which it is refused. EXPIRES-AT is mutable only so a test can drive the expiry path deterministically.

challenge-registry

Outstanding challenges keyed by nonce, so each is consumable exactly once.

proof-result

The outcome of a consume. SATISFIED-P is T with BOOTSTRAP-PUBKEY set when both anchors held; otherwise SATISFIED-P is NIL and REFUSAL names the failing anchor (or the spent/expired nonce).

Generic functions

material-bootstrap-pubkey

(material-bootstrap-pubkey material)

The ubik bootstrap public key (32 raw octets) MATERIAL carries, or NIL.

material-host-local-p

(material-host-local-p material)

True only when MATERIAL is a fulcrum enrolment-material carrying the un-forgeable :host-local-intake provenance. Every other object answers NIL: only fulcrum's root-only host-local intake can stamp that provenance.

material-sshsig-blob

(material-sshsig-blob material)

The armored SSHSIG blob (octets) MATERIAL carries, or NIL when it carries none.

Functions

challenge-expires-at

(challenge-expires-at instance)

Undocumented: this exported symbol needs a docstring.

challenge-nonce

(challenge-nonce instance)

Undocumented: this exported symbol needs a docstring.

consume

(consume registry challenge material trust-store)

Attempt to satisfy the proof-of-reality with MATERIAL against the single-use CHALLENGE from REGISTRY, verified against TRUST-STORE's pinned key + namespace.

The proof holds only when BOTH independent anchors hold, each evaluated on its own so a diagnostic can tell WHICH failed — never one combined predicate:

anchor 1 — HOST PRESENCE: MATERIAL carries fulcrum's un-forgeable :host-local-intake provenance. A terminus-facing path can fabricate a stand-in but not that stamp, so it can never satisfy this anchor. anchor 2 — PINNED-KEY REALITY: an operator SSHSIG over the issued nonce verifies against the single pinned Ed25519 key and fixed namespace.

Both anchors are computed up front; the cond then names the failing one. The challenge is single-use and time-bounded: a spent or expired nonce is refused before either anchor is weighed, and a claim consumes the nonce whether or not the proof succeeds. Returns a PROOF-RESULT; a satisfied result carries the ubik bootstrap pubkey. Minting a grant on a satisfied proof is a separate, later step — this stops at the verified result.

issue-challenge

(issue-challenge registry &key nonce (ttl-seconds *nonce-ttl-seconds*))

Mint a single-use, time-bounded challenge and record it outstanding in REGISTRY so it can be consumed exactly once.

In production NONCE defaults to crypto-quality random octets read from /dev/urandom, so every challenge is unpredictable. An explicit NONCE is a TEST AFFORDANCE only — it lets a test drive the verifier against a fixed signed message — and is never passed on the production path, where the fresh random nonce is the replay defense. TTL-SECONDS defaults to the config-tunable nonce lifetime.

make-challenge-registry

(make-challenge-registry)

A fresh, empty challenge registry.

mint-operator-equivalent-grant

(mint-operator-equivalent-grant proof custody trust-store &key (ttl-seconds *tier-1-token-ttl-seconds*))

On a SATISFIED proof, mint the single maximal operator-equivalent grant and return its "token:" bearer string for delivery to ubik. On an unsatisfied proof — either anchor missing — return NIL and mint nothing.

The grant is ONE owner-custody-signed capability token minted through the shared /cap mint path (never a bespoke construction here):

issuer = the owner custody root (CUSTODY's DID) — never ubik, so this is never a ubik->ubik maximal self-mint; audience = the ubik OCAP DID derived from the proof's bootstrap public key; designation = the namespace root "/", which prefix-covers every subtree, so the grant is maximal by reach, not by enumerating control axes; rights = the full set (:read :write :mount :delegate) — :delegate lets an operator-equivalent ubik mint its own scoped grants; expiry = now + TTL-SECONDS, the moderate, revocation-primary Tier-1 lifetime from the client-trust config.

The enrolled-client record (audience DID + the grant's revocation hash) is written to TRUST-STORE so revoking that hash later cascades to every descendant that chains through this grant. Blast radius is contained by revocability + TTL, never by scoping: this remains a revocable, key-bound OCAP token, never the seed.

proof-result-bootstrap-pubkey

(proof-result-bootstrap-pubkey instance)

Undocumented: this exported symbol needs a docstring.

proof-result-p

(proof-result-p object)

Undocumented: this exported symbol needs a docstring.

proof-result-refusal

(proof-result-refusal instance)

Undocumented: this exported symbol needs a docstring.

proof-result-satisfied-p

(proof-result-satisfied-p instance)

Undocumented: this exported symbol needs a docstring.