The NØNOS-ID Certificate

July 16, 2026 · View on GitHub

The certificate is a capsule publisher's durable identity. Where a manifest is a per-build statement that changes every time the capsule is rebuilt, the certificate is the long-lived root beneath it: it is signed by the trust anchor, it sets the hard ceiling on the capabilities any capsule under this identity may ever hold, it names the namespaces the identity is allowed to use, and it carries the publisher keys that sign manifests. This page is its exact schema.

The type is NonosIdCertificate (src/security/nonos_id_cert/schema/cert.rs:26). The current schema version is 2 (nonos_id_cert/schema/constants.rs:17).

The structure

  NonosIdCertificate
    schema_version           u16          currently 2
    cert_serial              u64          unique serial, used for revocation
    nonos_id                 [u8; 32]     the publisher identity
    namespace_globs          Vec<NamespaceGlob>   the namespaces authorised
    allowed_caps_ceiling     u64          the hard cap on capabilities
    metadata                 [u8; 256]    free-form publisher metadata
    metadata_len             u16
    valid_from_ms            u64          start of the validity window
    valid_until_ms           u64          end of the validity window
    trust_anchor_epoch       u64          the anchor epoch it was issued under
    publisher_keys           Vec<PublisherKey>          keys that sign manifests
    trust_anchor_signatures  Vec<TrustAnchorSignature>  the anchor's signatures

Three fields are the reason the certificate exists. allowed_caps_ceiling is the ceiling that stage two of verified spawn checks the manifest's requested capabilities against, so no capsule under this identity can ever exceed it, whatever a manifest asks for. namespace_globs limits the namespaces the identity may publish under. publisher_keys are the keys the manifest's own signatures are checked against. The remaining fields exist to make the certificate itself verifiable and revocable, covered under verification below.

The fixed sizes

As with the manifest, every bound is a named constant (nonos_id_cert/schema/constants.rs) and every text or key field is a fixed array with a length, so an untrusted certificate cannot drive an unbounded allocation:

  ID_CERT_SCHEMA_VERSION      2      the version this kernel accepts
  NONOS_ID_LEN               32      the identity length
  PUBLISHER_KEY_ID_LEN       16      a key identifier
  MAX_NAMESPACE_GLOBS         8      at most eight namespace patterns
  MAX_NAMESPACE_GLOB_LEN     96
  MAX_METADATA_LEN          256
  MAX_PUBLISHER_KEYS          4      at most four manifest-signing keys
  MAX_KEYS_PER_ALG            2      at most two keys per algorithm
  MAX_TRUST_ANCHOR_SIGNATURES 4      at most four anchor signatures

MAX_KEYS_PER_ALG is two rather than one on purpose: it allows a publisher to carry a current and a next key for the same algorithm so that manifests can be re-signed onto a new key before the old one is retired, without the certificate having to be reissued for every rotation.

What the certificate authorises

The capability ceiling is a plain u64 compared bitwise. Stage two of spawn rejects any manifest whose required | optional has a bit set outside allowed_caps_ceiling (caps::check_ceiling). Because the ceiling lives in the anchor-signed certificate and not in the manifest, a publisher cannot widen a capsule's authority by editing a manifest; the manifest can only ask for a subset of what the certificate already permits.

The namespace is matched by glob, not by equality. namespace_matches (cert.rs:47) returns true if any of the certificate's namespace_globs matches the manifest's namespace under glob_match (nonos_id_cert/schema/glob_match.rs), so a single certificate can authorise a family of namespaces, for example a publisher's whole reverse-DNS subtree, while still being bounded to it.

  NamespaceGlob
    bytes  [u8; 96]    the glob pattern
    len    u8

Publisher keys

The publisher keys are the keys that sign manifests (nonos_id_cert/schema/sub.rs:35):

  PublisherKey
    algorithm   AlgId
    key_id      [u8; 16]
    pubkey      [u8; MAX_PUBKEY_BYTES]
    pubkey_len  u16

A manifest's PublisherSignature names a key_id, and manifest verification looks the matching key up with publisher_key_by_id (cert.rs:51) rather than trying every key, then verifies the signature against that key's pubkey_bytes. This is the link between the two schemas: the certificate carries the public keys, the manifest carries signatures under them, and the certificate is itself signed by the anchor, so the whole chain reduces to the one key baked into the kernel.

Trust-anchor signatures

The certificate's own authenticity comes from the anchor (nonos_id_cert/schema/sub.rs:49):

  TrustAnchorSignature
    algorithm  AlgId
    sig        [u8; MAX_SIG_BYTES]
    sig_len    u16

Stage one of verified spawn checks a TrustAnchorSignature under each required algorithm against the anchor's keys, over the certificate's recomputed signed region. Production requires both Ed25519 and ML-DSA-65, which is why a certificate carries more than one anchor signature and MAX_TRUST_ANCHOR_SIGNATURES is four.

The two signing layers

Reading the two schemas together, trust flows down exactly two signature layers, and the kernel trusts only the bottom of them a priori:

  trust anchor (baked into the kernel)
      signs -> NØNOS-ID certificate            (trust_anchor_signatures)
                   carries publisher_keys, allowed_caps_ceiling, namespace_globs
                   signs -> capsule manifest    (publisher_signatures)
                                binds payload_hash -> the ELF

What verification reads

Stage one (nonos_id_cert/verify/checks.rs) reads trust_anchor_epoch to reject a certificate issued under a policy epoch older than the anchor's current one, reads cert_serial and nonos_id against the anchor's revocation lists, and reads valid_from_ms and valid_until_ms against the supplied time. Only after those pass does it verify the trust_anchor_signatures. The verified output is a small copy carrying just the three fields the rest of spawn needs:

  VerifiedNonosId
    nonos_id              [u8; 32]
    cert_serial           u64
    allowed_caps_ceiling  u64

The full pipeline that consumes this is the verified-spawn gate; the anchor it verifies against is the trust anchor; the manifest it authorises is the manifest schema.

Debugging a certificate rejection

Every certificate failure is an IdCertVerifyError (src/security/nonos_id_cert/error.rs:44), and at the loader it collapses to the single reason id_cert on the [RUNTIME-LOAD] FAILED line, so the reason string alone does not tell you which check fired. The variant does, and the two decode paths are worth separating.

A structural rejection is an IdCertDecodeError wrapped in Decode and comes from the bounded decoder refusing a malformed certificate: SchemaVersion when the schema_version is not the 2 this kernel accepts, NamespaceGlobCount or PublisherKeyCount or TrustAnchorSignatureCount when a vector exceeds its named maximum (MAX_NAMESPACE_GLOBS, MAX_PUBLISHER_KEYS, MAX_TRUST_ANCHOR_SIGNATURES), PublisherKeysPerAlg when one algorithm carries more than the two keys MAX_KEYS_PER_ALG allows, and PubkeyLen or SigLen when a key or signature does not have the length its algorithm requires. These mean the certificate is the wrong shape, not that its signature is wrong, and they are the errors to expect from a truncated or hand-edited certificate blob.

A policy rejection is one of the verification variants and each names a real state: EpochStale for a certificate issued under an anchor epoch older than the current one (a rollback), Revoked and NonosIdRevoked for a serial or publisher identity on an anchor list, NotYetValid and Expired for a time outside the validity window, TrustAnchorPolicy when no anchor key exists for the required algorithm, and TrustAnchorBadSig(alg) when a signature under that algorithm did not verify. The order of these is fixed in checks::run and the epoch, revocation, and time checks all run before any signature (verify/checks.rs:22), so a Revoked or EpochStale is decided without the cost of the anchor signature verify, and seeing one does not imply the signature was even reached. TrustAnchorBadSig is the one that means a well-formed, in-window, unrevoked certificate simply carried a signature that did not check out against any anchor key for that algorithm, which is the genuine "wrong key or tampered bytes" case.

Source map

  src/security/nonos_id_cert/schema/cert.rs       NonosIdCertificate, VerifiedNonosId
  src/security/nonos_id_cert/schema/constants.rs  the schema version and bounds
  src/security/nonos_id_cert/schema/sub.rs        NamespaceGlob, PublisherKey, TrustAnchorSignature
  src/security/nonos_id_cert/schema/glob_match.rs the namespace glob matcher
  src/security/nonos_id_cert/verify/checks.rs     epoch, revocation, validity
  src/security/nonos_id_cert/verify/dispatch.rs   the per-algorithm anchor signature check
  src/security/nonos_id_cert/error.rs             IdCertDecodeError and IdCertVerifyError

The anchor these signatures reduce to is the trust anchor; the pipeline that maps these errors onto the [RUNTIME-LOAD] reason=id_cert line is on the verified spawn page.