An open-source PKCS#11 v3.2 cryptographic module under Simorgh Labs
fhsm-csr — certification requests and roots with a key held in a PKCS#11 modulefhsm-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.
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.
keygen — create a key pairGenerates 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 requestfhsm-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 certificatefhsm-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).
| 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 |
--pinThe 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 …
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.
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
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.
| 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.
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.
docs/COMPOSITE_SIGS_GAP.md — what Composite ML-DSA is, what the module
implements, and the measurements behind both.FHSM_CA.md — issuing, revoking, CRLs and OCSP.docs/AGD_OPE.md — operator guidance for the module itself.draft-ietf-lamps-pq-composite-sigs-19 — the specification.