Skip to content
New kind of attack — caught and fixed before it was publicly known·Read the timeline
Probity Open Predicate Standard
/predicate/v1/kernel-substrate
View raw .md →

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

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:

{"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 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.

{
  "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 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 - 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 - 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 - 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 - RELATED, not overlapping. Provenance describes how an artifact was built; this predicate records that a publisher approved specific bytes.
  • launch-chain - the companion predicate that binds the rest of the boot pre-image. This one is its anchor.
  • 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 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.

Get access

Run your agents through Probity.

A few details about your setup and we'll get you gating agents fast.