Vendor trust-root set v1
The vendor trust-root set is the list of Stella Ops vendor public keys that an installation pins. Vendor-signed material, such as product update manifests and entitlement certificates, verifies against this set offline. The set is rotated by signed root updates: the current root authorises its successor, and the successor countersigns. An installation never holds a vendor private key.
Implementation: VendorTrustRootSet in src/__Libraries/StellaOps.Cryptography.Dsse.Signing/VendorTrustRootSet.cs.
Purposes
Each purpose has its own roots. A root pinned for one purpose never verifies material of another.
| Purpose | Verifies | Consumers |
|---|---|---|
manufacturer-release | Product update manifests (product-update-manifest-v1.md) | stella self update --check and --dry-run, and stella verify release-manifest, when the operator supplies no trust root |
vendor-entitlement | Entitlement certificates (entitlement-certificate-v1.md) | Entitlement verification; the Platform entitlement import (POST /api/v1/platform/entitlements) |
The purpose for signed vulnerability-intelligence manifests is added with that contract.
Location
- The CLI reads the set from the directory named by
STELLAOPS_VENDOR_TRUST_ROOTS. When the variable is not set, it reads the shipped set attrust/vendor-roots/in the directory that holds thestellabinary. - The Platform service reads the set the same way: from
STELLAOPS_VENDOR_TRUST_ROOTSwhen set, otherwise fromtrust/vendor-roots/in the directory that holds the Platform binary. It reads the set at each entitlement import, so an applied root update takes effect without a restart. - The release ships the set. Until the vendor root ceremony has run, the shipped set holds no vendor root, and every consumer that depends on it fails closed with
vendor_roots_missing:<purpose>. - The set belongs to the installation, like its binaries: only the installation’s administrator should be able to write to it.
- Roots that arrive with the material being verified, such as the
manifest/trust-roots/folder of an Offline Update Kit, are never part of the set.
Layout
<set>/<purpose>/anchors/*.pem roots pinned by the release, one PUBLIC KEY PEM block per file
<set>/<purpose>/updates/*.dsse.json root updates, applied in sequence order (file names are free)
Key identifier
A root’s key identifier is sha256: followed by the lowercase hexadecimal SHA-256 of its SubjectPublicKeyInfo DER encoding. It is derived from the key, so a root cannot be mislabelled.
Root update
A root update is a DSSE envelope:
{
"payloadType": "application/vnd.stellaops.vendor-root-update.v1+json",
"payload": "<base64 of the canonical payload bytes>",
"signatures": [
{ "keyid": "<previousKeyId>", "sig": "<base64 signature by the current root>" },
{ "keyid": "<newKeyId>", "sig": "<base64 signature by the new root>" }
]
}
The payload is canonical JSON (keys sorted, no insignificant whitespace). Every field is required and no other field is allowed.
| Field | Type | Meaning |
|---|---|---|
schemaVersion | string | stella.ops/vendorRootUpdate@v1. |
purpose | string | The purpose whose root is replaced. |
sequence | integer | Position in the purpose’s update chain, from 1, without gaps. |
previousKeyId | string | Key identifier of the root being replaced. |
newKeyId | string | Key identifier of the new root. |
newPublicKey | string | Standard base64 of the new root’s SubjectPublicKeyInfo DER. |
issuedAt | string | Time the update was signed, UTC, YYYY-MM-DDTHH:MM:SSZ. Informational. |
Both signatures cover the DSSE pre-authentication encoding of the payload type and the payload bytes. Supported algorithms: ES256 (P-256, IEEE P1363 r||s signature) and Ed25519.
Resolution rules
A verifier resolves the set for one purpose as follows. Any issue refuses the whole set: no root of that purpose is trusted, and no partial result is used.
- Load every anchor. Refuse private key material, a label other than
PUBLIC KEY, more than one PEM block in a file, and a duplicate root. - Order the updates by
sequence; the sequence must run from 1 without a gap or a repeat. - For each update, require that
purposematches; thatpreviousKeyIdnames a root that is active at that point (a superseded root cannot authorise another update); thatnewKeyIdis the identifier ofnewPublicKey; that the new key has not been used before; that a signature withkeyidequal topreviousKeyIdverifies under the current root; and that a signature withkeyidequal tonewKeyIdverifies under the new key. - Apply the update: the new root becomes active and the previous root is superseded.
- Material signed by a superseded root is refused (
vendor_root_superseded:<keyId>).
Operator-supplied trust roots (--trust-root, --trusted-public-key-pem, or the STELLAOPS_PRODUCT_UPDATE_TRUST_ROOT and STELLAOPS_RELEASE_TRUST_ROOT variables) replace the pinned set for that command. They are never merged with it.
Issue codes
| Code | Meaning |
|---|---|
vendor_roots_missing:<purpose> | No set, or no anchor, for the purpose. |
vendor_root_purpose_invalid:<purpose> | The purpose name is not ^[a-z][a-z0-9-]{1,63}$. |
private_key_material_rejected:<file> | A file in the set contains private key material. |
vendor_root_pem_invalid, vendor_root_pem_label_invalid, vendor_root_pem_multiple (:<file>) | An anchor is not exactly one PUBLIC KEY PEM block. |
vendor_root_duplicate:<keyId> | The same root is pinned twice. |
vendor_root_file_too_large:<file>, vendor_root_file_unreadable:<file> | A file exceeds 64 KiB or cannot be read. |
root_update_payload_type_invalid, root_update_payload_invalid, root_update_payload_not_canonical, root_update_payload_fields_invalid, root_update_signatures_missing, root_update_signature_malformed, root_update_invalid (:<file>) | The envelope or its payload does not match this contract. |
root_update_purpose_mismatch:<file> | The update belongs to another purpose. |
root_update_sequence_invalid:<file> | A gap or a repeated sequence number. |
root_update_previous_unknown:<file>, root_update_previous_superseded:<file> | The replaced root is not active. |
root_update_key_id_mismatch:<file>, root_update_key_reused:<file> | The new key’s identifier is wrong, or the key was used before. |
root_update_signature_invalid:<file>, root_update_countersignature_invalid:<file> | The current root’s signature or the new root’s countersignature does not verify. |
vendor_root_superseded:<keyId>, vendor_root_unknown:<keyId> | Material names a superseded or unknown root. |
Limits
- Resolution uses no network and no freshness deadline, so an air-gapped installation keeps verifying for as long as its set is current.
- The set cannot detect that an update file was deleted from the installation’s own disk; protect the directory as described under Location.
- Compromise recovery does not use a root update, because a compromised root could authorise an attacker’s key. The replacement set ships in a new release instead.
