Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

kipuka (Hawaiian) — an area of older land surrounded by younger lava flows; an island of stability in a landscape of constant change.

kipuka is a Rust-based EST (Enrollment over Secure Transport) server that issues and renews X.509 certificates at scale. It targets environments where compliance, high availability, and hardware-backed key protection are non-negotiable: government enclaves, regulated enterprise networks, IoT device fleets, and zero-trust architectures.

What kipuka does

  • Certificate enrollment and renewal — full RFC 7030 EST implementation including /cacerts, /simpleenroll, /simplereenroll, /serverkeygen, /fullcmc, and /csrattrs.
  • Multi-CA high availability — route enrollment requests to different Certificate Authorities based on EST labels, with automatic failover.
  • HSM integration — PKCS #11 support via cryptoki for hardware-backed CA signing keys (Thales Luna, YubiHSM 2, SoftHSM, and others).
  • One-time password enrollment — generate and validate OTPs through the admin API for initial device bootstrapping.
  • Profile-based routing — EST labels map incoming requests to specific CA configurations, certificate profiles, and policy sets.
  • Audit logging — structured, tamper-evident logs suitable for NIAP and CA/Browser Forum audit requirements.
  • Dogtag PKI back-end — delegate signing to a Red Hat Certificate System / Dogtag PKI instance when full CA lifecycle management is needed.
  • CoAP transport — constrained-device enrollment over RFC 7252 (CoAP) for IoT environments with limited bandwidth.

What kipuka does not do

  • Full CA lifecycle management — kipuka is an enrollment front-end, not a complete CA. It delegates signing to local key material, an HSM, or a back-end CA such as Dogtag PKI. It does not manage CRL publication, OCSP responders, or CA key ceremonies.
  • ACME — kipuka implements EST, not ACME (RFC 8555). Use a dedicated ACME server if your clients speak that protocol.
  • Certificate transparency — kipuka does not submit pre-certificates to CT logs. Pair it with a CT-aware CA if your trust model requires it.
  • End-entity key management — private keys for enrolled devices are generated client-side (or via /serverkeygen). kipuka never stores end-entity private keys beyond the lifetime of a single request.

Technology stack

ComponentCrate / LibraryRole
LanguageRust (edition 2021)Memory safety, performance, fearless concurrency
HTTP frameworkaxumAsync request routing and middleware
TLSrustlsTLS 1.2/1.3 termination with certificate-based client auth
DatabasesqlxAsync database access (SQLite, PostgreSQL)
ASN.1 / X.509syntaDER/BER encoding, CSR parsing, certificate construction
PKCS #11cryptokiHSM integration for hardware-backed signing

Standards implemented

kipuka targets conformance with the following specifications:

StandardScope
RFC 7030Enrollment over Secure Transport (EST)
RFC 8951Clarifications and updates to EST
RFC 5272Certificate Management over CMS (Full CMC)
RFC 8739Short-term, automatically renewed certificates
RFC 7252Constrained Application Protocol (CoAP) transport
CA/Browser Forum Baseline RequirementsTLS certificate issuance policy
NIAP CA Protection Profile v2.0Common Criteria for Certificate Authorities
FIPS 140-3Cryptographic module validation (via HSM)

Who this documentation is for

This book is organized for three audiences:

  1. Operators — you deploy, configure, and maintain kipuka in production. Start with the Quick Start and then read the Operator Guide for the full configuration reference, HA setup, HSM integration, and audit logging.

  2. API integrators — you write client software that enrolls certificates through kipuka. The API Reference documents every endpoint, request format, and response code. The Your First Certificate walkthrough gives you a working example in five minutes.

  3. Contributors — you want to build kipuka from source, run the test suite, or submit patches. The Developer Guide covers the workspace layout, architecture decisions, database migrations, and contribution process.

Quick navigation

I want to …Start here
Run kipuka in a container in under two minutesInstallation
Issue my first certificateYour First Certificate
Understand every configuration knobConfiguration Reference
Connect an HSMHSM Integration
Set up multi-CA high availabilityHigh Availability
Integrate with Dogtag PKIDogtag PKI Integration
Review RFC conformance detailsRFC Support Reference
Prepare for a NIAP evaluationNIAP CA Protection Profile
Read the EST API specificationEST Endpoints
Build from source and run testsDevelopment Setup

Installation

This page covers every way to get kipuka running: pulling a pre-built container image, building from source, and installing as a systemd service.

Prerequisites

RequirementMinimum versionNotes
Rust toolchain1.88+Only needed when building from source
OpenSSL dev headers1.1.1+ or 3.xNeeded for the build; not linked at runtime (kipuka uses rustls)
SQLite or PostgreSQLSQLite 3.35+ / PG 14+Database for OTP state and audit records

Container (fastest)

Pre-built images are published to quay.io for both x86_64 and aarch64:

# x86_64 (default)
podman pull quay.io/kipuka/kipuka:latest

# Apple Silicon / ARM servers
podman pull quay.io/kipuka/kipuka:latest-arm64

Run the container with a bind-mounted configuration directory:

podman run -d \
  --name kipuka \
  -p 9443:9443 \
  -v /etc/kipuka:/etc/kipuka:ro \
  -v /var/lib/kipuka:/var/lib/kipuka:rw \
  quay.io/kipuka/kipuka:latest \
  kipuka --config /etc/kipuka/kipuka.toml

The container image ships a minimal filesystem. All state lives in /var/lib/kipuka (database, OTP records) and all configuration is read from /etc/kipuka. TLS certificates and CA key material are expected under /etc/kipuka/tls/ and /etc/kipuka/ca/ respectively.

Tip: For Kubernetes or OpenShift deployments, mount the configuration as a ConfigMap and secrets (TLS keys, CA keys) as Secret volumes.

Building from source

Clone the repository and build in release mode:

git clone https://codeberg.org/czinda/kipuka.git
cd kipuka
cargo build --release

The workspace contains six crates:

CratePurpose
kipuka-estCore EST server, HTTP handlers, TLS, database
kipuka-hsmPKCS #11 / HSM integration via cryptoki
kipuka-otpOne-time password generation and validation
kipuka-utilShared utilities (ASN.1 helpers, configuration parsing)
kipuka-dogtagDogtag PKI back-end connector
kipuka-coapCoAP (RFC 7252) transport layer

The final binary is at target/release/kipuka.

OS-specific build dependencies

Fedora / RHEL / CentOS Stream

sudo dnf install openssl-devel clang cmake pkg-config

Debian / Ubuntu

sudo apt install libssl-dev clang cmake pkg-config

macOS

brew install openssl cmake
export OPENSSL_DIR=$(brew --prefix openssl)

Installing the binary

Copy the release binary to a location on $PATH:

sudo cp target/release/kipuka /usr/local/bin/
sudo chmod 755 /usr/local/bin/kipuka

Verify the installation:

kipuka --version

systemd service

Create a dedicated service account:

sudo useradd -r -s /sbin/nologin -d /var/lib/kipuka kipuka
sudo mkdir -p /var/lib/kipuka /var/log/kipuka /etc/kipuka
sudo chown kipuka:kipuka /var/lib/kipuka /var/log/kipuka

Install the unit file at /etc/systemd/system/kipuka.service:

[Unit]
Description=kipuka EST enrollment server
Documentation=https://codeberg.org/czinda/kipuka
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=kipuka
Group=kipuka
ExecStart=/usr/local/bin/kipuka --config /etc/kipuka/kipuka.toml
Restart=on-failure
RestartSec=5s

# Security hardening
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/kipuka /var/log/kipuka
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=kipuka

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now kipuka
sudo systemctl status kipuka

Note: The CAP_NET_BIND_SERVICE capability allows kipuka to bind to port 443 without running as root. If you run on a high port (e.g., 9443) you can remove both CapabilityBoundingSet and AmbientCapabilities lines.

Running tests

The full test suite runs against an in-memory SQLite database and does not require any external services:

cargo test

To run tests for a specific crate:

cargo test -p kipuka-est
cargo test -p kipuka-hsm

Integration tests that require a running EST server are gated behind a feature flag:

cargo test --features integration

Next: First Run walks you through creating a minimal configuration and starting the server.

First Run

This guide takes you from a freshly installed kipuka binary to a running EST server responding to /cacerts requests. By the end you will have a working server with a self-signed CA suitable for development and testing.

Step 1: Create directories and service account

sudo mkdir -p /etc/kipuka/{tls,ca}
sudo mkdir -p /var/lib/kipuka
sudo mkdir -p /var/log/kipuka
sudo useradd -r -s /sbin/nologin -d /var/lib/kipuka kipuka
sudo chown kipuka:kipuka /var/lib/kipuka /var/log/kipuka
PathPurpose
/etc/kipuka/tls/Server TLS certificate and private key
/etc/kipuka/ca/CA certificate and signing key
/var/lib/kipuka/Database files, OTP state
/var/log/kipuka/Audit logs (when file-based logging is enabled)

Step 2: Generate test certificates

The repository includes a helper script that creates a complete test PKI:

./contrib/local-dev/setup-ca.sh

This generates a root CA, a server TLS certificate, and a client certificate under contrib/local-dev/pki/. Copy the relevant files:

sudo cp contrib/local-dev/pki/ca.pem /etc/kipuka/ca/ca.pem
sudo cp contrib/local-dev/pki/ca-key.pem /etc/kipuka/ca/ca-key.pem
sudo cp contrib/local-dev/pki/server.pem /etc/kipuka/tls/server.pem
sudo cp contrib/local-dev/pki/server-key.pem /etc/kipuka/tls/server-key.pem

Manual certificate generation

If you prefer to create certificates by hand:

# Root CA
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout /etc/kipuka/ca/ca-key.pem \
  -out /etc/kipuka/ca/ca.pem \
  -days 3650 -nodes \
  -subj "/CN=kipuka Test CA"

# Server TLS certificate
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout /etc/kipuka/tls/server-key.pem \
  -out /tmp/server.csr -nodes \
  -subj "/CN=localhost"

openssl x509 -req -in /tmp/server.csr \
  -CA /etc/kipuka/ca/ca.pem \
  -CAkey /etc/kipuka/ca/ca-key.pem \
  -CAcreateserial \
  -out /etc/kipuka/tls/server.pem \
  -days 365 \
  -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1")

rm /tmp/server.csr

Set restrictive permissions on key material:

sudo chmod 600 /etc/kipuka/ca/ca-key.pem /etc/kipuka/tls/server-key.pem
sudo chown kipuka:kipuka /etc/kipuka/ca/* /etc/kipuka/tls/*

Step 3: Write a minimal configuration

Create /etc/kipuka/kipuka.toml:

[server]
listen = "0.0.0.0:9443"

[tls]
cert = "/etc/kipuka/tls/server.pem"
key = "/etc/kipuka/tls/server-key.pem"

[tls.client_auth]
# Trust anchor for EST client certificate authentication.
# Clients presenting a certificate signed by this CA are
# permitted to re-enroll without an OTP.
trust_anchors = ["/etc/kipuka/ca/ca.pem"]

[db]
# SQLite for development.  Use a PostgreSQL URL for production.
url = "sqlite:///var/lib/kipuka/kipuka.db?mode=rwc"

[[ca]]
id = "default"
cert = "/etc/kipuka/ca/ca.pem"
key = "/etc/kipuka/ca/ca-key.pem"

# Optional: restrict which subject names this CA will sign.
# allowed_subjects = ["CN=*"]

# Optional: set a default certificate validity period.
# validity_days = 365

Production note: For production deployments, replace the [db] URL with a PostgreSQL connection string and store CA keys in an HSM. See the Configuration Reference for the full set of options.

Step 4: Run database migrations

kipuka manages its own schema. Run the migration command before the first start:

sudo -u kipuka kipuka migrate --config /etc/kipuka/kipuka.toml

Expected output:

Applied 3 migrations to sqlite:///var/lib/kipuka/kipuka.db

Step 5: Start the server

Start kipuka in the foreground to verify everything works:

sudo -u kipuka kipuka --config /etc/kipuka/kipuka.toml

You should see log output similar to:

2026-06-24T12:00:00.000Z  INFO kipuka::server: kipuka v0.1.0 starting
2026-06-24T12:00:00.001Z  INFO kipuka::tls: TLS configured, client auth enabled
2026-06-24T12:00:00.002Z  INFO kipuka::ca: Loaded CA "default" (CN=kipuka Test CA)
2026-06-24T12:00:00.003Z  INFO kipuka::db: Database connected (sqlite)
2026-06-24T12:00:00.004Z  INFO kipuka::server: Listening on 0.0.0.0:9443

Press Ctrl+C to stop. For long-running deployments, use the systemd service instead.

Step 6: Verify the EST endpoint

The /cacerts endpoint returns the CA certificate(s) without requiring client authentication. Use it to confirm the server is responding:

curl -sk https://localhost:9443/.well-known/est/cacerts \
  | base64 -d \
  | openssl pkcs7 -inform DER -print_certs

You should see the PEM-encoded CA certificate:

subject=CN = kipuka Test CA
issuer=CN = kipuka Test CA
-----BEGIN CERTIFICATE-----
MIIBkTCB+...
-----END CERTIFICATE-----

If this works, your server is ready to issue certificates.

Logging

kipuka uses Rust’s tracing framework. Control verbosity with the RUST_LOG environment variable:

LevelWhat you see
RUST_LOG=errorOnly errors
RUST_LOG=warnErrors and warnings
RUST_LOG=infoStartup, shutdown, enrollment events (default)
RUST_LOG=debugRequest/response details, TLS handshake info
RUST_LOG=traceFull ASN.1 dumps, raw bytes, internal state

To set the log level when running directly:

RUST_LOG=debug kipuka --config /etc/kipuka/kipuka.toml

For the systemd service, add an environment override:

sudo systemctl edit kipuka
[Service]
Environment="RUST_LOG=debug"

Next: Your First Certificate walks through the complete enrollment flow — generating an OTP, submitting a CSR, and extracting the signed certificate.

Your First Certificate

This guide walks through a complete EST enrollment cycle: generating a one-time password, creating a certificate signing request, enrolling through the EST endpoint, and verifying the result. By the end you will have a signed X.509 certificate issued by your kipuka server.

Prerequisites: A running kipuka instance from the First Run guide and the CA certificate saved as ca.pem.

Step 1: Generate a one-time password

kipuka authenticates initial enrollment requests with a one-time password (OTP). Generate one through the admin API:

curl -sk -X POST https://localhost:9443/admin/otp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "test-device"}'

Response:

{
  "entity_id": "test-device",
  "otp": "a1b2c3d4e5f6",
  "expires_at": "2026-06-25T12:00:00Z"
}

Save the OTP value:

OTP="a1b2c3d4e5f6"

Note: The admin API bearer token is configured in kipuka.toml under [admin]. See the Admin API Reference for details on token management.

Step 2: Generate a CSR

Create an EC P-256 key pair and a certificate signing request:

openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout client.key \
  -out client.csr \
  -nodes \
  -subj "/CN=test-device"

This produces two files:

FileContents
client.keyPrivate key (stays on the device, never sent to the server)
client.csrCertificate signing request (sent to kipuka)

Tip: For production IoT devices, generate the key pair in a secure element or TPM and export only the CSR.

Step 3: Enroll with OTP

Submit the CSR to the EST /simpleenroll endpoint, authenticating with the entity ID and OTP as HTTP Basic credentials:

curl -sk \
  --cacert ca.pem \
  -u "test-device:${OTP}" \
  --data-binary @client.csr \
  -H "Content-Type: application/pkcs10" \
  -o cert.p7 \
  https://localhost:9443/.well-known/est/simpleenroll

The server returns a PKCS #7 (CMS) envelope containing the signed certificate in DER format, saved here as cert.p7.

If enrollment succeeds, the HTTP response code is 200. Common error codes:

CodeMeaning
401Invalid or expired OTP
400Malformed CSR
403Subject name not permitted by CA policy
503Back-end CA unavailable

Step 4: Extract the certificate

Convert the PKCS #7 envelope to PEM:

openssl pkcs7 -inform DER -in cert.p7 -print_certs -out client.pem

You now have the signed certificate in client.pem.

Step 5: Verify the certificate

Inspect the certificate details:

openssl x509 -in client.pem -text -noout

Expected output (abbreviated):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: ...
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: CN = kipuka Test CA
        Validity
            Not Before: Jun 24 12:00:00 2026 GMT
            Not After : Jun 24 12:00:00 2027 GMT
        Subject: CN = test-device
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                ...

Verify the certificate chains back to your CA:

openssl verify -CAfile ca.pem client.pem
client.pem: OK

Re-enrollment

Once a device holds a valid certificate, it can renew without an OTP by authenticating with TLS client certificate authentication. The EST /simplereenroll endpoint accepts the same CSR format:

# Generate a new CSR (optionally with a new key pair)
openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout client-new.key \
  -out client-new.csr \
  -nodes \
  -subj "/CN=test-device"

# Re-enroll using the existing certificate for authentication
curl -sk \
  --cacert ca.pem \
  --cert client.pem \
  --key client.key \
  --data-binary @client-new.csr \
  -H "Content-Type: application/pkcs10" \
  -o cert-new.p7 \
  https://localhost:9443/.well-known/est/simplereenroll

Extract the renewed certificate:

openssl pkcs7 -inform DER -in cert-new.p7 -print_certs -out client-new.pem

Key rotation: The example above generates a fresh key pair during re-enrollment. This is recommended practice. If your use case requires keeping the same key, omit -newkey and pass the existing key with -key client.key.

EST labels: profile-based routing

kipuka supports EST labels (also called path segments) to route enrollment requests to different CA configurations or certificate profiles. The label appears in the URL path between /.well-known/est/ and the operation name.

For example, if you configure a label called iot-devices that maps to a dedicated CA and profile:

[[ca]]
id = "iot"
cert = "/etc/kipuka/ca/iot-ca.pem"
key = "/etc/kipuka/ca/iot-ca-key.pem"
label = "iot-devices"
validity_days = 90

Clients enroll against the labeled path:

curl -sk \
  --cacert iot-ca.pem \
  -u "sensor-001:${OTP}" \
  --data-binary @sensor.csr \
  -H "Content-Type: application/pkcs10" \
  -o sensor-cert.p7 \
  https://localhost:9443/.well-known/est/iot-devices/simpleenroll

The /cacerts endpoint also respects labels, returning only the CA certificate(s) for that label:

curl -sk https://localhost:9443/.well-known/est/iot-devices/cacerts \
  | base64 -d \
  | openssl pkcs7 -inform DER -print_certs

Labels are a powerful mechanism for multi-tenant and multi-profile deployments. See EST Labels in the Operator Guide for the complete configuration reference.

Summary

You have completed a full EST enrollment lifecycle:

  1. Generated a one-time password via the admin API
  2. Created a CSR with an EC P-256 key pair
  3. Enrolled through /simpleenroll with OTP authentication
  4. Extracted and verified the signed certificate
  5. Learned how to re-enroll with certificate-based authentication
  6. Explored label-based routing for multi-CA deployments

Next steps:

Configuration Reference

This document provides a complete reference for kipuka.toml, the main configuration file for the kipuka EST server.

Configuration Sections

[server]

Core server settings for HTTP/HTTPS listeners and runtime behavior.

KeyTypeDefaultDescription
listenstring"0.0.0.0:8443"Address and port for the main EST endpoint (HTTPS).
admin_listenstring"127.0.0.1:9443"Address and port for the administrative API. Bind to localhost by default for security.
workersintegernum_cpusNumber of worker threads. Defaults to the number of CPU cores.
max_body_sizestring"1MB"Maximum allowed request body size. Accepts suffixes: B, KB, MB, GB.
shutdown_timeoutstring"30s"Grace period for connection draining during shutdown. Accepts suffixes: s, m, h.

[tls]

TLS configuration for the main EST endpoint.

KeyTypeDefaultDescription
certstringrequiredPath to PEM-encoded server certificate.
keystringrequiredPath to PEM-encoded private key for the server certificate.
min_versionstring"1.2"Minimum TLS protocol version. Valid values: "1.2", "1.3".
cipher_suitesarray of strings(TLS library defaults)Explicit list of allowed cipher suites. Omit to use secure defaults.

[tls.client_auth]

Mutual TLS (mTLS) client authentication settings.

KeyTypeDefaultDescription
trust_anchorsstringrequiredPath to PEM file containing trusted CA certificates for client authentication.
modestring"required"Client certificate verification mode: required, optional, or none.

[db]

Database connection settings.

KeyTypeDefaultDescription
urlstring"sqlite://kipuka.db"Database connection URL. Supports sqlite://, postgres://, and mysql:// (MariaDB) schemes.
max_connectionsinteger5Maximum number of database connections in the pool.
connect_timeoutstring"5s"Timeout for establishing database connections.
auto_migratebooleantrueAutomatically apply schema migrations on startup.

[[ca]]

Certificate Authority (CA) definitions. Multiple [[ca]] sections define multiple CAs.

KeyTypeDefaultDescription
idstringrequiredUnique identifier for this CA. Referenced by EST labels.
namestringrequiredHuman-readable CA name.
certstringrequiredPath to PEM-encoded CA certificate.
keystringrequiredPath to PEM-encoded CA private key (or HSM key reference).
chainstringoptionalPath to PEM-encoded intermediate chain (if applicable).
validity_daysinteger365Default validity period for issued certificates (days).
max_validity_daysinteger398Maximum allowed validity period. RFC 5280 and NIAP recommend 398 days max.
default_key_usagearray of strings["digitalSignature", "keyEncipherment"]Default X.509 Key Usage extensions.
default_ext_key_usagearray of strings["serverAuth"]Default Extended Key Usage OIDs.
hsm_slotintegeroptionalHSM slot number if this CA key is stored in an HSM.

[est]

EST protocol feature flags and global settings.

KeyTypeDefaultDescription
base_pathstring"/.well-known/est"Base URL path for EST endpoints.
cacertsbooleantrueEnable /cacerts endpoint (RFC 7030 section 4.1).
simpleenrollbooleantrueEnable /simpleenroll endpoint (initial certificate enrollment).
simplereenrollbooleantrueEnable /simplereenroll endpoint (certificate renewal).
fullcmcbooleanfalseEnable /fullcmc endpoint (full CMC protocol).
serverkeygenbooleanfalseEnable /serverkeygen endpoint (server-generated key pairs).
csrattrsbooleantrueEnable /csrattrs endpoint (CSR attribute hints).
csr_attributesarray of strings[]OIDs to return in /csrattrs response. Empty means no specific attributes.
retry_afterinteger60Seconds to return in Retry-After header for pending requests.

[[est.label]]

EST label definitions for CA-specific endpoints. Multiple [[est.label]] sections define multiple labels.

KeyTypeDefaultDescription
namestringrequiredLabel name. Appears in URL as /.well-known/est/{name}/simpleenroll.
ca_idstringrequiredID of the CA to use for this label (references [[ca]] section).
allowed_key_typesarray of strings[]Restrict key types (e.g., ["rsa-2048", "ecdsa-p256"]). Empty allows all.
required_ext_key_usagearray of strings[]Require specific EKU OIDs in CSR.
max_validity_daysinteger(inherited from CA)Override CA’s max validity for this label.
require_sanbooleantrueRequire Subject Alternative Name extension in CSR.
subject_patternstringoptionalRegex pattern for validating CSR subject DN.

[hsm]

Hardware Security Module (HSM) configuration.

KeyTypeDefaultDescription
librarystringrequiredPath to PKCS#11 library (e.g., /usr/lib/softhsm/libsofthsm2.so).
slotintegeroptionalDefault HSM slot number. Can be overridden per CA.
token_labelstringoptionalToken label for slot selection.
pinstringoptionalHSM PIN. NOT RECOMMENDED (use pin_env or pin_file instead).
pin_envstringoptionalEnvironment variable containing the HSM PIN.
pin_filestringoptionalPath to file containing the HSM PIN (first line, newline stripped).

[otp]

One-Time Password (OTP) authentication for initial enrollment.

KeyTypeDefaultDescription
enabledbooleanfalseEnable OTP authentication.
token_lengthinteger20Length of generated OTP tokens. NIAP requires >= 16 characters.
default_ttlstring"24h"Default OTP validity period.
max_usesinteger1Maximum times an OTP can be used before expiration.
hash_algorithmstring"argon2id"Password hashing algorithm: argon2id, bcrypt, or sha256-hmac.
max_failuresinteger5Maximum failed OTP attempts before lockout.
failure_windowstring"15m"Time window for counting failed attempts.
lockout_durationstring"30m"Duration of account lockout after exceeding max_failures.

[audit]

Audit logging configuration.

KeyTypeDefaultDescription
filestringoptionalPath to audit log file. JSON Lines format.
syslogstringoptionalSyslog server URL (e.g., tcp+tls://syslog.example.com:6514).
syslog_facilitystring"local0"Syslog facility for audit events.
eventsarray of strings(all events)Filter events to log. Omit to log all. Examples: enroll, renew, reject.
include_cert_databooleanfalseInclude full certificate PEM in audit logs. Increases log volume.

[ha]

High Availability configuration for CA failover.

KeyTypeDefaultDescription
enabledbooleanfalseEnable HA mode.
strategystring"active-passive"Global failover strategy: active-passive, round-robin, weighted, latency-based.
check_intervalstring"10s"Health check interval for CAs.
failure_thresholdinteger3Consecutive failures before marking CA unhealthy.
recovery_timeoutstring"60s"Time to wait before retrying a failed CA.
check_timeoutstring"5s"Timeout for individual health checks.

[[ha.group]]

HA groups for label-specific CA failover. Multiple [[ha.group]] sections define multiple groups.

KeyTypeDefaultDescription
namestringrequiredGroup name.
ca_idsarray of stringsrequiredList of CA IDs in priority order.
strategystring(inherited from [ha])Override global HA strategy for this group.

[admin]

Administrative API configuration.

KeyTypeDefaultDescription
enabledbooleanfalseEnable the admin API.
authstring"mtls"Authentication method: mtls, bearer, or both.
trust_anchorsstringoptionalPath to PEM file with trusted CAs for admin mTLS. Required if auth includes mtls.
bearer_token_envstringoptionalEnvironment variable containing bearer token. Required if auth includes bearer.

Configuration Examples

Minimal Configuration

A basic single-CA deployment with one EST label.

[server]
listen = "0.0.0.0:8443"

[tls]
cert = "/etc/kipuka/server.pem"
key = "/etc/kipuka/server-key.pem"

[tls.client_auth]
trust_anchors = "/etc/kipuka/client-ca.pem"
mode = "required"

[db]
url = "sqlite:///var/lib/kipuka/kipuka.db"

[[ca]]
id = "main-ca"
name = "Main CA"
cert = "/etc/kipuka/ca.pem"
key = "/etc/kipuka/ca-key.pem"
validity_days = 365
max_validity_days = 398

[est]
base_path = "/.well-known/est"

[[est.label]]
name = "default"
ca_id = "main-ca"
require_san = true

Production Configuration

A realistic production deployment with HSM, HA, audit logging, admin API, OTP, and multiple CAs.

[server]
listen = "0.0.0.0:8443"
admin_listen = "127.0.0.1:9443"
workers = 8
max_body_size = "2MB"
shutdown_timeout = "60s"

[tls]
cert = "/etc/kipuka/certs/server.pem"
key = "/etc/kipuka/certs/server-key.pem"
min_version = "1.3"

[tls.client_auth]
trust_anchors = "/etc/kipuka/certs/client-ca-bundle.pem"
mode = "required"

[db]
url = "postgres://kipuka:[email protected]/kipuka?sslmode=require"
max_connections = 20
connect_timeout = "10s"
auto_migrate = true

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
slot = 0
token_label = "kipuka-prod"
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "root-ca"
name = "Production Root CA"
cert = "/etc/kipuka/ca/root-ca.pem"
key = "pkcs11:object=root-ca-key"
chain = "/etc/kipuka/ca/root-chain.pem"
validity_days = 365
max_validity_days = 398
default_key_usage = ["digitalSignature", "keyEncipherment"]
default_ext_key_usage = ["serverAuth", "clientAuth"]
hsm_slot = 0

[[ca]]
id = "backup-ca"
name = "Production Backup CA"
cert = "/etc/kipuka/ca/backup-ca.pem"
key = "pkcs11:object=backup-ca-key"
chain = "/etc/kipuka/ca/backup-chain.pem"
validity_days = 365
max_validity_days = 398
default_key_usage = ["digitalSignature", "keyEncipherment"]
default_ext_key_usage = ["serverAuth", "clientAuth"]
hsm_slot = 1

[[ca]]
id = "iot-ca"
name = "IoT Device CA"
cert = "/etc/kipuka/ca/iot-ca.pem"
key = "pkcs11:object=iot-ca-key"
validity_days = 180
max_validity_days = 180
default_key_usage = ["digitalSignature"]
default_ext_key_usage = ["clientAuth"]
hsm_slot = 2

[est]
base_path = "/.well-known/est"
cacerts = true
simpleenroll = true
simplereenroll = true
fullcmc = false
serverkeygen = false
csrattrs = true
csr_attributes = ["2.5.4.3", "2.5.4.11"]
retry_after = 120

[[est.label]]
name = "servers"
ca_id = "root-ca"
allowed_key_types = ["rsa-2048", "rsa-4096", "ecdsa-p256"]
required_ext_key_usage = ["serverAuth"]
max_validity_days = 398
require_san = true
subject_pattern = "^CN=.*\\.example\\.com$"

[[est.label]]
name = "clients"
ca_id = "root-ca"
allowed_key_types = ["ecdsa-p256", "ecdsa-p384"]
required_ext_key_usage = ["clientAuth"]
max_validity_days = 365
require_san = true

[[est.label]]
name = "iot"
ca_id = "iot-ca"
allowed_key_types = ["ecdsa-p256"]
required_ext_key_usage = ["clientAuth"]
max_validity_days = 180
require_san = false

[otp]
enabled = true
token_length = 20
default_ttl = "24h"
max_uses = 1
hash_algorithm = "argon2id"
max_failures = 5
failure_window = "15m"
lockout_duration = "30m"

[audit]
file = "/var/log/kipuka/audit.jsonl"
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local0"
events = ["enroll", "renew", "reject", "revoke"]
include_cert_data = false

[ha]
enabled = true
strategy = "active-passive"
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

[[ha.group]]
name = "production"
ca_ids = ["root-ca", "backup-ca"]
strategy = "active-passive"

[[ha.group]]
name = "iot"
ca_ids = ["iot-ca"]
strategy = "active-passive"

[admin]
enabled = true
auth = "both"
trust_anchors = "/etc/kipuka/certs/admin-ca.pem"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Notes

  • Security: Never commit kipuka.toml with plaintext secrets to version control. Use environment variables or HSM-backed keys.
  • Validation: kipuka validates the configuration at startup and reports errors for missing required fields or invalid values.
  • Reloading: Configuration changes require a server restart. Plan maintenance windows for production deployments.
  • HSM Keys: When using HSM-backed CA keys, specify key as a PKCS#11 URI (e.g., pkcs11:object=my-key) and ensure hsm_slot matches the token slot.
  • NIAP Compliance: For NIAP Common Criteria compliance, ensure otp.token_length >= 16, tls.min_version = "1.3", and ca.max_validity_days <= 398.

Certificate Authorities

kipuka supports multiple Certificate Authorities (CAs) through its [[ca]] array configuration. This architecture enables operators to serve different trust domains, key algorithms, and PKI hierarchies from a single EST server instance.

Multi-CA Architecture

Each CA in kipuka is defined by a unique id field used to bind EST labels to specific certificate authorities. Multiple CAs are configured in kipuka.toml:

[[ca]]
id = "internal-rsa"
cert = "/etc/kipuka/ca/internal-rsa-ca.crt"
key = "/etc/kipuka/ca/internal-rsa-ca.key"
chain = "/etc/kipuka/ca/internal-rsa-chain.pem"
max_validity_days = 90

[[ca]]
id = "external-ecdsa"
cert = "/etc/kipuka/ca/external-ecdsa-ca.crt"
key = "/etc/kipuka/ca/external-ecdsa-ca.key"
chain = "/etc/kipuka/ca/external-ecdsa-chain.pem"
max_validity_days = 90

Use Cases for Multiple CAs

Separate Trust Domains: Internal infrastructure certificates from one CA, external-facing services from another. This isolation prevents compromise of one domain from affecting the other.

Algorithm Migration: Run both RSA and ECDSA CAs simultaneously to support legacy clients while transitioning to modern algorithms.

Tiered Assurance Levels: Different CAs for different assurance requirements (standard validation vs. extended validation, different key sizes).

Geographic or Organizational Boundaries: Separate CAs for different regions or business units within an organization.

Development vs. Production: Dedicated CAs for testing environments prevent accidental trust of development certificates in production.

CA Lifecycle

Creating a Root CA

Generate a self-signed root CA private key and certificate:

# RSA 4096-bit root CA
openssl genrsa -out root-ca.key 4096
openssl req -new -x509 -days 7300 -key root-ca.key -out root-ca.crt \
  -subj "/C=US/O=Example Corp/CN=Example Root CA"

# ECDSA P-384 root CA
openssl ecparam -name secp384r1 -genkey -noout -out root-ca.key
openssl req -new -x509 -days 7300 -key root-ca.key -out root-ca.crt \
  -subj "/C=US/O=Example Corp/CN=Example Root CA"

Root CAs typically have 10-20 year validity periods and are kept offline. The root private key should be stored in secure offline storage (HSM, encrypted media in a safe).

Creating an Intermediate CA

Generate an intermediate CA signed by the root:

# Generate intermediate CA private key
openssl ecparam -name prime256v1 -genkey -noout -out intermediate-ca.key

# Create CSR for intermediate CA
openssl req -new -key intermediate-ca.key -out intermediate-ca.csr \
  -subj "/C=US/O=Example Corp/CN=Example Issuing CA"

# Sign intermediate CSR with root CA (5 year validity)
openssl x509 -req -in intermediate-ca.csr -CA root-ca.crt -CAkey root-ca.key \
  -CAcreateserial -out intermediate-ca.crt -days 1825 \
  -extensions v3_ca -extfile openssl-ca.cnf

The openssl-ca.cnf must include intermediate CA extensions:

[v3_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always

Building the Certificate Chain

The chain file contains certificates from the issuing CA up to (but typically not including) the root:

# Create chain file (intermediate only, root excluded)
cat intermediate-ca.crt > chain.pem

# If there are multiple intermediates, order from issuing CA to root:
# cat issuing-intermediate.crt sub-intermediate.crt > chain.pem

Chain ordering is critical: the first certificate in the chain must be the issuer of the end-entity certificate, and each subsequent certificate must be the issuer of the previous one.

Configuring kipuka to Use the Intermediate

[[ca]]
id = "prod-issuing-ca"
cert = "/etc/kipuka/ca/intermediate-ca.crt"
key = "/etc/kipuka/ca/intermediate-ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 90

The /cacerts endpoint returns the intermediate certificate plus the chain (but not the root). Clients must have the root CA in their trust store independently.

Key Types and Recommendations

RSA Keys

# RSA 2048-bit (legacy compatibility)
openssl genrsa -out ca-rsa2048.key 2048

# RSA 3072-bit (transitional strength)
openssl genrsa -out ca-rsa3072.key 3072

# RSA 4096-bit (high assurance, slower)
openssl genrsa -out ca-rsa4096.key 4096

RSA 2048: Minimum for legacy compatibility. Acceptable for short-lived certificates (under 100 days). Avoid for new deployments.

RSA 3072: Equivalent security to AES-128. Reasonable choice for transitional deployments.

RSA 4096: Equivalent security to AES-256. Use for long-lived root CAs. Performance penalty for signing and verification operations.

ECDSA Keys

# NIST P-256 (secp256r1, prime256v1)
openssl ecparam -name prime256v1 -genkey -noout -out ca-p256.key

# NIST P-384 (secp384r1)
openssl ecparam -name secp384r1 -genkey -noout -out ca-p384.key

# NIST P-521 (secp521r1)
openssl ecparam -name secp521r1 -genkey -noout -out ca-p521.key

P-256 (prime256v1): Recommended for general use. Equivalent security to AES-128. Wide client support, fast operations, small certificate size.

P-384: Equivalent security to AES-192. Use for high-assurance environments. Moderate performance impact.

P-521: Equivalent security to AES-256. Use for maximum security requirements. Larger certificates and slower operations than P-256.

Algorithm Selection Guidance

For new deployments, prefer ECDSA P-256 unless specific requirements dictate otherwise:

  • Smaller key and certificate sizes reduce bandwidth
  • Faster signing and verification operations
  • Equivalent or better security than RSA 2048
  • Supported by all modern clients (TLS 1.2+, most TLS 1.3 implementations)

Use RSA 2048 only when legacy client compatibility is required (older embedded devices, Windows XP, ancient Java versions).

Use P-384 or RSA 4096 for root CAs in high-assurance environments where the performance penalty is acceptable.

HSM-Backed vs File-Based Keys

File-Based Keys

File-based keys are stored as PEM-encoded files on the filesystem:

[[ca]]
id = "file-based-ca"
cert = "/etc/kipuka/ca/ca.crt"
key = "/etc/kipuka/ca/ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 90

Advantages:

  • Simple configuration and operation
  • No additional hardware or drivers required
  • Easy backup and disaster recovery
  • Standard OpenSSL tooling for key generation

Disadvantages:

  • Private key exists in cleartext on disk (even if file permissions are restrictive)
  • Key compromise if filesystem is breached
  • No audit trail for key usage
  • Key can be copied without detection

Security Measures for File-Based Keys:

  • Store keys on encrypted filesystems
  • Use file permissions to restrict access (mode 0400, root-owned)
  • Consider encrypted key files with passphrase protection (requires manual unlock on service start)
  • Implement filesystem integrity monitoring (AIDE, Tripwire)
  • Regular key rotation

HSM-Backed Keys

HSM-backed keys reference a PKCS#11 token and slot instead of filesystem paths:

[hsm]
module = "/usr/lib64/libsofthsm2.so"
pin = "1234"

[[ca]]
id = "hsm-backed-ca"
cert = "/etc/kipuka/ca/ca.crt"
hsm_slot = 0
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 90

Advantages:

  • Private key never leaves the HSM hardware
  • Tamper-resistant hardware with physical security controls
  • Audit logging of all key usage operations
  • FIPS 140-2/140-3 compliance available
  • Key backup and recovery through HSM vendor mechanisms

Disadvantages:

  • Additional hardware cost and complexity
  • PKCS#11 driver setup and maintenance
  • Potential performance bottleneck for high-volume signing
  • Vendor lock-in for key backup/recovery
  • Operational complexity (HSM initialization, token management, PIN management)

HSM Selection Considerations:

Network HSMs (Thales Luna, Entrust nShield) provide centralized key management and high availability but introduce network dependencies.

USB HSMs (YubiHSM, Nitrokey HSM) offer lower cost and simpler deployment but limited performance and single points of failure.

Software HSMs (SoftHSM) provide PKCS#11 API compatibility for testing but offer no security advantage over file-based keys.

For production issuing CAs, network HSMs are recommended. For offline root CAs, USB HSMs stored in physically secure locations are acceptable.

Certificate Chain Configuration

The chain parameter provides the certificate chain from the issuing CA to the root (root typically excluded). This chain is returned via the /cacerts endpoint.

Chain File Format

Chain files are PEM-encoded, containing one or more certificates. Order matters:

-----BEGIN CERTIFICATE-----
[Issuing Intermediate CA Certificate]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Sub-Intermediate CA Certificate, if any]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Root CA Certificate - optional, often excluded]
-----END CERTIFICATE-----

Chain Ordering Rules

  1. First certificate is the issuer of the end-entity certificates that the CA signs
  2. Each subsequent certificate is the issuer of the previous certificate
  3. Last certificate is either the root CA or the certificate directly issued by the root
  4. Root CA is often excluded from the chain (clients must have it in their trust store)

Why Exclude the Root CA

Most TLS/PKI implementations expect clients to have the root CA in their local trust store. Including the root in the chain:

  • Increases bandwidth unnecessarily
  • May cause validation issues with strict clients
  • Violates some certificate profile specifications

However, some closed environments include the root in the chain for convenience. kipuka supports both approaches.

Verifying Chain Correctness

# Verify the intermediate can be validated against the root
openssl verify -CAfile root-ca.crt intermediate-ca.crt

# Verify the complete chain
openssl verify -CAfile root-ca.crt -untrusted chain.pem end-entity.crt

Multiple Intermediate Levels

For deep hierarchies (root -> policy CA -> issuing CA):

# Build chain with two intermediates
cat issuing-ca.crt policy-ca.crt > chain.pem

# Root CA is still excluded

kipuka returns the issuing CA certificate concatenated with the chain file contents via /cacerts.

CA/B Forum Validity Limits

The CA/Browser Forum mandates maximum validity periods for publicly-trusted certificates. These limits progressively shorten:

Effective DateMaximum Validity
Current398 days
March 15, 2026200 days
March 15, 2027100 days
March 15, 202947 days

Configuring max_validity_days

The max_validity_days parameter in each [[ca]] section enforces these limits:

[[ca]]
id = "public-ca-2026"
cert = "/etc/kipuka/ca/ca.crt"
key = "/etc/kipuka/ca/ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 200  # Ready for March 2026 limit

kipuka rejects CSRs requesting validity beyond max_validity_days. Clients must request shorter lifetimes or accept the CA’s default.

Planning for Shorter Lifetimes

March 2026 (200 days): Transition to quarterly certificate renewal cycles. Most organizations can adapt existing manual processes.

March 2027 (100 days): Manual renewal becomes impractical. EST-based automation is essential. Plan for 90-day certificate lifetimes to allow renewal buffer.

March 2029 (47 days): Monthly or bi-weekly renewal cycles. Full automation required. No manual certificate issuance workflows can scale.

Why Shorter Lifetimes Drive EST Adoption

Reduced Blast Radius: Compromised keys have limited validity windows. Stolen certificates expire quickly.

Faster Cryptographic Agility: Transitioning to new algorithms (post-quantum cryptography) becomes operationally feasible when certificates renew frequently.

Improved Revocation Handling: Short lifetimes reduce reliance on CRL and OCSP infrastructure. Expired certificates are automatically untrusted.

Forcing Automation: Organizations must build robust certificate lifecycle management. This operational maturity pays dividends in incident response and compliance.

EST as the Solution: Manual CSR generation, approval workflows, and certificate installation cannot scale to 47-day lifetimes. EST provides:

  • Automated enrollment via /simpleenroll
  • Automated renewal via /simplereenroll
  • Automated trust anchor updates via /cacerts
  • Standard protocol implemented by network equipment, IoT devices, operating systems

Operational Recommendations

Set max_validity_days to 90 days for new CAs. This provides:

  • Compliance with current and near-term CA/B Forum limits
  • 30-day renewal buffer (renew at 60 days remaining)
  • Operational experience with short-lived certificates before 47-day limit

For existing CAs, plan a staged reduction:

  • 2026: 180 days
  • 2027: 90 days
  • 2029: 45 days

Use monitoring to track certificate expiration across your fleet. Alert when certificates are not renewed on schedule. EST’s /simplereenroll should be triggered automatically when 1/3 of the validity period remains.

Internal vs. Publicly-Trusted CAs

CA/B Forum limits apply only to publicly-trusted CAs (trust anchors in browser and OS trust stores). Internal CAs can use longer lifetimes:

[[ca]]
id = "internal-ca"
cert = "/etc/kipuka/ca/internal-ca.crt"
key = "/etc/kipuka/ca/internal-ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 365  # Internal CA, not subject to CA/B Forum

However, adopting short lifetimes even for internal CAs provides operational benefits (automation, reduced compromise windows) and prepares infrastructure for future regulatory requirements.

EST Labels

EST labels provide a powerful mechanism for operating multiple certificate profiles from a single EST server instance. This chapter covers label configuration, policy enforcement, and practical deployment patterns.

What Are EST Labels

RFC 7030 Section 3.2.2 defines EST path segments that enable certificate profile selection. The URL pattern for enrollment operations follows this structure:

/.well-known/est/<label>/simpleenroll
/.well-known/est/<label>/simplereenroll

When a client makes a request without a label (e.g., /.well-known/est/simpleenroll), the EST server applies its default certificate profile. Labels allow a single EST server to issue different types of certificates with different policies, CA issuers, and validation rules.

Common use cases include:

  • Certificate purpose separation: Different labels for TLS server certificates, client authentication certificates, and code signing certificates
  • Policy enforcement: Distinct labels enforcing different validity periods, key requirements, or subject naming constraints
  • Multi-tenant operation: Separate labels for different organizational units or trust boundaries
  • CA hierarchy management: Different labels issuing from different intermediate CAs within the same PKI

Label Configuration

Each [[est.label]] section in kipuka.toml defines a named profile. Labels are bound to specific CAs and enforce policy constraints on certificate issuance.

Full Label Syntax

[[est.label]]
# Unique label name used in EST URLs
name = "server-tls"

# CA ID reference (must match a [[ca]] entry)
ca_id = "intermediate-tls"

# Allowed CSR key types (optional)
# If omitted, all key types are accepted
allowed_key_types = ["ecdsaP256", "ecdsaP384", "rsa2048", "rsa3072"]

# Required Extended Key Usage OIDs (optional)
# Forces these EKUs into all issued certificates
required_ext_key_usage = ["serverAuth"]

# Maximum certificate validity in days (optional)
# Overrides CA default if set
max_validity_days = 398

# Require Subject Alternative Names (default: true)
require_san = true

# Subject DN validation regex (optional)
# CSRs must match this pattern to be accepted
subject_pattern = "^CN=[a-z0-9-]+\\.example\\.com(,.*)?$"

All fields except name and ca_id are optional. Without constraints, the label inherits behavior from the referenced CA.

CA Binding

Every label must reference a CA through ca_id. This CA identifier must match the id field of a [[ca]] entry defined elsewhere in kipuka.toml.

Multiple Labels, Same CA

Different labels can reference the same CA to provide different policy enforcement while using the same issuer:

[[ca]]
id = "corp-intermediate"
cert_path = "/etc/kipuka/pki/intermediate.crt"
key_path = "/etc/kipuka/pki/intermediate.key"
default_validity_days = 365

[[est.label]]
name = "server"
ca_id = "corp-intermediate"
required_ext_key_usage = ["serverAuth"]
max_validity_days = 398

[[est.label]]
name = "client"
ca_id = "corp-intermediate"
required_ext_key_usage = ["clientAuth"]
max_validity_days = 90
require_san = false

Both labels issue certificates from the same intermediate CA but enforce different EKUs, validity periods, and SAN requirements.

Multiple Labels, Different CAs

Labels can reference different CAs to support complex PKI hierarchies:

[[ca]]
id = "public-tls-ca"
cert_path = "/etc/kipuka/pki/public-tls-intermediate.crt"
key_path = "/etc/kipuka/pki/public-tls-intermediate.key"

[[ca]]
id = "internal-device-ca"
cert_path = "/etc/kipuka/pki/internal-device-intermediate.crt"
key_path = "/etc/kipuka/pki/internal-device-intermediate.key"

[[est.label]]
name = "public-server"
ca_id = "public-tls-ca"
required_ext_key_usage = ["serverAuth"]

[[est.label]]
name = "iot-device"
ca_id = "internal-device-ca"
required_ext_key_usage = ["clientAuth"]
max_validity_days = 30

This configuration issues public TLS certificates from one CA and private IoT device certificates from another.

Key Type Restrictions

The allowed_key_types field restricts which cryptographic key algorithms are accepted in certificate signing requests. This prevents weak key types or enforces organizational cryptographic policies.

Supported Key Type Values

allowed_key_types = [
    "rsa2048",    # RSA with 2048-bit modulus
    "rsa3072",    # RSA with 3072-bit modulus
    "rsa4096",    # RSA with 4096-bit modulus
    "ecdsaP256",  # ECDSA with NIST P-256 curve
    "ecdsaP384",  # ECDSA with NIST P-384 curve
    "ecdsaP521",  # ECDSA with NIST P-521 curve
]

If allowed_key_types is omitted, the label accepts any supported key type. An empty array [] rejects all requests.

Example: ECDSA-Only Label

[[est.label]]
name = "modern-tls"
ca_id = "tls-ca"
allowed_key_types = ["ecdsaP256", "ecdsaP384"]

This configuration rejects all RSA CSRs and only accepts P-256 or P-384 ECDSA keys.

EKU Constraints

The required_ext_key_usage field forces specific Extended Key Usage OIDs into all certificates issued under this label. This ensures certificates are only used for their intended purpose.

Common EKU Values

required_ext_key_usage = [
    "serverAuth",        # TLS server authentication (1.3.6.1.5.5.7.3.1)
    "clientAuth",        # TLS client authentication (1.3.6.1.5.5.7.3.2)
    "emailProtection",   # S/MIME email (1.3.6.1.5.5.7.3.4)
    "codeSigning",       # Code signing (1.3.6.1.5.5.7.3.3)
]

You can specify multiple EKUs for dual-purpose certificates. If the field is omitted, the label does not enforce EKU requirements (though the CA may still add EKUs based on its own policy).

Example: Mutual TLS Label

[[est.label]]
name = "mtls"
ca_id = "corp-ca"
required_ext_key_usage = ["serverAuth", "clientAuth"]

Certificates issued under this label can be used for both server and client authentication.

SAN Requirements

The require_san field enforces the presence of Subject Alternative Names in certificate signing requests. This follows CA/Browser Forum Baseline Requirements, which mandate SAN extensions for publicly trusted TLS certificates.

require_san = true   # Default: reject CSRs without SAN
require_san = false  # Accept CSRs without SAN

When require_san = true, the EST server rejects any CSR that does not contain at least one SAN entry (DNS name, IP address, email, or URI).

When to Disable SAN Requirements

Set require_san = false for:

  • Client authentication certificates: User certificates often have meaningful Subject DNs but no SAN
  • Legacy device certificates: Older systems may not support SAN extension generation
  • Internal PKI: Private infrastructures may rely solely on Subject DN for identification
[[est.label]]
name = "user-cert"
ca_id = "user-ca"
required_ext_key_usage = ["clientAuth", "emailProtection"]
require_san = false  # Allow subject-only certificates

Subject DN Patterns

The subject_pattern field validates the Subject Distinguished Name of incoming CSRs using regular expressions. This prevents clients from requesting certificates with arbitrary subject fields.

Pattern Syntax

# Exact CN match
subject_pattern = "^CN=server\\.example\\.com$"

# Domain restriction with variable hostname
subject_pattern = "^CN=[a-z0-9-]+\\.example\\.com$"

# Multiple RDN components
subject_pattern = "^CN=[^,]+,O=Example Corp,C=US$"

# Flexible ordering (less restrictive)
subject_pattern = "CN=[a-z0-9-]+\\.example\\.com"

The regex engine is Rust’s regex crate, which supports standard PCRE-like syntax. Patterns are case-sensitive by default.

Example: Locked-Down Production Label

[[est.label]]
name = "prod-server"
ca_id = "prod-ca"
subject_pattern = "^CN=(www|api|mail)\\.example\\.com(,O=Example Corp)?(,C=US)?$"
required_ext_key_usage = ["serverAuth"]
allowed_key_types = ["ecdsaP256", "ecdsaP384"]

This pattern only allows CSRs with CN values of www.example.com, api.example.com, or mail.example.com, with optional Organization and Country fields.

Practical Examples

Server TLS Label

Configuration for public-facing TLS server certificates following CA/B Forum requirements:

[[ca]]
id = "public-tls-ca"
cert_path = "/etc/kipuka/pki/tls-intermediate.crt"
key_path = "/etc/kipuka/pki/tls-intermediate.key"
default_validity_days = 365

[[est.label]]
name = "server-tls"
ca_id = "public-tls-ca"
allowed_key_types = ["ecdsaP256", "ecdsaP384", "rsa2048", "rsa3072"]
required_ext_key_usage = ["serverAuth"]
max_validity_days = 398  # CA/B Forum 398-day limit
require_san = true
subject_pattern = "^CN=[a-z0-9-]+\\.example\\.com$"

Client enrollment:

# Generate CSR with SAN
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -nodes -keyout server-key.pem -out server-csr.pem \
  -subj "/CN=web.example.com" \
  -addext "subjectAltName=DNS:web.example.com,DNS:www.example.com"

# Enroll via EST
curl --cacert ca.pem --cert client.pem --key client-key.pem \
  --data-binary @server-csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o server-cert.p7 \
  https://est.example.com/.well-known/est/server-tls/simpleenroll

# Extract certificate from PKCS#7
openssl pkcs7 -in server-cert.p7 -inform DER -print_certs -out server.pem

Client Authentication Label

Configuration for mutual TLS client certificates with shorter validity and relaxed SAN requirements:

[[ca]]
id = "client-ca"
cert_path = "/etc/kipuka/pki/client-intermediate.crt"
key_path = "/etc/kipuka/pki/client-intermediate.key"

[[est.label]]
name = "client-auth"
ca_id = "client-ca"
allowed_key_types = ["ecdsaP256", "rsa2048"]
required_ext_key_usage = ["clientAuth"]
max_validity_days = 90
require_san = false
subject_pattern = "^CN=[a-z]+\\.[a-z]+@example\\.com$"

Client enrollment:

# Generate CSR (no SAN required)
openssl req -new -newkey rsa:2048 -nodes \
  -keyout client-key.pem -out client-csr.pem \
  -subj "/[email protected]"

# Enroll via EST
curl --cacert ca.pem --cert bootstrap.pem --key bootstrap-key.pem \
  --data-binary @client-csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o client-cert.p7 \
  https://est.example.com/.well-known/est/client-auth/simpleenroll

# Extract certificate
openssl pkcs7 -in client-cert.p7 -inform DER -print_certs -out client.pem

Device Identity Label

Configuration for IoT device certificates with strict key type enforcement and short validity:

[[ca]]
id = "device-ca"
cert_path = "/etc/kipuka/pki/device-intermediate.crt"
key_path = "/etc/kipuka/pki/device-intermediate.key"

[[est.label]]
name = "device"
ca_id = "device-ca"
allowed_key_types = ["ecdsaP256"]  # P-256 only for embedded devices
required_ext_key_usage = ["clientAuth"]
max_validity_days = 30
require_san = true
subject_pattern = "^CN=device-[0-9a-f]{8}$"

Device enrollment:

# Generate CSR with device-specific CN and SAN
DEVICE_ID="device-a1b2c3d4"
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -nodes -keyout device-key.pem -out device-csr.pem \
  -subj "/CN=${DEVICE_ID}" \
  -addext "subjectAltName=URI:urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890"

# Enroll via EST
curl --cacert ca.pem --cert device-bootstrap.pem --key device-bootstrap-key.pem \
  --data-binary @device-csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o device-cert.p7 \
  https://est.example.com/.well-known/est/device/simpleenroll

# Extract certificate
openssl pkcs7 -in device-cert.p7 -inform DER -print_certs -out device.pem

Label Selection Strategy

When designing your EST label architecture, consider:

  1. Certificate Purpose Separation: Create distinct labels for server authentication, client authentication, email protection, and code signing. Never mix purposes in a single label.

  2. Security Posture: Use restrictive labels for production workloads (subject_pattern, strict allowed_key_types) and permissive labels for development environments.

  3. Validity Period: Match max_validity_days to certificate rotation cadence. Short-lived certificates (30-90 days) for automated systems, longer validity for manually managed certificates.

  4. CA Hierarchy Mapping: Map labels to CAs based on trust requirements. Public-facing services may require certificates from a different CA than internal infrastructure.

  5. Client Capabilities: Consider what key types and extensions your clients can generate. Legacy systems may require require_san = false and broader allowed_key_types.

Label Discovery

Clients can discover available labels through the EST /csrattrs endpoint:

curl --cacert ca.pem --cert client.pem --key client-key.pem \
  https://est.example.com/.well-known/est/server-tls/csrattrs

However, RFC 7030 does not define a standard mechanism for enumerating all labels. Operators should document available labels and their intended purposes in client onboarding documentation.

Validation Behavior

When a client submits a CSR to a labeled endpoint, kipuka performs validation in this order:

  1. Key type validation: If allowed_key_types is set, reject CSRs with non-matching key algorithms
  2. SAN validation: If require_san = true, reject CSRs without SAN extension
  3. Subject DN validation: If subject_pattern is set, reject CSRs with non-matching Subject DN
  4. EKU injection: Add required_ext_key_usage OIDs to the issued certificate (does not validate CSR EKUs)
  5. Validity enforcement: Apply max_validity_days limit, overriding CA default if stricter

If any validation step fails, the EST server returns HTTP 400 Bad Request with an error message indicating which constraint was violated.

Performance Considerations

Label lookups occur on every EST request. For deployments with hundreds of labels, ensure kipuka.toml is stored on fast local storage. Label matching is O(n) in the number of configured labels.

If you anticipate very large numbers of labels (>100), consider deploying multiple kipuka instances, each serving a subset of labels, behind a reverse proxy that routes based on URL path.

Authentication

kipuka supports multiple authentication methods to accommodate different enrollment scenarios and enterprise environments. This chapter covers the three primary authentication mechanisms available for EST endpoints.

Authentication Methods

mTLS (Mutual TLS) Authentication

Mutual TLS is the primary authentication method for EST as defined in RFC 7030. The client presents a certificate during the TLS handshake, which the server validates against a configured trust store.

Configuration

mTLS is configured in the [tls.client_auth] section:

[tls]
# ... other TLS settings ...

[tls.client_auth]
# Path to CA bundle containing trusted client certificate issuers
trust_anchors = "/etc/kipuka/client-ca-bundle.pem"

# Client certificate verification mode
mode = "required"  # required | optional | none

Mode Options:

  • required: All EST endpoints require a valid client certificate. The TLS handshake fails if no certificate is presented or if the certificate is not signed by a trusted CA.
  • optional: Client certificate is verified if presented, but clients without certificates can still connect. This mode allows fallback to OTP or GSSAPI for initial enrollment.
  • required_for_reenroll: Enrollment optionally requires a client certificate, but reenrollment is only possible using an existing client certificate.
  • none: No client certificate verification. Not recommended for production environments.

Trust Anchors:

The trust_anchors file should contain one or more PEM-encoded CA certificates. These CAs define which client certificates are accepted for authentication.

# Example: concatenate multiple CA certificates
cat corporate-ca.pem partner-ca.pem > /etc/kipuka/client-ca-bundle.pem

Re-enrollment Workflow

For re-enrollment (/simplereenroll), the client typically uses its previously-issued certificate as the client certificate:

  1. Client initiates TLS connection with its current certificate
  2. Server validates certificate against trust anchors
  3. Client submits CSR for new certificate
  4. Server issues new certificate

Example: Initial Enrollment with mTLS

For clients that already have a certificate (e.g., issued by a different CA or manually installed):

curl --cert client.crt --key client.key \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @request.csr \
     https://est.example.com:8443/.well-known/est/simpleenroll

Example: Re-enrollment

# Use the previously-issued certificate for authentication
curl --cert current.crt --key current.key \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @renewal.csr \
     https://est.example.com:8443/.well-known/est/simplereenroll

OTP (One-Time Password) Authentication

OTP authentication is designed for initial enrollment scenarios where the client does not yet have a certificate. The administrator generates a token via the Admin API, delivers it to the client through a secure out-of-band channel, and the client uses it for authentication.

Configuration

[otp]
# Enable OTP authentication
enabled = true

# Hash algorithm for token storage
# Options: argon2id (recommended), bcrypt, sha256-hmac
hash_algorithm = "argon2id"

# Minimum token length (NIAP PP requires >= 16)
token_length = 20

# Single-use or multi-use tokens
max_uses = 1

# Rate limiting configuration
max_failures = 5
failure_window = 300  # 5 minutes
lockout_duration = 900  # 15 minutes

Hash Algorithms:

  • argon2id (recommended): NIAP-compliant, memory-hard algorithm resistant to GPU attacks
  • bcrypt: Industry-standard password hashing
  • sha256-hmac: Fast but less secure; use only for low-security environments

All implementations use timing-safe comparison to prevent timing attacks.

Rate Limiting:

  • max_failures: Maximum authentication failures before lockout
  • failure_window: Time window (seconds) for counting failures
  • lockout_duration: How long (seconds) the client is locked out after exceeding max_failures

OTP Workflow

  1. Token Generation: Administrator calls the Admin API to generate an OTP token
  2. Out-of-Band Delivery: Token is securely delivered to the client (email, SMS, secure portal, etc.)
  3. Client Authentication: Client uses the token in HTTP Basic authentication
  4. Certificate Issuance: Server validates token and issues certificate
  5. Future Renewals: Client uses mTLS with the issued certificate

Generating OTP Tokens

Tokens are generated via the Admin API:

# Generate a single-use OTP token for a client
curl -X POST https://est.example.com:8443/admin/otp \
     -H "Authorization: Bearer <admin-token>" \
     -H "Content-Type: application/json" \
     -d '{
       "client_id": "workstation-42",
       "max_uses": 1,
       "expires_at": "2026-06-30T23:59:59Z"
     }'

Response:

{
  "token": "xK9mP2vL8qR5wN3jT7fY",
  "client_id": "workstation-42",
  "created_at": "2026-06-24T10:15:00Z",
  "expires_at": "2026-06-30T23:59:59Z",
  "max_uses": 1
}

Using OTP for Enrollment

The client sends the OTP token as the password in HTTP Basic authentication. The username is ignored but should be provided for RFC compliance:

# Enroll using OTP authentication
curl -u ":xK9mP2vL8qR5wN3jT7fY" \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @request.csr \
     https://est.example.com:8443/.well-known/est/simpleenroll

After successful enrollment, the client stores the issued certificate and uses mTLS for all future operations, including re-enrollment.

NIAP PP Compliance

For NIAP Protection Profile compliance:

  • Set token_length >= 16
  • Use hash_algorithm = "argon2id"
  • Configure appropriate rate limiting
  • Enable audit logging for OTP generation and usage

GSSAPI/Kerberos Authentication

GSSAPI authentication provides enterprise Single Sign-On (SSO) integration using Kerberos tickets. This is particularly useful in Active Directory and FreeIPA environments.

Configuration

[gssapi]
# Enable GSSAPI/Kerberos authentication
enabled = true

# Path to server's keytab file
keytab = "/etc/kipuka/kipuka.keytab"

# Service principal name
# Format: HTTP/hostname@REALM
service_principal = "HTTP/[email protected]"

# Principal to certificate subject mapping
# Maps Kerberos principal to X.509 subject DN
[gssapi.principal_mapping]
"[email protected]" = "CN=User,OU=People,DC=example,DC=com"
"[email protected]" = "CN=Admin,OU=Admins,DC=example,DC=com"

# Default mapping template (optional)
# {principal} is replaced with the authenticated Kerberos principal
default_template = "CN={principal},OU=Users,DC=example,DC=com"

Server Setup

The EST server must have a keytab containing credentials for the HTTP service principal:

# On Active Directory or FreeIPA KDC, create the service principal
# and export the keytab

# FreeIPA example:
ipa service-add HTTP/est.example.com
ipa-getkeytab -s ipaserver.example.com \
              -p HTTP/est.example.com \
              -k /etc/kipuka/kipuka.keytab

# Set appropriate permissions
chown kipuka:kipuka /etc/kipuka/kipuka.keytab
chmod 600 /etc/kipuka/kipuka.keytab

Client Authentication

Clients use SPNEGO (RFC 4559) to send Kerberos tickets via the Negotiate HTTP authentication scheme:

# Obtain Kerberos ticket
kinit [email protected]

# Enroll using GSSAPI authentication
curl --negotiate -u : \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @request.csr \
     https://est.example.com:8443/.well-known/est/simpleenroll

The server:

  1. Receives the Negotiate header with Kerberos ticket
  2. Validates the ticket against the KDC
  3. Extracts the authenticated principal name
  4. Maps the principal to a certificate subject DN using the configured mapping rules
  5. Issues a certificate with the mapped subject

Authentication Modes

kipuka supports two GSSAPI authentication modes controlled by the require_crypto_verification setting:

Cryptographic Verification (default, recommended)

When require_crypto_verification = true (the default), kipuka uses libgssapi to perform full cryptographic verification of Kerberos tickets. This requires:

  • A valid keytab file or gssproxy configuration
  • The gssapi compile-time feature flag enabled
  • Network connectivity to the KDC for ticket validation
[gssapi]
enabled = true
keytab = "/etc/kipuka/kipuka.keytab"
service_principal = "HTTP/[email protected]"
require_crypto_verification = true

Structural Parsing Mode (development only)

When require_crypto_verification = false, kipuka performs structural parsing of the SPNEGO token without cryptographic verification. The service name (sname) is extracted from the cleartext portion of the ticket, but:

  • No cryptographic verification is performed
  • The client identity cannot be authenticated
  • This mode is not suitable for production

This mode is intended only for development, logging, or environments where TLS client certificates provide the primary authentication and GSSAPI is used only for identity hint extraction.

[gssapi]
enabled = true
require_crypto_verification = false  # WARNING: development only

Security Warning: Never set require_crypto_verification = false in production. Without cryptographic verification, any client can forge a SPNEGO token with an arbitrary principal name.

Credential Management

kipuka supports two mutually exclusive credential management strategies:

Keytab file (traditional):

[gssapi]
keytab = "/etc/kipuka/kipuka.keytab"

gssproxy (recommended for containerized deployments):

[gssapi]
gssproxy = true  # Uses gssproxy socket instead of keytab

gssproxy separates credential management from the application, allowing kipuka to run without direct access to the keytab file. This is particularly useful in container environments where mounting keytab files is operationally complex.

Principal Mapping

The principal_mapping table defines explicit mappings from Kerberos principals to certificate subject DNs. If no explicit mapping exists, the default_template is used (if configured).

Example: Principal [email protected] with default template "CN={principal},OU=Users,DC=example,DC=com" results in subject [email protected],OU=Users,DC=example,DC=com.

Authentication per Endpoint

Different EST endpoints have different authentication requirements:

EndpointAuthenticationNotes
/cacertsNonePublic endpoint; returns CA certificate chain
/csrattrsNonePublic endpoint; returns CSR attributes
/simpleenrollmTLS OR OTP OR GSSAPIInitial enrollment; at least one method must succeed
/simplereenrollmTLS requiredRe-enrollment requires an existing valid certificate
/serverkeygenmTLS OR OTP OR GSSAPIServer-side key generation; same as simpleenroll
/fullcmcmTLS requiredFull CMC protocol requires client certificate
/admin/*Admin authSeparate authentication via [admin] section

Admin Endpoint Authentication

Admin endpoints (OTP generation, certificate revocation, etc.) use separate authentication configured in the [admin] section:

[admin]
# mTLS for admin endpoints
[admin.tls]
trust_anchors = "/etc/kipuka/admin-ca-bundle.pem"
mode = "required"

# Bearer token authentication (alternative or in addition to mTLS)
[admin.bearer_token]
enabled = true
tokens = [
    { token_hash = "sha256:...", description = "CI/CD automation" },
    { token_hash = "sha256:...", description = "Admin portal" }
]

Admin authentication is independent of EST endpoint authentication. You can require mTLS for admin operations even if EST endpoints accept OTP or GSSAPI.

Authentication Precedence

When multiple authentication methods are enabled, kipuka evaluates them in the following order:

  1. mTLS: If a client certificate is presented (and mode != "none"), it is validated first
  2. GSSAPI: If no valid client certificate and Negotiate header is present, GSSAPI is attempted
  3. OTP: If no valid client certificate and Authorization: Basic header is present, OTP is checked

A request succeeds if any enabled method authenticates successfully. For endpoints requiring mTLS (e.g., /simplereenroll), only mTLS is evaluated.

Security Best Practices

  • Production environments: Use mode = "required" for mTLS and disable OTP after initial enrollment
  • Initial enrollment: Use mode = "optional" with OTP enabled, then migrate to mode = "required" after all devices are enrolled
  • Token delivery: Never send OTP tokens over the same channel as enrollment (e.g., do not email a token to an address that auto-forwards to the EST client)
  • Token entropy: Use token_length >= 20 for high-security environments
  • Keytab protection: Store GSSAPI keytabs with restrictive permissions (600) and limit access to the kipuka service account
  • Audit logging: Enable audit logs for all authentication events to detect suspicious activity

Troubleshooting

mTLS Issues

Error: “client certificate required”

  • Verify mode is set to optional or none if client does not have a certificate
  • Check that the client is sending a certificate (--cert and --key in curl)

Error: “certificate signed by unknown authority”

  • Ensure the client certificate is signed by a CA in trust_anchors
  • Verify the CA bundle is readable and contains valid PEM data

OTP Issues

Error: “invalid OTP token”

  • Check token expiration (expires_at)
  • Verify token has not exceeded max_uses
  • Ensure token is sent as password in Basic auth (username can be empty or any value)

Error: “too many failures, locked out”

  • Wait for lockout_duration to expire
  • Review rate limiting configuration (max_failures, failure_window)

GSSAPI Issues

Error: “GSSAPI authentication failed”

  • Verify client has a valid Kerberos ticket (klist)
  • Check server’s keytab is readable and contains the correct principal
  • Ensure clocks are synchronized between client, server, and KDC (Kerberos requires time sync within 5 minutes)

Error: “principal not mapped”

  • Add an explicit mapping in [gssapi.principal_mapping] or configure default_template
  • Check that the authenticated principal name matches the expected format

TLS Configuration

This guide covers TLS configuration for the kipuka EST server, including certificate requirements, protocol versions, cipher suites, client authentication modes, and production deployment scenarios.

Server Certificate Requirements

RFC 7030 Section 3.3.2 mandates that EST server certificates include the id-kp-cmcRA Extended Key Usage (EKU) with OID 1.3.6.1.5.5.7.3.28. This EKU identifies the server as an authorized Registration Authority (RA) for certificate enrollment operations.

Without this EKU, compliant EST clients may reject the server certificate, even if it is otherwise valid. In production deployments, include both:

  • id-kp-cmcRA (1.3.6.1.5.5.7.3.28) - Required for EST protocol compliance
  • id-kp-serverAuth (1.3.6.1.5.5.7.3.1) - Standard server authentication for web browser compatibility

Generating a Server Certificate with cmcRA EKU

Create an OpenSSL configuration file (est-server.cnf) with the required extensions:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = v3_req
prompt             = no

[ req_distinguished_name ]
CN = est.example.com
O  = Example Organization
C  = US

[ v3_req ]
keyUsage           = critical, digitalSignature, keyEncipherment
extendedKeyUsage   = serverAuth, 1.3.6.1.5.5.7.3.28
subjectAltName     = @alt_names

[ alt_names ]
DNS.1 = est.example.com
DNS.2 = kipuka.example.com

[ v3_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical, CA:true
keyUsage               = critical, keyCertSign, cRLSign

Generate the server private key and CSR:

# Generate private key
openssl genrsa -out est-server-key.pem 2048

# Generate certificate signing request
openssl req -new -key est-server-key.pem \
    -out est-server.csr \
    -config est-server.cnf

# Sign with your CA (replace ca-cert.pem and ca-key.pem)
openssl x509 -req -in est-server.csr \
    -CA ca-cert.pem -CAkey ca-key.pem \
    -CAcreateserial -out est-server-cert.pem \
    -days 365 -sha256 \
    -extensions v3_req -extfile est-server.cnf

# Verify the cmcRA EKU is present
openssl x509 -in est-server-cert.pem -text -noout | grep -A1 "Extended Key Usage"

Expected output should include:

X509v3 Extended Key Usage:
    TLS Web Server Authentication, 1.3.6.1.5.5.7.3.28

Self-Signed Certificate for Development

For development and testing only:

# Generate self-signed certificate with cmcRA EKU
openssl req -x509 -newkey rsa:2048 -nodes \
    -keyout est-server-key.pem \
    -out est-server-cert.pem \
    -days 365 -sha256 \
    -config est-server.cnf \
    -extensions v3_req

Warning: Self-signed certificates should never be used in production. Always obtain certificates from a trusted CA.

TLS Version Configuration

The min_version parameter in the [tls] section controls the minimum TLS protocol version accepted by the server.

[tls]
min_version = "1.2"  # or "1.3"

Supported Values

  • "1.2" (default): Accept TLS 1.2 and TLS 1.3 connections. This is the minimum version required for RFC 7030 compliance and maintains compatibility with older EST clients.
  • "1.3": Accept only TLS 1.3 connections. Recommended for new deployments where all clients support TLS 1.3, as it provides improved security and performance.

TLS 1.0 and TLS 1.1 are not supported, as they have been deprecated by RFC 8996 and are considered insecure.

Recommendation

For production environments deployed after 2024, set min_version = "1.3" unless you have specific legacy client requirements. TLS 1.3 removes obsolete cryptographic algorithms, reduces handshake latency, and encrypts more of the handshake metadata.

Cipher Suite Configuration

The cipher_suites array in the [tls] section specifies the allowed cipher suites. If omitted, kipuka uses a secure default set that excludes weak ciphers.

[tls]
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

TLS 1.2 Cipher Suites

The default cipher suite list for TLS 1.2 connections includes:

cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
]

All suites provide:

  • Forward Secrecy via ECDHE key exchange
  • AEAD encryption (GCM or ChaCha20-Poly1305)
  • Strong hash functions (SHA-256 or SHA-384)

TLS 1.3 Cipher Suites

TLS 1.3 defines only three mandatory cipher suites, which are always enabled when min_version = "1.3":

  • TLS_AES_256_GCM_SHA384
  • TLS_AES_128_GCM_SHA256
  • TLS_CHACHA20_POLY1305_SHA256

The cipher_suites configuration parameter does not affect TLS 1.3, as the protocol specification mandates these suites.

NIAP and FIPS Compliance

For systems requiring NIAP Common Criteria or FIPS 140-2/3 compliance, disable ChaCha20-Poly1305 cipher suites, as they are not FIPS-approved algorithms:

[tls]
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

Consult your organization’s security policy for approved cipher suites.

Client Authentication Modes

The mode parameter in the [tls.client_auth] section controls whether and how client certificates are verified.

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/client-ca.pem"

Available Modes

mode = "required"

All TLS connections must present a valid client certificate. The certificate must:

  • Chain to a CA in the trust_anchors file
  • Not be expired or revoked
  • Have valid signatures

Use this mode when:

  • All clients have existing certificates (e.g., re-enrollment only)
  • Initial enrollment is performed via a separate out-of-band mechanism
  • Maximum security is required

Note: With mode = "required", clients cannot perform initial enrollment unless they already possess a certificate. You must provision initial certificates through another channel (e.g., manual issuance, SCEP, or ACME).

mode = "optional"

Client certificates are verified if presented, but connections are allowed without a certificate. This mode supports:

  • mTLS re-enrollment: Clients with existing certificates use them for authentication
  • OTP-based enrollment: Clients without certificates authenticate with a one-time password (OTP) during initial enrollment

This is the recommended mode for production EST deployments, as it supports the full enrollment lifecycle.

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/client-ca.pem"

[enrollment]
require_proof_of_possession = true  # Enforce PoP for OTP enrollments

mode = "none"

No client certificate verification is performed. Connections are accepted without client authentication.

This mode is for development and testing only. Do not use in production, as it disables a critical security layer.

Trust Anchors

The trust_anchors parameter specifies a PEM file containing the CA certificates trusted for client authentication. This file may contain multiple concatenated certificates.

# Example trust_anchors file with two CAs
cat ca1-cert.pem ca2-cert.pem > /etc/kipuka/client-ca.pem

kipuka validates client certificates against this trust anchor bundle. Ensure the file is readable by the kipuka process user.

Separate Admin API TLS Configuration

The administrative API runs on a separate TCP port (default 9443) and can use independent TLS settings. This allows you to:

  • Bind the admin API to a management network interface
  • Use separate client authentication for administrative operations
  • Restrict admin access to authorized systems only

Basic Admin API Configuration

[server]
listen = "0.0.0.0:8443"        # EST API (client-facing)
admin_listen = "127.0.0.1:9443" # Admin API (localhost only)

[tls]
cert = "/etc/kipuka/est-server-cert.pem"
key = "/etc/kipuka/est-server-key.pem"

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/client-ca.pem"

[admin.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/admin-ca.pem"  # Separate CA for admin certs

In this example:

  • EST API on port 8443 accepts optional client certificates from end-user devices
  • Admin API on localhost port 9443 requires client certificates from administrator systems
  • Admin certificates are issued by a separate CA (admin-ca.pem)

Binding to Management Networks

For multi-homed systems, bind the admin API to a dedicated management network interface:

[server]
listen = "0.0.0.0:8443"           # EST API on all interfaces
admin_listen = "10.0.1.100:9443"  # Admin API on management VLAN

Configure firewall rules to restrict access to the management network. For example, using iptables:

# Allow admin API only from management subnet
iptables -A INPUT -p tcp --dport 9443 -s 10.0.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 9443 -j DROP

Localhost-Only Admin Access

For single-host deployments or when using a reverse proxy, bind the admin API to localhost:

[server]
admin_listen = "127.0.0.1:9443"

Administrators can access the API via SSH port forwarding:

ssh -L 9443:localhost:9443 [email protected]
curl --cert admin-cert.pem --key admin-key.pem \
     https://localhost:9443/admin/health

Practical Configuration Examples

Development Setup with Self-Signed Certificate

For local testing and development:

[server]
listen = "127.0.0.1:8443"
admin_listen = "127.0.0.1:9443"

[tls]
cert = "/home/dev/kipuka/dev-cert.pem"
key = "/home/dev/kipuka/dev-key.pem"
min_version = "1.2"

[tls.client_auth]
mode = "none"  # Accept all connections for testing

[enrollment]
require_proof_of_possession = false  # Simplified for dev

Generate the development certificate:

openssl req -x509 -newkey rsa:2048 -nodes \
    -keyout dev-key.pem -out dev-cert.pem \
    -days 365 -sha256 \
    -subj "/CN=localhost" \
    -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
    -addext "extendedKeyUsage=serverAuth,1.3.6.1.5.5.7.3.28"

Production Setup with Internal CA

For enterprise deployments with an internal PKI:

[server]
listen = "0.0.0.0:8443"
admin_listen = "10.0.1.100:9443"

[tls]
cert = "/etc/kipuka/certs/est-server-cert.pem"
key = "/etc/kipuka/private/est-server-key.pem"
min_version = "1.2"
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/ca/device-ca-bundle.pem"

[admin.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/ca/admin-ca.pem"

[enrollment]
require_proof_of_possession = true
allowed_key_types = ["rsa2048", "rsa4096", "ecdsa256", "ecdsa384"]

Certificate generation workflow:

# 1. Generate server private key
openssl genrsa -out est-server-key.pem 2048
chmod 600 est-server-key.pem

# 2. Create CSR with cmcRA EKU
openssl req -new -key est-server-key.pem \
    -out est-server.csr \
    -config est-server.cnf

# 3. Submit CSR to internal CA for signing
# (Process varies by CA implementation)

# 4. Install signed certificate
install -m 644 est-server-cert.pem /etc/kipuka/certs/
install -m 600 est-server-key.pem /etc/kipuka/private/
chown kipuka:kipuka /etc/kipuka/certs/est-server-cert.pem
chown kipuka:kipuka /etc/kipuka/private/est-server-key.pem

# 5. Verify cmcRA EKU
openssl x509 -in /etc/kipuka/certs/est-server-cert.pem -text -noout \
    | grep -A1 "Extended Key Usage"

High-Security Setup with TLS 1.3 Only

For environments requiring maximum security (e.g., classified networks, financial services):

[server]
listen = "10.0.1.50:8443"
admin_listen = "127.0.0.1:9443"

[tls]
cert = "/etc/kipuka/certs/est-server-cert.pem"
key = "/etc/kipuka/private/est-server-key.pem"
min_version = "1.3"  # TLS 1.3 only

# Note: cipher_suites parameter has no effect in TLS 1.3
# The following suites are always enabled:
# - TLS_AES_256_GCM_SHA384
# - TLS_AES_128_GCM_SHA256
# - TLS_CHACHA20_POLY1305_SHA256

[tls.client_auth]
mode = "required"  # All connections must present client cert
trust_anchors = "/etc/kipuka/ca/device-ca-bundle.pem"

[admin.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/ca/admin-ca.pem"

[enrollment]
require_proof_of_possession = true
allowed_key_types = ["ecdsa384"]  # P-384 only
max_validity_days = 365
require_san = true

Additional hardening:

# Restrict file permissions
chmod 700 /etc/kipuka/private
chmod 600 /etc/kipuka/private/*.pem
chmod 644 /etc/kipuka/certs/*.pem

# Run as unprivileged user
useradd -r -s /bin/false -d /var/lib/kipuka kipuka
chown -R kipuka:kipuka /etc/kipuka

# Enable mandatory access control (SELinux/AppArmor)
# Example SELinux policy (adjust for your distribution)
semanage fcontext -a -t cert_t "/etc/kipuka/certs(/.*)?"
semanage fcontext -a -t cert_t "/etc/kipuka/ca(/.*)?"
semanage fcontext -a -t cert_t "/etc/kipuka/private(/.*)?"
restorecon -R /etc/kipuka

FIPS-Compliant Configuration

For systems requiring FIPS 140-2/3 compliance:

[server]
listen = "0.0.0.0:8443"
admin_listen = "10.0.1.100:9443"

[tls]
cert = "/etc/kipuka/certs/est-server-cert.pem"
key = "/etc/kipuka/private/est-server-key.pem"
min_version = "1.2"

# Disable ChaCha20 (not FIPS-approved)
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

[tls.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/ca/device-ca-bundle.pem"

[enrollment]
allowed_key_types = ["rsa2048", "rsa4096", "ecdsa256", "ecdsa384"]
allowed_signature_algorithms = ["sha256", "sha384", "sha512"]

Ensure the underlying operating system is running in FIPS mode:

# RHEL/CentOS
fips-mode-setup --enable
reboot

# Verify FIPS mode
cat /proc/sys/crypto/fips_enabled  # Should output: 1

Troubleshooting

Client Rejects Server Certificate

Symptom: EST client fails with “server certificate verification failed” or similar.

Cause: Server certificate missing the id-kp-cmcRA EKU.

Solution: Verify the EKU is present:

openssl x509 -in est-server-cert.pem -text -noout | grep -A1 "Extended Key Usage"

Expected output:

X509v3 Extended Key Usage:
    TLS Web Server Authentication, 1.3.6.1.5.5.7.3.28

If 1.3.6.1.5.5.7.3.28 is missing, regenerate the certificate using the instructions in “Server Certificate Requirements.”

TLS Handshake Failure

Symptom: Connection fails during TLS handshake with “no shared cipher” or “protocol version mismatch.”

Cause: Client and server have no common cipher suites or TLS versions.

Solution: Enable TLS 1.2 support if clients do not support TLS 1.3:

[tls]
min_version = "1.2"

Check client cipher suite support and add compatible suites to cipher_suites.

Admin API Unreachable

Symptom: Cannot connect to admin API on port 9443.

Cause: Admin API bound to wrong interface or blocked by firewall.

Solution: Verify admin_listen binding:

[server]
admin_listen = "0.0.0.0:9443"  # Listen on all interfaces

Check firewall rules:

# List firewall rules
iptables -L -n -v | grep 9443

# Allow admin API port
iptables -A INPUT -p tcp --dport 9443 -j ACCEPT

For localhost-only access, use SSH port forwarding:

ssh -L 9443:localhost:9443 [email protected]

Security Best Practices

  1. Use TLS 1.3 for new deployments: Set min_version = "1.3" to eliminate legacy protocol weaknesses.
  2. Restrict cipher suites: Use the default list or a more restrictive set. Avoid CBC-mode ciphers and weak hashes.
  3. Require client authentication for re-enrollment: Set mode = "required" if all clients have certificates.
  4. Separate admin and client CAs: Use distinct trust anchors for administrative and end-user certificates.
  5. Bind admin API to management networks: Limit exposure of administrative endpoints.
  6. Rotate certificates before expiration: Monitor certificate expiry and renew at least 30 days in advance.
  7. Protect private keys: Store keys on encrypted filesystems or HSMs. Use chmod 600 and restrict access to the kipuka process user.
  8. Enable OCSP or CRL checking: Configure revocation checking for client certificates (see enrollment configuration documentation).
  9. Audit TLS configuration regularly: Use tools like testssl.sh or sslyze to verify cipher suite and protocol configuration.
  10. Follow your organization’s security policy: Consult internal PKI and cryptography standards before deployment.

References

  • RFC 7030: Enrollment over Secure Transport (EST)
  • RFC 8996: Deprecating TLS 1.0 and TLS 1.1
  • RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
  • NIST SP 800-52 Rev. 2: Guidelines for the Selection, Configuration, and Use of TLS
  • NIAP Protection Profile for Network Devices: Common Criteria requirements for TLS configuration

HSM Integration

kipuka supports Hardware Security Modules (HSMs) and software HSMs through the PKCS#11 (Cryptoki) standard. HSM integration ensures that CA private keys never leave the secure hardware boundary, with all signing operations delegated to the HSM.

PKCS#11 Overview

kipuka uses the PKCS#11 API to interface with HSMs. The integration model follows these principles:

  • The [hsm] section in kipuka.toml configures the PKCS#11 provider library and authentication
  • CA definitions reference HSM-stored keys using the hsm_slot parameter in the [[ca]] section
  • Private keys remain on the HSM at all times—signing operations are performed by the HSM
  • When hsm_slot is set for a CA, the key parameter is ignored

This architecture ensures that sensitive key material never exists in kipuka’s process memory or on the filesystem.

Supported Vendors and Library Paths

kipuka works with any PKCS#11-compliant HSM. The following table lists common vendors and their typical library paths:

VendorProductLibrary Path (Linux)Library Path (macOS)
EntrustnShield/opt/nfast/toolkits/pkcs11/libcknfast.soN/A
UtimacoCryptoServer/opt/utimaco/lib/libcs_pkcs11_R3.soN/A
ThalesLuna/usr/safenet/lunaclient/lib/libCryptoki2_64.so/usr/safenet/lunaclient/lib/libCryptoki2.dylib
KryopticSoftHSM-compatible/usr/lib/pkcs11/libkryoptic_pkcs11.sotarget/release/libkryoptic_pkcs11.dylib
SoftHSM2SoftHSM2/usr/lib/softhsm/libsofthsm2.so/usr/local/lib/softhsm/libsofthsm2.so
AWSCloudHSM/opt/cloudhsm/lib/libcloudhsm_pkcs11.soN/A
YubiHSMYubiHSM2/usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib

Consult your HSM vendor documentation for the exact library path on your system.

PIN Management

kipuka supports three methods for providing the HSM PIN, evaluated in the following priority order:

Set pin_env to the name of an environment variable containing the PIN:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin_env = "KIPUKA_HSM_PIN"

Then start kipuka with the PIN in the environment:

export KIPUKA_HSM_PIN="your-pin-here"
kipuka

For systemd services, use EnvironmentFile:

[Service]
EnvironmentFile=/etc/kipuka/hsm.env
ExecStart=/usr/local/bin/kipuka

Where /etc/kipuka/hsm.env contains:

KIPUKA_HSM_PIN=your-pin-here

Set restrictive permissions on the environment file:

chmod 0400 /etc/kipuka/hsm.env
chown kipuka:kipuka /etc/kipuka/hsm.env

PIN File

Set pin_file to a path containing only the PIN:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin_file = "/etc/kipuka/hsm.pin"

Secure the PIN file:

echo "your-pin-here" > /etc/kipuka/hsm.pin
chmod 0400 /etc/kipuka/hsm.pin
chown kipuka:kipuka /etc/kipuka/hsm.pin

For development only, the PIN can be stored directly in the configuration:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin = "1234"

Never use this method in production. The PIN will be visible in the configuration file and process listings.

Slot Configuration

PKCS#11 tokens are identified by either slot number or token label.

By Slot Number

Reference the HSM token by its numeric slot identifier:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
slot = 0
pin_env = "KIPUKA_HSM_PIN"

By Token Label

Reference the HSM token by its label:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin_env = "KIPUKA_HSM_PIN"

Using token_label is generally more portable across HSM reconfigurations.

Discovering Slots

Use pkcs11-tool to list available slots:

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --list-slots

Example output:

Available slots:
Slot 0 (0x1d6d28f6): SoftHSM slot ID 0x1d6d28f6
  token label        : kipuka
  token manufacturer : SoftHSM project
  token model        : SoftHSM v2
  token flags        : login required, rng, token initialized, PIN initialized
  hardware version   : 2.6
  firmware version   : 2.6
  serial num         : 4c8e0a766d6d28f6
  pin min/max        : 4/255

The token label from this output can be used as the token_label value.

Key Generation Examples

Before configuring kipuka to use HSM-stored keys, you must generate key pairs on the HSM. The following examples use pkcs11-tool from the OpenSC package.

Generate RSA 2048-bit Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type rsa:2048 \
  --id 01 \
  --label "kipuka-ca-rsa-2048"

Generate RSA 4096-bit Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type rsa:4096 \
  --id 02 \
  --label "kipuka-ca-rsa-4096"

Generate ECDSA P-256 Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type EC:prime256v1 \
  --id 03 \
  --label "kipuka-ca-ec-p256"

Generate ECDSA P-384 Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type EC:secp384r1 \
  --id 04 \
  --label "kipuka-ca-ec-p384"

List HSM Objects

Verify key generation by listing objects on the token:

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --list-objects

Example output:

Public Key Object; RSA 2048 bits
  label:      kipuka-ca-rsa-2048
  ID:         01
  Usage:      encrypt, verify, wrap
Private Key Object; RSA
  label:      kipuka-ca-rsa-2048
  ID:         01
  Usage:      decrypt, sign, unwrap

The ID field value is used as the hsm_slot parameter in kipuka’s CA configuration.

Development Setup with Kryoptic

Kryoptic is a Rust-based software PKCS#11 implementation suitable for development and testing. It provides a lightweight alternative to hardware HSMs.

Installation

Install Kryoptic from crates.io:

cargo install kryoptic

Or build from source:

git clone https://github.com/latchset/kryoptic.git
cd kryoptic
cargo build --release

The PKCS#11 library will be at target/release/libkryoptic_pkcs11.so (Linux) or target/release/libkryoptic_pkcs11.dylib (macOS).

Initialize a Token

Create a Kryoptic configuration directory:

mkdir -p ~/.config/kryoptic

Initialize a token using pkcs11-tool:

pkcs11-tool --module target/release/libkryoptic_pkcs11.so \
  --init-token \
  --label "kipuka-dev" \
  --so-pin 12345678

Set the user PIN:

pkcs11-tool --module target/release/libkryoptic_pkcs11.so \
  --init-pin \
  --so-pin 12345678 \
  --pin 1234

Generate Development Keys

Generate an RSA 2048-bit key pair:

pkcs11-tool --module target/release/libkryoptic_pkcs11.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type rsa:2048 \
  --id 01 \
  --label "kipuka-dev-ca"

Configure kipuka

Update kipuka.toml to use the Kryoptic library:

[hsm]
library = "/home/user/kryoptic/target/release/libkryoptic_pkcs11.so"
token_label = "kipuka-dev"
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "dev-ca"
name = "Development CA"
cert = "/etc/kipuka/ca/dev-ca.pem"
hsm_slot = 1
validity_days = 365

Generate the CA certificate (the private key remains on Kryoptic):

export KIPUKA_HSM_PIN=1234

# Generate a self-signed CA certificate using the HSM key
openssl req -new -x509 \
  -engine pkcs11 \
  -keyform engine \
  -key "pkcs11:token=kipuka-dev;id=%01;type=private" \
  -out /etc/kipuka/ca/dev-ca.pem \
  -days 3650 \
  -subj "/CN=kipuka Development CA"

Start kipuka:

kipuka

kipuka will now use the Kryoptic HSM for all signing operations for the dev-ca CA.

Full Configuration Example

The following example shows a complete HSM configuration with multiple CAs:

# HSM configuration
[hsm]
library = "/usr/lib/pkcs11/libkryoptic_pkcs11.so"
token_label = "kipuka"
pin_env = "KIPUKA_HSM_PIN"

# Server configuration
[server]
listen = "0.0.0.0:8443"
tls_cert = "/etc/kipuka/server.pem"
tls_key = "/etc/kipuka/server-key.pem"

# RSA CA using HSM key
[[ca]]
id = "hsm-rsa-ca"
name = "HSM-Protected RSA CA"
cert = "/etc/kipuka/ca/hsm-rsa-ca.pem"
hsm_slot = 1
validity_days = 365
key_usage = ["digitalSignature", "keyCertSign", "cRLSign"]

# ECDSA CA using HSM key
[[ca]]
id = "hsm-ec-ca"
name = "HSM-Protected ECDSA CA"
cert = "/etc/kipuka/ca/hsm-ec-ca.pem"
hsm_slot = 3
validity_days = 365
key_usage = ["digitalSignature", "keyCertSign", "cRLSign"]

Key points:

  • The hsm_slot parameter references the object ID assigned during key generation
  • When hsm_slot is set, the key parameter must be omitted—the private key exists only on the HSM
  • Multiple CAs can share the same HSM by using different slot identifiers
  • The CA certificate in the cert parameter is the public certificate; the private key remains on the HSM

Troubleshooting

Library Not Found

If kipuka reports that the PKCS#11 library cannot be loaded, verify:

  • The library path is correct for your system
  • The library file has appropriate read and execute permissions
  • Required dependencies are installed (consult HSM vendor documentation)

Authentication Failures

If kipuka reports authentication failures:

  • Verify the PIN is correct using pkcs11-tool --login
  • Check that the token is not locked due to failed login attempts
  • Ensure the environment variable or PIN file is readable by the kipuka process user

Key Not Found

If kipuka reports that the key cannot be found in the HSM:

  • List objects with pkcs11-tool --list-objects and verify the key exists
  • Ensure the hsm_slot value matches the key’s ID field
  • Check that the token label or slot number is correct

Performance Considerations

HSM signing operations have higher latency than software signing. For high-throughput deployments:

  • Use hardware HSMs with dedicated crypto acceleration
  • Consider load balancing across multiple kipuka instances
  • Monitor HSM session limits and adjust max_sessions if supported by your HSM

High Availability

kipuka provides multi-CA high availability at the application layer. When a Certificate Authority backend becomes unavailable—whether due to HSM failure, Dogtag server downtime, or certificate expiration—kipuka can automatically failover to an alternate CA. This ensures continuous certificate enrollment even when individual CA components fail.

High availability is configured through [ha] and [[ha.group]] sections in the configuration file.

Overview

Traditional PKI deployments often rely on infrastructure-level HA (load balancers, database replication) for a single CA. kipuka takes a different approach: it treats each CA as an independent backend and implements application-layer failover logic. This allows:

  • Heterogeneous CA backends: Mix HSM-backed CAs with file-based CAs, or Dogtag with other implementations
  • Gradual migration: Route a percentage of traffic to a new CA while maintaining the old one
  • Geographic distribution: Route requests to the nearest or fastest CA
  • Independent failure domains: HSM failure doesn’t take down file-based backup CAs

When a CA fails, kipuka’s circuit breaker immediately stops routing requests to it and redistributes traffic to healthy CAs in the same group. When the failed CA recovers, kipuka automatically reintegrates it based on the configured strategy.

Failover Strategies

kipuka supports four failover strategies, selectable globally via [ha] or per-group via [[ha.group]]:

active-passive

The primary CA handles all requests. If it fails, the secondary takes over. When the primary recovers, traffic automatically returns to it.

Use when:

  • You have a clear primary CA (e.g., HSM-backed) and want a hot standby
  • You need predictable CA assignment for audit or compliance
  • Your CAs have different trust levels or security characteristics

Behavior:

  • All requests routed to the first healthy CA in the group
  • On failure, immediately switch to the next CA
  • On recovery, immediately switch back to the primary

round-robin

Requests are distributed evenly across all healthy CAs in the group. Any failure removes that CA from rotation until it recovers.

Use when:

  • All CAs in the group have equivalent capacity and trust
  • You want to distribute load across multiple CAs
  • You need to maximize utilization of all CA backends

Behavior:

  • Each request goes to the next CA in a circular list
  • Failed CAs are skipped in the rotation
  • Load is balanced evenly across all healthy CAs

weighted

Requests are distributed according to configured weights (e.g., 80% to CA1, 20% to CA2). Allows proportional load distribution.

Use when:

  • CAs have different capacities (e.g., HSM vs. software)
  • You’re migrating from one CA to another gradually
  • You want to test a new CA with a small percentage of traffic

Behavior:

  • Each CA receives traffic proportional to its weight
  • Weights are specified per-CA in the group configuration
  • If a CA fails, its weight is redistributed to remaining CAs

Configuration:

[[ha.group]]
name = "primary"
ca_ids = ["hsm-ca", "file-ca"]
strategy = "weighted"
weights = { "hsm-ca" = 80, "file-ca" = 20 }

latency-based

Requests are routed to the CA with the lowest recent response time. Self-optimizing for geographically distributed CAs.

Use when:

  • CAs are in different geographic locations
  • Network latency varies significantly between CAs
  • You want automatic optimization without manual tuning

Behavior:

  • kipuka tracks rolling average latency for each CA
  • Each request goes to the CA with the lowest average latency
  • Latency is measured from health checks and actual signing operations
  • Failed CAs are assigned infinite latency

Circuit Breaker Pattern

kipuka implements a circuit breaker to prevent cascading failures and automatically recover from transient issues. The circuit breaker prevents the system from repeatedly attempting to use a failing CA, which could delay client requests or exhaust resources.

States

The circuit breaker transitions through five states:

Healthy: CA is responding normally. All requests are routed to it according to the failover strategy.

Degraded: CA has experienced some failures but remains below the failure threshold. Requests continue to be routed, but the CA is monitored more closely. This state provides early warning of potential issues.

Unhealthy: Failure count exceeds failure_threshold within the check window. The CA is immediately removed from rotation to prevent client impact.

CircuitOpen: After transitioning to Unhealthy, the circuit opens. No requests are sent to this CA. A timer starts for recovery_timeout seconds to allow the CA time to recover.

Recovering: After recovery_timeout expires, a single probe request is sent. If it succeeds, the CA transitions back to Healthy and rejoins rotation. If it fails, the circuit returns to CircuitOpen with an extended timeout (exponential backoff).

State Transitions

Healthy --> Degraded --> Unhealthy --> CircuitOpen --> Recovering --> Healthy
   ^                                      |               |
   |                                      +-------<-------+
   +------------------<-------------------+
  • Healthy → Degraded: First failure detected
  • Degraded → Unhealthy: Failure count exceeds threshold
  • Unhealthy → CircuitOpen: Immediate transition; timer starts
  • CircuitOpen → Recovering: After recovery_timeout expires
  • Recovering → Healthy: Probe succeeds; CA rejoins rotation
  • Recovering → CircuitOpen: Probe fails; extended timeout begins
  • Degraded → Healthy: CA recovers before hitting threshold
  • CircuitOpen → Healthy: Manual operator override (health check passes)

Configuration

Circuit breaker behavior is tuned via [ha]:

[ha]
check_interval = "10s"        # How often to probe each CA
failure_threshold = 3          # Consecutive failures before marking unhealthy
recovery_timeout = "60s"       # Wait time before probing a failed CA
check_timeout = "5s"           # Max time to wait for health check response
  • check_interval: Frequency of active health checks. Shorter intervals detect failures faster but increase CA load.
  • failure_threshold: Number of consecutive failures before removing a CA from rotation. Lower values improve responsiveness but may cause flapping; higher values increase tolerance for transient failures.
  • recovery_timeout: How long to wait before attempting recovery. This gives the CA time to fully restart or for transient issues to resolve. kipuka uses exponential backoff: if the first probe fails, the next timeout is doubled.
  • check_timeout: Maximum time to wait for a health check response. Should be shorter than check_interval to avoid overlapping checks.

HA Groups

HA groups define sets of CAs that provide redundancy for each other. All CAs in a group should issue from the same root (or cross-certified roots) so clients trust all issuers.

Configuration

[[ha.group]]
name = "primary"               # Unique group name
ca_ids = ["hsm-ca", "file-ca"] # Array of [[ca]] id values
strategy = "active-passive"    # Optional: override global strategy
  • name: Unique identifier for this group. Used in logs and metrics.
  • ca_ids: Array of CA identifiers. Must reference valid [[ca]] sections. Order matters for active-passive strategy.
  • strategy: Optional override of the global [ha] strategy. Allows different strategies for different groups.

EST Label Integration

EST labels reference individual CAs via their id. When the primary CA in a group fails, the HA system automatically routes requests to the next healthy CA in the group. From the client’s perspective, the EST label remains the same—failover is transparent.

Example:

[[ca]]
id = "hsm-ca"
backend = "dogtag"
# ... HSM configuration ...

[[ca]]
id = "file-ca"
backend = "file"
# ... file configuration ...

[[ha.group]]
name = "production"
ca_ids = ["hsm-ca", "file-ca"]
strategy = "active-passive"

[[est.label]]
name = "device-cert"
ca_id = "hsm-ca"  # References the group leader
profile = "deviceCert"

If hsm-ca fails, requests to the device-cert label automatically use file-ca until hsm-ca recovers.

Health Check Configuration

kipupa performs active health checks to detect CA failures and recoveries. Health checks are lightweight signing operations that verify the CA is fully functional—not just network-reachable.

Health Check Behavior

  1. Every check_interval seconds, kipuka sends a test signing request to each CA
  2. If the CA responds successfully within check_timeout, the check passes
  3. If the CA fails to respond or returns an error, the check fails
  4. After failure_threshold consecutive failures, the CA is marked Unhealthy
  5. After recovery_timeout seconds, kipuka sends a single probe to the failed CA
  6. If the probe succeeds, the CA returns to Healthy; if it fails, the timeout doubles

Tuning Recommendations

Low-latency environment (local CAs):

[ha]
check_interval = "5s"
failure_threshold = 2
recovery_timeout = "30s"
check_timeout = "2s"

High-latency environment (geographically distributed CAs):

[ha]
check_interval = "30s"
failure_threshold = 5
recovery_timeout = "120s"
check_timeout = "10s"

Production (balanced):

[ha]
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

Example Configurations

Two-CA Active-Passive with HSM Primary

A production deployment with an HSM-backed primary CA and a file-based backup. Normal traffic uses the HSM; if it fails, the file-based CA provides continuity.

[ha]
strategy = "active-passive"
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

[[ca]]
id = "hsm-ca"
backend = "dogtag"
[ca.dogtag]
url = "https://pki.example.com:8443"
ca_cert = "/etc/kipuka/pki-ca.pem"
auth_cert = "/etc/kipuka/ra-agent.pem"
auth_key = "pkcs11:token=HSM;object=ra-key"

[[ca]]
id = "backup-ca"
backend = "file"
[ca.file]
ca_cert = "/etc/kipuka/backup-ca.pem"
ca_key = "/etc/kipuka/backup-ca-key.pem"

[[ha.group]]
name = "production"
ca_ids = ["hsm-ca", "backup-ca"]

[[est.label]]
name = "device"
ca_id = "hsm-ca"  # HA group leader
profile = "deviceCert"

Expected behavior:

  • All requests use hsm-ca under normal conditions
  • If the HSM or Dogtag server fails, traffic immediately switches to backup-ca
  • When hsm-ca recovers, traffic returns to it within one check_interval
  • Clients see no difference—the device label works throughout

Three-CA Round-Robin for Load Distribution

Three identical CAs in different datacenters. Traffic is distributed evenly to maximize utilization and provide geographic redundancy.

[ha]
strategy = "round-robin"
check_interval = "15s"
failure_threshold = 3
recovery_timeout = "90s"
check_timeout = "7s"

[[ca]]
id = "ca-east"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-east.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-east.pem"
auth_key = "/etc/kipuka/ra-east-key.pem"

[[ca]]
id = "ca-west"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-west.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-west.pem"
auth_key = "/etc/kipuka/ra-west-key.pem"

[[ca]]
id = "ca-central"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-central.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-central.pem"
auth_key = "/etc/kipuka/ra-central-key.pem"

[[ha.group]]
name = "global"
ca_ids = ["ca-east", "ca-west", "ca-central"]

[[est.label]]
name = "iot"
ca_id = "ca-east"  # Any CA in the group; round-robin applies
profile = "iotDevice"

Expected behavior:

  • Requests are distributed 33/33/33 across the three CAs
  • If ca-east fails, traffic is redistributed 50/50 to ca-west and ca-central
  • When ca-east recovers, it rejoins the rotation
  • Each CA operates independently; no shared state required

Geographic HA with Latency-Based Routing

Two CAs in different regions. kipuka automatically routes requests to the CA with the lowest latency, optimizing performance for geographically distributed clients.

[ha]
strategy = "latency-based"
check_interval = "20s"
failure_threshold = 4
recovery_timeout = "120s"
check_timeout = "10s"

[[ca]]
id = "ca-us"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-us.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-us.pem"
auth_key = "/etc/kipuka/ra-us-key.pem"

[[ca]]
id = "ca-eu"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-eu.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-eu.pem"
auth_key = "/etc/kipuka/ra-eu-key.pem"

[[ha.group]]
name = "global"
ca_ids = ["ca-us", "ca-eu"]

[[est.label]]
name = "vpn"
ca_id = "ca-us"  # HA group leader; latency-based routing applies
profile = "vpnCert"

Expected behavior:

  • kipuka measures latency to both CAs during health checks
  • Requests are automatically routed to the faster CA (e.g., ca-us for US clients, ca-eu for EU clients)
  • If latency increases for one CA (network congestion, overload), traffic shifts to the other
  • If one CA fails completely, all traffic uses the remaining CA
  • No manual configuration required—self-optimizing based on network conditions

Weighted Migration from Old to New CA

Gradual migration from an existing CA to a new one. Start with 90% traffic on the old CA, gradually shift to 100% on the new CA, then decommission the old CA.

[ha]
strategy = "weighted"
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

[[ca]]
id = "old-ca"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-old.example.com:8443"
ca_cert = "/etc/kipuka/old-ca.pem"
auth_cert = "/etc/kipuka/ra-old.pem"
auth_key = "/etc/kipuka/ra-old-key.pem"

[[ca]]
id = "new-ca"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-new.example.com:8443"
ca_cert = "/etc/kipuka/new-ca.pem"
auth_cert = "/etc/kipuka/ra-new.pem"
auth_key = "/etc/kipuka/ra-new-key.pem"

[[ha.group]]
name = "migration"
ca_ids = ["old-ca", "new-ca"]
strategy = "weighted"
weights = { "old-ca" = 90, "new-ca" = 10 }

[[est.label]]
name = "server"
ca_id = "old-ca"
profile = "serverCert"

Migration procedure:

  1. Start with weights = { "old-ca" = 100, "new-ca" = 0 } (new CA online but unused)
  2. Update to weights = { "old-ca" = 90, "new-ca" = 10 } (10% canary traffic)
  3. Monitor logs and metrics; if no issues, increase to 80/20, 50/50, 20/80
  4. Finish with weights = { "old-ca" = 0, "new-ca" = 100 } (full cutover)
  5. Remove old-ca from the configuration once fully decommissioned

No client reconfiguration required—weights can be adjusted without restarting kipuka.

Database Backends

kipuka uses a database to store issued certificates, audit logs, OTP tokens, and certificate authority state. Three backend options are supported: SQLite, PostgreSQL, and MariaDB. The database backend is configured via the [db] section in kipuka.toml.

SQLite

SQLite is the default backend and requires zero configuration. It is ideal for single-node deployments, development environments, and small to medium workloads.

URL Format

[db]
url = "sqlite:///var/lib/kipuka/kipuka.db"
auto_migrate = true

For in-memory testing:

[db]
url = "sqlite://:memory:"

Features and Characteristics

  • Write-Ahead Logging (WAL): kipuka enables WAL mode by default, allowing concurrent reads while a write transaction is in progress.
  • File Permissions: The database file should be owned by the kipuka service user with mode 0600 to prevent unauthorized access.
  • Single-Writer: SQLite supports only one concurrent writer. This is not a limitation for single-node deployments but makes it unsuitable for high-availability configurations with shared state.
  • No Network Access: The database must be on the local filesystem. Network-mounted filesystems (NFS, SMB) are not recommended due to locking and performance issues.

Backup

SQLite databases can be backed up using:

  • File copy: While the WAL is checkpointed (kipuka handles this automatically), the database file can be copied. Use a consistent snapshot method to avoid corruption.

  • sqlite3 .backup: The SQLite command-line tool provides a .backup command for online backups:

    sqlite3 /var/lib/kipuka/kipuka.db ".backup /backup/kipuka-$(date +%Y%m%d).db"
    

Best For

  • Single-node deployments
  • Development and testing
  • Small to medium workloads (< 1000 requests/minute)
  • Scenarios where operational simplicity is prioritized

Limitations

  • Single writer (not suitable for multi-node HA with shared state)
  • No network access (cannot be shared across nodes)
  • Limited to local filesystem performance

PostgreSQL

PostgreSQL is the recommended backend for production multi-node deployments. It provides robust support for concurrent writes, network access, replication, and point-in-time recovery.

URL Format

[db]
url = "postgres://kipuka:[email protected]:5432/kipuka?sslmode=require"
max_connections = 20
connect_timeout = "5s"
auto_migrate = true

Features and Characteristics

  • Concurrent Writes: Multiple kipuka nodes can write to the database simultaneously.
  • Network Access: The database can be accessed over the network, enabling multi-node deployments.
  • Replication: PostgreSQL supports streaming replication for read replicas and high availability.
  • Point-in-Time Recovery (PITR): WAL archiving enables recovery to any point in time.
  • Connection Pooling: kipuka maintains a connection pool sized by max_connections.

Connection Pool Tuning

The max_connections setting controls the size of kipuka’s connection pool to PostgreSQL:

  • Default: 5 (suitable for low-load scenarios)
  • Formula: Start with workers * 2 and adjust based on monitoring.
  • Too Low: Connection pool exhaustion under load, causing requests to queue.
  • Too High: Unnecessary resource consumption; may exceed PostgreSQL’s max_connections limit (default 100).

Monitor pool utilization via the Admin API endpoint /admin/health, which reports connection pool statistics.

PostgreSQL Configuration

Ensure max_connections in postgresql.conf is set higher than the sum of all kipuka nodes’ max_connections. Leave headroom for other applications and administrative connections.

# postgresql.conf
max_connections = 100

SSL Connections

Append ?sslmode=require to the URL to enforce SSL/TLS connections:

url = "postgres://kipuka:${DB_PASSWORD}@db.example.com:5432/kipuka?sslmode=require"

Best For

  • Production environments
  • High-availability deployments (with replication)
  • High-throughput scenarios (> 1000 requests/minute)
  • Multi-node clusters with shared state

MariaDB

MariaDB is an alternative to PostgreSQL and supports Galera Cluster for synchronous multi-master replication. It is well-suited for organizations already standardized on MariaDB or requiring Galera-based high availability.

URL Format

[db]
url = "mysql://kipuka:[email protected]:3306/kipuka"
max_connections = 20
connect_timeout = "5s"
auto_migrate = true

Features and Characteristics

  • Galera Cluster: Provides synchronous multi-master replication, allowing writes to any node in the cluster.
  • Concurrent Writes: Multiple kipuka nodes can write to the database simultaneously.
  • Network Access: The database can be accessed over the network.

Galera Considerations

  • Replication Format: Use ROW-based replication (binlog_format=ROW) for deterministic replication.
  • Synchronous Reads: Set wsrep_sync_wait=1 to ensure reads reflect writes from all nodes (reads-after-writes consistency).
# MariaDB Galera configuration
wsrep_sync_wait=1
binlog_format=ROW

Best For

  • Organizations standardized on MariaDB
  • Galera Cluster-based high availability
  • Multi-master replication requirements

Connection URL Formats Summary

BackendURL FormatDefault Port
SQLitesqlite://path/to/dbN/A
PostgreSQLpostgres://user:pass@host:port/db5432
MariaDBmysql://user:pass@host:port/db3306

Migrations

kipuka uses versioned, idempotent migrations to manage database schema changes. Migrations can be applied automatically on startup or run manually as a separate step.

Automatic Migrations

Set auto_migrate = true (the default) to apply pending migrations on startup:

[db]
auto_migrate = true

This is convenient for development and single-node deployments, but may not be suitable for production environments where migrations should be reviewed and tested before deployment.

Manual Migrations

Set auto_migrate = false to disable automatic migrations:

[db]
auto_migrate = false

Then run migrations manually using the kipuka migrate command:

# Show pending migrations
kipuka migrate status

# Apply pending migrations
kipuka migrate run

Best Practices

  • Production: Run migrations as a separate step before starting the server. This allows you to review migrations, test them in a staging environment, and coordinate downtime if necessary.
  • Development: Use auto_migrate = true for convenience.
  • Idempotency: Migrations are idempotent and can be re-run safely. If a migration is interrupted, re-running it will complete the operation.

max_connections Tuning

The max_connections setting controls the size of kipuka’s database connection pool. Tuning this parameter is critical for balancing resource utilization and performance.

Default

The default is 5, which is suitable for SQLite and small deployments with low concurrency.

Tuning Formula

Start with the formula:

max_connections = workers * 2

Adjust based on monitoring and observed load. For example, if kipuka is configured with 4 workers, start with max_connections = 8.

Symptoms of Incorrect Settings

  • Too Low: Connection pool exhaustion under load. Requests will queue waiting for an available connection, increasing latency.
  • Too High: Unnecessary resource consumption (memory, file descriptors). May exceed the database’s max_connections limit, causing connection failures.

Monitoring

Use the Admin API endpoint /admin/health to monitor connection pool utilization:

curl http://localhost:8080/admin/health

The response includes metrics such as active connections, idle connections, and pool size.

PostgreSQL Configuration

Ensure PostgreSQL’s max_connections setting is higher than the sum of all kipuka nodes’ max_connections. For example:

  • 3 kipuka nodes, each with max_connections = 20 → 60 total
  • PostgreSQL max_connections = 100 → 40 connections available for other applications and administrative tasks

Credential Security

Database connection URLs often contain sensitive credentials (usernames and passwords). Follow these best practices to protect credentials:

Environment Variable Substitution

kipuka supports environment variable substitution in the url field using the ${ENV_VAR} syntax:

[db]
url = "postgres://kipuka:${DB_PASSWORD}@db.example.com:5432/kipuka"

Set the environment variable before starting kipuka:

export DB_PASSWORD="your-secure-password"
kipuka run

Or use a systemd service file with EnvironmentFile:

[Service]
EnvironmentFile=/etc/kipuka/db.env
ExecStart=/usr/bin/kipuka run

Never Commit Passwords

Never commit plaintext passwords to version control. Use:

  • Environment variables (as shown above)
  • Secret management systems (e.g., HashiCorp Vault, AWS Secrets Manager)
  • Encrypted configuration files with restricted file permissions

Example: Secrets Manager Integration

For production deployments, consider integrating with a secrets manager:

# Fetch password from secrets manager
export DB_PASSWORD=$(vault kv get -field=password secret/kipuka/db)

# Start kipuka
kipuka run

This ensures credentials are never written to disk in plaintext and can be rotated without modifying configuration files.

Audit Logging

kipuka implements comprehensive audit logging designed to meet NIAP Protection Profile for Certification Authorities requirements, specifically FAU_GEN.1 (Audit Data Generation). All security-relevant events are recorded with sufficient detail for forensic analysis and compliance verification.

NIAP FAU_GEN.1 Compliance

kipuka’s audit logging implementation satisfies the following FAU_GEN.1 requirements:

  • Completeness: All security-relevant events are recorded, including certificate issuance, revocation, authentication attempts, and administrative actions.
  • Sufficient Detail: Each audit record contains timestamp (UTC), event type, outcome (success/failure), subject identity, source IP address, and event-specific data necessary for forensic analysis.
  • Tamper Evidence: Audit records are append-only. The database audit tables permit only INSERT operations; UPDATE and DELETE operations are prohibited at the schema level.
  • Reliability: Audit records are written synchronously to the database, with optional asynchronous replication to file and syslog destinations.

Audit Event Types

kipuka records 22 distinct event types across four categories:

Event NameCategoryDescription
cert.issuedCertificateCertificate successfully issued
cert.deniedCertificateCertificate request denied (policy violation)
cert.revokedCertificateCertificate revoked
cert.renewedCertificateCertificate renewed via simplereenroll
cert.expiredCertificateCertificate reached expiration
enroll.requestEnrollmentsimpleenroll request received
enroll.successEnrollmentEnrollment completed successfully
enroll.failureEnrollmentEnrollment failed
reenroll.requestEnrollmentsimplereenroll request received
reenroll.successEnrollmentRe-enrollment completed successfully
reenroll.failureEnrollmentRe-enrollment failed
auth.mtls.successAuthenticationmTLS authentication succeeded
auth.mtls.failureAuthenticationmTLS authentication failed
auth.otp.successAuthenticationOTP authentication succeeded
auth.otp.failureAuthenticationOTP authentication failed
auth.otp.lockoutAuthenticationOTP account locked due to excessive failures
auth.gssapi.successAuthenticationGSSAPI/Kerberos authentication succeeded
auth.gssapi.failureAuthenticationGSSAPI/Kerberos authentication failed
admin.accessAdminAdmin API endpoint accessed
otp.createdAdminOTP token provisioned
ca.health.changedSystemCA health state changed (HA)
server.startupSystemkipuka server started
server.shutdownSystemkipuka server stopped

Audit Destinations

kipuka supports three audit destinations that operate independently. The database destination is always active; file and syslog destinations are optional.

Database (Always Active)

All audit events are written to the audit_log table in the kipuka database. This destination is always active and cannot be disabled.

Characteristics:

  • INSERT-only: The database schema enforces that audit records can only be inserted, never updated or deleted.
  • Queryable: Audit records can be retrieved via the Admin API for compliance reporting and forensic analysis.
  • Persistent: Records remain in the database until explicitly purged by an administrator through documented procedures.

File (Append-Only JSON)

When configured, kipuka writes audit events to a file in JSON Lines format (one JSON object per line). The file is opened with the O_APPEND flag to ensure records cannot be overwritten.

Configuration:

[audit]
file = "/var/log/kipuka/audit.json"

Rotation: Use logrotate with the copytruncate option or equivalent log rotation tools. Example logrotate configuration:

/var/log/kipuka/audit.json {
    daily
    rotate 90
    compress
    delaycompress
    copytruncate
    missingok
    notifempty
}

Example JSON Record:

{
  "timestamp": "2026-06-24T14:32:10.123456Z",
  "event_type": "cert.issued",
  "outcome": "success",
  "subject": "CN=server.example.com",
  "source_ip": "192.0.2.45",
  "serial": "4A:3F:21:8C:09:77:34:2B",
  "issuer": "CN=Example CA",
  "not_before": "2026-06-24T14:32:00Z",
  "not_after": "2027-06-24T14:32:00Z"
}

Syslog (RFC 5424, Optional TLS)

kipuka can forward audit events to a remote syslog server using RFC 5424 format. UDP, TCP, and TLS-encrypted TCP are supported.

Configuration:

[audit]
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local0"

Supported URL Schemes:

  • udp://host:port - Unencrypted UDP (not recommended for production)
  • tcp://host:port - Unencrypted TCP
  • tcp+tls://host:port - TLS-encrypted TCP (recommended for NIAP compliance)

Syslog Facilities: The syslog_facility option accepts local0 through local7. Default is local0.

Structured Data: Audit events are encoded as RFC 5424 structured data elements. Example syslog message:

<134>1 2026-06-24T14:32:10.123456Z server1 kipuka 12345 cert.issued [audit event_type="cert.issued" outcome="success" subject="CN=server.example.com" source_ip="192.0.2.45" serial="4A:3F:21:8C:09:77:34:2B"]

Event Filtering

The events array in the [audit] section controls which events are forwarded to file and syslog destinations. The database always records all events regardless of this filter.

Default Behavior: If the events array is omitted or empty, all event types are forwarded to file and syslog.

Filtering Example: To log only security-critical events to file/syslog:

[audit]
file = "/var/log/kipuka/audit.json"
events = [
    "cert.issued",
    "cert.denied",
    "cert.revoked",
    "auth.otp.failure",
    "auth.otp.lockout",
    "auth.mtls.failure",
    "admin.access",
]

In this configuration, successful routine operations (e.g., enroll.success, auth.otp.success) are recorded only in the database, reducing file and syslog volume while maintaining complete audit trail in the database for compliance queries.

Certificate Data in Audit Records

By default, audit records for certificate lifecycle events include metadata (serial number, subject, issuer, validity dates) but not the full certificate. The include_cert_data option controls whether the complete PEM-encoded certificate is included.

Default (include_cert_data = false):

{
  "timestamp": "2026-06-24T14:32:10.123456Z",
  "event_type": "cert.issued",
  "serial": "4A:3F:21:8C:09:77:34:2B",
  "subject": "CN=server.example.com",
  "issuer": "CN=Example CA",
  "not_before": "2026-06-24T14:32:00Z",
  "not_after": "2027-06-24T14:32:00Z"
}

With Full Certificate Data (include_cert_data = true):

{
  "timestamp": "2026-06-24T14:32:10.123456Z",
  "event_type": "cert.issued",
  "serial": "4A:3F:21:8C:09:77:34:2B",
  "subject": "CN=server.example.com",
  "issuer": "CN=Example CA",
  "not_before": "2026-06-24T14:32:00Z",
  "not_after": "2027-06-24T14:32:00Z",
  "certificate": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAEo/IYwJdzQr...\n-----END CERTIFICATE-----"
}

Trade-offs:

  • Enabling include_cert_data significantly increases audit log size (typical certificate is 1-2 KB).
  • Required by some compliance frameworks (e.g., certain government PKI policies) for complete certificate lifecycle tracking.
  • Useful for forensic analysis when the original certificate is no longer available in the database.

Configuration Examples

Minimal Configuration (File Only)

[audit]
file = "/var/log/kipuka/audit.json"

This configuration writes all audit events to a local file with default settings.

Production Configuration (File + Syslog over TLS)

[audit]
file = "/var/log/kipuka/audit.json"
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local1"
events = [
    "cert.issued",
    "cert.denied",
    "cert.revoked",
    "auth.otp.failure",
    "auth.otp.lockout",
    "auth.mtls.failure",
    "admin.access",
    "otp.created",
    "server.startup",
    "server.shutdown",
]

This configuration:

  • Logs all events to the database (always active).
  • Logs security-critical events to both file and remote syslog over TLS.
  • Reduces file and syslog volume by filtering routine successful operations.

NIAP-Compliant Configuration

[audit]
file = "/var/log/kipuka/audit.json"
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local0"
include_cert_data = true
# events array omitted: all events forwarded to file and syslog

This configuration:

  • Logs all 22 event types to database, file, and syslog.
  • Uses TLS-encrypted syslog for confidentiality and integrity.
  • Includes full certificate data in audit records for complete lifecycle tracking.
  • Meets NIAP FAU_GEN.1 requirements for completeness, detail, and tamper evidence.

Note: Enabling include_cert_data will substantially increase storage requirements. Plan for approximately 2-5 KB per certificate lifecycle event (issuance, renewal, revocation).

Admin API

The Admin API provides privileged endpoints for server management, monitoring, OTP provisioning, and certificate lifecycle operations. It runs on a separate port from the EST service to enable network-level access control and is disabled by default for security.

Enabling the Admin API

The Admin API is disabled by default. To enable it, configure both the [admin] section and the admin_listen address in [server].

The Admin API runs on a separate port (default 9443) to allow network-level access control. Bind to 127.0.0.1 for localhost-only access, or to a management VLAN IP for remote administration:

[server]
admin_listen = "127.0.0.1:9443"

[admin]
enabled = true
auth = "mtls"
trust_anchors = "/etc/kipuka/admin-ca.pem"

Authentication Methods

The Admin API supports three authentication modes configured via the auth parameter:

mTLS Authentication

Admin clients must present a certificate signed by the admin trust anchors CA. This is the most secure option and recommended for production environments.

[admin]
enabled = true
auth = "mtls"
trust_anchors = "/etc/kipuka/admin-ca.pem"

Bearer Token Authentication

Admin clients provide a bearer token in the Authorization header. The token value is loaded from the environment variable specified by bearer_token_env.

[admin]
enabled = true
auth = "bearer"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Clients authenticate using:

curl -H "Authorization: Bearer $KIPUKA_ADMIN_TOKEN" \
  https://localhost:9443/admin/health

Both Authentication

Either mTLS or bearer token accepted. Useful during migration periods or for mixed tooling environments.

[admin]
enabled = true
auth = "both"
trust_anchors = "/etc/kipuka/admin-ca.pem"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Endpoints Overview

MethodPathDescription
GET/admin/healthServer health and component status
GET/admin/health/readyReadiness probe (for Kubernetes)
GET/admin/health/liveLiveness probe (for Kubernetes)
GET/admin/caList all configured CAs with status
GET/admin/ca/{id}Get specific CA details and health
GET/admin/ca/{id}/chainGet CA certificate chain (PEM)
POST/admin/otpGenerate a new OTP token
GET/admin/otpList active (unused) OTP tokens
DELETE/admin/otp/{token_id}Revoke an unused OTP token
GET/admin/certsList issued certificates (paginated)
GET/admin/certs/{serial}Get certificate details by serial
POST/admin/certs/{serial}/revokeRevoke a certificate
GET/admin/auditQuery audit log (paginated, filterable)
GET/admin/metricsPrometheus-format metrics

OTP Provisioning Workflow

The OTP provisioning workflow enables secure initial enrollment for devices that do not yet have certificates.

Step 1: Admin Generates OTP

The administrator creates a new OTP token with specified constraints:

curl -s --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  -X POST https://localhost:9443/admin/otp \
  -H "Content-Type: application/json" \
  -d '{"label": "device-42", "ttl": "24h", "max_uses": 1}'

Response:

{
  "token_id": "01234567-89ab-cdef-0123-456789abcdef",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires": "2026-06-25T14:30:00Z",
  "label": "device-42",
  "max_uses": 1
}

Parameters:

  • label: Human-readable identifier for the token (for audit logs)
  • ttl: Time-to-live (e.g., “24h”, “7d”, “1h30m”)
  • max_uses: Maximum number of times the token can be used (typically 1)

Step 2: Admin Delivers Token to Client Operator

The token value must be delivered out-of-band through a secure channel:

  • Encrypted email
  • Ticketing system
  • Secure messaging platform
  • In-person handoff

Never transmit OTP tokens over unencrypted channels.

Step 3: Client Uses OTP for Initial Enrollment

The client operator uses the OTP token for initial EST enrollment:

curl --cacert ca.pem \
  -u ":eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  --data-binary @csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o cert.p7 \
  https://est.example.com/.well-known/est/simpleenroll

Note the colon prefix in the -u flag: HTTP Basic Auth uses username:password format, and OTP tokens are submitted as the password with an empty username.

Step 4: OTP Consumed After Successful Enrollment

After successful enrollment, the OTP is marked as consumed if max_uses=1. Subsequent attempts to use the same token will be rejected.

Step 5: Future Renewals Use Certificate-Based mTLS

Once the device has a certificate, all future operations (reenrollment, renewal) use the issued certificate for mTLS authentication. The OTP is no longer required.

CA Status Monitoring

List All CAs

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/ca

Response:

[
  {
    "id": "primary-ca",
    "name": "Primary Issuing CA",
    "status": "healthy",
    "last_check": "2026-06-24T12:34:56Z",
    "cert_not_after": "2028-12-31T23:59:59Z",
    "certs_issued_today": 42,
    "certs_issued_total": 15234
  },
  {
    "id": "backup-ca",
    "name": "Backup Issuing CA",
    "status": "healthy",
    "last_check": "2026-06-24T12:34:56Z",
    "cert_not_after": "2029-06-30T23:59:59Z",
    "certs_issued_today": 0,
    "certs_issued_total": 0
  }
]

Status values:

  • healthy: CA is operational and passing all health checks
  • degraded: CA is operational but experiencing issues (e.g., approaching expiration)
  • unhealthy: CA is not operational (e.g., expired, HSM unreachable)

Get Specific CA Details

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/ca/primary-ca

This endpoint provides detailed status for a single CA, including backend-specific health information.

Get CA Certificate Chain

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/ca/primary-ca/chain

Returns the CA certificate chain in PEM format. This is useful for distributing trust anchors to clients.

Aggregate Health Status

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/health

Response:

{
  "status": "healthy",
  "components": {
    "database": "healthy",
    "cas": {
      "primary-ca": "healthy",
      "backup-ca": "healthy"
    },
    "hsm": "healthy"
  },
  "uptime": "3d 14h 22m"
}

Use this endpoint for monitoring dashboards and alerting. Aggregate status reflects the worst status of any component:

  • All components healthy: healthy
  • Any component degraded: degraded
  • Any component unhealthy: unhealthy

Kubernetes Probes

For Kubernetes deployments, use the dedicated probe endpoints:

Readiness probe (server is ready to accept traffic):

curl https://localhost:9443/admin/health/ready

Liveness probe (server is running):

curl https://localhost:9443/admin/health/live

Configure in your Deployment manifest:

livenessProbe:
  httpGet:
    path: /admin/health/live
    port: 9443
    scheme: HTTPS
  initialDelaySeconds: 10
  periodSeconds: 30

readinessProbe:
  httpGet:
    path: /admin/health/ready
    port: 9443
    scheme: HTTPS
  initialDelaySeconds: 5
  periodSeconds: 10

Certificate Management

List Issued Certificates

List certificates with optional filtering and pagination:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  "https://localhost:9443/admin/certs?status=active&ca=primary-ca&page=1&per_page=50"

Query parameters:

  • status: Filter by certificate status (active, revoked, expired)
  • ca: Filter by issuing CA ID
  • subject: Filter by subject DN (substring match)
  • page: Page number (1-indexed)
  • per_page: Results per page (default 50, max 1000)

Response:

{
  "certs": [
    {
      "serial": "1a2b3c4d5e6f7890",
      "subject": "CN=device-42.example.com",
      "issuer": "CN=Primary Issuing CA",
      "not_before": "2026-06-24T00:00:00Z",
      "not_after": "2027-06-24T23:59:59Z",
      "status": "active",
      "ca_id": "primary-ca"
    }
  ],
  "total": 15234,
  "page": 1,
  "per_page": 50
}

Get Certificate Details

Retrieve detailed information for a specific certificate by serial number:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/certs/1a2b3c4d5e6f7890

Response includes full certificate details, issuance metadata, and audit trail.

Revoke a Certificate

Revoke a certificate by serial number with a reason code:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  -X POST https://localhost:9443/admin/certs/1a2b3c4d5e6f7890/revoke \
  -H "Content-Type: application/json" \
  -d '{"reason": "keyCompromise"}'

Valid reason codes (per RFC 5280):

  • unspecified: Default reason
  • keyCompromise: Private key compromise
  • caCompromise: CA key compromise
  • affiliationChanged: Subject changed affiliation
  • superseded: Certificate replaced
  • cessationOfOperation: Certificate no longer needed
  • privilegeWithdrawn: Certificate privileges revoked

Response:

{
  "serial": "1a2b3c4d5e6f7890",
  "status": "revoked",
  "revocation_time": "2026-06-24T14:30:00Z",
  "revocation_reason": "keyCompromise"
}

The certificate is immediately marked as revoked in the database and will appear in the next CRL update.

Audit Log Queries

Query the audit log for security analysis and compliance:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  "https://localhost:9443/admin/audit?action=enroll&start=2026-06-01&end=2026-06-24&page=1&per_page=100"

Query parameters:

  • action: Filter by action type (enroll, reenroll, serverkeygen, revoke, otp_generate, otp_use)
  • subject: Filter by certificate subject DN
  • ca: Filter by CA ID
  • start: Start timestamp (ISO 8601)
  • end: End timestamp (ISO 8601)
  • page: Page number (1-indexed)
  • per_page: Results per page (default 100, max 1000)

Response:

{
  "events": [
    {
      "timestamp": "2026-06-24T10:15:30Z",
      "action": "enroll",
      "subject": "CN=device-42.example.com",
      "ca_id": "primary-ca",
      "result": "success",
      "client_ip": "10.0.1.42",
      "auth_method": "otp",
      "serial": "1a2b3c4d5e6f7890"
    }
  ],
  "total": 1523,
  "page": 1,
  "per_page": 100
}

Metrics

The Admin API exposes Prometheus-format metrics for monitoring:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/metrics

Key metrics include:

  • kipuka_enrollments_total: Counter of enrollment operations by CA and result
  • kipuka_reenrollments_total: Counter of reenrollment operations by CA and result
  • kipuka_revocations_total: Counter of revocation operations by CA and reason
  • kipuka_otp_generated_total: Counter of OTP tokens generated
  • kipuka_otp_consumed_total: Counter of OTP tokens consumed
  • kipuka_request_duration_seconds: Histogram of request durations by endpoint
  • kipuka_active_certificates: Gauge of currently active certificates by CA
  • kipuka_ca_health: Gauge of CA health status (1=healthy, 0=unhealthy)

Configure Prometheus to scrape the Admin API endpoint with appropriate mTLS credentials.

Dogtag PKI Integration

Overview

kipuka can use Red Hat Certificate System (Dogtag PKI) as a backend CA instead of, or alongside, file-based or HSM-backed local CAs. This integration enables organizations already running Dogtag/RHCS to add EST enrollment capabilities without modifying their existing PKI infrastructure.

Architecture:

  • Dogtag provides a full-featured PKI with REST API, comprehensive audit logging, HSM support, and complete certificate lifecycle management
  • kipuka acts as an EST frontend (Registration Authority) to Dogtag’s CA subsystem
  • The integration preserves all Dogtag policies, approval workflows, and certificate profiles while exposing EST protocol endpoints

Benefits:

  • Leverage existing Dogtag infrastructure and operational expertise
  • Maintain centralized certificate policy enforcement in Dogtag profiles
  • Benefit from Dogtag’s enterprise features: HSM integration, audit trails, approval workflows, Key Recovery Authority (KRA)
  • Add EST enrollment to IoT devices, network equipment, and mobile endpoints without deploying new CA infrastructure

REST API Client

kipuka communicates with Dogtag exclusively via its REST API, typically exposed at https://dogtag.example.com:8443/ca/rest/certrequests.

Authentication

kipuka authenticates to Dogtag using a client certificate (agent cert) that has enrollment privileges. This certificate must be issued by the Dogtag CA itself and associated with an agent user in the Dogtag database.

Obtaining an agent certificate:

  1. On the Dogtag server, create an agent user (if not already present):

    pki -d /var/lib/pki/pki-tomcat/alias -c <password> \
        ca-user-add kipuka-agent --fullName "kipuka EST RA"
    
  2. Generate a certificate request and submit it for the agent certificate:

    pki -d /var/lib/pki/pki-tomcat/alias -c <password> \
        client-cert-request "CN=kipuka EST RA,O=Example Corp" \
        --profile caAgentServerCert
    
  3. Approve and issue the certificate via the Dogtag web UI or CLI

  4. Export the agent certificate and key for use by kipuka

Configuration

Configure Dogtag as a CA entry with type = "dogtag":

[[ca]]
id = "dogtag-ca"
name = "Dogtag Enterprise CA"
type = "dogtag"

# Dogtag instance URL (without /ca/rest suffix)
url = "https://dogtag.example.com:8443"

# Agent certificate for authenticating to Dogtag
agent_cert = "/etc/kipuka/dogtag/agent.pem"
agent_key = "/etc/kipuka/dogtag/agent-key.pem"

# Dogtag CA certificate chain for TLS verification
ca_cert = "/etc/kipuka/dogtag/ca-chain.pem"

# Default certificate profile for this CA
profile = "caServerCert"

# Optional: Request timeout (default: 30s)
timeout = "30s"

# Optional: Enable certificate caching to reduce Dogtag load
cache_certificates = true
cache_ttl = "1h"

File formats:

  • agent_cert and ca_cert: PEM-encoded X.509 certificates
  • agent_key: PEM-encoded RSA or EC private key (can be encrypted; kipuka will prompt for passphrase)

Enrollment

When kipuka receives an EST /simpleenroll request, it translates the PKCS#10 CSR into a Dogtag certificate enrollment request and submits it to the Dogtag REST API.

Profile Mapping

Dogtag uses certificate profiles (e.g., caServerCert, caUserCert, caTPSCert) to define certificate content, extensions, and policy constraints. kipuka maps EST labels to Dogtag profiles:

[[est.label]]
name = "server-tls"
ca_id = "dogtag-ca"
dogtag_profile = "caServerCert"

[[est.label]]
name = "client-auth"
ca_id = "dogtag-ca"
dogtag_profile = "caUserCert"

[[est.label]]
name = "token-signing"
ca_id = "dogtag-ca"
dogtag_profile = "caTPSCert"

Default profile: If no label is provided in the EST request, the profile specified in the [[ca]] entry is used.

Enrollment Flow

  1. Client sends EST /simpleenroll request with PKCS#10 CSR
  2. kipuka validates the CSR and authenticates the client (via HTTP Basic or TLS client cert)
  3. kipuka determines the Dogtag profile based on the EST label
  4. kipuka submits the CSR to Dogtag: POST /ca/rest/certrequests
  5. Dogtag processes the request according to the profile policy:
    • If auto-approval is enabled in the profile, the certificate is issued immediately
    • If approval is required, the request enters pending state
  6. kipuka polls Dogtag until the certificate is issued (or timeout expires)
  7. kipuka returns the issued certificate to the EST client in PKCS#7 format

Manual Approval Workflow

If the Dogtag profile requires manual approval (common for high-assurance certificates):

  1. The enrollment request remains in Dogtag’s pending queue
  2. An agent approves the request via Dogtag web UI or CLI:
    pki ca-cert-request-review <request_id> --action approve
    
  3. kipuka’s next poll retrieves the issued certificate
  4. The EST client receives the certificate (this may take seconds to minutes depending on poll interval)

Polling configuration:

[[ca]]
id = "dogtag-ca"
type = "dogtag"
# ... other config ...

# Poll interval for pending certificate requests (default: 5s)
poll_interval = "5s"

# Maximum time to wait for approval (default: 5m)
approval_timeout = "5m"

If approval_timeout expires before the certificate is issued, kipuka returns an HTTP 202 response to the client with a retry-after header.

Revocation

kipuka’s Admin API revocation calls are forwarded to Dogtag’s revocation endpoint.

Revoke via Admin API

curl -X POST https://kipuka.example.com/admin/revoke \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "serial": "0x1A2B3C4D",
    "reason": "keyCompromise"
  }'

Revocation Flow

  1. kipuka receives the revocation request via Admin API
  2. kipuka maps the serial number to the issuing CA (in this case, dogtag-ca)
  3. kipuka submits a revocation request to Dogtag: POST /ca/rest/agent/certs/<serial>/revoke
  4. Dogtag revokes the certificate and updates its internal database
  5. Dogtag automatically updates CRLs and OCSP responder (if enabled)
  6. kipuka returns success to the caller

Revocation Reason Mapping

kipuka maps its revocation reason codes to Dogtag’s reason codes per RFC 5280:

kipuka ReasonDogtag Reason CodeRFC 5280 Code
unspecified00
keyCompromise11
caCompromise22
affiliationChanged33
superseded44
cessationOfOperation55
certificateHold66
removeFromCRL88
privilegeWithdrawn99
aaCompromise1010

Note: Dogtag does not support removeFromCRL (un-hold) via the REST API for certificates revoked with certificateHold. Use the Dogtag CLI for un-hold operations.

KRA Server-Side Key Generation

EST’s /serverkeygen endpoint can use Dogtag’s Key Recovery Authority (KRA) subsystem for server-side key generation and archival.

Overview

When server-side key generation is requested:

  1. kipuka forwards the request to Dogtag with the KRA archival flag enabled
  2. Dogtag’s KRA subsystem generates the key pair server-side
  3. The private key is encrypted and archived in the KRA database
  4. Dogtag issues the certificate using the generated public key
  5. kipuka receives both the certificate and the encrypted private key
  6. The private key is returned to the EST client as a PKCS#8 EncryptedPrivateKeyInfo wrapped in the EST response

Prerequisites

  • Dogtag KRA subsystem must be installed and configured
  • The KRA must be configured to communicate with the CA subsystem
  • The agent certificate used by kipuka must have key archival privileges

Configuration

[[ca]]
id = "dogtag-ca"
type = "dogtag"
url = "https://dogtag.example.com:8443"

# KRA endpoint (often same host as CA)
kra_url = "https://dogtag.example.com:8443"

# KRA agent credentials (may be same as CA agent or separate)
kra_agent_cert = "/etc/kipuka/dogtag/kra-agent.pem"
kra_agent_key = "/etc/kipuka/dogtag/kra-agent-key.pem"

# ... other CA config ...

If kra_url is not specified, server-side key generation is disabled for this CA.

EST Request

curl -X POST https://kipuka.example.com/.well-known/est/serverkeygen \
  --user client:password \
  --cacert ca.pem \
  -H "Content-Type: application/pkcs10" \
  --data-binary @csr.p10 \
  -o response.p7

The response is a PKCS#7 structure containing:

  • The issued certificate
  • The encrypted private key (PKCS#8 EncryptedPrivateKeyInfo)
  • Optional additional CA certificates

Client-side decryption: The client must decrypt the private key using the password provided during enrollment (passed in the CSR’s challenge password attribute, or via out-of-band key).

Key Recovery

If a client loses their private key, authorized users can recover it from the KRA:

pki kra-key-recover --keyID <key_id> --output recovered-key.p12

This operation requires dual approval from KRA agents (depending on KRA policy).

Supported Key Algorithms

The KRA supports server-side generation of the following key types:

AlgorithmKRA ParameterNotes
RSA 2048rsa:2048Minimum for CA/B Forum BR compliance
RSA 3072rsa:3072Recommended for new deployments
RSA 4096rsa:4096Maximum RSA key size
ECDSA P-256ec:secp256r1NIST P-256 curve
ECDSA P-384ec:secp384r1NIST P-384 curve
ML-KEM-768ml-kem:768Post-quantum key encapsulation (requires Dogtag 11.6+)

The key algorithm is determined by the Dogtag certificate profile. When the EST client’s CSR specifies a key algorithm via the csrattrs response, kipuka maps it to the appropriate KRA parameter.

CMC Passthrough Details

When Full CMC passthrough is enabled (est.fullcmc = true), kipuka acts as a transparent CMC gateway between EST clients and Dogtag. The CMC message format is preserved end-to-end:

  1. Request path: Client -> EST /fullcmc -> kipuka -> Dogtag /ca/rest/certrequests/cmc
  2. Response path: Dogtag CMC response -> kipuka -> EST client

kipuka validates the outer CMS SignedData wrapper but does not modify the inner CMC PKIData payload. This enables:

  • Batch enrollment: Multiple certificate requests in a single CMC transaction
  • Key recovery: CMC-wrapped key recovery requests forwarded to KRA
  • Revocation: CMC revocation requests processed by Dogtag’s RA
  • TPS integration: Token Processing System operations over EST transport

The synta-cmc crate handles CMC message parsing and construction, providing full RFC 5272/5273/5274 compliance.

Multi-CA Pool with Circuit Breaker

Multiple Dogtag instances can be configured as separate CA entries to provide high availability and load distribution.

Configuration

[[ca]]
id = "dogtag-primary"
type = "dogtag"
url = "https://dogtag1.example.com:8443"
agent_cert = "/etc/kipuka/dogtag/agent.pem"
agent_key = "/etc/kipuka/dogtag/agent-key.pem"
ca_cert = "/etc/kipuka/dogtag/ca-chain.pem"
profile = "caServerCert"

[[ca]]
id = "dogtag-secondary"
type = "dogtag"
url = "https://dogtag2.example.com:8443"
agent_cert = "/etc/kipuka/dogtag/agent.pem"
agent_key = "/etc/kipuka/dogtag/agent-key.pem"
ca_cert = "/etc/kipuka/dogtag/ca-chain.pem"
profile = "caServerCert"

[ha]
enabled = true
strategy = "active-passive"

[[ha.group]]
name = "dogtag-pool"
ca_ids = ["dogtag-primary", "dogtag-secondary"]

# Health check interval
health_check_interval = "30s"

# Circuit breaker thresholds
failure_threshold = 3
success_threshold = 2
timeout = "10s"

Failover Behavior

Active-Passive Strategy:

  • All requests are routed to dogtag-primary
  • If dogtag-primary fails health checks (3 consecutive failures), the circuit breaker trips
  • Traffic is routed to dogtag-secondary while dogtag-primary is unavailable
  • Once dogtag-primary passes health checks (2 consecutive successes), traffic is restored to primary

Active-Active Strategy (future):

[ha]
enabled = true
strategy = "active-active"

[[ha.group]]
name = "dogtag-pool"
ca_ids = ["dogtag-primary", "dogtag-secondary"]
load_balancing = "round-robin"  # or "least-connections"

Health Checks

kipuka performs health checks against each Dogtag instance:

GET /ca/rest/certs/ca HTTP/1.1
Host: dogtag1.example.com:8443

A successful response (HTTP 200 with the CA certificate) indicates the instance is healthy.

Monitoring endpoint:

curl https://kipuka.example.com/health/ca

Response:

{
  "dogtag-primary": {
    "status": "healthy",
    "last_check": "2026-06-24T10:30:00Z",
    "consecutive_failures": 0
  },
  "dogtag-secondary": {
    "status": "circuit_open",
    "last_check": "2026-06-24T10:30:00Z",
    "consecutive_failures": 5
  }
}

Full CMC Passthrough (RFC 5272)

When fullcmc = true in the [est] configuration, kipuka can pass Full CMC (Certificate Management over CMS) requests directly to Dogtag.

Overview

Full CMC (RFC 5272/5273/5274) supports complex certificate management operations beyond simple enrollment:

  • Batch enrollment (multiple certificates in a single request)
  • Certificate update/renewal
  • Key recovery requests
  • Revocation requests
  • Get certificate/CRL operations

Dogtag is one of the few CAs that fully implements RFC 5272/5273/5274, making it ideal for enterprise CMC deployments.

Configuration

[est]
enabled = true
fullcmc = true

# Optional: CMC-specific settings
[est.cmc]
# Maximum CMC message size (default: 10MB)
max_message_size = "10MB"

# Allow unsigned CMC requests (default: false)
allow_unsigned = false

CMC Request Flow

  1. Client constructs a Full CMC request (typically using CMCRequest tool or smart card management software)
  2. Client submits the CMC request to kipuka’s EST endpoint: POST /.well-known/est/fullcmc
  3. kipuka validates the CMC request structure and authenticates the client
  4. kipuka forwards the CMC payload to Dogtag: POST /ca/rest/certrequests/cmc
  5. Dogtag processes the CMC request according to its internal policies
  6. Dogtag returns a CMC Full PKI Response
  7. kipuka returns the CMC response to the EST client

Example: Batch Enrollment via CMC

# Create a CMC request with multiple CSRs
CMCRequest \
  -d /path/to/nssdb \
  -i csr1.pem \
  -i csr2.pem \
  -i csr3.pem \
  -o batch-request.cmc

# Submit to kipuka
curl -X POST https://kipuka.example.com/.well-known/est/fullcmc \
  --user agent:password \
  --cacert ca.pem \
  -H "Content-Type: application/pkcs7-mime" \
  --data-binary @batch-request.cmc \
  -o batch-response.cmc

# Parse the response
CMCResponse -d /path/to/nssdb -i batch-response.cmc

Token Processing System (TPS) Integration

Dogtag’s Token Processing System (TPS) uses Full CMC for smart card lifecycle management:

  • Format and enroll smart cards
  • Update certificates on existing tokens
  • Recover keys from KRA to re-provision tokens

kipuka acts as a CMC gateway, enabling TPS to operate over EST endpoints. This is particularly useful for remote TPS operations where direct Dogtag access is restricted by firewall policy.

TPS configuration example:

[[ca]]
id = "dogtag-ca"
type = "dogtag"
url = "https://dogtag.example.com:8443"
kra_url = "https://dogtag.example.com:8443"
tps_url = "https://dogtag.example.com:8443"
# ... agent certs ...

[est.cmc]
# TPS may send large CMC messages with multiple key recovery requests
max_message_size = "50MB"

# TPS operations require signed CMC requests
allow_unsigned = false

Prerequisites

Before configuring kipuka with Dogtag integration, ensure:

  1. Dogtag PKI Installation:

    • Dogtag PKI 10.x or 11.x (RHEL 7/8/9: Red Hat Certificate System 9.x or 10.x)
    • Fedora: Dogtag PKI 11.x available via dnf
    • CA subsystem installed and operational
  2. Agent Certificate:

    • Agent certificate issued by the Dogtag CA with enrollment privileges
    • For KRA integration: separate agent cert with key archival/recovery privileges
    • Agent user configured in Dogtag database and added to appropriate groups
  3. Network Connectivity:

    • kipuka must reach Dogtag REST API port (default: 8443)
    • If using Dogtag behind a load balancer, ensure session affinity for approval workflows
    • Firewall rules permit bidirectional HTTPS between kipuka and Dogtag
  4. TLS Certificates:

    • Dogtag CA certificate chain for TLS verification
    • If Dogtag uses a private CA, install the root and intermediate certificates on kipuka host
  5. Optional: KRA Subsystem:

    • KRA installed and configured if server-side key generation is required
    • KRA transport certificate and storage certificate properly configured
    • KRA connector enabled in CA subsystem
  6. Profiles:

    • Ensure the certificate profiles referenced in kipuka configuration exist in Dogtag
    • Verify profile policies match your security requirements
    • Test profile submission via Dogtag CLI before configuring kipuka

Verification

Test Dogtag connectivity from the kipuka host:

# Verify CA REST API is accessible
curl -k https://dogtag.example.com:8443/ca/rest/certs/ca

# Submit a test certificate request using agent cert
curl -k https://dogtag.example.com:8443/ca/rest/certrequests \
  -X POST \
  --cert /etc/kipuka/dogtag/agent.pem \
  --key /etc/kipuka/dogtag/agent-key.pem \
  -H "Content-Type: application/json" \
  -d '{
    "ProfileID": "caServerCert",
    "Renewal": false,
    "Input": [{
      "Name": "cert_request_type",
      "Value": "pkcs10"
    }, {
      "Name": "cert_request",
      "Value": "<base64-encoded-csr>"
    }]
  }'

A successful response indicates the agent certificate is configured correctly and has enrollment privileges.

See Also

CMP Protocol Configuration

The Certificate Management Protocol (CMP) is defined in RFC 4210 and updated in RFC 9810 (CMP v3). kipuka implements CMP as an alternative to EST for certificate lifecycle operations, providing stronger cryptographic protection options and more granular control over request types.

Overview

CMP vs EST

While EST provides a simpler REST-based API over HTTP, CMP offers:

  • Stronger authentication: MAC-based or signature-based protection of all messages
  • Fine-grained request control: Enable/disable specific operations (initial enrollment, renewal, revocation)
  • Standardized error handling: Well-defined error codes and failure messages
  • Vendor interoperability: Widely implemented in enterprise PKI systems

CMP runs alongside EST on the same server. The endpoint is located at:

/.well-known/cmp

Configuration

Enable CMP in your kipuka.toml configuration file:

[cmp]
enabled = true

# Request types allowed
allow_ir = true      # Initialization Request (initial enrollment)
allow_cr = true      # Certification Request (enrollment with existing cert)
allow_kur = true     # Key Update Request (renewal/rekey)
allow_rr = false     # Revocation Request (certificate revocation)

# Protection mechanisms
allow_mac_protection = true
mac_algorithm = "hmac-sha256"  # Options: hmac-sha256, hmac-sha384, hmac-sha512

# Certificate profile for new enrollments
reference_cert_profile = "server-cert"

# MAC shared secrets for authentication
[[cmp.mac_secrets]]
reference = "device-001"
secret_hex = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"

[[cmp.mac_secrets]]
reference = "device-002"
secret_hex = "f1e2d3c4b5a69788796a5b4c3d2e1f0"

Configuration Options

OptionTypeDefaultDescription
enabledboolfalseEnable CMP endpoint
allow_irbooltrueAllow initialization requests (first-time enrollment)
allow_crbooltrueAllow certification requests (enrollment with proof-of-possession)
allow_kurbooltrueAllow key update requests (renewal/rekey)
allow_rrboolfalseAllow revocation requests
allow_mac_protectionbooltrueAccept MAC-protected messages
mac_algorithmstring“hmac-sha256”MAC algorithm for protection
reference_cert_profilestringNoneDefault certificate profile

Protection Mechanisms

CMP supports two message protection modes:

MAC-Based Protection

MAC-based protection uses a pre-shared secret between the client and server. This is suitable for automated enrollment scenarios where devices are provisioned with shared secrets.

Advantages:

  • No existing certificate required for initial enrollment
  • Simpler client implementation
  • Suitable for IoT and embedded devices

Configuration:

[[cmp.mac_secrets]]
reference = "fleet-alpha"
secret_hex = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"

The reference field matches the sender field in the CMP message header. The secret_hex must be a hex-encoded value (even number of characters).

Signature-Based Protection

Signature-based protection uses an existing certificate to sign CMP messages. This provides stronger authentication when the client already possesses a valid certificate.

Advantages:

  • Stronger cryptographic binding to identity
  • No pre-shared secrets to manage
  • Required for CR and KUR operations in high-security environments

Configuration:

Signature-based protection is automatically available when allow_mac_protection = true is not the only protection method configured. Clients provide their certificate chain in the extraCerts field.

Request Types

IR (Initialization Request)

Used for first-time enrollment when the client has no existing certificate.

Requirements:

  • allow_ir = true
  • MAC protection or administrator-approved request
  • Valid proof-of-possession (POP) in the request

Example curl command (using MAC protection):

# Note: CMP uses binary ASN.1 encoding; this is a conceptual example
# Real CMP clients use libraries like OpenSSL, Bouncy Castle, or CMP-native tools

openssl cmp -cmd ir \
  -server https://est.example.com/.well-known/cmp \
  -ref "device-001" \
  -secret "pass:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
  -newkey device-key.pem \
  -certout device-cert.pem

CR (Certification Request)

Used for enrollment when the client already has a certificate (possibly from another CA).

Requirements:

  • allow_cr = true
  • Signature-based protection using existing certificate
  • Valid proof-of-possession

KUR (Key Update Request)

Used for certificate renewal or rekey operations.

Requirements:

  • allow_kur = true
  • Signature-based protection using current certificate
  • New public key (for rekey) or same key (for renewal)

Example curl command:

openssl cmp -cmd kur \
  -server https://est.example.com/.well-known/cmp \
  -cert current-cert.pem \
  -key current-key.pem \
  -newkey new-key.pem \
  -certout renewed-cert.pem

RR (Revocation Request)

Used to revoke a certificate.

Requirements:

  • allow_rr = true (disabled by default for security)
  • Signature-based protection
  • Authorization check against certificate ownership

Security Note: Revocation requests should be carefully controlled. Consider keeping allow_rr = false and using the administrative API for revocations instead.

Security Considerations

MAC Secret Management

  1. Generate strong secrets: Use at least 32 bytes (256 bits) of cryptographically random data

    openssl rand -hex 32
    
  2. Unique secrets per device: Never reuse MAC secrets across multiple devices

  3. Rotation: Plan for periodic secret rotation. Update both server configuration and device provisioning.

  4. Storage: Protect kipuka.toml with appropriate file permissions (0600). Consider using environment variables or secret management systems for production deployments.

Algorithm Selection

The default hmac-sha256 provides 256-bit security strength, suitable for most deployments. Use stronger algorithms for high-security environments:

mac_algorithm = "hmac-sha384"  # 384-bit security
mac_algorithm = "hmac-sha512"  # 512-bit security

Ensure clients support the selected algorithm.

Request Type Restrictions

Disable unused request types to reduce attack surface:

# Production configuration for renewal-only deployment
allow_ir = false   # Initial enrollment via EST only
allow_cr = false   # No cross-CA enrollment
allow_kur = true   # Renewal enabled
allow_rr = false   # Revocation via admin API only

Troubleshooting

Client reports “MAC verification failed”

Cause: Mismatch between client and server shared secret, or incorrect reference number.

Solution:

  1. Verify the reference field in the client configuration matches a [[cmp.mac_secrets]] entry
  2. Confirm the shared secret is identical (case-sensitive hex string)
  3. Check server logs for the reference number received

“Request type not allowed” error

Cause: The requested operation (IR, CR, KUR, RR) is disabled in server configuration.

Solution:

  1. Check allow_ir, allow_cr, allow_kur, allow_rr settings
  2. Enable the required request type or use an alternative enrollment method (EST)

Certificate profile errors

Cause: reference_cert_profile points to a non-existent profile.

Solution:

  1. Verify the profile name matches an entry in the [cert_profiles] section
  2. Remove reference_cert_profile to use the default profile
  3. Check server logs for available profile names

Performance issues with many devices

Solution:

  • Consider using signature-based protection for high-volume deployments (eliminates MAC secret storage per device)
  • Implement client-side caching of CA certificates
  • Use HTTP/2 connection pooling

Integration Examples

IoT Device Provisioning

Provision devices with unique MAC secrets during manufacturing:

[[cmp.mac_secrets]]
reference = "sensor-{{ DEVICE_SERIAL }}"
secret_hex = "{{ PROVISIONED_SECRET }}"

Use template expansion in your provisioning system to generate unique entries.

Enterprise PKI Integration

Allow CMP for renewals, EST for initial enrollment:

[cmp]
enabled = true
allow_ir = false   # Use EST for initial enrollment
allow_kur = true   # CMP for renewals
allow_mac_protection = false  # Signature-based only

Air-Gapped Environments

Combine CMP with CMS-EST for disconnected operations:

[cmp]
enabled = true
allow_mac_protection = true

[cms_est]
enabled = true
require_signed_requests = true

See CMS-EST Configuration for details on offline enrollment workflows.

CMS-EST Configuration

CMS-EST (Cryptographic Message Syntax for EST) extends the Enrollment over Secure Transport protocol with CMS wrapping, as defined in RFC 8295. This enables EST operations in disconnected, air-gapped, or store-and-forward scenarios where direct HTTPS connectivity is unavailable.

Overview

CMS-EST vs Plain EST

Plain EST (RFC 7030) requires a direct HTTPS connection between client and server. CMS-EST allows:

  • Offline enrollment: Generate CMS-wrapped requests on disconnected systems
  • Store-and-forward: Transport requests via removable media, email, or batch processing
  • Enhanced security: Mandatory message signing and optional encryption
  • Air-gapped operations: No network connectivity required during request generation

The trade-off is increased complexity and message size due to CMS envelope overhead.

Use Cases

Air-Gapped Manufacturing

Enroll devices in a secure manufacturing facility without internet access:

  1. Device generates CSR and wraps it in CMS on the factory floor
  2. Operator transfers CMS file to network-connected workstation via USB
  3. Workstation submits to kipuka CMS-EST endpoint
  4. Signed certificate response is transferred back via USB
  5. Device imports certificate

Batch Processing

Process enrollment requests from hundreds of devices simultaneously:

  1. Devices generate CMS-wrapped enrollment requests during off-peak hours
  2. Requests are queued on a management server
  3. Batch submission to kipuka during maintenance window
  4. Encrypted responses are distributed back to devices

Disconnected Field Operations

Military, maritime, or remote infrastructure scenarios:

  1. Field technicians generate enrollment requests locally
  2. Requests are stored until network connectivity is available
  3. Periodic sync transfers requests to headquarters
  4. Responses are synced back during next connectivity window

Frozen Device Enrollment During PQC Migration

As organizations migrate from classical cryptography (RSA/ECC) to post-quantum algorithms (ML-DSA, ML-KEM), some devices cannot migrate — sealed hardware, regulatory-locked platforms (FDA 510(k), Common Criteria EAL4+), and long-lifecycle embedded systems (20-30 year weapon platforms, industrial SCADA controllers). These “frozen devices” will continue using classical certificates for their entire remaining service life.

CMS-EST is the enrollment protocol for frozen devices because these systems typically:

  • Operate behind air gaps or data diodes with no direct CA connectivity
  • Cannot run ACME clients (constrained protocol stacks)
  • Require offline enrollment that survives network outages spanning weeks or months
  • Need certificate renewal without firmware updates

Enrollment workflow for frozen devices:

  1. Frozen device generates a CSR using its existing classical key (RSA/ECC)
  2. CSR is wrapped in CMS SignedData for integrity and optionally EnvelopedData for confidentiality
  3. CMS-wrapped request crosses the air gap via removable media, data diode, or courier
  4. Operator submits to kipuka’s CMS-EST endpoint on the connected network
  5. kipuka processes the request against the Dogtag CA, which issues a classical certificate per the device’s certificate profile
  6. Signed certificate response is CMS-wrapped and carried back across the air gap
  7. Device imports the certificate

MTC transparency integration:

When combined with Merkle Tree Certificate transparency logging, every classical certificate issued to a frozen device is recorded in a tamper-evident log. An independent monitor validates each issuance against a frozen device registry:

  • Authorized: The device identity, issuing CA, and algorithm match the frozen device record → issuance proceeds normally
  • Unauthorized identity: A classical certificate is requested for an entity not in the frozen device registry → alert (potential attack or misconfiguration)
  • Wrong CA: A classical certificate for a known frozen device is issued by an unexpected CA → alert (CA compromise)
  • Wrong algorithm: A classical certificate uses a weaker algorithm than authorized (e.g., RSA-1024 instead of RSA-2048) → alert (downgrade)
  • Expired authorization: A classical certificate is requested after the frozen device record’s expiry date → alert (device should have been migrated or decommissioned)

This turns frozen devices from an unmonitored security blind spot into an audited, policy-controlled exception within the broader PQC migration.

Configuration for frozen device profiles:

[cms_est]
enabled = true
require_signed_requests = true
encrypt_responses = true

# Frozen device certificate profiles restrict algorithm and validity
# These are enforced by the Dogtag CA certificate profile, not kipuka
# kipuka passes the enrollment request to the CA, which applies profile constraints

The Dogtag CA certificate profile for frozen devices should enforce:

  • Classical algorithms only (RSA-2048, RSA-3072, or ECDSA-P256 per device capability)
  • Maximum validity period aligned with the device’s remaining service life
  • Key usage restrictions appropriate to the device’s function
  • Extended key usage limiting the certificate’s applicability

Configuration

Enable CMS-EST in your kipuka.toml configuration:

[cms_est]
enabled = true

# Message protection requirements
require_signed_requests = true     # Mandate digital signatures on all requests
encrypt_responses = true           # Encrypt all response messages

# Encryption algorithms (in preference order)
allowed_content_encryption = [
    "AES-256-GCM",   # Preferred: AEAD with 256-bit key
    "AES-128-GCM",   # Fallback: AEAD with 128-bit key
    "AES-256-CBC"    # Legacy support (avoid if possible)
]

Configuration Options

OptionTypeDefaultDescription
enabledboolfalseEnable CMS-EST endpoints
require_signed_requestsbooltrueReject unsigned enrollment requests
encrypt_responsesbooltrueEncrypt certificate responses with recipient’s public key
allowed_content_encryptionarray[“AES-256-GCM”, “AES-128-GCM”]Supported encryption algorithms (preference order)

Content Encryption Algorithms

kipuka supports three encryption algorithms for CMS-EST responses:

Authenticated Encryption with Associated Data (AEAD) providing both confidentiality and integrity. Requires clients support GCM mode.

Advantages:

  • Single operation provides encryption and authentication
  • No padding oracle vulnerabilities
  • Faster than CBC for hardware-accelerated platforms

Configuration:

allowed_content_encryption = ["AES-256-GCM"]

AES-128-GCM

Similar to AES-256-GCM but with 128-bit key length. Suitable for resource-constrained devices.

Use when:

  • Client hardware lacks 256-bit AES support
  • Regulatory requirements mandate AES-128 minimum
  • Performance constraints require smaller key sizes

AES-256-CBC (Legacy)

Cipher Block Chaining mode with 256-bit key. Included for backward compatibility with older CMS implementations.

Considerations:

  • Requires separate HMAC for integrity protection
  • Vulnerable to padding oracle attacks if improperly implemented
  • Slower than GCM on modern hardware

Only use if:

  • Interoperating with legacy systems that don’t support GCM
  • Regulatory compliance requires CBC mode specifically

Request/Response Flow

CMS-Wrapped Enrollment Request

  1. Client generates CSR:

    openssl req -new -key device-key.pem -out device.csr
    
  2. Client wraps CSR in CMS:

    # Sign the CSR with client's existing certificate
    openssl cms -sign \
      -in device.csr \
      -out device-cms-request.p7s \
      -signer client-cert.pem \
      -inkey client-key.pem \
      -outform DER \
      -binary \
      -nodetach
    
  3. Submit CMS request to kipuka:

    curl -X POST \
      --data-binary @device-cms-request.p7s \
      -H "Content-Type: application/pkcs7-mime" \
      -o device-cms-response.p7c \
      https://est.example.com/.well-known/est/simpleenroll
    
  4. Client unwraps CMS response:

    # Decrypt the response (if encrypted)
    openssl cms -decrypt \
      -in device-cms-response.p7c \
      -recip client-cert.pem \
      -inkey client-key.pem \
      -out device-cert.pem
    

CMS-Wrapped Reenrollment

For certificate renewal, the flow is similar but uses the current certificate:

# 1. Generate new CSR
openssl req -new -key new-key.pem -out renew.csr

# 2. Sign with current certificate
openssl cms -sign \
  -in renew.csr \
  -out renew-cms-request.p7s \
  -signer current-cert.pem \
  -inkey current-key.pem \
  -outform DER \
  -binary

# 3. Submit to reenroll endpoint
curl -X POST \
  --data-binary @renew-cms-request.p7s \
  -H "Content-Type: application/pkcs7-mime" \
  -o renew-cms-response.p7c \
  https://est.example.com/.well-known/est/simplereenroll

# 4. Decrypt response
openssl cms -decrypt \
  -in renew-cms-response.p7c \
  -recip current-cert.pem \
  -inkey current-key.pem \
  -out renewed-cert.pem

Security Considerations

Signature Verification

When require_signed_requests = true, kipuka performs these checks on incoming CMS requests:

  1. Signature validity: Cryptographic signature must verify against signer’s certificate
  2. Certificate chain: Signer’s certificate must chain to a trusted root
  3. Certificate status: Signer’s certificate must not be revoked (OCSP/CRL check)
  4. Timestamp validation: CMS signature timestamp must be within acceptable window

Recommendation: Always enable require_signed_requests in production. Unsigned requests bypass authentication and should only be allowed in isolated test environments.

Response Encryption

When encrypt_responses = true, kipuka encrypts the certificate response using the public key from the signer’s certificate. This ensures:

  • Confidentiality: Certificate is only readable by the requesting device
  • Binding: Response is cryptographically bound to the request
  • Non-repudiation: Requester cannot claim they didn’t receive the certificate

Recommendation: Enable encryption unless all CMS-EST operations occur over physically secure channels.

Algorithm Selection

Order allowed_content_encryption from strongest to weakest:

allowed_content_encryption = [
    "AES-256-GCM",   # Try first
    "AES-128-GCM",   # Fallback for compatibility
]
# Omit AES-256-CBC unless required for legacy interop

kipuka selects the first algorithm supported by both client and server.

Transport Security

CMS-EST still requires HTTPS transport when used over a network. The CMS wrapping provides:

  • Protection of request/response content during offline storage
  • End-to-end security spanning disconnected segments
  • Defense against transport-layer downgrade attacks

Do not transmit CMS-EST messages over unencrypted HTTP.

Example Workflows

Air-Gapped Device Enrollment

On disconnected device:

# Generate key and CSR
openssl genpkey -algorithm RSA -out device-key.pem -pkeyopt rsa_keygen_bits:2048
openssl req -new -key device-key.pem -out device.csr \
  -subj "/CN=device-12345/O=Example Corp"

# Create self-signed temp cert for CMS signing (if no existing cert)
openssl req -x509 -key device-key.pem -out temp-cert.pem -days 1 \
  -subj "/CN=device-12345/O=Example Corp"

# Sign CSR with temp cert
openssl cms -sign -in device.csr -out device-cms-request.p7s \
  -signer temp-cert.pem -inkey device-key.pem -outform DER -binary

# Transfer device-cms-request.p7s to USB drive

On network-connected workstation:

# Submit to kipuka
curl -X POST \
  --data-binary @/media/usb/device-cms-request.p7s \
  -H "Content-Type: application/pkcs7-mime" \
  -o /media/usb/device-cms-response.p7c \
  https://est.example.com/.well-known/est/simpleenroll

Back on disconnected device:

# Extract certificate from response
openssl cms -decrypt \
  -in device-cms-response.p7c \
  -recip temp-cert.pem \
  -inkey device-key.pem \
  -out device-cert.pem

# Verify certificate
openssl x509 -in device-cert.pem -text -noout

Batch Processing Script

#!/bin/bash
# batch-cms-enroll.sh - Process multiple CMS requests

REQUEST_DIR="/var/cms-est/requests"
RESPONSE_DIR="/var/cms-est/responses"
EST_URL="https://est.example.com/.well-known/est/simpleenroll"

for request in "$REQUEST_DIR"/*.p7s; do
    base=$(basename "$request" .p7s)
    echo "Processing $base..."
    
    curl -X POST \
      --data-binary @"$request" \
      -H "Content-Type: application/pkcs7-mime" \
      -o "$RESPONSE_DIR/${base}.p7c" \
      -w "HTTP %{http_code}\n" \
      "$EST_URL"
    
    if [ $? -eq 0 ]; then
        echo "  ✓ Response saved to $RESPONSE_DIR/${base}.p7c"
        mv "$request" "$REQUEST_DIR/processed/"
    else
        echo "  ✗ Failed to process $base"
    fi
done

Troubleshooting

“CMS signature verification failed”

Cause: Signer’s certificate doesn’t chain to a trusted root, or the signature is invalid.

Solution:

  1. Verify the signer’s certificate chains to a CA trusted by kipuka
  2. Check that the private key used for signing matches the certificate
  3. Ensure the signer’s certificate is not expired or revoked
  4. Inspect server logs for detailed verification errors

“Unsupported content encryption algorithm”

Cause: Client requested an encryption algorithm not in allowed_content_encryption.

Solution:

  1. Add the algorithm to the server configuration:
    allowed_content_encryption = ["AES-256-GCM", "AES-128-GCM", "AES-256-CBC"]
    
  2. Or update the client to request a supported algorithm
  3. Check client and server logs for algorithm negotiation details

Response decryption fails

Cause: Response was encrypted for a different recipient certificate.

Solution:

  1. Ensure the certificate used for decryption matches the one used to sign the request
  2. Verify the decryption key corresponds to the certificate’s public key
  3. Check for corruption during file transfer (verify file hash)

Large message sizes

Cause: CMS envelope overhead adds significant size to requests/responses.

Solution:

  • Use binary DER encoding instead of PEM (saves ~33% space)
  • Minimize certificate chain length by using direct trust anchors
  • Compress CMS files during transport (CMS content is not compressible, but envelope metadata is)
  • Consider plain EST for high-volume, connected deployments

Integration Examples

Python Client

#!/usr/bin/env python3
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import requests

# Generate key
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)

# Create CSR
csr = x509.CertificateSigningRequestBuilder().subject_name(
    x509.Name([
        x509.NameAttribute(x509.NameOID.COMMON_NAME, "device-67890"),
        x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, "Example Corp"),
    ])
).sign(private_key, hashes.SHA256(), default_backend())

# Wrap in CMS (requires python-cms library)
# ... CMS signing code ...

# Submit to kipuka
response = requests.post(
    "https://est.example.com/.well-known/est/simpleenroll",
    data=cms_request,
    headers={"Content-Type": "application/pkcs7-mime"}
)

# Unwrap CMS response
# ... CMS decryption code ...

Performance Considerations

CMS-EST operations are slower than plain EST due to:

  • Signature verification: ~10-50ms per request depending on key size
  • Encryption/decryption: ~5-20ms per response
  • Certificate validation: ~50-200ms including OCSP/CRL checks

For high-throughput deployments:

  1. Cache trusted certificates to reduce chain validation overhead
  2. Use OCSP stapling to eliminate outbound OCSP requests
  3. Consider hardware acceleration for AES-GCM operations
  4. Batch multiple enrollment requests when possible

Typical throughput: 100-500 CMS-EST enrollments per second on modern server hardware.

STAR Certificates Configuration

Short-Term, Automatically Renewed (STAR) certificates are defined in RFC 8739. kipuka implements STAR to address the industry-wide shift toward short-lived certificates mandated by the CA/Browser Forum’s 47-day maximum validity requirement.

Overview

Why STAR?

Traditional certificate management with multi-year validity periods creates operational challenges:

  • Revocation complexity: Long-lived certificates require reliable OCSP/CRL infrastructure
  • Compromise risk: Stolen private keys remain valid until detected and revoked
  • Manual renewal: Annual or multi-year renewals are error-prone and disruptive

The CA/Browser Forum now mandates maximum 47-day validity for publicly trusted certificates. STAR automates this at scale.

How STAR Works

Instead of manually renewing every 47 days, STAR:

  1. Order creation: Client requests a STAR certificate with desired lifetime (e.g., 1 year)
  2. Automatic issuance: kipuka issues the first 47-day certificate immediately
  3. Automatic renewal: kipuka generates and publishes new certificates before expiration
  4. Client polling: Client periodically fetches the latest certificate from the STAR endpoint
  5. Lifecycle management: Order runs until max lifetime or cancellation

STAR vs Traditional EST

FeatureTraditional ESTSTAR
EnrollmentManual per certificateOne-time order
RenewalManual simplereenrollAutomatic
Validity periodOperator-defined47 days (CA/B Forum)
Client pollingNoneRequired
RevocationOCSP/CRLCancellation

Configuration

Enable STAR in your kipuka.toml configuration:

[star]
enabled = true

# Renewal intervals (seconds)
min_renewal_interval_secs = 3600       # 1 hour (minimum allowed)
max_renewal_interval_secs = 604800     # 7 days (maximum allowed)
default_renewal_interval_secs = 86400  # 1 day (default when not specified)

# Lifecycle limits
max_lifetime_days = 365                # Maximum order lifetime
max_active_orders = 10000              # Resource exhaustion protection

# Renewal timing
pre_renewal_factor = 0.5               # Renew at 50% of certificate lifetime

Configuration Options

OptionTypeDefaultRangeDescription
enabledboolfalseEnable STAR endpoints
min_renewal_interval_secsu643600≥3600Minimum time between renewals (1 hour)
max_renewal_interval_secsu64604800≤604800Maximum time between renewals (7 days)
default_renewal_interval_secsu64864003600-604800Default renewal interval (1 day)
max_lifetime_daysu323651-730Maximum order lifetime (1 year)
max_active_ordersusize10000≥1Maximum concurrent STAR orders
pre_renewal_factorf640.50.1-0.9When to renew (0.5 = halfway through cert life)

Renewal Timing

The pre_renewal_factor controls when kipuka generates the next certificate:

renewal_time = (certificate_validity * pre_renewal_factor)

Examples:

  • pre_renewal_factor = 0.5 (default): Renew 47-day cert after 23.5 days
  • pre_renewal_factor = 0.75: Renew 47-day cert after 35.25 days
  • pre_renewal_factor = 0.25: Renew 47-day cert after 11.75 days

Recommendations:

  • High-reliability: 0.25-0.33 — More frequent renewals, longer grace period
  • Balanced (default): 0.5 — Reasonable renewal frequency and grace period
  • Resource-constrained: 0.75-0.8 — Fewer renewals, minimal grace period

Lower values increase server load but provide more time to recover from renewal failures.

STAR Endpoints

Create Order

POST /.well-known/est/{label}/star-create
Content-Type: application/pkcs10

Request body: PKCS#10 CSR (DER-encoded)

Response:

{
  "order_id": "7f3b9c2a-1e4d-4c8b-9a2e-6d5c8b7a3f1e",
  "status": "pending",
  "lifetime_days": 365,
  "renewal_interval_secs": 86400,
  "certificate_validity_days": 47,
  "fetch_url": "https://est.example.com/.well-known/est/prod/star/7f3b9c2a-1e4d-4c8b-9a2e-6d5c8b7a3f1e"
}

Fetch Certificate

GET /.well-known/est/{label}/star/{order-id}

Response: Current certificate (PEM-encoded)

Clients should poll this endpoint based on renewal_interval_secs to fetch renewed certificates.

Cancel Order (Admin API)

DELETE /admin/api/v1/star/{order-id}
Authorization: Bearer <admin-token>

Immediately cancels the order and stops automatic renewals.

Order Lifecycle

          ┌────────┐
          │ Create │
          │ Order  │
          └───┬────┘
              │
              ▼
        ┌──────────┐
        │ pending  │◄──────────────┐
        └────┬─────┘               │
             │                     │
             │ Initial cert        │
             │ issued              │
             ▼                     │
        ┌──────────┐               │
        │  active  │               │
        └────┬─────┘               │
             │                     │
             │ Renewal timer       │
             │ fires               │
             ├─────────────────────┘
             │
             │ Lifetime expired
             │ or cancelled
             ▼
        ┌──────────┐
        │ expired  │
        │cancelled │
        └──────────┘

Status States

pending

Order created but initial certificate not yet issued. Typical duration: <1 second.

Transitions to:

  • active — Initial certificate issued successfully
  • expired — Order creation failed or timed out

active

Certificate issued and automatic renewals enabled. Server generates new certificates based on pre_renewal_factor.

Transitions to:

  • expired — Lifetime exceeded max_lifetime_days
  • cancelled — Admin or client cancellation request

expired

Order lifetime exceeded. No further renewals will be issued.

Final state: Order cannot return to active.

cancelled

Order cancelled by administrator or client. Immediate termination.

Final state: Order cannot return to active.

Client Implementation

Python Example

#!/usr/bin/env python3
import time
import requests
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend

# Configuration
EST_SERVER = "https://est.example.com"
EST_LABEL = "prod"
RENEWAL_CHECK_INTERVAL = 86400  # 1 day

# Generate key pair
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)

# Create CSR
csr = x509.CertificateSigningRequestBuilder().subject_name(
    x509.Name([
        x509.NameAttribute(x509.NameOID.COMMON_NAME, "www.example.com"),
    ])
).sign(private_key, hashes.SHA256(), default_backend())

# Submit STAR order
response = requests.post(
    f"{EST_SERVER}/.well-known/est/{EST_LABEL}/star-create",
    data=csr.public_bytes(serialization.Encoding.DER),
    headers={"Content-Type": "application/pkcs10"}
)

order = response.json()
order_id = order["order_id"]
fetch_url = order["fetch_url"]
print(f"STAR order created: {order_id}")

# Polling loop
while True:
    # Fetch current certificate
    cert_response = requests.get(fetch_url)
    
    if cert_response.status_code == 200:
        cert_pem = cert_response.text
        cert = x509.load_pem_x509_certificate(
            cert_pem.encode(), 
            default_backend()
        )
        
        print(f"Certificate valid until: {cert.not_valid_after}")
        
        # Install certificate
        with open("/etc/ssl/certs/server.crt", "w") as f:
            f.write(cert_pem)
        
        # Reload web server (example: nginx)
        import subprocess
        subprocess.run(["systemctl", "reload", "nginx"])
        
    elif cert_response.status_code == 404:
        print("Order expired or cancelled")
        break
    
    # Wait before next poll
    time.sleep(RENEWAL_CHECK_INTERVAL)

Systemd Timer

Run the polling script as a systemd service:

# /etc/systemd/system/star-cert-renew.service
[Unit]
Description=STAR Certificate Renewal
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/star-renew.py
Restart=always
RestartSec=3600

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/star-cert-renew.timer
[Unit]
Description=STAR Certificate Renewal Timer

[Timer]
OnBootSec=5min
OnUnitActiveSec=1d

[Install]
WantedBy=timers.target

Enable:

systemctl enable --now star-cert-renew.timer

Resource Exhaustion Protection

The max_active_orders setting prevents resource exhaustion attacks:

max_active_orders = 10000

When this limit is reached:

  1. New STAR order requests return HTTP 429 (Too Many Requests)
  2. Existing orders continue renewing normally
  3. Orders transition to expired or cancelled states free up capacity

Monitoring:

# Check active STAR order count
curl -H "Authorization: Bearer <admin-token>" \
  https://est.example.com/admin/api/v1/star/stats

# Response:
# {
#   "active_orders": 8567,
#   "max_orders": 10000,
#   "capacity_percent": 85.67
# }

Capacity Planning:

  • Small deployment (<100 devices): max_active_orders = 1000
  • Medium deployment (100-1000 devices): max_active_orders = 5000
  • Large deployment (1000-10000 devices): max_active_orders = 25000
  • Enterprise deployment (>10000 devices): max_active_orders = 100000

Each order consumes ~1KB of memory for metadata storage.

Security Considerations

Order ID Entropy

Order IDs are generated using cryptographically secure random UUIDs (128 bits of entropy). This prevents:

  • Enumeration attacks: Guessing valid order IDs
  • Prediction: Deriving order IDs from observable data
  • Collisions: Duplicate order IDs across the system

Fetch URL Authorization

The fetch endpoint (/.well-known/est/{label}/star/{order-id}) requires:

  1. Knowledge of order ID: Only clients with the UUID can fetch certificates
  2. TLS transport security: Prevents interception of fetch requests
  3. No additional authentication: Order ID serves as bearer token

Implications:

  • Protect order IDs like API keys
  • Don’t log order IDs in publicly accessible logs
  • Use HTTPS for all fetch operations
  • Rotate orders if order ID is compromised (cancel old, create new)

Cancellation Authorization

Only authenticated administrators can cancel STAR orders via the admin API. This prevents:

  • Unauthorized service disruption
  • Denial-of-service through order cancellation
  • Client-initiated cancellation (clients must contact admin)

Monitoring and Observability

Metrics

kipuka exposes Prometheus metrics for STAR operations:

# Total active STAR orders
star_active_orders{label="prod"} 8567

# Orders by status
star_orders_by_status{label="prod",status="active"} 8560
star_orders_by_status{label="prod",status="pending"} 7
star_orders_by_status{label="prod",status="expired"} 0
star_orders_by_status{label="prod",status="cancelled"} 0

# Renewal operations
star_renewals_total{label="prod",result="success"} 156234
star_renewals_total{label="prod",result="failure"} 12

# Fetch operations
star_fetches_total{label="prod",http_code="200"} 298765
star_fetches_total{label="prod",http_code="404"} 45

Logging

STAR operations generate structured logs:

{
  "timestamp": "2026-06-25T14:32:01Z",
  "level": "INFO",
  "event": "star_order_created",
  "order_id": "7f3b9c2a-1e4d-4c8b-9a2e-6d5c8b7a3f1e",
  "lifetime_days": 365,
  "renewal_interval_secs": 86400,
  "subject": "CN=www.example.com"
}
{
  "timestamp": "2026-06-25T15:00:00Z",
  "level": "INFO",
  "event": "star_certificate_renewed",
  "order_id": "7f3b9c2a-1e4d-4c8b-9a2e-6d5c8b7a3f1e",
  "serial": "3a:b2:c4:d5:e6:f7:a8:b9",
  "not_before": "2026-06-25T15:00:00Z",
  "not_after": "2026-08-11T15:00:00Z"
}

Health Checks

Monitor STAR renewal health:

# Check for failed renewals in the last hour
curl -s https://est.example.com/admin/api/v1/star/health | jq '.failed_renewals_1h'

# Alert if > 0

Troubleshooting

Certificates not renewing

Symptoms:

  • Client fetches return expired certificates
  • star_renewals_total{result="failure"} metric increasing

Causes:

  1. CA backend unavailable
  2. CA policy rejecting renewals
  3. HSM communication failure

Diagnosis:

# Check renewal logs
journalctl -u kipuka -g "star_certificate_renewal_failed" --since "1 hour ago"

# Check CA backend health
curl https://est.example.com/admin/api/v1/ca/health

Solution:

  • Verify CA backend connectivity
  • Check CA audit logs for rejection reasons
  • Ensure HSM is accessible and responsive

High fetch failure rate

Symptoms:

  • star_fetches_total{http_code="404"} increasing rapidly

Causes:

  1. Clients polling cancelled or expired orders
  2. Order IDs leaked or mistyped
  3. Mass order expiration event

Diagnosis:

# List recently expired orders
curl -H "Authorization: Bearer <admin-token>" \
  https://est.example.com/admin/api/v1/star/orders?status=expired&limit=100

Solution:

  • Notify clients to recreate orders
  • Investigate mass expiration (check max_lifetime_days configuration)
  • Audit for unauthorized cancellations

Resource exhaustion

Symptoms:

  • HTTP 429 responses on order creation
  • star_active_orders metric at max_active_orders limit

Causes:

  1. Legitimate growth exceeding capacity
  2. Order creation abuse
  3. Orders not expiring (configuration issue)

Solution:

# Increase capacity
# Edit kipuka.toml:
# max_active_orders = 50000

# Restart kipuka
systemctl restart kipuka

# Or cancel stale orders
curl -X DELETE -H "Authorization: Bearer <admin-token>" \
  https://est.example.com/admin/api/v1/star/orders/cleanup?older_than_days=400

Integration with Existing PKI

Hybrid Deployment

Run STAR alongside traditional EST for gradual migration:

# EST for initial enrollment
[est]
enabled = true

# STAR for automated renewals
[star]
enabled = true
max_lifetime_days = 365

Workflow:

  1. Clients perform initial enrollment via EST /simpleenroll
  2. Clients create STAR order for automatic renewals
  3. Clients cancel STAR order when device is decommissioned

Certificate Revocation

STAR orders don’t require revocation because:

  1. Short validity: 47-day certificates have limited exposure window
  2. Cancellation: Stop renewals by cancelling the order
  3. Immediate effect: Next fetch returns HTTP 404

Comparison to OCSP/CRL:

MethodRevocation SpeedInfrastructure
OCSP~1 hour (cache TTL)OCSP responder required
CRL~24 hours (CRL TTL)CRL distribution required
STAR cancellationImmediate (next fetch)None

When to use traditional revocation:

  • Certificates issued outside STAR workflow
  • Compliance requirements mandate OCSP/CRL
  • Legacy clients don’t support STAR

Performance Tuning

Renewal Batch Processing

Group renewals to reduce database load:

# Increase renewal interval to reduce frequency
default_renewal_interval_secs = 259200  # 3 days

# Higher pre_renewal_factor = later renewals
pre_renewal_factor = 0.75

Client Polling Optimization

Instruct clients to use HTTP conditional requests:

# Fetch with If-Modified-Since
curl -H "If-Modified-Since: Wed, 25 Jun 2026 14:00:00 GMT" \
  https://est.example.com/.well-known/est/prod/star/{order-id}

# Server returns 304 Not Modified if cert unchanged

Database Indexing

Ensure database has indexes on:

  • order_id (primary key)
  • status (for querying active orders)
  • next_renewal_time (for renewal scheduling)

Future Enhancements

Planned features in future kipuka releases:

  • Multi-certificate orders: Single order for multiple domains (SAN certificates)
  • Client-initiated cancellation: Allow clients to cancel their own orders
  • Renewal notifications: Webhook or email alerts before renewal
  • Order transfer: Migrate orders between EST labels
  • Analytics dashboard: Web UI for STAR order visualization

OCSP Configuration

The Online Certificate Status Protocol (OCSP) is defined in RFC 6960. kipuka implements OCSP checking to verify the revocation status of client certificates during mutual TLS authentication for EST enrollment operations.

Overview

OCSP in EST Context

EST enrollment requires client authentication via mutual TLS (mTLS). kipuka validates client certificates by:

  1. Chain verification: Certificate chains to a trusted root
  2. Validity period: Certificate is not expired
  3. Revocation status: Certificate has not been revoked (via OCSP or CRL)

OCSP checking happens during the TLS handshake before EST operations are processed. Revoked certificates are rejected at the transport layer.

OCSP vs CRL

FeatureOCSPCRL
Response timeReal-timePeriodic updates
BandwidthMinimal (per-cert query)High (full list download)
PrivacyReveals which certs are checkedAnonymous
FreshnessSecondsHours/days
FallbackCRLNone

kipuka prefers OCSP for real-time revocation checking but falls back to CRL when OCSP is unavailable.

Configuration

Enable OCSP in your kipuka.toml configuration:

[ocsp]
enabled = true

# OCSP responder URL (optional, overrides AIA extension)
responder_url = "http://ocsp.example.com"

# Caching and performance
cache_ttl_secs = 300       # Cache OCSP responses for 5 minutes
timeout_secs = 10          # OCSP request timeout

# Security settings
require_nonce = true       # Require OCSP nonce (RFC 6960 §4.4.1)
soft_fail = false          # Reject clients if OCSP check fails

Configuration Options

OptionTypeDefaultDescription
enabledboolfalseEnable OCSP checking for client certificates
responder_urlstringNoneOverride OCSP responder URL; None = use AIA extension
cache_ttl_secsu64300Cache OCSP responses (seconds)
timeout_secsu6410OCSP request timeout (seconds)
require_noncebooltrueRequire nonce in OCSP request/response
soft_failboolfalseAllow authentication if OCSP check fails

OCSP Responder URL

kipuka determines the OCSP responder URL using this priority:

  1. Configuration override: responder_url if set
  2. AIA extension: OCSP URL from certificate’s Authority Information Access extension
  3. Fail: Reject certificate if neither is available (unless soft_fail = true)

When to use responder_url override:

  • All certificates use the same OCSP responder
  • AIA extension points to an internal URL not reachable from kipuka’s network
  • Testing with a local OCSP responder

Example:

# Override for all certificates
responder_url = "http://ocsp.internal.example.com:8080"

When to omit responder_url:

  • Certificates from multiple CAs with different OCSP responders
  • AIA extensions are correctly configured and reachable
  • Production environments (prefer AIA for proper PKI hygiene)

Soft-Fail vs Hard-Fail

The soft_fail setting determines behavior when OCSP checking encounters errors.

Hard-Fail Mode (Default)

soft_fail = false

Behavior:

  • OCSP responder unreachable → Reject client certificate
  • OCSP response timeout → Reject client certificate
  • OCSP response invalid → Reject client certificate
  • Certificate has no AIA extension → Reject client certificate

Use when:

  • Security is paramount (zero-trust environments)
  • OCSP infrastructure is highly available (99.9%+ uptime)
  • Network connectivity to OCSP responders is reliable

Risks:

  • Service outage if OCSP responder fails
  • Denial of service if network connectivity is lost

Soft-Fail Mode

soft_fail = true

Behavior:

  • OCSP responder unreachable → Allow client certificate, log warning
  • OCSP response timeout → Allow client certificate, log warning
  • OCSP response invalid → Allow client certificate, log warning
  • Certificate explicitly revoked → Reject client certificate
  • Certificate has no AIA extension → Allow client certificate, log warning

Use when:

  • Availability is more important than absolute security
  • OCSP infrastructure has known reliability issues
  • Gradual OCSP deployment (monitoring before enforcement)

Risks:

  • Revoked certificates may be accepted if OCSP is down
  • Increased attack surface during OCSP outages

Recommendation: Start with soft_fail = true in development, transition to soft_fail = false in production after verifying OCSP infrastructure reliability.

Nonce Requirements

RFC 6960 §4.4.1 recommends using nonces to prevent OCSP response replay attacks.

Nonce Enabled (Default)

require_nonce = true

Behavior:

  • kipuka includes a random nonce in OCSP requests
  • OCSP responder must echo the nonce in the response
  • Responses without matching nonce are rejected

Security benefit:

  • Prevents attacker from replaying old “good” responses for revoked certificates
  • Ensures response freshness

Compatibility:

  • All modern OCSP responders support nonces
  • Some legacy responders may not support nonces

Nonce Disabled

require_nonce = false

Use when:

  • OCSP responder doesn’t support nonces
  • Interoperating with legacy PKI infrastructure
  • OCSP responses are pre-generated and cached (can’t include unique nonces)

Security implication:

  • Attacker could replay old OCSP responses during the cache window
  • Mitigation: Use shorter cache_ttl_secs (e.g., 60 seconds)

Cache Behavior

kipuka caches OCSP responses in memory to reduce load on OCSP responders and improve performance.

Cache Mechanics

  1. Cache key: SHA-256 hash of certificate serial + issuer DN
  2. Cache value: OCSP response (Good, Revoked, Unknown) + timestamp
  3. TTL: cache_ttl_secs from time of OCSP response receipt
  4. Eviction: LRU (Least Recently Used) when cache size limit reached

Cache Tuning

# High-throughput deployment (many unique clients)
cache_ttl_secs = 600      # 10 minutes
cache_size_entries = 100000

# Low-latency deployment (few clients, strict freshness)
cache_ttl_secs = 60       # 1 minute
cache_size_entries = 1000

Trade-offs:

TTLOCSP LoadRevocation DelayLatency
60sHigh0-60sLow
300sMedium0-300sLow
600sLow0-600sLow
3600sVery Low0-3600sVery Low

Recommendation: Use 300-600 seconds for production environments. This balances OCSP responder load with reasonable revocation detection latency.

Cache Invalidation

kipuka automatically invalidates cache entries when:

  • TTL expires
  • Certificate status changes from Good to Revoked (received from OCSP)
  • Cache size limit exceeded (LRU eviction)
  • kipuka restart (cache is not persisted to disk)

Manual cache invalidation:

# Clear OCSP cache via admin API
curl -X POST -H "Authorization: Bearer <admin-token>" \
  https://est.example.com/admin/api/v1/ocsp/cache/clear

CRL Fallback

When OCSP is unavailable or disabled, kipuka automatically falls back to Certificate Revocation Lists (CRL).

Fallback Scenarios

OCSP → CRL fallback occurs when:

  1. ocsp.enabled = false (CRL-only mode)
  2. OCSP responder unreachable and soft_fail = true
  3. OCSP response timeout and soft_fail = true
  4. Certificate has no AIA extension for OCSP

CRL Configuration

[crl]
enabled = true
cache_ttl_secs = 3600       # Cache CRL for 1 hour
download_timeout_secs = 30  # CRL download timeout
max_crl_size_mb = 100       # Reject CRLs larger than 100 MB

CRL URLs are extracted from the certificate’s CRL Distribution Points (CDP) extension.

OCSP + CRL Combined

Best practice is to enable both:

[ocsp]
enabled = true
soft_fail = true   # Fallback to CRL on OCSP failure

[crl]
enabled = true

This provides:

  • Fast path: OCSP for real-time revocation checking
  • Fallback: CRL when OCSP is unavailable
  • Redundancy: Multiple revocation mechanisms

Performance Tuning

Latency Analysis

OCSP checking adds latency to the TLS handshake:

ComponentTypical Latency
Network RTT to OCSP responder10-50ms
OCSP responder processing10-30ms
Signature verification5-10ms
Total (cache miss)25-90ms
Total (cache hit)<1ms

High-Throughput Optimization

For deployments with >1000 concurrent connections:

[ocsp]
enabled = true
cache_ttl_secs = 600           # Longer cache = fewer OCSP requests
timeout_secs = 5               # Shorter timeout = faster failure
soft_fail = true               # Avoid blocking on OCSP outages

# Connection pooling to OCSP responder
http_max_idle_connections = 100
http_max_connections_per_host = 50

Network Optimization

Reduce OCSP latency by:

  1. Deploying local OCSP responders: Place responders near kipuka instances
  2. Using HTTP/2: Modern OCSP responders support HTTP/2 multiplexing
  3. Enabling OCSP stapling: Clients provide OCSP responses during TLS handshake (future kipuka feature)

Security Considerations

OCSP Response Validation

kipuka performs these checks on OCSP responses:

  1. Signature verification: Response signed by trusted OCSP responder
  2. Nonce match: Response nonce matches request nonce (if require_nonce = true)
  3. Timestamp freshness: Response producedAt within acceptable window
  4. Responder authorization: Responder certificate authorized to sign OCSP responses

OCSP Responder Trust

OCSP responders must be trusted via:

  • Delegated responder: Responder certificate issued by the same CA that issued the client certificate
  • CA-signed responder: Responder certificate has the id-kp-OCSPSigning extended key usage

kipuka validates the responder certificate chain and EKU before accepting responses.

Replay Attack Prevention

Even with nonces, OCSP responses can be replayed within the cache window. Mitigate by:

  • Using shorter cache_ttl_secs in high-security environments
  • Enabling require_nonce = true
  • Monitoring for unusual OCSP response patterns (same serial queried repeatedly)

Privacy Considerations

OCSP requests reveal which certificates are being checked to the OCSP responder. This has privacy implications:

  • User tracking: OCSP responder can track certificate usage patterns
  • Surveillance: Network observers see OCSP queries

Mitigations:

  • Use OCSP responders operated by trusted entities
  • Consider CRL for privacy-sensitive deployments (queries are anonymous)
  • Future: OCSP stapling (clients cache and provide their own responses)

Monitoring and Observability

Metrics

kipuka exposes Prometheus metrics for OCSP operations:

# OCSP check results
ocsp_checks_total{result="good"} 156234
ocsp_checks_total{result="revoked"} 12
ocsp_checks_total{result="unknown"} 3
ocsp_checks_total{result="error"} 45

# Cache performance
ocsp_cache_hits_total 298765
ocsp_cache_misses_total 5432
ocsp_cache_size_entries 8765

# Response times
ocsp_request_duration_seconds{quantile="0.5"} 0.025
ocsp_request_duration_seconds{quantile="0.9"} 0.089
ocsp_request_duration_seconds{quantile="0.99"} 0.156

Logging

OCSP operations generate structured logs:

{
  "timestamp": "2026-06-25T14:32:01Z",
  "level": "INFO",
  "event": "ocsp_check_success",
  "serial": "3a:b2:c4:d5:e6:f7:a8:b9",
  "issuer": "CN=Example CA",
  "status": "good",
  "cache_hit": true,
  "duration_ms": 0.8
}
{
  "timestamp": "2026-06-25T14:35:12Z",
  "level": "WARN",
  "event": "ocsp_check_revoked",
  "serial": "1a:2b:3c:4d:5e:6f:7a:8b",
  "issuer": "CN=Example CA",
  "status": "revoked",
  "revocation_time": "2026-06-20T10:00:00Z",
  "revocation_reason": "keyCompromise"
}
{
  "timestamp": "2026-06-25T14:40:00Z",
  "level": "ERROR",
  "event": "ocsp_check_failed",
  "serial": "9a:8b:7c:6d:5e:4f:3a:2b",
  "issuer": "CN=Example CA",
  "error": "timeout",
  "responder_url": "http://ocsp.example.com",
  "soft_fail": true
}

Alerting Rules

Example Prometheus alerting rules:

groups:
  - name: ocsp
    interval: 30s
    rules:
      - alert: OCSPHighErrorRate
        expr: |
          rate(ocsp_checks_total{result="error"}[5m]) > 0.1
        for: 5m
        annotations:
          summary: "High OCSP error rate"
          description: "OCSP checks failing at {{ $value }} errors/sec"

      - alert: OCSPResponderDown
        expr: |
          ocsp_checks_total{result="error"} > 0
        for: 2m
        annotations:
          summary: "OCSP responder appears down"
          description: "OCSP errors detected, responder may be unreachable"

      - alert: RevokedCertificateDetected
        expr: |
          increase(ocsp_checks_total{result="revoked"}[1m]) > 0
        annotations:
          summary: "Revoked certificate detected"
          description: "A revoked certificate attempted authentication"

Troubleshooting

OCSP timeout errors

Symptoms:

  • ocsp_checks_total{result="error"} increasing
  • Logs show “timeout” error

Causes:

  1. OCSP responder slow or overloaded
  2. Network connectivity issues
  3. Firewall blocking OCSP traffic

Diagnosis:

# Test OCSP responder manually
openssl ocsp \
  -issuer ca-cert.pem \
  -cert client-cert.pem \
  -url http://ocsp.example.com \
  -resp_text

# Check network connectivity
curl -I http://ocsp.example.com

Solution:

  • Increase timeout_secs if responder is consistently slow
  • Enable soft_fail = true temporarily to avoid service disruption
  • Contact CA to report OCSP responder performance issues
  • Deploy local OCSP responder replica

High OCSP load on responder

Symptoms:

  • OCSP responder CPU/bandwidth saturation
  • Slow OCSP responses

Causes:

  • cache_ttl_secs too low
  • Many unique client certificates (low cache hit rate)

Solution:

# Increase cache TTL to reduce OCSP queries
cache_ttl_secs = 600   # 10 minutes instead of 5

# Increase cache size to accommodate more certificates
cache_size_entries = 50000

Monitor ocsp_cache_hits_total / (ocsp_cache_hits_total + ocsp_cache_misses_total) — aim for >80% cache hit rate.

Revoked certificate accepted

Symptoms:

  • Known revoked certificate successfully authenticates

Causes:

  1. OCSP response cached before revocation
  2. soft_fail = true and OCSP responder down
  3. Clock skew between kipuka and OCSP responder

Diagnosis:

# Check cache status
curl -H "Authorization: Bearer <admin-token>" \
  https://est.example.com/admin/api/v1/ocsp/cache/lookup?serial=3a:b2:c4:d5:e6:f7:a8:b9

# Force cache clear
curl -X POST -H "Authorization: Bearer <admin-token>" \
  https://est.example.com/admin/api/v1/ocsp/cache/clear

Solution:

  • Clear OCSP cache to force fresh check
  • Reduce cache_ttl_secs for faster revocation propagation
  • Verify system clocks are synchronized (NTP)
  • Switch to soft_fail = false to enforce strict checking

No OCSP URL in certificate

Symptoms:

  • Logs show “No AIA extension found” or “No OCSP URL”
  • ocsp_checks_total{result="error"} increasing

Causes:

  • Certificate lacks Authority Information Access extension
  • AIA extension present but missing OCSP URL

Diagnosis:

# Check for AIA extension
openssl x509 -in client-cert.pem -text -noout | grep -A4 "Authority Information Access"

# Expected output:
# Authority Information Access:
#     OCSP - URI:http://ocsp.example.com

Solution:

  • Configure CA to include AIA extension in issued certificates
  • Set responder_url to provide fallback OCSP URL:
    responder_url = "http://ocsp.example.com"
    
  • Enable CRL fallback:
    [crl]
    enabled = true
    

Integration Examples

Python OCSP Client

#!/usr/bin/env python3
from cryptography import x509
from cryptography.x509 import ocsp
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
import requests

# Load certificate to check
with open("client-cert.pem", "rb") as f:
    cert = x509.load_pem_x509_certificate(f.read(), default_backend())

# Load issuer certificate
with open("issuer-cert.pem", "rb") as f:
    issuer = x509.load_pem_x509_certificate(f.read(), default_backend())

# Build OCSP request
builder = ocsp.OCSPRequestBuilder()
builder = builder.add_certificate(cert, issuer, hashes.SHA256())
req = builder.build()

# Send to OCSP responder
response = requests.post(
    "http://ocsp.example.com",
    data=req.public_bytes(serialization.Encoding.DER),
    headers={"Content-Type": "application/ocsp-request"}
)

# Parse OCSP response
ocsp_resp = ocsp.load_der_ocsp_response(response.content)

# Check status
if ocsp_resp.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL:
    status = ocsp_resp.certificate_status
    if status == ocsp.OCSPCertStatus.GOOD:
        print("Certificate is GOOD")
    elif status == ocsp.OCSPCertStatus.REVOKED:
        print(f"Certificate REVOKED at {ocsp_resp.revocation_time}")
    else:
        print("Certificate status UNKNOWN")
else:
    print(f"OCSP error: {ocsp_resp.response_status}")

OpenSSL OCSP Query

# Query OCSP responder for certificate status
openssl ocsp \
  -issuer ca-cert.pem \
  -cert client-cert.pem \
  -url http://ocsp.example.com \
  -header "Host" "ocsp.example.com" \
  -resp_text

# Output:
# OCSP Response Data:
#     OCSP Response Status: successful (0x0)
#     Response Type: Basic OCSP Response
#     ...
#     Cert Status: good
#     This Update: Jun 25 14:00:00 2026 GMT
#     Next Update: Jun 25 15:00:00 2026 GMT

Best Practices

  1. Enable both OCSP and CRL:

    [ocsp]
    enabled = true
    soft_fail = true
    
    [crl]
    enabled = true
    
  2. Use reasonable cache TTLs: 300-600 seconds balances performance and freshness

  3. Monitor cache hit rate: Target >80% to minimize OCSP responder load

  4. Start with soft-fail: Deploy with soft_fail = true, transition to hard-fail after stability verification

  5. Require nonces: Keep require_nonce = true for security unless compatibility issues arise

  6. Set appropriate timeouts: timeout_secs = 10 is reasonable for most networks

  7. Monitor revocation events: Alert on ocsp_checks_total{result="revoked"} for security incidents

  8. Test OCSP infrastructure: Periodically verify OCSP responder availability and performance

  9. Plan for OCSP outages: Document incident response procedures for OCSP responder failures

  10. Keep clocks synchronized: Use NTP to prevent timestamp-related OCSP validation failures

CoAP Transport

kipuka supports EST-over-CoAP (RFC 9483) for constrained IoT devices. This enables lightweight certificate enrollment over UDP with DTLS protection, reducing overhead compared to HTTP/TLS while maintaining security.

Overview

CoAP (Constrained Application Protocol, RFC 7252) is designed for resource-constrained devices with limited power, memory, and bandwidth. EST-over-CoAP adapts the EST protocol to CoAP’s request/response model, making it suitable for:

  • IoT sensors and actuators
  • Embedded devices with limited RAM (< 1 MB)
  • Low-power wireless networks (LoRaWAN, NB-IoT, 802.15.4)
  • Environments where TCP overhead is prohibitive

The kipuka-coap crate implements the full RFC 9483 specification with DTLS 1.2/1.3 support, block-wise transfer for large payloads (critical for post-quantum certificates), and session resumption.

Implementation Architecture

The CoAP transport is implemented across two layers:

  • kipuka-coap crate – Contains the CoapDtlsServer that manages the UDP listener and DTLS sessions (via OpenSSL), parses CoAP messages, handles block-wise transfer reassembly, and dispatches EST operations through the EstHandler trait.
  • CoapEstHandler (in the main kipuka-est crate) – Implements the EstHandler trait, bridging parsed CoAP requests to the same CA signing, CSR validation, and certificate issuance logic used by the HTTPS transport. This ensures both transports produce identical certificates and audit records.

When CoAP is enabled, CoapDtlsServer::run() is called at startup alongside the axum HTTP server. Both listeners share the same AppState, CA configurations, and database connections.

Currently supported EST operations over CoAP:

OperationStatus
/cacerts (GET /crts)Implemented
/simpleenroll (POST /sen)Implemented
/simplereenroll (POST /sren)Implemented
/csrattrs (GET /att)Implemented
/serverkeygen (POST /skg)Not yet implemented over CoAP

Configuration

Enable CoAP in kipuka.toml under the [coap] section:

[coap]
enabled = true
listen_addr = "0.0.0.0:5684"
dtls_enabled = true
block_size = 512
max_payload = 65536
session_timeout_secs = 300
max_sessions = 1024

Configuration Parameters

ParameterTypeDefaultDescription
enabledboolfalseEnable CoAP transport
listen_addrString"0.0.0.0:5684"UDP socket address (5684 is IANA-assigned for DTLS)
dtls_enabledbooltrueRequire DTLS encryption (strongly recommended)
block_sizeu16512Block size for block-wise transfer (16/32/64/128/256/512/1024)
max_payloadusize65536Maximum CoAP payload size (64 KB)
session_timeout_secsu64300DTLS session idle timeout (5 minutes)
max_sessionsusize1024Maximum concurrent DTLS sessions

Security Note: Always enable dtls_enabled = true in production. Plain CoAP exposes CSRs and certificates in cleartext over the network.

DTLS Setup

kipuka supports DTLS 1.2 (RFC 6347) and DTLS 1.3 (RFC 9147) for encrypted CoAP transport. DTLS provides:

  • Encryption and integrity protection equivalent to TLS
  • UDP-friendly handshake with retransmission
  • Session resumption to reduce handshake overhead
  • Client authentication via PSK or certificates (mTLS)

DTLS Configuration

DTLS shares TLS certificates from the main [tls] section:

[tls]
cert_chain = "/path/to/server-chain.pem"
private_key = "/path/to/server-key.pem"
ca_bundle = "/path/to/trusted-cas.pem"

For PSK-based DTLS (pre-shared key authentication):

[coap]
dtls_psk_enabled = true
dtls_psk_identity = "IoTDevice001"
dtls_psk_key_hex = "deadbeef..."  # Hex-encoded shared secret

Session Resumption

DTLS session resumption cache reduces handshake latency for returning clients:

  • Session ID-based resumption (DTLS 1.2)
  • Ticket-based resumption (DTLS 1.3, RFC 8446)
  • Cache expires after session_timeout_secs

EST Path Mapping

RFC 9483 section 5.1 defines CoAP path segments for EST operations. kipuka maps standard EST endpoints to shortened CoAP paths:

CoAP PathEST EndpointHTTP MethodCoAP MethodDescription
/sen/simpleenrollPOSTPOSTSimple enrollment (CSR → certificate)
/sren/simplereenrollPOSTPOSTSimple re-enrollment (renewal with proof-of-possession)
/skg/serverkeygenPOSTPOSTServer-side key generation
/att/csrattrsGETGETCSR attributes hint
/cacerts or /crts/cacertsGETGETCA certificate chain

Example: To enroll a device, send a POST to coaps://est.example.com:5684/sen with the CSR in PKCS#10 format.

Content-Format IDs

CoAP uses numeric Content-Format IDs instead of MIME strings. RFC 9483 section 5.4 assigns:

IDMIME TypeUsage
280application/pkcs7-mime; smime-type=server-generatedServer-generated key response
281application/pkcs7-mime; smime-type=certs-onlyCertificate-only response (cacerts, enroll)
285application/pkcs10PKCS#10 CSR request
286application/pkcs8PKCS#8 encrypted private key
287application/csrattrsCSR attributes response

Request and response payloads use these IDs in the CoAP Content-Format option.

Block-Wise Transfer

CoAP message size is limited by UDP MTU (typically 1280 bytes for IPv6). Block-wise transfer (RFC 7959) splits large payloads across multiple CoAP messages using Block1 (request) and Block2 (response) options.

Why Block Transfer Matters

Post-quantum certificates (ML-DSA-87 signatures) can exceed 7 KB. A typical PQC certificate chain:

  • Root CA cert: ~3 KB (ML-DSA-87 signature)
  • Intermediate CA cert: ~3.5 KB
  • End-entity cert: ~4 KB
  • Total: ~10.5 KB → requires ~21 blocks at 512-byte block size

Configuration

Set block_size to match your network’s MTU constraints:

  • 16-64 bytes: Very constrained links (802.15.4)
  • 128-256 bytes: Low-power WAN (NB-IoT, LoRa)
  • 512 bytes (default): General IoT deployments
  • 1024 bytes: High-bandwidth local networks

Trade-off: Larger blocks reduce round trips but increase packet loss impact.

Example Flow

Client requests /cacerts with Block2 option:

  1. Client: GET /cacerts, Block2: 0/0/512 (request 512-byte blocks)
  2. Server: 2.05 Content, Block2: 0/1/512 (block 0, more blocks follow)
  3. Client: GET /cacerts, Block2: 1/0/512 (request next block)
  4. Server: 2.05 Content, Block2: 1/1/512 (block 1, more blocks)
  5. Server: 2.05 Content, Block2: 20/0/512 (final block)

Example with coap-client

Install libcoap tools:

# Fedora/RHEL
sudo dnf install libcoap

# Ubuntu/Debian
sudo apt install libcoap2-bin

Fetch CA certificates

coap-client -m get \
  coaps://est.example.com:5684/cacerts \
  -u Client001 -k deadbeef... \
  -B 512 \
  -o cacerts.p7
  • -m get: GET method
  • -u/-k: DTLS PSK identity and key (hex)
  • -B 512: Request 512-byte blocks
  • -o: Save response to file

Simple enrollment

# Generate CSR
openssl req -new -newkey rsa:2048 -nodes \
  -keyout device.key -out device.csr

# Convert CSR to DER
openssl req -in device.csr -outform DER -out device.csr.der

# Send enrollment request
coap-client -m post \
  coaps://est.example.com:5684/sen \
  -u Client001 -k deadbeef... \
  -f device.csr.der \
  -t application/pkcs10 \
  -B 512 \
  -o device-cert.p7
  • -f: Request payload file
  • -t: Content-Type (use MIME string; libcoap translates to ID 285)

Troubleshooting

DTLS handshake fails

Symptom: Client timeout or “DTLS handshake failed” error.

Causes:

  • Firewall blocking UDP port 5684
  • Mismatched PSK identity or key
  • Client doesn’t support DTLS 1.2/1.3
  • Certificate verification failure (mTLS mode)

Debug:

# Enable verbose logging
coap-client -v 9 ...

# Check kipuka logs
journalctl -u kipuka -f | grep -i dtls

Block transfer incomplete

Symptom: Partial payload received, transfer hangs.

Causes:

  • Network packet loss exceeds retransmission limits
  • block_size too large for MTU
  • max_payload too small for certificate chain

Fix:

  • Reduce block_size to 256 or 128
  • Increase max_payload to 131072 (128 KB)
  • Check network MTU: ip link show | grep mtu

Session timeout errors

Symptom: “Session expired” after idle period.

Cause: Client inactive for > session_timeout_secs.

Fix:

  • Increase session_timeout_secs to 600 (10 minutes)
  • Implement session resumption in client
  • Send periodic keepalive messages

Max sessions exceeded

Symptom: New clients refused with “Too many sessions”.

Cause: max_sessions limit reached (zombie sessions).

Fix:

  • Increase max_sessions to 2048
  • Reduce session_timeout_secs to 120 (2 minutes)
  • Restart kipuka to clear session cache

Performance Considerations

Memory Usage

Each DTLS session consumes ~10-20 KB RAM:

  • DTLS 1.2 state: ~8 KB
  • DTLS 1.3 state: ~12 KB
  • Session resumption cache: ~2 KB per session

1024 sessions → ~12-24 MB RAM overhead.

Throughput

CoAP throughput is limited by:

  • UDP round-trip time (RTT)
  • Block-wise transfer overhead
  • DTLS cryptographic processing

Example: 10 KB certificate chain, 512-byte blocks, 50ms RTT:

  • 20 blocks × 1 round trip each = 20 RTT
  • Total time: 20 × 50ms = 1 second

Larger block sizes reduce latency but require higher MTU.

Security Recommendations

  1. Always enable DTLS (dtls_enabled = true)
  2. Use strong PSK keys (≥ 128 bits entropy)
  3. Rotate PSK keys regularly (monthly for IoT)
  4. Limit session lifetime (session_timeout_secs ≤ 600)
  5. Monitor session count (alert on max_sessions threshold)
  6. Validate client certificates (mTLS mode) or OTP (EST enrollment)
  7. Use firewall rules to restrict CoAP port 5684 to authorized subnets
  8. Enable audit logging for all enrollment requests

Deployment Notes

UDP Port and Firewall

The IANA-assigned port for CoAP over DTLS is 5684. Ensure your firewall allows inbound UDP on this port:

# firewalld (Fedora/RHEL)
sudo firewall-cmd --permanent --add-port=5684/udp
sudo firewall-cmd --reload

# iptables
sudo iptables -A INPUT -p udp --dport 5684 -j ACCEPT

Container Deployment

When running kipuka in a container, expose both the HTTPS and CoAP/DTLS ports:

podman run --rm \
  -v ./kipuka.toml:/etc/kipuka/kipuka.toml:ro \
  -v ./certs:/etc/kipuka/certs:ro \
  -p 9443:9443 \
  -p 5684:5684/udp \
  quay.io/kipuka/kipuka:latest

Note the /udp suffix on port 5684 – without it, only TCP is exposed.

DTLS Certificate and Key

The CoAP/DTLS listener shares the TLS certificate and private key from the [tls] section. No separate certificate configuration is needed:

[tls]
cert_chain = "/etc/kipuka/certs/server-chain.pem"
private_key = "/etc/kipuka/certs/server-key.pem"
ca_bundle = "/etc/kipuka/certs/trusted-cas.pem"

[coap]
enabled = true
listen_addr = "0.0.0.0:5684"
dtls_enabled = true

Full Configuration Example

A minimal configuration enabling both HTTPS and CoAP transports:

[server]
listen_addr = "0.0.0.0:9443"

[tls]
cert_chain = "/etc/kipuka/certs/server-chain.pem"
private_key = "/etc/kipuka/certs/server-key.pem"
ca_bundle = "/etc/kipuka/certs/trusted-cas.pem"

[[ca]]
id = "iot-ca"
cert = "/etc/kipuka/certs/iot-ca.pem"
key = "/etc/kipuka/certs/iot-ca-key.pem"
chain = "/etc/kipuka/certs/iot-ca-chain.pem"
validity_days = 365

[database]
url = "sqlite:///var/lib/kipuka/kipuka.db"

[coap]
enabled = true
listen_addr = "0.0.0.0:5684"
dtls_enabled = true
block_size = 512
max_payload = 65536
session_timeout_secs = 300
max_sessions = 1024

References

  • RFC 9483: Constrained Application Protocol (CoAP) over DTLS for EST
  • RFC 7252: The Constrained Application Protocol (CoAP)
  • RFC 7959: Block-Wise Transfers in CoAP
  • RFC 6347: Datagram Transport Layer Security (DTLS) 1.2
  • RFC 9147: The Datagram Transport Layer Security (DTLS) 1.3
  • RFC 7030: Enrollment over Secure Transport (EST)

RFC Support Reference

This page documents every standards specification that kipuka implements, which sections are covered, and any caveats relevant to conformance testing.

Core Enrollment Standards

RFC 7030 – Enrollment over Secure Transport (EST)

SectionOperationStatusNotes
4.1/cacertsImplementedReturns the CA certificate chain as a PKCS#7 certs-only message. Unauthenticated; available without client credentials.
4.2/simpleenrollImplementedPKCS#10 CSR in, signed certificate out. Supports OTP, mTLS, GSSAPI, and CMS-based authentication.
4.2.2/simplereenrollImplementedRenewal with the existing client certificate presented via mTLS. Subject and SAN matching enforced by default.
4.3/fullcmcImplementedFull CMC (RFC 5272) request/response over the EST transport. Disabled by default; enable with est.fullcmc = true.
4.4/serverkeygenImplementedServer generates the key pair; returns a PKCS#7 EnvelopedData wrapping the private key and the signed certificate. Disabled by default.
4.5/csrattrsImplementedReturns a DER-encoded sequence of OIDs and attribute definitions the server expects in a CSR.
3.2EST-over-TLS bindingImplementedAll endpoints served over TLS 1.2 or 1.3 via rustls. The well-known URI prefix /.well-known/est is configurable via est.base_path.
3.2.2EST labelsImplementedLabel-specific paths (/.well-known/est/{label}/...) route to per-CA configurations with independent certificate profiles, allowed key types, and validity limits.
3.3.2HTTP-based client authImplementedHTTP-layer authentication (OTP via Authorization header) supplements TLS-layer mTLS authentication.
4.2.3Retry-After handlingImplementedWhen a request is deferred (e.g., pending manual approval), kipuka returns HTTP 202 with a Retry-After header. The interval is configurable via est.retry_after.
3.5Linking identity and POPImplementedProof-of-Possession via CSR self-signature is verified. When mTLS is used, the binding between the TLS identity and the CSR subject is enforced.

RFC 8951 – Clarifications for EST

RFC 8951 addresses ambiguities in RFC 7030. kipuka incorporates all clarifications that affect server behavior:

ClarificationStatusNotes
Content-Type enforcementImplementedStrict application/pkcs10 for enrollment; application/pkcs7-mime; smime-type=certs-only for /cacerts responses.
Base64 encoding rulesImplementedAccepts both raw DER and Base64-encoded bodies. Responses use Base64 with Content-Transfer-Encoding: base64.
Error response formatImplementedHTTP 4xx/5xx responses include a Content-Type: application/json body with a machine-readable error code and human-readable message.
TLS-unique channel bindingImplementedChannel binding values used for proof-of-possession when available (TLS 1.2 with tls-unique; TLS 1.3 uses Exporter instead).

RFC 5272 – Certificate Management over CMS (Full CMC)

The /fullcmc endpoint accepts a Full PKI Request (a ContentInfo containing a PKIData structure) and returns a Full PKI Response.

FeatureStatusNotes
PKIData parsingImplementedParses TaggedRequest sequences containing PKCS#10 or CRMF requests.
PKIResponse constructionImplementedReturns ResponseBody with certificates and optional control attributes.
Control attributesPartialid-cmc-statusInfoV2, id-cmc-identification, id-cmc-transactionId, and id-cmc-senderNonce / id-cmc-recipientNonce are supported. RA-delegated controls are not yet implemented.
Nested CMS signingImplementedRequests may be signed by a Registration Authority; kipuka validates the RA certificate against a configured RA trust store.

RFC 8739 – Short-Term, Automatically Renewed Certificates (STAR)

kipuka implements the server-side portion of STAR for short-lived certificate management. The kipuka::star module tracks renewal orders and issues replacement certificates before expiry.

FeatureStatusNotes
STAR order lifecycleImplementedStarOrder tracks each auto-renewal series through pending, active, expired, and cancelled states.
Automatic re-issuanceImplementedThe StarManager monitors active orders and issues replacement certificates before the not-after of the current certificate. Renewal interval is configurable per order.
Certificate fetch endpointImplementedThe /.well-known/est/{label}/star/{order-id} path returns the latest certificate in the renewal series.
CancellationImplementedOperators can cancel a STAR order via the admin API. The server stops issuing renewals and the order transitions to cancelled.

Transport

RFC 7252 – Constrained Application Protocol (CoAP)

The kipuka-coap crate implements EST-over-CoAP for resource-constrained devices (IoT sensors, embedded controllers) that cannot maintain persistent TCP connections.

FeatureStatusNotes
CoAP message parsingImplementedConfirmable (CON) and Non-confirmable (NON) message types. Token matching for request/response correlation.
DTLS transportImplementedCoAP endpoints served over DTLS 1.2 with PSK or certificate-based authentication via the kipuka_coap::dtls module.
Block-wise transferImplementedRFC 7959 Block1/Block2 options for transferring certificates and CSRs that exceed a single CoAP datagram. Block size is negotiated automatically.
Content-Format mappingImplementedEST content types mapped to CoAP Content-Format option values per the EST-coaps draft.
/cacerts over CoAPImplementedReturns the CA certificate chain using block-wise transfer.
/simpleenroll over CoAPImplementedEnrollment via PKCS#10 CSR over CoAP/DTLS.

RFC 4210 / RFC 9810 – Certificate Management Protocol (CMP)

The CMP endpoint at /.well-known/cmp provides a comprehensive certificate lifecycle protocol with its own ASN.1 message format, independent of the EST transport.

FeatureStatusNotes
Initialization Request (ir)ImplementedNew certificate enrollment via CMP.
Certification Request (cr)ImplementedCertificate request from an entity with an existing certificate.
Key Update Request (kur)ImplementedCertificate renewal / rekeying.
Revocation Request (rr)ImplementedCertificate revocation via CMP (disabled by default).
MAC-based protectionImplementedHMAC-SHA256 with shared secret for initial enrollment (RFC 4210 section 5.1.3.1).
Signature-based protectionImplementedProtection using an existing certificate and private key.
Error handlingImplementedPKIFailureInfo codes returned in error responses.

RFC 8295 – EST with CMS-Level Security (CMS-EST)

CMS-EST wraps EST messages in CMS SignedData and EnvelopedData structures, providing message-level security independent of the TLS transport.

FeatureStatusNotes
CMS SignedData requestsImplementedAll EST requests wrapped in CMS SignedData for authentication.
CMS EnvelopedData responsesImplementedResponses encrypted to the client’s certificate using CMS EnvelopedData.
Content encryptionImplementedAES-256-GCM and AES-128-GCM supported. Configurable via allowed_content_encryption.
Air-gapped deploymentImplementedCMS-EST enables EST enrollment without direct TLS connectivity.

RFC 9483 – EST over CoAP (EST-coaps)

Extends the RFC 7252 CoAP transport entry above with EST-specific path mapping and content-format negotiation.

FeatureStatusNotes
Compressed URI pathsImplemented/sen, /sren, /skg, /att, /crts mapped to EST operations per section 5.1.
Content-Format IDsImplementedCoAP content-format option values 280-287 per section 5.4.
DTLS 1.2 transportImplementedRFC 6347 with PSK and certificate-based authentication.
DTLS 1.3 transportImplementedRFC 9147 support.
Block-wise transferImplementedRFC 7959 Block1/Block2 for PQC certificates exceeding single datagram size.

RFC 7959 – Block-Wise Transfers in CoAP

FeatureStatusNotes
Block1 (request)ImplementedReassembles fragmented CSR uploads.
Block2 (response)ImplementedSplits certificate responses into negotiated block sizes (16-1024 bytes).
Block size negotiationImplementedServer respects client-requested block size within configured limits.

Composite Post-Quantum Signatures

SpecificationStatusNotes
draft-ietf-lamps-pq-composite-sigs-19ImplementedComposite ML-DSA + classical algorithm combinations for hybrid migration.
CNSA Suite 2.0TrackedConfiguration defaults align with NSA CNSA 2.0 timeline (2025 prefer, 2030 require PQC).

Certificate Policy

CA/Browser Forum Baseline Requirements

kipuka enforces the CA/B Forum Baseline Requirements for TLS server certificates when configured to issue publicly-trusted certificates. See CA/B Forum Baseline Requirements for the full mapping.

RequirementStatusNotes
Certificate profileEnforcedSubject fields, key types, extensions validated against BR profiles. Non-compliant CSRs are rejected before signing.
Serial number generationEnforced160-bit serials from OS CSPRNG (exceeds the BR minimum of 64 bits).
Maximum validity periodEnforcedConfigurable max_validity_days with declining defaults that track the BR timeline (398/200/100/47 days).
Key size minimumsEnforcedRSA >= 2048 bits, ECDSA P-256 or P-384. Smaller keys are rejected at CSR validation.

NIAP Common Criteria – CA Protection Profile v2.0

kipuka is designed to satisfy the Security Functional Requirements (SFRs) of the NIAP CA Protection Profile. See NIAP CA Protection Profile for the full SFR-by-SFR mapping.

Cryptographic Standards

FIPS 140-3 – Cryptographic Module Validation

kipuka does not itself hold a FIPS 140-3 certificate. FIPS compliance is achieved by delegating all cryptographic operations to a validated PKCS#11 HSM module.

AspectMechanismNotes
Key generationC_GenerateKeyPairRSA and ECDSA key pairs generated inside the HSM boundary.
SigningC_SignAll certificate signing operations execute within the validated module.
Random number generationC_GenerateRandomSerial number entropy sourced from the HSM’s validated DRBG when an HSM is configured; falls back to OS CSPRNG (getrandom) otherwise.
Key storageHSM tokenPrivate keys are CKA_SENSITIVE and CKA_EXTRACTABLE=false by default.

When kipuka operates with a software-only key (no HSM), it uses the Synta crate’s software implementations. These are suitable for testing and non-FIPS environments but do not carry a FIPS validation.

FIPS 204 – ML-DSA (Post-Quantum Digital Signatures)

kipuka supports ML-DSA (formerly CRYSTALS-Dilithium) signing via two paths:

PathMechanismLevels
Synta softwarekipuka_hsm::sign::sign_ml_dsaML-DSA-44 (L2), ML-DSA-65 (L3), ML-DSA-87 (L5)
PKCS#11 HSMCKM_IBM_DILITHIUM or vendor-specificDepends on HSM firmware; see HSM Compatibility Matrix

ML-DSA support is experimental. CA certificates and end-entity certificates can use ML-DSA key pairs, but client and browser ecosystem support is limited as of 2026.

FIPS 203 – ML-KEM (Post-Quantum Key Encapsulation)

ML-KEM (formerly CRYSTALS-Kyber) is supported for key encapsulation in hybrid key exchange scenarios. The kipuka_hsm::key module defines MlKemLevel variants L1, L3, and L5 corresponding to ML-KEM-512, ML-KEM-768, and ML-KEM-1024 respectively.

PathMechanismLevels
Synta softwareSoftware KEMML-KEM-512 (L1), ML-KEM-768 (L3), ML-KEM-1024 (L5)
PKCS#11 HSMVendor-specific mechanismsDepends on HSM firmware support

ML-KEM is primarily relevant for /serverkeygen responses where the server encrypts the generated private key for transport to the client.

Summary Matrix

StandardScopeStatus
RFC 7030EST protocolCore implementation – all six endpoints
RFC 8951EST clarificationsFully implemented
RFC 4210 / 9810CMP protocolInitialization, certification, key update, revocation
RFC 5272CMC (Full)/fullcmc endpoint, partial control attributes
RFC 8295CMS-ESTCMS-wrapped EST for air-gapped environments
RFC 8739STAR auto-renewalShort-lived certificate management
RFC 7252CoAP transportConstrained device enrollment over DTLS
RFC 9483EST-coapsEST over CoAP with compressed URI paths
RFC 7959CoAP block transferBlock1/Block2 for large certificate payloads
RFC 6960OCSPClient certificate revocation checking
RFC 6347DTLS 1.2CoAP transport security
RFC 9147DTLS 1.3CoAP transport security (modern)
CA/B Forum BRCertificate profiles, validityEnforced at CSR validation and signing
NIAP CA PP v2.0Protection ProfileSFR mapping documented
FIPS 140-3Cryptographic modulesVia HSM integration
FIPS 204ML-DSA post-quantum signingVia Synta / PKCS#11
FIPS 203ML-KEM post-quantum KEMVia Synta / PKCS#11
Composite PQCHybrid ML-DSA + classicalMigration path to post-quantum
CNSA Suite 2.0NSA algorithm guidanceTimeline tracking for PQC transition
RFC 9908CSR Attributes ClarificationServer-templated CSR subjects + key constraints
draft-est-renewal-infoEST Renewal InformationSuggested renewal window scheduling

RFC 9908 – CSR Attributes Template

The /csrattrs endpoint supports the RFC 9908 CertificationRequestInfoTemplate attribute alongside the traditional OID-list mode (backward compatible).

FeatureStatusNotes
id-aa-certificationRequestInfoTemplate OID (1.2.840.113549.1.9.16.2.61)ImplementedAttribute wraps the template in CsrAttrs SEQUENCE
Subject NameTemplate with optional valuesImplementedSingleAttributeTemplate — absent value means client supplies
SubjectPublicKeyInfoTemplate [0] IMPLICITImplementedAlgorithmIdentifier with EC curve or RSA NULL params
Mandatory [1] attributes fieldImplementedAlways emitted, even as empty SET (A1 00) per ASN.1 spec
id-aa-extensionReqTemplate OID (1.2.840.113549.1.9.16.2.62)ImplementedExtension OIDs with absent values for client-filled extensions
Per-label template variationImplementedDifferent labels can advertise different template constraints
Backward compatibility (§4)ImplementedOID-list entries coexist with template Attribute in same SEQUENCE

Configuration

[est.csr_template]
key_algorithm = "ec:P-256"
required_extensions = ["2.5.29.17"]

[[est.csr_template.subject]]
oid = "2.5.4.10"
value = "Example Corp"

[[est.csr_template.subject]]
oid = "2.5.4.3"
# No value — client must supply commonName

draft-ietf-lamps-est-renewal-info – EST Renewal Information

The GET /.well-known/est/renewal-info/{cert_id} endpoint returns a JSON object with a suggested renewal window for the identified certificate.

FeatureStatusNotes
cert_id format (base64url(AKI).base64url(serial))ImplementedURL-safe alphabet without padding
suggestedWindow JSON responseImplementedISO 8601 start and end timestamps
Retry-After headerImplementedConfigurable interval
AKI mismatch rejectionImplementedReturns 404 if AKI doesn’t match any known CA
cert_id length validationImplementedRejects >256 characters (400)

Conformance Test Suite

kipuka ships a wire-format conformance test suite in contrib/conformance/ that validates protocol compliance beyond HTTP status codes. Each suite uses openssl asn1parse, openssl pkcs7, openssl x509, and xxd for DER-level assertion.

./contrib/conformance/run-all.sh           # against running server
./contrib/conformance/run-all.sh --deploy  # full lifecycle
SuiteRFCAssertionsStatus
rfc7030-estRFC 703031PKCS#7 structure, cert chain, Content-Type, WWW-Authenticate
rfc9908-csrattrsRFC 990819Template OID, version, NameTemplate, SPKI, mandatory [1]
auth-otp12OTP lifecycle, consumption, revocation, reuse rejection
auth-mtlsRFC 7030 §4.2.26mTLS re-enrollment, subject matching
admin-api15Health, CAs, certs, auth boundary
tls-complianceRFC 7030 §3.37TLS 1.2+, AEAD ciphers, chain verification
est-renewal-infodraft9cert_id, JSON schema, Retry-After, error cases
rfc8739-starRFC 87398Order lifecycle (skips if manager not initialized)
rfc8295-cms-estRFC 82956Endpoint reachability, Content-Type handling
rfc4210-cmpRFC 42105Endpoint, Content-Type, error responses
rfc9483-coapRFC 94831169 cargo tests + URI routing + content-format IDs
audit-niapNIAP CA PP8Audit event source verification
Total129

NIAP CA Protection Profile

This page maps kipuka capabilities to the Security Functional Requirements (SFRs) defined in the NIAP Certificate Authority Protection Profile v2.0. The mapping is intended for evaluation teams preparing a Common Criteria assessment and for operators who need to verify that their deployment satisfies the Protection Profile.

Note: A Common Criteria evaluation requires an accredited evaluation facility. This document describes how kipuka addresses each SFR at the implementation level; it does not constitute a certified Security Target.

Audit (FAU)

FAU_GEN.1 – Audit Data Generation

Requirement: The TOE shall generate an audit record for each auditable event, including the date/time, event type, subject identity, and outcome.

How kipuka satisfies it:

The kipuka::audit module records structured audit events through the AuditEvent struct. Every event includes:

  • Timestamp (UTC, microsecond precision)
  • Event type (AuditEventType enum)
  • Subject identity (client certificate DN, OTP identifier, or GSSAPI principal)
  • Outcome (success or failure with reason)
  • Source IP address and EST label

The following event types are audited:

Event TypeTrigger
EnrollRequestClient submits a CSR to /simpleenroll, /simplereenroll, or /fullcmc
CertIssueCertificate successfully signed and returned to client
CertReenrollCertificate re-enrollment completed
CertRevokeCertificate revocation processed
EnrollRejectCSR rejected (policy violation, invalid key, unauthorized)
AuthSuccessClient authentication succeeded (any method)
AuthFailureClient authentication failed (bad OTP, invalid cert, GSSAPI error)
KeyGenerateCA key pair generated (HSM or software)
KeyLoadCA key loaded from HSM slot or file
KeyDestroyCA key destroyed (HSM C_DestroyObject or file wipe)
OtpCreateOTP token created via admin API
OtpUseOTP token consumed during enrollment
OtpRevokeOTP token revoked via admin API
OtpExpireOTP token expired (TTL exceeded)
AdminLoginOperator authenticated to admin API
AdminLogoutOperator session ended
AdminActionOperator performed an administrative operation
CaStartCA instance started and ready for signing
CaStopCA instance shut down
CaHealthChangeCA health status changed (HA failover event)
CrlGenerateCRL generated (when using Dogtag back-end)
SecurityViolationPolicy violation detected (tampered request, replay, etc.)

Implementation location: kipuka::audit::record() is called from every EST handler and authentication layer.

Evaluation notes: The event set covers all “minimum audit events” listed in Table 5 of the Protection Profile. Additional events (OTP lifecycle, HA health changes) exceed the minimum.

FAU_GEN.2 – User Identity Association

Requirement: The TOE shall associate each auditable event with the identity of the user that caused the event.

How kipuka satisfies it:

The AuditEvent struct contains a subject field populated from the authenticated identity:

  • mTLS: Subject DN and serial number of the client certificate.
  • OTP: The OTP identifier (opaque token ID, not the secret).
  • GSSAPI: The Kerberos principal name.
  • CMS: The signer DN from the CMS SignerInfo.
  • Unauthenticated (/cacerts): Recorded as "anonymous" with the source IP.

FAU_STG.1 – Protected Audit Trail Storage

Requirement: The TOE shall protect the stored audit records from unauthorized deletion.

How kipuka satisfies it:

Audit records are written through two channels, both designed for append-only semantics:

  1. Database – Audit records are INSERT-only. The database schema does not expose UPDATE or DELETE operations on the audit table. When using PostgreSQL, row-level security policies can further restrict modification.

  2. File – When audit.file is configured, events are appended to a JSON Lines file. kipuka opens the file in append-only mode (O_APPEND). File-system permissions should restrict the audit file to the kipuka service account.

  3. Syslog – When audit.syslog is configured, events are forwarded to a remote syslog server over TLS, placing the audit trail outside the TOE boundary.

Evaluation notes: The Protection Profile requires that “the TSF shall be able to prevent unauthorized deletion of the stored audit records.” Operators must ensure that file-system permissions and database RBAC are configured to prevent the kipuka process from deleting its own audit records. Shipping audit events to a remote syslog server (channel 3) provides the strongest protection.

Cryptographic Support (FCS)

FCS_CKM.1 – Cryptographic Key Generation

Requirement: The TOE shall generate asymmetric cryptographic keys in accordance with a specified key generation algorithm and key sizes.

How kipuka satisfies it:

Key TypeGeneration MethodModule
RSA 2048, 3072, 4096PKCS#11 C_GenerateKeyPair (HSM) or Synta softwarekipuka_hsm::key
ECDSA P-256, P-384, P-521PKCS#11 C_GenerateKeyPair (HSM) or Synta softwarekipuka_hsm::key
ML-DSA-44, ML-DSA-65, ML-DSA-87Synta software or vendor PKCS#11kipuka_hsm::key
Certificate serial numbersOS CSPRNG (getrandom) or PKCS#11 C_GenerateRandomkipuka::ca::issue

Serial numbers are 160 bits (20 bytes) from a CSPRNG, exceeding the CA/B Forum minimum of 64 bits and the NIAP requirement for unpredictable serial numbers.

Implementation location: kipuka_hsm::key::HsmKeyPair for CA keys; kipuka_est::serverkeygen for end-entity keys generated via /serverkeygen.

FCS_CKM.2 – Cryptographic Key Distribution

Requirement: The TOE shall distribute cryptographic keys in accordance with a specified key distribution method.

How kipuka satisfies it:

  • CA certificate distribution – The /cacerts endpoint returns the CA certificate chain as a PKCS#7 certs-only message. Distribution is over TLS (FCS_TLSS_EXT.1).

  • Server-generated key distribution – When /serverkeygen is enabled, the generated private key is encrypted to the client using PKCS#7 EnvelopedData:

    • AES-256 key wrapping with the content-encryption key wrapped to the client’s public key via RSA-OAEP or ECDH-ES.
    • The wrapped key and signed certificate are returned as a single multipart MIME response.

Implementation location: kipuka_est::serverkeygen for key wrapping; kipuka_est::cacerts for CA chain distribution.

FCS_COP.1 – Cryptographic Operation

Requirement: The TOE shall perform cryptographic operations in accordance with specified algorithms and key sizes.

How kipuka satisfies it:

OperationAlgorithm(s)Key SizeModule
Certificate signingRSA PKCS#1 v1.5, RSA-PSS, ECDSA>= 2048 (RSA), P-256/P-384/P-521 (EC)kipuka_hsm::sign
Certificate signing (PQC)ML-DSAL2/L3/L5kipuka_hsm::sign::sign_ml_dsa
HashingSHA-256, SHA-384, SHA-512Synta / HSM
TLSTLS 1.2 / TLS 1.3Per cipher suiterustls
OTP hashingArgon2id, bcrypt, SHA-256-HMACkipuka_otp
Key wrapping (/serverkeygen)AES-256-WRAP, RSA-OAEP256 (AES), >= 2048 (RSA)kipuka_est::serverkeygen

Implementation location: kipuka_hsm::sign dispatches to sign_rsa_pkcs1, sign_rsa_pss, sign_ecdsa, or sign_ml_dsa depending on the CA key type.

FCS_RBG_EXT.1 – Random Bit Generation

Requirement: The TOE shall use a DRBG that is seeded by an entropy source.

How kipuka satisfies it:

  • With HSM: Random bytes are obtained via PKCS#11 C_GenerateRandom, which invokes the HSM’s FIPS-validated DRBG.
  • Without HSM: Random bytes are obtained from the operating system’s CSPRNG via getrandom(2) on Linux or the equivalent on other platforms.

Serial number generation always uses whichever RBG is active.

FCS_TLSS_EXT.1 – TLS Server

Requirement: The TOE shall implement TLS 1.2 and/or TLS 1.3 as a server.

How kipuka satisfies it:

kipuka uses rustls for TLS termination.

ParameterConfigurationNotes
Minimum versiontls.min_version (default "1.2")Set to "1.3" to disable TLS 1.2 entirely.
Cipher suitestls.cipher_suitesDefaults to rustls safe defaults (TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256 for TLS 1.3; ECDHE suites for TLS 1.2).
Client authtls.client_authSupports required, optional, and none modes.
Server certificate EKUid-kp-cmcRAThe server certificate should include the CMC Registration Authority extended key usage OID (1.3.6.1.5.5.7.3.28) for NIAP compliance.

Implementation location: kipuka::tls module configures rustls::ServerConfig.

Identification and Authentication (FIA)

FIA_AFL.1 – Authentication Failure Handling

Requirement: The TOE shall detect when a configurable number of unsuccessful authentication attempts occur and take action.

How kipuka satisfies it:

The OTP authentication subsystem implements rate limiting and lockout:

ParameterConfig KeyDefault
Max consecutive failuresotp.max_failures5
Failure counting windowotp.failure_window15 minutes
Lockout durationotp.lockout_duration30 minutes

When the threshold is reached:

  1. An AuthFailure audit event is recorded with reason "lockout".
  2. Subsequent authentication attempts for that OTP identifier return HTTP 403 until the lockout expires.
  3. If the lockout is triggered by mTLS (invalid client cert), the TLS handshake fails before the HTTP layer is reached; the audit event records the client IP and certificate serial number.

Implementation location: kipuka::auth::otp for OTP lockout; kipuka::auth::mtls for mTLS failure logging.

FIA_UAU.1 – Timing of Authentication

Requirement: The TOE shall allow only the retrieval of CA certificates before the user is authenticated.

How kipuka satisfies it:

  • /cacerts – Unauthenticated. No client credential is required.
  • All other endpoints (/simpleenroll, /simplereenroll, /fullcmc, /serverkeygen, /csrattrs) – Require authentication via at least one configured method (mTLS, OTP, GSSAPI, or CMS signature).

The EstAuth extractor in kipuka::auth enforces this policy. The OptionalAuth extractor is used only for /cacerts and returns AuthMethod::None when no credential is presented.

Implementation location: kipuka::auth::EstAuth and kipuka::auth::OptionalAuth.

FIA_UAU.5 – Multiple Authentication Mechanisms

Requirement: The TOE shall provide multiple authentication mechanisms.

How kipuka satisfies it:

kipuka supports five authentication methods, selectable per EST label:

MethodAuthMethod VariantUse Case
Mutual TLSMtlsPrimary method for re-enrollment and machine identity
One-Time PasswordOtpInitial device bootstrapping
GSSAPI / KerberosGssapiEnterprise domain-joined devices
CMS signatureCmsFull CMC requests signed by an RA
None (unauthenticated)None/cacerts only

Security Management (FMT)

FMT_SMR.1 – Security Roles

Requirement: The TOE shall maintain the roles of operator and user.

How kipuka satisfies it:

kipuka defines two distinct roles:

RoleAccessAuthentication
OperatorAdmin API (OTP management, CA health, HA control, audit retrieval)Admin-specific mTLS certificate or bearer token, on a separate listener (admin_listen)
UserEST endpoints (enrollment, renewal, CA certificate retrieval)EST client credentials (mTLS, OTP, GSSAPI)

The admin API listens on a separate address and port (server.admin_listen, default 127.0.0.1:9443) with independent TLS configuration. No EST client credential grants access to the admin API, and no admin credential grants access to EST endpoints on behalf of a client.

Implementation location: kipuka::routes::admin for operator endpoints; kipuka::routes::est for user endpoints.

FMT_SMF.1 – Management Functions

Requirement: The TOE shall provide management functions for authorized operators.

How kipuka satisfies it:

The admin API provides:

FunctionEndpointDescription
OTP managementPOST /admin/otp, DELETE /admin/otp/{id}Create and revoke OTP tokens
CA healthGET /admin/ca/healthView CA status, signing latency, HSM connectivity
HA controlPOST /admin/ha/failoverForce failover to a backup CA
Audit retrievalGET /admin/auditQuery audit records with time range and event type filters
Configuration reloadPOST /admin/reloadHot-reload certificate and label configuration without restart

Trusted Path (FTP)

FTP_TRP.1 – Trusted Path

Requirement: The TOE shall provide a communication path between itself and remote users that is logically distinct from other paths and provides assured identification of its endpoints.

How kipuka satisfies it:

  • EST over TLS – All enrollment communication uses TLS with server certificate authentication and (for authenticated endpoints) mutual TLS. The server certificate identifies the kipuka instance; the client certificate identifies the enrolling device.

  • Admin on separate TLS – The admin API uses an independent TLS listener with its own certificate and trust anchors. This provides logical separation between the user-facing EST path and the operator-facing management path.

  • DTLS for CoAP – CoAP enrollment uses DTLS 1.2 for the trusted path, providing the same endpoint authentication guarantees over UDP.

Implementation location: kipuka::tls for EST and admin TLS; kipuka_coap::dtls for DTLS.

SFR Coverage Summary

SFRRequirementkipuka ModuleStatus
FAU_GEN.1Audit data generationkipuka::auditSatisfied
FAU_GEN.2User identity associationkipuka::auditSatisfied
FAU_STG.1Protected audit trailkipuka::audit, DB, syslogSatisfied
FCS_CKM.1Key generationkipuka_hsm::keySatisfied
FCS_CKM.2Key distributionkipuka_est::serverkeygen, kipuka_est::cacertsSatisfied
FCS_COP.1Crypto operationskipuka_hsm::sign, rustlsSatisfied
FCS_RBG_EXT.1Random bit generationOS CSPRNG, PKCS#11Satisfied
FCS_TLSS_EXT.1TLS serverkipuka::tls (rustls)Satisfied
FIA_AFL.1Auth failure handlingkipuka::auth::otpSatisfied
FIA_UAU.1Timing of authenticationkipuka::authSatisfied
FIA_UAU.5Multiple auth mechanismskipuka::authSatisfied
FMT_SMR.1Security roleskipuka::routes::admin, kipuka::routes::estSatisfied
FMT_SMF.1Management functionskipuka::routes::adminSatisfied
FTP_TRP.1Trusted pathkipuka::tls, kipuka_coap::dtlsSatisfied

CA/B Forum Baseline Requirements

This page documents compliance with the CA/Browser Forum Baseline Requirements for the Issuance and Management of Publicly-Trusted TLS Server Certificates, Version 2.2.8 (16-Jun-2026). Operators issuing certificates under a publicly-trusted root must configure kipuka and the issuing CA to satisfy these requirements jointly; operators running private PKI may relax constraints through configuration.

Responsibility Boundary

kipuka is a Registration Authority (RA) and enrollment server. It validates CSRs and enforces certificate profile constraints, but it does not perform domain validation, CAA checking, or revocation services. Those responsibilities belong to the issuing Certification Authority.

The following table maps each BR area to the specific software component that owns it. When deploying under a publicly-trusted root, all affected components must be configured correctly.

kipuka (RA) Responsibilities

BR AreaCrateStatus
CSR validation (key size, SAN, extensions)kipuka-estEnforced
RSA exponent / key quality checks (§6.1.6)kipuka-est, syntaNot Implemented
Weak key rejection (§6.1.1.3)kipuka-estNot Implemented
Serial number generationkipuka-est, kipuka-hsmEnforced
Certificate construction (AKI, SKI, BC, KU, EKU, SAN)syntaEnforced
AIA / CDP injectionsyntaNot Implemented
Certificate Policies (Reserved OIDs)syntaPartial
Validity period clampingkipuka-estEnforced
notBefore 48-hour enforcementkipuka-estNot Enforced
SHA-1 signing rejectionkipuka-estNot Enforced
organizationalUnitName prohibitionkipuka-estNot Implemented
Server-side key generationkipuka-est, syntaEnforced
OTP authenticationkipuka-otpEnforced
HSM key operationskipuka-hsmEnforced
CoAP/DTLS transportkipuka-coapEnforced
Audit logging (enrollment)kipuka-estEnforced
Certificate linting (pre-sign)kipuka-estNot Implemented
Dogtag CA integrationkipuka-dogtagEnforced

Issuing CA (Dogtag PKI) Responsibilities

BR AreaSubsystemStatus
Domain validation (DCV)base/acmeAvailable
CAA record processing (§4.2.2.1)base/caNot Implemented
DNSSEC validation of CAAbase/caNot Implemented
Multi-Perspective Issuance (§3.2.2.9)base/acmeNot Implemented
Certificate Transparency (SCT)base/ca (CTEngine)Available
Certificate linting (post-sign)base/caNot Implemented
OCSP responderbase/ocspAvailable
CRL generation and publishingbase/caAvailable
Revocation processingbase/caAvailable
Key archival / escrowbase/kraAvailable
Signed audit loggingbase/serverAvailable

Certificate Profile Enforcement

kipuka validates every CSR against the BR certificate profile before forwarding to the CA for signing. A CSR that violates any rule is rejected with an EnrollReject audit event and an HTTP 400 response.

Subject Fields

FieldBR Sectionkipuka BehaviorStatus
commonName§7.1.4.3Rejected if CN not in SANEnforced
organizationName§3.2.2.2Not verified; use subject_pattern to restrictCA Responsibility
organizationalUnitName§7.1.2.10.2Not stripped or rejectedNot Implemented
serialNumberNot included by defaultN/A
countryName§3.2.2.3ISO 3166 format-validatedEnforced

Key Type and Size Requirements (§6.1.5)

Key TypeMinimumStatus
RSA2048 bitsEnforced (rejected if smaller)
RSA3072 bitsConfigurable via allowed_key_types
ECDSA P-256256 bitsEnforced
ECDSA P-384384 bitsEnforced
ECDSA P-521521 bitsEnforced (not recommended by BR)

To restrict a label to specific key types:

[[est.label]]
name = "web-servers"
ca_id = "issuing-ca-1"
allowed_key_types = ["rsa-3072", "rsa-4096", "ecdsa-p256", "ecdsa-p384"]

Public Key Quality Checks (§6.1.6)

CheckBR RequirementStatus
RSA public exponent is odd and >= 3SHALL confirm (§6.1.6)Not Implemented
RSA exponent in range 2^16+1 to 2^256-1SHOULD (§6.1.6)Not Implemented
RSA modulus divisible by 8, not prime power, no factors < 752SHOULD (§6.1.6)Not Implemented
ECDSA point validation (ECC Full or Partial)SHOULD (§6.1.6)Not Implemented

Weak Key Rejection (§6.1.1.3)

The BR requires CAs to reject keys known to be weak. kipuka does not currently perform weak key checks.

CheckSourceStatus
Debian weak keysgithub.com/cabforum/Debian-weak-keysNot Implemented
ROCA vulnerabilitygithub.com/crocs-muni/rocaNot Implemented
Close Primes (Fermat factorization, 100 rounds)fermatattack.secvuln.infoNot Implemented

Serial Number Generation

The BR requires that certificate serial numbers contain at least 64 bits of output from a CSPRNG (§7.1).

kipuka generates 160-bit (20-byte) serial numbers, significantly exceeding the minimum. Serial numbers must be non-sequential, greater than zero, and less than 2^159.

ConfigurationSourceEntropy
HSM configuredPKCS#11 C_GenerateRandom160 bits from HSM’s FIPS-validated DRBG
Software-onlygetrandom(2)160 bits from OS CSPRNG

Serial numbers are encoded as unsigned integers with the high bit set to zero to ensure a positive ASN.1 INTEGER encoding (per RFC 5280 §4.1.2.2).

Extension Enforcement

Mandatory Extensions (§7.1.2.7.6)

ExtensionCriticalkipuka BehaviorStatus
Authority Key IdentifierNSHA-1 hash of CA public keyEnforced
Subject Key IdentifierNSHA-1 hash of EE public keyEnforced
Basic ConstraintsYCA:FALSE, no pathLenEnforced
Key UsageYFrom ca.default_key_usageEnforced
Extended Key UsageNserverAuth + clientAuthEnforced
Subject Alternative Name*Required (require_san = true)Enforced
Authority Info AccessNCA profile responsibilityNot Implemented
Certificate PoliciesNOperator-configuredPartial
CRL Distribution PointsNCA profile responsibilityNot Implemented

Notes:

  • AIA MUST contain at least one AccessDescription. The id-ad-caIssuers method (SHOULD) provides a URL to the issuing CA’s certificate. When using Dogtag, this is set in the certificate profile.
  • CRL Distribution Points presence depends on whether the certificate includes an AIA OCSP pointer and the certificate’s validity period (§7.1.2.11.2).
  • Certificate Policies MUST include a Reserved Certificate Policy Identifier matching the validation type: DV (2.23.140.1.2.1), IV (2.23.140.1.2.3), OV (2.23.140.1.2.2), or EV (2.23.140.1.1). kipuka does not enforce the correct OID per validation type.

Extension Validation Rules

  • CSRs with basicConstraints CA:TRUE are rejected.
  • CSRs with Key Usage keyCertSign or cRLSign are rejected.
  • pathLenConstraint MUST NOT be present in subscriber certificates (§7.1.2.7.8). kipuka does not include it, but does not actively reject CSRs that request it.
  • SAN entries are validated: DNS names must be valid hostnames (no wildcards unless enabled), IP addresses must parse as IPv4/IPv6, email addresses must contain exactly one @.
  • notBefore MUST be within 48 hours of the certificate signing operation (§7.1.2.7). kipuka does not enforce this constraint.

Subscriber Certificate Key Usage (§7.1.2.7.11)

For RSA keys, digitalSignature SHOULD be set. keyEncipherment MAY be set for TLS 1.2 compatibility. dataEncipherment is a Pending Prohibition.

For ECC keys, digitalSignature MUST be set. keyAgreement is a Pending Prohibition.

kipuka defaults: digitalSignature, keyEncipherment for RSA, digitalSignature for ECDSA. Configurable via ca.default_key_usage.

Subscriber Certificate EKU (§7.1.2.7.10)

Key PurposeOIDBR Presencekipuka Default
id-kp-serverAuth1.3.6.1.5.5.7.3.1MUSTIncluded
id-kp-clientAuth1.3.6.1.5.5.7.3.2MAYIncluded
id-kp-codeSigning1.3.6.1.5.5.7.3.3MUST NOTNot included
id-kp-emailProtection1.3.6.1.5.5.7.3.4MUST NOTNot included
id-kp-timeStamping1.3.6.1.5.5.7.3.8MUST NOTNot included
id-kp-OCSPSigning1.3.6.1.5.5.7.3.9MUST NOTNot included
anyExtendedKeyUsage2.5.29.37.0MUST NOTNot included

Validity Period Enforcement (§6.3.2)

The CA/B Forum is reducing maximum certificate validity on a declining timeline. kipuka tracks this through the max_validity_days parameter.

Issued on or afterIssued beforeSHOULD NOT exceedMUST NOT exceedRecommended config
2026-03-15397 days398 days398
2026-03-152027-03-15199 days200 days200
2027-03-152029-03-1599 days100 days100
2029-03-1546 days47 days47

For the purpose of these calculations, a day is measured as 86,400 seconds. Any amount of time greater than this, including fractional seconds and/or leap seconds, represents an additional day. Certificates SHOULD NOT be issued for the maximum permissible time by default, to account for clock drift.

Each CA and each EST label can specify a maximum validity:

[[ca]]
id = "public-ca"
name = "Public TLS CA"
cert = "/etc/kipuka/ca/public-ca.pem"
key = "/etc/kipuka/ca/public-ca.key"
validity_days = 90
max_validity_days = 200

The effective maximum is the minimum of the CA’s max_validity_days, the label’s max_validity_days, and the client’s requested validity. If the client requests more, kipuka clamps notAfter to the allowed maximum and logs the original and clamped values.

STAR Integration

STAR (RFC 8739) auto-renewal certificates typically use much shorter validity periods (hours or days). STAR renewal orders track their own interval independently of max_validity_days.

Domain Validation

kipuka does not perform domain validation. This is a CA responsibility. When using Dogtag as the issuing CA, the ACME subsystem provides HTTP-01 and DNS-01 challenge validation.

Validation Data Reuse (§4.2.1)

Domain and IP address validation data has maximum reuse periods that decline over time:

Issued on or afterIssued beforeMax reuse period
2026-03-15398 days
2026-03-152027-03-15200 days
2027-03-152029-03-15100 days
2029-03-1510 days

Prohibited Names (§4.2.2)

CAs SHALL NOT issue certificates containing Internal Names or Reserved IP Addresses. Effective 2026-03-15, certificates SHALL NOT contain Domain Names ending in an IP Reverse Zone Suffix.

CAA Record Processing (§4.2.2.1)

Status: CA Responsibility – not implemented in Dogtag.

The CA MUST retrieve and process CAA records in accordance with RFC 8659 for each dNSName in the subjectAltName extension before certificate issuance. The CA MUST process the issue, issuewild, and iodef property tags.

RequirementBR SectionEffectiveStatus
CAA record retrieval and processing§4.2.2.1CurrentNot Implemented
Process accounturi and validationmethods parameters§4.2.2.1.2SHOULD now, MUST 2027-03-15Not Implemented
DNSSEC validation of CAA queries to IANA root trust anchor§4.2.2.1.32026-03-15Not Implemented
DNSSEC SERVFAIL is NOT permission to issue§4.2.2.1.32026-03-15Not Implemented

Impact: Neither kipuka nor Dogtag currently implements CAA checking. Any deployment under a publicly-trusted root must add CAA processing upstream or implement it in the CA before issuance.

Multi-Perspective Issuance Corroboration (§3.2.2.9)

Status: CA Responsibility – not implemented in Dogtag.

The CA MUST corroborate domain validation and CAA checks from multiple remote Network Perspectives before certificate issuance.

EffectiveMin Remote PerspectivesMin RIR RegionsQuorum (non-corroborations allowed)
2025-03-152Per Quorum Table
2026-03-1532Per Quorum Table
2026-06-1542Per Quorum Table
2026-12-1552Per Quorum Table

Quorum table (Table 5 of the BR):

Remote Perspectives UsedMax Non-Corroborations
2–51
6+2

Impact: This requirement is already in effect. Deployments under publicly-trusted roots cannot issue certificates without MPIC infrastructure.

Certificate Transparency (§4.3.1.2, §7.1.2.9)

Status: CA Responsibility – available in Dogtag.

RequirementBR SectionStatus
Lint to-be-signed certificate content before signing§4.3.1.2Not Implemented
Submit Precertificate to CT logs§7.1.2.9Available in Dogtag (CTEngine.java)
Embed SCT list in final certificate§7.1.2.9Available in Dogtag (caServerCertWithSCT.cfg)
Precertificate Signing CA profile (sunset 2026-03-15)§7.1.2.4N/A (already past effective date)

When using Dogtag with CT enabled, the CA creates a precertificate with the poison extension (OID 1.3.6.1.4.1.11129.2.4.3), submits it to configured CT logs, and replaces the poison with an SCT list extension (OID 1.3.6.1.4.1.11129.2.4.2) in the final certificate. kipuka receives the final certificate with embedded SCTs and returns it to the client.

Certificate Linting (§4.3.1.2)

Status: Not Implemented in either component.

Effective 2025-03-15, the CA SHALL implement a Linting process to test the technical conformity of each to-be-signed certificate artifact prior to signing. CAs SHOULD use widely-adopted Linting tools (see cabforum.org/resources/tools).

Linting could be split across the stack: kipuka could lint the CSR and proposed certificate content pre-submission, while the CA lints the to-be-signed tbsCertificate post-assembly. Neither integration exists today.

Revocation and OCSP

Revocation Requirements (§4.9)

Status: CA Responsibility.

RequirementBR SectionDogtag Support
Revoke within 24 hours for key compromise, mis-issuance, failed CAA§4.9.1.1ServiceRevoke handles revocation
Revoke within 5 days for non-critical reasons§4.9.1.1Supported
Investigate Certificate Problem Reports within 24 hours§4.9.5Operational procedure
24x7 revocation request acceptance§4.9.3Dogtag web UI + REST API
Mass revocation plan with 9 provisions§5.7.1.2Operational procedure (effective 2025-12-01)

CRL Requirements (§4.9.7)

RequirementFrequencyDogtag Support
Subscriber cert CRL (with AIA OCSP pointer)At least every 7 daysCRLIssuingPoint auto-update
Subscriber cert CRL (without OCSP pointer)At least every 4 daysConfigurable
CRL after revocationWithin 24 hoursImmediate on ServiceRevoke
CA certificate CRLAt least every 12 monthsConfigurable

OCSP Requirements (§4.9.9)

RequirementBR SectionStatus
OCSP response available within 15 minutes of issuance§4.9.9CA Responsibility (effective 2025-01-15)
OCSP GET method support§4.9.9Dogtag CAOCSPServlet
OCSP validity < 16 hours: update before half validity§4.9.9Configurable in Dogtag
OCSP validity >= 16 hours: update at least 8 hours before nextUpdate, no later than 4 days after thisUpdate§4.9.9Configurable in Dogtag
MUST NOT respond “unknown” for issued certs (effective 2025-01-15)§4.9.9Dogtag CRL-based OCSP

SHA-1 Sunset (§7.1.3.2)

Status: Not Enforced.

RequirementEffectiveStatus
SHA-1 MUST NOT be used in certificate signaturesCurrentkipuka does not validate signing algorithm
SHA-1 MUST NOT be used in CRL signatures2026-09-15CA Responsibility

kipuka should reject enrollment requests that specify SHA-1 as the signature algorithm. This check is not currently implemented.

Server-Side Key Generation (§6.1.2)

The /serverkeygen endpoint generates a key pair and returns the private key to the client in a PKCS#7 EnvelopedData structure.

ComponentAlgorithmNotes
Content encryptionAES-256-CBC or AES-256-GCMSymmetric encryption of the private key
Key wrapping (RSA)RSA-OAEP (SHA-256)Wraps the CEK to the client’s public key
Key wrapping (ECDH)ECDH-ES + AES-256-WRAPKey agreement with client’s EC public key

Additional BR constraints:

  • If the subscriber certificate will contain id-kp-serverAuth, the CA SHALL NOT generate a key pair on behalf of the subscriber and SHALL NOT accept a certificate request using a key pair previously generated by the CA (§6.1.1.3). kipuka does not enforce this cross-request check.
  • Parties other than the Subscriber SHALL NOT archive the Subscriber Private Key without authorization (§6.1.2).
  • Key pairs are generated using the same CSPRNG path as serial numbers and exist in kipuka’s memory only for the duration of the request.

Name Constraints and Encoding

Distinguished Name Encoding (§7.1.4)

All Distinguished Name components are encoded as UTF8String per RFC 5280 §4.1.2.4 and BR §7.1.4. kipuka does not use PrintableString or TeletexString encoding.

Name Constraints

kipuka enforces Name Constraints from the CA certificate at CSR validation time:

  • Permitted subtrees – SAN DNS names must fall within permitted DNS subtrees. IP addresses must fall within permitted IP ranges.
  • Excluded subtrees – SAN entries matching excluded subtrees cause rejection.

Enforcement before signing means a constraint violation produces an EnrollReject audit event rather than a mis-issued certificate.

Internationalized Domain Names (§7.1.4.2)

kipuka accepts IDN in SAN dNSName entries only in A-label (Punycode) form. U-label (Unicode) forms are rejected at CSR validation.

Compliance Timeline

Operators deploying kipuka under a publicly-trusted root should track these upcoming BR deadlines:

DateSectionRequirementOwner
Effective§4.2.2.1.3DNSSEC validation on CAADogtag CA
Effective§3.2.2.9MPIC with 4 perspectivesDogtag ACME
Effective§4.3.1.2Certificate lintingBoth
Effective§4.9.9OCSP within 15 minutesDogtag OCSP
Effective§6.3.2Max validity 200 dayskipuka
2026-07-15§5.4.1Detailed verification logsBoth
2026-09-15§7.1.3.2.1SHA-1 sunset (certs + CRLs)Both
2026-12-15§3.2.2.9MPIC with 5 perspectivesDogtag ACME
2027-03-15§4.2.2.1.2CAA accounturi processingDogtag CA
2027-03-15§3.2.2.4DCV method sunsetDogtag ACME
2027-03-15§6.3.2Max validity 100 dayskipuka
2029-03-15§6.3.2Max validity 47 dayskipuka
2029-03-15§4.2.1DCV reuse max 10 daysDogtag ACME

Compliance Checklist

kipuka Enforcement

RequirementSectionStatus
RSA >= 2048 bits§6.1.5Enforced
ECDSA P-256/P-384/P-521§6.1.5Enforced
RSA exponent validation§6.1.6Not Implemented
Weak key rejection§6.1.1.3Not Implemented
Serial >= 64 bits CSPRNG§7.1Exceeded (160 bits)
AKI present§7.1.2.11.1Enforced
SKI present§7.1.2.11.4Enforced
CA:FALSE for EE certs§7.1.2.7.8Enforced
pathLen MUST NOT for EE§7.1.2.7.8Enforced (absent)
Key Usage critical§7.1.2.7.11Enforced
EKU with serverAuth§7.1.2.7.10Enforced
SAN required§7.1.2.7.12Enforced
CN matches SAN§7.1.4.3Enforced
AIA (OCSP, caIssuers)§7.1.2.7.7Not Implemented
CRL Distribution Points§7.1.2.11.2Not Implemented
Certificate Policies (Reserved OID)§7.1.2.7.9Partial
OU prohibited§7.1.2.10.2Not Implemented
Max validity period§6.3.2Enforced
notBefore within 48 hours§7.1.2.7Not Enforced
DN encoding (UTF8String)§7.1.4Enforced
IDN A-label only§7.1.4.2Enforced
Name Constraints§7.1.5Enforced
/serverkeygen key protection§6.1.2Enforced
SHA-1 signing rejection§7.1.3.2Not Enforced
Certificate linting (pre-sign)§4.3.1.2Not Implemented

Dogtag CA Enforcement

RequirementSectionStatus
Domain validation§3.2.2.4Available (ACME)
CAA record processing§4.2.2.1Not Implemented
DNSSEC validation of CAA§4.2.2.1.3Not Implemented
Multi-Perspective Issuance§3.2.2.9Not Implemented
Certificate Transparency§4.3.1.2Available (CTEngine)
Certificate linting (post-sign)§4.3.1.2Not Implemented
OCSP within 15 minutes§4.9.9Available
CRL issuance frequency§4.9.7Available
Revocation processing§4.9.1Available
Mass revocation plan§5.7.1.2Procedure required

Public Trust Roadmap

This page outlines the engineering plan for making the kipuka + Dogtag stack eligible for inclusion in browser root trust stores (Chrome, Mozilla, Apple) under the CA/Browser Forum Baseline Requirements.

Tracking: RHELBU-3179 | BR Version: 2.2.8 (June 2026) | Target: Production readiness by mid-2027

Target Customers

CustomerUse CaseScale
GSA Federal PKIPublic trust TLS for .gov websites800K–1M certs
DISA PKICross-pair trust with Federal PKI~50M certs
EnterpriseAny customer joining the public trustVariable

Architecture

The public trust stack separates enrollment (kipuka), issuance (Dogtag), and validation (external services):

┌─────────────────────────────────────────────────────────────┐
│                    Browser Root Stores                       │
│                 (Chrome, Mozilla, Apple)                     │
└────────────────────────┬────────────────────────────────────┘
                         │ Trust
┌────────────────────────▼────────────────────────────────────┐
│                   Dogtag CA (RHCS)                           │
│    Certificate signing · CT log submission · CRL · OCSP      │
│    Enrollment profiles · Key archival (KRA)                  │
└────────────────────────┬────────────────────────────────────┘
                         │ Issue
┌──────────┐  ┌──────────▼──────────┐  ┌──────────────────┐
│ pkimetal  │◄─│    kipuka (RA/EST)   │─►│    Open MPIC     │
│ (linter)  │  │                      │  │ (geo-validated)  │
│           │  │  CAA · Key blocklist  │  │ 5 cloud regions  │
│ Container │  │  ACME · Audit log     │  │ across 3+ RIRs   │
│ sidecar   │  │  Conformance suite    │  │                  │
└──────────┘  └──────────┬──────────┘  └──────────────────┘
                         │ Enroll
              ┌──────────▼──────────┐
              │  EST / ACME Clients  │
              │  certbot · curl      │
              │  libest · cert-mgr   │
              └─────────────────────┘

Component responsibilities:

  • kipuka — Registration Authority. Authenticates clients, validates CSRs, checks CAA records, enforces key blocklist, calls pkimetal for linting, calls MPIC for multi-perspective validation, forwards to Dogtag for signing.
  • Dogtag CA — Certificate Authority. Signs certificates, manages CT log submission, generates CRLs, operates OCSP responder, enforces enrollment profiles.
  • pkimetal — Pre-issuance linting sidecar. Wraps zlint, pkilint, certlint, and special-purpose checks (ROCA, Fermat, pwnedkeys) behind a single REST API.
  • Open MPIC — Multi-perspective validation service. Corroborates domain validation and CAA checks from geographically distributed network perspectives.

Compliance Gaps

What’s Already Compliant

RequirementBR SectionComponentNotes
Serial number entropy (≥64 bits)§7.1kipuka160-bit CSPRNG
Key size enforcement (RSA ≥2048)§6.1.5kipukaEnforced in CSR validation
Certificate extensions (AKI, SKI, BC, KU, EKU)§7.1.2kipuka + syntaAll mandatory extensions
PKCS#7 certs-only response format§4.2.3kipukaFixed in v0.1.0
Audit logging§5.4kipuka + DogtagNIAP FAU_GEN.1 compliant
OCSP responder§4.9.9DogtagAvailable
CRL generation§4.9.7DogtagAuto-update configurable
Certificate Transparency§7.1.2.9DogtagCTEngine with SCT embedding
Revocation processing§4.9.1DogtagWith CRLReason codes
DN encoding (UTF8String)§7.1.4kipuka + syntaPer RFC 5280
Name Constraints enforcement§7.1.5kipukaAt CSR validation time
STAR short-lived certificatesRFC 8739kipukaFor 47-day mandate
RFC conformance test suitekipuka12 suites, 129 assertions

What Needs to Be Built

#GapBR SectionComponentStatus
P0Validity period enforcement§6.3.2kipukaLive bug — hardcoded 398, should be 200
P1Default EKU serverAuth-only§7.1.2.7.10kipukaDeadline passed — includes clientAuth
P2CAA record checking§4.2.2.1kipukaNot implemented
P3Pre-issuance linting§4.3.1.2kipuka + pkimetalNot implemented
P4Key compromise blacklisting§6.1.1.3kipukaNot implemented
P5ACME protocolRFC 8555kipukaNot implemented
P6Multi-perspective validation§3.2.2.9Open MPICNot implemented
P7CA hierarchy separationChrome Root ProgramDogtag + operationalArchitecture decision

Phased Delivery Plan

Phase 1: Fix Live Compliance Bugs (1 week)

P0 — Validity Period (§6.3.2)

The certificate validity check uses a hardcoded 398-day maximum. The CA/B Forum reduced this to 200 days effective March 15, 2026 (Ballot SC-081v3). This is a date-aware stepped schedule:

EffectiveMax Validity
Before 2026-03-15398 days
2026-03-15 – 2027-03-14200 days ← current
2027-03-15 – 2029-03-14100 days
2029-03-15+47 days

Fix: Replace the static constant with a date-aware schedule function in src/ca/issue.rs. Three code locations.

P1 — EKU Default (Chrome Root Program v1.6)

The default enrollment profile includes both serverAuth and clientAuth in the Extended Key Usage extension. Chrome Root Program v1.6+ requires serverAuth-only for publicly trusted TLS certificates. The June 15, 2026 deadline for leaf certificates has passed.

Fix: Change default EKU to serverAuth-only. One code location.

Phase 2: Hard Compliance Blockers (2–3 months)

P2 — CAA Record Checking (RFC 8659)

Before issuing a certificate, the RA must query DNS CAA records for each domain in the CSR’s Subject Alternative Name. If CAA records exist and do not authorize kipuka’s CA identity, issuance must be denied.

RequirementEffectiveStatus
CAA record processingSep 2017Not implemented
RFC 8657 accounturi / validationmethodsSHOULD now, MUST Mar 2027Not implemented
DNSSEC validation of CAA queriesMar 2026Not implemented

Implementation: New src/caa/ module in kipuka using the hickory-resolver crate for async DNS with DNSSEC. The RFC 8659 tree-climbing algorithm queries CAA records from the FQDN up through parent domains. ~1200 lines of Rust.

P3 — Pre-Issuance Linting (Ballot SC-75)

Mandatory since March 2025. The CA must lint every to-be-signed certificate before signing. kipuka will integrate with pkimetal, a Go-based meta-linter that wraps zlint, pkilint, certlint, and special-purpose checks behind a REST API.

Implementation: pkimetal runs as a container sidecar. kipuka calls its REST API after certificate construction, before returning to the client. ~500 lines of Rust for the HTTP client and config.

P4 — Key Compromise Blacklisting (Ballot SC-073)

Mandatory since November 2024. The CA must reject certificate requests using keys known to be compromised or weak.

CheckSource
Previously compromised keysDynamic blocklist (revocation + reports)
Debian weak keys (CVE-2008-0166)cabforum/Debian-weak-keys
ROCA vulnerabilitycrocs-muni/roca
Close primes (Fermat, 100 rounds)BR §6.1.1.3

Implementation: New blocked_keys database table with SHA-256 of SPKI DER. CSR validation hook before issuance. Revocation cascade when keys are reported compromised. ~600 lines of Rust.

Phase 3: MPIC Infrastructure (2–3 months, parallel)

P6 — Multi-Perspective Issuance Corroboration (Ballot SC-067v3)

Already mandatory. Domain validation and CAA checks must be corroborated from multiple geographically distributed network perspectives.

EffectiveMin PerspectivesMin RIR Regions
Sep 20252
Mar 202632
Jun 202642 ← current
Dec 202652

Implementation: Deploy Open MPIC on OpenShift across 5 cloud regions (ARIN, RIPE, APNIC minimum). kipuka calls the MPIC Coordinator REST API as a pre-issuance hook. ~300 lines of Rust integration + infrastructure deployment.

Phase 4: ACME Protocol (3–4 months)

P5 — ACME (RFC 8555)

Not mandated by CA/B Forum, but required by customers for automated certificate lifecycle. At 47-day validity (March 2029), manual enrollment is unsustainable — ACME or EST automation becomes essential.

Dogtag has an existing ACME responder (dogtag-pki-acme) but it lacks TLS-ALPN-01 (RFC 8737), External Account Binding, and is classified as Technology Preview.

Options:

  1. Build kipuka-acme crate alongside EST (preferred — full control, all 3 challenge types, reuses kipuka infrastructure)
  2. Enhance Dogtag’s Java ACME responder (existing code, but limited)
  3. Deploy step-ca alongside Dogtag (mature ACME, but adds operational complexity)

Scope: Account management, order state machine, HTTP-01/DNS-01/ TLS-ALPN-01 challenge validators, CSR finalization through existing issuance pipeline. 3–4 person-months.

Phase 5: Architecture (parallel)

P7 — CA Hierarchy Separation (Chrome Root Program)

Chrome Root Program v1.6+ requires that publicly trusted TLS certificates carry only serverAuth EKU — not just at the leaf level, but throughout the entire CA hierarchy. This means:

  • Dedicated TLS root → TLS intermediate(s): serverAuth only
  • Separate hierarchy for client auth, S/MIME, code signing

This is an operational / architectural decision, not a code change. Dogtag and kipuka already support multiple CA configurations with distinct profiles.

Timeline

Jul 2026  ████ Phase 1: Fix P0 (validity) + P1 (EKU)
          ████ Phase 2 starts: P2 (CAA), P3 (linting), P4 (key blocklist)
Aug 2026  ████████ Phase 2 continues
          ████ Phase 3 starts: P6 (MPIC deployment)
Sep 2026  ████████ Phase 2 completes
Oct 2026  ████████ Phase 3: MPIC operational (4 perspectives)
          ████ Phase 4 starts: P5 (ACME)
Nov 2026  ████████ Phase 4 continues
Dec 2026  ████ Phase 3: MPIC 5th perspective added
Jan 2027  ████████ Phase 4: ACME MVP (HTTP-01 + DNS-01)
Feb 2027  ████ Phase 4: TLS-ALPN-01 + client compat testing
Mar 2027  ████ Phase 5: P7 hierarchy design finalized
          ████ WebTrust / ETSI audit engagement
Jun 2027  ████ Production readiness

Additional BR Requirements (lower priority)

These are tracked in the detailed BR mapping but are not on the critical path for initial public trust:

RequirementBR SectionStatus
RSA exponent / key quality checks§6.1.6Not implemented
organizationalUnitName prohibition§7.1.2.10.2Not implemented
notBefore 48-hour enforcement§7.1.2.7Not enforced
SHA-1 signing rejection§7.1.3.2Not enforced
AIA / CDP extension injection§7.1.2.7.7Not implemented (CA profile)
Certificate Policies OID enforcement§7.1.2.7.9Partial
Debian weak key checks§6.1.1.3Not implemented (covered by P4)

References

HSM Compatibility Matrix

This page documents the Hardware Security Modules that kipuka has been tested with, their supported mechanisms, configuration specifics, and known limitations. kipuka communicates with HSMs exclusively through the PKCS#11 (Cryptoki) interface via the cryptoki crate.

Compatibility Overview

FeatureEntrust nShieldUtimaco CryptoServerKryopticThales Luna 7
PKCS#11 version2.402.402.402.40
RSA 2048YesYesYesYes
RSA 3072YesYesYesYes
RSA 4096YesYesYesYes
ECDSA P-256YesYesYesYes
ECDSA P-384YesYesYesYes
ECDSA P-521YesYesYesYes
RSA PKCS#1 v1.5 signingYesYesYesYes
RSA-PSS signingYesYesYesYes
ECDSA signingYesYesYesYes
ML-DSA (FIPS 204)NoFirmware-dependentYes (software)No
AES-WRAP key wrappingYesYesYesYes
RSA-OAEP key wrappingYesYesYesYes
Concurrent sessionsUp to 256Up to 128UnlimitedUp to 64
FIPS 140-3 levelLevel 3Level 3 (CP5)N/A (software)Level 3
Key non-exportabilityCKA_EXTRACTABLE=falseCKA_EXTRACTABLE=falseConfigurableCKA_EXTRACTABLE=false
Remote/network HSMYes (nShield Connect)Yes (LAN)N/AYes (Luna Network HSM)

Signing Mechanisms

kipuka uses the following PKCS#11 mechanisms for certificate signing, selected automatically based on the CA key type:

Key TypePKCS#11 Mechanismkipuka Function
RSA (PKCS#1 v1.5)CKM_RSA_PKCSsign_rsa_pkcs1
RSA (PSS)CKM_RSA_PKCS_PSSsign_rsa_pss
ECDSA (any curve)CKM_ECDSAsign_ecdsa
ML-DSACKM_IBM_DILITHIUM (vendor-specific)sign_ml_dsa

The hash algorithm for RSA mechanisms is configurable per CA via the RsaHashAlgorithm enum: SHA-256 (default), SHA-384, or SHA-512.

Key Wrapping for /serverkeygen

When /serverkeygen generates a key pair on the server, the private key must be encrypted for transport to the client. kipuka supports the following wrapping methods:

Wrapping MethodPKCS#11 MechanismClient Key TypeNotes
AES-256-WRAPCKM_AES_KEY_WRAPAny (symmetric wrapping)Content encryption key wrapped; CEK encrypts the private key via AES-CBC or AES-GCM
RSA-OAEP (SHA-256)CKM_RSA_PKCS_OAEPRSACEK wrapped directly to client’s RSA public key
ECDH-ES + AES-WRAPCKM_ECDH1_DERIVE + CKM_AES_KEY_WRAPECDSA/ECDHEphemeral ECDH key agreement derives a wrapping key

Not all HSMs support all wrapping combinations. The following table shows which wrapping methods are available per vendor:

Wrapping MethodEntrust nShieldUtimaco CryptoServerKryopticThales Luna 7
AES-256-WRAPYesYesYesYes
RSA-OAEP (SHA-256)YesYesYesYes
RSA-OAEP (SHA-384)YesNoYesYes
ECDH-ES + AES-WRAPYesYesYesYes

Per-Vendor Configuration

Entrust nShield

The nShield HSM family includes the Solo XC (PCIe), Connect XC (network), and Edge (compact form factor) models.

Library path:

[hsm]
library = "/opt/nfast/toolkits/pkcs11/libcknfast.so"

Environment variables:

# Security World configuration
export NFAST_HOME="/opt/nfast"
export NFAST_KMDATA="/opt/nfast/kmdata/local"

Key generation (via nShield tools):

# Generate an RSA 4096 CA key in the Security World
generatekey pkcs11 \
  --key-type=RSA \
  --size=4096 \
  --token-label="kipuka-ca" \
  --key-label="ca-signing-key" \
  --protect=module

# Generate an ECDSA P-384 CA key
generatekey pkcs11 \
  --key-type=EC \
  --curve=P-384 \
  --token-label="kipuka-ca" \
  --key-label="ca-ec-signing-key" \
  --protect=module

Configuration example:

[hsm]
library = "/opt/nfast/toolkits/pkcs11/libcknfast.so"
token_label = "kipuka-ca"
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "nshield-ca"
name = "nShield-backed CA"
cert = "/etc/kipuka/ca/nshield-ca.pem"
key = "pkcs11:token=kipuka-ca;object=ca-signing-key"
hsm_slot = 0

Known limitations:

  • nShield requires the Security World to be initialized and the module to be in operational state before kipuka starts.
  • Concurrent session limits depend on the module model and license. The Solo XC supports up to 256 concurrent PKCS#11 sessions.
  • ML-DSA is not supported as of nShield firmware 13.x. Use Synta software fallback for post-quantum signing with nShield.

Utimaco CryptoServer

Utimaco CryptoServer models include the LAN appliance, PCIe card, and Se-Series (cloud HSM).

Library path:

[hsm]
library = "/usr/lib/utimaco/libcs_pkcs11_R3.so"

Configuration file:

Utimaco requires a configuration file referenced by the CS_PKCS11_R3_CFG environment variable:

export CS_PKCS11_R3_CFG="/etc/utimaco/cs_pkcs11_R3.cfg"

Example cs_pkcs11_R3.cfg:

[Global]
Logging = 0
Logpath = /var/log/utimaco
SlotCount = 10

[CryptoServer]
Device = TCP:192.168.1.100:3001
Timeout = 30000

Key generation (via Utimaco tools):

# Generate RSA 4096 CA key
p11tool2 --module=/usr/lib/utimaco/libcs_pkcs11_R3.so \
  --login --so-pin=$SO_PIN \
  --generate-rsa --bits=4096 \
  --label="ca-signing-key" \
  --id=01

# Generate ECDSA P-384 CA key
p11tool2 --module=/usr/lib/utimaco/libcs_pkcs11_R3.so \
  --login --so-pin=$SO_PIN \
  --generate-ec --curve=P-384 \
  --label="ca-ec-signing-key" \
  --id=02

Configuration example:

[hsm]
library = "/usr/lib/utimaco/libcs_pkcs11_R3.so"
slot = 0
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "utimaco-ca"
name = "Utimaco-backed CA"
cert = "/etc/kipuka/ca/utimaco-ca.pem"
key = "pkcs11:slot=0;object=ca-signing-key"
hsm_slot = 0

Known limitations:

  • RSA-OAEP with SHA-384 is not supported; use SHA-256 for /serverkeygen key wrapping.
  • Maximum 128 concurrent PKCS#11 sessions. Size the kipuka connection pool accordingly (db.max_connections should not exceed this when HSM-bound operations dominate).
  • ML-DSA support depends on firmware version. Check with Utimaco for availability on your CryptoServer model.

Kryoptic

Kryoptic is a software PKCS#11 implementation used for development, testing, and non-FIPS deployments. It implements the standard PKCS#11 interface without requiring physical hardware.

Library path:

[hsm]
library = "/usr/lib/kryoptic/libkryoptic_pkcs11.so"

On macOS:

[hsm]
library = "/usr/local/lib/libkryoptic_pkcs11.dylib"

Token initialization:

# Initialize a Kryoptic token
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --init-token --label="kipuka-dev" \
  --so-pin=12345678

# Set the user PIN
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --init-pin --token-label="kipuka-dev" \
  --so-pin=12345678 --new-pin=userpin

Key generation:

# Generate RSA 2048 key for testing
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --login --pin=userpin \
  --token-label="kipuka-dev" \
  --keypairgen --key-type=RSA:2048 \
  --label="test-ca-key" --id=01

# Generate ECDSA P-256 key for testing
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --login --pin=userpin \
  --token-label="kipuka-dev" \
  --keypairgen --key-type=EC:prime256v1 \
  --label="test-ec-key" --id=02

Configuration example:

[hsm]
library = "/usr/lib/kryoptic/libkryoptic_pkcs11.so"
token_label = "kipuka-dev"
pin = "userpin"  # Acceptable for development only

[[ca]]
id = "dev-ca"
name = "Development CA"
cert = "/etc/kipuka/ca/dev-ca.pem"
key = "pkcs11:token=kipuka-dev;object=test-ca-key"

Known limitations:

  • Kryoptic is a software implementation and does not hold a FIPS 140-3 validation. Do not use it for production deployments that require hardware-backed key protection.
  • ML-DSA signing uses the Synta software fallback through SoftwarePqcFallback, not native PKCS#11 mechanisms.
  • Token state is stored on the filesystem. Protect the Kryoptic data directory with appropriate file permissions.
  • Useful for CI/CD pipelines and integration testing where a real HSM is not available.

Thales Luna 7 (Network HSM and PCIe)

The Thales Luna 7 family includes the Luna Network HSM (SA 7000) and Luna PCIe HSM.

Library path:

[hsm]
library = "/usr/safenet/lunaclient/lib/libCryptoki2_64.so"

On macOS (Luna client):

[hsm]
library = "/usr/local/lib/libCryptoki2.dylib"

Client configuration:

Luna requires a client certificate registered with the HSM partition. Configuration is managed through the Luna client tools and the Chrystoki.conf file:

Chrystoki2 = {
  LibUNIX64 = /usr/safenet/lunaclient/lib/libCryptoki2_64.so;
}

LunaSA Client = {
  ServerCAFile = /etc/Chrystoki2/certs/server.pem;
  ClientCertFile = /etc/Chrystoki2/certs/client.pem;
  ClientPrivKeyFile = /etc/Chrystoki2/certs/client-key.pem;
  ServerName00 = luna-hsm.example.com;
  ServerPort00 = 1792;
}

Key generation (via Luna tools):

# Generate RSA 4096 CA key in partition
cmu generatekeypair \
  -modulusLen=4096 \
  -publicExponent=65537 \
  -label="ca-signing-key" \
  -sign=True \
  -verify=True \
  -extractable=False \
  -token=True

# Generate ECDSA P-384 CA key
cmu generatekeypair \
  -keyType=EC \
  -curvetype=3 \
  -label="ca-ec-signing-key" \
  -sign=True \
  -verify=True \
  -extractable=False \
  -token=True

Configuration example:

[hsm]
library = "/usr/safenet/lunaclient/lib/libCryptoki2_64.so"
slot = 1
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "luna-ca"
name = "Luna 7-backed CA"
cert = "/etc/kipuka/ca/luna-ca.pem"
key = "pkcs11:slot=1;object=ca-signing-key"
hsm_slot = 1

Known limitations:

  • Maximum 64 concurrent PKCS#11 sessions per partition. For high-throughput deployments, consider using HA groups across multiple partitions or HSMs.
  • ML-DSA is not supported as of Luna firmware 7.x. Use Synta software fallback for post-quantum signing.
  • Luna Network HSM requires a network partition and registered client certificate. The client certificate must be registered before kipuka can connect to the HSM.
  • Session timeout: Luna partitions have a configurable idle session timeout. Set Idle Sessions Timeout to 0 (disabled) or to a value longer than kipuka’s HA health check interval to prevent session churn.

HSM Session Management

kipuka maintains a pool of PKCS#11 sessions to avoid the overhead of opening and closing sessions for every signing operation. The pool is managed by the Pkcs11Context struct in kipuka_hsm::pkcs11.

ParameterDescriptionRecommendation
Session pool sizeControlled by db.max_connections (shared pool)Set to the lesser of the HSM’s max concurrent sessions and the expected peak signing rate
Login stateSessions are logged in once at pool creationThe HSM PIN is used at startup; it is not stored in memory after login
Error recoveryFailed sessions are dropped and replacedkipuka logs a CaHealthChange audit event when session errors exceed the HA threshold

Adding a New HSM

kipuka’s HSM support is modular. The kipuka_hsm::providers module contains per-vendor configuration through the HsmProvider enum:

  • Entrust – Entrust nShield family
  • Utimaco – Utimaco CryptoServer family
  • Kryoptic – Kryoptic software PKCS#11
  • ThalesCsp – Thales CipherTrust / SafeNet (legacy)
  • ThalesTct – Thales Luna 7 and later

Each provider module exports:

  • default_library_path() – Platform-specific default library location
  • provider_config() – Vendor-specific PKCS#11 initialization settings
  • supported_mechanisms() – List of PKCS#11 mechanisms the provider is known to support

To add support for a new HSM vendor, implement these three functions in a new module under kipuka_hsm::providers and add a variant to the HsmProvider enum. The PKCS#11 standard interface means most HSMs will work without vendor-specific code as long as the library path is configured correctly; the provider module primarily documents known quirks and default paths.

Post-Quantum Cryptography Readiness

kipuka supports post-quantum cryptographic algorithms to prepare for the quantum computing threat to current public-key cryptography. This page covers ML-DSA digital signatures, ML-KEM key encapsulation, composite hybrid algorithms, and the CNSA Suite 2.0 transition timeline.

Overview

Quantum threat: Shor’s algorithm (running on a cryptographically relevant quantum computer, CRQC) can break RSA, ECDSA, and Diffie-Hellman in polynomial time. Current estimates suggest CRQCs may emerge in the 2030s, but data encrypted today using classical algorithms can be harvested and decrypted later (“store now, decrypt later” attacks).

NIST PQC standards (August 2024):

  • FIPS 204: Module-Lattice-Based Digital Signature Standard (ML-DSA, formerly CRYSTALS-Dilithium)
  • FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM, formerly CRYSTALS-Kyber)
  • FIPS 205: Stateless Hash-Based Digital Signature Standard (SLH-DSA, formerly SPHINCS+, not yet supported in kipuka)

kipuka implements ML-DSA for certificate signing and ML-KEM for key establishment, with composite hybrid algorithms that pair PQC with classical algorithms (RSA/ECDSA) for defense-in-depth.

ML-DSA (Digital Signatures)

ML-DSA (FIPS 204) is a lattice-based signature scheme offering three security levels aligned with AES key strengths:

AlgorithmSecurity LevelPublic Key SizeSignature SizeClassical Equivalent
ML-DSA-44Level 2 (AES-128)1,312 bytes2,420 bytesRSA-2048, ECDSA P-256
ML-DSA-65Level 3 (AES-192)1,952 bytes3,309 bytesRSA-3072, ECDSA P-384
ML-DSA-87Level 5 (AES-256)2,592 bytes4,627 bytesRSA-4096, ECDSA P-521

Key observations:

  • Larger signatures: ML-DSA-87 signatures (~4.6 KB) are 8x larger than RSA-4096 (~512 bytes) and 36x larger than ECDSA P-384 (~128 bytes).
  • Faster verification: ML-DSA verification is 2-5x faster than RSA for equivalent security.
  • Deterministic: Unlike ECDSA, ML-DSA signatures are deterministic (no nonce selection vulnerability).

Certificate Size Impact

A typical X.509 certificate with ML-DSA-87:

  • TBSCertificate (To-Be-Signed): ~1.5 KB (subject, issuer, validity, extensions)
  • Public key (subjectPublicKeyInfo): 2,592 bytes
  • CA signature: 4,627 bytes
  • Total: ~8.7 KB per certificate

A three-certificate chain (root → intermediate → end-entity) totals ~26 KB, compared to ~6 KB for RSA-2048 or ~3 KB for ECDSA P-256.

Implications:

  • HTTP/TLS max record size (16 KB) may require fragmentation
  • CoAP block-wise transfer essential (see CoAP Transport)
  • Increased network bandwidth and storage

ML-KEM (Key Encapsulation)

ML-KEM (FIPS 203) is a lattice-based key encapsulation mechanism (KEM) for establishing shared secrets, used in TLS handshakes and S/MIME key exchange.

AlgorithmSecurity LevelPublic Key SizeCiphertext SizeClassical Equivalent
ML-KEM-512Level 1 (AES-128)800 bytes768 bytesRSA-2048, ECDH P-256
ML-KEM-768Level 3 (AES-192)1,184 bytes1,088 bytesRSA-3072, ECDH P-384
ML-KEM-1024Level 5 (AES-256)1,568 bytes1,568 bytesRSA-4096, ECDH P-521

Usage in kipuka:

  • EST serverkeygen: CA encrypts generated private key for client using ML-KEM
  • CMS EnvelopedData: Encrypt EST responses with ML-KEM key exchange
  • Future TLS 1.3 integration: Hybrid key exchange (X25519 + ML-KEM-768)

Performance:

  • Encapsulation: 10-50 µs (faster than RSA encryption)
  • Decapsulation: 10-50 µs (much faster than RSA decryption)

Composite Hybrid Algorithms

To mitigate the risk of a breakthrough attack on lattice cryptography, kipuka supports composite hybrid signatures that bind a PQC algorithm with a classical algorithm. Both signatures must verify for the certificate to be valid.

Rationale: If ML-DSA is broken, the classical signature (RSA/ECDSA) still provides security. If the classical algorithm is broken by quantum computers, ML-DSA provides quantum resistance. This “belt-and-suspenders” approach is recommended during the PQC transition period (2025-2035).

Composite Signature Algorithms

Based on draft-ietf-lamps-pq-composite-sigs-19 (IETF LAMPS working group, expected RFC in 2025):

Composite AlgorithmComponentsOID Sub-ArcSecurity Level
ML-DSA-44-RSA2048-PSS-SHA256ML-DSA-44 + RSA-2048-PSS37Level 2 (128-bit)
ML-DSA-44-RSA2048-PKCS15-SHA256ML-DSA-44 + RSA-2048-PKCS#1 v1.538Level 2 (legacy compat)
ML-DSA-44-Ed25519-SHA512ML-DSA-44 + Ed2551939Level 2 (hybrid EdDSA)
ML-DSA-44-ECDSA-P256-SHA256ML-DSA-44 + ECDSA P-25640Level 2 (NIST curve)
ML-DSA-65-RSA3072-PSS-SHA512ML-DSA-65 + RSA-3072-PSS41Level 3 (192-bit)
ML-DSA-65-RSA3072-PKCS15-SHA512ML-DSA-65 + RSA-3072-PKCS#1 v1.542Level 3 (legacy compat)
ML-DSA-65-ECDSA-P384-SHA512ML-DSA-65 + ECDSA P-38443Level 3 (NIST curve)
ML-DSA-65-ECDSA-brainpoolP256r1-SHA512ML-DSA-65 + ECDSA brainpoolP256r144Level 3 (Brainpool)
ML-DSA-65-Ed25519-SHA512ML-DSA-65 + Ed2551945Level 3 (hybrid EdDSA)
ML-DSA-87-ECDSA-P384-SHA512ML-DSA-87 + ECDSA P-38446Level 5 (256-bit)
ML-DSA-87-ECDSA-brainpoolP384r1-SHA512ML-DSA-87 + ECDSA brainpoolP384r147Level 5 (Brainpool)
ML-DSA-87-Ed448-SHA512ML-DSA-87 + Ed44848Level 5 (hybrid EdDSA)

OID structure: id-composite-sig.{sub-arc} where id-composite-sig = 2.16.840.1.114027.80.5.2 (IANA PEN 114027, Entrust).

Example OID:

  • ML-DSA-65-RSA3072-PSS-SHA512 → 2.16.840.1.114027.80.5.2.41

Signature Format

A composite signature contains two independent signatures concatenated:

CompositeSignatureValue ::= SEQUENCE {
  pqcSignature BIT STRING,      -- ML-DSA signature
  traditionalSignature BIT STRING  -- RSA/ECDSA/EdDSA signature
}

Verification:

  1. Extract pqcSignature and verify using ML-DSA public key
  2. Extract traditionalSignature and verify using RSA/ECDSA/EdDSA public key
  3. Both must verify for the certificate to be valid

Certificate size: Composite signatures roughly double the signature size:

  • ML-DSA-65-RSA3072-PSS: 3,309 (ML-DSA) + 384 (RSA-PSS) ≈ 3,693 bytes
  • ML-DSA-87-ECDSA-P384: 4,627 (ML-DSA) + 96 (ECDSA) ≈ 4,723 bytes

CNSA Suite 2.0 Transition Timeline

The Commercial National Security Algorithm (CNSA) Suite 2.0 (NSA, September 2022) provides guidance for U.S. government systems transitioning to post-quantum cryptography:

YearRequirementDetails
2025Prefer PQCPQC algorithms should be used when available; classical algorithms still permitted
2030Require PQC for signingAll certificates, firmware signatures, and document signing must use ML-DSA or composite hybrid
2033Require PQC for key establishmentAll TLS handshakes, key wrapping, and encryption must use ML-KEM or hybrid KEMs
2035Phase out classical-only systemsSystems using only RSA/ECDSA/ECDH must be decommissioned or upgraded

Recommendations for kipuka deployments:

  • 2025-2029: Use composite hybrid algorithms (ML-DSA + RSA/ECDSA) for compatibility
  • 2030-2032: Transition to ML-DSA-only for signing, hybrid KEM for key exchange
  • 2033+: Pure PQC (ML-DSA + ML-KEM) for all new certificates

Grace periods: Existing certificates issued before 2030 may remain valid until expiration (typical 1-3 year lifetime), but renewals must use PQC.

Configuration

Enable ML-DSA signing

[ca]
default_signature_algorithm = "ML-DSA-65"  # or ML-DSA-44, ML-DSA-87

Enable composite hybrid signing

[ca]
default_signature_algorithm = "ML-DSA-65-RSA3072-PSS-SHA512"
# or ML-DSA-44-ECDSA-P256-SHA256, ML-DSA-87-ECDSA-P384-SHA512, etc.

Per-profile configuration

[[ca.profiles]]
name = "TLS Server (PQC Ready)"
signature_algorithm = "ML-DSA-65-ECDSA-P384-SHA512"
key_usage = ["digitalSignature", "keyEncipherment"]
extended_key_usage = ["serverAuth"]

[[ca.profiles]]
name = "Code Signing (Pure PQC)"
signature_algorithm = "ML-DSA-87"
key_usage = ["digitalSignature"]
extended_key_usage = ["codeSigning"]

Algorithm constraints

[ca.algorithm_policy]
allowed_signature_algorithms = [
  "ML-DSA-44",
  "ML-DSA-65",
  "ML-DSA-87",
  "ML-DSA-65-RSA3072-PSS-SHA512",
  "ML-DSA-87-ECDSA-P384-SHA512",
]
minimum_security_level = 3  # Reject Level 1 (ML-DSA-44) for production CAs

OpenSSL 3.5 Requirement

Critical dependency: PQC support in software (non-HSM) mode requires OpenSSL 3.5 or later with the pqc provider enabled.

OpenSSL 3.5 features:

  • Native ML-DSA (Dilithium) and ML-KEM (Kyber) implementations
  • Composite signature support (via pqc-combined provider)
  • EVP_PKEY interfaces for PQC key generation, signing, verification

Check OpenSSL version:

openssl version
# OpenSSL 3.5.0-dev  24 Apr 2025 (Library: OpenSSL 3.5.0-dev  24 Apr 2025)

Enable PQC provider in openssl.cnf:

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
pqc = pqc_sect

[default_sect]
activate = 1

[pqc_sect]
activate = 1

Verify PQC support:

openssl list -signature-algorithms | grep -i dilithium
# ML-DSA-44 @ pqc
# ML-DSA-65 @ pqc
# ML-DSA-87 @ pqc

Alternative (Docker): Use the kipuka container image which includes OpenSSL 3.5 with PQC support:

podman run -it quay.io/heebus/kipuka:latest openssl version

HSM Support Roadmap

Current status (June 2026): PQC key generation and signing in HSMs is vendor-dependent. Most HSMs do not yet support FIPS 204 (ML-DSA) in firmware.

HSM VendorModelML-DSA SupportStatusETA
ThalesLuna Network HSM 7.4+ML-DSA-44, ML-DSA-65, ML-DSA-87Beta (firmware 7.10)Q3 2026 GA
EntrustnShield 5c+ML-DSA-65, ML-DSA-87RoadmapQ4 2026
UtimacoSecurityServer Se Gen2ML-DSA-44, ML-DSA-65ExperimentalQ1 2027
KryopticFOSS HSMML-DSA-44, ML-DSA-65, ML-DSA-87AlphaQ3 2026
AWS CloudHSM-NoneNot announcedUnknown
Azure Key VaultManaged HSMNoneNot announcedUnknown

Workaround: Use software-based PQC signing (OpenSSL 3.5) for CA operations until HSM support is available. Root CA key can remain in HSM with RSA/ECDSA, using composite hybrid algorithms (ML-DSA-65-RSA3072-PSS) where the RSA component is HSM-backed.

Migration path:

  1. 2025-2026: Root CA uses RSA-4096 (HSM), intermediate CA uses ML-DSA-65-RSA3072-PSS-SHA512 (composite, RSA in HSM)
  2. 2027: Intermediate CA migrated to ML-DSA-65-only (HSM firmware updated)
  3. 2028: Root CA re-keyed to ML-DSA-87 (HSM-backed)

CoAP Considerations

Post-quantum certificates significantly impact CoAP deployments due to UDP MTU constraints.

Challenge: A typical ML-DSA-87 certificate (~8.7 KB) requires:

  • 16 blocks at 512-byte block size
  • 68 blocks at 128-byte block size (constrained networks)

Recommendations:

  1. Use ML-DSA-44 (Level 2) for IoT devices: ~5 KB certificates fit in 10 blocks at 512 bytes
  2. Increase block size to 1024 bytes for higher-bandwidth networks (reduces to 9 blocks)
  3. Enable block-wise transfer with max_payload = 131072 (128 KB) to accommodate full chains
  4. Use composite algorithms sparingly (adds ~400 bytes per certificate for RSA component)
  5. Compress certificate chains (remove intermediate CA if client has it pre-installed)

Example CoAP config for PQC:

[coap]
enabled = true
block_size = 1024  # Larger blocks for PQC
max_payload = 131072  # 128 KB (3x typical chain size)
session_timeout_secs = 600  # Longer timeout for multi-block transfers

Performance impact:

  • 16 blocks × 200ms RTT = 3.2 seconds for certificate chain retrieval (vs. 0.4s for RSA-2048)
  • Mitigate with session resumption (cache CA certs client-side after first fetch)

Example Workflows

Workflow 1: Issue ML-DSA-65 certificate

# Generate ML-DSA-65 key pair (requires OpenSSL 3.5+)
openssl genpkey -algorithm ML-DSA-65 -out device-pqc.key

# Create CSR
openssl req -new -key device-pqc.key \
  -subj "/CN=pqc-device.example.com/O=Example Inc" \
  -out device-pqc.csr

# Submit to kipuka via EST
curl -X POST https://ca.example.com/.well-known/est/simpleenroll \
  --cert client-cert.pem --key client-key.pem \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device-pqc.csr \
  -o device-pqc-cert.p7

# Extract certificate
openssl pkcs7 -in device-pqc-cert.p7 -print_certs -out device-pqc-cert.pem

# Verify signature algorithm
openssl x509 -in device-pqc-cert.pem -text -noout | grep "Signature Algorithm"
# Signature Algorithm: ML-DSA-65

Workflow 2: Issue composite hybrid certificate

# Generate RSA-3072 key pair
openssl genrsa -out device-hybrid.key 3072

# Generate ML-DSA-65 key pair
openssl genpkey -algorithm ML-DSA-65 -out device-hybrid-pqc.key

# Combine keys into composite key (format TBD by OpenSSL provider)
# For now, CA generates composite signature from single CSR

# Create CSR (traditional key only; CA adds PQC component)
openssl req -new -key device-hybrid.key \
  -subj "/CN=hybrid-device.example.com/O=Example Inc" \
  -addext "signatureAlgorithm=ML-DSA-65-RSA3072-PSS-SHA512" \
  -out device-hybrid.csr

# Submit via EST
curl -X POST https://ca.example.com/.well-known/est/simpleenroll \
  --cert client-cert.pem --key client-key.pem \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device-hybrid.csr \
  -o device-hybrid-cert.p7

# Verify composite signature
openssl x509 -in device-hybrid-cert.pem -text -noout | grep "Signature Algorithm"
# Signature Algorithm: ML-DSA-65-RSA3072-PSS-SHA512

Testing PQC Support

# Verify kipuka supports PQC algorithms
curl https://ca.example.com/.well-known/est/csrattrs
# Response includes ML-DSA OIDs

# Generate test ML-DSA-44 certificate
openssl req -x509 -newkey ML-DSA-44 -keyout test.key -out test.crt -days 1

# Verify signature
openssl verify -CAfile test.crt test.crt
# test.crt: OK

# Check certificate size
ls -lh test.crt
# -rw-r--r-- 1 user user 5.2K Jun 25 12:34 test.crt

Migration Best Practices

  1. Start with composite hybrid (2025-2027): Use ML-DSA-65-RSA3072-PSS for broad compatibility
  2. Test PQC-only in staging (2026-2028): Deploy ML-DSA-65-only CAs in non-production
  3. Monitor certificate sizes (ongoing): Track storage and bandwidth impact of PQC certs
  4. Plan HSM upgrades (2026-2027): Budget for firmware updates or HSM replacement
  5. Train operations teams (2025-2026): PQC troubleshooting differs from classical crypto
  6. Update incident response (ongoing): Quantum attacks may manifest as unexplained signature failures

Security Considerations

  1. Algorithm agility: Ensure CA can switch algorithms quickly if ML-DSA is compromised
  2. Hybrid during transition: Use composite signatures until CRQC threat is imminent (2030+)
  3. Monitor NIST updates: FIPS 204/203 may receive errata or revision (subscribe to NIST announcements)
  4. Avoid ML-DSA-44 in production CAs: Level 2 security may be insufficient for long-lived root CAs
  5. Audit PQC implementations: Software bugs in lattice reduction libraries are discovered regularly
  6. Plan for post-quantum hash functions: SHA-256/SHA-512 are quantum-resistant for now, but monitor SHA-3 adoption

References

  • FIPS 204: Module-Lattice-Based Digital Signature Standard (ML-DSA)
  • FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM)
  • CNSA Suite 2.0 Cryptographic Algorithms (NSA, September 2022)
  • draft-ietf-lamps-pq-composite-sigs-19: Composite ML-DSA for use in X.509 Public Key Infrastructure
  • NIST PQC Standardization: https://csrc.nist.gov/projects/post-quantum-cryptography
  • OpenSSL PQC Provider: https://github.com/openssl/openssl/blob/master/doc/designs/pqc-provider.md

EST Endpoints

kipuka implements the six operations defined by RFC 7030 (Enrollment over Secure Transport) and the clarifications in RFC 8951. All EST operations are served under the /.well-known/est/ path on the EST listener (default port 9443).

Base URL and EST labels

The base URL for all operations is:

https://<host>:9443/.well-known/est/

When EST labels are configured, a label segment is inserted between /est/ and the operation name to route the request to a specific CA:

https://<host>:9443/.well-known/est/{label}/simpleenroll

For example, if two labels are configured – iot-fleet backed by a hardware-HSM CA and corp-devices backed by a Dogtag CA – clients target the appropriate CA by including the label in the URL:

/.well-known/est/iot-fleet/simpleenroll
/.well-known/est/corp-devices/simpleenroll

Requests to the unlabeled path (/.well-known/est/simpleenroll) use the default CA. See EST Labels for configuration details.

Content types

EST uses CMS (PKCS #7) encoding throughout. All request and response bodies are base64-encoded DER unless otherwise noted.

DirectionContent-TypeEncoding
CSR requestapplication/pkcs10base64-encoded DER PKCS #10
Certificate responseapplication/pkcs7-mime; smime-type=certs-onlybase64-encoded DER PKCS #7
CMC requestapplication/pkcs7-mime; smime-type=CMC-requestbase64-encoded DER CMS
CMC responseapplication/pkcs7-mime; smime-type=CMC-responsebase64-encoded DER CMS
Server keygen responsemultipart/mixedSee Server-Side Key Generation
CSR attributesapplication/csrattrsbase64-encoded DER

GET /cacerts

Retrieve the CA certificate chain. This operation requires no authentication and is typically the first call a client makes to bootstrap trust.

Authentication: None

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only. The body is a base64-encoded PKCS #7 certs-only message containing the CA certificate chain in order from the issuing CA to the root.

Example

# Fetch the CA chain (no auth required)
curl -o cacerts.p7b \
  https://est.example.com:9443/.well-known/est/cacerts

# Decode the base64 PKCS#7 and view the certificates
base64 -d cacerts.p7b | openssl pkcs7 -inform DER -print_certs -noout

# Save decoded certificates to a PEM file
base64 -d cacerts.p7b | openssl pkcs7 -inform DER -print_certs -out ca-chain.pem

With an EST label:

curl -o cacerts.p7b \
  https://est.example.com:9443/.well-known/est/iot-fleet/cacerts

POST /simpleenroll

Submit a PKCS #10 certificate signing request (CSR) for initial enrollment. The server validates the request, signs the certificate using the configured CA, and returns the signed certificate in a PKCS #7 response.

Authentication: OTP (HTTP Basic) or mTLS (client certificate).

For initial enrollment of a device that does not yet have a certificate, use OTP authentication. For enrollment of a device that already holds a valid certificate from a trusted CA, use mTLS.

Request body: base64-encoded DER PKCS #10 CSR, Content-Type application/pkcs10.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only.

Example with OTP authentication

# Generate a private key and CSR
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout device.key -out device.csr -nodes \
  -subj "/CN=device-001.example.com"

# Base64-encode the CSR (DER format)
openssl req -in device.csr -outform DER | base64 > device.csr.b64

# Enroll using OTP (entity-id:otp-value as HTTP Basic auth)
curl -X POST \
  --cacert ca-chain.pem \
  -u "device-001:a7f3b9c2d1e4" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  -o enrolled.p7b \
  https://est.example.com:9443/.well-known/est/simpleenroll

# Decode the response to get the signed certificate
base64 -d enrolled.p7b | openssl pkcs7 -inform DER -print_certs -out device.crt

Example with mTLS authentication

# Enroll using an existing client certificate for authentication
curl -X POST \
  --cacert ca-chain.pem \
  --cert existing-device.crt \
  --key existing-device.key \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  -o enrolled.p7b \
  https://est.example.com:9443/.well-known/est/simpleenroll

Example with an EST label

curl -X POST \
  --cacert ca-chain.pem \
  -u "device-001:a7f3b9c2d1e4" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  -o enrolled.p7b \
  https://est.example.com:9443/.well-known/est/corp-devices/simpleenroll

POST /simplereenroll

Renew or rekey an existing certificate. The client authenticates with its current certificate via mTLS and submits a new CSR. The server validates that the CSR subject matches the authenticated client certificate (or that the change is permitted by policy) and returns a fresh certificate.

Authentication: mTLS required. The client must present the certificate being renewed.

Request body: base64-encoded DER PKCS #10 CSR, Content-Type application/pkcs10.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only.

Example

# Generate a new CSR using the existing key (or a new key for rekeying)
openssl req -new -key device.key -out renew.csr -nodes \
  -subj "/CN=device-001.example.com"

# Base64-encode the CSR
openssl req -in renew.csr -outform DER | base64 > renew.csr.b64

# Re-enroll using the current device certificate for mTLS auth
curl -X POST \
  --cacert ca-chain.pem \
  --cert device.crt \
  --key device.key \
  -H "Content-Type: application/pkcs10" \
  --data-binary @renew.csr.b64 \
  -o renewed.p7b \
  https://est.example.com:9443/.well-known/est/simplereenroll

# Decode the renewed certificate
base64 -d renewed.p7b | openssl pkcs7 -inform DER -print_certs -out device-renewed.crt

POST /fullcmc

Full Certificate Management over CMS (RFC 5272). This operation accepts a full CMC request wrapped in a CMS SignedData structure and returns a full CMC response. Full CMC supports advanced features not available through simple enrollment: certificate revocation, key update with proof-of-possession, and batch operations.

Authentication: The CMC request itself carries authentication (the CMS SignedData is signed by the requester). mTLS may also be required depending on server policy.

Request body: base64-encoded DER CMS, Content-Type application/pkcs7-mime; smime-type=CMC-request.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=CMC-response.

Example

# Full CMC requests are typically constructed by a CMC-aware client library.
# This example uses a pre-built CMC request.
curl -X POST \
  --cacert ca-chain.pem \
  --cert ra-agent.crt \
  --key ra-agent.key \
  -H "Content-Type: application/pkcs7-mime; smime-type=CMC-request" \
  --data-binary @cmc-request.b64 \
  -o cmc-response.p7b \
  https://est.example.com:9443/.well-known/est/fullcmc

# Decode the CMC response
base64 -d cmc-response.p7b | openssl cms -inform DER -cmsout -print

Note: Full CMC support requires the fullcmc feature in the server configuration. When Dogtag PKI is the back-end CA, CMC requests are forwarded to the Dogtag CMC profile.


POST /serverkeygen

Request server-side key generation. The server generates a key pair, signs the certificate, and returns both the certificate and the encrypted private key. This operation is used in environments where the client device cannot generate strong keys locally, or where key escrow through a Key Recovery Authority (KRA) is required.

Authentication: OTP (HTTP Basic) or mTLS.

Request body: base64-encoded DER PKCS #10 CSR, Content-Type application/pkcs10. The CSR provides the subject and requested attributes; the public key in the CSR is replaced by the server-generated key.

Response: 200 OK with Content-Type multipart/mixed. The response contains two parts:

PartContent-TypeContents
1application/pkcs7-mime; smime-type=certs-onlySigned certificate (base64 PKCS #7)
2application/pkcs8Encrypted private key (base64 PKCS #8)

The private key is encrypted to the client using the asymmetric key from the CSR or a pre-shared symmetric key, depending on configuration.

Example

# Generate a throwaway CSR (the server will generate the real key pair)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout throwaway.key -out serverkeygen.csr -nodes \
  -subj "/CN=device-002.example.com"

# Base64-encode
openssl req -in serverkeygen.csr -outform DER | base64 > serverkeygen.csr.b64

# Request server-side key generation
curl -X POST \
  --cacert ca-chain.pem \
  -u "device-002:b8e4c1d5f2a7" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @serverkeygen.csr.b64 \
  -o serverkeygen-response.mime \
  https://est.example.com:9443/.well-known/est/serverkeygen

# The response is multipart/mixed -- extract the certificate and key
# (exact parsing depends on the MIME boundary in the response headers)

Note: Server-side key generation requires a KRA (Key Recovery Authority) when key escrow is mandated by policy. With Dogtag PKI, the KRA subsystem handles key archival automatically.


GET /csrattrs

Retrieve the set of CSR attributes that the server expects or recommends clients include in their certificate signing requests. The response is an ASN.1 sequence of OIDs and attribute definitions.

Authentication: None (or mTLS, depending on server policy).

Response: 200 OK with Content-Type application/csrattrs. The body is a base64-encoded DER CsrAttrs structure as defined in RFC 7030 Section 4.5.2.

Example

# Fetch supported CSR attributes
curl --cacert ca-chain.pem \
  -o csrattrs.b64 \
  https://est.example.com:9443/.well-known/est/csrattrs

# Decode and inspect (requires ASN.1 tools)
base64 -d csrattrs.b64 | openssl asn1parse -inform DER

Typical attributes returned include:

  • ecPublicKey with secp256r1 or secp384r1 – indicating preferred key algorithms
  • challengePassword – when the server requires a challenge in the CSR
  • Extension requests – specific X.509v3 extensions the server will honor

When [est.csr_template] is configured, the response also includes an RFC 9908 CertificationRequestInfoTemplate. See CSR Attributes Template below.


GET /star/

Fetch the latest certificate in a STAR (Short-Term Automatic Renewal) order. STAR orders are created via the /simpleenroll endpoint when the client includes STAR-specific attributes in the CSR. Once active, the server automatically renews the certificate at the configured interval.

Authentication: mTLS required. The client must present a certificate that is authorized to access the STAR order.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only. The body contains the most recently issued certificate in the STAR renewal series.

Example

# Fetch the latest STAR certificate
curl --cacert ca-chain.pem \
  --cert device.crt \
  --key device.key \
  -o star-cert.p7b \
  https://est.example.com:9443/.well-known/est/iot-fleet/star/order-abc123

# Decode the certificate
base64 -d star-cert.p7b | openssl pkcs7 -inform DER -print_certs -out star.crt

STAR Order Status

The order lifecycle follows these states:

StateDescription
pendingOrder created, awaiting first certificate issuance
activeCertificates are being automatically renewed
expiredOrder lifetime has elapsed; no further renewals
cancelledOrder cancelled by operator via Admin API

Note: STAR support requires [star] enabled = true in the server configuration. See STAR Certificates for configuration details.


CoAP Transport

In addition to the HTTPS transport described above, kipuka supports EST-over-CoAP (RFC 9483) for constrained IoT devices. When the [coap] section is enabled, the server listens on UDP port 5684 (DTLS) and accepts CoAP requests that map to the same EST operations.

Abbreviated URI Paths

RFC 9483 section 5.1 defines shortened CoAP URI paths to reduce message overhead on constrained links:

CoAP PathEST EndpointMethodDescription
/sen/simpleenrollPOSTSimple enrollment (CSR to certificate)
/sren/simplereenrollPOSTSimple re-enrollment (renewal)
/skg/serverkeygenPOSTServer-side key generation
/att/csrattrsGETCSR attributes hint
/crts/cacertsGETCA certificate chain

Example: To enroll a constrained device, POST a DER-encoded PKCS#10 CSR to coaps://est.example.com:5684/sen.

Content-Format IDs

CoAP uses numeric Content-Format IDs in lieu of MIME type strings. The following IDs are defined by RFC 9483 section 5.4:

IDMIME EquivalentUsage
280application/pkcs7-mime; smime-type=server-generatedServer-generated key response
281application/pkcs7-mime; smime-type=certs-onlyCertificate-only response (/cacerts, enrollment)
285application/pkcs10PKCS#10 CSR request body
286application/pkcs8PKCS#8 encrypted private key
287application/csrattrsCSR attributes response

Block-Wise Transfer

CoAP messages are limited by UDP MTU (typically 1280 bytes for IPv6). When a certificate chain or CSR exceeds the negotiated block size, kipuka automatically uses block-wise transfer (RFC 7959):

  • Block1 – Reassembles fragmented CSR uploads from the client
  • Block2 – Splits certificate responses into negotiated block sizes (16-1024 bytes)

This is especially important for post-quantum certificates (ML-DSA-87 signatures can exceed 7 KB), where a full certificate chain may require 20+ blocks at the default 512-byte block size.

The block size is configurable via [coap].block_size in kipuka.toml.

Differences from HTTPS Transport

AspectHTTPSCoAP/DTLS
TransportTCP + TLS (rustls)UDP + DTLS (OpenSSL)
EncodingBase64-encoded DERRaw DER (no base64)
Content-TypeMIME stringsNumeric Content-Format IDs
URI paths/.well-known/est/simpleenroll/sen
AuthenticationOTP, mTLS, GSSAPIPSK, DTLS client certificates
Large payloadsHTTP chunked transferCoAP block-wise transfer (RFC 7959)

See the CoAP Operator Guide for configuration and deployment details.


Error responses

All EST endpoints return standard HTTP error codes. The response body for errors is application/json on the admin API and plain text on the EST endpoints.

StatusMeaningWhen returned
400 Bad RequestMalformed CSR, invalid base64 encoding, missing Content-Type, or CSR fails policy validation.Any POST endpoint
401 UnauthorizedMissing or invalid authentication. For OTP: the OTP value is wrong, expired, or already consumed. For mTLS: no client certificate presented or the certificate is not trusted./simpleenroll, /simplereenroll, /serverkeygen
403 ForbiddenAuthentication succeeded but the client is not authorized for the requested operation. Common cause: re-enrollment with a certificate whose subject does not match the CSR./simplereenroll
404 Not FoundUnknown EST label.Any endpoint with an invalid label
500 Internal Server ErrorServer-side failure: HSM unreachable, database error, or CA signing failure.Any endpoint
503 Service UnavailableThe requested CA is temporarily unavailable (all replicas down, HSM session exhausted).Any endpoint

Retry-After

When an operation requires asynchronous processing (for example, a Dogtag PKI back-end that queues signing requests for RA approval), the server returns 202 Accepted with a Retry-After header indicating the number of seconds the client should wait before polling.

HTTP/1.1 202 Accepted
Retry-After: 30
Content-Length: 0

The client should repeat the same request (with the same CSR and authentication) after the indicated interval. The server tracks pending requests and returns the signed certificate when it becomes available, or another 202 if the request is still pending.

Example: handling a 401

# A failed OTP enrollment returns 401
curl -v -X POST \
  --cacert ca-chain.pem \
  -u "device-001:wrong-otp-value" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  https://est.example.com:9443/.well-known/est/simpleenroll

# Response:
# < HTTP/1.1 401 Unauthorized
# < WWW-Authenticate: Basic realm="est"

TLS requirements

The EST listener enforces TLS 1.2 or 1.3 for all connections. The server certificate must be configured in [tls] in the kipuka configuration file.

For mTLS-authenticated endpoints, the server’s TLS configuration must include a client_ca trust anchor that covers the certificates clients present. See TLS Configuration for details.

# Verify server TLS configuration
openssl s_client -connect est.example.com:9443 -showcerts </dev/null

GET /renewal-info/:cert_id

Draft: draft-ietf-lamps-est-renewal-info

Returns a JSON object with a suggested renewal window for the identified certificate. No authentication required.

Certificate ID construction

cert_id = base64url(AKI.keyIdentifier) + "." + base64url(Serial)
  • AKI keyIdentifier: From the certificate’s Authority Key Identifier extension, base64url-encoded (no padding)
  • Serial: The certificate serial number DER value bytes, base64url-encoded (no padding)

Response

{
  "suggestedWindow": {
    "start": "2026-07-20T00:00:00Z",
    "end": "2026-07-24T00:00:00Z"
  }
}
HeaderValue
Content-Typeapplication/json
Retry-After86400 (configurable)

Configuration

[est]
renewal_window_days = 30          # Days before expiry to start window
renewal_retry_after_secs = 86400  # Retry-After header value

Example

# Build cert_id: base64url(AKI) + "." + base64url(Serial)
AKI=$(openssl x509 -in client.pem -noout -text \
  | grep -A1 'Authority Key Identifier' | tail -1 \
  | tr -d ': ' | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=')
SER=$(openssl x509 -in client.pem -noout -serial \
  | cut -d= -f2 | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=')

curl --cacert ca-chain.pem \
  https://est.example.com:9443/.well-known/est/renewal-info/${AKI}.${SER}

Error responses

StatusReason
400Malformed cert_id (bad base64url, missing dot separator)
404Certificate not found or AKI mismatch

CSR Attributes Template (RFC 9908)

When [est.csr_template] is configured, the /csrattrs response includes a CertificationRequestInfoTemplate attribute alongside the standard OID list.

This is backward compatible — clients that don’t understand the template ignore it and process only the OIDs.

Example

# Fetch csrattrs with template and inspect the ASN.1 structure
curl --cacert ca-chain.pem \
  https://est.example.com:9443/.well-known/est/csrattrs \
  | base64 -d | openssl asn1parse -inform DER

Configuration

[est.csr_template]
key_algorithm = "ec:P-256"
required_extensions = ["2.5.29.17"]  # subjectAltName

[[est.csr_template.subject]]
oid = "2.5.4.10"
value = "Example Corp"    # pre-filled by server

[[est.csr_template.subject]]
oid = "2.5.4.3"           # client must provide CN

Admin API Reference

The admin API provides management operations for kipuka: health checks, CA status, OTP lifecycle, and issued certificate queries. It runs on a separate TLS listener (default port 9444) from the EST enrollment endpoints, allowing operators to restrict admin access at the network level independently of client enrollment traffic.

Admin listener configuration

The admin API binds to its own address and port, configured in the [admin] section of the kipuka configuration file:

[admin]
listen = "0.0.0.0:9444"

[admin.tls]
cert = "/etc/kipuka/admin-server.crt"
key = "/etc/kipuka/admin-server.key"
client_ca = "/etc/kipuka/admin-ca.pem"

The admin TLS certificate and trust anchors are independent of the EST listener. This separation allows operators to:

  • Issue admin certificates from a different CA than enrollment certificates.
  • Restrict admin access to a management VLAN by binding to a specific interface.
  • Apply different TLS policies (cipher suites, minimum version) for admin traffic.

Authentication

The admin API supports two authentication methods:

MethodHeaderDescription
mTLS(client certificate)The client presents a certificate trusted by the admin client_ca. Preferred for automated integrations.
Bearer tokenAuthorization: Bearer <token>A static token configured in the [admin] section. Suitable for quick manual access and scripts.
[admin]
bearer_token = "${KIPUKA_ADMIN_TOKEN}"

All examples below use Bearer token authentication. Substitute --cert / --key flags for mTLS.


Endpoints

GET /admin/health

System health check. Returns server uptime, version, and the health status of every configured CA.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/health | jq .
{
  "status": "healthy",
  "version": "0.4.0",
  "uptime_seconds": 86412,
  "cas": [
    {
      "id": "default",
      "label": null,
      "status": "healthy",
      "last_sign_time": "2026-06-24T14:22:01Z"
    },
    {
      "id": "iot-fleet",
      "label": "iot-fleet",
      "status": "healthy",
      "last_sign_time": "2026-06-24T14:18:33Z"
    },
    {
      "id": "corp-devices",
      "label": "corp-devices",
      "status": "degraded",
      "last_sign_time": "2026-06-24T13:55:10Z",
      "error": "HSM session pool exhausted, 1 of 2 replicas available"
    }
  ],
  "database": {
    "status": "healthy",
    "backend": "postgresql",
    "pool_size": 10,
    "active_connections": 3
  }
}

The top-level status field aggregates CA and database health:

ValueMeaning
healthyAll CAs and the database are operational.
degradedAt least one CA is degraded (partial replica loss) but enrollment is still possible.
unhealthyA critical CA or the database is down. Enrollment requests will fail.

GET /admin/ca

List all configured CAs with their current health status.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/ca | jq .
[
  {
    "id": "default",
    "label": null,
    "type": "local",
    "status": "healthy",
    "last_sign_time": "2026-06-24T14:22:01Z"
  },
  {
    "id": "iot-fleet",
    "label": "iot-fleet",
    "type": "hsm",
    "status": "healthy",
    "last_sign_time": "2026-06-24T14:18:33Z"
  },
  {
    "id": "corp-devices",
    "label": "corp-devices",
    "type": "dogtag",
    "status": "degraded",
    "last_sign_time": "2026-06-24T13:55:10Z"
  }
]

GET /admin/ca/:id

Retrieve detailed information about a specific CA, including its type, EST label binding, certificate chain, and operational status.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/ca/iot-fleet | jq .
{
  "id": "iot-fleet",
  "label": "iot-fleet",
  "type": "hsm",
  "status": "healthy",
  "hsm": {
    "module": "/usr/lib/libCryptoki2_64.so",
    "slot": 1,
    "key_label": "iot-signing-key"
  },
  "certificate_chain": [
    {
      "subject": "CN=IoT Fleet Issuing CA, O=Example Corp",
      "issuer": "CN=Example Root CA, O=Example Corp",
      "serial": "0A:1B:2C:3D:4E:5F",
      "not_before": "2025-01-15T00:00:00Z",
      "not_after": "2030-01-15T00:00:00Z",
      "key_algorithm": "ECDSA P-384"
    },
    {
      "subject": "CN=Example Root CA, O=Example Corp",
      "issuer": "CN=Example Root CA, O=Example Corp",
      "serial": "01",
      "not_before": "2020-01-01T00:00:00Z",
      "not_after": "2040-01-01T00:00:00Z",
      "key_algorithm": "RSA 4096"
    }
  ],
  "last_sign_time": "2026-06-24T14:18:33Z",
  "total_certs_issued": 14832
}

Error: 404 Not Found if the CA id does not exist.


POST /admin/otp

Generate a one-time password for initial device enrollment. The OTP is bound to an entity identifier (typically a device hostname or serial number) and is valid for a limited time and number of uses.

Request body: application/json

FieldTypeRequiredDescription
entity_idstringyesIdentifier for the device or entity. Must match the HTTP Basic username during enrollment.
ttlintegernoTime-to-live in seconds. Default: 3600 (1 hour).
max_usesintegernoMaximum number of times the OTP can be used. Default: 1.

Response: 201 Created

curl -s -X POST \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  --cacert admin-ca.pem \
  -d '{
    "entity_id": "device-001.example.com",
    "ttl": 7200,
    "max_uses": 1
  }' \
  https://admin.example.com:9444/admin/otp | jq .
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "entity_id": "device-001.example.com",
  "otp": "a7f3b9c2d1e4",
  "created": "2026-06-24T15:00:00Z",
  "expires": "2026-06-24T17:00:00Z",
  "max_uses": 1,
  "remaining_uses": 1
}

The otp value is returned only in this response and is never stored in plaintext on the server (the server stores a salted hash). Record it immediately.


GET /admin/otp

List all active (non-expired, non-exhausted) OTPs. The OTP secret value is not included – only metadata.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/otp | jq .
[
  {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "entity_id": "device-001.example.com",
    "created": "2026-06-24T15:00:00Z",
    "expires": "2026-06-24T17:00:00Z",
    "max_uses": 1,
    "remaining_uses": 1
  },
  {
    "id": "e29b1d4a-7c83-4f91-b234-1a23c4d5e678",
    "entity_id": "device-002.example.com",
    "created": "2026-06-24T14:30:00Z",
    "expires": "2026-06-24T15:30:00Z",
    "max_uses": 3,
    "remaining_uses": 2
  }
]

DELETE /admin/otp/:id

Revoke an active OTP before it expires or is fully consumed. Revoked OTPs are immediately invalid for enrollment.

Response: 204 No Content

curl -s -X DELETE \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/otp/f47ac10b-58cc-4372-a567-0e02b2c3d479

Error: 404 Not Found if the OTP id does not exist or has already expired.


GET /admin/certs

List certificates issued by kipuka. Results are paginated and can be filtered by subject, issuer, status, or date range.

Query parameters:

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed).
per_pageinteger50Results per page (max 500).
subjectstringFilter by subject CN (substring match).
statusstringFilter by status: active, expired, revoked.
issued_afterstringISO 8601 datetime. Only certificates issued after this time.
issued_beforestringISO 8601 datetime. Only certificates issued before this time.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  "https://admin.example.com:9444/admin/certs?status=active&per_page=5" | jq .
{
  "page": 1,
  "per_page": 5,
  "total": 14832,
  "certificates": [
    {
      "serial": "0A:1B:2C:3D:4E:5F:60:71",
      "subject": "CN=device-001.example.com",
      "issuer": "CN=IoT Fleet Issuing CA, O=Example Corp",
      "not_before": "2026-06-24T14:22:01Z",
      "not_after": "2027-06-24T14:22:01Z",
      "status": "active",
      "ca_id": "iot-fleet",
      "enrollment_type": "simpleenroll"
    },
    {
      "serial": "0A:1B:2C:3D:4E:5F:60:72",
      "subject": "CN=device-002.example.com",
      "issuer": "CN=IoT Fleet Issuing CA, O=Example Corp",
      "not_before": "2026-06-24T14:18:33Z",
      "not_after": "2027-06-24T14:18:33Z",
      "status": "active",
      "ca_id": "iot-fleet",
      "enrollment_type": "serverkeygen"
    }
  ]
}

Error responses

Admin API errors return JSON with a consistent structure:

{
  "error": "not_found",
  "message": "CA with id 'nonexistent' does not exist"
}
StatusMeaning
400 Bad RequestInvalid request body or query parameters.
401 UnauthorizedMissing or invalid Bearer token; untrusted client certificate.
404 Not FoundResource (CA, OTP, certificate) not found.
500 Internal Server ErrorServer-side failure.

Rust API Reference

kipuka’s Rust API documentation is auto-generated from source code doc comments using cargo doc and published alongside this book. The generated docs are the authoritative reference for types, traits, function signatures, and module structure.

Online API docs: kipuka.dev/api/kipuka/

Workspace crates

The kipuka workspace is organized into six crates, each with a focused responsibility:

CratePathAPI docsDescription
kipuka-estcrates/kipuka-estkipuka_estEST protocol implementation. Axum route handlers for all six RFC 7030 operations, TLS listener setup with rustls, mTLS client authentication, CSR validation, and certificate response encoding.
kipuka-hsmcrates/kipuka-hsmkipuka_hsmPKCS #11 HSM integration via the cryptoki crate. Manages HSM sessions, slot enumeration, key lookup by label, signing operations (RSA-PSS, ECDSA), and session pool lifecycle.
kipuka-otpcrates/kipuka-otpkipuka_otpOTP lifecycle management. Generation of cryptographically random OTP values, salted hash storage, validation against entity ID binding, use-count tracking, and expiry enforcement.
kipuka-utilcrates/kipuka-utilkipuka_utilShared types and utilities. Configuration file parsing (TOML), ASN.1 helpers built on synta, error type hierarchy, database connection pooling via sqlx, and audit log formatting.
kipuka-dogtagcrates/kipuka-dogtagkipuka_dogtagDogtag PKI REST client. Submits certificate signing requests to a Dogtag CA subsystem, retrieves signed certificates, and interacts with the KRA subsystem for server-side key generation and escrow.
kipuka-coapcrates/kipuka-coapkipuka_coapCoAP transport layer (RFC 7252). Provides EST-over-CoAP endpoints for constrained IoT devices that cannot use HTTP/TLS, with DTLS for transport security.

Building the docs locally

To generate and open the API documentation from a local checkout:

# Clone the repository
git clone https://codeberg.org/czinda/kipuka.git
cd kipuka

# Build docs for all workspace crates (skip dependency docs for speed)
cargo doc --no-deps --open

This builds HTML documentation into target/doc/ and opens it in your default browser. The landing page lists all six crates with links to their module trees.

To build docs for a single crate:

cargo doc --no-deps -p kipuka-est --open

Including private items

By default, cargo doc only documents public API surface. To include private functions, types, and modules (useful during development):

cargo doc --no-deps --document-private-items --open

Prerequisites

Building the docs requires:

  • Rust 1.88+ (edition 2021)
  • A working C toolchain (required by cryptoki build script for PKCS #11 header compilation)
  • SQLx offline mode or a running database for query checking – see Development Setup for details

Documentation conventions

The codebase follows these doc comment conventions:

  • Every public type, trait, function, and module has a /// doc comment.
  • Examples in doc comments are runnable via cargo test --doc where practical.
  • Cross-references use intra-doc links ([OtpStore], [CaConfig]) for navigable HTML output.
  • Safety invariants on unsafe blocks are documented with # Safety sections.
  • Error conditions are documented with # Errors sections listing the specific error variants returned.

CMP Endpoints

kipuka implements the Certificate Management Protocol (CMP, RFC 4210 / RFC 9810) for automated certificate lifecycle management. CMP provides a standardized framework for certificate requests, renewals, revocations, and CA interactions with cryptographic protection.

Endpoint

CMP operations are served at the IANA-assigned well-known URI:

POST /.well-known/cmp

This single endpoint handles all CMP message types, with the operation determined by the PKIMessage body type.

Request Format

HTTP Headers

POST /.well-known/cmp HTTP/1.1
Host: ca.example.com
Content-Type: application/pkixcmp
Content-Length: 1234

[Binary PKIMessage]

Content Type

  • Request: application/pkixcmp
  • Response: application/pkixcmp

CMP messages are ASN.1 DER-encoded structures defined in RFC 4210 section 5.1.

Response Format

HTTP/1.1 200 OK
Content-Type: application/pkixcmp
Content-Length: 5678

[Binary PKIMessage response]

Note: CMP always returns HTTP 200 OK. Error conditions are encoded in the PKIStatus field of the PKIMessage body, not HTTP status codes.

Message Types

CMP supports the following request/response pairs:

Request TypeCodeDescriptionResponse TypeCode
ir (Initialization Request)0Initial certificate request from new entityip (Initialization Response)1
cr (Certification Request)2Certificate request from existing entitycp (Certification Response)3
kur (Key Update Request)7Certificate renewal with new key pairkup (Key Update Response)8
rr (Revocation Request)11Certificate revocation requestrp (Revocation Response)12
error23Error message (response only)N/AN/A

Initialization Request (ir → ip)

Used for first-time enrollment when the entity has no existing certificate from the CA.

Request fields:

  • certReqId: Integer identifier for this request
  • certTemplate: Certificate template with subject DN, public key, extensions
  • popo: Proof of possession (signature, key agreement, or key encipherment)

Response fields:

  • certReqId: Matches request ID
  • status: PKIStatusInfo (granted/rejected/waiting)
  • certifiedKeyPair: Issued certificate + optional encrypted private key
  • rspInfo: Additional response information (OCSPResponse, timestampToken)

Certification Request (cr → cp)

Used for subsequent certificate requests when the entity already has a certificate from the CA.

Differences from ir:

  • Request must be signature-protected using existing certificate
  • May reference existing certificate in oldCertId
  • CA may skip out-of-band verification (already authenticated)

Key Update Request (kur → kup)

Used for certificate renewal with a new key pair. Similar to cr but explicitly signals key rollover.

Request fields:

  • certTemplate: New subject DN (if changing), new public key, validity period
  • oldCertId: Reference to certificate being renewed

Response:

  • New certificate with updated serialNumber and subjectPublicKeyInfo
  • Original subject DN preserved (unless change requested)

Revocation Request (rr → rp)

Revokes an issued certificate.

Request fields:

  • certDetails: Certificate serial number + issuer DN to revoke
  • crlEntryDetails: Optional revocation reason (keyCompromise, affiliationChanged, cessationOfOperation, etc.)

Response:

  • status: Revocation accepted/rejected
  • revCerts: List of revoked certificate identifiers

Protection Methods

CMP messages must be cryptographically protected to prevent tampering and verify sender identity. kipuka supports two protection mechanisms:

1. MAC-Based Protection (Shared Secret)

Uses HMAC-SHA256 with a pre-shared secret (password or OTP).

Protection fields:

protectionAlg: id-PasswordBasedMac (1.2.840.113533.7.66.13)
  salt: OCTET STRING (16 bytes, random)
  iterationCount: INTEGER (10000)
  mac: AlgorithmIdentifier (hmacWithSHA256)
protection: BIT STRING (HMAC-SHA256 digest)

Use case: Initial enrollment when client has no certificate yet.

Security: Shared secret must be delivered out-of-band (OTP, provisioning portal, physical token).

2. Signature-Based Protection (Certificate)

Uses the entity’s existing certificate to sign the message.

Protection fields:

protectionAlg: sha256WithRSAEncryption (or ECDSA/Ed25519)
senderKID: KeyIdentifier (SKI of signing cert)
protection: BIT STRING (Digital signature)
extraCerts: [1] SEQUENCE OF Certificate (signing cert + chain)

Use case: Re-enrollment, key update, revocation (client already has valid certificate).

Verification: kipuka validates signature against extraCerts chain and checks revocation status (CRL/OCSP).

Status Codes

The PKIStatusInfo structure in responses contains:

Status CodeNameDescription
0acceptedRequest granted, certificate issued
1grantedWithModsGranted but some requested values modified (e.g., validity period)
2rejectionRequest rejected (see failInfo for reason)
3waitingRequest pending manual approval (CA policy)
4revocationWarningCertificate issued but old cert revocation failed
5revocationNotificationCertificate not issued due to revocation issue
6keyUpdateWarningKey update succeeded but with warnings

Failure Reasons (failInfo)

When status = rejection, the failInfo bit string indicates why:

BitNameMeaning
0badAlgUnacceptable algorithm (e.g., MD5, RSA-1024)
1badMessageCheckMessage integrity check failed
2badRequestMalformed request (invalid ASN.1)
5badDataFormatIncorrect data encoding
14badCertTemplateInvalid certificate template (e.g., subject DN constraints)
17notAuthorizedAuthentication failed or insufficient privileges

Error Handling

Client Errors (Invalid Requests)

kipuka returns HTTP 400 Bad Request for:

  • Invalid Content-Type (not application/pkixcmp)
  • Malformed ASN.1 DER encoding
  • Missing required PKIMessage fields

CMP-Level Errors

CMP protocol errors return HTTP 200 OK with an error message type (code 23):

PKIMessage {
  header: { sender, recipient, messageTime, transactionID }
  body: error [23] {
    pKIStatusInfo: {
      status: rejection (2)
      statusString: "MAC verification failed"
      failInfo: badMessageCheck (bit 1)
    }
    errorCode: INTEGER (optional)
    errorDetails: UTF8String (optional)
  }
}

Server Errors (CA Unavailable)

If the CA backend is unreachable or times out, kipuka returns:

HTTP/1.1 503 Service Unavailable
Retry-After: 60

CA temporarily unavailable

Examples

Example 1: Initial Enrollment (MAC-Protected)

Request:

POST /.well-known/cmp HTTP/1.1
Host: ca.example.com
Content-Type: application/pkixcmp
Content-Length: 832

[Binary PKIMessage with ir body, MAC protection]

Key fields:

  • body.ir.certReqMsg.certTemplate.subject: CN=device001.example.com
  • body.ir.certReqMsg.certTemplate.publicKey: RSA 2048-bit public key
  • protectionAlg: id-PasswordBasedMac with HMAC-SHA256
  • protection: HMAC computed over entire message

Response:

HTTP/1.1 200 OK
Content-Type: application/pkixcmp
Content-Length: 1456

[Binary PKIMessage with ip body]

Key fields:

  • body.ip.status.status: accepted (0)
  • body.ip.certifiedKeyPair.certOrEncCert: Issued X.509 certificate (DER)

Example 2: Renewal (Signature-Protected)

Request:

POST /.well-known/cmp HTTP/1.1
Host: ca.example.com
Content-Type: application/pkixcmp
Content-Length: 1124

[Binary PKIMessage with kur body, signature protection]

Key fields:

  • body.kur.certTemplate.publicKey: New RSA 3072-bit public key
  • body.kur.oldCertId: Serial number of certificate being renewed
  • protectionAlg: sha256WithRSAEncryption
  • protection: Digital signature using old certificate’s private key
  • extraCerts: [old_cert, issuing_ca_cert]

Response:

HTTP/1.1 200 OK
Content-Type: application/pkixcmp
Content-Length: 1489

[Binary PKIMessage with kup body]

Key fields:

  • body.kup.status.status: accepted (0)
  • body.kup.certifiedKeyPair.certOrEncCert: New certificate with updated key and serial

Example 3: Revocation

Request:

POST /.well-known/cmp HTTP/1.1
Host: ca.example.com
Content-Type: application/pkixcmp
Content-Length: 892

[Binary PKIMessage with rr body]

Key fields:

  • body.rr.certDetails.serialNumber: 0x123456789abcdef
  • body.rr.certDetails.issuer: CN=Kipuka Issuing CA
  • body.rr.crlEntryDetails.reasonCode: keyCompromise (1)

Response:

HTTP/1.1 200 OK
Content-Type: application/pkixcmp
Content-Length: 456

[Binary PKIMessage with rp body]

Key fields:

  • body.rp.status[0].status: accepted (0)
  • body.rp.revCerts[0].certId: Matches request serialNumber

curl Examples

Generate CMP ir message with OpenSSL

# Generate key pair
openssl genrsa -out device.key 2048

# Create CMP initialization request (requires OpenSSL CMP support)
openssl cmp -cmd ir \
  -server ca.example.com:8080/.well-known/cmp \
  -path /.well-known/cmp \
  -ref device001 \
  -secret "OTP:abc123def456" \
  -newkey device.key \
  -subject "/CN=device001.example.com/O=Example Inc" \
  -certout device.pem

# OpenSSL handles PKIMessage encoding, MAC protection, and response parsing

Note: OpenSSL 3.0+ includes built-in CMP client support. For older versions, use third-party tools like libcmp or Python asn1crypto.

Raw curl (for debugging)

# Construct PKIMessage manually (requires ASN.1 encoding library)
# Example: Python script using asn1crypto or pyasn1

python3 cmp_client.py \
  --operation ir \
  --subject "CN=device002.example.com" \
  --key device.key \
  --secret "abc123" \
  --output ir.der

# Send to server
curl -X POST https://ca.example.com/.well-known/cmp \
  -H "Content-Type: application/pkixcmp" \
  --data-binary @ir.der \
  -o ip.der

# Parse response
openssl asn1parse -inform DER -in ip.der

Security Considerations

  1. Always use HTTPS for CMP transport (TLS 1.2+ with strong ciphers)
  2. Validate MAC/signature before processing requests (prevent replay attacks)
  3. Use nonces and transaction IDs to prevent replay (RFC 9480)
  4. Rotate shared secrets regularly (monthly for OTP-based enrollment)
  5. Check certificate revocation status before accepting signature-protected requests
  6. Limit request rate to prevent DoS (max 10 requests/min per client)
  7. Audit all operations (enrollment, renewal, revocation) with client identity

Configuration

Enable CMP in kipuka.toml:

[cmp]
enabled = true
endpoint_path = "/.well-known/cmp"
max_message_size = 65536  # 64 KB
mac_iterations = 10000    # PBKDF2 iterations for MAC protection
signature_verify_chain = true
require_popo = true       # Require proof-of-possession
allowed_algorithms = ["RSA-2048", "RSA-3072", "RSA-4096", "ECDSA-P256", "ECDSA-P384", "Ed25519"]

References

  • RFC 4210: Internet X.509 Public Key Infrastructure Certificate Management Protocol (CMP)
  • RFC 9480: Certificate Management Protocol (CMP) Updates (nonce, transaction ID)
  • RFC 9810: Update to CMP Algorithms (modern crypto algorithms)
  • RFC 9483: Lightweight CMP Profile (for constrained environments)

CMS-EST Endpoints

kipuka supports CMS-based EST (RFC 8295) for enhanced security through Cryptographic Message Syntax (CMS) wrapping. CMS-EST provides confidentiality (encryption), integrity (signing), and authentication beyond standard EST’s TLS-only protection.

Overview

Standard EST (RFC 7030) relies on TLS for transport security but transmits certificate requests and responses as plaintext PKCS#7 or PKCS#10 within the TLS tunnel. CMS-EST adds an additional layer by wrapping EST payloads in CMS structures:

  • Signed data (SignedData): Authenticates sender and ensures integrity
  • Enveloped data (EnvelopedData): Encrypts payload for confidentiality
  • Signed-and-enveloped data: Combines both (sign-then-encrypt)

Use cases:

  • Defense-in-depth security (protect against TLS compromise)
  • End-to-end encryption between client and CA (bypassing intermediate proxies)
  • Compliance requirements (NIAP, FIPS) mandating application-layer encryption
  • Protection of sensitive CSR attributes (email, organizationIdentifier)

Endpoint Paths

CMS-EST uses the same operation names as standard EST but under the /.well-known/cms-est/ prefix:

EndpointHTTP MethodDescription
/.well-known/cms-est/cacertsGETRetrieve CA certificate chain (CMS-wrapped)
/.well-known/cms-est/csrattrsGETRetrieve CSR attributes hint (CMS-wrapped)
/.well-known/cms-est/simpleenrollPOSTSimple enrollment (CMS-wrapped CSR → CMS-wrapped certificate)
/.well-known/cms-est/simplereenrollPOSTSimple re-enrollment (renewal with proof-of-possession)
/.well-known/cms-est/serverkeygenPOSTServer-side key generation (CMS-wrapped response with encrypted private key)
/.well-known/cms-est/fullcmcPOSTFull CMC (Certificate Management over CMS, RFC 5272)

Note: /fullcmc is available in both standard EST and CMS-EST. In CMS-EST, the CMC request itself is CMS-wrapped for double encapsulation.

Content Types

CMS-EST uses different MIME types than standard EST to indicate CMS wrapping:

Standard EST (for comparison)

OperationRequest Content-TypeResponse Content-Type
/simpleenrollapplication/pkcs10application/pkcs7-mime; smime-type=certs-only
/simplereenrollapplication/pkcs10application/pkcs7-mime; smime-type=certs-only
/cacertsN/A (GET)application/pkcs7-mime; smime-type=certs-only

CMS-EST

OperationRequest Content-TypeResponse Content-Type
/simpleenrollapplication/pkcs7-mime; smime-type=CMC-request (signed)application/pkcs7-mime; smime-type=CMC-response (signed)
/simplereenrollapplication/pkcs7-mime; smime-type=CMC-requestapplication/pkcs7-mime; smime-type=CMC-response
/serverkeygenapplication/pkcs7-mime; smime-type=CMC-requestapplication/pkcs7-mime; smime-type=enveloped-data (encrypted private key)
/cacertsN/A (GET)application/pkcs7-mime; smime-type=certs-only (same as standard EST)
/csrattrsN/A (GET)application/pkcs7-mime; smime-type=CMC-response (signed attributes)

Key differences:

  1. Requests use smime-type=CMC-request instead of application/pkcs10
  2. Responses use smime-type=CMC-response instead of smime-type=certs-only
  3. Enveloped data (smime-type=enveloped-data) for encrypted payloads (serverkeygen)

CMS Wrapping Details

Signed Data (SignedData)

Used for authentication and integrity. The original EST payload (PKCS#10 CSR or certificate response) is embedded in a CMS SignedData structure.

Structure:

SignedData ::= SEQUENCE {
  version CMSVersion,
  digestAlgorithms DigestAlgorithmIdentifiers,
  encapContentInfo EncapsulatedContentInfo,  -- Contains original payload
  certificates [0] IMPLICIT CertificateSet OPTIONAL,  -- Signer's cert chain
  signerInfos SignerInfos  -- Digital signature
}

Verification:

  1. Extract signer certificate from certificates field
  2. Verify certificate chain against CA bundle
  3. Validate signature in signerInfos using signer’s public key
  4. Check signer certificate is not revoked (CRL/OCSP)

Enveloped Data (EnvelopedData)

Used for confidentiality. The payload is encrypted with a symmetric key (AES-256-GCM), and the symmetric key is encrypted with the recipient’s public key.

Structure:

EnvelopedData ::= SEQUENCE {
  version CMSVersion,
  recipientInfos RecipientInfos,  -- Encrypted symmetric key
  encryptedContentInfo EncryptedContentInfo  -- Encrypted payload
}

Decryption:

  1. Extract recipientInfos (key encryption key, KEK)
  2. Decrypt KEK using recipient’s private key (RSA-OAEP or ECDH)
  3. Decrypt encryptedContentInfo using KEK (AES-256-GCM)
  4. Parse decrypted payload as ASN.1 DER

Signed-and-Enveloped Data

For requests requiring both authentication and confidentiality, the payload is signed first, then the SignedData structure is enveloped:

Original CSR → SignedData → EnvelopedData → CMS-EST request

Recipient decrypts the envelope, then verifies the signature.

Example Requests

Example 1: Simple Enrollment (Signed CSR)

Request:

POST /.well-known/cms-est/simpleenroll HTTP/1.1
Host: ca.example.com
Content-Type: application/pkcs7-mime; smime-type=CMC-request
Content-Length: 1456

[Binary CMS SignedData containing PKCS#10 CSR]

CMS structure:

SignedData {
  digestAlgorithms: { sha256 }
  encapContentInfo: {
    eContentType: id-data (1.2.840.113549.1.7.1)
    eContent: [PKCS#10 CSR in DER format]
  }
  certificates: [client_cert, issuing_ca_cert]
  signerInfos: {
    sid: IssuerAndSerialNumber (client_cert)
    digestAlgorithm: sha256
    signatureAlgorithm: rsaEncryption (or ECDSA)
    signature: BIT STRING
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/pkcs7-mime; smime-type=CMC-response
Content-Length: 2048

[Binary CMS SignedData containing issued certificate]

CMS structure:

SignedData {
  encapContentInfo: {
    eContentType: id-data
    eContent: [Issued X.509 certificate in DER format]
  }
  certificates: [ca_cert_chain]
  signerInfos: {
    sid: SubjectKeyIdentifier (CA signing key)
    signatureAlgorithm: sha256WithRSAEncryption
    signature: BIT STRING (CA signature over certificate)
  }
}

Example 2: Server Key Generation (Enveloped Response)

Request:

POST /.well-known/cms-est/serverkeygen HTTP/1.1
Host: ca.example.com
Content-Type: application/pkcs7-mime; smime-type=CMC-request
Content-Length: 892

[Binary CMS SignedData containing server-keygen request]

Request payload:

  • encapContentInfo.eContent: Empty or minimal CSR (no key material, CA generates key)

Response:

HTTP/1.1 200 OK
Content-Type: application/pkcs7-mime; smime-type=enveloped-data
Content-Length: 3456

[Binary CMS EnvelopedData containing certificate + encrypted private key]

CMS structure:

EnvelopedData {
  version: 0
  recipientInfos: {
    rid: IssuerAndSerialNumber (client_cert)
    keyEncryptionAlgorithm: rsaesOaep (RSA-OAEP with SHA-256)
    encryptedKey: [Encrypted AES-256 key]
  }
  encryptedContentInfo: {
    contentType: id-data
    contentEncryptionAlgorithm: aes256-GCM
    encryptedContent: [Encrypted payload = issued cert + PKCS#8 private key]
  }
}

Decrypted payload:

  • Certificate in DER format
  • PKCS#8 EncryptedPrivateKeyInfo (private key encrypted with client’s transport key)

Example 3: Retrieve CA Certificates (No CMS Difference)

Request:

GET /.well-known/cms-est/cacerts HTTP/1.1
Host: ca.example.com

Response:

HTTP/1.1 200 OK
Content-Type: application/pkcs7-mime; smime-type=certs-only
Content-Length: 2048

[Binary PKCS#7 certs-only structure (same as standard EST)]

Note: /cacerts does not require CMS wrapping in most implementations, since it’s public data. RFC 8295 allows this for simplicity.

Differences from Standard EST

AspectStandard ESTCMS-EST
Request formatPKCS#10 CSR (DER)CMS SignedData containing PKCS#10 CSR
Response formatPKCS#7 certs-onlyCMS SignedData or EnvelopedData
Content-Type (request)application/pkcs10application/pkcs7-mime; smime-type=CMC-request
Content-Type (response)application/pkcs7-mime; smime-type=certs-onlyapplication/pkcs7-mime; smime-type=CMC-response
AuthenticationTLS client certificate or HTTP Basic AuthCMS signature (signed request)
ConfidentialityTLS transport encryption onlyCMS enveloped data (end-to-end encryption)
Proof-of-possessionSelf-signed CSR signatureCMS signature over request
Use caseGeneral EST deploymentsHigh-security, defense-in-depth, compliance

OpenSSL Examples

Generate CMS-EST enrollment request

# Generate key pair
openssl genrsa -out device.key 2048

# Create PKCS#10 CSR (standard)
openssl req -new -key device.key \
  -subj "/CN=device003.example.com/O=Example Inc" \
  -out device.csr

# Wrap CSR in CMS SignedData
openssl cms -sign \
  -in device.csr \
  -signer client-cert.pem \
  -inkey client-key.pem \
  -out cms-request.der \
  -outform DER \
  -binary

# Send to CA
curl -X POST https://ca.example.com/.well-known/cms-est/simpleenroll \
  -H "Content-Type: application/pkcs7-mime; smime-type=CMC-request" \
  --data-binary @cms-request.der \
  -o cms-response.der

Verify and extract certificate from CMS response

# Verify CMS signature
openssl cms -verify \
  -in cms-response.der \
  -inform DER \
  -CAfile ca-bundle.pem \
  -out cert.der

# Convert to PEM
openssl x509 -in cert.der -inform DER -out cert.pem

# Inspect certificate
openssl x509 -in cert.pem -text -noout

Decrypt serverkeygen response (EnvelopedData)

# Receive encrypted response from CA
curl -X POST https://ca.example.com/.well-known/cms-est/serverkeygen \
  -H "Content-Type: application/pkcs7-mime; smime-type=CMC-request" \
  --data-binary @cms-serverkeygen-req.der \
  -o cms-encrypted-response.der

# Decrypt using client certificate's private key
openssl cms -decrypt \
  -in cms-encrypted-response.der \
  -inform DER \
  -recip client-cert.pem \
  -inkey client-key.pem \
  -out decrypted-payload.der

# Extract certificate and private key
# (payload format is application-specific; may be concatenated DER or PKCS#12)

Security Considerations

  1. TLS is still required – CMS-EST is not a replacement for TLS. Always use HTTPS.
  2. Validate CMS signatures – Verify SignedData against trusted CA bundle before processing.
  3. Check certificate revocation – Ensure signer certificates are not revoked (CRL/OCSP).
  4. Use strong algorithms – Require SHA-256+ for digests, RSA-3072+/ECDSA-P256+ for signatures.
  5. Protect recipient keys – Private keys for decrypting EnvelopedData must be HSM-protected.
  6. Audit all operations – Log CMS signature validation results and decryption attempts.
  7. Limit request size – CMS wrapping increases payload size; enforce max_body_size limits (e.g., 1 MB).

Configuration

Enable CMS-EST in kipuka.toml:

[cms_est]
enabled = true
endpoint_prefix = "/.well-known/cms-est"
require_signed_requests = true
require_enveloped_responses = false  # Set true for high-security deployments
max_message_size = 1048576  # 1 MB
allowed_digest_algorithms = ["SHA-256", "SHA-384", "SHA-512"]
allowed_signature_algorithms = ["RSA-PSS", "ECDSA", "Ed25519"]
allowed_encryption_algorithms = ["AES-256-GCM", "AES-256-CBC"]

Troubleshooting

CMS signature verification failed

Symptom: HTTP 400 Bad Request with “CMS signature verification failed”.

Causes:

  • Signer certificate expired or revoked
  • Signer certificate not in certificates field
  • CA bundle doesn’t include signer’s issuing CA
  • Signature algorithm not allowed (e.g., SHA-1, MD5)

Fix:

  • Verify signer certificate: openssl x509 -in cert.pem -text -noout
  • Check revocation: openssl ocsp -issuer ca.pem -cert cert.pem -url http://ocsp.example.com
  • Update CA bundle with intermediate CAs

Decryption failed (EnvelopedData)

Symptom: openssl cms -decrypt fails with “wrong recipient”.

Causes:

  • RecipientInfo references wrong certificate
  • Private key doesn’t match recipient certificate
  • Unsupported key encryption algorithm

Fix:

  • Inspect recipientInfos: openssl asn1parse -in cms-response.der -inform DER
  • Verify certificate matches: openssl x509 -in cert.pem -pubkey -noout | openssl dgst -sha256
  • Use supported KEK algorithms: RSA-OAEP (preferred) or RSAES-PKCS1-v1_5 (legacy)

Payload too large

Symptom: HTTP 413 Payload Too Large.

Cause: CMS wrapping increased request size beyond max_message_size.

Fix:

  • Increase max_message_size to 2 MB or 5 MB
  • Reduce certificate chain length in certificates field (send only leaf cert)
  • Use DER instead of PEM (smaller encoding)

References

  • RFC 8295: EST Extensions
  • RFC 5652: Cryptographic Message Syntax (CMS)
  • RFC 3370: Cryptographic Message Syntax (CMS) Algorithms
  • RFC 5083: Cryptographic Message Syntax (CMS) Authenticated-Enveloped-Data Content Type
  • RFC 7030: Enrollment over Secure Transport (EST)

Architecture

kipuka is structured as a Cargo workspace with six crates, each owning a distinct responsibility. This separation enforces module boundaries at the compilation level and allows operators to build only the features they need.

Workspace layout

flowchart TD
    clients["Clients"]:::client

    clients -->|"TLS + mTLS / OTP"| est
    clients -->|"CoAP / DTLS<br/>(UDP)"| coap

    subgraph core [" "]
        est["<b>kipuka-est</b><br/>axum HTTP · TLS · EST protocol<br/>auth · HA · admin API"]:::server
        coap["<b>kipuka-coap</b><br/>CoAP / DTLS transport<br/>RFC 9483 · RFC 7252"]:::server
    end

    subgraph modules [" "]
        otp["<b>kipuka-otp</b><br/>OTP lifecycle<br/>argon2id · rate limiting"]:::auth
        hsm["<b>kipuka-hsm</b><br/>PKCS #11 / HSM<br/>cryptoki FFI"]:::crypto
        util["<b>kipuka-util</b><br/>shared types · config<br/>ASN.1 · zeroize"]:::server
    end

    est --> otp
    est --> hsm
    est --> util
    coap --> util

    hsm --> dogtag
    dogtag["<b>kipuka-dogtag</b><br/>Dogtag PKI<br/>REST client"]:::infra

    otp --> db
    est --> db
    db[("sqlx<br/>SQLite · PostgreSQL<br/>MariaDB")]:::store

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef store  fill:#1f1a0a,stroke:#ca8a04,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0
    classDef infra  fill:#1f0a18,stroke:#db2777,color:#e2e8f0

    style core fill:none,stroke:none
    style modules fill:none,stroke:none

Crate responsibilities

CrateRole
kipuka-estCore server binary. Owns the axum HTTP router, TLS termination (rustls), EST protocol handlers (/cacerts, /simpleenroll, /simplereenroll, /serverkeygen, /fullcmc, /csrattrs), request authentication, CSR validation, certificate construction (via synta), the admin API, database access (sqlx), and the HA state machine.
kipuka-hsmPKCS #11 integration via the cryptoki crate. Provides a Signer trait implementation that delegates cryptographic operations to an HSM. Handles slot enumeration, session management, key lookup, and sign operations. Isolates all unsafe FFI behind a safe Rust API.
kipuka-otpOne-time password lifecycle: generation (CSPRNG), hashing (argon2id / bcrypt), storage, validation, rate limiting, and expiration. Exposes an internal API consumed by kipuka-est for enrollment authentication and by the admin API for token provisioning.
kipuka-utilShared types and configuration parsing. Owns kipuka.toml deserialization (via serde + toml), X.509 helper functions, ASN.1 OID constants, error types, and the zeroize-aware wrappers for sensitive data.
kipuka-dogtagREST client for Red Hat Certificate System / Dogtag PKI. Translates EST enrollment requests into Dogtag profile-based certificate issuance calls, delegating signing to a full CA back-end instead of local key material.
kipuka-coapCoAP/DTLS transport layer (RFC 7252 / RFC 9483) for constrained-device enrollment. The CoapDtlsServer opens a UDP listener with DTLS 1.2/1.3 encryption (via OpenSSL), parses CoAP messages, and dispatches EST operations through the EstHandler trait. Block-wise transfer (RFC 7959) splits large certificate payloads across multiple datagrams. The main crate provides a CoapEstHandler that bridges CoAP requests to the shared EST enrollment logic, so both HTTPS and CoAP paths use identical CA signing, CSR validation, and audit code.

Dependencies flow strictly downward: kipuka-est depends on all other crates; leaf crates (kipuka-util, kipuka-coap) depend on nothing project-internal except kipuka-util.


EST operation data flow

A certificate enrollment request traverses one of two transport paths – HTTPS or CoAP/DTLS – before reaching the shared EST enrollment logic.

HTTPS transport (default)

flowchart TD
    C["Client"]:::client

    subgraph transport ["Transport"]
        TLS["<b>rustls</b><br/>TLS 1.2 / 1.3 handshake<br/>mTLS client cert or<br/>OTP via HTTP Basic"]:::crypto
    end

    subgraph routing ["Routing"]
        ROUTER["<b>axum router</b><br/>/.well-known/est/{label}/simpleenroll<br/>/.well-known/est/{label}/simplereenroll<br/>/.well-known/est/{label}/cacerts<br/>/.well-known/est/{label}/serverkeygen"]:::server
        LABEL["<b>Label resolution</b><br/>look up [[est.label]] entry<br/>resolve bound [[ca]] config"]:::server
    end

    subgraph authn ["Authentication"]
        direction LR
        MTLS["<b>mTLS</b><br/>verify client<br/>certificate chain"]:::auth
        OTP["<b>OTP</b><br/>argon2id hash<br/>comparison"]:::auth
        GSS["<b>GSSAPI</b><br/>SPNEGO /<br/>Kerberos"]:::auth
    end

    subgraph issue ["Issuance"]
        CSR["<b>CSR parsing</b><br/>synta · key type · SANs<br/>policy validation"]:::server
        TBS["<b>Certificate construction</b><br/>X.509 TBS · serial from<br/>OsRng (CSPRNG)"]:::server
    end

    subgraph sign ["Signing"]
        direction LR
        FILE["<b>File key</b><br/>synta direct<br/>signing"]:::crypto
        HSM["<b>HSM</b><br/>PKCS#11<br/>session"]:::crypto
        DOG["<b>Dogtag</b><br/>REST API<br/>delegation"]:::infra
    end

    RESP["<b>Response</b><br/>PKCS#7 / CMS envelope<br/>DER-encoded"]:::server
    AUDIT["<b>Audit</b><br/>file · syslog · database"]:::store

    C --> TLS
    TLS --> ROUTER
    ROUTER --> LABEL
    LABEL --> authn
    MTLS --> CSR
    OTP --> CSR
    GSS --> CSR
    CSR --> TBS
    TBS --> sign
    FILE --> RESP
    HSM --> RESP
    DOG --> RESP
    RESP --> AUDIT

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef store  fill:#1f1a0a,stroke:#ca8a04,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0
    classDef infra  fill:#1f0a18,stroke:#db2777,color:#e2e8f0

    style transport fill:none,stroke:#30363d,stroke-dasharray:5 5
    style routing fill:none,stroke:#30363d,stroke-dasharray:5 5
    style authn fill:none,stroke:#30363d,stroke-dasharray:5 5
    style issue fill:none,stroke:#30363d,stroke-dasharray:5 5
    style sign fill:none,stroke:#30363d,stroke-dasharray:5 5

CoAP/DTLS transport (constrained devices)

flowchart TD
    D["IoT device"]:::client

    subgraph transport ["Transport"]
        DTLS["<b>OpenSSL</b><br/>DTLS 1.2 / 1.3<br/>PSK or certificate mode"]:::crypto
    end

    subgraph coap_parse ["CoAP processing"]
        COAP["<b>CoAP parser</b><br/>method · URI path · Content-Format"]:::server
        BLK["<b>Block-wise transfer</b><br/>RFC 7959 · reassemble<br/>incoming blocks"]:::server
    end

    subgraph paths ["CoAP → EST path mapping"]
        direction LR
        SEN["/sen → SimpleEnroll"]:::server
        SREN["/sren → SimpleReenroll"]:::server
        SKG["/skg → ServerKeygen"]:::server
        ATT["/att → CsrAttrs"]:::server
        CRTS["/crts → CaCerts"]:::server
    end

    EST["<b>CoapEstHandler</b><br/>shared EST logic<br/>(same as HTTPS steps 5–9)"]:::server
    RESP["<b>CoAP 2.05 Content</b><br/>raw DER · Content-Format ID"]:::server
    AUDIT["<b>Audit log</b>"]:::store

    D -->|"UDP :5684"| DTLS
    DTLS --> COAP
    COAP --> BLK
    BLK --> paths
    SEN --> EST
    SREN --> EST
    SKG --> EST
    ATT --> EST
    CRTS --> EST
    EST --> RESP
    RESP --> AUDIT

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef store  fill:#1f1a0a,stroke:#ca8a04,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0

    style transport fill:none,stroke:#30363d,stroke-dasharray:5 5
    style coap_parse fill:none,stroke:#30363d,stroke-dasharray:5 5
    style paths fill:none,stroke:#30363d,stroke-dasharray:5 5

Both paths converge on the same CSR validation, certificate construction, and signing logic — the EstHandler trait abstracts transport details so the core enrollment code is shared between HTTPS and CoAP.


Multi-CA HA failover state machine

When [ha] is enabled, each CA in an [[ha.group]] transitions through four states. The HA controller runs periodic health checks and manages transitions automatically.

flowchart LR
    H["<b>Healthy</b><br/>operational<br/>receiving traffic"]:::server
    D["<b>Degraded</b><br/>checks failing<br/>still serving"]:::auth
    F["<b>Failed</b><br/>removed from<br/>routing pool"]:::infra
    R["<b>Recovery</b><br/>probing<br/>no traffic yet"]:::crypto

    H -- "health check<br/>fails" --> D
    D -- "check<br/>passes" --> H
    D -- "failure_threshold<br/>consecutive<br/>failures" --> F
    F -- "recovery_timeout<br/>elapsed" --> R
    R -- "sustained success<br/>(threshold<br/>checks pass)" --> H
    R -- "check fails<br/>during<br/>recovery" --> F

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef store  fill:#1f1a0a,stroke:#ca8a04,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0
    classDef infra  fill:#1f0a18,stroke:#db2777,color:#e2e8f0

State definitions:

  • Healthy – CA is operational. Health checks pass. Enrollment requests are routed to this CA normally.
  • Degraded – One or more health checks have failed but the threshold has not been reached. The CA continues to receive traffic. An alert is raised.
  • Failed – Consecutive failures have reached failure_threshold. The CA is removed from the routing pool. If the CA was the active node in an active-passive group, the next CA in ca_ids order is promoted.
  • Recovery – After recovery_timeout elapses, the HA controller begins probing the failed CA. It must pass failure_threshold consecutive checks before returning to Healthy. During recovery the CA does not receive enrollment traffic.

Failover strategies (set per [[ha.group]] or globally in [ha]):

StrategyBehavior
active-passiveFirst healthy CA in ca_ids order handles all requests. Failover promotes the next CA in order.
round-robinRequests are distributed across all healthy CAs in rotation.
weightedCAs are weighted by a configurable priority; higher-priority CAs receive more traffic.
latency-basedHealth check latency is tracked; requests are routed to the CA with the lowest observed latency.

The health check itself performs a lightweight signing operation (or, for Dogtag-backed CAs, a REST API ping) to verify that the CA can actually issue certificates. Network reachability alone is insufficient – a reachable HSM that has entered an error state must still be detected as unhealthy.


Authentication flow

OTP validation path

flowchart TD
    C["<b>Client</b><br/>HTTP Basic: entity_id : otp_value"]:::client

    LOOKUP["Look up entity_id<br/>in database"]:::store

    EXP{"Expired?<br/>expires_at > now"}:::auth
    USE{"Max uses<br/>reached?"}:::auth
    LOCK{"Locked out?<br/>failed_attempts<br/>≥ max_failures"}:::auth

    HASH["Hash provided OTP<br/>with argon2id"]:::crypto
    CMP{"Timing-safe comparison<br/>subtle::ConstantTimeEq"}:::crypto

    OK["<b>Success</b><br/>increment use count<br/>clear failure counter"]:::server
    FAIL["<b>Failure</b><br/>increment failed_attempts<br/>check lockout threshold"]:::infra

    C --> LOOKUP
    LOOKUP --> EXP
    EXP -- "not expired" --> USE
    EXP -- "expired" --> FAIL
    USE -- "within limit" --> LOCK
    USE -- "exhausted" --> FAIL
    LOCK -- "not locked" --> HASH
    LOCK -- "locked" --> FAIL
    HASH --> CMP
    CMP -- "match" --> OK
    CMP -- "mismatch" --> FAIL

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef store  fill:#1f1a0a,stroke:#ca8a04,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0
    classDef infra  fill:#1f0a18,stroke:#db2777,color:#e2e8f0

OTP values are never stored in plaintext. The argon2id hash is computed at token generation time and only the hash is persisted. The raw token is returned to the administrator exactly once in the generation response.

mTLS certificate chain validation

flowchart TD
    C["<b>Client</b><br/>TLS ClientHello<br/>+ Certificate"]:::client

    subgraph rustls_verify ["rustls verification"]
        SIG["Verify certificate<br/>signature is valid"]:::crypto
        EXP["Check certificate<br/>is not expired"]:::crypto
        CHAIN["Issuer chain terminates<br/>at configured trust anchor"]:::crypto
        KU["Key usage includes<br/>digitalSignature"]:::crypto
        EKU["Extended key usage<br/>includes clientAuth"]:::crypto
    end

    EXTRACT["<b>kipuka-est extracts</b><br/>Subject DN (audit + authz)<br/>Serial number (identity tracking)<br/>SAN entries (device identification)"]:::server

    C --> SIG
    SIG --> EXP
    EXP --> CHAIN
    CHAIN --> KU
    KU --> EKU
    EKU --> EXTRACT

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0

    style rustls_verify fill:none,stroke:#30363d,stroke-dasharray:5 5

GSSAPI / Kerberos

flowchart LR
    C["<b>Client</b><br/>Negotiate:<br/>SPNEGO token"]:::client
    CTX["Accept security<br/>context using<br/>server keytab"]:::auth
    PRINC["Extract<br/>authenticated<br/>principal<br/>user@REALM"]:::auth
    MAP["Map principal to<br/>certificate subject<br/>via principal_mapping<br/>or default_template"]:::server
    SUBJ["Mapped subject →<br/>certificate<br/>construction"]:::crypto

    C --> CTX --> PRINC --> MAP --> SUBJ

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0

Database schema overview

kipuka uses sqlx with compile-time checked queries. The schema is managed through sequential migrations in migrations/{sqlite,postgres,mariadb}/.

Core tables

otps – One-time password state.

ColumnTypeDescription
idINTEGER / SERIALPrimary key
entity_idTEXTClient identifier (e.g., device hostname)
otp_hashTEXTArgon2id hash of the OTP value
created_atTIMESTAMPToken generation time
expires_atTIMESTAMPToken expiration time
max_usesINTEGERMaximum allowed uses
use_countINTEGERCurrent use count
failed_attemptsINTEGERConsecutive failed validation attempts
last_failure_atTIMESTAMPTime of most recent failed attempt
locked_untilTIMESTAMPLockout expiration (NULL if not locked)

audit_log – Tamper-evident audit trail.

ColumnTypeDescription
idINTEGER / SERIALPrimary key
timestampTIMESTAMPEvent time (UTC)
event_typeTEXTEvent category (enroll, renew, reject, otp_create, etc.)
entity_idTEXTClient or device identifier
ca_idTEXTCA that processed the request
labelTEXTEST label (NULL for unlabeled requests)
auth_methodTEXTAuthentication method (mtls, otp, gssapi)
outcomeTEXTsuccess or failure
detailTEXTHuman-readable detail or error message
cert_fingerprintTEXTSHA-256 fingerprint of issued certificate (NULL on failure)
client_ipTEXTSource IP address

certs – Certificate inventory.

ColumnTypeDescription
idINTEGER / SERIALPrimary key
serial_numberTEXTCertificate serial (hex-encoded)
subject_dnTEXTSubject distinguished name
issuer_dnTEXTIssuer distinguished name
not_beforeTIMESTAMPValidity start
not_afterTIMESTAMPValidity end
fingerprintTEXTSHA-256 fingerprint
ca_idTEXTIssuing CA identifier
labelTEXTEST label used for issuance
entity_idTEXTAssociated entity (from OTP or mTLS subject)
pemTEXTFull PEM-encoded certificate (optional, controlled by config)

EST label routing

EST labels are the primary mechanism for multi-profile and multi-CA operation. When a request arrives at /.well-known/est/{label}/simpleenroll, kipuka resolves the label to a [[est.label]] configuration entry:

flowchart TD
    REQ["<b>Request URL</b><br/>/.well-known/est/<b>iot-devices</b>/simpleenroll"]:::client

    LABEL["<b>Label lookup</b><br/>name == iot-devices"]:::server

    CA["<b>CA lookup</b><br/>ca_id == iot-ca<br/>cert · key · chain<br/>validity_days · hsm_slot"]:::crypto

    HA{"CA in HA group<br/>and in Failed state?"}:::infra

    NEXT["Route to next healthy<br/>CA in group ca_ids"]:::server

    POLICY["<b>Policy enforcement</b><br/>allowed_key_types<br/>required_ext_key_usage<br/>require_san · subject_pattern<br/>max_validity_days"]:::auth

    ISSUE["<b>Certificate issuance</b><br/>using resolved CA key material<br/>and label policy"]:::crypto

    REQ --> LABEL
    LABEL -- "ca_id = iot-ca" --> CA
    CA --> HA
    HA -- "yes" --> NEXT
    NEXT --> POLICY
    HA -- "no" --> POLICY
    POLICY --> ISSUE

    classDef client fill:#1a2332,stroke:#3b82f6,color:#e2e8f0
    classDef server fill:#132218,stroke:#16a34a,color:#e2e8f0
    classDef crypto fill:#2d1a0a,stroke:#f0883e,color:#e2e8f0
    classDef store  fill:#1f1a0a,stroke:#ca8a04,color:#e2e8f0
    classDef auth   fill:#1a1025,stroke:#8b5cf6,color:#e2e8f0
    classDef infra  fill:#1f0a18,stroke:#db2777,color:#e2e8f0

Requests without a label segment (e.g., /.well-known/est/simpleenroll) use the first [[ca]] entry as the default CA with no additional label-level policy enforcement.

When HA is enabled, label routing is extended: the label’s ca_id is checked against [[ha.group]] memberships. If the CA belongs to an HA group and is in a Failed state, the request is transparently routed to the next healthy CA in the group’s ca_ids list. The label’s policy constraints (key types, subject pattern, etc.) are still enforced regardless of which CA in the group handles the request.

Development Setup

This guide covers everything needed to build kipuka from source and run it locally with Docker Compose.

Prerequisites

ToolMinimum versionPurpose
Rust toolchain1.88+Compiler and cargo
Docker or Podman24+ / 4+Container runtime for Compose profiles
Docker Compose2.20+Orchestrates database and HSM dev containers
OpenSSL CLI1.1.1+ or 3.xGenerating test certificates
pkg-configanyLocating system libraries during build

Clone and build

git clone https://codeberg.org/czinda/kipuka.git
cd kipuka
cargo build

A release-optimized build (slower compilation, faster binary):

cargo build --release

The workspace produces a single binary at target/debug/kipuka (or target/release/kipuka).

OS-specific dependencies

Fedora / RHEL / CentOS Stream

sudo dnf install openssl-devel clang cmake pkg-config sqlite-devel

Debian / Ubuntu

sudo apt install libssl-dev clang cmake pkg-config libsqlite3-dev

macOS

brew install openssl cmake pkg-config
export OPENSSL_DIR=$(brew --prefix openssl)

Docker Compose profiles

The repository includes a compose.yaml with profiles for different database backends and an HSM development environment. Each profile starts only the services relevant to that backend.

Available profiles

ProfileServices startedUse case
sqlite (default)kipuka onlyMinimal local development; SQLite file on disk
postgreskipuka + PostgreSQL 16Testing against PostgreSQL
mariadbkipuka + MariaDB 11Testing against MariaDB
hsmkipuka + Kryoptic SoftHSMPKCS#11 development without hardware

Running with SQLite (default)

docker compose up

This starts kipuka with an in-process SQLite database. No external database container is required.

Running with PostgreSQL

docker compose --profile postgres up

The PostgreSQL container is preconfigured with:

  • Database: kipuka
  • User: kipuka
  • Password: kipuka-dev
  • Port: 5432

The kipuka service automatically uses the connection string postgres://kipuka:kipuka-dev@postgres:5432/kipuka.

Running with MariaDB

docker compose --profile mariadb up

The MariaDB container is preconfigured with:

  • Database: kipuka
  • User: kipuka
  • Password: kipuka-dev
  • Port: 3306

Running with Kryoptic HSM

docker compose --profile hsm up

This starts a Kryoptic SoftHSM container alongside kipuka. Kryoptic is an open-source PKCS#11 implementation written in Rust, suitable for development and testing. It provides the same PKCS#11 API as hardware HSMs without requiring physical tokens.

See the HSM development section below for slot configuration and key management.

Generate test certificates

The repository includes a helper script that creates a complete test PKI hierarchy suitable for local development:

./contrib/local-dev/setup-ca.sh

This generates the following files under contrib/local-dev/pki/:

FileContents
ca.pemSelf-signed root CA certificate
ca-key.pemRoot CA private key
server.pemServer TLS certificate (SAN: localhost, 127.0.0.1)
server-key.pemServer TLS private key
client.pemClient certificate for mTLS testing
client-key.pemClient private key

The script is idempotent – running it again regenerates all certificates.

Minimal kipuka.toml for local development

Create a kipuka.toml in the repository root:

[server]
listen = "0.0.0.0:9443"

[tls]
cert = "contrib/local-dev/pki/server.pem"
key = "contrib/local-dev/pki/server-key.pem"

[tls.client_auth]
trust_anchors = "contrib/local-dev/pki/ca.pem"
mode = "optional"

[db]
url = "sqlite://kipuka-dev.db?mode=rwc"
auto_migrate = true

[[ca]]
id = "dev-ca"
name = "Development CA"
cert = "contrib/local-dev/pki/ca.pem"
key = "contrib/local-dev/pki/ca-key.pem"
validity_days = 365

[est]
base_path = "/.well-known/est"

[[est.label]]
name = "default"
ca_id = "dev-ca"

[otp]
enabled = true
token_length = 16
default_ttl = "24h"
max_uses = 1
hash_algorithm = "argon2id"

[admin]
enabled = true
auth = "bearer"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Run the server:

export KIPUKA_ADMIN_TOKEN="dev-token-do-not-use-in-production"
cargo run -- --config kipuka.toml

Running against each database backend

SQLite

No additional setup. The database file is created automatically when auto_migrate = true:

[db]
url = "sqlite://kipuka-dev.db?mode=rwc"
auto_migrate = true

PostgreSQL

Start a local PostgreSQL instance or use the Compose profile:

docker compose --profile postgres up -d postgres

Update kipuka.toml:

[db]
url = "postgres://kipuka:kipuka-dev@localhost:5432/kipuka"
auto_migrate = true

Run migrations explicitly if auto_migrate is disabled:

cargo run -- migrate --config kipuka.toml

MariaDB

Start MariaDB via the Compose profile:

docker compose --profile mariadb up -d mariadb

Update kipuka.toml:

[db]
url = "mysql://kipuka:kipuka-dev@localhost:3306/kipuka"
auto_migrate = true

HSM development with Kryoptic

Kryoptic provides a PKCS#11 interface compatible with the cryptoki crate used by kipuka-hsm. It stores keys in software but exposes the same API as a hardware token.

Starting Kryoptic

docker compose --profile hsm up -d kryoptic

The container exposes the PKCS#11 shared library at a bind-mounted path. Check the compose.yaml for the exact mount point (typically /usr/lib/libkryoptic.so inside the container).

Initializing a token

Use pkcs11-tool (from the OpenSC package) to initialize a slot and generate a CA signing key:

# Initialize the token in slot 0
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-token --slot 0 \
  --label "kipuka-dev" \
  --so-pin 12345678

# Set the user PIN
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-pin --slot 0 \
  --login --so-pin 12345678 \
  --new-pin 1234

# Generate an ECDSA P-256 key pair for CA signing
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --keypairgen --key-type EC:prime256v1 \
  --id 01 --label "dev-ca-key"

Configuring kipuka for HSM

Update kipuka.toml to reference the PKCS#11 module:

[hsm]
library = "/usr/lib/libkryoptic.so"
slot = 0
token_label = "kipuka-dev"
pin = "1234"  # For dev only; use pin_env or pin_file in production

[[ca]]
id = "hsm-dev-ca"
name = "HSM Development CA"
cert = "contrib/local-dev/pki/ca.pem"
key = "pkcs11:object=dev-ca-key"
hsm_slot = 0

Listing objects in the token

pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --list-objects

Verifying HSM signing

# Sign a test payload to confirm the PKCS#11 path works
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --sign --mechanism ECDSA \
  --id 01 \
  --input-file /dev/urandom --read-write

IDE setup

rust-analyzer

kipuka works out of the box with rust-analyzer. The workspace Cargo.toml at the repository root defines all crate members, so rust-analyzer automatically discovers the full project.

VS Code

Recommended extensions:

ExtensionPurpose
rust-lang.rust-analyzerRust language support, inline type hints, go-to-definition
vadimcn.vscode-lldbDebugger for Rust binaries
tamasfe.even-better-tomlTOML syntax highlighting and validation
serayuzgur.cratesCrate version hints in Cargo.toml
usernamehw.errorlensInline compiler error display

Recommended .vscode/settings.json for the workspace:

{
  "rust-analyzer.check.command": "clippy",
  "rust-analyzer.check.extraArgs": ["--", "-D", "warnings"],
  "rust-analyzer.cargo.features": "all",
  "editor.formatOnSave": true,
  "[rust]": {
    "editor.defaultFormatter": "rust-lang.rust-analyzer"
  }
}

Environment variables for development

VariablePurposeExample
RUST_LOGLog verbositydebug, kipuka_est=trace
KIPUKA_ADMIN_TOKENAdmin API bearer tokendev-token-do-not-use-in-production
KIPUKA_HSM_PINHSM PIN (production)(set in env, not in config file)

Set these in a .env file (excluded from version control via .gitignore) or export them in your shell.

Next steps

  • Testing – run the test suite and perform protocol-level verification
  • Database Migrations – create and manage schema changes
  • Contributing – code style, commit conventions, and security invariants

Testing

kipuka’s test suite covers unit tests, integration tests, EST protocol conformance, HSM operations, and full-stack enrollment flows.

Unit tests

Unit tests run against in-memory state and do not require any external services. They execute quickly and are suitable for rapid iteration:

cargo test

Per-crate testing

Run tests for a specific crate to reduce compilation time during focused development:

cargo test -p kipuka-est
cargo test -p kipuka-hsm
cargo test -p kipuka-otp
cargo test -p kipuka-util
cargo test -p kipuka-dogtag
cargo test -p kipuka-coap

Filtering tests

Run a single test or a subset by name:

# Run all tests with "otp" in the name
cargo test otp

# Run a specific test function
cargo test -p kipuka-otp -- test_argon2_hash_roundtrip

# Show test output (including println! in passing tests)
cargo test -- --nocapture

Integration tests

Integration tests exercise the full HTTP stack: TLS handshake, request routing, authentication, CSR validation, certificate issuance, and database persistence. They are gated behind a feature flag because they start a real axum server on a random port:

cargo test --features integration

Integration tests create a temporary SQLite database for each test run and clean it up on completion. No external database is required.

What integration tests cover

  • Full /simpleenroll flow with OTP authentication
  • Full /simplereenroll flow with mTLS client certificate
  • /cacerts response format and PKCS#7 encoding
  • /csrattrs response with configured OID sets
  • /serverkeygen key pair generation and certificate issuance
  • EST label routing to different CAs
  • CSR policy rejection (wrong key type, missing SAN, bad subject DN)
  • OTP rate limiting and lockout behavior
  • Concurrent enrollment under load
  • HA failover (with mock CA health checks)
  • Audit log correctness

EST protocol testing with curl and openssl

These commands verify kipuka’s EST implementation at the protocol level. They assume a running server at localhost:9443 with the test PKI from contrib/local-dev/setup-ca.sh.

Fetch CA certificates

The /cacerts endpoint requires no authentication:

curl -sk https://localhost:9443/.well-known/est/cacerts \
  | base64 -d \
  | openssl pkcs7 -inform DER -print_certs

Expected output: the PEM-encoded CA certificate chain.

Enroll with OTP

Generate an OTP via the admin API, then enroll:

# Generate OTP
OTP=$(curl -sk -X POST https://localhost:9443/admin/otp \
  -H "Authorization: Bearer $KIPUKA_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "test-device"}' \
  | jq -r '.otp')

# Generate CSR
openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout test.key -out test.csr -nodes \
  -subj "/CN=test-device"

# Enroll
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  -u "test-device:${OTP}" \
  --data-binary @test.csr \
  -H "Content-Type: application/pkcs10" \
  -o test.p7 \
  -w "%{http_code}\n" \
  https://localhost:9443/.well-known/est/simpleenroll

# Extract certificate
openssl pkcs7 -inform DER -in test.p7 -print_certs -out test.pem

# Verify
openssl verify -CAfile contrib/local-dev/pki/ca.pem test.pem

Re-enroll with client certificate

Use the certificate from the previous step for mTLS authentication:

# Generate new CSR (with fresh key pair)
openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout test-new.key -out test-new.csr -nodes \
  -subj "/CN=test-device"

# Re-enroll with mTLS
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  --cert test.pem --key test.key \
  --data-binary @test-new.csr \
  -H "Content-Type: application/pkcs10" \
  -o test-new.p7 \
  -w "%{http_code}\n" \
  https://localhost:9443/.well-known/est/simplereenroll

# Extract renewed certificate
openssl pkcs7 -inform DER -in test-new.p7 -print_certs -out test-new.pem

Label routing

Test enrollment against a specific EST label:

# Enroll against the "iot" label (if configured)
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  -u "sensor-01:${OTP}" \
  --data-binary @sensor.csr \
  -H "Content-Type: application/pkcs10" \
  -o sensor.p7 \
  https://localhost:9443/.well-known/est/iot/simpleenroll

# Verify the issuer matches the CA bound to the "iot" label
openssl pkcs7 -inform DER -in sensor.p7 -print_certs \
  | openssl x509 -noout -issuer

Server key generation

Test the /serverkeygen endpoint (if enabled in config):

# Request server-generated key pair
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  --cert test.pem --key test.key \
  --data-binary @test.csr \
  -H "Content-Type: application/pkcs10" \
  -o serverkeygen.p7 \
  https://localhost:9443/.well-known/est/serverkeygen

The response is a multipart MIME message containing both the certificate (PKCS#7) and the server-generated private key (PKCS#8).

TLS handshake inspection

Use openssl s_client to inspect the TLS configuration:

# Connect and show server certificate
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  -servername localhost \
  2>/dev/null | openssl x509 -noout -text

# Test mTLS with client certificate
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  -cert contrib/local-dev/pki/client.pem \
  -key contrib/local-dev/pki/client-key.pem \
  -servername localhost

# Show negotiated cipher and protocol
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  2>/dev/null | grep -E "Protocol|Cipher"

# Test TLS 1.3 only
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  -tls1_3 2>/dev/null | grep "Protocol"

HSM testing with Kryoptic

Test the PKCS#11 code path using the Kryoptic SoftHSM container from compose.yaml:

# Start Kryoptic
docker compose --profile hsm up -d kryoptic

# Initialize token and generate test key (see Development Setup for details)
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-token --slot 0 --label "test-hsm" --so-pin 12345678
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-pin --slot 0 --login --so-pin 12345678 --new-pin 1234
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --keypairgen --key-type EC:prime256v1 \
  --id 01 --label "test-ca-key"

# Run HSM-specific tests
cargo test -p kipuka-hsm --features integration

HSM tests verify:

  • PKCS#11 session lifecycle (open, login, operate, close)
  • Key lookup by label and ID
  • ECDSA and RSA signing operations
  • Error handling for unavailable slots, wrong PINs, missing keys
  • Concurrent signing from multiple threads (session pooling)

CI pipeline

What runs in CI

Every push and merge request triggers the following:

StepCommandPurpose
Format checkcargo fmt --checkEnforce consistent formatting
Lintcargo clippy -- -D warningsCatch common mistakes and style issues
Unit testscargo testAll per-crate unit tests
Integration testscargo test --features integrationFull-stack EST protocol tests
Build (release)cargo build --releaseVerify release compilation succeeds
Documentationcargo doc --no-depsVerify rustdoc builds without warnings

What requires manual or environment-specific testing

These tests cannot run in a standard CI runner and must be performed during development or in dedicated test environments:

TestReasonHow to run
Hardware HSM signingRequires physical HSM hardwareConnect a YubiHSM 2 or Luna, run cargo test -p kipuka-hsm --features hsm-hardware
PostgreSQL integrationRequires a running PostgreSQL instancedocker compose --profile postgres up -d && cargo test --features integration-postgres
MariaDB integrationRequires a running MariaDB instancedocker compose --profile mariadb up -d && cargo test --features integration-mariadb
Dogtag PKI back-endRequires a running Dogtag instanceDeploy Dogtag in a container, configure connection in test config
GSSAPI authenticationRequires a KDC (FreeIPA or AD)Set up FreeIPA in a container, create service principal, run GSSAPI tests
NIAP compliance auditManual review against Protection ProfileFollow the checklist in docs/compliance/niap.md

Integration testing with idm-ci / Beaker

For full end-to-end testing of kipuka integrated with FreeIPA (IPA-to-kipuka certificate enrollment flows), use the idm-ci framework or Beaker test infrastructure.

idm-ci

idm-ci provisions multi-host test environments with FreeIPA servers, kipuka instances, and client machines. Tests exercise the complete enrollment lifecycle:

  1. FreeIPA issues a Kerberos ticket to the client
  2. Client authenticates to kipuka using GSSAPI
  3. kipuka maps the Kerberos principal to a certificate subject
  4. kipuka issues a certificate
  5. Client installs the certificate and uses it for mTLS re-enrollment

These tests are defined outside the kipuka repository and are triggered by the IdM CI infrastructure.

Local smoke test with FreeIPA container

For a lightweight local approximation:

# Start FreeIPA in a container
podman run -d --name freeipa \
  -h ipa.example.test \
  -p 389:389 -p 443:443 -p 88:88 -p 464:464 \
  quay.io/freeipa/freeipa-server:latest \
  ipa-server-install --unattended \
    --realm EXAMPLE.TEST \
    --domain example.test \
    --ds-password Secret123 \
    --admin-password Secret123

# Wait for installation to complete (5-10 minutes)
podman logs -f freeipa

# Create a service principal for kipuka
podman exec freeipa kinit admin <<< "Secret123"
podman exec freeipa ipa service-add HTTP/kipuka.example.test
podman exec freeipa ipa-getkeytab \
  -s ipa.example.test \
  -p HTTP/kipuka.example.test \
  -k /tmp/kipuka.keytab

# Copy the keytab out
podman cp freeipa:/tmp/kipuka.keytab ./kipuka.keytab

Then configure kipuka’s [gssapi] section to point to the extracted keytab and run enrollment tests using curl --negotiate.

Test data cleanup

Test runs that create database state (OTP tokens, certificates, audit entries) use temporary SQLite databases by default. To clean up after manual testing against a persistent database:

# Delete the development SQLite database
rm -f kipuka-dev.db

# For PostgreSQL, drop and recreate the database
psql -U kipuka -h localhost -c "DROP DATABASE kipuka; CREATE DATABASE kipuka;"

Re-run migrations after cleanup:

cargo run -- migrate --config kipuka.toml

Database Migrations

kipuka manages its database schema through sequential migration files. Every schema change is expressed as a migration that runs exactly once on each database, tracked by a migration history table.

Migration file layout

Migrations live under migrations/ with one subdirectory per supported database backend:

migrations/
  sqlite/
    0001_initial_schema.sql
    0002_add_audit_log.sql
    0003_add_cert_inventory.sql
  postgres/
    0001_initial_schema.sql
    0002_add_audit_log.sql
    0003_add_cert_inventory.sql
  mariadb/
    0001_initial_schema.sql
    0002_add_audit_log.sql
    0003_add_cert_inventory.sql

Naming convention

Migration files follow the pattern NNNN_description.sql where:

  • NNNN is a zero-padded sequential number starting at 0001
  • description is a lowercase, underscore-separated summary of the change
  • The .sql extension is required

Examples:

0004_add_otp_lockout_columns.sql
0005_create_ha_state_table.sql
0006_add_label_to_certs.sql

The numeric prefix determines execution order. kipuka runs migrations in ascending order and skips any that have already been applied (tracked in the _sqlx_migrations table).

Running migrations

Automatic migration on startup

When auto_migrate = true in the [db] configuration section, kipuka applies pending migrations automatically when the server starts:

[db]
url = "sqlite:///var/lib/kipuka/kipuka.db?mode=rwc"
auto_migrate = true

This is convenient for development but may not be appropriate for production environments where schema changes require review and approval.

Explicit migration command

Run migrations manually using the migrate subcommand:

kipuka migrate --config kipuka.toml

Output:

Applied 0001_initial_schema (23ms)
Applied 0002_add_audit_log (11ms)
Applied 0003_add_cert_inventory (15ms)
3 migrations applied successfully

If all migrations have already been applied:

No pending migrations

Checking migration status

View which migrations have been applied:

kipuka migrate --config kipuka.toml --status

Output:

Migration                        Applied At
0001_initial_schema              2026-06-20T10:00:00Z
0002_add_audit_log               2026-06-20T10:00:00Z
0003_add_cert_inventory          2026-06-20T10:00:01Z
0004_add_otp_lockout_columns     (pending)

The three-backend rule

Every migration must have counterparts for all three database backends. A migration that adds a column to a table in SQLite must also add that column in PostgreSQL and MariaDB. This ensures that kipuka can be deployed against any supported backend without schema drift.

The migration runner selects the correct subdirectory based on the database URL scheme:

URL schemeMigration directory
sqlite://migrations/sqlite/
postgres:// or postgresql://migrations/postgres/
mysql:// or mariadb://migrations/mariadb/

Creating a new migration

Step 1: Choose a descriptive name

Pick a name that describes the change, not the ticket number:

Good:  0007_add_gssapi_principal_mapping.sql
Bad:   0007_issue_42.sql

Step 2: Write the SQL for each backend

Create three files with the same sequence number and description:

touch migrations/sqlite/0007_add_gssapi_principal_mapping.sql
touch migrations/postgres/0007_add_gssapi_principal_mapping.sql
touch migrations/mariadb/0007_add_gssapi_principal_mapping.sql

Each file contains the DDL for that specific backend.

SQLite example

-- migrations/sqlite/0007_add_gssapi_principal_mapping.sql

CREATE TABLE IF NOT EXISTS gssapi_mappings (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    principal   TEXT NOT NULL UNIQUE,
    subject_dn  TEXT NOT NULL,
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);

CREATE INDEX idx_gssapi_mappings_principal ON gssapi_mappings(principal);

PostgreSQL example

-- migrations/postgres/0007_add_gssapi_principal_mapping.sql

CREATE TABLE IF NOT EXISTS gssapi_mappings (
    id          SERIAL PRIMARY KEY,
    principal   TEXT NOT NULL UNIQUE,
    subject_dn  TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_gssapi_mappings_principal ON gssapi_mappings(principal);

MariaDB example

-- migrations/mariadb/0007_add_gssapi_principal_mapping.sql

CREATE TABLE IF NOT EXISTS gssapi_mappings (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    principal   VARCHAR(512) NOT NULL UNIQUE,
    subject_dn  VARCHAR(1024) NOT NULL,
    created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE INDEX idx_gssapi_mappings_principal ON gssapi_mappings(principal);

Step 3: Update sqlx query metadata

After adding migrations, regenerate the sqlx offline query data so that compile-time query checking works without a live database:

cargo sqlx prepare --workspace

This updates the .sqlx/ directory with query metadata for all three backends.

Schema differences between backends

While the logical schema is identical across backends, SQL syntax differences require backend-specific migration files.

Type mapping

Logical typeSQLitePostgreSQLMariaDB
Auto-increment PKINTEGER PRIMARY KEY AUTOINCREMENTSERIAL PRIMARY KEYINT AUTO_INCREMENT PRIMARY KEY
TimestampTEXT (ISO 8601 strings)TIMESTAMPTZTIMESTAMP
Variable textTEXTTEXTVARCHAR(n) or TEXT
BooleanINTEGER (0/1)BOOLEANTINYINT(1)
Binary dataBLOBBYTEABLOB

Default value expressions

BackendCurrent timestampUUID generation
SQLitestrftime('%Y-%m-%dT%H:%M:%SZ', 'now')Application-generated
PostgreSQLNOW()gen_random_uuid()
MariaDBCURRENT_TIMESTAMPUUID()

Feature availability

  • PostgreSQL supports ON CONFLICT DO UPDATE (upsert) natively.
  • SQLite supports INSERT OR REPLACE and ON CONFLICT (3.24+).
  • MariaDB uses INSERT ... ON DUPLICATE KEY UPDATE.

When writing migrations that use upsert-like behavior, use the backend-appropriate syntax.

Rules for migration safety

Never modify a released migration

Once a migration has been applied to any environment (including other developers’ local databases), it is immutable. To change an existing table:

  1. Create a new migration with the next sequence number
  2. Use ALTER TABLE to modify the schema
  3. Provide the new migration for all three backends

Additive changes only

Prefer adding columns, tables, and indexes. Avoid dropping columns or tables unless absolutely necessary. If a column is no longer used:

  1. Stop writing to it in the application code
  2. After a release cycle, add a migration to drop the column

Handle NULL for new columns

When adding a column to an existing table, either provide a DEFAULT value or allow NULL. Existing rows cannot retroactively satisfy a NOT NULL constraint without a default:

-- Correct: new column with a default
ALTER TABLE otps ADD COLUMN locked_until TEXT DEFAULT NULL;

-- Incorrect: will fail if the table has existing rows
ALTER TABLE otps ADD COLUMN locked_until TEXT NOT NULL;

Test data migration

If a migration transforms existing data (not just schema), include the data transformation in the same migration file:

-- Add new column
ALTER TABLE audit_log ADD COLUMN auth_method TEXT DEFAULT 'unknown';

-- Backfill existing rows
UPDATE audit_log SET auth_method = 'mtls' WHERE auth_method = 'unknown';

Testing migrations against all backends

Before committing a new migration, verify it applies cleanly against all three database backends:

# SQLite (in-memory)
cargo run -- migrate --config test-sqlite.toml

# PostgreSQL
docker compose --profile postgres up -d
cargo run -- migrate --config test-postgres.toml

# MariaDB
docker compose --profile mariadb up -d
cargo run -- migrate --config test-mariadb.toml

The CI pipeline runs migrations against SQLite automatically. PostgreSQL and MariaDB migration tests require the corresponding Compose profiles and are part of the extended test suite.

Rollback strategy

kipuka migrations are forward-only. There is no built-in migrate down command. This is a deliberate design choice: automated rollback of DDL changes is unreliable in production (data loss, constraint violations, transaction semantics vary by backend).

Manual rollback procedure

If a migration must be undone:

  1. Write a new forward migration that reverses the change:

    -- 0008_revert_gssapi_mappings.sql
    DROP TABLE IF EXISTS gssapi_mappings;
    
  2. Apply it normally:

    kipuka migrate --config kipuka.toml
    

Emergency rollback

In an emergency where the server cannot start due to a bad migration:

  1. Restore the database from backup
  2. Remove the problematic migration files from migrations/
  3. Restart kipuka

For PostgreSQL and MariaDB, point-in-time recovery (PITR) can restore the database to the moment before the migration ran. For SQLite, restore from a filesystem-level backup.

Backup before migrating

Always back up the database before applying migrations in production:

# SQLite
cp /var/lib/kipuka/kipuka.db /var/lib/kipuka/kipuka.db.bak

# PostgreSQL
pg_dump -U kipuka kipuka > kipuka-backup.sql

# MariaDB
mysqldump -u kipuka -p kipuka > kipuka-backup.sql

Contributing

kipuka welcomes contributions. This document covers the license, code style, commit conventions, security invariants, and how to report vulnerabilities.

License

kipuka is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). The full license text is in the LICENSE file at the repository root.

Source: https://codeberg.org/czinda/kipuka

Contribution licensing

kipuka uses an inbound = outbound contribution model. By submitting a patch, you agree that your contribution is licensed under the same GPL-3.0-or-later terms as the rest of the project.

Developer Certificate of Origin (DCO)

All commits must include a Signed-off-by trailer certifying that you have the right to submit the contribution under the project’s license. This follows the Developer Certificate of Origin (DCO v1.1).

Add it automatically with git commit -s:

git commit -s -m "Add OTP lockout duration configuration"

This appends a line like:

Signed-off-by: Your Name <[email protected]>

Commits without a valid Signed-off-by line will be rejected.

Code style

Formatting

All Rust code must pass cargo fmt with no modifications:

cargo fmt --check

Format before committing:

cargo fmt

Linting

All code must compile cleanly under cargo clippy with warnings treated as errors:

cargo clippy -- -D warnings

Fix clippy warnings before submitting. If a specific lint is intentionally suppressed, add an #[allow(...)] attribute with a comment explaining why:

#![allow(unused)]
fn main() {
// Clippy suggests using `map_or`, but the explicit match is clearer
// for this error-handling path.
#[allow(clippy::manual_map)]
fn resolve_ca(&self, label: &str) -> Option<&CaConfig> {
    // ...
}
}

Rust edition

kipuka uses Rust edition 2021. Do not use unstable features or nightly-only APIs.

Commit message conventions

Follow a conventional commit style:

<type>: <short summary>

<optional body explaining what and why>

Signed-off-by: Your Name <[email protected]>

Types

TypeUse for
featNew functionality visible to users or API consumers
fixBug fix
refactorCode restructuring with no behavior change
testAdding or updating tests
docsDocumentation changes
choreBuild system, CI, dependency updates
securitySecurity-related fixes (see disclosure process below)

Examples

feat: add GSSAPI principal-to-subject mapping

Administrators can now define explicit mappings from Kerberos principals
to X.509 subject DNs in kipuka.toml under [gssapi.principal_mapping].
A default template is also supported for unmapped principals.

Signed-off-by: Alice Engineer <[email protected]>
fix: prevent OTP reuse after max_uses reached

The use_count check was off-by-one, allowing one extra use beyond
max_uses.  This commit corrects the comparison to use strict less-than.

Signed-off-by: Bob Developer <[email protected]>

Scope

Keep commits focused on a single logical change. A commit that adds a feature, fixes a bug, and reformats unrelated code should be split into three commits.

Security invariants

The following invariants are critical to kipuka’s security posture. Any patch that weakens or removes these protections will be rejected.

OTP values are never stored in plaintext

OTP tokens are hashed with argon2id (or the configured hash algorithm) immediately upon generation. The raw token is returned to the administrator exactly once in the HTTP response. Only the hash is persisted to the database.

#![allow(unused)]
fn main() {
// Correct: hash before storage
let hash = argon2::hash_encoded(otp.as_bytes(), &salt, &config)?;
db::store_otp_hash(entity_id, &hash).await?;

// WRONG: never store the raw OTP
// db::store_otp(entity_id, &otp).await?;
}

CA private keys are zeroized on drop

All types that hold CA private key material implement Zeroize and ZeroizeOnDrop from the zeroize crate. This ensures that key bytes are overwritten with zeros when the value goes out of scope, preventing residual key material in freed memory.

#![allow(unused)]
fn main() {
use zeroize::{Zeroize, ZeroizeOnDrop};

#[derive(Zeroize, ZeroizeOnDrop)]
struct CaPrivateKey {
    bytes: Vec<u8>,
}
}

Never use std::mem::forget or ManuallyDrop on types containing key material.

Timing-safe comparisons for all authentication checks

All authentication comparisons (OTP validation, bearer token checks, HMAC verification) use constant-time operations from the subtle crate. This prevents timing side-channel attacks that could leak information about valid credentials.

#![allow(unused)]
fn main() {
use subtle::ConstantTimeEq;

fn verify_token(provided: &[u8], expected: &[u8]) -> bool {
    provided.ct_eq(expected).into()
}
}

Never use == for comparing secrets, tokens, or hashes.

CSPRNG for serial numbers

Certificate serial numbers are generated using OsRng from the rand crate, which delegates to the operating system’s cryptographically secure random number generator (/dev/urandom on Linux, CryptGenRandom on Windows).

#![allow(unused)]
fn main() {
use rand::rngs::OsRng;
use rand::RngCore;

fn generate_serial() -> [u8; 20] {
    let mut serial = [0u8; 20];
    OsRng.fill_bytes(&mut serial);
    serial[0] &= 0x7f; // Ensure positive (RFC 5280)
    serial
}
}

Never use rand::thread_rng(), rand::random(), or any non-CSPRNG source for serial numbers, nonces, or key material. Non-cryptographic PRNGs are predictable and can lead to serial number collisions or key compromise.

No logging of sensitive material

The following data must never appear in log output at any verbosity level, including RUST_LOG=trace:

  • CA private keys or key bytes
  • OTP token values (raw or hashed)
  • CSR private keys
  • HSM PINs or passwords
  • Bearer tokens
  • TLS session keys

When logging certificate-related operations, log only the certificate fingerprint, subject DN, serial number, and outcome. When logging OTP operations, log only the entity ID and outcome (success/failure).

#![allow(unused)]
fn main() {
// Correct: log the fingerprint, not the key
tracing::info!(
    entity_id = %entity_id,
    fingerprint = %cert.fingerprint_sha256(),
    "Certificate issued"
);

// WRONG: never log key material
// tracing::debug!(key = ?ca_key, "Signing with CA key");
}

If you are unsure whether a value is sensitive, treat it as sensitive.

Reporting security vulnerabilities

Do not open a public issue for security vulnerabilities.

Report vulnerabilities privately by emailing [email protected] with:

  1. A description of the vulnerability
  2. Steps to reproduce
  3. Affected versions (if known)
  4. Any suggested fix (optional)

You will receive an acknowledgment within 48 hours and a detailed response within 7 business days. Security fixes are developed privately and disclosed after a patch is available.

If you prefer encrypted communication, request a PGP key in your initial email.

Submitting patches

Workflow

  1. Fork the repository on Codeberg

  2. Create a feature branch from main

  3. Make your changes (following the conventions above)

  4. Run the full check suite:

    cargo fmt --check
    cargo clippy -- -D warnings
    cargo test
    cargo test --features integration
    
  5. Commit with Signed-off-by (use git commit -s)

  6. Open a pull request against main

Pull request checklist

Before requesting review, verify:

  • All commits have Signed-off-by trailers
  • cargo fmt --check passes
  • cargo clippy -- -D warnings passes
  • cargo test passes
  • cargo test --features integration passes
  • New database migrations exist for all three backends (if applicable)
  • Security invariants listed above are preserved
  • No sensitive data in log statements, comments, or test fixtures

Review process

All pull requests require at least one maintainer review. Security-sensitive changes require two reviews. CI must pass before merge.

Feature requests and bug reports

File issues on the Codeberg issue tracker:

https://codeberg.org/czinda/kipuka/issues

For feature requests, describe the use case and expected behavior. For bugs, include:

  • kipuka version (kipuka --version)
  • Operating system and architecture
  • Database backend and version
  • Steps to reproduce
  • Expected vs. actual behavior
  • Relevant log output (with sensitive data redacted)