Module Documentation Index » Crypto Module

Crypto Module

src/crypto is the device's identity and signature core. It holds the device keypair, manages a small keyring of trusted public keys, provides the sign/verify primitives that the rest of the firmware uses, and decides — based on key flags — who is allowed to do what.

Regulatory context first

PacketRF is built for amateur radio operation. On amateur bands, encrypting payload traffic is generally prohibited; authentication is generally not. The crypto module reflects that exactly: it does signatures, not confidentiality. Digital signatures provide authentication, integrity and command authorization without encrypting any payload, which is precisely the part of cryptography amateur regulations leave open.

If PacketRF ever leaves the amateur context — for non-amateur deployments where confidentiality matters — encryption becomes a sensible addition. That can happen later as an optional extension without changing the signature API contract. The point of the current design is not "we cannot do encryption"; it is "we deliberately do not do encryption, because doing it would be wrong here".

Primitives

The module uses Monocypher for the crypto primitives. The selected algorithms are:

  • Ed25519 for digital signatures.
  • BLAKE2b for key fingerprint derivation.

For clarity: X25519 is key exchange and is not used for signatures here — signatures are Ed25519 only.

What the module owns

The crypto module is responsible for the device keypair lifecycle, the trusted public key database with metadata, the sign/verify API that other modules call into, helpers for authorization decisions on admin operations, and the persistence interface for both the private key material and the keyring.

It does not own CoAP transport, CBOR parser internals, or NPR frame formats. Those live elsewhere. The crypto module is a clearly delimited trust core, not a grab bag of "anything security-adjacent".

Storage

Persistence uses LittleFS. The proposed file layout:

  • /sec/device_sk.bin — private key material.
  • /sec/device_pk.bin — cached device public key (optional, can be derived).
  • /sec/keyring.cbor — trusted public keys with metadata.

The private key must never be exposed by control endpoints; the public key and key metadata are exposed for management. Without a secure element, physical extraction of the private key remains a real risk. That is accepted in phase 1 and documented honestly in the security limitations section below; future hardware revisions are expected to address it with a secure element.

Device identity

A device has exactly one local asymmetric identity:

  • a device private key (local only),
  • a device public key (exportable),
  • a device key identifier key_id derived from the public key.

key_id is BLAKE2b-128(public_key), encoded as lowercase hex for tooling and logs. It is the value that ends up as kid in COSE protected headers.

The trusted keyring

The keyring is a list of trusted public keys with metadata. Each entry carries at least:

  • key_id,
  • alg (currently ed25519),
  • pubkey,
  • flags (bitfield),
  • label (optional operator-readable name).

The current flags:

  • trusted — the device is allowed to rely on this key for trust decisions.
  • admin — the key is authorized for configuration writes.
  • npr_peer — marks a key as belonging to another NPR modem, for NPR-specific policy.

trusted is intentionally separate from npr_peer. A key may be discovered from NPR signaling and stored locally as metadata without yet being trusted; an operator can then review it and promote it to trusted status through the control interface. This two-step model is what lets the keyring grow without quietly trusting whatever the network sends.

The current revocation model is delete-only. There are no validity intervals or certificate chains in phase 1.

Signing envelope rules

Control payloads are CBOR. When the protocol layer needs an inline-signed CBOR map (rather than a COSE_Sign1 wrapping, which is what /mgmt actually uses today), the convention is:

  • the signature field is at unsigned integer key 255,
  • all other protocol keys are unsigned 8-bit values,
  • with canonical CBOR ordering, key 255 is naturally the last entry in the map.

The "to be signed" (TBS) byte sequence is the canonical CBOR encoding of the message map without key 255. Signing builds the map without the signature, encodes canonical CBOR, signs the TBS bytes with the device private key, appends key 255, and re-encodes. Verification reverses the steps: parse, extract the signature, rebuild the map without it, encode canonical CBOR TBS, verify against the sender's public key.

(The currently active management traffic uses COSE_Sign1 wrapping rather than this inline scheme. The inline scheme is kept in the design because some lower-level use cases — e.g. NPR signaling extensions — may benefit from a smaller envelope than COSE.)

Replay handling

Phase 1 implements nonce-based replay suppression only. Timestamp-window checks are not enforced because no reliable clock is available on a freshly-booted device.

The nonce policy is straightforward: a request carries sender_key_id and nonce; the module keeps a bounded recent-nonce cache per sender key; reuse of the same nonce for the same sender key is rejected. "LRU cache nonce per key" means each sender has a finite list of most recent nonce values, and on capacity limit the oldest is dropped.

This protects against short-term replay while the device is running. After reboot, nonce memory is cleared in phase 1; that is a known limitation and is documented as such.

First-key bootstrap

This is the chicken-and-egg of trust: a config write should require a trusted admin key, but a fresh device has no trusted admin key yet. Two bootstrap modes were considered.

Mode A — open bootstrap. The device starts in an open control mode, the operator writes the first admin key, and policy switches to locked mode. Phase 1 implements this.

Mode B — offline provisioning tool. A host tool prepares a LittleFS image with the keyring and base config, the host writes the prepared image over a USB provisioning path, and the device starts already locked with an admin key in place. This is preferred for production provisioning and is on the roadmap.

The current bootstrap flow is described in the Control Module page; the crypto module is the part that actually verifies the bootstrap-install request and commits the first admin key into the keyring.

Public API sketch

The proposed service contract:

  • device_public_key()
  • device_key_id()
  • sign(message_bytes)
  • verify(message_bytes, signature, key_id)
  • list_keys()
  • add_key(entry)
  • remove_key(key_id)
  • set_key_flags(key_id, flags)
  • is_admin_key(key_id)

Two backend interfaces keep storage replaceable for a future secure element:

  • IPrivateKeyStore
  • IKeyringStore

Control endpoints

The /crypto/* paths under /mgmt:

  • /crypto/device — returns key_id and the device public key.
  • /crypto/keys/list — returns the keyring.
  • /crypto/keys/add — adds a key with initial trusted, admin, and npr_peer flags.
  • /crypto/keys/remove — removes a key.
  • /crypto/keys/set-flags — updates trusted, admin, and npr_peer.
  • /crypto/bootstrap/status — bootstrap state.
  • /crypto/bootstrap/install — first admin key install (only in bootstrap mode; see Control Module).

Phase 1 authorization policy: configuration writes require a valid admin signature, configuration read responses are signed by the device key, and system/healthcheck-style commands are signature-optional and report verification status to the caller.

NPR integration

Future NPR work uses the same crypto module. The first concrete use is authenticated slave admission (NEP-0002): a slave's connect request carries a signed CLIENT_AUTH TLV; the master verifies the signature against its trusted keyring; the master admits only known keys. Later work may sign additional NPR frames if the on-air budget permits.

Security limitations (phase 1)

These are listed openly because hiding them would be worse than naming them.

  • No secure element. The private key is stored in the flash filesystem.
  • No persistent anti-replay state across reboot.
  • No PKI chain or expiration logic.
  • No payload encryption (HAM mode).
  • No timestamp checks without a trusted clock.

These are accepted for phase 1 and on the roadmap to be addressed in later phases.

Test strategy

CBOR canonical encoding vectors live with src/cbor tests, not here. The crypto tests focus on signature, key management, and authorization behavior. Planned groups:

  • device identity initialization and stable key_id derivation,
  • sign/verify happy path with known vectors,
  • verify failure cases (wrong key, modified payload, modified signature),
  • TBS reconstruction tests for inline-signed CBOR maps,
  • keyring CRUD with edge cases and duplicates,
  • admin-flag authorization checks for control writes,
  • nonce replay cache behavior and bounded eviction,
  • corrupted keyring/private key file recovery.

All host-runnable, deterministic, with explanatory comments.

Implementation phases

Phase 1, core library: add src/crypto, integrate Monocypher, implement keygen/sign/verify/fingerprint, implement LittleFS-backed private-key and keyring stores.

Phase 2, control integration: add /crypto/* paths on top of /mgmt, enforce admin signatures for configuration writes, sign configuration read responses.

Phase 3, operational hardening: improve bootstrap workflow and provisioning tooling, add stronger replay policy when a time source exists, prepare the backend abstraction for a secure element.

Phase 4, NPR admission: extend the NPR connect/auth flow using the crypto API, gate network admission by the trusted keyring.

Design summary

The crypto module provides one reusable trust core for the whole firmware. It is signature-first, storage-aware, and transport-agnostic. It respects amateur radio constraints today while leaving a clean path open for future secure hardware and optional encryption modes.