CMP/CMC/EST
Here is presented SYNRC СA RA OCSP TSP TAMP SCVP server with CMP/CMC/EST enrollment protocols in Elixir.
IETF follow up (PKIX): 7030, 6960, 6818, 6844, 6712, 6664, 6402, 6277, 6170, 6024, 6025, 5934, 5912--5914, 5877, 5816, 5755, 5756, 5758, 5697, 5636, 5480, 5272--5274, 5280, 5055, 5019, 4985, 4683, 4630, 4476, 4387, 4325, 4158, 4210, 4211, 4055, 4043, 3874, 3779, 3820, 3739, 3709, 3628, 3161, 3029, 2797, 2559, 2587, 3039, 3029, 2511, 2510.

Compatibility: OpenSSL, Cisco, Red Hat, Siemens, Nokia, IBM.
Вступ
Ця стаття могла би називати «Як написати CMP сервер за 30 хвилин», але на відміну від попередньої статті про LDAP, ця вже покриває більше ніж тузінь ASN.1 файлів, добре шо ми вже познайомилися з CMS та LDAP бібіліотеками та їх ASN.1 файлами. В цій статті про CMP нас в основному цікавитимуть PKIXCMP-2009, PKIXCRMF-2009 та EnrollmentMessageSyntax-2009 для CMC.
CMS-AES-CCM-and-AES-GCM-2009.asn1
CMSAesRsaesOaep-2009.asn1
CMSECCAlgs-2009-02.asn1
CMSECDHAlgs-2017.asn1
CryptographicMessageSyntax-2009.asn1
CryptographicMessageSyntax-2010.asn1
CryptographicMessageSyntaxAlgorithms-2009.asn1
EnrollmentMessageSyntax-2009.asn1
PKCS-10.asn1
PKCS-7.asn1
PKIX1Explicit-2009.asn1
PKIX1Implicit-2009.asn1
PKIXAlgs-2009.asn1
PKIXCMP-2009.asn1
PKIXCRMF-2009.asn1
CSR
Отже починається написання CMP серверу з найголовнішої його функції: видачі сертифікату по PCKS-10 CSR реквесту. Схема наступна: Клієнт генерує приватний ключ, конвертує його в PEM файл, відсилає як P10CR повідомлення у складі payload PKIMessage, отримує відповідь CP, після чого клієнт шле ще одне повідомлення CERTCONF, після якого CMP сервер повинен відповисти PKICONF повідомленням.
def csr(user) do
{ca_key, ca} = read_ca()
priv = X509.PrivateKey.new_ec(:secp384r1)
der = :public_key.der_encode(:ECPrivateKey, priv)
pem = :public_key.pem_encode([{:ECPrivateKey, der, :not_encrypted}])
:file.write_file(user <> ".key", pem)
:io.format '~p~n', [priv]
csr = X509.CSR.new(priv, "/C=UA/L=Kyiv/O=SYNRC/CN=" <> user,
extension_request: [
X509.Certificate.Extension.subject_alt_name(["n2o.dev"])])
:io.format 'CSR: ~p~n', [csr]
:file.write_file(user <> ".csr", X509.CSR.to_pem(csr))
true = X509.CSR.valid?(csr)
subject = X509.CSR.subject(csr)
:io.format 'Subject ~p~n', [subject]
:io.format 'CSR ~p~n', [csr]
X509.Certificate.new(X509.CSR.public_key(csr), subject, ca, ca_key,
extensions: [subject_alt_name:
X509.Certificate.Extension.subject_alt_name(["n2o.dev", "erp.uno"]) ])
csr
end
RFC2510
Перед початком роботи CMP сервера повинен бути згенерований рутовий CA сертифікат з приватним ключем, ці два файла ми зберігаємо на диск, і у всіх подальших операціях користуємося ними. Для генерації файлів використовуємо функцію CA.CSR.ca.
def ca() do
ca_key = X509.PrivateKey.new_ec(:secp384r1)
ca = X509.Certificate.self_signed(ca_key,
"/C=UA/L=Kyiv/O=SYNRC/CN=CSR-CMP", template: :root_ca)
der = :public_key.der_encode(:ECPrivateKey, ca_key)
pem = :public_key.pem_encode([{:ECPrivateKey, der, :not_encrypted}])
:file.write_file "ca.key", pem
:file.write_file "ca.pem", X509.Certificate.to_pem(ca)
{ca_key, ca}
end
def read_ca() do
{:ok, ca_key_bin} = :file.read_file "ca.key"
{:ok, ca_bin} = :file.read_file "ca.pem"
{:ok, ca_key} = X509.PrivateKey.from_pem ca_key_bin
{:ok, ca} = X509.Certificate.from_pem ca_bin
{ca_key, ca}
end
Для одноразовоїї генерації серверних сертифікатів які обсуговують клієнтські TLS сесії можна використати наступний код.
def server(name) do
{ca_key, ca} = read_ca()
server_key = X509.PrivateKey.new_ec(:secp384r1)
X509.Certificate.new(X509.PublicKey.derive(server_key),
"/C=UA/L=Kyiv/O=SYNRC/CN=" <> name, ca, ca_key,
extensions: [subject_alt_name:
X509.Certificate.Extension.subject_alt_name(["n2o.dev", "erp.uno"]) ])
end
CMS
Детально сімейство протоколів і CMS кодування описано в окремій статті присвяченій CMS Compliance. CMS кодування використовується тільки для CMC сервера, тому ми це поки висвітлювати не будемо.
CMP/CSR/TCP
RFC 6712, 4210. Для початку напишемо простий PKIMessage сервер.
defmodule CA.CMP do
@moduledoc "CA/CMP TCP server."
require CA
def start(), do: :erlang.spawn(fn -> listen(1829) end)
def listen(port) do
{:ok, socket} = :gen_tcp.listen(port,
[:binary, {:packet, 0}, {:active, false}, {:reuseaddr, true}])
accept(socket)
end
def accept(socket) do
{:ok, fd} = :gen_tcp.accept(socket)
:erlang.spawn(fn -> __MODULE__.loop(fd) end)
accept(socket)
end
def loop(socket) do
case :gen_tcp.recv(socket, 0) do
{:ok, data} ->
[headers,body] = :string.split data, "\r\n\r\n", :all
{:ok,dec} = :'PKIXCMP-2009'.decode(:'PKIMessage', body)
{:PKIMessage, header, body, code, _extra} = dec
__MODULE__.message(socket, header, body, code)
loop(socket)
{:error, :closed} -> :exit
end
end
PKIMessage.protection
Розберемося з полем PKIMessage.protection, в якому зберігається результат PBKDF2 алгоритма. Майте на увазі шо OpenSSL за замовчування використовує 20-байтні ключі та HMAC/SHA-1 у якості MAC функції, хоча OWF в 500 ітераціях обчислюється за допомогою OWF функції SHA-256.
def baseKey(pass, salt, iter, owf \\ :sha256), do:
:lists.foldl(fn _, acc ->
:crypto.hash(owf, acc) end, pass <> salt,
:lists.seq(1,iter))
def protection(:asn1_NOVALUE), do: {"","","","",1}
def protection(protectionAlg) do
{_,oid,{_,param}} = protectionAlg
{:ok, parameters} = :"PKIXCMP-2009".decode(:'PBMParameter', param)
{:PBMParameter, salt, {_,owf,_}, counter, {_,mac,_} } = parameters
{oid, salt, owf, mac, counter}
end
def validateProtection(header, body, code) do
{:PKIHeader, _, _, _, _, protectionAlg, _, _, _, _, _, _, _} = header
{oid, salt, owfoid, _mac, counter} = protection(protectionAlg)
case CA.ALG.lookup(oid) do
{:'id-PasswordBasedMac', _ } ->
protection = CA."ProtectedPart"(header: header, body: body)
{:ok, bin} = :"PKIXCMP-2009".encode(:'ProtectedPart', protection)
{owf,_} = CA.ALG.lookup(owfoid)
pbm = :application.get_env(:ca, :pbm, "0000")
verifyKey = baseKey(pbm, salt, counter, owf)
mac = CA.KDF.hs(:erlang.size(code))
:crypto.mac(:hmac, mac, verifyKey, bin)
{_, _ } ->
""
end
end
ANSWER
Оскільки CMP сервер повинен працювати по HTTP/1.0 згідно стандартів додаємо необхідні HTTP заголовки.
def answer(socket, header, body, code) do
message = CA."PKIMessage"(header: header, body: body, protection: code)
{:ok, bytes} = :'PKIXCMP-2009'.encode(:'PKIMessage', message)
res = "HTTP/1.0 200 OK\r\n"
<> "Server: SYNRC CA/CMP\r\n"
<> "Content-Type: application/pkixcmp\r\n\r\n"
<> :erlang.iolist_to_binary(bytes)
:gen_tcp.send(socket, res)
end
P10CR/CP
Запускаємо сервер та генеруємо сертифікати CA та CSR користувача:
$ mix deps.get
$ iex -S mix
> CA.CSR.ca
> CA.CSR.csr "maxim"
Запускаємо клієнтський запит за допомогою OpenSSL:
# openssl cmp -cmd p10cr -server localhost:1829 \
# -path . -srvcert ca.pem -ref cmptestp10cr \
# -secret pass:0000 -certout $client.pem -csr $client.csr
Пишему функцію видачі сертифікату:
def message(socket, header, {:p10cr, csr} = body, code) do
{:PKIHeader, pvno, from, to, messageTime, protectionAlg,
_senderKID, _recipKID, transactionID, senderNonce,
_recipNonce, _freeText, _generalInfo} = header
true = code == validateProtection(header, body, code)
{ca_key, ca} = CA.CSR.read_ca()
subject = X509.CSR.subject(csr)
:io.format '~p~n',[subject]
true = X509.CSR.valid?(CA.parseSubj(csr))
cert = X509.Certificate.new(X509.CSR.public_key(csr),
CA.CAdES.subj(subject), ca, ca_key,
extensions: [subject_alt_name:
X509.Certificate.Extension.subject_alt_name(["synrc.com"]) ])
reply = CA."CertRepMessage"(response:
[ CA."CertResponse"(certReqId: 0,
certifiedKeyPair: CA."CertifiedKeyPair"(certOrEncCert:
{:certificate, {:x509v3PKCert, CA.convertOTPtoPKIX(cert)}}),
status: CA."PKIStatusInfo"(status: 0))])
pkibody = {:cp, reply}
pkiheader = CA."PKIHeader"(sender: to, recipient: from, pvno: pvno,
recipNonce: senderNonce, transactionID: transactionID,
protectionAlg: protectionAlg, messageTime: messageTime)
answer(socket, pkiheader, pkibody,
validateProtection(pkiheader, pkibody, code))
end
CERTCONF/PKICONF
def message(socket, header, {:certConf, statuses}, code) do
{:PKIHeader, _, from, to, _, _, _, _, _, senderNonce, _, _, _} = header
:lists.map(fn {:CertStatus,bin,no,{:PKIStatusInfo, :accepted, _, _}} ->
:logger.info 'CERTCONF ~p request ~p~n', [no,:binary.part(bin,0,8)]
end, statuses)
pkibody = {:pkiconf, :asn1_NOVALUE}
pkiheader = CA."PKIHeader"(header, sender: to, recipient: from,
recipNonce: senderNonce)
answer(socket, pkiheader, pkibody,
validateProtection(pkiheader, pkibody, code))
end
В результаті в консолі повинні спостерігати:
CMP info: sending P10CR
CMP info: received CP
CMP info: sending CERTCONF
CMP info: received PKICONF
CMP info: received 1 enrolled certificate(s), saving to file 'maxim.pem'
GENM/GENP
Далі можете написати інші функції:
# openssl cmp -cmd genm -server 127.0.0.1:1829 \
# -recipient "/CN=CMPserver" -ref 1234 -secret pass:0000
def message(_socket, _header, {:genm, req} = _body, _code) do
:io.format 'generalMessage: ~p~n', [req]
end
IR/IP
# openssl cmp -cmd ir -server 127.0.0.1:1829 \
# -path . -srvcert ca.pem -ref NewUser \
# -secret pass:0000 -certout maxim.pem \
# -newkey maxim.key -subject "/CN=maxim/O=SYNRC/ST=Kyiv/C=UA"
def message(_socket, _header, {:ir, req}, _) do
:lists.map(fn {:CertReqMsg, req, sig, code} ->
:io.format 'request: ~p~n', [req]
:io.format 'signature: ~p~n', [sig]
:io.format 'code: ~p~n', [code]
end, req)
end
CR/CP
# openssl cmp -cmd cr -server 127.0.0.1:1829 \
# -path . -srvcert ca.pem -ref NewUser \
# -secret pass:0000 -certout maxim.pem \
# -newkey maxim.key -subject "/CN=maxim/O=SYNRC/ST=Kyiv/C=UA"
CMC/CMS/TCP
RFC 5272--5274, 2797, 6402.
EST/CMS/TLS
RFC 7030
Висновки
defmodule CA do
use Application
use Supervisor
require Record
Enum.each(Record.extract_all(from_lib: "ca/include/PKIXCMP-2009.hrl"),
fn {name, definition} -> Record.defrecord(name, definition) end)
Enum.each(Record.extract_all(from_lib: "public_key/include/public_key.hrl"),
fn {name, definition} -> Record.defrecord(name, definition) end)
def init([]), do: {:ok, { {:one_for_one, 5, 10}, []} }
def start(_type, _args) do
:logger.add_handlers(:ldap)
CA.CMP.start
CA.CMC.start
:supervisor.start_link({:local, __MODULE__}, __MODULE__, [])
end
end
Ну PasswordBasedMac в нас є, тепер треба DHMac, але shared secret можна і в PBM засунути. Є ше Proof Of Posession (POP) — там зразу ECDSA verify. Я до речі думаю в CA тримати ключі для всіх кривих, і коли я виставлятиму сервіс то я буду виставляти його на N портах і N ключах, щоб будь який клієнтський TLS сертифікат приймався як рідний! Chat 💬 X.509 дає можливість вибирати TLS сертифікати автоматично по обраних кривих. Ви вибираєте під якими ключами сьогодні заходити. LDAP, MQTT, NS, CA — в кожного сервісу свої N портів і N серверних TLS сертифікатів. Передбачається що перший сертифікат видається DH по TCP а потім зразу всьо переходить в TLS режим і всі наступні сертифікати вже видаються всередині клієнтського TLS. При реєстрації користувач зразу доступний в LDAP якшо захотів зробити себе відкритим для пошуку. Після реєстрації пошук в директорії і френдування (обмін ключами) і поїхали чат в обгортках CAdES, CMS, ECDSA/AES — лейби біля повідомлень ПІДПИС/ШИФР.
[1]. Cisco. PKI: Simplify Certificate Provisioning with EST. 2016
[2]. SYNRC CA