Implement client configuration delivery and reissue lifecycle
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# Client configuration rendering
|
||||
|
||||
`delivery` contains pure rendering functions. They accept the committed
|
||||
byte-preserving `ConfigDocument`, validated saved settings, and either the new
|
||||
client's private key or a freshly password-retrieved `RetrievedClientRecord`.
|
||||
Each renderer derives the server public key inside its boundary from exactly one
|
||||
valid `[Interface] PrivateKey` in that saved document. Missing or duplicate
|
||||
interface sections, missing or duplicate private-key directives, malformed key
|
||||
text, and structurally malformed private-key directives fail closed. Callers
|
||||
cannot supply or retain an unrelated server identity. The derivation keeps the
|
||||
source key borrowed and the parsed secret local to the call; neither is copied
|
||||
into an error or retained object. The renderer does not read paths, inspect the
|
||||
host, access the running interface, save a file, or authorize a QR request.
|
||||
|
||||
The renderer locates the client's peer by public key and requires one unique
|
||||
peer with one valid public key field and supported authoritative address routes.
|
||||
Addresses come from that saved peer's `AllowedIPs` host entries. Each assigned
|
||||
address is rendered as a host prefix (`/32` for IPv4 and `/128` for IPv6); these
|
||||
client `[Interface] Address` values remain distinct from server-side
|
||||
`[Peer] AllowedIPs` routes. A new client requires one IPv4 and one IPv6 saved
|
||||
address, uses `0.0.0.0/0, ::/0`, and gets endpoint and DNS from the saved
|
||||
application settings. No MTU, keepalive, or preshared-key defaults are added.
|
||||
|
||||
For imported reissue, the record's client addresses and optional saved server
|
||||
public key are checked against current authoritative saved values. A mismatch,
|
||||
stale address, missing or ambiguous peer, unsupported peer shape, or malformed
|
||||
retained field fails closed. The renderer never truncates routes. It preserves
|
||||
validated retained DNS, MTU, endpoint, AllowedIPs, keepalive, and preshared key
|
||||
when present; application DNS and endpoint settings supply values absent from
|
||||
the imported record. Saved peer addresses and derived server identity always
|
||||
remain authoritative. DNS and endpoint values are parsed and emitted as
|
||||
canonical single-line text; unknown directives and hooks are never copied.
|
||||
|
||||
The precedence is therefore:
|
||||
|
||||
1. The saved interface private key determines the server public identity, and
|
||||
saved server peer identity and host addresses are authoritative for both
|
||||
rendering paths. Imported retained server identity, when present, must match
|
||||
this derived identity.
|
||||
2. For new clients, saved application endpoint and DNS settings fill the
|
||||
template; full-tunnel routes are the fixed new-client template.
|
||||
3. For imported clients, authenticated retained optional values take precedence
|
||||
over current endpoint and DNS defaults when present; retained server identity
|
||||
and addresses must still match saved state. Retained AllowedIPs are preserved
|
||||
after validation.
|
||||
|
||||
`ClientConfigPayload` owns rendered plaintext in a zeroizing byte buffer. Its
|
||||
`Debug` output reports only length and redaction; it has no generic serialization
|
||||
or display implementation. `as_bytes()` is the explicit disclosure boundary
|
||||
that later delivery code must keep operation-local. The payload is capped at
|
||||
64 KiB. Temporary Base64 encodings and application-owned copies created after
|
||||
`as_bytes()` cannot be erased by this owner; delivery code must minimize and
|
||||
bound those copies. No payload, password, or key is returned in errors.
|
||||
|
||||
Fresh retrieval is required for imported records: `VaultBundle` checks the
|
||||
authenticated reissue-blocked state before constructing a
|
||||
`RetrievedClientRecord`. This renderer offers no password-free retrieval API
|
||||
and does not create first-issue eligibility. Retained fields are readable
|
||||
through reference accessors, while their authenticated values cannot be changed
|
||||
by library callers after retrieval. The imported renderer requires imported
|
||||
provenance and a present matching server public key; a key-only new-template
|
||||
record has no such binding and cannot use the imported path. The application grants initial
|
||||
output only after Commit in the same creation session and supplies the payload
|
||||
only inside its file-publication closure.
|
||||
|
||||
Later reissue is exposed by `Application::reissue_client_file`. Every call
|
||||
reads installed saved bytes, freshly decrypts the installed vault identity with
|
||||
the supplied per-action password, checks the authenticated blocked status, and
|
||||
renders against the saved server document and settings. Draft peers and
|
||||
retained original client files are never consulted. The short-lived decrypted
|
||||
record is dropped after the file and optional QR operation; no unlocked vault
|
||||
or identity is cached in the application.
|
||||
|
||||
Authenticated key-only records created by this application are classified as
|
||||
new-template records only when every retained connection field is absent or
|
||||
empty. That validated provenance path renders the current saved peer addresses
|
||||
with the saved endpoint/DNS template. An empty address list combined with any
|
||||
retained field is treated as a corrupt partial record. Imported records must
|
||||
carry valid addresses and a server identity; those values are checked against
|
||||
the saved peer and derived server identity, and retained optional parameters
|
||||
are validated before rendering. Missing or malformed required retained values
|
||||
fail closed instead of falling through to the new-client template.
|
||||
|
||||
First-issue file and file-plus-QR success results, including definite
|
||||
publication with uncertain post-publication bookkeeping, carry an activation
|
||||
warning. The warning is suppressed only when the current application session
|
||||
has verified the exact installed saved bytes and matching saved/applied peer
|
||||
snapshot. Unknown runtime state and historical verification timestamps do not
|
||||
suppress it.
|
||||
|
||||
Reissue failures expose a fixed stage (`SavedState`, `Authorization`,
|
||||
`Rendering`, or `FilePublication`) and secret-free category. The mandatory file
|
||||
is published before optional QR encoding; QR failure is returned separately
|
||||
after file success. Reissue uses the same generation-bound activation warning.
|
||||
Delivery performs no runtime read, service operation, restart, or applied-state
|
||||
mutation.
|
||||
|
||||
## Plaintext file publication
|
||||
|
||||
Initial delivery asks the caller for an explicit destination path; aliases are
|
||||
never used to construct a path. Destination preparation performs no writes.
|
||||
It rejects non-absolute paths, parent traversal, symlinks in existing path
|
||||
components, symlink or special-file targets, the settings/server/internal-state/
|
||||
vault files, the data and archive directories, and every existing entry in the
|
||||
configured original-client directory. An existing regular file can be selected,
|
||||
but preparation records its filesystem identity and requires a separate
|
||||
confirmation tied to that prepared destination. The configured source directory
|
||||
is enumerated with the same entry bound used by import; enumeration failures or
|
||||
an unsafe source directory stop output preparation.
|
||||
|
||||
Publication requires the chosen output parent to be owned by the effective
|
||||
user or root. The output parent cannot be group- or world-writable; existing
|
||||
ancestors cannot be group- or world-writable unless the sticky bit protects
|
||||
entries (for example `/tmp`). These checks are repeated immediately before
|
||||
opening the parent, and the opened directory is checked as well. This keeps
|
||||
other users from substituting the temporary entry or destination inside the
|
||||
output parent. A bounded payload is written to a same-directory temporary
|
||||
file created with mode `0600` and a 128-bit operating-system-random name, then
|
||||
all bytes are flushed and synced before publication. A new destination uses
|
||||
no-replace publication, so a file created after preparation is not silently
|
||||
overwritten. A confirmed replacement uses an atomic exchange: the displaced
|
||||
entry is checked against the confirmed inode before it is removed. If a
|
||||
different entry was substituted at publication, the adapter attempts a second
|
||||
exchange and checks both resulting identities. It never unlinks by name during
|
||||
rollback: a same-privilege concurrent substitution could otherwise cause an
|
||||
unconfirmed entry to be destroyed. Even after an identity-verified rollback,
|
||||
the staged configuration remains under its mode-`0600` temporary name because
|
||||
unlink-by-name cannot be made conditional on inode identity. A completed
|
||||
rollback is reported as `DestinationChanged` only after the parent directory
|
||||
sync succeeds. If the second exchange, identity checks, or sync fail,
|
||||
publication is reported as uncertain and all entries still reachable by their
|
||||
names are retained.
|
||||
|
||||
Missing parent directories are created with mode `0700`; existing directories
|
||||
are not chmodded. The destination parent is synced after publication. On a
|
||||
pre-publication error, the temporary file is removed and first-issue eligibility
|
||||
remains retryable. If removal of a pre-publication temporary file fails, the
|
||||
operation reports that a private temporary configuration may remain and
|
||||
requires manual cleanup. First-issue retry requires an explicit cleanup
|
||||
acknowledgement; inspect the selected output parent for
|
||||
`.wg-admin-client-*.tmp` files and remove the retained file after verifying its
|
||||
identity. This result does not mean the selected destination was published.
|
||||
If parent sync fails after publication, the output may
|
||||
already exist: it is reported as uncertain, retained, and a retry requires a
|
||||
fresh selection and explicit overwrite confirmation. The application never
|
||||
removes a published file to pretend an uncertain save did not happen.
|
||||
|
||||
The Linux exchange operation is atomic for the two names but cannot condition
|
||||
the exchange on previously observed inode identities. The parent-directory
|
||||
ownership and permission checks plus the unpredictable temporary name prevent
|
||||
other users from racing those names. A process with the same effective
|
||||
privilege can still change names after identity checks; concurrent edits by the
|
||||
administrator are outside this adapter's single-administrator model. Callers
|
||||
must treat `PublicationUncertain` as requiring inspection before retrying. A
|
||||
retained temporary file is private to the current user and may need explicit
|
||||
cleanup after the output state is understood.
|
||||
|
||||
Successful file publication consumes the session's first-issue eligibility. If
|
||||
the file is published but the subsequent saved-state read needed to record that
|
||||
transition fails, the result explicitly reports the file as published with
|
||||
bookkeeping uncertain and drops the cached first-issue key; a later issuance
|
||||
requires password authorization.
|
||||
The selected path and confirmation are operation-local and contain no
|
||||
configuration bytes or key material. These checks are filesystem adapter tests
|
||||
using synthetic payloads and temporary directories; they do not represent a
|
||||
privileged production-path test.
|
||||
|
||||
The renderer is verified with synthetic keys and configurations, including
|
||||
round-trip parsing through `vault::import::parse_client_config`. The file
|
||||
adapter is verified with synthetic payloads and temporary paths. This is not
|
||||
evidence of a phone import, live tunnel, production filesystem, or QR scan.
|
||||
|
||||
## Optional QR output
|
||||
|
||||
An authorized first-file operation may request a QR representation in the same
|
||||
operation. The app first publishes the mandatory configuration file, then gives
|
||||
the same borrowed `ClientConfigPayload` to the QR encoder. There is no QR-only
|
||||
entry point and no reconstruction from cached key material. File publication
|
||||
failure skips QR encoding and preserves the existing retry behavior. If QR
|
||||
encoding fails after the file was published, the result reports the file as
|
||||
delivered and the QR error separately; first-issue eligibility is consumed.
|
||||
|
||||
The QR module and encoder are crate-private. External callers can request QR
|
||||
output only through the application file-and-QR operation; the returned matrix
|
||||
and fixed-category error types remain public so callers can display the result.
|
||||
|
||||
The encoder uses `qrcode` 0.14.1 with default features disabled (no image, SVG,
|
||||
or external utility). The application owns a row-major matrix of 0/1 modules with
|
||||
a four-module quiet zone. Its side is at most 185 modules (version 40 plus the
|
||||
quiet zone). Matrix access is explicit; its `Debug` output redacts module data,
|
||||
and the owner zeroizes its buffer on drop. The qrcode encoder necessarily keeps
|
||||
internal payload/codeword and matrix copies that this application cannot
|
||||
zeroize; the app drops its local encoder result immediately after copying the
|
||||
modules into the bounded owner. QR errors contain only a fixed category.
|
||||
|
||||
Tests establish that the encoder receives bytes equal to the just-published file,
|
||||
that encoding succeeds for a synthetic payload, and that capacity failure does
|
||||
not undo file delivery. They do not decode the matrix, establish phone scanning
|
||||
compatibility, or test terminal rendering; those remain future UI/integration
|
||||
checks.
|
||||
Reference in New Issue
Block a user