Config-driven PKI ceremony tool -- zero dependencies, airgap-safe, post-quantum ready.
A single static binary that generates your entire PKI hierarchy from a YAML config file. No OpenSSL runtime required. No system dependencies. Download it, write your config, run the ceremony on an airgapped machine, and walk away with encrypted keys, signed certificates, paper backups, and deployment-ready archives. Supports classical RSA-4096 and Ed25519 alongside post-quantum ML-DSA-87 and ML-KEM-1024 (NIST FIPS 203/204, security level 5) via per-cert algorithm selection.
Copyright Hedonistic, LLC. Built for internal use and released as open source because PKI tooling should not require a week of OpenSSL incantations.
- Config-driven: Describe your entire PKI hierarchy in YAML or JSON -- every CA, intermediate, and leaf -- as a flat list with parent references. The tool topologically sorts the hierarchy and builds certificates in the correct order.
- Zero system dependencies: Single static binary. No OpenSSL, no GnuTLS, no shared libraries. Copy it to a USB stick and run it anywhere.
- Airgap-safe: Runs fully offline. No network calls, no telemetry, no update checks. Designed for ceremony machines that never touch a network.
- Post-quantum ready: ML-DSA-87 digital signatures and ML-KEM-1024 key
encapsulation (NIST FIPS 203/204, security level 5). Select PQ algorithms per-cert
via the
algorithmfield, or generate parallel classical + PQ keys withparallel_keys. - Paper backups: Barcode-based paper backup generation (QR, Aztec, PDF417) with multi-part splitting for disaster recovery. Print it, laminate it, lock it in a safe.
- Deployment packages: Automated classification of ceremony outputs into deployment-ready tar.gz archives (offline keys, online keys, public trust bundle).
- Memory hardened: All private keys encrypted in memory with ChaCha20-Poly1305 using an ephemeral vault key from OS CSPRNG. Memory pages mlock'd (Linux). Core dumps disabled. All sensitive memory zeroized on drop. Panic = abort.
- Encrypted on disk: Private keys written with PKCS#8 + AES-256-CBC encryption. Nothing leaves the binary unencrypted.
- Passphrase vault: After a ceremony, optionally save all passphrases to an
AES-256-GCM encrypted vault file for transfer to a password manager. Decrypt
later with
vault-decrypt.
# 1. Download the binary for your platform
curl -LO https://github.com/hedonistic-io/hedonistic-pki/releases/latest/download/hedonistic-pki-linux-x86_64
chmod +x hedonistic-pki-linux-x86_64
# 2. Write your ceremony config
cat > ceremony.yaml << 'EOF'
name: acme-pki
organization: "Acme Corp"
passphrases:
min_length: 16
hierarchy:
- name: root-ca
cn: "Acme Corp Root CA"
cert_type: root
algorithm: rsa_4096
validity:
years: 20
offline: true
subject:
country: US
organization: "Acme Corp"
organizational_unit: "Certificate Authority"
- name: intermediate-ca
cn: "Acme Corp Intermediate CA"
cert_type: intermediate
parent: root-ca
algorithm: rsa_4096
validity:
years: 10
pathlen: 0
subject:
country: US
organization: "Acme Corp"
- name: code-signing
cn: "Acme Corp Code Signing"
cert_type: leaf
parent: intermediate-ca
algorithm: rsa_4096
validity:
years: 2
extensions:
extended_key_usage: [codeSigning]
subject:
organizational_unit: Engineering
EOF
# 3. Run the ceremony (dry run first to validate)
./hedonistic-pki-linux-x86_64 ceremony --config ceremony.yaml --dry-run
# 4. Run for real
./hedonistic-pki-linux-x86_64 ceremony --config ceremony.yamlPre-built static binaries are available for Linux (x86_64, aarch64) and macOS (Apple Silicon):
# Linux x86_64
curl -LO https://github.com/hedonistic-io/hedonistic-pki/releases/latest/download/hedonistic-pki-linux-x86_64
# Linux aarch64
curl -LO https://github.com/hedonistic-io/hedonistic-pki/releases/latest/download/hedonistic-pki-linux-arm64
# macOS Apple Silicon
curl -LO https://github.com/hedonistic-io/hedonistic-pki/releases/latest/download/hedonistic-pki-darwin-arm64
chmod +x hedonistic-pki-*Requires Rust 1.93+ (2024 edition):
git clone https://github.com/hedonistic-io/hedonistic-pki.git
cd hedonistic-pki
cargo build --release
# Binary: target/release/hedonistic-pkiFor cross-compilation to Linux from macOS (requires cross-compilation toolchain):
./build-linux.shThe primary command. Reads a YAML or JSON config that describes your desired PKI hierarchy and executes the full ceremony: key generation, certificate signing, post-quantum key generation, paper backup, and deployment packaging.
hedonistic-pki ceremony --config ceremony.yaml
# Override output directory
hedonistic-pki ceremony --config ceremony.yaml --output /mnt/usb/pki
# Dry run -- validate config and show the planned hierarchy
hedonistic-pki ceremony --config ceremony.yaml --dry-run
# Disable paper backup generation
hedonistic-pki ceremony --config ceremony.yaml --no-paper-backup
# Disable deployment archive generation
hedonistic-pki ceremony --config ceremony.yaml --no-deployDuring the ceremony, you will be prompted interactively for passphrases. Certs
sharing the same parent are automatically grouped so you only enter one passphrase
per group. Certs with no_passphrase: true are skipped. Passphrases are encrypted
in the memory vault immediately after input and never stored in plaintext.
Legacy command for simple three-tier PKI generation without a config file. Generates a Root CA, Intermediate CA, and Code Signing certificate with sensible defaults.
hedonistic-pki generate --output /mnt/usb/pki
# Custom minimum passphrase length (default: 16)
hedonistic-pki generate --output /mnt/usb/pki --min-passphrase-length 24Output structure:
/mnt/usb/pki/
root-ca/
root-ca.crt # Root CA certificate (20yr, RSA-4096)
root-ca.key # Root CA private key (PKCS#8 encrypted)
intermediate-ca/
intermediate-ca.crt # Intermediate CA certificate (10yr)
intermediate-ca.key # Intermediate CA private key (PKCS#8 encrypted)
chain.crt # Full chain (intermediate + root)
code-signing/
code-signing.crt # Code signing certificate (2yr)
code-signing.key # Code signing private key (PKCS#8 encrypted)
pq/
root-ca.vk # ML-DSA-87 verification key
root-ca.sk # ML-DSA-87 signing key
intermediate-ca.vk # ML-DSA-87 verification key
intermediate-ca.sk # ML-DSA-87 signing key
code-signing.vk # ML-DSA-87 verification key
code-signing.sk # ML-DSA-87 signing key
code-signing.ek # ML-KEM-1024 encapsulation key
code-signing.dk # ML-KEM-1024 decapsulation key
manifest.json # PQ key manifest (hex-encoded public keys)
*.endorsement.sig # PQ cross-signing chain
Generate a completely new PKI chain, revoke the old chain via CRL, and prepare a self-recompilation package so the binary can be rebuilt with the new CA embedded.
hedonistic-pki rekey --old-pki /mnt/usb/pki --output /mnt/usb/pki-newThis command:
- Generates a fresh PKI chain (identical to
generate) - Creates a Certificate Revocation List (CRL) for the old chain
- Extracts the embedded source code and writes a recompilation script
You will be prompted for the old Root CA passphrase (to sign the CRL) and three new passphrases for the replacement chain.
Display binary metadata and provide instructions for verifying the binary's CMS signature against the certificate chain.
hedonistic-pki verifyGenerate a paper backup from an existing PKI directory. Produces an HTML document with barcodes encoding the private keys, suitable for printing and archival storage.
hedonistic-pki paper-backup --pki-dir /mnt/usb/pki
# Custom output path
hedonistic-pki paper-backup --pki-dir /mnt/usb/pki --output backup.htmlDecrypt an offline passphrase vault and display all stored passphrases. After a ceremony,
copy the ceremony-vault.enc file to a networked machine and use this command to transfer
passphrases into 1Password or another password manager.
hedonistic-pki vault-decrypt --vault ./pki/ceremony-vault.encYou will be prompted for the master password you set during the ceremony. Passphrases are printed to stderr. Clear your terminal history afterward and securely delete the vault file:
shred -u ceremony-vault.enc # LinuxExtract the encrypted source code embedded in the binary. Useful for auditing or recompilation.
hedonistic-pki extract-source --output ./source --key /mnt/usb/pki/code-signing/code-signing.keyThe ceremony config file is YAML or JSON. Every field is documented below.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | -- | Internal name for this ceremony |
organization |
string | yes | -- | Organization name embedded in certificate subjects |
output_dir |
string | no | null |
Base output directory (overridable via --output CLI flag) |
passphrases |
object | no | see below | Passphrase policy settings |
hierarchy |
array | yes | -- | Flat list of certificate specs (see CertSpec below) |
paper_backup |
object | no | null |
Paper backup settings (see below) |
deployment |
object | no | null |
Deployment archive settings (see below) |
| Field | Type | Default | Description |
|---|---|---|---|
min_length |
integer | 16 |
Minimum passphrase length enforced during ceremony |
manager_hint |
string | null |
Hint displayed during passphrase prompts (e.g., "use 1Password") |
Each entry in the hierarchy array describes one certificate:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | -- | Internal identifier, used as parent reference by children |
cn |
string | yes | -- | Common Name for the X.509 subject |
cert_type |
enum | yes | -- | One of: root, intermediate, sub_ca, leaf |
parent |
string | no | null |
name of the parent CA that signs this cert. Required for all non-root types. Root certs must not have a parent. |
algorithm |
enum | no | rsa_4096 |
Key algorithm: rsa_4096, ed25519, ml_dsa_87, ml_kem_1024 |
hash |
enum | no | null |
Hash algorithm: sha256, sha384, sha512 |
validity |
object | yes | -- | Must contain years (integer) and/or days (integer) |
pathlen |
integer | no | null |
X.509 path length constraint. Only valid on CA types, not leaves. |
offline |
boolean | no | false |
Mark this cert as offline (affects passphrase grouping and deployment classification) |
no_passphrase |
boolean | no | false |
Skip passphrase for this key (e.g., CI/CD service keys) |
parallel_keys |
array | no | [] |
Additional algorithms to generate alongside the primary. E.g., [ed25519, ml_dsa_87] generates those keys in parallel with the primary algorithm. |
extensions |
object | no | null |
X.509 extension overrides (see below) |
subject |
object | no | null |
Additional X.509 subject fields (see below) |
tags |
array | no | [] |
Arbitrary string tags for metadata |
deploy_to |
string | no | null |
Deployment target hint (e.g., github-actions) |
| Field | Type | Default | Description |
|---|---|---|---|
key_usage |
array | [] |
X.509 Key Usage values (e.g., [keyCertSign, cRLSign]) |
extended_key_usage |
array | [] |
Extended Key Usage values (e.g., [codeSigning, serverAuth, clientAuth]) |
basic_constraints_ca |
boolean | null |
Explicit CA flag override |
| Field | Type | Default | Description |
|---|---|---|---|
country |
string | null |
Two-letter country code |
organization |
string | null |
Organization name (overrides top-level organization for this cert) |
organizational_unit |
string | null |
Organizational unit |
| Field | Type | Default | Description |
|---|---|---|---|
formats |
array | [] |
Barcode formats to generate: qr, aztec, pdf417 |
output |
string | "html" |
Output format identifier |
include_pem |
boolean | true |
Include PEM-encoded key text alongside barcodes |
| Field | Type | Description |
|---|---|---|
packages |
array | List of deployment packages to create |
Each package (DeployPackage):
| Field | Type | Description |
|---|---|---|
name |
string | Archive name (produces deploy/<name>.tar.gz) |
target |
string | Target directory path for the archive |
patterns |
array | Glob patterns for files to include (e.g., ["*.crt", "*.pub"]) |
See examples/quickstart.yaml for a complete working config.
There is no top-level post-quantum configuration block. PQ algorithms are selected
per-cert via the algorithm field:
hierarchy:
- name: pq-signer
cn: "PQ Code Signer"
cert_type: leaf
parent: intermediate-ca
algorithm: ml_dsa_87
validity:
years: 2To generate both classical and PQ keys for the same cert, use parallel_keys:
hierarchy:
- name: hybrid-root
cn: "Hybrid Root CA"
cert_type: root
algorithm: rsa_4096
validity:
years: 20
parallel_keys:
- ed25519
- ml_dsa_87Passphrase grouping is automatic -- there is no config field for it. The rules are:
- Certs with
no_passphrase: trueget no passphrase - Each root CA gets its own passphrase
- Non-root certs sharing the same parent share a passphrase
This means you are prompted once per root CA and once per group of siblings, rather than once per cert.
All private key material is encrypted in memory immediately after generation. The
vault uses ChaCha20-Poly1305 with a 256-bit ephemeral key sourced from the OS CSPRNG
(getrandom). The vault key exists only in process memory and is never written to
disk.
When a key needs to be used (e.g., to sign a child certificate), it is decrypted
into a Zeroizing<Vec<u8>> buffer, used, and immediately zeroized. At no point do
plaintext private keys exist in unprotected memory.
On Linux targets, the binary applies additional protections at startup:
- mlock: All current and future memory pages are locked (
mlockall), preventing the kernel from swapping sensitive data to disk. - Core dumps disabled:
prctl(PR_SET_DUMPABLE, 0)prevents core dumps that could contain key material. - panic=abort: The binary aborts on panic rather than unwinding the stack, which prevents stack traces from leaking sensitive data.
On non-Linux platforms, these protections are noted as unavailable but the vault encryption and zeroization still apply.
The tool generates post-quantum keys using NIST-standardized algorithms:
- ML-DSA-87 (FIPS 204, formerly Dilithium): Digital signatures at security level 5. Used to sign the PQ endorsement chain that parallels the classical X.509 chain.
- ML-KEM-1024 (FIPS 203, formerly Kyber): Key encapsulation at security level 5. Generated for code signing leaves to enable post-quantum encrypted distribution channels.
The classical and post-quantum chains are independent but cross-signed. Verification requires both chains to validate -- this is a hybrid "and" model, not "or".
Private keys are encrypted before writing to disk using PKCS#8 with AES-256-CBC key derivation via scrypt. The passphrase you provide during the ceremony is the only way to decrypt the keys. If OpenSSL is not available on the system, keys are written with restrictive file permissions (0400) as a fallback, and a warning is printed.
The binary embeds an encrypted copy of its own source code, signed with the code signing certificate. This allows the binary to be recompiled with a new CA during rekey ceremonies without needing to trust an external source archive.
Paper backups are generated as self-contained HTML files designed for printing. Each page contains:
- Header: Certificate name, generation timestamp, key fingerprint
- Barcode: The private key material encoded as a barcode (QR, Aztec, or PDF417)
- PEM Text: The full PEM-encoded key printed below the barcode as a fallback
(when
include_pemis true) - Reconstruction Instructions: Step-by-step procedure printed on each page
When multiple formats are specified in paper_backup.formats, each key gets a
barcode in each requested format. Keys are split across multiple barcodes when they
exceed a single barcode's capacity. Each part is labeled (e.g., "Part 1 of 3") and
includes a SHA-256 checksum of the complete key for verification after reassembly.
- Scan all barcode parts for a given key
- Concatenate the parts in order
- Verify the SHA-256 checksum matches the value printed on each page
- The result is the PEM-encoded private key
If barcode scanning fails, manually type the PEM text from the printed backup.
- Rust 1.93+ (2024 edition)
- No system dependencies required for the build itself
cargo buildcargo build --releaseThe release profile is optimized for size (opt-level = "z") with LTO enabled,
single codegen unit, symbols stripped, and panic=abort.
The included build-linux.sh script handles cross-compilation to
x86_64-unknown-linux-gnu from macOS:
./build-linux.shFor other targets, use standard Rust cross-compilation:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslThe build.rs script handles embedding the encrypted source archive into the binary.
It compresses and encrypts the src/ directory at compile time so that the binary is
self-contained for rekey operations.
Apache-2.0. See LICENSE.
Copyright 2026 Hedonistic, LLC. Part of the Hedonistic brand family of tools for infrastructure, security, and AI orchestration.
Post-quantum cryptography powered by the ml-dsa and ml-kem crates implementing NIST FIPS 203/204. Classical certificate generation via rcgen. Memory vault uses chacha20poly1305 from the RustCrypto project.