Secret values stay out of ordinary output
link-secret-value holds every secret Link holds, in one of its types
by where it may go, so a wrong way doesn't compile. Beside them are
the one HMAC every signature is, the one way in a post from outside is
checked by, a process safe to hold secrets, and the audit that keeps
each secret in its type.
Its types
Each type says where its secret may go: nowhere, to a program by name, into a signature, or sealed to one recipient.
OpaqueSecret
One Link uses and nobody reads, such as an agent's proof secret or a service's signing secret. It signs and checks; there is no way to its bytes.
Secret
A value another program or service must be given, such as a model's
API key, a connection's token or a password. It's read only by asking for
it by name, Secret::expose, at a place this package's audit lists;
it's never serialized but by bare, never copied but by
Secret::duplicate.
SigningSecret
An Ed25519 key pair's private half, such as a machine's key, which signs and says its public half, and nothing else.
Sealed
Either, encrypted to one recipient, the only form that's saved or sent
as it is: HPKE (RFC 9180) in its base mode, with X25519, HKDF-SHA256
and ChaCha20-Poly1305. What a sealed value is (About: whose, what
kind, which version) is its additional data, so it opens only as what
it was sealed as.
A recipient for one transfer
The recipient's pair is made for one transfer (Recipient) and dropped
when it's done, so a stream captured on the way can never be opened
later. A sealed stream (stream) seals a fresh ChaCha20-Poly1305
secret in its header; numbered records authenticate their length and
order, including a required empty final record, and a failed writer
never makes a successful end.
What none of them does
None prints what it holds: Debug writes <redacted>, so a log line,
an error or a struct printed with {:?} carries none. None has
Display: what takes one would send <redacted> as the value, so it
doesn't compile, and the value is exposed there, by name. Their memory
is zeroed when they're dropped, and compared in constant time
(subtle).
hmac
The one HMAC (SHA-256, on ring) every signature Link makes or checks
is, and the one hex a signature is written in: an agent's proof, a
bridge's, a webhook's. No other workspace signs or decodes a signature
itself.
inbound
Every post from outside that a secret vouches for (a connection's webhook,
a payment's, a repository's, an agent's own) is checked here, built
once. A receiver says how its sender signs, as a value (Scheme); it
never writes verification.
What Inbound::verify gives every receiver
The body capped before anything is read (Refused::TooBig); closed by
default, one without a secret refusing every post
(Refused::Unconfigured), and nothing builds one that takes anyone; a
constant-time compare, of the secret or of its HMAC; and a signed time
checked against its window (Refused::Stale).
Replays, rolling and answers
The delivery's id, for a Seen store to refuse a replay
(Inbound::take); both the current secret and the one before it
tried, so any secret rolls with no restart (Secrets); and one answer
for each refusal (Refused::status).
process
A process safe to hold secrets, and its children given none but on purpose.
guard
First in every program that holds a secret: no core dump, no other
process of its user reading its memory, its environment or its
descriptors (/proc/<pid> is root's once it isn't dumpable), and its
memory locked out of swap where the machine allows it.
Secret::from_env
Marks the variable it read as taken, and takes it out of the environment while the process has one thread, since changing the environment with another thread running isn't safe.
command and ours
command starts every child: its environment the parent's but every
variable taken and every one whose name says it's a secret's
(names_a_secret). ours starts a child that's one of Link's own
programs: the same, and every secret this one holds handed to it on a
pipe, where its Secret::from_env finds them, so a key you set for
lnk reaches the plugin that reads it.
The pipe
It goes to that child alone: every child started here closes it, so it reaches no program the child starts in turn, but a program of its own not built on this package (a plugin from outside Link) gets it as Link's own do.
ends_with_parent, stand_in_front and carried
ends_with_parent starts a child that ends when this process does,
however it ends; stand_in_front, a child this process waits for, its
stop passed on to it; and carried, a program run for the one that
started this one, as a wrapper runs it, with the environment it was
given for it, whole.
give
The one way to hand a child a secret in its environment, for a program that reads one nowhere else: each a place the audit lists, with why.
OwnedChild
process::OwnedChild owns a subprocess and its process group on Unix.
It observes exit without reaping the leader, stops the group before
reaping and refuses numeric group signals if another reaper has taken
ownership. Its child borrow is for pipes and identity only, never
wait or try_wait. release transfers the complete cleanup obligation
to a separate owner, as a durable service controller does. Descendants
must remain in the owned group for group cleanup to cover them.
The audit
Every folder runs it over its own code, from a test of its own (cargo test -p link-secret-value --test audit for this one, with each
package's secrets.toml or audit.toml): a secret leaves its type
only where a list says it does, so a new place fails until it's listed,
which a review sees. Code under tests/, or in a #[cfg(test)]
module, isn't read.
Counted against the list
exposes: Secret::expose and its kin, and Recipient::open_sealed
and open_json, counted by file against [exposes]. bare: a
secret written as its value (bare, OpaqueSecret::keep) against
[bare]. children: a secret given in a child's environment on
purpose (process::give) or its environment passed on whole
(process::carried) against [children].
Never listed
copies: an exposed value copied out unsaid (clone, to_string,
format! and the like). argv: an exposed value in a command's
arguments. env: a variable that may hold a secret, by its setting
or its name (…_TOKEN, …_KEY, …_SECRET, …_PASSWORD, …_AUTH,
…_CREDENTIALS, …DATABASE_URL), read with env::var or a helper
that returns no Secret.
Never listed either
old ways: zeroize named outside this package, a Debug written
by hand to hide something, or a constant-time compare written by hand.
children: a child started with this process's environment whole,
a secret put in its environment by hand, or the environment's values
read whole. A type merely ending in Command, such as
AvailableCommand, starts no process. programs: a main of a package that uses this one
whose first statement isn't process::guard.
Fields
A field named like a secret (a word of it token, key, secret,
password, auth, credential, apikey) whose type names none of
the secret types, a path, a number or a flag, against [fields],
which maps "<file> <field>: <type>" to why it holds none, so a field
of another type by the same name is a new one.
What it can't see
It reads code as text, so a review still sees a value exposed through
more than one binding before it reaches .env or .arg; a Command
made in one statement and given its environment in another (start it
with process::command); a helper that returns the environment's
values under its own name; and a child started by a library.