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