Skip to content

Gerege eID — Onboarding to the national CA (L1 CSR → chain)

By law, Gerege must chain to the national root CA — a subordinate CA, not a standalone root. The final hierarchy:

National Root CA (Government HSM)
  └── Gerege CA  (L1)  — the national root signs ONLY this;  pathlen:1
        ├── Gerege Personal Issuing CA      (L2, pathlen:0) → Natural Person cert
        └── Gerege Organization Issuing CA  (L2, pathlen:0) → Legal Person cert

eIDAS naming: Personal = Natural Person, Organization = Legal Person. Two separate L2 Issuing CAs under L1 (separated by purpose/policy) — validated on a real HSM (TestHSMProxyDualIssuingCA).

1. L1 CSR — contents (validated on a real HSM)

TestPrepareL1CSR in internal/crypto produces the L1 CSR with the HSM key (PoP):

Subject:             C=MN, O=Gerege Systems LLC, CN=Gerege eID CA
Public-Key:          RSA-2048  (ceremony July 2048; 4096 ~1 year later)
Requested Extensions:
  Basic Constraints: critical, CA:TRUE, pathlen:1     ← REQUIRED for 2-tier (L1→L2)
  Key Usage:         critical, Certificate Sign, CRL Sign
Signature:           sha256WithRSAEncryption  (proof-of-possession verify OK)

Match the Subject DN to the national CA's naming rules (they may require a specific O/CN/ organizationIdentifier). The above is a recommendation.

2. (!) What to obtain IN WRITING from the national CA

Item Why
pathLenConstraint ≥ 1 The national root decides the pathlen of the final L1 cert (the CSR only requests). If 0, Gerege cannot issue L2 → the 2-tier structure FAILS. Include this in the subordinate CA agreement.
Algorithm / key size RSA-2048 (currently) — must be compatible with their ceremony/CP-CPS
Validity L1 is shorter than the root
DN / naming rules O/CN/organizationIdentifier
CRL/OCSP rules Where AIA/CDP URLs point

3. WHEN to generate the real CSR

The real L1 key is generated at the official key ceremony (July, RSA-2048) with M-of-N custodians, tamper bags, and witnesses — hsm-thales-gerege/docs/eid-issuing-ca-key-ceremony-script.md. Do not use the demo key. During the ceremony, generate the CSR by one of two methods:

A. Go (hsm-proxy, this repo): given the ceremony's key label,

HSMPROXY_E2E_URL=https://<proxy>:8443 HSMPROXY_E2E_CERT=admin.crt HSMPROXY_E2E_KEY=admin.key \
CSR_OUT=gerege-l1.csr  go test ./internal/crypto -run TestPrepareL1CSR -v
# (change the test to match the label to the ceremony key; NOTE that the demo deletes the key)

B. Luna native (cmu, compatible with the ceremony script):

cmu requestcertificate -slot <issuing> -lco \
    -publichandle <ph> -privatehandle <prh> \
    -sha256withrsa -outputfile gerege-l1.csr \
    -cn "Gerege eID CA" -o "Gerege Systems LLC" -c MN

cmu does not put extensions in the CSR — the national root sets pathlen/keyUsage by agreement.

4. After the L1 cert arrives

  1. Import the L1 cert signed by the national root into the Gerege HSM (the cert is public, safe).
  2. Within Gerege, create two separate L2 issuing CAs (each signed by L1):
  3. Personal (Natural Person) — key label e.g. gerege-personal-issuing-ca
  4. Organization (Legal Person) — key label e.g. gerege-org-issuing-ca Each with pathlen:0. On the organization leaf, organizationIdentifier (OID 2.5.4.97, e.g. NTRMN-).
  5. Go server configuration (one server loads both issuing CAs at once):
  6. SMARTID_PKI_CA_PROVIDER=hsmproxy
  7. Personal: SMARTID_HSMPROXY_KEY_LABEL=<Personal L2 key>, SMARTID_HSMPROXY_ISSUING_CERT=<Personal L2 cert>
  8. Organization (optional): SMARTID_HSMPROXY_ORG_KEY_LABEL=<Org L2 key>, SMARTID_HSMPROXY_ORG_ISSUING_CERT=<Org L2 cert>
  9. Automatic selection: by ETSI prefix — PNOMN-… → Personal CA, NTRMN-… → Organization CA (domain.SubjectTypeForEtsi + store.caFor). If Org is not configured, it falls back to Personal.
  10. the full chain (L2+L1) is attached to the leaf; trust anchor = national root.

Validated on a real HSM: - TestHSMProxy3LayerChain — National Root → L1 (pathlen:1) → L2 (pathlen:0) → leaf. - TestHSMProxyDualIssuingCA — L1 → {Personal, Organization} issuing CA → two leaves, two chains verify OK.

Cloud HSM failover (automatic switchover when on-prem Luna goes down)

To prevent enrollment/seal from stopping when the on-prem Luna session drops (slot ... not found / CKR_SESSION_HANDLE_INVALID), you can configure a fallback hsm-proxy (behind a Thales DPoD Luna Cloud HSM). The same CA key (same label) must have been cloned into the DPoD partition via backup/restore (the partition config must be identical to prodpart1 — hsm-thales-gerege/docs/ceremony-rehearsal-results.md).

SMARTID_HSMPROXY_FALLBACK_URL=https://<dpod-proxy>:8443     # Cloud HSM-backed hsm-proxy
SMARTID_HSMPROXY_FALLBACK_CLIENT_CERT=/app/hsmproxy/dpod-client.crt  # if empty, uses the primary's
SMARTID_HSMPROXY_FALLBACK_CLIENT_KEY=/app/hsmproxy/dpod-client.key
SMARTID_HSMPROXY_FALLBACK_SERVER_CA=/app/hsmproxy/dpod-proxy-ca.crt
SMARTID_HSMPROXY_PREFER=primary   # "fallback" puts Cloud FIRST (choose on-prem as failover manually)
  • Behavior: if primary is unreachable or returns 5xx (slot/session) → automatically to fallback; 4xx (wrong label) → no failover. The signature is verified with the issuing CA cert's pubkey (a fallback with the wrong key → fail-closed). It applies to Personal + Organization issuing CA and the e-Seal client alike.
  • Manual switchover: SMARTID_HSMPROXY_PREFER=fallback (restart) — makes Cloud the primary.
  • Tests: TestHSMProxyFailover_* (IssuingCA/PreferFallback/BothDown/4xxNoFailover/SealClientEC).

Enabling the Organization issuing CA in production (in the stage WITHOUT a national root)

The current production Personal CA (eidmongol-issuing-ca-v1) is self-signed (the national L1 does not exist yet). Set up the Organization CA the same way — a new key + self-signed cert in the HSM:

  1. On the HSM host (ceremony — hsm-thales-gerege/docs/eid-issuing-ca-key-ceremony-script.md):
    cmu generatekeypair -slot <issuing> -lco \
        -keytype rsa -modulusbits 4096 -publicexponent 65537 -mech prime \
        -labelpublic "eidmongol-org-issuing-ca-v1" -labelprivate "eidmongol-org-issuing-ca-v1" \
        -sign 1 -verify 1 -encrypt 0 -decrypt 0 -wrap 0 -unwrap 0 -derive 0
    
    (public/private label SAME — hsm-proxy searches by label; match the Personal key's convention.)
  2. Mint the cert remotely from the app host (no need to return to the HSM host) — cmd/hsmca-selfsign creates a self-signed CA cert via the proxy's /pubkey + /sign-digest:
    hsmca-selfsign -url https://hsm-proxy:8443 -label eidmongol-org-issuing-ca-v1 \
      -client-cert client.crt -client-key client.key -server-ca proxy-ca.crt \
      -subject "CN=eID Mongolia Organization Issuing CA, O=Gerege, C=MN" \
      -years 20 -out org-issuing-ca.crt        # into deploy/secrets/hsmproxy/
    
  3. .env (app host): SMARTID_HSMPROXY_ORG_KEY_LABEL=eidmongol-org-issuing-ca-v1, SMARTID_HSMPROXY_ORG_ISSUING_CERT=/app/hsmproxy/org-issuing-ca.crtdocker compose up -d app.
  4. Verification: startup log "HSM-proxy Organization CA enabled", GET /crl/org 200, and when issuing a seal cert from admin, the issuer is the Organization CA.

Later, once the national root is ready, both CAs are re-signed by L1 (see §2 above).

Making the e-Seal a QSCD (seal leaf key in the HSM)

By default the e-Seal leaf key is generated on the server (software HSM, DB AES-GCM), so its level is QUALIFIED. To make it a real QSCD (QcSSCD + QCP-l-qscd policy), every seal key must stay inside the HSM and the private key must never come to the server. The model is the same as the issuing CA: generate the key on the HSM via ceremony, and the server operates only via /pubkey + /sign-digest.

Modeling: the e-Seal is a contracted (managed) service like SK — the operator performs a separate HSM key ceremony for each organization (the organization does not generate the seal key itself).

  1. On the HSM host (per org, EC P-256): label = <prefix><register code> (default prefix eseal-; e.g. register 1234567 → eseal-1234567):
    cmu generatekeypair -slot <issuing> -lco \
        -keytype ec -curve prime256v1 \
        -labelpublic "eseal-1234567" -labelprivate "eseal-1234567" \
        -sign 1 -verify 1 -encrypt 0 -decrypt 0 -wrap 0 -unwrap 0 -derive 0
    
    (public/private label SAME; hsm-proxy searches by label. Change the prefix via SMARTID_SEAL_KEY_PREFIX.)
  2. .env (app host): SMARTID_SEAL_QSCD=true (the hsmproxy CA provider + HSMPROXY creds must already be present — otherwise the server fail-fasts to stop, preventing a false QSCD declaration). If the prefix differs, SMARTID_SEAL_KEY_PREFIX=eseal-.
  3. Issue the seal cert (admin or RP): the server gets the pubkey of that org's eseal-<code> label from /pubkey and issues the cert via the Organization CA. The private key stays in the HSM.
  4. Sealing: POST /v3/seal/{orgEtsi} — the server signs the digest via /sign-digest (HSM key) and returns it, verified with the cert's pubkey.
  5. Verification: the seal cert's certificateLevel=QSCD; on the admin "Organization CA" screen, "QSCD (QcSSCD + QCP-l-qscd)"; in openssl x509, a QcSSCD statement + policy 0.4.0.194112.1.3.

If you try to issue a seal cert for an org that has NOT had the ceremony, the server gets a /pubkey 404 and returns an error (fail-closed) — it will not silently fall back to a software key while a QSCD is declared.