# Probity Launch-Chain Attestation, v1

**predicateType:** `https://getprobity.dev/predicate/v1/launch-chain`

A launch-chain attestation is a signed, offline-verifiable statement about the **rest of the boot
pre-image**: everything a microVM is launched with beyond the kernel image itself. It binds the
initrd digest, the exact kernel command line, and the ordered set of dm-verity root hashes the rootfs
is mounted under. The [kernel-substrate](kernel-substrate.md) attestation says which kernel bytes are
approved; this one says what the kernel is approved to be *started with*, which is where a substituted
command line or a swapped rootfs image would otherwise pass unnoticed.

The two are deliberately separate statements under separate predicate types and separate signature
domains, so a signature minted for one can never be replayed as the other under the same key. They
are also deliberately at different enforcement postures: the kernel-substrate gate is fail-closed by
default, and this one is an explicit, named opt-in. The reasons are recorded under Enforcement
posture and are properties of the current deployment, not of the format.

This predicate does **not** ride the DSSE / in-toto envelope specified in
[signing-envelope.md](signing-envelope.md). Like its companion it is signed as a standalone canonical
payload under a domain-separated Pre-Authentication Encoding, described in full below.

The reference implementation is the engine's kernel-attestation component: the canonical launch-chain
payload builder, its verifier, the enforcement entry point and the mandatory-enforcement predicate,
together with the frozen-SUT pin manifest and its loader.

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).

## Subject

**The v1 wire form carries no subject member, and this is the predicate's principal known weakness.**

The measurements in the payload are the launch chain; the kernel they belong to is not named anywhere
inside the signed bytes. The join to a kernel is made at the distribution layer instead: the bundle is
published to, and read from, a location derived from the kernel's manifest-index digest (below). That
means the binding between a launch chain and its kernel rests on a naming convention a registry
operator controls, rather than on a cryptographic fact. A `version` string is carried in the payload
and ties the statement to a release by label, but a label is not a binding.

A consumer MUST NOT treat a verified launch-chain attestation as evidence that these measurements
belong to any particular kernel image. It is evidence that the publisher signed these measurements.
The v2 successor closes this by making the kernel index digest the statement subject; see Relationship
to the v2 wire form.

## Signed payload

The signed pre-image is a single JSON object with exactly five members:

```json
{"predicateType":"https://getprobity.dev/predicate/v1/launch-chain","initrdDigest":"sha256:<64-hex>","kernelCmdline":"<exact boot command line>","rootfsVerityRootHashes":["<64-hex>"],"version":"<kernel-version>"}
```

The byte layout is normative:

- The members appear in the order `predicateType`, `initrdDigest`, `kernelCmdline`,
  `rootfsVerityRootHashes`, `version`. This is the field declaration order of the producer's struct,
  and **declaration order is the format** for this predicate. It is emphatically *not* sorted-member
  order: sorting these five names would put `predicateType` third. A producer that canonicalizes by
  sorting member names emits different bytes and every verification fails.
- There is no insignificant whitespace, no indentation, and no trailing newline.
- `rootfsVerityRootHashes` is a JSON array of strings and its element order is significant (see below).
  There are no JSON objects nested inside the payload and no maps, so no other ordering question
  remains open.

**String escaping.** `kernelCmdline` MUST NOT contain the characters `<`, `>`, or `&`. This is not an
aesthetic restriction: some JSON encoders escape those three characters to the six-character
sequences `\u003c`, `\u003e`, and `\u0026` while others emit them literally, so two
conforming-looking producers would disagree on the signed bytes for the same command line, and
byte-equality verification would fail with no diagnostic pointing at the cause. Excluding them from
the input removes the divergence. All other characters are escaped identically by any correct JSON
encoder.

## Field semantics

- **`predicateType`** (REQUIRED, string) - MUST be exactly
  `https://getprobity.dev/predicate/v1/launch-chain`. It is inside the signed bytes, so a byte-equality
  check against a reconstructed pre-image transitively binds it.

- **`initrdDigest`** (REQUIRED, string) - the OCI digest of the initrd, in `sha256:<64 lowercase hex>`
  form. **This member has three distinguished values and conflating any two of them is the single most
  likely implementation error in this predicate:**
  - a real digest - the boot loads that initrd;
  - the **empty-input digest**
    `sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`, which is SHA-256 over
    zero bytes and is the canonical way to say **the boot loads no initrd at all**. A Firecracker
    direct kernel boot has no initramfs, so there is no file to hash; this value is a real, promoted
    measurement, not an absence marker;
  - the **all-zero placeholder** `sha256:0000...0000` (64 zeros), which means **not yet promoted** -
    the publisher has not committed a real measurement. This is the opposite of the case above: one is
    a fact, the other is the absence of one.

  The empty string is not a legal value; it does not satisfy the digest form and cannot be pinned.

- **`kernelCmdline`** (REQUIRED, string, non-empty) - the exact kernel command line the boot uses,
  verbatim and complete. Comparison against it is an **exact string match**, never a prefix, substring,
  or containment check - the point of binding it is that an added or altered parameter (one that
  disables a guard, for instance) must not pass. In the reference deployment the per-sandbox boot data
  deliberately does not travel on the command line, which is what makes an exact match a workable
  requirement rather than a permanently failing one.

- **`rootfsVerityRootHashes`** (REQUIRED, array of strings, non-empty) - the dm-verity root hashes of
  the rootfs, one per mounted verity target, each as **bare 64 lowercase hex with no `sha256:`
  prefix** (the `veritysetup` root-hash form, deliberately not the OCI form the initrd digest uses).
  - **Order is significant and is compared as an ordered sequence, never as set membership.** The
    position of a hash is which target it is, so two statements listing the same hashes in different
    orders bind different things and MUST NOT compare equal.
  - An element MAY be the all-zero root hash
    `0000000000000000000000000000000000000000000000000000000000000000`, meaning that measurement is not
    yet promoted.
  - An empty array is not conforming: it would silently bind no rootfs integrity at all.
  - A hash pinned here MUST be produced with a **fixed salt** (`veritysetup format --salt=<hex>`).
    `veritysetup` randomizes the salt per invocation by default, and a random salt makes the root hash
    non-reproducible, so a defaulted salt yields a value nothing can ever re-derive.
  - **Which targets appear here is a deployment decision, not a rule of this format.** In the reference
    deployment the array carries the fixed substrate root only - the element-0 init-root, identical for
    every sandbox - while per-sandbox workload overlay-lower hashes are carried on a separate signed
    channel and verified inside the guest. The array shape is retained for a future multi-target
    substrate. A consumer therefore MUST NOT read this array as a complete inventory of every verity
    target a running guest mounts; it is the set the publisher pinned.

- **`version`** (REQUIRED, string, non-empty) - the kernel version the launch chain corresponds to,
  tying this statement to the same release as the [kernel-substrate](kernel-substrate.md) attestation.
  As there, **it is a label under signature, not a derived fact**; nothing verifies that the named
  version is the kernel these measurements were gathered against.

There are no optional members in v1, and a producer MUST NOT add one: any additional member changes
the signed bytes and byte-equality verification fails outright rather than degrading.

## Signature construction

The signature is a raw ed25519 signature over the DSSE **Pre-Authentication Encoding (PAE)** of the
payload, never over the bare payload bytes.

```
PAE = "DSSEv1 " || LEN(payloadType) || " " || payloadType || " " || LEN(body) || " " || body
```

where `LEN` is the ASCII decimal byte length, `body` is the canonical payload above, and

```
payloadType = "application/vnd.probity.launch-chain.v1+json"   (44 bytes)
```

This type is distinct from - and is not a prefix of - the kernel-substrate payload type, so a
signature minted for one statement class can never be replayed as the other under the same key. A
verifier MUST reconstruct the PAE itself and MUST NOT verify over the payload alone.

As with the companion predicate, the payload type is not `application/vnd.in-toto+json`, there is no
DSSE envelope object, and the signature is detached.

## Bundle format and distribution

The bundle format is identical to the [kernel-substrate](kernel-substrate.md) one - the same two
base64 members, verified by the same recipe - so an implementation shares one code path and differs
only in the canonical payload, the payload type, and the enforcement disposition.

```json
{
  "payload": "<base64(canonical payload bytes)>",
  "signature": "<base64(raw 64-byte ed25519 signature over the PAE)>"
}
```

The bundle carries no `keyid`, no certificate, and no transparency-log entry.

A publisher SHOULD make the bundle resolvable from the pinned kernel digest alone. The reference
implementation publishes it as a one-layer OCI artifact at the deterministic ref

```
<registry>/kernel:launch-chain-<kernel-digest-hex>
```

using a tag prefix distinct from the kernel-substrate bundle's so the two classes never collide, in
the same registry and under the same authentication as the kernel. This ref is the *only* thing
connecting the launch chain to its kernel; see Subject.

A consumer MUST cap the size of a bundle it reads. The reference implementation caps registry and
on-disk reads at 1 MiB. A locally cached bundle MUST be keyed by the pinned kernel digest, so that a
republished kernel is a cache miss by construction rather than a silent reuse of the previous
measurements.

## Trust root

Identical to [kernel-substrate](kernel-substrate.md): a configured ed25519 public key, embedded in
PKIX/PEM form in the verifying binary in the reference implementation, cosign-compatible, with the
private half held out of band. A third party verifies with

```
cosign verify-blob --key probity.pub --signature <sig-file> <pae-file>
```

over the PAE bytes reconstructed per the construction above.

## Verification (third-party, offline)

A verifier begins from the measurements it intends to bind - not from the bundle.

1. Reconstruct the canonical payload for the intended `initrdDigest`, `kernelCmdline`,
   `rootfsVerityRootHashes` (in order), and `version`, per Signed payload.
2. Decode the bundle's `payload` from base64 and require it to be **byte-equal** to the reconstruction.
   This one check binds all four measurements and the `predicateType` together.
3. Reconstruct the PAE over the decoded payload with the payload type above.
4. Decode the bundle's `signature` from base64 and verify it as a raw ed25519 signature over the PAE
   under the configured public key.

The attestation is verified only when steps 2 and 4 both succeed. No network access and no live
execution environment is required once the bundle and the key are in hand.

### Verifying against a live boot

An enforcing runtime performs the same procedure with one addition that carries most of the value:
**it reconstructs the canonical payload from the measurements the boot is about to actually use, not
from the values it pinned.** The bundle binds the pin, so a byte-equality against the live
reconstruction succeeds only when live equals pin equals attested. A caller that reconstructs from its
own pin has proved only that it agrees with itself.

An implementation SHOULD also make the not-verified state unforgeable by construction. The reference
implementation returns an enumerated outcome whose zero value is a non-verified state, so a dropped
error or an unassigned result can never read as verified; only a present bundle that verified over the
live measurements yields the verified value.

## Enforcement posture

**This gate is off by default, and that is a deliberate posture difference from the fail-closed
[kernel-substrate](kernel-substrate.md) gate.** It is an explicit named opt-in: setting the
environment variable `PROBITY_REQUIRE_LAUNCH_CHAIN` to `1`, `true`, or `yes` (compared
case-insensitively after trimming surrounding whitespace) turns it on. There is no silent warn-only
mode and no third setting.

Two preconditions are unmet in the reference deployment, and defaulting the gate on would fail every
legitimate boot rather than catch an illegitimate one:

1. **No launch-chain bundle is published.** The CI workflow signs the kernel-substrate attestation
   only; no step mints or pushes a launch-chain bundle. With the gate on, every boot would fail closed
   on an absent bundle.
2. **Verity boot is opt-in.** A default, non-verity boot gathers a different command line and an empty
   verity set, which a field check against the promoted pin would correctly reject - bricking every
   non-verity sandbox.

Making it default-on requires, in order: a producer that signs and publishes the bundle; verity boot
as the default so the live boot matches the promoted pin; and only then the flip, with a narrow named
development escape of the kind the kernel-substrate gate has. A third condition was met more recently
than the first two and is worth recording because its absence was invisible: a staging step that
actually fetches the published bundle to the location the verifier reads. Without it, an operator who
published a bundle and set the variable would have received a hard absent-bundle failure on every
boot, with nothing in the outcome to distinguish "never published" from "never retrieved".

Disposition, by state:

| State | Gate off (default) | Gate on |
|---|---|---|
| Pinned measurements still the all-zero placeholder | not verified, proceed | refuse: enforcing against an unpromoted pin would bind zero integrity |
| Live command line, verity set, or initrd digest differs from the pin | logged warning, not verified, proceed | hard failure, per-field |
| Bundle absent | not verified, proceed | hard failure |
| Bundle present, binds different measurements | logged warning, not verified, proceed | hard failure |
| Bundle present, malformed or signature invalid | **hard failure** | hard failure |
| Bundle present, verifies over the live measurements | verified | verified |

The last two rows are the load-bearing ones. A forged or malformed bundle is a hard failure **whether
or not the gate is on** - permissiveness extends to absence and to a not-yet-byte-stable boot, never
to a bad signature. And the off-the-gate rows are all non-verified: no configuration produces a
verified result without a present bundle that checked out over the live boot.

Refusing to enforce against an all-zero placeholder pin deserves its own note, since refusing looks
like the weaker choice. It is not. Enforcing against an unpromoted pin either bricks a real boot or
"passes" a gate that binds nothing, and a gate that passes while binding nothing is worse than an
absent gate because it reads as evidence.

## What this predicate does NOT assert (coverage)

- **That these measurements belong to any particular kernel.** No kernel digest is in the signed bytes;
  see Subject. This is the largest gap in the v1 form.
- **That the named version is accurate.** `version` is an unverified label.
- **Every verity target a guest mounts.** The array is what the publisher pinned - in the reference
  deployment the fixed substrate root only. Per-sandbox workload measurements travel elsewhere.
- **That dm-verity was actually enabled for a given boot.** The statement binds root hashes; whether a
  particular launch used them is the enforcing runtime's live comparison, not a property of the bundle.
- **Anything about the kernel image itself.** That is [kernel-substrate](kernel-substrate.md).
- **Any host-side confinement.** A host seccomp filter is explicitly not bound by this v1 payload. It
  is a candidate for a later revision once the confinement is byte-stable, and until then a consumer
  MUST NOT infer it.
- **Build provenance, transparency-log inclusion, revocation, or key legitimacy** - as for
  [kernel-substrate](kernel-substrate.md), and for the same reasons.

## What a verifier can and cannot independently check (honesty)

- A verifier CAN confirm the bundle is authentic under a configured key and that it binds exactly the
  initrd digest, command line, ordered verity root set, and version the verifier intended - all
  offline, from the bundle and the public key alone. An enforcing runtime CAN additionally confirm that
  the boot it is about to perform uses those same measurements.
- A verifier CANNOT confirm, from the attestation alone, which kernel these measurements belong to,
  that the command line is safe, that the verity root hashes correspond to any particular filesystem
  image, or that the publisher's promotion decision was sound. The guarantee is integrity, provenance
  under a named key, and - for an enforcing runtime - equality with the live boot.

## Status and maturity

- **Format, signing construction, and verification: implemented.** The canonical payload builder, the
  PAE domain separation, the byte-equality binding check, the live-measurement enforcement entry point,
  and the bundle staging path all exist and are exercised.
- **Producer: does not exist yet.** No CI step mints or publishes a launch-chain bundle. In practice
  this means a runtime today reaches the not-verified-and-that-is-not-a-failure state, and no
  conforming bundle is published for a third party to fetch. This is stated plainly rather than left
  to be inferred from an empty registry tag.
- **Enforcement: opt-in**, per Enforcement posture, with the cutover conditions listed there.
- **Kernel binding: absent in v1**, closed in v2.

## Relationship to the v2 wire form

A successor exists in the reference implementation at
`https://getprobity.dev/predicate/v2/launch-chain`. Its four measurement members are unchanged in name
and meaning; what changes is that the statement becomes an in-toto Statement v1 whose **subject is the
kernel index digest**, canonicalized under RFC 8785 and wrapped in the DSSE envelope this standard
specifies elsewhere. That makes the launch chain and the kernel it belongs to one cryptographic fact
instead of a registry naming convention, which is the open weakness this document records against
itself under Subject.

The v2 form also retires the `<`, `>`, `&` prohibition above, because RFC 8785 fixes the escaping and
the producer's canonicalizer normalizes it, leaving the divergence no place to occur. Under the v1
byte layout the prohibition stands.

This document remains normative for the v1 URI, and a verifier presented with a v1 bundle MUST apply
the v1 rules rather than in-toto Statement parsing. A consumer dispatches on the full `predicateType`
string, so the two never collide.

## Related standards

- **[DSSE](https://github.com/secure-systems-lab/dsse)** - PARTIALLY ALIGNED. PAE and its domain
  separation are used verbatim; the envelope object is not.
- **[in-toto attestation](https://github.com/in-toto/attestation)** - NOT USED in v1; the v2 successor
  is the in-toto-shaped form.
- **[Sigstore / cosign](https://docs.sigstore.dev/)** - PARTIALLY ALIGNED. Static-key `sign-blob` /
  `verify-blob` over the PAE bytes, without Fulcio certificates and without Rekor upload.
- **[dm-verity](https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html)** - the
  root-hash form `rootfsVerityRootHashes` carries is `veritysetup`'s, bare hex, fixed salt.
- **[kernel-substrate](kernel-substrate.md)** - the anchor this predicate extends.
- **[signing-envelope.md](signing-envelope.md)** - the DSSE / in-toto envelope the rest of this standard
  uses, and which this v1 predicate deliberately does not.

## Versioning

This is the launch-chain v1 predicate and its member set is closed. Verification is byte-equality over
a canonical pre-image, so there is no additive-optional-field path: adding, renaming, removing, or
reordering a member - including changing the element order or the element form of
`rootfsVerityRootHashes` - changes the signed bytes and is breaking. Any such change is a new major
version URI. The additive-field forward-compatibility rule in the standard's [README](README.md)
applies to predicates carried inside the in-toto envelope and does not apply to this v1 wire form.
