Current section

Files

Jump to
ca lib services cmp.ex
Raw

lib/services/cmp.ex

defmodule CA.CMP do
@moduledoc "CA/CMP TCP server."
require CA
require CA.CMP.Scheme
require Logger
# Authority PKI X.509 CMP over TCP RFC 4210 9480 9481
# [1] https://datatracker.ietf.org/doc/html/rfc4210
# [2] https://datatracker.ietf.org/doc/html/rfc9480
def ref() do
to_string(:lists.filter(fn x -> true == x > 44 and x < 59 end, :erlang.ref_to_list(:erlang.make_ref())))
end
def start_link(port: port), do: {:ok, :erlang.spawn_link(fn -> listen(port) end)}
def child_spec(opt) do
%{
id: CMP,
start: {CA.CMP, :start_link, [opt]},
type: :supervisor,
restart: :permanent,
shutdown: 500
}
end
def listen(port) do
:logger.info(~c"Running CA.CMP with Authority 5.11.15 at 0.0.0.0:~p (tcp)", [port])
{:ok, socket} = :gen_tcp.listen(port, [:binary, {:active, false}, {:reuseaddr, true}])
accept(socket)
end
def accept(socket) do
{:ok, fd} = :gen_tcp.accept(socket)
{:ok, _pid} = Task.Supervisor.start_child(CA.TaskSupervisor, fn -> loop(fd, []) end, restart: :temporary)
accept(socket)
end
def loop(socket, ca) do
case :gen_tcp.recv(socket, 0) do
{:error, _} ->
:exit
{:ok, stage1} ->
try do
[_headers | body] = :string.split(stage1, "\r\n\r\n", :all)
case body do
[""] ->
case :gen_tcp.recv(socket, 0) do
{:error, _} -> :exit
{:ok, stage2} -> handleMessage(socket, stage2)
end
_ ->
handleMessage(socket, body)
end
__MODULE__.loop(socket, ca)
catch
_ ->
__MODULE__.loop(socket, ca)
end
end
end
def handleMessage(socket, body) do
binary_body = IO.iodata_to_binary(body)
case :"PKIXCMP-2009".decode(:PKIMessage, binary_body) do
{:ok, {:PKIMessage, header, body, code, _extra}} ->
__MODULE__.message(socket, header, body, code)
{:error, reason} ->
Logger.info("Malformed CMP request: #{inspect(reason)}, body: #{inspect(binary_body)}")
bad_request(socket, "Malformed CMP request")
{:error, {:invalid_pki_message, reason}}
end
end
defp bad_request(socket, message) do
body = message <> "\n"
response =
"HTTP/1.0 400 Bad Request\r\n" <>
"Server: SYNRC CA/CMP\r\n" <>
"Content-Length: #{byte_size(body)}\r\n" <>
"Content-Type: text/plain; charset=utf-8\r\n" <>
"Connection: close\r\n\r\n" <>
body
_ = :gen_tcp.send(socket, response)
:gen_tcp.close(socket)
end
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, macoid, counter} = protection(protectionAlg)
case CA.ALG.lookup(oid) do
{:"id-PasswordBasedMac", _} ->
incomingProtection = CA.CMP.Scheme."ProtectedPart"(header: header, body: body)
{:ok, bin} = :"PKIXCMP-2009".encode(:ProtectedPart, incomingProtection)
# SHA-2
{owf, _} = CA.ALG.lookup(owfoid)
# DH shared secret
pbm = :application.get_env(:ca, :pbm, "0000")
verifyKey = baseKey(pbm, salt, counter, owf)
:logger.info(~c"TCP counter ~p~n", [counter])
hash = CA.KDF.hs(:erlang.size(code))
res =
case CA.ALG.lookup(macoid) do
{:"hMAC-SHA1", _} -> :crypto.mac(:hmac, hash, verifyKey, bin)
_ -> :crypto.mac(:hmac, hash, verifyKey, bin)
end
:logger.info(~c"TCP validateProtection ~p~n", [res])
res
{_, _} ->
""
end
end
def answer(socket, header, body, code) do
message = CA.CMP.Scheme."PKIMessage"(header: header, body: body, protection: code)
{:ok, bytes} = :"PKIXCMP-2009".encode(:PKIMessage, message)
:logger.info(~c"TCP answer ~p~n", [message])
bin = :erlang.iolist_to_binary(bytes)
res =
"HTTP/1.0 200 OK\r\n" <>
"Server: SYNRC CA/CMP\r\n" <>
"Content-Length: #{byte_size(bin)}\r\n" <>
"Content-Type: application/pkixcmp\r\n\r\n" <>
bin
:gen_tcp.send(socket, res)
end
def storeReply(csr, cert, cn, profile, certReqId \\ -1) do
{:ok, _} = :"PKCS-10".encode(:CertificationRequest, csr)
:file.write_file("#{CA.CSR.dir(profile)}/#{cn}.csr", X509.CSR.to_pem(csr))
:file.write_file("#{CA.CSR.dir(profile)}/#{cn}.cer", X509.Certificate.to_pem(cert))
cert = :public_key.pkix_decode_cert(:public_key.pkix_encode(:OTPCertificate, cert, :otp), :plain)
[
CA.CMP.Scheme."CertResponse"(
certReqId: certReqId,
certifiedKeyPair: CA.CMP.Scheme."CertifiedKeyPair"(certOrEncCert: {:certificate, {:x509v3PKCert, cert}}),
status: CA.CMP.Scheme."PKIStatusInfo"(status: 0)
)
]
end
def message(_socket, _header, {:ir, req}, _) do
:lists.map(
fn {:CertReqMsg, req, sig, code} ->
:logger.info(~c"request: ~p ~p ~p~n", [req, sig, code])
end,
req
)
end
def message(_socket, _header, {:genm, req} = _body, _code) do
:io.format(~c"generalMessage: ~p~n", [req])
end
def message(socket, header, {:p10cr, csr} = body, code) do
{:PKIHeader, pvno, from, to, messageTime, protectionAlg, _senderKID, _recipKID, transactionID, senderNonce,
_recipNonce, _freeText, _generalInfo} = header
val_prot = validateProtection(header, body, code)
:io.format(~c"DEBUG: Code size: ~p, ValProt size: ~p~n", [:erlang.size(code), :erlang.size(val_prot)])
:io.format(~c"DEBUG: Code: ~p~nValProt: ~p~n", [code, val_prot])
true = code == val_prot
profile = CA.RDN.profile(csr)
{ca_key, ca} = CA.CSR.read_ca(profile)
subject = CA.RDN.decodeAttrs(X509.CSR.subject(csr))
true = X509.CSR.valid?(csr)
public_key = X509.CSR.public_key(csr)
cert =
X509.Certificate.new(public_key, subject, ca, ca_key,
extensions: [subject_alt_name: X509.Certificate.Extension.subject_alt_name(["synrc.com"])]
)
# :io.format 'X509 Subj ~tw~n', [subject]
# :io.format 'X509 Key ~tw~n', [public_key]
# :io.format 'X509 CA ~tw~n', [ca]
# :io.format 'X509 CA Key ~tw~n', [ca_key]
# :io.format 'X509 Extensions ~tw~n', [[subject_alt_name: X509.Certificate.Extension.subject_alt_name(["synrc.com"]) ]]
# :io.format 'X509 Client Certificate Generated ~tw~n', [cert]
reply =
case Keyword.get(CA.RDN.rdn(subject), :cn) do
nil ->
storeReply(csr, cert, ref(), profile)
cn ->
case :filelib.is_regular("#{CA.CSR.dir(profile)}/#{cn}.csr") do
false -> storeReply(csr, cert, cn, profile)
true -> []
end
end
pkibody = {:cp, CA.CMP.Scheme."CertRepMessage"(response: reply)}
pkiheader =
CA.CMP.Scheme."PKIHeader"(
sender: to,
recipient: from,
pvno: pvno,
recipNonce: senderNonce,
transactionID: transactionID,
protectionAlg: protectionAlg,
messageTime: messageTime
)
:logger.info(~c"TCP P10CR request ~p~n", [csr])
:ok = answer(socket, pkiheader, pkibody, validateProtection(pkiheader, pkibody, code))
end
def message(socket, header, {:certConf, statuses}, code) do
{:PKIHeader, _, from, to, _, _, _, _, _, senderNonce, _, _, _} = header
:lists.map(
fn {:CertStatus, bin, no, {:PKIStatusInfo, :accepted, _, _}} ->
:logger.info(~c"TCP CERTCONF ~p request ~p~n", [no, :base64.encode(bin)])
end,
statuses
)
pkibody = {:pkiconf, :asn1_NOVALUE}
pkiheader = CA.CMP.Scheme."PKIHeader"(header, sender: to, recipient: from, recipNonce: senderNonce)
:ok = answer(socket, pkiheader, pkibody, validateProtection(pkiheader, pkibody, code))
end
def message(_socket, _header, body, _code) do
:logger.info(~c"Strange PKIMessage request ~p", [body])
end
# WSL Service
# netsh interface portproxy add v4tov4 listenport=8829 listenaddress=192.168.0.3 connectport=8829 connectaddress=172.31.45.170
# netsh interface portproxy add v4tov4 listenport=8047 listenaddress=192.168.0.3 connectport=8047 connectaddress=172.31.45.170
# New-NetFireWallRule -DisplayName 'CMP-OUT' -Direction Outbound -LocalPort 8829 -Action Allow -Protocol TCP
# New-NetFireWallRule -DisplayName 'CMP-IN' -Direction Inbound -LocalPort 8829 -Action Allow -Protocol TCP
# New-NetFireWallRule -DisplayName 'EST-OUT' -Direction Outbound -LocalPort 8047 -Action Allow -Protocol TCP
# New-NetFireWallRule -DisplayName 'EST-IN' -Direction Inbound -LocalPort 8047 -Action Allow -Protocol TCP
end