# Probity Kernel-Substrate Attestation, v1

**predicateType:** `https://getprobity.dev/predicate/v1/kernel-substrate`

A kernel-substrate attestation is a signed, offline-verifiable statement that a publisher approved
**exactly one set of guest-kernel bytes**, identified by the immutable OCI manifest-index digest of
the published kernel image, and that the publisher labels those bytes with a named kernel version.
Where the verdict predicates in this standard describe what happened to an artifact that was run, this
predicate describes the substrate the run happens *on*: the kernel image a runtime is permitted to
boot a microVM with. It is the cryptographic anchor the [launch-chain](launch-chain.md) attestation
extends.

The statement is intentionally the smallest thing that can carry that meaning. Its whole content is
"these bytes are approved", the bytes are named by digest, and the version is a label carried under
the signature rather than a derived fact.

This predicate does **not** ride the DSSE / in-toto envelope specified in
[signing-envelope.md](signing-envelope.md). It is signed as a standalone canonical payload under a
domain-separated Pre-Authentication Encoding, described in full below. That divergence is a property
of the v1 wire form and is stated here rather than left for a reader to discover by failing to parse
an in-toto Statement (see Relationship to the v2 wire form).

The reference implementation is the engine's kernel-attestation component: the canonical payload
builder, the verifier, and the mandatory-enforcement predicate, together with the embedded trust
root it verifies against and the release workflow that produces the signed bundle.

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 **approved guest-kernel image, by OCI manifest-index digest**. In this v1 wire form
the subject is not an in-toto `subject` array; it is the `subjectDigest` member of the signed payload
itself.

The digest is the multi-platform manifest-index digest, in OCI form `sha256:<64 lowercase hex>`, of
the kernel image as published to its registry. It is deliberately the index digest and not a mutable
registry tag: a force-pushed tag can substitute different bytes, and a digest cannot.

## Signed payload

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

```json
{"predicateType":"https://getprobity.dev/predicate/v1/kernel-substrate","subjectDigest":"sha256:<64-hex>","version":"<kernel-version>"}
```

The byte layout is normative and is the whole reason a third party can reconstruct it:

- The members appear in the order `predicateType`, `subjectDigest`, `version`. This is the field
  declaration order of the producer's struct, and **declaration order is the format** for this
  predicate. It is not a sorted-member canonicalization; a producer that sorts members by name
  happens to agree here only because these three names are already in sorted order, and MUST NOT rely
  on that coincidence.
- There is no insignificant whitespace, no indentation, and no trailing newline.
- Every value is a JSON string. There are no maps and no arrays, so no ordering question remains open.

A producer MUST emit these bytes and no others. A verifier MUST NOT re-serialize a decoded payload;
it compares the bytes it was handed against bytes it reconstructs itself.

## Field semantics

- **`predicateType`** (REQUIRED, string) - MUST be exactly
  `https://getprobity.dev/predicate/v1/kernel-substrate`. It is carried inside the signed bytes, so a
  byte-equality check against a reconstructed pre-image transitively binds it; a verifier that
  performs that check need not compare the field separately.

- **`subjectDigest`** (REQUIRED, string) - the OCI manifest-index digest of the approved kernel image,
  in `sha256:<64 lowercase hex>` form. This is the artifact the statement is about. A signature over a
  payload naming a *different* digest is a downgrade attack, not a partial match; see Verification.

- **`version`** (REQUIRED, string, non-empty) - the kernel version the publisher associates with those
  bytes (for example `6.1.137`). **It is a label under signature, not a derived fact.** Nothing in the
  attestation, and nothing a verifier can do with it, establishes that the named version is what
  produced those bytes. It exists so the digest can be read against a human-meaningful release, and so
  the [launch-chain](launch-chain.md) statement can be tied to the same release.

There are no optional members in v1. A producer MUST NOT add one: an additional member changes the
signed bytes, and byte-equality verification then fails everywhere rather than degrading gracefully.
Additive evolution for this predicate is a new major version, not a new field.

## 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.kernel-attestation.v1+json"   (50 bytes)
```

Binding the payload type inside the signed bytes is the domain separation that makes a signature
minted for a kernel-substrate attestation unusable as any other Probity payload class under the same
key, and vice versa. A verifier MUST reconstruct the PAE itself and MUST NOT verify over the payload
alone.

Note that the payload type is **not** `application/vnd.in-toto+json` and there is no DSSE envelope
object: the PAE construction is borrowed, the envelope is not. The signature is detached.

## Bundle format and distribution

The attestation is distributed as a **bundle**: a JSON object pairing the exact signed payload bytes
with the detached signature.

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

- **`payload`** (REQUIRED) - base64 of the canonical payload. It is base64-encoded specifically so the
  exact bytes survive a JSON round-trip; the signature commits to the decoded bytes.
- **`signature`** (REQUIRED) - base64 of the raw ed25519 signature. This is the value `cosign
  sign-blob --output-signature` emits.

The bundle carries no `keyid`, no certificate, and no transparency-log entry. Key identity is
established out of band by the trust root a verifier configures (below).

A publisher SHOULD make the bundle resolvable from the pinned kernel digest alone, with no
out-of-band tag lookup. The reference implementation publishes it as a one-layer OCI artifact - a tar
whose single regular file entry is the bundle JSON - at the deterministic ref

```
<registry>/kernel:attestation-<digest-hex>
```

where `<digest-hex>` is `subjectDigest` with the `sha256:` prefix removed. The bundle is kept in the
same registry, under the same authentication, as the kernel it describes, so it is exactly as private
as the image.

A consumer MUST cap the size of a bundle it reads from a registry or from disk. The reference
implementation caps both at 1 MiB and reports an oversized bundle explicitly rather than truncating
it, because a silent truncation resurfaces downstream as a misleading parse error.

## Trust root

Verification is against a configured ed25519 public key. The reference implementation embeds one key,
in PKIX/PEM form, in the verifying binary (`pkg/kernel/keys/probity.pub`), memoized and parsed once;
the matching private key is an out-of-band CI secret and is never in a repository. The same key
identity verifies the kernel attestation and the verdict bundles the runtime emits.

The key is cosign-compatible, so a third party with the public key and a bundle can verify without any
Probity code:

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

where `<pae-file>` holds the PAE bytes reconstructed per the construction above and `<sig-file>` holds
the bundle's base64 `signature`. The producer signs `<pae-file>` with `cosign sign-blob --yes --key
env://<secret> --tlog-upload=false`, so the in-process verifier and the CLI verify over identical
bytes. `--tlog-upload=false` is load-bearing: no entry is written to a public transparency log, which
keeps the attestation as private as the artifact.

## Verification (third-party, offline)

A verifier begins from the digest and version it intends to bind - not from the bundle.

1. Reconstruct the canonical payload for the intended `subjectDigest` and `version`, per Signed
   payload.
2. Decode the bundle's `payload` from base64 and require it to be **byte-equal** to the reconstruction.
   A byte-equal payload is strictly stronger than a field-by-field comparison: it binds the digest, the
   version, and the `predicateType` in one check. A payload that differs MUST be refused as a
   different statement, never repaired or re-parsed leniently.
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, no registry, and
no live execution environment is required once the bundle and the key are in hand.

Order matters. Checking the signature first and the binding second would accept a validly signed
statement about a *different* kernel, which is the whole shape of a downgrade attack.

## Enforcement posture

This is the fail-closed rail of the pair.

- **Enforcement is mandatory by default.** In the reference implementation the predicate that decides
  this is `mandatoryAttestation()`, defined as the negation of the development escape below, so with
  no environment configuration at all a missing attestation bundle is a hard failure and the kernel is
  not used. The justification is a producer-side invariant: a digest is promoted into the runtime's pin
  manifest only by a CI gate that has already verified a freshly minted bundle against the trust root,
  so a real bundle exists for every pinned digest and an absent one means something is wrong.

- **A present bundle that fails verification is always a hard failure.** A malformed bundle, a
  signature that does not verify, or a payload that binds a digest other than the one requested is
  refused unconditionally. No environment variable, including the one below, relaxes this.

- **One narrow escape exists, and it covers absence only.** Setting the environment variable
  `PROBITY_DEV_ALLOW_UNATTESTED_KERNEL` to `1`, `true`, or `yes` (compared case-insensitively after
  trimming surrounding whitespace) downgrades an **absent** bundle to a logged warning and a
  not-verified result, so a developer can build against a kernel whose bundle has not been published
  yet. It is explicitly named, it is the only way to disable the gate, and the fail-closed posture is
  restored the moment it is unset. It is never appropriate in production, and it does not make a
  forged bundle acceptable.

A consumer implementing this specification independently SHOULD adopt the same shape: default-deny on
absence, unconditional-deny on a present-but-invalid bundle, and any relaxation named explicitly
rather than folded into a general warn-only mode.

Cached copies deserve one note, because the failure they prevent is quiet. A locally cached bundle
MUST be keyed by the digest it binds, not by the version alone. A republished kernel keeps its version
and mints a new digest, so a version-keyed cache would silently reuse the previous digest's bundle; a
digest-keyed cache makes a digest change a cache miss by construction.

## What this predicate does NOT assert (coverage)

The coverage statement is part of the contract. A conforming kernel-substrate attestation says nothing
about any of the following, and a consumer that reads any of them into it has over-read the statement:

- **That the named version produced these bytes.** `version` is an unverified label.
- **Anything about the rest of the boot.** The initrd, the kernel command line, and the dm-verity root
  hashes are outside this statement entirely; they are the [launch-chain](launch-chain.md) predicate's
  subject. An approved kernel booted with a substituted command line is fully consistent with a valid
  kernel-substrate attestation.
- **Anything about how the kernel was built.** There is no build provenance here: no source revision,
  no builder identity, no toolchain, no reproducibility claim. This is an approval statement, not a
  SLSA provenance statement.
- **Anything about the kernel's behaviour, configuration, or security properties.** The statement is
  that the publisher approved these bytes, not that the bytes are good.
- **Public discoverability or non-repudiation via a transparency log.** Signing deliberately does not
  upload to a public log, so there is no third-party inclusion proof and no independent timestamp.
- **Revocation.** Nothing in the bundle expires or is revoked. A consumer that needs revocation
  obtains it out of band.
- **That the publisher's key is the right key.** Key identity is a trust-anchor configuration
  decision, not something the bundle establishes about itself.

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

- A verifier CAN confirm the bundle is authentic under a configured key, that it binds exactly the
  kernel digest and version the verifier intended, and that its payload type is domain-separated from
  every other Probity signature class. All of that is checkable offline from the bundle and the public
  key alone.
- A verifier CANNOT, from the attestation alone, confirm that the digest names a kernel with any
  particular property, that the version string is accurate, that the approval decision behind the
  signature was sound, or that a more recent approval has superseded this one. The guarantee is
  integrity, provenance under a named key, and exact subject binding - not a judgement about the
  artifact.

## Status and maturity

- **Format, signing, and verification: implemented.** The producer (a CI workflow that builds the
  canonical payload, builds the PAE, signs it with static-key `cosign sign-blob`, verifies its own
  output before promoting anything, re-resolves the digest to close the promotion TOCTOU, and pushes
  the bundle to the deterministic ref) and the in-process verifier both exist and agree byte-for-byte.
- **Enforcement: live and fail-closed by default**, per Enforcement posture.
- **Trust model: single static key.** A configurable multi-anchor or keyless model is not part of this
  predicate's v1 wire form.

## Relationship to the v2 wire form

A successor exists in the reference implementation at
`https://getprobity.dev/predicate/v2/kernel-substrate`. It is a schema change, not only an envelope
change: in v2 the approved bytes move out of the predicate body and become the in-toto Statement
subject, the predicate carries the version alone, and the statement is canonicalized under RFC 8785
and wrapped in the DSSE envelope this standard specifies elsewhere. Removing a member is breaking by
this standard's own versioning rule, which is why it is a new URI rather than a revision of this one.

This document remains normative for the v1 URI. Bundles already minted under
`.../predicate/v1/kernel-substrate` keep addressing it, and a verifier presented with one MUST apply
the v1 rules above 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. The PAE construction and
  its domain-separation property are taken verbatim; the envelope object, its `payloadType` value, and
  its `keyid` field are not used in this v1 wire form.
- **[in-toto attestation](https://github.com/in-toto/attestation)** - NOT USED in v1. The subject lives
  in the payload as `subjectDigest` rather than in an in-toto `subject` array. The v2 successor above
  is the in-toto-shaped form.
- **[Sigstore / cosign](https://docs.sigstore.dev/)** - PARTIALLY ALIGNED. Static-key `cosign
  sign-blob` / `verify-blob` over the PAE bytes, deliberately without Fulcio certificates and without
  Rekor transparency-log upload.
- **[SLSA provenance](https://slsa.dev/spec/v1.0/provenance)** - RELATED, not overlapping. Provenance
  describes how an artifact was built; this predicate records that a publisher approved specific bytes.
- **[launch-chain](launch-chain.md)** - the companion predicate that binds the rest of the boot
  pre-image. This one is its anchor.
- **[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 kernel-substrate v1 predicate and its member set is closed. Because verification is
byte-equality over a canonical pre-image, there is no additive-optional-field path: adding, renaming,
removing, or reordering a member 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; it does not apply to
this v1 wire form, and stating that plainly is preferable to implying a tolerance no verifier of this
predicate can offer.
