# secmngr — Design

- **Component:** `secmngr` — a bbfdm dm-service plugin (`libsecmngr.so`) implementing
  TR-181 2.21 `Device.Security.`
- **Scope of this document:** what `secmngr` does, how it stores and manages certificate
  and CA-bundle material, its operation semantics, upgrade/security posture, and current
  limitations. It intentionally does not describe bbfdm plugin internals (data-model
  registration, dmmap caching mechanics, etc.) — see the code for that.

## 1. Overview and rationale

`secmngr` is the device's single authority for X.509 certificate and key material, exposed
through the TR-181 `Device.Security.` data model. It gives a unified, spec-compliant view of
every installed certificate, commands to add/delete certificates and CA bundles, and a
standard output (`GetCertificateURI()`) that any TLS consumer can point its stack at.

It exists to replace scattered, per-application provisioning. Today `opconf`'s onboarding
handlers decode certs from the config JSON and write them to fixed per-app paths, then set
each consumer's UCI:

| Provisioner | Writes to | Sets |
|---|---|---|
| `opconf` `01_ca_certificates.sh` | `/etc/ssl/certs/<hash>.0`, appends `/etc/ssl/cert.pem` | system trust store |
| `opconf` `40_management.sh` (CWMP) | `/etc/icwmpd/cwmp_ca_cert.pem`, `cwmp_client.pem`, `cwmp_client.key` | `cwmp.acs.ssl_capath`, `cwmp.cpe.client_cert_path` |
| `opconf` `40_management.sh` (USP) | `/etc/obuspa/rootca.pem`, `client.pem` | `obuspa.global.trust_cert`, `client_cert` |

Each consumer then loads from its own location with its own API (icwmp via libcurl, obuspa
via `X509_STORE`, fluent-bit via `tls.*`). `secmngr` centralises the lifecycle and the
storage so consumers reference a data-model object instead of a hand-provisioned path.

## 2. What it manages

`secmngr` tracks two kinds of objects:

- **Certificates** (`Device.Security.Certificate.{i}.`) — an end-entity cert (optionally
  with a private key) or a CA cert, each backed by one PEM file on disk.
- **CA bundles** (`Device.Security.CABundle.{i}.`) — a named, ordered group of Certificate
  rows, used by TLS consumers as a trust anchor set.

Certificates come from two sources, merged into one inventory:

- the **system trust store** (`/etc/ssl/certs`, populated by the `ca-certificates` package
  and operator onboarding) — always present, always read-only to `secmngr`;
- **data-model-added** certificates, under `/etc/secmngr/` — created by
  `AddCertificateBundle()` (cert + private key) or as a side effect of `AddCABundle()`
  (CA certs, no key).

A certificate's identity is its `SerialNumber` + `Issuer` pair (per spec, the functional
unique key for a `Certificate` row), not its file path or on-disk name. This identity drives
de-duplication: adding a cert that's already tracked — whether discovered independently,
added directly, or already a member of another bundle — reuses the existing row instead of
creating a second one.

System-generated rows (the trust-store certs and the aggregate system CA bundle) are
read-only: a write or `Delete()` against one is accepted and reports success, but has no
effect — this keeps automation that blindly walks every row from failing on the entries it
can't touch, while guaranteeing the system trust anchors can never be altered through the
data model.

## 3. On-disk storage

Two tiers, split cleanly by ownership:

| Tier | Location | Owner | Written by |
|---|---|---|---|
| System trust store | `/etc/ssl/certs`, `/etc/ssl/cert.pem` | `ca-certificates` / operator onboarding | never `secmngr` |
| Managed material | `/etc/secmngr/{certs,private,cabundles}` | `secmngr` | `AddCertificateBundle()`, `AddCABundle()` |

Under `/etc/secmngr/`:

| Source | On-disk | Notes |
|---|---|---|
| `AddCertificateBundle()` | cert → `certs/<serial>_<issuer-hash>.pem`; key → `private/<serial>_<issuer-hash>.key` (mode **0600**, dir **0700**) | filename is derived from the cert's own identity, so re-adding the same cert overwrites its own files in place |
| `AddCABundle()` | each member cert → `certs/<serial>_<issuer-hash>.pem`; the full submitted blob is also kept as `cabundles/<name>.pem` | member certs are ordinary `Certificate` rows — deduplicated against everything already tracked, not private to the bundle |

The base directory (`/etc/secmngr`) is fixed rather than configurable. In practice this is
a simplicity/robustness trade rather than a gap: one well-known location keeps upgrade
preservation (§6) unambiguous, with no risk of a misconfigured path silently breaking trust.

The combined bundle file (`cabundles/<name>.pem`) is a convenience snapshot of a bundle's
members at the time `AddCABundle()` ran; the `secmngr` init script reconciles it against the
bundle's current member list at boot and on every UCI reload (§6), so it self-heals if a
member is later removed independently rather than staying silently stale.

## 4. Operations

### 4.1 `AddCertificateBundle(Alias?, Name, BundleFormat, Bundle)` → `Certificate`
Accepts a PEM blob containing one certificate and one private key (in either order); an
encrypted key is decrypted using the `pkcs12_password` UCI option if set. On success:

- if the certificate's identity (`SerialNumber`+`Issuer`) already matches a tracked,
  `secmngr`-owned entry with the **same** `Name` → replaces it in place, reusing its file
  paths (including any previously-tracked key);
- if it matches an existing entry with a **different** `Name`, or matches a system-store
  cert, → rejected as an identity conflict (spec fault 7270), rather than silently taking
  over someone else's entry;
- otherwise → written as a new `Certificate` row.

`Name` must also be globally unique across all certificates (spec requirement); reusing a
name already held by a different certificate is likewise rejected.

### 4.2 `AddCABundle(Name, Bundle)` → `CABundle`, `NumberOfCertificates`
Accepts one or more concatenated PEM CA certificates. Each is installed as its own
`Certificate` row (deduplicated by identity as above — a cert shared with another bundle,
or already in the system store, is referenced rather than duplicated), and the bundle
records membership as a reference list. `Name` must not collide with any existing bundle
(fault 7267).

### 4.3 `Certificate.{i}.Delete()`
Removes a `secmngr`-owned cert's file(s) and tracking entry, and prunes it from any CA
bundle that referenced it. A `Delete()` against a system-store cert is a no-op that reports
success (§2).

### 4.4 `CABundle.{i}.Delete()`
Removes the bundle's own record and its persisted combined-blob file. Member certificates
are left untouched — each remains an independent `Certificate` row until deleted on its own.

### 4.5 `GetCertificateURI()`
Returns a `file://` URI for the certificate, and — only for certs that carry a tracked
private key — a `file://` URI for the key.

## 5. Configuration

`/etc/config/secmngr` `globals` section:

| Option | Default | Purpose |
|---|---|---|
| `pkcs12_password` | (empty) | password to decrypt an encrypted private key supplied to `AddCertificateBundle()` (name kept for compatibility; PKCS#12 bundles themselves aren't accepted as of now — see §7) |

`SECMNGR_SKIP_SYSTEM_CERTS` (build-time option, `Config.in` in the feed package): excludes
the system trust store and its aggregate CA bundle from the data model entirely, exposing
only data-model-added certificates and bundles.

## 6. Upgrade preservation

Everything under `/etc/secmngr/` (certs, keys, CA bundles) must survive a firmware upgrade.
Because the exact upgrade path a device takes depends on its `ConfigScope` setting
(an opconf/sysmngr concept — `UserOnly` vs `All`), preservation is registered in **both**
places sysmngr can take:

- `/lib/upgrade/keep.d/secmngr` — covers the `ConfigScope=All` path; preserves
  `/etc/secmngr` wholesale, permissions included.
- `/etc/userconf/keep.d/userconf.conf` (opconf's shared drop-in list) — covers the default
  `UserOnly` path, via explicit globs (`/etc/secmngr/certs/*`, `/etc/secmngr/private/*`,
  `/etc/secmngr/cabundles/*`) since this path only copies individual regular files, never
  directories.

The `secmngr` init script also reconciles each CA bundle's combined file against its current
member list at boot and on reload (§3), so a bundle restored across an upgrade is verified
consistent rather than trusted blindly.

## 7. Current limitations

These are deliberate simplifications for the current stage of the component, each with a
clear path forward rather than an open-ended gap:

- **PEM only, no PKCS#12.** `AddCertificateBundle()`/`AddCABundle()` accept PEM bundles
  only. This keeps the parsing path small and predictable; PKCS#12 support can be reinstated
  behind the existing `BundleFormat` argument without any data-model change.
- **Fixed storage base path.** `/etc/secmngr` is not UCI-configurable (§3) — a deliberate
  trade for predictability over flexibility that isn't currently needed.

None of these affect the read/write split between the system trust store and
`secmngr`-managed material (§3), which is enforced unconditionally.

## 8. Security considerations

- Private keys are written `0600` under `/etc/secmngr/private/` (dir `0700`) and are the
  only world-unreadable material; the upgrade-preservation paths (§6) must not weaken this.
- The system trust store (`/etc/ssl`) is read-only to `secmngr`; no data-model write can
  ever alter the anchors other services trust — enforced at the operation level (§2, §4.3),
  not just by filesystem permissions.
- `pkcs12_password` sits in UCI in the clear — acceptable for import-time decryption of a
  private key.
- Identity-conflict checks (§4.1) prevent one certificate from silently overwriting a
  different logical certificate that happens to share a serial number or on-disk name.
