PKCS#12 (.p12/.pfx) bundle export¶
PKCS#12 export is the server-side-keygen key-delivery path for end-entity certificates. In one operation secsy-pki generates a subject keypair, issues a leaf under a profile, and returns a password-protected PKCS#12 bundle containing the subject private key, the leaf certificate, and the full issuer chain (leaf → intermediate(s) → root).
It exists because some subscribers legitimately need their own private key delivered to them — the two primary consumers are:
- S/MIME (S/MIME e-mail protection): mail clients import a
.p12to sign and decrypt mail. The encryption key in particular is often generated centrally and escrowed so encrypted mail is recoverable. - Device / MDM enrollment (SCEP & EST): an MDM or
provisioning system that cannot run a CSR-based enrollment can be handed a
ready-to-install
.p12.
The invariant: the CA key never leaves the HSM¶
Only the freshly-generated subject key is ever placed in the bundle. The CA signing key lives in the HSM and is used solely as a signer during issuance — it is never marshaled or exported. Concretely, export:
- generates the subject keypair in software, in memory;
- builds and self-signs a PKCS#10 CSR from it (proof of possession);
- issues the leaf through the normal CA manager path (HSM-signed, with the same lint / CAA / CT / name-constraint gates as any other issuance);
- packs the subject key + leaf + issuer chain into the PKCS#12;
- scrubs the in-memory plaintext copy of the subject key.
This is exercised end-to-end against SoftHSM in the acceptance test
(server/internal/pkcs12): the CA key is on the token, the bundle decrypts to
the subject key alone, and the produced .p12 round-trips through both the
go-pkcs12 decoder and openssl pkcs12 -info.
Key types and encoders¶
Key types: ecdsa (default, P-256; -key-bits 384|521 for P-384/P-521) or
rsa (default 3072-bit; minimum 2048). Ed25519 is intentionally unsupported —
PKCS#12 with Ed25519 keys interoperates poorly across common consumers.
Encoders (the PKCS#12 encryption/MAC algorithms):
| Encoder | Algorithms | Reads in |
|---|---|---|
modern (default) |
PBES2 (PBKDF2-HMAC-SHA-256 + AES-256-CBC), HMAC-SHA-256 MAC | OpenSSL 1.1.1+, Java 12+, Windows Server 2019+ |
legacy |
3DES (certs + key), HMAC-SHA-1 MAC | Very old software (OpenSSL -descert parity) |
legacyrc2 |
RC2 (certs) + 3DES (key), HMAC-SHA-1 MAC | Oldest software; needs OpenSSL -legacy provider to read |
Prefer modern and a high-entropy password (e.g. openssl rand -hex 16);
PKCS#12 confidentiality rests entirely on the password. Under
FIPS mode the legacy encoders (3DES/RC2 + SHA-1) are refused and only
modern is permitted.
CLI: secsy-ca export-p12¶
# S/MIME certificate for a mailbox, ECDSA key, modern encoding.
SECSY_P12_PASSWORD='…' \
secsy-ca export-p12 \
-ca corp-issuing-ca \
-profile smime \
-cn "Jane Doe" -email jane@example.com \
-key-type ecdsa \
-out jane.p12
# TLS client certificate for a device, RSA-3072, with SANs.
SECSY_P12_PASSWORD='…' \
secsy-ca export-p12 \
-ca device-ca -profile client \
-cn device-01 -dns device-01.example.com -ip 10.0.0.5 \
-key-type rsa -key-bits 3072 \
-out device-01.p12
The password is sourced (in priority order) from -password-file, -password,
or $SECSY_P12_PASSWORD; sourcing it outside the -password flag keeps it off
the process argument list. Run secsy-ca export-p12 -h for all flags (subject
DN, -dns/-ip/-email/-uri SANs, -encoder, -validity-days, -escrow).
Verify a bundle with OpenSSL:
REST API¶
POST /api/ca/{id}/pkcs12 — gated by the same issue capability as POST
/api/ca/{id}/issue (a platform/tenant issuer role, or a per-CA
SIGN_CERTIFICATE grant), tenant-scoped to the CA's tenant.
// request
{
"profile": "smime",
"common_name": "Jane Doe",
"emails": ["jane@example.com"],
"key_type": "ecdsa",
"encoder": "modern",
"password": "…",
"escrow": false
}
// 201 response (abridged)
{
"serial": "…",
"profile": "smime",
"not_after": "2027-07-03T00:00:00Z",
"pkcs12": "<base64 DER PKCS#12>",
"chain": "-----BEGIN CERTIFICATE----- … (leaf + issuers)",
"key_type": "ecdsa-p256",
"encoder": "modern"
}
The private key is delivered only inside the password-protected pkcs12 field;
it is never returned in the clear.
Console¶
The PKCS#12 page in the operator console drives the same
endpoint: pick a CA and profile, set the subject/SANs, key type, encoder and
password, optionally tick Escrow subject key, and the browser downloads the
.p12 directly.
Optional key escrow (M-of-N)¶
With -escrow (CLI) or "escrow": true (API) the freshly-generated subject key
is additionally escrowed under the configured
M-of-N recovery policy:
its data key is Shamir-split across the recovery agents and wrapped to
their keys, and the escrow envelope is returned (API) or written to
-escrow-out (CLI) for you to store in a break-glass vault. This is the
recommended posture for S/MIME encryption keys, so that encrypted mail
remains recoverable if the subscriber loses their key.
Escrow requires a secret KEK (secret.kek_label) and secret.escrow to be
configured. The escrow envelope is sealed under the CA tenant's KEK and bound to
the certificate serial via the encryption context pkcs12/<serial>; recovery is
the usual dual-control ceremony:
secsy-secret recover -in jane.escrow.json -context "pkcs12/<serial>" \
-agent alice -agent bob # a quorum of recovery agents
The recovered plaintext is the subject private key in PKCS#8 DER.
Auditing & metrics¶
- Audit: every export appends a
cert.pkcs12event (actor, CA, serial, profile/key/encoder detail). When escrow is used a pairedsecret.escrowevent records the break-glass wrapping — an unauditable key-escrow operation is refused. - Metrics: exports increment
secsy_certificates_total{operation="pkcs12"}with the usualsuccess/error/deniedresult label.
See also¶
- Certificate authority — issuance, profiles, CRL/OCSP
- S/MIME e-mail protection — the primary consumer of
.p12delivery - Password / secret encryption — the escrow/recovery layer