# Probity Launch-Chain Attestation, v2

**predicateType:** `https://getprobity.dev/predicate/v2/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, **to the kernel those measurements belong to**. 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 kernel binding is what v2 is for.** The [v1 form](../v1/launch-chain.md) carries no subject at
all: its measurements sit in a bespoke payload and the join to a kernel is made at the distribution
layer, by publishing the bundle at a registry tag derived from the kernel digest. That binding rests
on a naming convention a registry operator controls rather than on a cryptographic fact, and the v1
document records it against itself as the predicate's principal known weakness. In v2 the **kernel
index digest is the in-toto Statement subject**, so a launch chain and the kernel it belongs to are
one cryptographic fact. Nobody controls what is inside a signature.

The four measurement members are unchanged in name and meaning from v1. Everything around them
changes: the statement becomes an
[in-toto Statement v1](https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md)
canonicalized under [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) and carried in the DSSE
envelope specified in [signing-envelope.md](../v1/signing-envelope.md), and verification stops being
a byte-equality reconstruction and becomes verify-then-read. The v1 document normatively fixes a wire
form that stops being true under those changes, and changing a published wire form in place is
exactly the silent in-place change this standard forbids, so v2 is a new URI even for the members
that did not move.

The reference implementation is the engine's attestation package (statement construction, envelope
construction, verification and decoding), its producer-side minting sibling, and the `probity-attest`
command, which is the only producer of these bytes.

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 subject is the **kernel image the launch chain belongs to, by OCI manifest-index digest** - not
any of the predicate's own measurements. It is byte-for-byte the same subject shape the
[kernel-substrate](kernel-substrate.md) statement carries, and for the same kernel it is the same
value, which is what lets a consumer join the two statements without trusting an address.

```json
"subject": [{"digest": {"sha256": "<64 lowercase hex>"}, "name": "guest-kernel"}]
```

- The digest value is **bare lowercase hex with no `sha256:` prefix**, the in-toto convention. Note
  the deliberate asymmetry with the predicate below, which keeps the OCI `sha256:`-prefixed form for
  `initrdDigest` and the bare `veritysetup` form for the verity hashes. Conflating the spellings is
  the most likely implementation error here; normalize in one place.
- Uppercase hex is rejected rather than folded. Two spellings of one digest would make two distinct
  statements compare equal.
- `name` is REQUIRED and non-empty, conventionally `guest-kernel`. It is inside the signed bytes and
  so must be deterministic, but **a verifier binds on the digest map and MUST NOT bind on the name**.
- The array MUST be non-empty.

A consumer that has verified this statement MAY now assert that these measurements were signed *for
this kernel*. Under v1 it could not, and the v1 document says so explicitly.

## Statement and canonicalization

The signed payload is the RFC 8785 canonical serialization of the Statement. A real example, from the
committed conformance vectors, with a verity command line and a two-element ordered verity array:

```json
{"_type":"https://in-toto.io/Statement/v1","predicate":{"initrdDigest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","kernelCmdline":"console=ttyS0 reboot=k panic=1 dm-mod.create=\"root,,,ro,0 100 verity 1 /dev/vda /dev/vda 4096 4096 100 1 sha256 a599...93a4 0000...0000\" root=/dev/dm-0 init=/init","rootfsVerityRootHashes":["a599...93a4","bbbb...bbbb"],"version":"6.1.137"},"predicateType":"https://getprobity.dev/predicate/v2/launch-chain","subject":[{"digest":{"sha256":"04d1...9c8d"},"name":"guest-kernel"}]}
```

- **Member order is RFC 8785 order, which is sorted by member name**, at the Statement level
  (`_type`, `predicate`, `predicateType`, `subject`) and inside the predicate (`initrdDigest`,
  `kernelCmdline`, `rootfsVerityRootHashes`, `version`). The v1 form's member order was the
  producer's struct declaration order and *was* the format; here nothing depends on a struct's field
  order, and `predicateType` moves from first to third.
- There is no insignificant whitespace, no indentation, and no trailing newline.
- **`rootfsVerityRootHashes` is a JSON array and its element order is significant.** RFC 8785 sorts
  object members; it does not reorder array elements, and it must not. See Field semantics.
- A producer MUST emit RFC 8785 canonical bytes. A verifier MUST NOT re-canonicalize the payload as
  part of verification and MUST NOT reconstruct the payload in order to compare it; see Verification.

**The v1 prohibition on `<`, `>` and `&` in `kernelCmdline` is retired, and this predicate is why it
mattered.** Go's `encoding/json` rewrites those three characters as the six-character sequences
`\u003c`, `\u003e` and `\u0026` while other encoders emit them literally, so two conforming-looking
producers disagreed on the signed bytes for the same command line and byte-equality verification
failed with no diagnostic pointing at the cause. The v1 documents handle that by forbidding the
characters - a producer rule standing in for a fix. It was a live hazard rather than a theoretical
one precisely here: kernel-substrate's value slots are a URI, a digest and a version, none of which
can contain those characters, while `kernelCmdline` is an unconstrained string. RFC 8785 decodes the
escapes and re-emits the raw characters, both encoders land on the same bytes, and the prohibition is
removed rather than restated. The conformance vectors carry a command line containing all three
characters for exactly this reason: a rail that reproduces that vector has reproduced the fix.

### Parse rules a verifier MUST apply to the verified bytes

These are checks on the *already-verified* payload, and each exists so two verifiers cannot disagree
about what a signed statement says:

- **Duplicate object members are rejected**, anywhere in the document. Go's `encoding/json` takes the
  last occurrence of a repeated member and a strict parser elsewhere takes the first.
- **Trailing data after the top-level value is rejected**, and input holding no complete top-level
  value is rejected.
- **Nesting is capped at 128 levels**, the same bound the producer's canonicalizer applies, so a
  payload a verifier accepts is one the producer could have produced. A conforming statement nests
  three levels.
- **Unknown members are ignored, never rejected.** See Versioning.

## Field semantics

The predicate body carries the four measurements:

```json
{"initrdDigest":"sha256:<64-hex>","kernelCmdline":"<exact boot command line>","rootfsVerityRootHashes":["<64-hex>"],"version":"<kernel-version>"}
```

- **`initrdDigest`** (REQUIRED, string) - the digest of the initrd, in **OCI `sha256:<64 lowercase
  hex>` form**, prefix included. The prefix is kept here, unlike in the subject, because this is a
  predicate value with three distinguished cases 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.

  A producer MUST reject a value that does not carry the `sha256:` prefix and MUST reject one that is
  not 64 lowercase hex characters after it. The empty string is not a legal value.

- **`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.

  A producer SHOULD take this value from a **file rather than a command-line argument**. A kernel
  command line carries double quotes (`dm-mod.create="..."`) and passing it through a shell argument
  is the class of quoting accident that produced the encoder divergence above; reading it from a file
  means the bytes a human wrote are the bytes that get signed. The reference producer strips at most
  one trailing newline (because the ordinary way to write such a file ends in one and a kernel command
  line never does) and **rejects** any other newline, a NUL byte, and invalid UTF-8. Stripping more
  would let trimming hide content; accepting an embedded newline would sign a value no bootloader can
  pass; and Go's JSON encoder silently substitutes U+FFFD for invalid UTF-8, so accepting it would
  sign a value that is not the one on disk.

- **`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 `initrdDigest` 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 producer
    MUST refuse one.
  - 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 travel on a separate
    signed channel and are verified inside the guest. 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 after trimming) - 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. In v2 the version is no longer load-bearing for the join to a kernel, because the
  subject does that cryptographically.

A producer validates **form only** and deliberately does not judge meaning. An all-zero placeholder is
a well-formed value that a pin legitimately carries before promotion; deciding whether an unpromoted
pin may be enforced against is the enforcing consumer's decision, not the producer's.

A producer MAY add optional members and a verifier MUST ignore members it does not recognize. The v1
form could not offer that: any additional member changed the signed bytes and broke byte-equality
verification outright.

## Signature construction

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

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

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

```
payloadType = "application/vnd.in-toto+json"   (28 bytes)
```

so a conforming pre-image begins `DSSEv1 28 application/vnd.in-toto+json `.

**This is the same payload type the [kernel-substrate](kernel-substrate.md) statement is signed
under, which is a deliberate change from v1 and moves a security property.** The v1 forms used
distinct private media types (`application/vnd.probity.launch-chain.v1+json`, 44 bytes, here;
`application/vnd.probity.kernel-attestation.v1+json`, 50 bytes, there), so a signature minted for one
class could never be replayed as the other because the PAE differed. In v2 the separation between the
two substrate classes lives in `predicateType`, and **a verifier MUST check it as a field**. Under
byte-equality it never had to be examined, because the type string sat inside bytes that were compared
whole. A verifier that does not demand a predicate type accepts a kernel approval where it wanted a
launch chain.

Separation from every *other* Probity artifact class is unaffected and still lives in the payload
type: a catch record is signed under its own payload type, a Statement under this one, and PAE's
length prefixes make the pair injective.

A verifier MUST reconstruct the PAE with the payload-type **constant above**, never with the value
read off the wire, even after confirming the two agree.

## Envelope format and distribution

Identical to [kernel-substrate](kernel-substrate.md): the DSSE envelope of
[signing-envelope.md](../v1/signing-envelope.md), with a mandatory `keyid` in every signature entry
equal to the lowercase-hex SHA-256 of the RFC 7638 JWK thumbprint input for the ed25519 public key.

```json
{
  "payload": "<base64(canonical Statement bytes)>",
  "payloadType": "application/vnd.in-toto+json",
  "signatures": [{"keyid": "<64 lowercase hex>", "sig": "<base64(raw 64-byte ed25519 signature over the PAE)>"}]
}
```

An implementation therefore shares one envelope code path with the companion predicate and differs
only in the predicate body, the predicate type it demands, and the binding comparison it runs. The
`payloadType` assertion runs before any signature work, because it is the cheapest place to catch a
cross-class replay. The `keyid` is a selector and creates no trust: it is excluded from the PAE and
therefore unauthenticated, a verifier resolves it against configured anchors and independently
re-derives it from the resolved key, and a matching keyid is never evidence of anything on its own.

A consumer MUST cap the envelope bytes it will consider; the reference implementation caps at
**1 MiB**, matching the cap its v1 read paths already applied.

**Distribution for the v2 form is not yet fixed by the reference implementation.** The v1 launch-chain
bundle has a specified address (a `launch-chain-<kernel-digest-hex>` tag, deliberately distinct from
the kernel-substrate tag prefix so the two classes never collide) precisely because that address was
the *only* thing connecting a v1 launch chain to its kernel. In v2 the address is no longer
load-bearing for that join - the subject is - so a publisher is free to choose one, and this document
specifies the artifact rather than its location. A publisher SHOULD still make the envelope resolvable
from the pinned kernel digest alone, and SHOULD keep it under the same authentication as the image it
describes. A locally cached envelope MUST be keyed by the kernel digest it binds, so 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 set of ed25519 public keys indexed
by the keyid derived from each key, so a key can never be resolved under an id it does not hash to.
The reference implementation embeds one key in PKIX/PEM form in the verifying binary
(`pkg/kernel/keys/probity.pub`) with the private half held out of band as a CI secret, but nothing in
the format names or assumes a particular key, and an empty anchor set MUST be refused at construction
rather than silently rejecting every envelope.

Multiple signature entries are the rotation path: an entry whose keyid does not resolve is skipped
rather than fatal, as is an entry whose `sig` is not decodable base64. "No configured anchor was
named" and "an anchor was named but nothing verified" MUST be distinct outcomes; only the second is
evidence of tampering.

The key is cosign-compatible, so a third party verifies with no Probity code:

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

over the PAE bytes reconstructed per the construction above. The producer signs the same file with
`cosign sign-blob --yes --key env://<secret> --tlog-upload=false`. `--tlog-upload=false` is
load-bearing: no entry is written to a public transparency log, so the attestation stays as private as
the artifact and there is no third-party inclusion proof to appeal to.

## Verification (third-party, offline)

The procedure is **verify-then-read**. A v1 verifier reconstructs the signed bytes from measurements
it already holds and compares them whole, so binding happens before the signature check; a v2 verifier
checks the signature and then reads the bytes that verified, and binding is a mandatory comparison
afterwards. What must never happen, under either ordering, is a path that returns "verified" while an
attested value differs from the value the verifier pinned.

1. **Cap and parse the envelope.**
2. **Assert `payloadType`** equals `application/vnd.in-toto+json`, before any signature work.
3. **Assert `signatures` is non-empty and every entry carries a `keyid`.**
4. **Base64-decode `payload`.**
5. **Resolve each entry's `keyid` against the configured anchors** and verify the signature as raw
   ed25519 over `PAE("application/vnd.in-toto+json", payload)`, reconstructed from the constant.
6. **Parse the decoded bytes** under the parse rules above and assert `_type` is
   `https://in-toto.io/Statement/v1`, `predicateType` is exactly
   `https://getprobity.dev/predicate/v2/launch-chain`, `subject` is non-empty, and `predicate` is
   present.
7. **Bind, and this step is MANDATORY and total.** Compare, against values the verifier holds
   independently:
   - the **subject digest** against the kernel index digest it intends to trust; and
   - **all four predicate members**, with `rootfsVerityRootHashes` compared as an **ordered
     sequence**.

   There is no partial mode worth having. A gate that verifies a signature without binding what the
   signature says is a vacuous check; the reference producer's own history contains a stdin-starved
   pipeline that published a zero-byte payload which signed and gate-verified perfectly. Report a
   binding mismatch with a sentinel distinct from a signature failure, and name the member that
   differs, so a failing gate says which measurement moved rather than only that something did.

No network access and no live execution environment is required once the envelope 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 binds against the measurements the boot is about to actually use, not against the values it
pinned.** The statement binds the publisher's promotion, so a comparison against a live reconstruction
succeeds only when live equals pin equals attested. A caller that compares the attested predicate to
its own pin has proved only that it agrees with itself.

An implementation SHOULD also make the not-verified state unforgeable by construction, so that a
dropped error or an unassigned result can never read as verified; only a present envelope that
verified and then bound over the live measurements may yield a verified value.

### CANON-EQ

A payload can be signed and yet not be RFC 8785 canonical, if a producer is defective. Asserting that
the verified payload is already canonical is called CANON-EQ, and where it runs is deliberately
asymmetric: a **boot-critical verifier does not run it**, because enforcing it there would import a
JSON canonicalizer and its unbounded recursive-descent parser into a boot path to defend against a
producer defect; a **producer, a CI promotion gate, an auditor, or any non-boot-critical third-party
verifier SHOULD run it**, and the reference `probity-attest verify` does. The honest cost is that a
producer bug emitting non-canonical-but-signed bytes would reach a boot path; the mitigations are that
the promotion gate will not publish it and that the mandatory binding contains what it could do.

## Enforcement posture

**The v2 form is not on any enforcement path in the reference implementation.** Nothing in the boot
path calls the v2 verifier; the v1 bespoke bundle path is untouched and is still the only thing that
runs there.

- **No environment variable governs the v2 form.** `PROBITY_REQUIRE_LAUNCH_CHAIN` and
  `PROBITY_DEV_ALLOW_UNATTESTED_KERNEL` govern the v1 kernel package alone and have no effect on a
  v2 statement. This document names no others, because none exist in current source.
- **The v1 posture is unchanged and remains live**: the v1 launch-chain gate is an explicit named
  opt-in, off by default, while a present-but-malformed or invalidly-signed v1 bundle is a hard
  failure whether or not the gate is on. See [the v1 document](../v1/launch-chain.md) for the
  disposition table and for the conditions that must be met before that gate can default on.

Two of those conditions are properties of the deployment rather than of either format and still
apply: **no launch-chain envelope is published** by any workflow, and **verity boot is opt-in**, so a
default non-verity boot gathers a different command line and an empty verity set that a live
comparison against a promoted pin would correctly reject. A gate turned on before a producer exists
fails every legitimate boot rather than catching an illegitimate one.

One disposition is worth restating as a rule of this format rather than of a deployment: **an
enforcing consumer SHOULD refuse to enforce against an all-zero placeholder pin.** Refusing looks like
the weaker choice and 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)

The coverage statement is part of the contract. A conforming launch-chain attestation says nothing
about any of the following:

- **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 an enforcing runtime's live comparison, not a property of the
  envelope.
- **That the kernel bytes named by the subject are approved.** The subject binds *which* kernel these
  measurements belong to; whether that kernel is approved is the
  [kernel-substrate](kernel-substrate.md) statement, and a consumer that needs both MUST verify both.
  The shared subject digest is what lets it join them without trusting an address.
- **Any host-side confinement.** A host seccomp filter is not bound by this predicate. Under v2 it
  could be added as an optional member without a new major version, which the v1 form could not do;
  until a producer emits one, a consumer MUST NOT infer it.
- **That the command line is safe, or that the verity root hashes correspond to any particular
  filesystem image.**
- **Build provenance, transparency-log inclusion, revocation, or key legitimacy** - as for
  [kernel-substrate](kernel-substrate.md), and for the same reasons.
- **That the payload was canonical**, unless the verifier ran CANON-EQ.

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

- A verifier CAN confirm the envelope is authentic under a configured anchor, which anchor admitted
  it, that the payload is an in-toto Statement v1 of exactly this predicate type, **which kernel index
  digest the measurements are bound to**, and that the initrd digest, command line, ordered verity
  root set and version are exactly the ones it intended - all offline, from the envelope 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 statement alone, that the command line is safe, that the verity
  root hashes correspond to any particular filesystem image, that the publisher's promotion decision
  was sound, that the kernel named by the subject is itself approved (that is the companion
  statement), or that a more recent promotion has superseded this one.

## Status and maturity

- **Format, canonicalization, signing construction, and verification: implemented.** One
  implementation produces the canonical bytes and one verifies them, from the same packages, so a
  producer and a consumer cannot diverge by construction. Committed cross-language conformance vectors
  carry the public key, the payload type, the canonical bytes, the PAE pre-image, the signature and
  the assembled envelope for a realistic verity command line, for a command line exercising the
  retired escaping hazard, and for a statement with unknown members.
- **Producer: a command exists; no CI step runs it.** `probity-attest statement launch-chain` builds
  the statement, `pae` emits the pre-image for `cosign sign-blob`, and `envelope` assembles the
  envelope from the committed public key. No workflow mints or publishes a v2 launch-chain envelope,
  and no distribution address is defined. As under v1, no conforming launch-chain artifact is
  published for a third party to fetch.
- **Consumer: not wired.** No boot path calls the v2 verifier.
- **Kernel binding: closed.** This is the v1 weakness this predicate exists to fix, and it is fixed in
  the format and in the producer, not merely proposed.

## Relationship to the v1 wire form

[The v1 predicate](../v1/launch-chain.md) remains normative for the v1 URI. A verifier presented with
a v1 bundle MUST apply the v1 rules - reconstruct the bespoke five-member payload in declaration order
and compare bytes - rather than in-toto Statement parsing. A consumer dispatches on the full
`predicateType` string, so the two never collide, and a v1 bundle carries no `payloadType` member for
a v2 verifier to accept in the first place.

Every difference, in one place:

| | v1 | v2 |
|---|---|---|
| Kernel binding | none in the signed bytes; a registry tag convention | the kernel index digest is the Statement subject |
| Container | bespoke `{payload, signature}` JSON object | DSSE envelope `{payload, payloadType, signatures[]}` |
| On-wire payload type | absent | `application/vnd.in-toto+json`, checked before any signature work |
| Signed payload | bespoke 5-member object, no `_type`, no subject | in-toto Statement v1 |
| Predicate members | the same four, plus `predicateType` in the same object | the same four, in a `predicate` object |
| Canonicalization | producer's field declaration order | RFC 8785, sorted member names |
| `predicateType` position | first | third (`_type`, `predicate`, `predicateType`, `subject`) |
| Domain separation, class to class | private per-class payload type (44 bytes) | shared in-toto payload type; `predicateType` checked as a field |
| Key identity on the wire | none | mandatory `keyid`, RFC 7638 thumbprint, selector only |
| Verification order | reconstruct, compare bytes, then check signature | check signature, then read, then bind |
| Binding scope | one byte comparison covers everything | subject digest plus all four members, verity array ordered |
| Unknown members | forbidden; any extra member breaks verification | MUST be ignored |
| Duplicate members | unreachable (bytes compared whole) | explicitly rejected |
| `<`, `>`, `&` in `kernelCmdline` | prohibited by producer rule | permitted; RFC 8785 fixes the escaping |
| Multiple signers | not representable | permitted; unresolved keyids skipped |
| Untrusted-signer vs forged | indistinguishable | distinct failures |
| Enforcement | opt-in via a named environment variable | not wired to any enforcement path |
| Published artifact | none | none |

## Related standards

- **[in-toto attestation](https://github.com/in-toto/attestation)** - USED. The payload is an in-toto
  Statement v1 and the subject convention is in-toto's; making the kernel digest the subject is what
  closes the v1 binding gap.
- **[DSSE](https://github.com/secure-systems-lab/dsse)** - USED, envelope and PAE both.
- **[RFC 8785 (JCS)](https://www.rfc-editor.org/rfc/rfc8785)** - USED as the canonicalization, which
  is what retires the v1 escaping prohibition on `kernelCmdline`.
- **[RFC 7638 (JWK thumbprint)](https://www.rfc-editor.org/rfc/rfc7638)** - USED as the keyid
  derivation; see [signing-envelope.md](../v1/signing-envelope.md).
- **[Sigstore / cosign](https://docs.sigstore.dev/)** - PARTIALLY ALIGNED. Static-key `sign-blob` /
  `verify-blob` over the PAE bytes, without Fulcio certificates and without transparency-log 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, and in v2 the
  statement it shares a subject with.
- **[signing-envelope.md](../v1/signing-envelope.md)** - the envelope this predicate rides, and which
  its v1 form deliberately did not.

## Versioning

The versioning rules in the standard's [README](../v1/README.md) apply to this predicate in full, and
they could not apply to its v1 form:

- **Major version in the URI.** A consumer dispatches on the full `predicateType` string and MUST
  reject a type it does not recognize.
- **Additive-optional members are a minor change and stay within the major version.** A producer MAY
  add optional members and a verifier MUST ignore what it does not recognize. This works because
  verification never rebuilds the bytes, so a member the verifier does not know about is a member it
  does not read. A host seccomp filter digest is the obvious candidate and would not require a v3.
- **Rename, removal, or any semantic change of an existing member is a new major version URI** - and
  that includes changing the element order or the element form of `rootfsVerityRootHashes`, since the
  order is which target is which.
