Software-key implementations of SignCore.Signer for PKCS#12 (.p12 / .pfx) bundles and PKCS#8 PEM private keys.
Part of the pkcs11ex family. Pair with sign_core to sign PDFs, XMLs, or JWS payloads with filesystem-resident keys.
When to use
- Vendor-issued PKCS#12 bundles (e.g., government tax-authority signing certs)
- Cloud / server deployments where keys live as PEM files alongside certs
- Dev/test environments where standing up a SoftHSM2 instance is overkill
- Migration paths from legacy systems that ship
.p12instead of HSM access
If your production deployment runs against a hardware HSM, use pkcs11ex instead — keep soft_signer out of the dep tree to enforce "no software signing" at the package boundary.
PKCS#12
{:ok, signer} = SoftSigner.PKCS12.load("invoice-signer.p12", password: "...")
{:ok, signed_pdf} =
SignCore.PDF.sign(pdf,
signer: signer,
alg: :PS256,
x5c: SoftSigner.PKCS12.cert_chain(signer) # P12 carries its own chain
)P12 decryption shells out to the openssl pkcs12 CLI — pure-Erlang PKCS#12 decode is fragile across vendor encodings, so we let openssl handle the bytes-to-PEM step. Sign math runs through :public_key.sign/3 with PSS padding for :PS256 or PKCS#1 v1.5 for :RS256.
Errors:
{:error, :bundle_not_found}— file path doesn't exist{:error, {:openssl, "PKCS#12 password incorrect"}}— bad password{:error, {:openssl, msg}}— other openssl failures
PKCS#8 PEM (key + separate cert)
{:ok, signer} =
SoftSigner.PKCS8.load(
key_path: "/keys/legal-proxy.key.pem",
cert_path: "/keys/legal-proxy.cert.pem",
password: "..." # only if the PEM is encrypted
)
{:ok, signed_pdf} =
SignCore.PDF.sign(pdf,
signer: signer,
alg: :PS256,
x5c: SoftSigner.PKCS8.cert_chain(signer)
)Supports:
- Unencrypted PKCS#8 (
-----BEGIN PRIVATE KEY-----) - Encrypted PKCS#8 (
-----BEGIN ENCRYPTED PRIVATE KEY-----) with:password - PKCS#1 RSA (
-----BEGIN RSA PRIVATE KEY-----) — the older format
You can supply key and cert from in-memory PEM strings instead of paths:
{:ok, signer} =
SoftSigner.PKCS8.load(
key_pem: System.get_env("SIGNING_KEY_PEM"),
cert_pem: File.read!("/keys/legal-proxy.cert.pem")
)The cert PEM may contain a single certificate or a chain (leaf first, then intermediates). Errors:
{:error, :missing_key}/{:error, :missing_cert}— neither path nor pem supplied{:error, {:pem_not_found, path}}— supplied path doesn't exist{:error, :no_pem_entries}— file isn't valid PEM{:error, :no_rsa_private_key}— PEM has no key entry{:error, :no_cert_entries}— cert PEM has noCertificateentries{:error, :password_required}— encrypted key, no:passwordsupplied{:error, :wrong_password}— encrypted key, bad password
Why two structs not one
Both SoftSigner.PKCS12 and SoftSigner.PKCS8 produce the same internal shape (%{rsa_key, leaf_der, chain_ders}) and share their defimpl SignCore.Signer logic. They're separate modules because the load contract differs:
- PKCS#12 takes a single file path and a password. The cert chain comes for free.
- PKCS#8 takes separate key + cert sources. The cert chain is whatever the caller supplies.
Keeping them separate makes the type signature of each load function unambiguous and avoids a Boolean opt to switch between modes.
Algorithm support
:PS256— RSASSA-PSS, SHA-256, MGF1-SHA-256, salt length 32 (the JOSE convention):RS256— RSASSA-PKCS1-v1_5, SHA-256
Adding ECDSA / Ed25519 is a small extension to the defimpl block; not implemented yet because the production keys we've encountered are all RSA-2048.
License
Apache 2.0.