TLS and Workspace PKI
Hovel's workspace PKI gives one daemon-owned lifecycle to the X.509 credentials used by Mesh providers, listening posts, implants, stagers, services, payloads, and external integrations. It replaces copied PEM strings and provider-specific certificate state with immutable generations, explicit trust, staged assignment changes, revocation, bounded signing authority, and auditable secret use.
This is workspace infrastructure, not a public Web PKI and not a general secret manager. The daemon owns certificate lifecycle and custody; a provider advertises how it can consume selected material; the consuming TLS stack owns the live handshake.
What problem it solves
A workspace certificate is more than a key and a PEM file. Hovel must also know:
- which authority and exact authority generation issued it;
- which profile, crypto backend, compatibility target, and TLS groups were resolved at issuance;
- which logical consumer is serving it and which trust generation that consumer uses;
- whether a new credential is merely staged or has become active;
- whether a generation was revoked and which fresh CRL communicates that fact;
- when a signing key or private-key projection was authorized and used; and
- how a provider received material without copying secrets into durable operation records.
The workspace PKI makes those relationships explicit. A failed issue, delivery, or rollout cannot silently replace a consumer's active credential, and changing a trust set's default does not silently move assignments already pinned to an older immutable trust generation.
Custody is not target-created credential evidence
Hovel uses the word credential for two deliberately different things. Keep their storage and display policies separate.
| Resource | Why it exists | Custody and visibility | Do not do this |
|---|---|---|---|
CredentialCapability |
A target-created account or access secret recorded as chain state and remediation evidence. | Stored and shown to the operator in plaintext by design. sensitive guides export and downstream handling; it is not a CLI-redaction switch. |
Do not treat it as a daemon-held PKI key or assume the operator can recover it after redaction. |
| Workspace PKI key and certificate generation | A daemon-owned identity with issuance, assignment, trust, revocation, and audit history. | Certificate DER, public-key SPKI, fingerprints, and metadata are inspectable. Local private material is envelope-encrypted and is absent from normal list and inspect responses. | Do not copy private-key bytes into a capability, provider config, log, error, or artifact metadata field. |
| Provider delivery | A short-lived projection of one active assignment for one declared provider slot and operation scope. | Bytes, protected paths, and scoped references are transient. Durable execution records retain provider identity, scope, digests, sizes, and non-secret receipts. | Do not return resolved bytes to the external caller or persist provider-owned references verbatim. |
The model
- Authority
- active CA generation
- certificate lineage
- immutable leaf generation
- Assignment
- consumer
- Trust set
- active / staged trust generation
- anchors + intermediates + CRLs
- assignment-pinned peer trust
| Model | Identity and contents | Lifecycle rule |
|---|---|---|
| Authority | A logical root or subordinate CA. It records role, origin, signer mode, profile, export policy, lifecycle state, signer reference, and one active CA generation. | The logical authority survives inspection and rollover bookkeeping. Root creation starts in locked; subordinate creation starts in active. Actual signing still requires a scoped lease. |
| Certificate | A logical lineage represented by a stable certificateId shared by its generations. There is no mutable certificate blob that is overwritten in place. |
Renewal and rotation append generations to the lineage; assignment activation decides which one a consumer serves. |
| Certificate generation | Immutable certificate DER, public-key SPKI, fully resolved template, key ID, issuer IDs, generation number, fingerprints, purpose, backend commitments, compatibility snapshot, key-establishment policy, and TLS named groups. | Current issuance creates an active inventory generation. Revocation changes that generation to revoked; it never edits its issued bytes. |
| Trust set | A logical trust name with active and staged generation pointers, state, and optimistic-concurrency revision. | A trust-set default may move, but existing assignments remain pinned to their exact active trust generation until explicitly restaged and activated. |
| Trust-set generation | An immutable ordered set of anchor generation IDs, intermediate generation IDs, and optional CRL generation IDs. | It must contain at least one anchor. Certificates must be usable CAs; every CRL must be fresh and its issuer generation must be present in the same trust generation. |
| Assignment | A stable binding from a purpose and profile to a typed consumer such as a Mesh listener, node, implant, payload, service, or external integration. It holds active/staged certificate and trust generation pointers. | Stage first, deliver or validate outside the mutation, then activate with the expected revision. Peer-verifying purposes pin the current active trust generation at stage time. |
| Signing lease | A transient grant for one authority, actor, and operation, with grant and expiry times. | Explicit approval is required. The default is five minutes, the maximum is fifteen minutes, and another actor or operation cannot use or revoke the lease. |
| Revocation | An immutable RFC 5280 reason, effective time, original generation state, serial, and exact issuer identity for one certificate generation. | Revoking an actively served generation degrades affected assignments; a staged-only reference is cleared. A self-signed root is distrusted through trust change or rollover, not revoked by itself. |
| CRL generation | One immutable full CRL: authority and issuer generation, monotonically reserved number, sorted revocation IDs, validity window, backend commitment, concrete signature algorithm, fingerprint, and DER. | The current model accepts a five-minute to seven-day window, defaults publication to 24 hours, and caps a full CRL at 100,000 revocations. |
pending, superseded,
expired, invalid, disabled,
compromised, and destroyed. Their presence in an enum does not
imply that a current CLI or daemon method can enter every state.
Leaf lifecycle: issue, stage, activate
- persist issuance plan
- generate or reuse key
- sign + independently validate
- store immutable generation
- stage assignment
- deliver / test
- activate assignment
Issuance persists an idempotent intent before a backend can create a key or sign. The intent commits the exact backends, capability hashes, compatibility target, resolved TLS groups, template, issuer generation, key identity, and generation identity. A retry cannot silently switch to a different backend or policy snapshot.
A newly renewed or rotated generation is active in certificate inventory, but it does not replace an assignment automatically. Stage it on each intended assignment, perform the provider or deployment work, and activate only after that work succeeds. The assignment's previous active pointers remain authoritative until the revision-fenced activation commits.
| Action | What changes | What does not change automatically | Use it when |
|---|---|---|---|
| Renew | Creates a later generation in the same certificate lineage with the same key ID and public key. | No assignment moves to it. | A constrained consumer requires key reuse. |
| Rotate | Creates a later generation in the same lineage with a distinct key ID and public key; a registered backend may be selected. | No assignment moves to it, and the authority is not rolled over. | The normal leaf lifecycle or a key must be replaced. |
| Revoke | Invalidates one non-root generation, records the reason, degrades active assignments, and clears staged-only use in one transaction. | It does not publish or activate a CRL by itself. | A generation must no longer be trusted. |
| Roll over | Replaces an authority through overlap trust, consumer acknowledgements, leaf rotation, final trust, and retirement. | It is not a leaf rotation and cannot be represented as one atomic certificate swap. | A root or subordinate CA identity must change without a trust gap. |
Trust, revocation, and CRL publication
Revocation recording and CRL publication are separate on purpose. Revocation first changes workspace truth synchronously. Publication then builds a durable signing plan from the authority's effective revocation snapshot.
- revoke generation
- degrade / clear assignments
- plan CRL + reserve number
- sign + checkpoint
- validate immutable CRL
- stage trust generation
- activate trust set
CRL publication advances through planned, signing, and
signed phases before a completed immutable CRL generation is stored. An
ownership lease fences concurrent workers; a signed checkpoint lets authorized crash
reconciliation validate and finish without invoking an ambiguous signer twice. These
worker leases are concurrency controls, not signing authorization.
Trust resolution rechecks certificate validity and CRL freshness. An expired CRL makes that trust generation unusable even though the immutable historical record remains inspectable. Automatic CRL refresh scheduling is not implemented, so publication and subsequent trust staging remain explicit operations.
Authority rollover without a trust gap
The daemon API implements rollover as one durable, revision-fenced operation. Root rollover places old and replacement CA generations in the overlap generation's anchor list. Subordinate rollover places them in the intermediate list and preserves the ancestor chain to an anchor.
- stage overlap trust; start operation
awaiting-overlap-acknowledgements- activate overlap; previous authority becomes
retiring awaiting-leaf-rotation- rotate required leaves; stage final trust
awaiting-final-acknowledgements- activate final trust; previous authority becomes
retired
The operation can track all eligible peer-trust assignments, an explicit set, or no Hovel-tracked consumers. Live rollovers reserve the trust set and required assignments so unrelated mutations cannot invalidate the proof. Cancellation is available only before overlap activation; once active trust changes, operators must complete the fenced workflow.
Rollover methods, acknowledgement inspection, and reservation errors are implemented
in the daemon RPC and OpenAPI contract. There are no matching imperative
hovel pki rollover commands today.
Signing leases are authority, not authority state
Authority lifecycle state and permission to use its signing key are independent. An
unlock request grants an in-memory SigningLease scoped to the authenticated
actor and operation. The lease expires automatically, disappears on daemon restart,
and does not rewrite the authority or its CA generation.
- Unlock requires the request's explicit signing-lease approval.
- Every signing use rechecks authority state, lease scope, expiry, CA constraints, generation ownership, validity, and key binding.
- Lock removes only the matching actor and operation's lease; another scope cannot revoke it.
- Key access and signing authorization produce audit events without recording private bytes.
Encryption at rest and secret boundaries
secrets/pki-master-keys.json- Local key material is serialized into a versioned AES-256-GCM envelope. Associated data binds schema, cipher, master-key version, workspace identity, key ID, and algorithm.
- Authority, generation, and issuance JSON remains inspectable metadata; HMAC-SHA-256 tags detect tampering with authenticated persisted records. Encryption is for private key material, not for public certificate inventory.
- The master-key file is the local root of trust. Its keys are base64-encoded, not encrypted by Hovel, so owner, permission, regular-file, symlink/reparse-point, and link-count checks matter.
- Initialization and open are separate. If an initialized workspace loses its master-key file, ordinary open fails closed rather than creating an unrelated key.
- The envelope code supports versioned master-key rotation and rewrap, but no current CLI or daemon RPC method exposes that maintenance operation.
Normal list and inspect methods never return private-key material. The daemon's
ExportPKIBundle response is the one external control response that can:
private export must match the generation's explicit-private-export policy,
carry approvePrivateKeyExport, use the privileged local transport, and is
audited and marked Cache-Control: no-store. For an externally held key, a
bundle carries a provider reference rather than private bytes.
Crypto backends, compatibility targets, and credential providers
These three extension points answer different questions:
| Boundary | Question it answers | Current implementation | Extension status |
|---|---|---|---|
| Crypto backend | How are keys generated and certificates or CRLs signed? | The default daemon registers only builtin-x509 version 1: ECDSA, RSA, and Ed25519 keys/signatures, full CRLs, private export, and custom extensions. It does not advertise import or CSR support. |
The application has typed backend and independent-validator registries, capability hashes, package digests, and external-key handles. There is no backend installation API or bundled HSM/KMS/PKCS #11 backend. |
| Compatibility target | Which certificate algorithms, encodings, and TLS groups can the consumer use? | portable-x509 and go-1.26-pq-hybrid are built in. The selected version and resolved group list are persisted on every generation. |
Mbed TLS, wolfSSL, platform-specific, and other targets can be added as versioned contracts; none is registered today. |
| Credential provider | How does one module process consume an already issued assignment? | Go, Python, and Rust SDKs implement optional credential.describe, runtime, protected-file, provider-encoding, and standard/advanced stamping contracts. |
No bundled production module currently advertises the credential-provider surface. A provider implements only the capabilities and slots it actually supports. |
A crypto backend is privileged daemon infrastructure; a credential provider is a module-side consumer. Selecting a runtime TLS library does not select the issuer, and selecting an issuer does not force a TLS library on the consumer. See credential delivery and stamping and the provider development guide for the plugin boundary.
Classical and post-quantum-hybrid profiles
| Use case | Built-in profile | X.509 identity | TLS key establishment |
|---|---|---|---|
| Root or subordinate CA | root-modern, subordinate-modern |
ECDSA P-256 with a classical ECDSA signature. | not-applicable; CA certificates do not negotiate TLS groups. |
| TLS, mTLS, or dual-role mTLS | tls-server, tls-client, mtls-server, mtls-client, dual-role-mtls |
ECDSA P-256 with a classical ECDSA signature. | classical-compatible using the portable target's classical groups. |
| Legacy RSA endpoint | legacy-rsa-server, legacy-rsa-client |
RSA 2048 with a classical RSA signature. | classical-compatible. |
| Hybrid-protected TLS, mTLS, or dual-role mTLS | pq-hybrid-tls-server, pq-hybrid-tls-client, pq-hybrid-dual-role-mtls |
Still ECDSA P-256 with a classical ECDSA identity signature. | hybrid-pq-required with x25519-mlkem768, secp256r1-mlkem768, and secp384r1-mlkem1024 under the Go 1.26 compatibility target. |
The domain reserves ML-DSA key and signature identifiers and a
hybrid-pq-preferred policy for future capability negotiation. The default
backend and current compatibility targets do not advertise ML-DSA, and no built-in
profile selects the preferred-with-classical-fallback policy. Those are extension
points, not current provider support.
A minimal operator workflow
The imperative CLI is daemon-backed. The following uses only current commands. Replace angle-bracket values with IDs returned by the preceding JSON/list operations; keep idempotency keys stable only when retrying the same request.
# Discover or initialize local custody.
hovel pki status --workspace .hovel --json
hovel pki init --workspace .hovel --yes
hovel pki backend list --workspace .hovel
hovel pki profile list --workspace .hovel
# Create a lab root, then read its generated authority ID.
hovel pki authority create "Lab Root" \
--workspace .hovel \
--role root \
--profile root-modern \
--idempotency-key lab-root-v1 \
--yes
hovel pki authority list --workspace .hovel --json
# Grant this CLI actor/operation a short signing lease and issue one leaf.
hovel pki authority unlock <authority-id> \
--workspace .hovel \
--duration 5m \
--approve
hovel pki certificate issue listener.internal \
--workspace .hovel \
--issuer <authority-id> \
--profile tls-server \
--idempotency-key listener-cert-v1 \
--yes
hovel pki certificate list --workspace .hovel --json
# Bind a stable consumer slot, then stage and activate the returned generation.
hovel pki assignment bind mesh-provider/listener-west \
--workspace .hovel \
--id assignment-listener-west \
--consumer-type mesh-listener \
--purpose tls-server \
--profile tls-server \
--idempotency-key listener-assignment-v1 \
--yes
hovel pki assignment stage assignment-listener-west <generation-id> \
--workspace .hovel \
--revision 1 \
--idempotency-key listener-stage-v1 \
--yes
hovel pki assignment inspect assignment-listener-west --workspace .hovel --json
hovel pki assignment activate assignment-listener-west \
--workspace .hovel \
--revision 2 \
--idempotency-key listener-activate-v1 \
--yes
hovel pki authority lock <authority-id> --workspace .hovel
Revision values above match a new assignment's first stage and activation only. In
automation, inspect immediately before each mutation and send the observed revision;
stale revisions must fail. A plain tls-server assignment does not verify a
peer and may omit trust. TLS-client and mTLS purposes require an active trust set, and
staging pins its exact active trust generation alongside the certificate generation.
Assignment-backed delivery into Mesh
The current external delivery path is deliberately narrow and implemented on
StartMeshListener, RunMeshTask,
OpenMeshStream, and OpenMeshBridge. The caller sends non-secret
assignment, slot, projection, and form selectors plus a credential request context
with approveCredentialUse: true. The daemon derives the allowed provider,
listener, and node consumer bindings; verifies the active assignment and the provider's
exact descriptor; resolves material; and invokes delivery and the consuming Mesh method
in the same provider process.
POST /hovel.daemon.v1.DaemonService/StartMeshListener
Content-Type: application/json
{
"moduleId": "lab-relay-mesh@v1.0.0",
"request": {
"listenerId": "listener-west",
"name": "west TLS listener",
"kind": "https-beacon",
"deployment": "separate",
"management": "provider",
"config": { "bind": "127.0.0.1:8443" }
},
"credentials": [
{
"requestId": "listener-west-cert-v1",
"assignmentId": "assignment-listener-west",
"slotName": "listener-certificate",
"capability": "runtime",
"material": {
"projection": "certificate-der",
"form": "public"
}
},
{
"requestId": "listener-west-private-key-v1",
"assignmentId": "assignment-listener-west",
"slotName": "listener-private-key",
"capability": "runtime",
"material": {
"projection": "private-key-pkcs8",
"form": "private-bytes"
}
}
],
"credentialContext": {
"actorId": "operator-web",
"operationId": "start-listener-west-v1",
"correlationId": "change-listener-west-v1",
"approveCredentialUse": true
}
}
This request is valid only if the provider advertises the exact runtime slot and the
active assignment is bound to the listener operation. The production selector accepts
only runtime certificate-der/public,
public-key-spki/public, and
private-key-pkcs8/private-bytes combinations. It never returns
resolved material to the HTTP caller. One assignment-and-slot pair may appear only once
in a request, so a TLS identity must use distinct certificate and private-key slots for
its certificate-der/public and
private-key-pkcs8/private-bytes selections.
What is implemented, and what is not
| Area | Implemented now | Not implemented or not externally initiable |
|---|---|---|
| Custody and issuance | Workspace initialization; owner-only file master keys; AES-256-GCM key envelopes; authenticated metadata; built-in X.509 generation and independent validation; durable idempotent issuance. | PKI import, CSR workflows, public master-key maintenance commands, and backend installation. |
| Lifecycle | Authority create/list/inspect; scoped unlock/lock; leaf issue/renew/rotate/revoke; assignment and trust-set create/stage/activate; CRL publish/list/inspect. | Automatic renewal/rotation/CRL scheduling. A non-empty assignment rotationPolicyId is currently rejected. |
| Daemon-only control | Bundle export, issuance and CRL reconciliation, authority rollover and acknowledgements, operation inspection, plus credential stamp/execution list and inspect. | Equivalent imperative CLI commands for export, reconciliation, rollover, stamp initiation, or credential-execution initiation. |
| Mesh credential use | Approved assignment-backed runtime selection for listener start, task, stream, and bridge operations; same-process delivery; non-secret execution bookkeeping. | External selection of bundles, chains, trust, CRLs, signer references, protected files, or provider encodings; general non-Mesh consumption. |
| Provider SDK | Go, Python, and Rust descriptor, runtime, protected-file, encoding, and standard/advanced stamp contracts and dispatch; secret-redacting wrappers; provider runner execution. | A production external endpoint that drives the complete stamp plan → resolve → provider → ingest → terminal transition flow, and a bundled production credential-provider module. |
| Automation clients | Stable local daemon HTTP/JSON RPC and source-backed OpenAPI; the in-tree Go daemon client used by front ends. | PKI manifests, pki plan/pki apply, a PKI wizard, MCP PKI tools, and published daemon-control SDKs. The generated Go/Python/Rust SDK references document module/provider SDKs, not a public PKI control client. |
| Post-quantum scope | Go TLS group mapping and hybrid-required built-in endpoint profiles using ML-KEM key establishment with classical X.509 identities. | ML-DSA identity issuance, a fully post-quantum certificate chain, or registered Mbed TLS/wolfSSL hybrid compatibility targets. |
Decision guide
| If you need to... | Use... | Because... |
|---|---|---|
| Record a target account for cleanup | CredentialCapability | It is operator-visible evidence, not daemon PKI custody. |
| Give a stable service or Mesh resource an identity | Certificate generation + assignment | The assignment stages and activates exact immutable generations without hiding rollout state. |
| Change certificate validity while preserving a constrained key | Renew | The new generation reuses the key but still requires explicit assignment activation. |
| Replace a leaf key | Rotate | It guarantees a distinct key and public key in the same certificate lineage. |
| Replace a root or subordinate CA | Authority rollover | Overlap and final trust plus acknowledgements avoid an atomic-swap trust gap. |
| Protect TLS session key establishment against harvest-now/decrypt-later risk | A pq-hybrid-* profile on compatible Go endpoints | Hybrid ML-KEM groups protect key establishment, while the documentation remains honest about classical identity. |
| Support a constrained TLS library | A versioned compatibility target | Consumer constraints remain independent from the daemon's signing backend. |
| Keep a key in an HSM or platform store | A future registered crypto backend with an external handle | The typed port exists, but the default daemon does not ship or install that backend today. |
| Deliver an active assignment into a Mesh provider now | Approved runtime credential selectors on a Mesh mutation | The daemon validates consumer binding and runs delivery and consumption in one provider process. |
| Patch a generated artifact with credential material | Credential stamp contracts for provider development and tests | The model and provider execution exist, but production external initiation is not yet available. |