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
Holder of the node's own identity keys 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.
Conditions
owner-keyfile-malformed
Signalled when a key file holds a number of octets other than the 32 of an Ed25519 seed: PROBLEM is :size, SIZE the octet count found, and REASON names it and the 32 expected, never an octet of the file. A kind of owner-keyfile-unsafe, so every refusal that takes an unsafe key file takes this one: an owner verb answers 2 before anything is dialled, and a node boot ends as refused, held down by its supervisor.
owner-keyfile-unsafe
Signalled when the file at the owner keyfile's path is not one the node will take as its key. PROBLEM says what was wrong: :not-regular-file for a symbolic link, anything else that is not a regular file, or a path that cannot lead to one, :owner for a file another account owns, with REASON naming its owner and the account this process runs as, :mode for one that grants any permission to its group or to others, and :unopenable for one this account may not open. REASON says what was found, and never carries key material. Nothing is read from the file, and no key is created in its place.
I refuse these because the key is the node's identity. A file someone else put at the path, or one others could have rewritten, would otherwise be taken as the owner's key, and the node would run as an owner nobody chose with nothing reporting a failure.
Generic functions
custody-keyring
(custody-keyring object)
The owner-keyring attached to the store, or NIL when none is, in which case the store admits only the node's own key. Settable with setf.
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.
owner-keyfile-malformed-size
(owner-keyfile-malformed-size condition)
Undocumented: this exported symbol needs a docstring.
owner-keyfile-unsafe-path
(owner-keyfile-unsafe-path condition)
Undocumented: this exported symbol needs a docstring.
owner-keyfile-unsafe-problem
(owner-keyfile-unsafe-problem condition)
Undocumented: this exported symbol needs a docstring.
owner-keyfile-unsafe-reason
(owner-keyfile-unsafe-reason condition)
Undocumented: this exported symbol needs a docstring.
Functions
create-keyfile
(create-keyfile path)
Generate a fresh Ed25519 key, write its 32-octet seed to a new file at PATH at mode 0600, and return the custody store for it. Never replaces anything at PATH and never follows a symbolic link there: the open refuses both, signalling sb-posix:syscall-error (EEXIST or ELOOP) before anything is written.
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-owner-principal-p
(custody-owner-principal-p store auth-identity)
Return true when AUTH-IDENTITY is a live owner authenticator for STORE: the node's own key, as custody-seed-principal-p answers, or a key live on STORE's own keyring under STORE's owner DID, in either DID form. custody-seed-principal-p admits the node's own key alone.
False for a nil STORE, the anonymous sentinel, and a non-principal, and for an enrolled key when STORE carries no keyring or one bound to another owner. Only the store the factotum holds carries a keyring, so every other store admits only the node's own key.
custody-publisher-did
(custody-publisher-did store)
The did:key of the publisher's creating identity on the node whose keys STORE holds. The same keyfile always gives the same did:key, and it is never the owner's.
I derive the creating identity from the node's bootstrap seed rather than store it, so the backup that already carries the seed covers the publisher's root too, and there is no second secret to lose, leak or restore. The derived key is never kept: ask again and it is derived again.
custody-publisher-sign-bytes
(custody-publisher-sign-bytes store octets)
The 64-octet Ed25519 signature the publisher's creating identity makes over OCTETS, on the node whose keys STORE holds. It verifies under CUSTODY-PUBLISHER-DID and never under the owner's key.
This is the only way a creating identity signs in valis: the key is derived for the signature and discarded with it, so nothing here can hand the key out, and no function takes one in.
custody-seed-principal-p
(custody-seed-principal-p store auth-identity)
Return true when AUTH-IDENTITY presents the node's own key, the key STORE holds, and no other. Gates that need that key itself use this; every other owner gate uses custody-owner-principal-p.
Both owner DIDs are compared. A Noise handshake recovers the peer's X25519 static key and derives a did:key from it, so a principal arriving from a real keyed attach is X25519-based and never string-matches the Ed25519 DID that custody holds; internal modules and tests present the Ed25519 one. A check against either alone is a check that passes in one setting and fails in the other.
STORE is nil in a unit test and before the fabric is up. With no custody there is no owner to be, and this returns false, which leaves the caller on its fail-closed arm.
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.
In valis, ironclad:sign-message is called only in this file: here for the owner, and in CUSTODY-PUBLISHER-SIGN-BYTES for the publisher's creating identity. Other valis code (the /cap service, token minting) routes all signing through the custody store, and the raw Ed25519 private key is never passed outside this file. mercer signs with keys it holds itself, so this is a claim about valis source, not about every signature the image makes.
load-keyfile
(load-keyfile path)
The custody store for the keyfile at PATH, or NIL when nothing is there. Never creates a keyfile. Signals owner-keyfile-unsafe, and owner-keyfile-malformed for a file of the wrong size, exactly as load-or-create-keyfile does when it loads.
load-or-create-keyfile
(load-or-create-keyfile path)
Return a CUSTODY-STORE rooted at PATH.
If a keyfile is at PATH: read the 32-byte Ed25519 seed, 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.
The keyfile must be a regular file, reached without following a symbolic link, owned by this process's effective account, and granting nothing to its group or to others, such as 0600 or 0400. Anything else signals OWNER-KEYFILE-UNSAFE and nothing is read: a key someone else could have put there, or rewritten, is not the owner's key.
If nothing is at PATH: generate a fresh Ed25519 key pair, write the seed to a new file at PATH private to this account at 0600, derive the X25519 transport key and DID.
The returned store is the only holder of the owner's keys in the image.
Note: if PATH contains an old X25519 keyfile (from before the Ed25519 re-root), delete it before starting: the 32-byte 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 the Ed25519 seed). 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:
- Parse Signature-Input (RFC 8941): malformed → NIL.
- Parse Signature, decoding the base64 byte sequence through the file's own fail-closed BASE64-DECODE: corrupt → NIL.
- Require the signed keyid to equal the owner DID.
- Check the replay window WITHOUT recording the nonce (SIGNATURE-WINDOW-FRESH-P), a side-effect-free gate.
- 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.
- 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/keyring
Classes
keyring-entry
One enrolled device key. DID is its Ed25519 did:key; TRANSPORT-DID is the X25519 did:key derived from it and never stored. ENROLLED-BY and RETIRED-BY are Ed25519 DIDs; the times are Unix seconds. RETIRED-AT is NIL while the entry is live.
owner-keyring
The device keys enrolled under OWNER-DID. OWNER-TRANSPORT-DID is the owner's X25519 did:key, so the seed is recognized in either form. PATH is the backing file, or NIL for an in-memory keyring. ENTRIES holds every entry in enrolment order. LIVE maps both DIDs of every live entry to it and is read without the lock; KNOWN does the same for every entry, live or retired. LOCK is held across each change.
Conditions
key-already-enrolled
Signalled when an enrolment names a key that is live on the ring.
key-already-retired
Signalled when an enrolment or a retirement names a key already retired.
key-did-invalid
Signalled when a DID to enrol, or the enrolling or retiring DID, is not an Ed25519 did:key that re-encodes to itself. An X25519 did:key is refused here: the keyring records the Ed25519 form and derives the other.
key-label-invalid
Signalled when an enrolment's label is missing or carries a character outside [A-Za-z0-9._-], or more than 64 of them. Whitespace, = and newlines are excluded so that no label can forge a record.
key-not-enrolled
Signalled when a retirement names a key the ring has never enrolled.
key-retirement-sweep-incomplete
Signalled with WARN when a step of the sweep that follows a durable retirement signals. STEP is :fence, :revoke, or :hook (the hook as a whole). DIDS is the retired key's Ed25519 and X25519 DID. CAUSE is the condition the step signalled. The retirement is durable and stands; the failed step is abandoned.
keyring-belongs-to-another-owner
Signalled when the keyring file's header binds it to RECORDED-OWNER and the custody owner is EXPECTED-OWNER: a keyring copied from another node, or a seed minted fresh beside an old keyring. None of its keys is admitted and no restart is offered.
keyring-change-refused
Parent of every enrolment or retirement the keyring refuses. DID is the key the change named. A refused change writes nothing.
keyring-corrupt
Signalled when the keyring file does not parse exactly. LINE is the 1-based line, or NIL for the file as a whole, and PROBLEM is one of: :torn-tail, the last line has no newline; :unparseable, a line is not exactly a header, an enrol, or a retire record, or the file is not ASCII; :unknown-version, the header names a version this node does not read; :missing-header, the file is empty or its first line is not a header; :retire-without-enrol, a retirement names a key never enrolled; :invalid-key, a key or enrolling DID does not name a usable Ed25519 key; :owner-key, a record names the owner's own key; :duplicate-enrolment, a key is enrolled twice; :duplicate-retirement, a key is retired twice. No line is ever skipped and no restart is offered: a keyring with any line that does not parse is refused whole.
keyring-error
Root of every condition the owner keyring signals. PATH is the keyring file concerned, or NIL for an in-memory keyring.
keyring-file-unsafe
Signalled when the file at the keyring's path is not one the node will read or append to. PROBLEM is one of: :not-regular-file, a symbolic link, a FIFO, socket, device, or directory, or a path that cannot lead to a regular file; :owner, a file another account owns, with REASON naming its owner and the account this process runs as; :mode, a file granting any permission to its group or to others; :unopenable, a file this account may not open. Nothing is read from the file and nothing is written through it. A restart finds the same file the same way.
keyring-unusable
Parent of every condition that stops a keyring file being loaded. The node refuses to start on one rather than admit by a record it cannot read.
keyring-write-failed
Signalled when appending a record fails at a system call before the record is durable. ERRNO is the failing call's errno. The in-memory keyring is unchanged. RESTORED is true when the file was put back to the bytes it held before the append, or removed when the append created it; false when putting it back failed too, which is logged at warn.
seed-key-refused
Signalled when an enrolment or retirement names the owner's own key, in either DID form.
Generic functions
key-label-invalid-label
(key-label-invalid-label condition)
The label the enrolment was refused for, or NIL when none was given.
key-retirement-sweep-incomplete-cause
(key-retirement-sweep-incomplete-cause condition)
The condition the failed sweep step signalled.
key-retirement-sweep-incomplete-dids
(key-retirement-sweep-incomplete-dids condition)
The retired key's two DID strings, as a list: its Ed25519 DID, then its X25519 DID.
key-retirement-sweep-incomplete-step
(key-retirement-sweep-incomplete-step condition)
The keyword naming the sweep step that signalled: :fence, :revoke, or :hook.
keyring-belongs-to-another-owner-expected-owner
(keyring-belongs-to-another-owner-expected-owner condition)
The owner DID of the custody store the keyring was loaded for.
keyring-belongs-to-another-owner-path
(keyring-belongs-to-another-owner-path condition)
The path of the keyring file bound to another owner.
keyring-belongs-to-another-owner-recorded-owner
(keyring-belongs-to-another-owner-recorded-owner condition)
The owner DID the keyring file's header records.
keyring-change-refused-did
(keyring-change-refused-did condition)
The DID the refused enrolment or retirement named, as given.
keyring-corrupt-line
(keyring-corrupt-line condition)
The 1-based number of the line that does not parse, or NIL when the file as a whole is refused.
keyring-corrupt-problem
(keyring-corrupt-problem condition)
The keyword naming why the keyring does not parse, one of those listed on keyring-corrupt.
keyring-error-path
(keyring-error-path condition)
The keyring file the condition concerns, as a pathname, or NIL for an in-memory keyring.
keyring-file-unsafe-path
(keyring-file-unsafe-path condition)
The path of the refused keyring file. The node neither reads it nor appends to it.
keyring-file-unsafe-problem
(keyring-file-unsafe-problem condition)
The keyword naming why the file is unsafe: :not-regular-file, :owner, :mode, or :unopenable.
keyring-file-unsafe-reason
(keyring-file-unsafe-reason condition)
A phrase saying why the file is refused, such as the account that owns it.
keyring-refusal-token
(keyring-refusal-token condition)
The closed-set reason a refused keyring change replies with: seed-key, invalid-key, invalid-label, already-enrolled, already-retired, or not-enrolled.
keyring-write-failed-errno
(keyring-write-failed-errno condition)
The errno of the system call that failed while appending a record.
keyring-write-failed-restored
(keyring-write-failed-restored condition)
True when the keyring file was put back to its bytes before the failed append, or removed when that append created it; false when putting it back failed too.
Functions
append-keyring-retirements
(append-keyring-retirements octets dids by)
OCTETS, a whole keyring file, followed by one retire record for each of DIDS, in order, at the current time and recorded as by BY.
enrol-owner-key
(enrol-owner-key keyring did &key label by)
Enrol the device key DID, an Ed25519 did:key, on KEYRING under LABEL, recording BY, the enrolling DID, which defaults to the owner's. Returns the new entry. Signals, before anything is written: key-did-invalid when DID is not an Ed25519 did:key in its canonical spelling; ed25519-public-key-invalid when it names no valid key; key-label-invalid for a bad LABEL; seed-key-refused for the owner's own DID; key-already-enrolled or key-already-retired when DID is already on the ring. Signals keyring-file-unsafe when the file at the keyring's path is not one the node keeps, and keyring-write-failed when the append fails before it is durable, leaving KEYRING unchanged either way and the file put back as it was. A failed close after the record is durable does not undo the enrolment.
install-keyring-octets
(install-keyring-octets path octets)
Install OCTETS, a whole keyring file, at PATH at mode 0600 and return the entries it holds, live and retired, in enrolment order. The octets are parsed whole first: keyring-corrupt is signalled and nothing is written unless they parse exactly. The file is written beside PATH and renamed over it, so a crash leaves no file cut short.
key-label-valid-p
(key-label-valid-p label)
True when LABEL is a label an enrolment accepts: 1 to 64 characters from A-Z, a-z, 0-9, dot, underscore, and hyphen. The rule enrolment applies, for a client that checks a label before suggesting it.
keyring-admits-p
(keyring-admits-p keyring owner-did did)
True when DID, in either form, is a live key on KEYRING and KEYRING is bound to OWNER-DID. False for a NIL keyring. Takes no lock, so an attach never waits on a durable write.
keyring-entries
(keyring-entries keyring)
Every entry on KEYRING, live and retired, in enrolment order, as a fresh list.
keyring-entry-did
(keyring-entry-did instance)
The entry's Ed25519 did:key, the form the keyring file records.
keyring-entry-enrolled-at
(keyring-entry-enrolled-at instance)
When the key was enrolled, in Unix seconds.
keyring-entry-enrolled-by
(keyring-entry-enrolled-by instance)
The Ed25519 did:key of the key that enrolled this one.
keyring-entry-for-did
(keyring-entry-for-did keyring did)
The entry for DID in either form, live or retired, or NIL.
keyring-entry-label
(keyring-entry-label instance)
The label given when the key was enrolled.
keyring-entry-live-p
(keyring-entry-live-p entry)
True while ENTRY has not been retired.
keyring-entry-p
(keyring-entry-p object)
True when the object is a keyring-entry.
keyring-entry-retired-at
(keyring-entry-retired-at instance)
When the key was retired, in Unix seconds, or NIL while the entry is live.
keyring-entry-retired-by
(keyring-entry-retired-by instance)
The Ed25519 did:key of the key that retired this one, or NIL while the entry is live.
keyring-entry-transport-did
(keyring-entry-transport-did instance)
The entry's X25519 did:key, the form a Noise session presents; derived from the Ed25519 key, never stored.
keyring-file-octets
(keyring-file-octets path &key keyring)
The octets of the keyring file at PATH, or NIL when no file is there. Read under KEYRING's lock when KEYRING is the open keyring backed by PATH, so the octets never hold part of a change. Signals keyring-file-unsafe unless the file is a regular file this account owns and keeps private, and keyring-corrupt unless the octets parse exactly.
keyring-listing-lines
(keyring-listing-lines keyring)
One line per entry on KEYRING, in enrolment order, without newlines: proto=noise key=<ed25519 did> transport=<x25519 did> label=<label> enrolled=<unix> by=<did> state=live, or, for a retired entry, state=retired@<unix> retired-by=<did> in place of state=live.
keyring-path-beside
(keyring-path-beside keyfile)
The keyring file's path for the owner keyfile at KEYFILE: the keyfile's own pathname with the type keyring.
keyring-retirements-named
(keyring-retirements-named octets owner-did dids)
The members of DIDS, Ed25519 DIDs live in some candidate keyring, that OCTETS, read as OWNER-DID's keyring file however damaged, retires or names, in the order of DIDS. A second value pairs each with the line that named it, as (did . text). A DID is named by: a retirement among the whole records the loader takes; any line it does not take, the torn tail included, carrying a key= value V that is non-empty and equal to the DID or a prefix of it; any such line whose first token is a non-empty prefix of retire and that carries no non-empty key= value, which names every member of DIDS. Returns NIL when the header names another owner.
load-owner-keyring
(load-owner-keyring path owner-did)
The keyring kept in the file at PATH, bound to OWNER-DID. No file at PATH gives an empty keyring backed by PATH, and nothing is written; an empty file is refused. Signals keyring-file-unsafe unless the file is a regular file this account owns and keeps private, keyring-corrupt unless it parses exactly, and keyring-belongs-to-another-owner when its header names an owner other than OWNER-DID.
make-owner-keyring
(make-owner-keyring &key owner-did path)
An empty keyring bound to OWNER-DID and backed by PATH, or held only in memory when PATH is NIL. Nothing is written.
owner-keyring-owner-did
(owner-keyring-owner-did instance)
The owner's Ed25519 did:key, the DID the keyring is bound to.
owner-keyring-owner-transport-did
(owner-keyring-owner-transport-did instance)
The owner's X25519 did:key, or NIL when the owner DID is not a usable Ed25519 did:key.
owner-keyring-p
(owner-keyring-p object)
True when the object is an owner-keyring.
owner-keyring-path
(owner-keyring-path instance)
The pathname of the backing keyring file, or NIL for an in-memory keyring.
parse-owner-keyring-octets
(parse-owner-keyring-octets octets owner-did &key path)
The keyring OCTETS hold, a whole keyring file, bound to OWNER-DID and backed by PATH, or held only in memory when PATH is NIL. Signals what load-owner-keyring signals for a file holding OCTETS: keyring-corrupt unless they parse exactly, and keyring-belongs-to-another-owner when the header names an owner other than OWNER-DID.
read-owner-keyring-file
(read-owner-keyring-file path)
Every octet of the keyring file at PATH, unparsed, or NIL when nothing is there. Signals keyring-file-unsafe, before reading a byte, unless the file is a regular file, reached without following a symbolic link, that this account owns and keeps private.
retire-owner-key
(retire-owner-key keyring did &key by)
Retire the key DID, given in either DID form, recording BY, the retiring DID, which defaults to the owner's. Returns the entry. The retirement is durable, then the key leaves the live index, then key-retirement-hook is called with both of its DIDs. A hook that signals never unwinds past the retirement: the failure is logged at warn, signalled as a key-retirement-sweep-incomplete warning with step :hook, and the entry is returned. Signals, before anything is written: seed-key-refused for the owner's own DID in either form; key-not-enrolled for a key never enrolled; key-already-retired for one already retired. Signals keyring-file-unsafe when the file at the keyring's path is not one the node keeps, and keyring-write-failed when the append fails before it is durable, leaving KEYRING unchanged either way and the file put back as it was. A failed close after the record is durable does not undo the retirement.
salvage-owner-keyring-octets
(salvage-owner-keyring-octets octets owner-did &key path)
The longest prefix of whole valid records OCTETS hold, read as OWNER-DID's keyring file, as two values: the octets of that prefix, header included and byte for byte as OCTETS hold it, and the lines left out, each (line-number problem text) in file order. The torn tail is listed as :torn-tail, the first refused line with its keyring-corrupt problem, and every line after it as :after-dropped-line. A missing or unparseable header gives a fresh header for OWNER-DID and no records. Records are judged by the loader's own record parser. Signals keyring-belongs-to-another-owner when the header names another owner, and keyring-corrupt :unknown-version for a header of another version; PATH names the file in either.
Variables
*key-retirement-hook*
A function of one argument, the list of a retired entry's two DID strings, called by retire-owner-key after the retirement is durable and the key is out of the live index, with the keyring lock released. A hook that signals is logged at warn, reported as a key-retirement-sweep-incomplete warning, and skipped; the retirement stands. NIL when nothing is installed.
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/private-file
Functions
check-private-descriptor
(check-private-descriptor path fd refuse &key (noun "file"))
The status of the file open on FD, once it is known to be a regular file owned by this process's effective account and granting nothing to its group or to others. Calls REFUSE naming PATH otherwise: :not-regular-file, :owner naming both account numbers, or :mode naming the mode in octal. NOUN names the file in the :owner reason. The descriptor is examined rather than the path, so the file checked is the file used.
open-private-file-for-append
(open-private-file-for-append path refuse &key create)
A descriptor open for appending on the file at PATH, which the caller closes. Without CREATE, answers NIL when nothing is there. With CREATE, makes a new file at mode 0600 less the umask and signals sb-posix:syscall-error when anything is already at PATH. Never follows a symbolic link and never waits on a FIFO. Calls REFUSE as open-private-file-for-reading does, and for a directory. Pass the descriptor to check-private-descriptor before writing to it.
open-private-file-for-reading
(open-private-file-for-reading path refuse)
A descriptor open for reading on the file at PATH, which the caller closes, or NIL when nothing is there. Never follows a symbolic link and never waits on a FIFO. Calls REFUSE for a symbolic link, a socket or device, a path through something that is not a directory, or a file this account may not open. Pass the descriptor to check-private-descriptor before reading from it.
Constants
+o-cloexec+
OCLOEXEC, so a child the node starts never inherits a descriptor open on a private file. sb-posix does not name it on every SBCL, so the Linux value stands in when it does not; valis runs only on Linux.
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), where :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.