Skip to content

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 sending raVerified itself 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

  1. In the Administration workspace, open Infrastructure > Certificate Profiles, open the profile and click Edit Profile.
  2. Unfold Automatic enrollment (ACME, EST, CMP, SCEP), then in CMP Protocol tick Enable CMP enrolment.
  3. 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, for ir, cr and kur, 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_VERIFIED or CMP_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