# Trust

---

# The trust store

_Peios / Using Peios / Trust_

> What a Peios machine trusts when it validates a certificate — Mozilla's roots as package data, additions and distrusts as registry decisions, composed by trustd and rendered to /etc/ssl.

When a program on this machine validates a TLS certificate, the set of
certificate authorities it will accept comes from one place: **trustd**,
the trust store.

trustd composes that set from two inputs and serves the result.

| Input | Where it lives | Who changes it |
|---|---|---|
| Mozilla's root store | `/usr/share/ca-certificates/mozilla.crt`, shipped by the `ca-certificates` package | An upgrade of that package |
| Additions and distrusts | `Machine\System\Trust\Certificates` in the registry | An administrator, or the domain |

The split is the whole design. The roots are **package data** — a hundred
and twenty-odd certificates nobody on this machine decided anything about
— and package upgrades already do the right thing with them, including
removing a root Mozilla has withdrawn. The registry holds **decisions**:
each entry is something somebody chose, which is what keeps an audit of
`Machine\System\Trust` worth reading.

## What programs actually read

The store itself is a socket, `/run/trustd/trust.sock`. Peios-native
programs ask it directly and are told when the answer changes, so a
certificate withdrawn at ten past three stops being accepted by a running
service at ten past three.

Almost nothing does that yet, so trustd also *renders* the store to the
files the portable software ecosystem expects:

```
/etc/ssl/certs/ca-certificates.crt   the bundle Go and most software read
/etc/ssl/cert.pem                    OpenSSL's default CAfile
/etc/ssl/certs/<hash>.0              OpenSSL's default CApath
```

Those files are a **copy of an answer**, not the answer. Editing one
changes nothing: trustd overwrites it the next time anything changes. The
`GenerateLinuxTrustFiles` value decides whether they exist at all — see
[the compat files](/peios/using-peios/trust/the-compat-files.md).

> [!NOTE]
> There is no `update-ca-certificates` and no directory to drop a `.crt`
> into. Both exist on other systems because trust policy lives in files
> there; here it lives in the registry, where changing it is an access
> check against a key's descriptor rather than a question of who can write
> to a directory.

## Seeing what is in force

```
$ trust status
generation   7
health       ok
roots        121 in force
  shipped    121
  added      0
  distrusted 0
compat       GenerateLinuxTrustFiles = 1
  rendered   /system/retc/ssl/certs/ca-certificates.crt
  rendered   /system/retc/ssl/cert.pem
```

`generation` increases every time the store changes, so a program holding
a copy can tell whether it is current without fetching it again.
`health` is `degraded` when the last composition failed — see
[when the store is degraded](/peios/using-peios/trust/adding-and-distrusting.md).

## Composition is all or nothing

If the shipped bundle cannot be read, or yields implausibly few roots, or
every root would be distrusted, trustd **keeps whatever it rendered last**
and reports itself degraded. It never writes a partial store.

That is deliberate, and the reasoning is worth stating: a silently empty
trust store breaks every TLS client on the machine at once, and a silently
truncated one might be missing the distrust that was the point of the last
change. Stale-but-whole is a recoverable state; partial is not.

A single bad *entry* is different. An addition that is not a certificate,
is not a CA, or has expired is skipped with a warning and everything else
proceeds — one administrator's typo does not take the machine's trust
down.

## Who may change it

`Machine\System\Trust\Certificates` and its subkeys, like any registry
key, are governed by their own security descriptor. By default SYSTEM and
Administrators may write; everybody may read. That descriptor is the
*only* gate: [`trust`](/peios/using-peios/trust/the-trust-command.md) and `reg` write the
same values through the same check, so there is no second permission model
to keep in step with the first.

Reading is open to everyone, because the root set is public — in the
default configuration it is sitting in a world-readable file.

## What trustd is not

**It holds no private key.** Not the machine's, not anybody's. A service
that holds keys is a signing oracle if it is ever compromised, and wants
almost the opposite security posture from one whose socket is open to
every program on the machine. Machine identity — the certificates and keys
a machine presents when *it* is the server — is a separate concern, and
will be a separate service.

**It does not fetch anything.** No root is downloaded on demand and no
revocation list is retrieved, in this version. What the machine trusts
changes when a package is upgraded or somebody writes to the registry, and
at no other time.

---

# Adding and distrusting certificates

_Peios / Using Peios / Trust_

> Trust a certificate authority the shipped set does not include, stop trusting one it does, and understand what happens when the store cannot be composed.

Two things can be decided about this machine's trust, and both are values
under `Machine\System\Trust\Certificates`.

## Trusting a certificate authority

An internal CA, a test CA, a partner's CA:

```
$ trust add corp-ca /path/to/corp-root.pem
added corp-ca
  CN=Corp Issuing CA,O=Example Ltd,C=GB
  SHA-256 4f2a…c19b
```

The name is yours to choose — it names the *decision*, not the
certificate, so `corp-ca` is a better name than `4f2a`. PEM or DER is
accepted; a file holding more than one certificate is refused, because a
bundle added under one name would be one decision covering several
authorities.

The certificate is checked before it is written: it must parse, it must be
a CA (`basicConstraints`), and it must not have expired. trustd checks it
again when it reads it — it never trusts the writer — but doing it here
means a mistake is a message rather than a line in a log.

To undo:

```
$ trust remove corp-ca
```

### Purposes

By default an addition is trusted for `ServerAuth` — validating a TLS
server. Say otherwise if you mean otherwise:

```
$ trust add corp-ca corp-root.pem --purposes ServerAuth,CodeSigning
```

This version renders `ServerAuth` roots and carries the rest, so a root
recorded for another purpose is stored faithfully and used when the
feature that consumes it arrives.

## Distrusting a certificate authority

This is the one that has to work in a hurry, and it works on any
certificate — one Mozilla shipped or one somebody added:

```
$ trust distrust 018e13f0772532cf --reason "withdrawn, see CA/B incident 2026-08"
distrusted 018e13f0772532cf809bd1b17281867283fc48c6e13be9c69812854a490c1b05
  was: CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US
```

Within a second or two the root is gone from the store, gone from the
rendered bundle, and its file is gone from the hashed directory. Nothing
is rebuilt and nothing is rebooted, which is the reason the shipped roots
can be package data in the first place: **withdrawal never waits for a
package**.

A certificate is named by its SHA-256 fingerprint — never by subject name,
which is forgeable and reused. Any of these work:

```
018e13f0772532cf                      the prefix `trust list` prints
018E13F0…C1B05                        upper case
01:8e:13:f0:…                          colon-separated, as other tools print it
/path/to/the-certificate.pem          the file itself
```

Distrusting a certificate this machine does not have is legitimate and is
recorded rather than refused — the entry sits there, and if that root ever
arrives in a bundle upgrade or an addition, it is already refused.

To see what has been decided, and to undo it:

```
$ trust list --distrusted
018e13f0772532cf  withdrawn, see CA/B incident 2026-08

1 distrusted

$ trust restore 018e13f0772532cf
restored 018e13f0772532cf809bd1b17281867283fc48c6e13be9c69812854a490c1b05
```

## Doing it from the registry

`trust` writes registry values and nothing else, so `reg` does the same job
and is subject to exactly the same access check:

```
Machine\System\Trust\Certificates\
  Add\<name>\
    Certificate   REG_BINARY    the certificate, DER
    Purposes      REG_MULTI_SZ  default ServerAuth
  Distrust\
    <fingerprint> REG_SZ        why, for whoever reads it later
```

Write `Certificate` **last** when creating an entry by hand. Until it
exists the entry is incomplete, and trustd deliberately skips incomplete
entries rather than acting on half of one.

This is also how a domain distributes trust: pushing a value to
`Trust\Certificates\Add` on every member is an ordinary policy write, not
a special mechanism.

## Packages may offer trust, never take it

A package must never widen what the whole machine trusts by being
installed — `peipkg install` quietly adding a CA is a supply-chain hole,
and every system that allowed it eventually closed it.

A package that comes with a CA ships a *seed* (`/usr/share/regim/…`),
inert until an image names it in `[registry] autoapply` or an
administrator applies it. Installing the package grants nothing.

A package shipping a trust store for **its own** private use — a browser,
a language runtime — is unaffected by any of this. Those are ordinary
files that only that program reads.

## When the store is degraded

```
$ trust status
health       degraded — cannot read /usr/share/ca-certificates/mozilla.crt: …
```

`degraded` means the last attempt to compose the store failed, and what is
rendered is what was rendered before — possibly stale, never partial. The
machine keeps working with the trust it last had.

The usual causes are a missing or damaged `ca-certificates` package, or a
distrust list that would empty the store entirely. Fix the cause and:

```
$ trust reload
```

Skipped entries are different and are not a degraded state: `trust status`
counts them and the log names each one.

---

# The trust command

_Peios / Using Peios / Trust_

> Every verb of the trust command — what it reads, what it writes, and what its exit statuses mean.

`trust` is the trust store's operator command. It **reads over trustd's
socket and writes to the registry**, and the asymmetry is worth
understanding: only trustd knows the *effective* set, because the shipped
roots are package data rather than registry entries, so a listing read from
the registry would show a handful of local decisions and none of the
hundred and fifty roots actually in force. Writes go the other way, to the
registry, so that `trust` and `reg` pass through the same access check.

## Verbs

| Command | Reads | Writes |
|---|---|---|
| `trust list [--purpose P]` | socket | — |
| `trust list --distrusted` | registry | — |
| `trust show <fingerprint> [--pem]` | socket | — |
| `trust status` | socket | — |
| `trust add <name> <file\|-> [--purposes A,B]` | the file | registry |
| `trust remove <name>` | — | registry |
| `trust distrust <fingerprint\|file> [--reason R]` | socket, then | registry |
| `trust restore <fingerprint>` | registry | registry |
| `trust reload` | — | — (asks trustd to recompose) |

## Listing

```
$ trust list
018e13f0772532cf  shipped    CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US
0a81ec5a929777f1  shipped    CN=GlobalSign,O=GlobalSign,OU=GlobalSign Root CA - R3
4f2ab19c8e10dd77  added:corp-ca  CN=Corp Issuing CA,O=Example Ltd,C=GB

121 root(s)
```

The first column is the first sixteen characters of the SHA-256
fingerprint, and every verb that takes a fingerprint accepts that prefix —
what is printed can be pasted back in. An ambiguous prefix is refused
rather than guessed at.

`--purpose ServerAuth` narrows the listing to roots carrying that purpose.

## Showing one root

```
$ trust show 4f2ab19c
subject      CN=Corp Issuing CA,O=Example Ltd,C=GB
fingerprint  4f2ab19c8e10dd77…
source       added
added as     corp-ca
purposes     ServerAuth
expires      2074003199

-----BEGIN CERTIFICATE-----
…
```

`--pem` prints the certificate alone, for piping into a file or another
tool.

## Exit statuses

| Status | Meaning |
|---|---|
| `0` | Done. |
| `2` | Asked about something that is not there — no root matches, no such addition, not distrusted. |
| `1` | It went wrong: the daemon is unreachable, the file is not a certificate, the registry refused the write. |
| `64` | The command line was not understood. |

The separation of `2` from `1` is what lets a script tell "this machine
does not trust that CA" from "I could not find out".

## When a write is refused

```
$ trust distrust 018e13f0772532cf
trust: not permitted to change Machine\System\Trust\Certificates\Distrust —
       changing what this machine trusts is governed by that key's descriptor
```

`trust` has no privilege of its own to grant. Changing the machine's trust
is a registry write, and whether you may make it is decided by the key's
security descriptor — the same answer `reg` would get.

---

# The files under /etc/ssl

_Peios / Using Peios / Trust_

> Why the trust store is rendered to files at all, what each of the three artifacts is for, and what turning them off does.

The machine's trust store is a socket. The files under `/etc/ssl` are a
compatibility rendering of it, for software that reads trust from a path —
which today is very nearly all software.

They are generated. Editing one changes nothing except until the next time
anything changes, and a header at the top of the bundle says so.

## Three artifacts, because the ecosystem does not agree

| Path | Who reads it |
|---|---|
| `/etc/ssl/certs/ca-certificates.crt` | Go probes this first; most software is configured with it |
| `/etc/ssl/cert.pem` | OpenSSL's default `CAfile` on this system |
| `/etc/ssl/certs/<subject-hash>.0` | OpenSSL's default `CApath` |

The hashed directory is not decoration. OpenSSL looks a certificate up
there by a hash of its subject, and a stale file left behind for a root
that has been distrusted would go on being trusted through that path. So
the directory is replaced **atomically** — the new one is built alongside
and exchanged in a single operation — and there is no moment at which it
holds a mixture of the old set and the new.

## Turning them off

`Machine\System\Trust GenerateLinuxTrustFiles` decides whether the files
exist:

| Value | Effect |
|---|---|
| `1` (default) | Rendered and kept current. |
| `0` | No files. Programs that read a path find nothing and fail with certificate errors. |
| `2` | Reserved. Treated as `1`. |

`0` is the machine whose entire software set asks the socket — an
appliance image, in practice, where you know what is installed. On a
general-purpose system it breaks curl, Python, Go binaries and anything
else that reads a bundle, and the breakage looks like a certificate
problem rather than a configuration one, so set it deliberately.

Switching to `0` **removes** the rendered files rather than leaving them.
Stale trust left in force is worse than none: a root distrusted after the
switch would go on being accepted by everything reading the abandoned
copy.

```
$ reg set Machine/System/Trust GenerateLinuxTrustFiles dword:0
$ trust status
compat       GenerateLinuxTrustFiles = 0 (no files; the socket is the only store)
```

The socket keeps serving throughout — `trust list` still answers, and so
does any program that speaks to trustd directly.

## Why a copy, when name resolution gets a pointer

`/etc/resolv.conf` on Peios is a constant file naming the resolver, and
nothing is ever generated into it. Trust cannot work that way: there is no
"ask over here" a certificate bundle can express. A file that lists
authorities has to *be* the list.

That is the whole difference between the two, and the reason this one
carries a knob and a warning while the other does not.

> [!NOTE]
> The rendered files come out readable by everyone, which is correct — the
> set of certificate authorities a machine trusts is not a secret, and
> every program on the machine needs it. What is protected is *changing*
> it, which is a registry write.
