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 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 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 canonicalized under RFC 8785 and carried in the DSSE envelope specified in 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.
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 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.
"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 OCIsha256:-prefixed form forinitrdDigestand the bareveritysetupform 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.
nameis REQUIRED and non-empty, conventionallyguest-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:
{"_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, andpredicateTypemoves from first to third. - There is no insignificant whitespace, no indentation, and no trailing newline.
rootfsVerityRootHashesis 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/jsontakes 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:
{"initrdDigest":"sha256:<64-hex>","kernelCmdline":"<exact boot command line>","rootfsVerityRootHashes":["<64-hex>"],"version":"<kernel-version>"}
-
initrdDigest(REQUIRED, string) - the digest of the initrd, in OCIsha256:<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 nosha256:prefix (theveritysetuproot-hash form, deliberately not the OCI forminitrdDigestuses).- 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>).veritysetuprandomizes 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 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 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: the DSSE envelope of
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.
{
"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: 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.
-
Cap and parse the envelope.
-
Assert
payloadTypeequalsapplication/vnd.in-toto+json, before any signature work. -
Assert
signaturesis non-empty and every entry carries akeyid. -
Base64-decode
payload. -
Resolve each entry's
keyidagainst the configured anchors and verify the signature as raw ed25519 overPAE("application/vnd.in-toto+json", payload), reconstructed from the constant. -
Parse the decoded bytes under the parse rules above and assert
_typeishttps://in-toto.io/Statement/v1,predicateTypeis exactlyhttps://getprobity.dev/predicate/v2/launch-chain,subjectis non-empty, andpredicateis present. -
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
rootfsVerityRootHashescompared 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_CHAINandPROBITY_DEV_ALLOW_UNATTESTED_KERNELgovern 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 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.
versionis 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 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, 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-chainbuilds the statement,paeemits the pre-image forcosign sign-blob, andenvelopeassembles 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 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 - 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 - USED, envelope and PAE both.
- RFC 8785 (JCS) - USED as the canonicalization, which
is what retires the v1 escaping prohibition on
kernelCmdline. - RFC 7638 (JWK thumbprint) - USED as the keyid derivation; see signing-envelope.md.
- Sigstore / cosign - PARTIALLY ALIGNED. Static-key
sign-blob/verify-blobover the PAE bytes, without Fulcio certificates and without transparency-log upload. - dm-verity - the
root-hash form
rootfsVerityRootHashescarries isveritysetup's: bare hex, fixed salt. - kernel-substrate - the anchor this predicate extends, and in v2 the statement it shares a subject with.
- 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 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
predicateTypestring 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.