CMP (RFC 4210, RFC 9483)¶
CMP (Certificate Management Protocol, RFC 4210, updated by RFC 9480; Lightweight CMP Profile, RFC 9483) is the enrollment protocol of telecom networks (3GPP base stations and network functions), industrial automation and automotive systems. Every CMP message is itself protected, by a signature or by a MAC computed from a shared secret, so it can travel over plain HTTP. openssl cmp (OpenSSL 3.0 and later) is a reference client.
PKIFactor runs a CMP gateway in front of your certification authorities. Each certificate profile with CMP enabled gets its own CMP address; the gateway verifies the message protection and the proof of possession, the profile decides what may be issued (authority, algorithms, names, validity, organization), and every certificate lands in the inventory.
Tested
Tested with openssl cmp, messages protected by the profile's shared secret (PBM):
- p10cr: certificate issued by Microsoft AD CS;
- ir with signature proof of possession: certificate issued by EJBCA behind a CMP authority connector (EJBCA CMP alias in RA mode with
allowraverifypopo=true), on the appliance and on OpenShift. A client sendingraVerifieditself was refused, as expected; getCaCerts; a replayed request was rejected.
Not tested: signature-protected requests, PBMAC1, cr, kur, rr, ccr, the CoAP transport, and enrollment by a telecom or industrial device.
What is supported¶
| Message | Purpose | Status |
|---|---|---|
p10cr |
Enrollment with a PKCS#10 request | Supported, with any authority |
ir |
Initial enrollment (CRMF request) | Supported, only with an authority that accepts it (see ir, cr and kur) |
cr |
Enrollment of an entity that already has a certificate | Same as ir |
kur |
Key update (renewal with a new key) | Same as ir; the request must be signed with the certificate being renewed, for the same subject |
rr |
Revocation | Supported: signature-protected, and an end entity may only revoke the certificate it signs with. MAC-protected revocation is refused |
certConf |
Confirmation | Supported (answered with pkiconf) |
pollReq |
Polling | Always answers "check again later": asynchronous issuance is not supported |
nested |
Messages wrapped by a local RA | Refused in this version (see Modes) |
genm |
General messages (CA certificates, request template...) | Not supported. Use the HTTP helpers below |
ccr |
Cross-certification | Handled like ir; not tested |
Proof of possession (ir, cr, kur): signature (POPOSigningKey) is accepted. raVerified is refused from an end entity talking directly to the gateway. Key encipherment and key agreement proofs are not supported.
Protection:
| Protection | Details |
|---|---|
| MAC (PBM / PBMAC1) | Shared secret of the profile. At least 500 iterations are required (the openssl cmp default; raise the floor with CMP_PBM_MIN_ITERATIONS). SHA-1 is refused as the PBM one-way function |
| Signature | The signing certificate is taken from the message's extraCerts and must chain to the CA chain of the profile's PKI connector. A revoked signing certificate is refused |
| Unprotected messages | Refused |
Each protected message must carry a messageTime within 60 seconds of the gateway's clock (CMP_MESSAGE_TIME_SKEW_SECONDS), or a senderNonce, or both. A senderNonce is accepted only once: the record is kept in the database, so a message replayed to another node of a high-availability cluster is also refused. Keep device clocks synchronized.
Over HTTP, the gateway limits requests per client address (ENROLL_RATE_LIMIT, 30 per minute by default) and refuses a message larger than 1 MiB (CMP_MAX_BODY_BYTES, HTTP 413).
Responses are protected like the request: with the profile's secret for a MAC request; for a signed request, with a responder certificate that the gateway obtains from the profile's own CA on the first signed request (common name cmp-responder.pkifactor.com by default, set by CMP_RESPONDER_CN; this certificate appears in the inventory). If the profile does not allow that name, responses to signed requests are sent unprotected and strict clients reject them.
Addresses¶
The CMP gateway has its own DNS name (for example cmp.example.com, see Requirements) and is reached through nginx:
| Purpose | Request |
|---|---|
| CMP messages (RFC 6712) | POST http://cmp.example.com/cmp/<path> (or https://), Content-Type: application/pkixcmp |
| CA chain (JSON) | GET https://cmp.example.com/cmp/<path>/getCaCerts returns {"chain_pem": [...]} |
| Request constraints (JSON) | GET https://cmp.example.com/cmp/<path>/getCertReqTemplate |
| Root CA update | GET .../getRootCaCertUpdate answers 501: not implemented |
The two helpers return JSON; they are a PKIFactor convenience, not the genm messages of RFC 9483.
CMP messages are protected end to end, and RFC 6712 allows plain HTTP: nginx answers on port 80 directly (no redirection) as well as on 443. If none of your clients needs plain HTTP, close port 80 for this name at your firewall (see Network flows).
<path> is the profile's CMP path. PKIFactor generates it from the profile name when you enable CMP (cmp- followed by the name in lower case, for example cmp-base-stations) and keeps it afterwards. Disabling CMP on the profile clears it.
The gateway exists only when the cmp service is started and CMP_ENABLED=true (see Docker Compose).
CoAP (RFC 9482): the gateway also listens on 5683/UDP, path /cmp/<path>, for constrained devices. nginx does not relay UDP: on a single-node appliance this port is bound to 127.0.0.1 only. Publish it explicitly if your devices use CoAP.
Enable CMP on a profile¶
- In the Administration workspace, open Infrastructure > Certificate Profiles, open the profile and click Edit Profile.
- Unfold Automatic enrollment (ACME, EST, CMP, SCEP), then in CMP Protocol tick Enable CMP enrolment.
- Fill in the fields below, then save. The profile page then shows the Enrollment path and the Enrollment endpoint.
| Field | Meaning |
|---|---|
| Protection modes: Signature (sig), MAC (PBM / PBMAC1) | Which protections the profile accepts. A request with another protection is refused. If none is ticked, signature is accepted, and MAC only when a shared secret is set |
| Allowed deployment modes: EE-direct, LRA, RA | See Deployment modes. Keep EE-direct |
| Transports: HTTP, CoAP | Informative in this version: the gateway does not filter on it. Control CoAP by publishing or not publishing 5683/UDP |
| PBM shared secret (MAC) | The secret for MAC protection. Leave it blank to keep the current one (already set is shown when there is one). It is stored encrypted with the platform's database encryption key, which the CMP gateway shares to verify the MAC |
Rules applied when you save:
| Rule | Why |
|---|---|
| Require validation before issuance is turned off and cannot be ticked | Message protection replaces manual approval (see Approval) |
| The profile must belong to one organization | Nobody is there to choose an organization behind a gateway. Otherwise the save is refused: Un profil activé pour ACME, EST, SCEP ou CMP doit être rattaché à une seule organisation : une passerelle ne peut pas choisir. |
Via the API
curl -s -c jar -X POST https://clm.example.com/api/auth/login \
-d "email=admin@example.com" --data-urlencode "password=$ADMIN_PASSWORD"
curl -s -b jar -X PUT https://clm.example.com/api/v1/clm/templates/16 \
-H "Content-Type: application/json" \
-d '{"cmp_enabled": true, "cmp_protection_modes": ["mac"],
"cmp_allowed_modes": ["ee-direct"], "cmp_transports": ["http"],
"cmp_pbm_secret": "'"$CMP_SECRET"'", "requires_approval": false}'
The response contains the generated cmp_path. The secret is never returned.
Deployment modes¶
| Mode | Meaning | In this version |
|---|---|---|
| EE-direct | The end entity talks to the gateway itself | Supported. raVerified is refused |
| LRA | A local RA wraps the end entities' messages in nested messages |
The gateway accepts a nested message only from an RA whose signing certificate subject is registered for the profile, and there is no way to register one yet: nested messages are refused |
| RA | A trusted RA sends requests on behalf of end entities | Same limitation: the RA mode is never selected |
ir, cr and kur: which authorities¶
ir, cr and kur carry a CRMF request whose proof of possession is bound to the CMP message, not a signed PKCS#10. The gateway verifies that proof, then passes the request on with a CSR it builds itself, which is not signed by the device's key (the device's key never leaves it). What happens next depends on the profile's authority:
| Authority behind the profile | p10cr |
ir, cr, kur |
|---|---|---|
CMP authority connector to an upstream CA whose CMP alias runs in RA mode and accepts RA-verified proofs (EJBCA: operationmode=ra, allowraverifypopo=true) |
Works | Works: PKIFactor vouches for the proof (raVerified) to the upstream CA. Tested with EJBCA |
| CAs that sign a CSR and re-verify its signature: HashiCorp Vault, EJBCA through its REST API, Microsoft AD CS | Works | Refused by the CA |
When the CA refuses, the device receives: target CA re-verifies the certificate-request signature (POPO); ir/cr with a gateway-synthesised CSR cannot satisfy it for a client-generated key. Use the p10cr transaction (client sends a signed PKCS#10), or a CMP-native CA connector that consumes the CMP POP directly.
So: use p10cr with any authority, or put a CMP authority connector in front of a CA that accepts RA-verified requests. See PKI connectors. With a CMP authority connector, also fill in its CA Chain (PEM): getCaCerts and signature verification use it.
Approval and issuance policies¶
CMP requests are never sent for manual approval: message protection stands for it. This exemption applies to requests that arrive through the CMP gateway only.
Issuance policies still apply: a rule that blocks a request blocks it over CMP too. Since version 1.8.50, the device receives a response with status rejection, the failure code badCertTemplate (ir, cr, kur) or badRequest (p10cr), and the text rejected by policy: ... with the rule's message. The audit trail records the refusal (POLICY_BLOCK, and CMP_HANDLER_FAILED with the reason policy). If the policies cannot be evaluated, the request is refused rather than allowed (POLICY_EVAL_ERROR). A rule that would require approval cannot be honored on an automated channel; the request goes through and the event POLICY_AUTO_ALLOWED is recorded.
Audit and inventory¶
- Each certificate appears in the inventory under the profile's organization, requested by the service account
cmp@pkifactor.com. - The PKIFactor audit trail (Oversight workspace, Compliance > Audit Trail) records the issuance (
CERTIFICATE_ISSUED), policy refusals (POLICY_BLOCK) and, forir,crandkur,CMP_POP_TRUSTED: the backend accepted a CSR without its signature because the gateway verified the proof of possession. See Audit. - The gateway writes one JSON line per step in its container log, with the transaction identifier, the protection and the mode:
CMP_REQUEST_RECEIVED,CMP_PROTECTION_VERIFIEDorCMP_PROTECTION_REJECTED,CMP_MODE_RESOLVED,CMP_MODE_POLICY_DENIED,CMP_REPLAY_REJECTED,CMP_TIME_SKEW_REJECTED,CMP_HANDLER_DISPATCHED,CMP_RESPONSE_SENT...
Client example¶
# CA chain of the profile
curl -s https://cmp.example.com/cmp/cmp-base-stations/getCaCerts \
| jq -r '.chain_pem[]' > ca-chain.pem
# p10cr, MAC protection with the profile's shared secret (works with any authority)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout device.key -out device.csr -subj "/CN=gnb-0042.example.com"
openssl cmp -cmd p10cr \
-server http://cmp.example.com:80 -path /cmp/cmp-base-stations \
-ref gnb-0042 -secret env:CMP_SECRET \
-recipient "/CN=Example Issuing CA" \
-csr device.csr -certout device.crt
# ir, signature proof of possession (needs an authority that accepts RA-verified requests)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out device2.key
openssl cmp -cmd ir \
-server http://cmp.example.com:80 -path /cmp/cmp-base-stations \
-ref gnb-0043 -secret env:CMP_SECRET \
-recipient "/CN=Example Issuing CA" \
-newkey device2.key -subject "/CN=gnb-0043.example.com" -popo 1 \
-certout device2.crt
-ref is the sender key identifier; the gateway uses one secret per profile. The flow ends with certConf and pkiconf, which openssl cmp sends and checks on its own. For signature-protected responses, give the client the CA chain with -trusted ca-chain.pem.
Troubleshooting¶
| Symptom | Cause and fix |
|---|---|
415 content-type must be application/pkixcmp |
Send exactly Content-Type: application/pkixcmp |
| Error message unknown cmp_path: ... | Wrong CMP path, CMP disabled on the profile, or profile inactive |
| PKIMessage protection rejected or protection: ... | Wrong shared secret, PBM iteration count below 500, SHA-1 as one-way function, or a signing certificate that does not chain to the profile's CA chain |
protection mode 'mac' not allowed by profile (or 'sig') |
Tick that protection in Protection modes, or set a shared secret |
| messageTime outside allowed skew | The device clock is more than 60 seconds off: synchronize it (NTP) |
| replayed message (senderNonce already seen) | The client re-sent a message: generate a new one |
| raVerified rejected by policy | Use -popo 1 (signature), the openssl cmp default |
| target CA re-verifies the certificate-request signature (POPO)... | The authority does not accept ir/cr/kur: use p10cr, or a CMP authority connector (see above) |
| KUR requires signature protection by the certificate being renewed | kur must be signed with the current certificate, not protected by a MAC |
| nested message requires an authorized RA signer | LRA mode is not available in this version: send the messages directly |
| unsupported transaction: ... | genm and other messages outside the table above are not supported |
rejected by policy: ... (status rejection) |
An issuance policy or the profile refused the request; the text gives the reason |
backend rejected request: ... (status rejection) on p10cr, ir or cr |
The authority refused the request or could not be reached: check the backend logs |
413 PKIMessage exceeds CMP_MAX_BODY_BYTES |
Message larger than 1 MiB |
429 |
Too many requests from the same address: wait and retry |
404 on getCaCerts |
No CA chain for the profile's connector: fill in CA Chain (PEM) on the PKI connector |
| The client rejects the response protection | For signed requests, give the client -trusted with the CA chain, and check that the profile allows the responder name (CMP_RESPONDER_CN) |
pollRep again and again |
The authority did not issue at once; polling is not supported in this version |