FreeHSM C

An open-source PKCS#11 v3.2 cryptographic module under Simorgh Labs

View the Project on GitHub afchine1337/freehsm

fhsm-csr — certification requests and roots with a key held in a PKCS#11 module

fhsm-csr creates a key pair inside a PKCS#11 module and uses it to produce PKCS#10 certification requests and self-signed CA certificates. The private key is generated in the module and never leaves it; every signature is made by the module through C_Sign.

The key is a post-quantum composite (ML-DSA-65 + Ed25519) by default, or any of ECDSA P-256 and P-384, RSA-PSS, RSA PKCS#1 v1.5, Ed25519 and ML-DSA-44/65/87. All but the composite are approved in the default nist-approved-only profile, and everything made with them can be checked by OpenSSL and the rest of the ordinary tooling.

For a composite key, read Limitations before relying on it. The composite algorithm is not yet implemented by general-purpose tooling, which constrains what a request produced with one can be used for today.


Synopsis

fhsm-csr keygen --label NAME [--alg ALG] [--module PATH] [--slot N]
fhsm-csr csr    --label NAME --subject DN [--out FILE] [--pem] [--module PATH] [--slot N]
fhsm-csr root   --label NAME --subject DN [--days N] [--serial N] [--out FILE] [--pem] ...

fhsm-ca  issue  --label NAME --ca-cert FILE --csr FILE [--subject DN] [--san LIST] [--crl-url URL]... [--days N] ...
fhsm-ca  revoke --db FILE --serial HEX [--reason NAME] [--date WHEN]
fhsm-ca  crl    --label NAME --ca-cert FILE --db FILE [--days N] [--out FILE] [--pem]

The PIN is read from the FHSM_PIN environment variable.


Commands

keygen — create a key pair

Generates a key pair as two token objects sharing one label: a public key and a private key, the private one sensitive and for signing only. --alg chooses the algorithm, once: every other command — here, in fhsm-ca and in fhsm-sign — reads it off the key and signs with the mechanism that goes with it, so none of them takes an --alg of its own.

--alg Key Signs with In X.509
composite (default) ML-DSA-65 + Ed25519 CKM_COMPOSITE_MLDSA65_ED25519 id-MLDSA65-Ed25519-SHA512
ecdsa-p256 EC P-256 CKM_ECDSA_SHA256 ecdsa-with-SHA256
ecdsa-p384 EC P-384 CKM_ECDSA_SHA384 ecdsa-with-SHA384
rsa-pss RSA 3072 CKM_SHA256_RSA_PKCS_PSS RSASSA-PSS, SHA-256, MGF1-SHA-256, salt 32
rsa-pkcs1 RSA 3072 CKM_SHA256_RSA_PKCS sha256WithRSAEncryption
ed25519 Ed25519 CKM_EDDSA id-Ed25519
ml-dsa-44, -65, -87 ML-DSA CKM_ML_DSA id-ml-dsa-44/65/87
export FHSM_PIN=…
fhsm-csr keygen --label ca                       # composite
fhsm-csr keygen --label web --alg ecdsa-p256

An RSA key alone does not say which padding it is for, so keygen sets the key’s CKA_ALLOWED_MECHANISMS to the one signing mechanism chosen: the tools read it back, and the module refuses any other. An RSA key made elsewhere, without that attribute, is used with PSS. The public key is read from the standard attributes (CKA_EC_PARAMS and CKA_EC_POINT, CKA_MODULUS and CKA_PUBLIC_EXPONENT, an ML-DSA key’s CKA_VALUE), so keys made by another module, or by another tool, can be used as long as their type is one of these.

A composite key pair cannot be assembled from two existing keys — there is no option to do so, and that is deliberate. draft-ietf-lamps-pq-composite-sigs §3.1 forbids reusing component key material between a composite and a non-composite, or between two composites; the prohibition is met by making the situation unrepresentable rather than by checking for it.

csr — produce a PKCS#10 certification request

fhsm-csr csr --label ca \
             --subject "/C=FR/O=Simorgh Labs/CN=demo.example" \
             --out demo.csr.der

The request is signed by the module over the DER of CertificationRequestInfo.

root — produce a self-signed CA certificate

fhsm-csr root --label ca \
              --subject "/C=FR/O=Simorgh Labs/CN=Simorgh Composite Root" \
              --days 3650 --serial 1 \
              --out ca.der

A v3 certificate with basicConstraints CA:TRUE and keyUsage keyCertSign,cRLSign, both critical, plus a subjectKeyIdentifier computed as SHA-1 of the composite public key (RFC 5280 §4.2.1.2 method 1).


Options

Option Meaning Default
--module PATH PKCS#11 module to load ./libfreehsm.so
--slot N Slot identifier, as reported by C_GetSlotList the one slot holding a token
--label NAME Key label. Required by every command. —
--subject DN Subject, OpenSSL one-line form: /C=FR/O=…/CN=… —
--days N Validity in days (root only) 3650
--serial N Certificate serial (root only), must be > 0 1
--out FILE Output file stdout
--pem PEM instead of DER DER

The PIN, and why there is no --pin

The PIN is read from FHSM_PIN and from nowhere else. Passing --pin prints an explanation and exits non-zero.

A PIN given as a command-line argument is visible in ps to every user on the machine, and it lands in shell history. A tool that offers the convenient insecure option is a tool whose users take it, so the option does not exist.

If a script needs the PIN, keep it in a file readable only by the service account and source it, or pass it through the process environment from a secrets manager:

FHSM_PIN="$(systemd-creds cat fhsm-pin)" fhsm-csr csr --label ca --subject …

Working with another PKCS#11 module

fhsm-csr loads the module at runtime and uses only standard PKCS#11 calls, so it drives any module implementing CKM_COMPOSITE_MLDSA65_ED25519, not only FreeHSM. The composite DER encoding is built inside the tool; the key stays wherever the module keeps it.

fhsm-csr keygen --label ca --module /usr/lib/softhsm/libsofthsm2.so --slot 1

An institution that already owns a hardware HSM can use these tools with it. That is the point of the layering, and it is the one thing a PKI which merely talks to HSMs cannot offer.


A complete example

export FHSM_PIN=…

# 1. a key for the CA, generated inside the module
fhsm-csr keygen --label root-ca

# 2. the CA's own certificate, self-signed, ten years
fhsm-csr root --label root-ca \
              --subject "/C=FR/O=Université Exemple/CN=Exemple Root CA" \
              --days 3650 --out root-ca.crt --pem

# 3. a key and a request for a server
fhsm-csr keygen --label web01
fhsm-csr csr --label web01 \
             --subject "/C=FR/O=Université Exemple/CN=web01.exemple.fr" \
             --out web01.csr --pem

Both outputs can be read with the usual tools:

openssl x509 -in root-ca.crt -text -noout
openssl req  -in web01.csr   -text -noout

Limitations

Everything below is about the composite. Requests and certificates made with the other algorithms are ordinary PKIX, and tests/pki_tools_algs.sh has the openssl command line check each of them.

A composite signature cannot be verified by generally available tooling. openssl req -text and openssl x509 -text will print Unable to load Public Key and, for a certificate, a self-signature warning. This is not a defect in the output. OpenSSL 3.5 has no implementation of Composite ML-DSA — it prints the algorithm as a bare OID because it has no name for it — and cannot verify an algorithm it does not implement.

The consequences are worth being precise about:

Where the specification stands, checked 2026-09-02. draft-ietf-lamps-pq-composite-sigs-19 (revision of 21 April 2026) is in the RFC Editor queue; the state last moved on 26 August 2026 to In Progress (First Edit), IANA is at RFC-Ed-Ack, and no RFC number has been assigned. The draft expires 23 October 2026. It is advancing; it has not published.

The interoperability half of this lifts when the RFC publishes and implementations follow, and nothing in the output format is expected to change.

Not FIPS-approved, and that part does not lift. The mechanism is available in the all-mechanisms profile only, permanently, and for a reason that publication will not alter. §10.2 of the draft is explicit about its own standing:

This guidance is not authoritative and has not been endorsed by US NIST.

What follows it is a design goal — that a composite “be able to be considered FIPS-approved even when one of the component algorithms is not” — about certification, which does not apply here; the reasoning is in README.md under Certification, and the same notice heads docs/AGD_PRE.md and docs/AGD_OPE.md. A mechanism enters nist-approved-only because it is approved, not because a specification argues that it ought to be, so CKM_COMPOSITE_MLDSA65_ED25519 stays outside that profile whatever the RFC Editor does.

This is a stated choice rather than a provisional state, and it is not a limitation of the implementation: the composite signatures produced here match the draft’s own Appendix D vectors byte for byte. See docs/COMPOSITE_SIGS_GAP.md for what conformance was checked against.

Issuance, revocation, CRLs and OCSP live in fhsm-ca and are documented in FHSM_CA.md. fhsm-csr root signs the CA’s own certificate; everything the CA then does to somebody else’s request is the other tool.


Exit codes

Code Meaning
0 Success
1 Usage error, FHSM_PIN unset, or --pin passed
2 Module could not be loaded, or a PKCS#11 call failed
3 No key with that label, or more than one; or a malformed revocation database
4 The request’s signature does not match the key it carries (fhsm-ca issue)
5 That serial is already revoked; the database was left unchanged

Two keys sharing a label is refused rather than resolved by taking the first match: signing with a key the operator did not intend is worse than a command that fails.


Troubleshooting

C_OpenSession failed (0xe0) — CKR_TOKEN_NOT_PRESENT. The slot has no initialised token. Run fhsm-token init first (see FHSM_TOKEN.md), and check FHSM_TOKENS_DIR points where you think it does.

This sentence used to say “or the module’s own provisioning tool”. There was none, so the advice sent the reader looking for something that did not exist — which is how the gap was eventually found.

C_Login failed (0xa0) — wrong PIN. After FHSM_PIN_MAX_FAILED consecutive failures the role locks; see AGD_OPE.md §4.1.

C_GenerateKeyPair failed (0x70) — CKR_MECHANISM_INVALID. The module was built in the nist-approved-only profile, where the composite mechanism is deliberately refused, or the module does not implement it at all.

no public key labelled "x" — run keygen first, or check the label. The tool looks for objects whose CKA_LABEL matches exactly.



See also