Signing and verification
A node in orbit runs whatever reaches it over the link, so it has to tell your release from anyone else's bytes without asking the ground. Every SpaceOS build is therefore signed, and every node checks the signature before it runs anything. Development builds can use a local identity. Release builds require an explicit non-development identity and a rollback index.
Signing provides authenticity and integrity. It does not encrypt an image or hide its contents.
Build identities
A build identity contains two independently generated signing keys:
- Ed25519 provides the classical signature.
- ML-DSA-65 provides the post-quantum signature.
space keys generate stores the two private keys in separate files and records
their paths under one identity name. Separate files do not provide separate
custody by themselves. Move each file to the custody system that owns it when
the two keys must be controlled independently.
The identity fingerprint printed by the CLI is the digest of the Ed25519 public key. It is not a digest of both keys.
List the local identities with space keys ls. The star marks the identity a
development build signs with. The two sections under it hold the other kind of
key this directory keeps, the operator authority a deploy approval roots in and
the operator identities issued under it, and a machine that has never run
space keys ca has neither. Pass --json for the key paths, the fingerprint
each file hashes to today, and the state of every row:
$ space keys lsSigning identities NAME FINGERPRINT* dev sha256:6a3753b41d98eeebebac07438fa2d89810e380c7fd30d3d07c95552dc1043010No identity here signed a launch-trust root in the image store; the next `space build` writes one.Operator authorityNo operator authority; `space keys ca` creates one.Operator identitiesNo operator identities; `space keys issue NAME` issues one.Create an identity, select it, and export its public key:
$ space keys generate flight$ space keys use flight$ space keys public flight > flight.ed25519.pub.pemspace keys public writes the Ed25519 public key. Keep the public key or its
fingerprint in an independent channel when another machine must verify an
image.
Algorithms and CNSA 2.0
A reviewer checking algorithm compliance compares the parameter sets a build
uses against a named suite. SpaceOS signs and establishes keys under one
profile, default, at NIST post-quantum security category 3:
| Use | Algorithm |
|---|---|
| Post-quantum signature | ML-DSA-65 (FIPS 204) |
| Classical signature | Ed25519 |
| Key establishment for a sealed release and SDLS rekeying | ML-KEM-768 (FIPS 203) |
| Authenticated encryption | AES-256-GCM |
| Content addressing | SHA-256 |
A project's CycloneDX SBOM lists each algorithm as a cryptographic asset with
its category. space show derives the list from the profile a build signs
under:
$ space show --sbom --format cyclonedx | jq -r '.components[] | select(.cryptoProperties.algorithmProperties.primitive == "signature" or .cryptoProperties.algorithmProperties.primitive == "kem") | "\(.name) category \(.cryptoProperties.algorithmProperties.nistQuantumSecurityLevel)"'jq: parse error: Invalid numeric literal at line 1, column 3[5]ECDSA-P256 keys the TPM sealing policy and the local development certificates. RSA-2048 is the signature the Raspberry Pi CM5 boot ROM requires on the board's own boot image.
CNSA 2.0 requires category 5 for national security systems: ML-DSA-87 for
signatures, ML-KEM-1024 for key establishment, and SHA-384. SpaceOS defines a
second profile, cnsa-2.0, with those parameter sets. No option selects it
yet, and space build refuses to sign under a profile the build cannot honor.
Three parts are missing for cnsa-2.0:
- The signing library has no ML-DSA-87.
- The release sealing format accepts ML-KEM-768 only.
- OCI content addressing is SHA-256.
A build therefore meets category 3 and does not meet CNSA 2.0.
Development builds
If no identity is configured, the first development build creates and selects
an identity named dev. Later development builds reuse the selected identity.
$ space build -t <your-registry>/app:devThe development identity is for the local build loop. A release build refuses
the dev identity and refuses the same development key if it is renamed or
passed by file path.
Release builds
Create or import the release identity before running the release build. This local command is suitable for a bench identity:
$ space keys generate releaseFor production custody, generate the keys in the system that authorizes the release. Make both private-key files available to the build only for the signing operation.
Build the release with an explicit identity and rollback index:
$ space build -t <your-registry>/app:release \> --release \> --signing-key release \> --rollback-index 1A release build enforces these checks:
- The signing identity already exists. The build does not create it.
- The identity is not the recorded development identity.
--rollback-indexis at least1.- The secret keystore posture is
sealed. The posture says howspace-secretprotects workload secrets on the node:devkeeps them in a plaintext file, andsealedencrypts them under a key-encryption key (KEK) that a platform source, such as a TPM, releases at boot. A release rejectsdev.
A tag ending in :release, :prod, or :production does not enable release
behavior. The build refuses such a tag unless --release is present.
You can name the two private-key files instead of an identity:
$ space build -t <your-registry>/app:release \> --release \> --signing-key-ed25519 ./release.ed25519.pem \> --signing-key-mldsa65 ./release.mldsa65.pem \> --rollback-index 1Both file arguments are required. The CLI does not search beside one key for the other.
The --keystore option controls the workload secret store in the measured
launch plan. It does not control build-signing key storage. A sealed workload
keystore does not prove that the release signing keys were held in a hardware
security module (HSM), a device that stores private keys and signs inside
itself so the key never leaves it, or in a separate signing service.
Verify delivered build evidence
space image verify rechecks a delivered image in the local OCI store. It requires a
publisher public key obtained independently from the image.
Pull the image if it is not already present, then verify it:
$ space image pull <registry>/<project>/app:release$ space image verify <registry>/<project>/app:release \> --anchor ./publisher.ed25519.pub.pemThe command checks this evidence chain:
- The image index contains digest-valued SBOM and provenance annotations.
- The referenced blobs exist and match those digests.
- The blobs decode as in-toto statements. An in-toto statement is a JSON document that names a subject by digest and makes one claim about it, here its SBOM or its provenance.
- The detached SCITT signed statement for each item verifies under
--anchor. A SCITT signed statement is that document wrapped in a COSE signature, the IETF SCITT format for signed supply-chain claims. - The statement subjects match the partitions published by the image index.
The command exits nonzero when a link fails. It distinguishes a bad anchor, a wrong signer, a malformed annotation, a missing local image, tampered evidence, and a tampered published artifact.
space image verify does not pull the image. It does not inspect a running node,
verify board secure boot, or prove release-key custody. Those are separate
checks.
Node admission
The node authorizes launch bytes through a TUF metadata chain. The roles are TUF's, and the encoding is CBOR (RFC 8949), a compact binary form of the JSON data model, signed with COSE (RFC 9052), the signature format defined over CBOR. The TUF specification encodes its metadata as canonical JSON, so go-tuf and python-tuf cannot read this chain; that departure from the specification is a known defect, to be removed by moving to the standard encoding:
| Metadata role | Check |
|---|---|
root | A threshold of pinned keys authorizes the root metadata. |
timestamp | Selects the current snapshot metadata. |
snapshot | Selects one targets metadata version. |
targets | Authorizes a path, byte length, and SHA-256 digest. |
The node starts from public root keys pinned out of band. It verifies the root threshold, then timestamp, snapshot, and targets. It rejects a target when the path, length, or digest does not match the launch bytes.
The signed metadata and content travel with the release, so a node verifies the
chain it has selected without reaching the network. A tag in the local store
selects that chain: spaceos-trust:current for launch trust, and
spaceos-release-trust:current for release trust. Retained root and targets
versions refuse an older set.
A tag is a mutable pointer, and no signature covers what it points at. The store cannot tell a maintainer re-pointing a tag from an attacker doing it, so a tag decides which chain a node checks. The pinned root keys bound this. A chain reached through a tag still has to verify under them, so an attacker who controls a tag cannot forge trust metadata. The node also keeps, for each role, the highest version it has admitted, in a ledger tied to a monotonic counter, and refuses a lower one. What remains open to that attacker is any chain the pinned keys signed whose versions are not below those: a node that never saw the newest chain can be held on an older signed one. The gap closes when a node takes the chain by digest: the anchor pinned out of band when the node is provisioned, and everything the chain selects named by metadata the node has already verified as current.
A root can require both Ed25519 and ML-DSA-65 signatures. A threshold of two rejects a root carrying only one of those signatures.
Keep the boundaries separate
- A build signature authenticates build evidence and release metadata. It does not encrypt image contents.
- The Ed25519 key printed by
space keys publicis the anchor accepted byspace image verify. The ML-DSA-65 half is used by the launch-trust metadata chain. - The sealed workload keystore protects runtime secret material. It is not the release signing-key store.
- Board secure boot, mission-command authorization, and link security use
separate verification paths. A successful
space image verifyresult does not assert those paths.
See Boot and updates for target boot and rollback behavior.