HOVEL // chapter 07
Chapter 07 Part 02 / Runtime Platform

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.

Current-behavior guide. The implementation boundary below follows the current Go domain and daemon code plus the OpenAPI contract. Sections explicitly marked as extension points are not claims about available commands, providers, or API fields.

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.
Never move material between these rails for convenience. Target-created evidence must remain available for remediation. Workspace private keys must remain in custody unless a separately authorized export or provider delivery resolves them.

The model

Logical names point to immutable material
  1. Authority
  2. active CA generation
  3. certificate lineage
  4. immutable leaf generation
  5. Assignment
  6. consumer
  1. Trust set
  2. active / staged trust generation
  3. anchors + intermediates + CRLs
  4. assignment-pinned peer trust
ModelIdentity and contentsLifecycle 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.
Typed states are not all public transitions. The domain includes additional states such as 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

Changing inventory is not the same as changing a consumer
  1. persist issuance plan
  2. generate or reuse key
  3. sign + independently validate
  4. store immutable generation
  5. stage assignment
  6. deliver / test
  7. 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.

ActionWhat changesWhat does not change automaticallyUse 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.

Revocation reaches consumers through an explicit trust generation
  1. revoke generation
  2. degrade / clear assignments
  3. plan CRL + reserve number
  4. sign + checkpoint
  5. validate immutable CRL
  6. stage trust generation
  7. 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.

Durable authority rollover state machine
  1. stage overlap trust; start operation
  2. awaiting-overlap-acknowledgements
  3. activate overlap; previous authority becomes retiring
  4. awaiting-leaf-rotation
  5. rotate required leaves; stage final trust
  6. awaiting-final-acknowledgements
  7. 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

Local custody boundary
Owner-only filesecrets/pki-master-keys.json
AES-256-GCMworkspace-bound key envelope
SQLiteciphertext + authenticated metadata
SQLiteselected active generation
hoveldpolicy + audit + short-lived resolution
Approved sinkexport response or same provider process
  • 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.
Encryption at rest is not a provider sandbox. A process that receives an approved private projection can misuse or log it, and compromise of both the workspace database and master-key file defeats local envelope custody. Providers must use the SDK secret wrappers, keep material in memory only as long as needed, and return non-secret receipts.

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:

BoundaryQuestion it answersCurrent implementationExtension 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 caseBuilt-in profileX.509 identityTLS 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.
Hybrid does not mean a post-quantum certificate. In the implemented profiles, ML-KEM contributes to TLS session key establishment. X.509 authentication remains classical ECDSA, so a future cryptographically relevant quantum computer could still threaten identity signatures even when recorded session traffic received hybrid key-establishment protection. Hovel must not label that chain “fully post-quantum.”

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

AreaImplemented nowNot 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 cleanupCredentialCapabilityIt is operator-visible evidence, not daemon PKI custody.
Give a stable service or Mesh resource an identityCertificate generation + assignmentThe assignment stages and activates exact immutable generations without hiding rollout state.
Change certificate validity while preserving a constrained keyRenewThe new generation reuses the key but still requires explicit assignment activation.
Replace a leaf keyRotateIt guarantees a distinct key and public key in the same certificate lineage.
Replace a root or subordinate CAAuthority rolloverOverlap and final trust plus acknowledgements avoid an atomic-swap trust gap.
Protect TLS session key establishment against harvest-now/decrypt-later riskA pq-hybrid-* profile on compatible Go endpointsHybrid ML-KEM groups protect key establishment, while the documentation remains honest about classical identity.
Support a constrained TLS libraryA versioned compatibility targetConsumer constraints remain independent from the daemon's signing backend.
Keep a key in an HSM or platform storeA future registered crypto backend with an external handleThe typed port exists, but the default daemon does not ship or install that backend today.
Deliver an active assignment into a Mesh provider nowApproved runtime credential selectors on a Mesh mutationThe daemon validates consumer binding and runs delivery and consumption in one provider process.
Patch a generated artifact with credential materialCredential stamp contracts for provider development and testsThe model and provider execution exist, but production external initiation is not yet available.