Skip to content
Generating and listing a signing identity work today and run on this page; the release build and space image verify need a registry and a builder VM, so they are shown rather than run by the documentation gate.

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:

Terminal window
$ space keys ls
Signing identities
NAME FINGERPRINT
* dev sha256:6a3753b41d98eeebebac07438fa2d89810e380c7fd30d3d07c95552dc1043010
No identity here signed a launch-trust root in the image store; the next `space build` writes one.
Operator authority
No operator authority; `space keys ca` creates one.
Operator identities
No operator identities; `space keys issue NAME` issues one.

Create an identity, select it, and export its public key:

Terminal window
$ space keys generate flight
$ space keys use flight
$ space keys public flight > flight.ed25519.pub.pem

space 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:

UseAlgorithm
Post-quantum signatureML-DSA-65 (FIPS 204)
Classical signatureEd25519
Key establishment for a sealed release and SDLS rekeyingML-KEM-768 (FIPS 203)
Authenticated encryptionAES-256-GCM
Content addressingSHA-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:

Terminal window
$ 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.

Terminal window
$ space build -t <your-registry>/app:dev

The 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:

Terminal window
$ space keys generate release

For 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:

Terminal window
$ space build -t <your-registry>/app:release \
> --release \
> --signing-key release \
> --rollback-index 1

A 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-index is at least 1.
  • The secret keystore posture is sealed. The posture says how space-secret protects workload secrets on the node: dev keeps them in a plaintext file, and sealed encrypts them under a key-encryption key (KEK) that a platform source, such as a TPM, releases at boot. A release rejects dev.

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:

Terminal window
$ space build -t <your-registry>/app:release \
> --release \
> --signing-key-ed25519 ./release.ed25519.pem \
> --signing-key-mldsa65 ./release.mldsa65.pem \
> --rollback-index 1

Both 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:

Terminal window
$ space image pull <registry>/<project>/app:release
$ space image verify <registry>/<project>/app:release \
> --anchor ./publisher.ed25519.pub.pem

The command checks this evidence chain:

  1. The image index contains digest-valued SBOM and provenance annotations.
  2. The referenced blobs exist and match those digests.
  3. 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.
  4. 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.
  5. 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 roleCheck
rootA threshold of pinned keys authorizes the root metadata.
timestampSelects the current snapshot metadata.
snapshotSelects one targets metadata version.
targetsAuthorizes 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 public is the anchor accepted by space 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 verify result does not assert those paths.

See Boot and updates for target boot and rollback behavior.