Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

Binary Signing

Single-page view · as markdown

Binary signing

Peios / Peios Security Fundamentals / Binary Signing

A signed binary carries a cryptographic signature that lets the kernel decide what trust level a process should run at when it execs that binary. The signature is the gate to process integrity protection — every PIP-protected process is PIP-protected because the binary it is running was signed at the appropriate trust level. There is no other way to acquire PIP. No syscall, no privileged operation, no runtime escalation. The signature is it.

The signing model is built on a single principle: signing only adds trust; it never blocks execution. An unsigned binary still runs — it just runs as pip_type = None, unprotected. A binary with a malformed or invalid signature also runs as None. The signature is the kernel's way of recognising "this binary has been blessed at this level by someone we trust"; its absence or invalidity is "we did not recognise this binary, so we will treat it as untrusted". That is different from "we are not going to let it run". Permissiveness is the rule that distinguishes signing from anti-virus.

What signing is for #

Signing serves one purpose: it lets the kernel assign a PIP type and trust level at exec. The kernel reads the binary, finds (or doesn't find) a signature, verifies it against its compiled-in public-key catalog, and sets the new process's pip_type and pip_trust accordingly. From that moment on, PIP enforcement uses those fields.

This is what PIP-protected processes need. peinit, authd, loregd, lpsd, eventd — the TCB daemons — are signed with the TCB key during image build. When the kernel execs them, it sets pip_type = Protected and pip_trust = 8192. Once running, no non-TCB caller can interfere with them because no non-TCB caller dominates their PIP label.

Without signing, the model has nowhere to anchor. There would be no way for the kernel to know that one binary is the real authd and another is malware impersonating it. The signature is the attestation that links a binary on disk to a trust level the kernel will assign at exec.

Where signing fits in the OS #

Signing is a kernel concern, not a userspace one. The kernel:

  • Reads the signature blob from the binary.
  • Verifies it against the compiled-in public-key catalog.
  • Sets the pip_type and pip_trust fields on the new process's PSB.

Userspace tools — typically the image builder, peiso — produce the signature at build time. They have the private key, compute the hash, generate the signature blob, attach it to the binary, and ship it. After that, the private key must not be present on any running Peios system. Verification is public-key only; the only credential a running system needs is the public key, which is compiled into the kernel image.

The split between userspace signing and kernel verification means a Peios system can never sign new binaries itself. Distribution of new signed binaries happens through the package system, with signatures produced offline by whoever holds the relevant private key.

The permissiveness rule #

The kernel's behaviour when a signature is absent or invalid is the design decision that distinguishes signing from anti-virus.

Condition at execResult
No signature at allProcess runs at pip_type = None, pip_trust = 0.
Signature present, version byte unrecognisedSame.
Signature present, key not in catalogSame.
Signature present, but bytes do not verify (hash mismatch)Same.
Signature present and verifies against a catalog keyProcess runs at the PIP level the catalog entry specifies.

The first four rows all produce the same outcome: the exec succeeds, the process runs at None. The fifth row produces a PIP-protected process. In no case does the exec fail because of signing.

The rationale: signing is the layer that adds trust to specific binaries. It is not the layer that decides "only these binaries may run". A user-mode application not signed at any level should still run; it should just not have PIP protection. The mechanism that decides what may run at all is mount policy (FACS), file permissions (DACL), and capabilities (KACS). Signing is a separate axis that sits on top.

This is why signing does not have a "policy" you configure to "require all binaries to be signed". The kernel does not enforce that. The signing layer itself is permissive.

The corollary is that mmap(PROT_EXEC) is not permissive — that is one of the Process mitigations (LSV, Library Signature Verification), and it enforces signing for libraries loaded into a PIP-protected process. But that is the mitigation, not the signing layer. Signing decides PIP at exec; LSV decides what libraries may then be loaded.

What signing does not do #

A few clarifications:

  • Signing does not authenticate the user. A binary signed at TCB level run by an ordinary user is still a TCB-level process from PIP's perspective. The user is not the signer; the signer is whoever owned the private key when the binary was built. Identity-vs-trust is the two-axis story.
  • Signing does not prevent execution. A binary that fails verification still runs. The signing layer does not gate exec; it gates PIP.
  • Signing is not revocation. A previously-trusted binary later found to be malicious cannot be invalidated short of removing it from the filesystem or replacing the kernel's public-key catalog. There is no hash-based revocation list in v0.20.
  • Signing does not verify provenance. The signature confirms that whoever held the corresponding private key produced this exact bytes-on-disk. It does not confirm where the binary came from, who built it, or whether it does what its name suggests.
  • Signing does not handle scripts. A script is not a signed binary; the script's PIP comes from the interpreter's PIP. See Verification and pinning.

What the user sees #

For most users, signing is invisible. A binary built into the Peios image — peinit, authd, the rest — is signed by peiso during build, and the user never sees the process. When they exec these binaries (or peinit execs them on the user's behalf), the kernel verifies the signature and sets PIP, and the binary runs as expected.

For binaries distributed through the package system, the package's .peipkg is itself signed at the package layer (different from the binary-signing layer this topic covers), and the binary inside it may or may not carry a Peios-format signature for PIP purposes. In v0.20, only TCB binaries do. Future versions may add Authenticode-style App and third-party tiers.

For binaries the user produces — a compiled program, a self-built tool — there is no signature, exec runs them at pip_type = None, and they get no PIP protection. They are still bound by all the other access control layers; they just are not high-trust.

Where to start #

If you want the on-disk format — what the signature blob actually looks like, where it lives in an ELF binary, how non-ELF files carry it — read Signature format.

If you want the kernel's behaviour at exec — how verification proceeds, the stable-snapshot rule, what content pinning does after a binary is verified, and how scripts and interpreters interact — read Verification and pinning.

If you want the key management story — where the TCB key lives, how peiso uses the private key at build time, the constraints on key handling — read Keys and image build.

Signature format

Peios / Peios Security Fundamentals / Binary Signing

The signature blob is the same shape regardless of where it sits — 3310 bytes, structured as a version byte followed by an ML-DSA-65 signature. What changes is where the blob lives and what bytes it covers. ELF binaries hold the blob in a dedicated section; non-ELF executables hold it in an extended attribute or alongside the file. The hash being signed is computed differently for each.

This page covers the blob's structure, where it can live, and how the content hash is computed for each placement.

The signature blob #

Every Peios signature is exactly 3310 bytes:

BytesFieldMeaning
0Version0x01. The only version supported in v0.20; any other value is rejected.
1–3309ML-DSA-65 signatureA 3309-byte raw ML-DSA-65 signature. The thing the kernel verifies.

The version byte is what gives the kernel room to evolve the format. A signature with version 0x02 (for example) would be treated as unrecognised in v0.20 — the kernel does not panic, it just declines to verify the signature, and the process runs at pip_type = None. Future versions may define new layouts; current versions will see them as if they were absent.

The signature is computed over a 32-byte content hash (covered below) using pure ML-DSA — FIPS 204's ML-DSA.Sign — and not HashML-DSA. The pre-hash variant HashML-DSA is for cases where the signer hashes the message first; here the signer treats the 32-byte hash as the message and signs it directly. There is no double-hashing.

The FIPS 204 context string is always empty. Signers must not set one, and a signature produced with a non-empty context will not verify. This matters if you are building your own signing tool: most ML-DSA libraries accept an optional context parameter, and leaving it at its default (empty) is what Peios expects.

The 3309-byte signature is the raw ML-DSA-65 signature encoding — no ASN.1 wrapper, no envelope, no metadata. The kernel parses it as raw bytes and feeds them straight into the verification routine.

ML-DSA is a post-quantum signature scheme, so a signature is considerably larger than the elliptic-curve signatures you may be used to. That size is why the blob is 3310 bytes rather than the 65 an Ed25519 design would need, and it is the reason the parameter set is ML-DSA-65 rather than the larger ML-DSA-87 — see Keys and image build.

Where the blob lives #

A signature blob can live in three places, depending on the file:

PlacementWhen used
ELF section .peios.sigELF binaries. The signature lives inside the file.
security.peios.sig xattrNon-ELF binaries. Also a fallback for ELF binaries without a .peios.sig section.
Detached .sig fileUsed during image build for non-ELF executables. The image builder reads the detached file and stamps the xattr.

The order matters. For ELF binaries the kernel looks for the .peios.sig section first; if found, the xattr is not consulted as a fallback. The ELF section is the canonical location.

For non-ELF binaries — scripts (which are themselves not signed; see Verification and pinning), data files used by tools, anything that does not begin with the ELF magic — only the xattr is consulted.

Detached .sig files are a transient form. They exist on the file system being assembled by peiso (the image builder) so that the builder can stamp the xattr from them. By the time the image boots, detached files are not used; the xattrs are what the kernel reads.

The ELF .peios.sig section #

For ELF binaries, the signature lives in a section of the binary's own ELF structure:

AttributeValue
Section name.peios.sig
Section typeSHT_PROGBITS (typical ELF section type, holds raw data)
Section sizeExactly 3310 bytes

The section is part of the ELF file. It travels with the binary through every operation a normal ELF travels through: filesystem copy, network transfer, package archive, image-build, anything. As long as the ELF is intact, the signature is part of it.

This is why the ELF section is preferred for ELF binaries. An xattr is filesystem metadata; it can be lost in a copy that does not preserve xattrs (a cp without --preserve=xattr, an archiver that does not understand the namespace, a network transfer that strips them). An ELF section is part of the file itself; nothing short of editing the ELF strips it.

The security.peios.sig xattr #

For non-ELF files, the same 3310-byte blob lives in an extended attribute:

AttributeValue
xattr namesecurity.peios.sig
xattr valueThe 3310-byte blob, identical to what an ELF section would hold

The security.* namespace requires privileged access to set — ordinary writes do not propagate to the xattr, which is the security boundary. The image builder sets the xattr at build time using its privileged access; the kernel reads it at exec.

Loss of the xattr (a copy without xattr preservation, a backup-restore through a tool that ignores security.*) results in the file being treated as unsigned. The signature blob can be re-applied if the corresponding .sig file is preserved, but the xattr layer is not the most robust place for it. ELF binaries get more durability by virtue of their structural signature; non-ELF binaries depend on xattr-preserving operations.

Detached .sig files #

During image build, peiso accepts detached signature files alongside the executables they cover. The convention is <binary>.sig — a 3310-byte file containing exactly the blob that should be stamped as the xattr.

The detached form has no role in a running system. It exists only as a packaging convention so that the build system can keep signatures next to their files without modifying the files themselves. peiso reads the .sig files, validates them, stamps the xattrs, and discards the detached forms.

The content hash #

The signature is over a 32-byte SHA-256 hash of the file's content. The bytes that go into the hash differ between ELF and non-ELF files because of how the signature is placed.

ELF content hash #

For ELF binaries, the hash is computed over the binary with the .peios.sig section's contents zeroed. The section header entry — the entry in the section header table that describes the .peios.sig section — is preserved. Only the section's 3310 bytes of payload are replaced with zeros for the hash computation.

This rule lets the section travel with the file without the signature signing itself. If the hash were computed over the binary including the signature, the signer would face a chicken-and-egg problem: sign first to know what to put in the section, but the section's contents change the hash. By zeroing the section contents during the hash, the section's position and size are part of the hash (via the section header) but its contents are not.

Verification follows the same rule: the kernel zeros the section contents (in its own working copy, not on disk), computes the hash, and verifies the signature against that hash. If anything else in the binary changed, the hash differs and verification fails.

Non-ELF content hash #

For non-ELF files, the hash is simpler: SHA-256 of the entire file, byte-for-byte, no exclusions.

The signature for a non-ELF file does not live in the file, so there is no need to exclude any region from the hash. The xattr holds the signature, the file's bytes are unmodified, and the hash covers all of them.

Why a section header found means no xattr fallback #

A subtle rule worth knowing: if an ELF binary has a .peios.sig section header entry (even an empty or malformed one), the kernel uses that path and does not fall back to the xattr.

This is to prevent confusion. A binary that appears to be ELF-signed but has a corrupt section gives the kernel a deterministic answer: unsigned. The xattr is not consulted as a second chance. The reasoning is to keep the verification rule simple — once the kernel has decided "this is an ELF binary with a signing section", that is the one and only place it looks. A binary that has both a (broken) section and a (working) xattr is also unsigned; the section's existence shadows the xattr.

The implication for someone signing binaries: choose one path per binary. ELF binaries should have a .peios.sig section or an xattr, not both. Mixed cases are valid but the section wins, and a broken section makes the binary unsigned regardless of what the xattr says.

For non-ELF binaries, the xattr is the only path — there is no section to find. These binaries are signed via xattr exclusively.

Limits and constants #

For completeness:

Limit / constantValue
Signature blob size3310 bytes (1 byte version + 3309 bytes ML-DSA-65 signature)
Version byte0x01 in v0.20; anything else is treated as unrecognised
Content hashSHA-256 (32 bytes)
Signature algorithmML-DSA-65 pure, empty context (not HashML-DSA)
ELF section name.peios.sig
ELF section typeSHT_PROGBITS
Xattr namesecurity.peios.sig
Detached file convention<binary>.sig (build-time only)

See also #

  • Binary signing — what the signature accomplishes.
  • Verification and pinning — how the kernel consumes this format at exec.
  • Keys and image build — who produces these blobs and with what key.

Verification and pinning

Peios / Peios Security Fundamentals / Binary Signing

When a process execs, the kernel runs the signing layer between resolving the binary and starting the new program. The flow is: find the signature, verify it, set the new PSB's PIP fields based on the result, then proceed with the exec. If the binary verifies, the resulting process is PIP-protected at the level the catalog says; if not, it runs as pip_type = None.

This page covers the verification flow, the stable-snapshot rule that handles concurrent writes during verification, what happens to the binary's inode after a successful verification, how scripts and interpreters work, and the symlink resolution rule.

The verification flow #

flowchart LR
    A["execve()"] --> B["Resolve binary"]
    B --> C["Find signature"]
    C -->|no signature| N["pip_type = None"]
    C -->|signature present| V["Verify against key catalog"]
    V -->|invalid| N
    V -->|valid| P["Look up key's PIP level"]
    P --> S["Set PSB pip_type/pip_trust"]
    N --> X["Continue exec"]
    S --> X
    X --> R["Process starts running"]

In order:

  1. The kernel resolves the path. Standard exec resolution — symlinks followed, mount-policy honoured — produces the target file. The resolution rules for symlinks are covered below.
  2. The kernel looks for a signature. For ELF binaries, it scans the section header table for .peios.sig. For non-ELF binaries, it reads the security.peios.sig xattr. If neither is present, the binary is unsigned.
  3. The kernel computes the content hash. SHA-256 over the binary, following the rules from Signature format — section contents zeroed for ELF, full file for non-ELF.
  4. The kernel verifies the signature. ML-DSA-65 verification of the 3309-byte signature against the 32-byte hash, using each of the public keys in the catalog in turn. If any key verifies the signature, the kernel records which key matched.
  5. The kernel looks up the key's PIP level in the catalog. The catalog entry says pip_type = X, pip_trust = Y. These values are written to the new process's PSB.
  6. Or, on any failure, the PSB's PIP fields are set to pip_type = None, pip_trust = 0. The exec proceeds.
  7. The exec continues. The new program starts running.

Note that step 6 also fires for any of: signature absent; signature present but version byte unrecognised; signature present but no key in the catalog verifies; signature present and verifies, but the catalog entry is somehow malformed. The kernel does not distinguish these cases in the result — they all produce pip_type = None.

The stable-snapshot rule #

A subtle case the kernel handles: what if the binary is being written to during verification?

The kernel reads the binary's content twice during exec — once to compute the hash, once (potentially) when mapping the binary into the new process's address space. If the file size or content changes between these reads, the hash computed in the first read might not match the bytes that end up executed in the second.

The kernel addresses this with a stable snapshot: the file's size is recorded at the start of verification, and the kernel only hashes bytes within that size. If the file size changes during verification, the kernel detects the change and treats the binary as unsigned (the hash was computed over a state that may no longer be valid). The resulting process runs at pip_type = None.

This is the "invalid or unstable = unsigned" case. The kernel does not retry; it does not block until the file stabilises; it simply gives up on verification for this exec. The same binary, exec'd a moment later when writes have stopped, will verify normally.

The rule applies only to the read side of the snapshot. Once the kernel has the hash and has verified the signature, the file is pinned (see below) and further writes are rejected — there is no window after verification where the binary's bytes can drift.

Scripts and interpreters #

A script — a text file starting with a shebang line — is not a signed binary. Scripts are interpreted by another program (the interpreter named in the shebang); they themselves carry no executable code that the kernel verifies. So how does signing interact with them?

The answer: the process's PIP comes from the interpreter, not the script. When you exec a script, the kernel actually execs the interpreter, passing the script as an argument. Verification runs on the interpreter binary. The resulting process's PIP is whatever the interpreter's signature says.

The consequence:

  • A signed-at-TCB interpreter (say, a Python signed at TCB level) running an untrusted script runs that script as a TCB process. The script's content is irrelevant; PIP comes from the interpreter.
  • An unsigned interpreter running a signed script runs as None — there is nothing to verify on the script.
  • Two scripts running under the same interpreter run at the same PIP level, regardless of what the scripts do.

In v0.20 no interpreter is signed at the TCB level. The TCB binaries (peinit, authd, etc.) are compiled binaries, not interpreters. This is intentional: signing an interpreter at TCB level would trivially give every script running under it TCB authority, which is exactly the wrong tool. If a future Peios version signs interpreters, they will be signed at lower trust tiers (App, perhaps) where the script content is also trusted.

For administrators: do not assume scripts are protected by PIP. They run at the interpreter's level, which in v0.20 is None for every common interpreter. If you need a script to run as a trusted process, compile it into a binary (or run it under a future Peios-blessed interpreter, when one exists).

Symlink resolution #

When exec is given a path that is a symlink, the symlink is followed before the signing check runs. The kernel resolves the symlink chain to the final target file, then runs verification on that file. The signature on the target is what counts; signatures on intermediate symlinks are ignored.

Specifically:

  • A symlink itself does not carry a signature. Symlinks are filesystem metadata, not executable content.
  • A signature xattr on a symlink is not consulted by the kernel.
  • The verification runs on the target's bytes, with the target's signature (xattr or ELF section).

This is the natural behaviour — exec follows symlinks anyway, and the signature is a property of the binary, not the path. But it has one implication worth noting: replacing a binary by changing a symlink's target is also changing what the kernel will verify. A symlink that currently points at a signed TCB binary, redirected to point at an unsigned attacker-controlled binary, will cause exec to run the latter and assign pip_type = None. The signing layer does not detect that the symlink target changed; it only verifies whatever the resolution lands on.

This is the right place for protection to come from elsewhere — typically the DACL on the symlink (denying writes by non-administrators) plus the DACL on the target directory. Signing is a content check; path integrity is the filesystem's job.

Content pinning #

Once a binary has been successfully verified and the resulting process has been assigned a non-zero pip_type, the kernel pins the binary's inode. From that moment on:

  • Writes are rejected. Any attempt to write() to the file (via any open fd) is denied.
  • Truncation is rejected. truncate(), ftruncate() on the file are denied.
  • fallocate is rejected. Anything that would mutate the file's data or size, including hole-punching and zero-fill operations.
  • xattr mutation of security.peios.sig is rejected. The signature itself cannot be modified once verified. Other xattrs may be touchable (depending on FACS policy), but the signature xattr is part of the pinning.

The pin is on the inode, not the path. A file with the same name created at the same location with a new inode is not pinned; it is a separate file with a separate signature.

This is the "update by inode replacement" pattern. To update a verified binary:

  1. Place the new (signed) binary at a different path.
  2. Rename it to overwrite the old path. The rename creates a new inode at the target name; the old inode (still pinned) is no longer reachable by name.
  3. Once no process is holding the old inode open, it gets reclaimed.

The new binary at the new inode is unverified until something execs it. The first exec triggers verification, and if successful, that new inode is pinned in turn.

This is also the deletion pattern for a verified binary: the file can be deleted via unlink() (the name goes away; the inode stays alive as long as anyone has it open), but the file cannot be modified in place. Atomic replacement is the only update path.

Why pin? #

Pinning exists because verification is a snapshot. The kernel verified the bytes at one moment; later modifications to those bytes would mean the running process's PIP no longer matches what is on disk. Pinning prevents that drift.

Consider the alternative: a verified TCB binary whose bytes are modifiable. An attacker who can write to that file could change its contents to inject malicious code. Future execs of the same path would still verify (the signature is over the old bytes; the section/xattr still says it is signed), but the actual bytes running would be the modified ones. The kernel would unwittingly grant TCB authority to malicious code.

Pinning closes the gap. After verification, the bytes are locked. The next exec re-reads the bytes and re-verifies — same bytes, same signature, same result. There is no window in which the on-disk file differs from what was verified.

The pin is in-kernel state, not a filesystem flag. A reboot loses all pins; the next exec of each binary verifies again from scratch. This is fine because the pins are operationally short-lived — they exist for the duration of a binary's use after first exec, which is typically the lifetime of the kernel.

A subtle case: pinning before exec completes #

A binary that is being verified has not yet finished exec'ing. The pin happens after verification completes; before that, the binary is just a file. If verification fails (returns "unsigned"), the file is not pinned — there is nothing to pin, since the resulting process has pip_type = None and no other process has gained a TCB-relevant interest in this inode.

But if verification succeeds and exec then fails for unrelated reasons (out of memory, mmap failure, etc.), the PIP values are not committed to the PSB. The pin, however, has been applied. A subsequent exec of the same file will re-verify and re-pin (or rather, find the pin already in place), but the original failed exec does not roll back the pin.

This is consistent with the pin being about the inode's bytes, not about any specific process. Once an inode has been verified, the kernel knows what those bytes are and will not let them change, independent of whether any specific exec succeeded in using them.

Where to go next #

For where the public keys come from and how signed binaries are produced, read Keys and image build.

For the exact blob layout and content-hash rules verification relies on, read Signature format.

For what the assigned PIP fields go on to enforce, read Process integrity protection.

Keys and image build

Peios / Peios Security Fundamentals / Binary Signing

A signed binary's trust level is whatever the key that signed it says it should be. The mapping from "this key" to "this PIP level" is the kernel's public-key catalog. The catalog is compiled into the kernel image; there is no way to add keys to a running system, and there is no way to remove them either short of replacing the kernel.

This page covers the catalog's structure, who holds the private keys, how peiso (the image builder) uses them, the constraints on key handling, and the v0.20 limitation of having only one key.

The catalog #

The kernel holds an in-memory table mapping each known public key to the PIP level its signature confers. Each entry is structured:

FieldSizeMeaning
Public key1952 bytesThe raw ML-DSA-65 public key.
pip_type4 bytes (little-endian u32)The PIP type this key represents.
pip_trust4 bytes (little-endian u32)The PIP trust level this key represents.

Each entry is 1960 bytes. The table is a contiguous array of entries terminated by an all-zero entry — the kernel walks until it sees a sentinel.

At exec, when the kernel verifies a signature, it tries each key in the table in turn until one verifies the signature or the table runs out. If a key verifies, its pip_type and pip_trust are written to the new process's PSB. If no key verifies, the binary is treated as unsigned.

The table is part of the kernel image. It is read-only at runtime; there is no syscall to add an entry, modify an entry, or remove one. Replacing the table requires replacing the kernel.

Why ML-DSA-65 #

FIPS 204 defines three parameter sets — ML-DSA-44, ML-DSA-65 and ML-DSA-87 — trading signature size against security margin. Peios uses ML-DSA-65, and the deciding constraint is where signatures live rather than cryptographic preference.

Non-ELF files carry their signature in the security.peios.sig extended attribute. Most ext4-family filesystems cap an xattr value at one filesystem block, typically 4096 bytes, unless the large-xattr (ea_inode) feature is enabled. ML-DSA-65's 3310-byte blob fits inside that with room to spare; ML-DSA-87's would be 4628 bytes and would not fit. ML-DSA-44 would fit too, but saves under a kilobyte per signature in exchange for a smaller security margin — a poor trade for an OS expected to have a long life.

So ML-DSA-65 is the largest parameter set that keeps signatures storable as ordinary extended attributes on ordinary filesystems.

What v0.20 actually contains #

In v0.20, the catalog has one entry: the TCB key. The mapping is:

Keypip_typepip_trustWhat it represents
TCB public key512 (Protected)8192Peios TCB binaries — peinit, authd, loregd, lpsd, eventd

That is it. No App-level key, no Authenticode key, no AntiMalware key. The categorical PIP labels described in Process integrity protection — Protected/1024 for Authenticode, Protected/1536 for AntiMalware, Protected/2048 for App, Protected/4096 for Peios — are defined in the model but no key in v0.20 corresponds to them. No binary on a v0.20 system will have these PIP values because nothing exists to sign them at those levels.

The implication: every signed binary on a v0.20 Peios system runs at PIP Protected/8192. There are no other PIP-protected processes. PIP is binary: a process is either TCB-level or unprotected.

Future Peios versions will add keys for the other tiers. App-level signing will let Peios-distributed applications get their own PIP protection (mid-trust); Authenticode-level will let third-party signed binaries run at low-but-non-zero trust; AntiMalware-level will protect security tooling. The catalog grows; the model accommodates it without code changes.

The TCB private key #

The TCB private key is what peiso uses to sign TCB binaries at image build. It is the most security-sensitive artefact in the entire system: anyone with the TCB private key can sign a binary that the kernel will then trust at the highest level. There is no revocation, no per-binary blocklist, no way to invalidate a signed binary short of removing it.

Caution

The TCB private key must never be present on any running Peios system. It is for image build, and only image build. A running system has no need for the private key — the kernel verifies with the public key only.

The other constraints on the private key are just as absolute:

  • The key is held by whoever builds the system image (in practice, the Peios project's release infrastructure for the official distribution; potentially a downstream packager for a fork).
  • The key is not distributed. Users do not get a copy of the private key. They get the binaries the private key was used to sign.

The standard pattern: build infrastructure with the private key produces image artefacts. The artefacts include the public key compiled into the kernel image and the signed TCB binaries. The artefacts ship; the private key stays in the build infrastructure.

A system whose private key has been compromised loses the security guarantees of PIP. There is no way to retroactively recover from such a compromise; the only fix is to release a new image with a new public key and re-sign all TCB binaries with the new private key, then deploy. Existing systems running the old image continue to trust the old key.

peiso and the build flow #

peiso is the Peios image builder. Its job is to assemble a complete bootable Peios image — kernel, root filesystem, signed binaries, build artefacts. Signing is one of its tasks.

At build time, peiso:

  1. Compiles or assembles the kernel image. The public-key catalog is built into this image; peiso embeds the kernel-build's public keys (or asserts that they are already embedded).
  2. Collects the binaries that should be signed at TCB level. The full list is defined per release — typically peinit, authd, loregd, lpsd, eventd.
  3. For each binary, peiso:
    • Computes the appropriate content hash (ELF section zeroed for ELF, full file for non-ELF — see Signature format).
    • Signs the hash with the TCB private key, producing a 3309-byte ML-DSA-65 signature.
    • Constructs the 3310-byte blob (version byte + signature).
    • For ELF binaries: reserves the .peios.sig section (3310 zero bytes) before hashing, then writes the blob into the reserved section after signing. Because the section is zeroed during hashing per the ELF rule, the hash the kernel computes at verification time matches the one that was signed.
    • For non-ELF binaries: writes the blob as a detached .sig file next to the binary. At image-assembly time, peiso reads the detached file and stamps the security.peios.sig xattr on the binary in the image's filesystem.
  4. The signed binaries are placed in the image. The detached .sig files are discarded.
  5. The image is finalised and emitted as an installable artefact.

At runtime — when the image boots — the kernel reads its own embedded public key, the TCB binaries' embedded sections (or xattrs) provide their signatures, and verification proceeds. No private key is involved at runtime.

Why one key in v0.20 #

A reasonable question: why does v0.20 ship with only the TCB key? Why not at least an App key for Peios-distributed applications?

The answer is that the App, Authenticode, and AntiMalware tiers require infrastructure that v0.20 does not have:

  • App-level signing requires a Peios-managed signing service that signs released applications. The service exists conceptually; the operational pipeline does not.
  • Authenticode-level signing requires a trust agreement with one or more third-party CAs or a Peios-managed equivalent. None of that is in place.
  • AntiMalware requires a vetting process for security-tooling vendors. Same status.

The TCB key, by contrast, is just the project's own key, used to sign the project's own binaries. The infrastructure is the project's own release pipeline. It works on day one.

Future Peios versions will add the other tiers as the corresponding infrastructure comes online. The model accommodates them. The catalog grows. Until then, the only PIP-protected processes are TCB processes, and the model effectively has two levels: TCB and not-TCB.

Implications for ordinary deployments #

For someone running Peios:

  • No signing tools are available on a running system. You cannot sign your own binaries to get PIP protection. The TCB private key is not available; no other key is defined.
  • Binaries you write or compile yourself run as pip_type = None. Including binaries built from source on the running Peios system. This is correct behaviour — there is no path for an untrusted-by-the-OS binary to acquire trust.
  • The only PIP-protected processes are TCB. peinit, authd, loregd, lpsd, eventd, and that is it. Everything else runs at None.
  • Updates to TCB binaries come through the package system. A new release of authd, say, is in a .peipkg signed by the project's package-signing infrastructure (separate from the TCB binary signing — packages have their own signature for distribution integrity). The binary inside the package carries the TCB-key signature for PIP. Installing the package places the binary on disk; the kernel verifies it at next exec.

For administrators of a custom Peios fork (using their own kernel image with their own key catalog), the same model applies with whatever keys they have defined: their TCB-equivalent key signs their TCB binaries, those binaries run at TCB-equivalent PIP, and nothing else has PIP protection unless they add keys to their catalog.

Where to go next #

For the enforcement flow these keys feed — verification at exec and inode pinning — read Verification and pinning.

For the per-process hardening flags that build on signing — LSV gates executable mappings by signature trust — read Process mitigations.

Peios Learn — documentation for the Peios project.

Built with Trail.