Building an EAI Middleware Layer
Different Certificates, Different Formats — Stand Up the External Gateway
Goal
Build an external gateway that connects to two external institutions over mutual TLS. Get certificates issued per institution, keep institution profiles as data, translate to the institution's specification and send, receive only from allowed addresses, and monitor certificate expiry.
Why it matters
External institutions do not adapt to our standard. If you do not confine the differences in specification, certificate and allowed address in one place, the external gateway, you end up fixing the inner systems every time an institution is added. And the regulars of external outages — certificate expiry, certificate mix-up, and misunderstanding the length criterion — can all be prevented or noticed early in this layer.
Steps
- Create the two institutions' CAs with
python3 /opt/lab/fixtures/eaimw/fep/pki.py init /root/eaimw/fep/partners. For each institution, create a private key and a CSR (withO=LabHub Bankin the subject andlabhubin the CN), and get them signed withpython3 /opt/lab/fixtures/eaimw/fep/pki.py sign A <CSR> /root/eaimw/fep/a.crtand… sign B <CSR> /root/eaimw/fep/b.crt. The keys are/root/eaimw/fep/a.keyand/root/eaimw/fep/b.key. - Look at the specification
/opt/lab/fixtures/eaimw/fep/ORGS.mdand write/root/eaimw/fep/orgs.json. The top-level keys are institution codes (201and301), and the values arehost,port,format(fixed/json),encoding(euc_kr/utf-8),len_includes_self(true for A, null for B),ca,cert,key(absolute paths) andallow_ips(a list). - Start the institution A server (
nohup python3 /opt/lab/fixtures/eaimw/fep/org_server.py A > /root/eaimw/fep/a.out 2>&1 &, 9441), give a.crt, a.key and the institution A CA toopenssl s_client, send a single linePING, and save thePONG <지문>(PONG followed by the fingerprint) you receive to/root/eaimw/fep/a-ping.txt. - Put b.crt and b.key into the same command and try to connect to institution A, and save standard output and standard error together (
2>&1) to/root/eaimw/fep/wrong.txt. The handshake must be rejected. /root/eaimw/fep/fepgw.py send <기관코드> <요청JSON파일>(institution code, request JSON file): find the institution in the profile (FEP_ORGS, default/root/eaimw/fep/orgs.json), connect with mutual TLS using that institution's certificate, and verify the institution server's certificate with that institution's CA. If the environment variableFEP_PORT_<기관코드>(institution code) exists, use it for the port. For 201, send the request{guid,date,amount,acct,name}in the A specification's fixed-length format (the length includes itself) and output the result as one line of JSON{"org","rspCode","guid","realName","clientFp"}; for 301, send{guid,custId}withPOST /v1/credit-checkand output{"org","rspCode","guid","grade","clientFp"}. Convert response codes to LH-STD (00→0000, 14→B202, 30→E102, anything else E500)./root/eaimw/fep/inbound.py --port <P>: receive connections made by external institutions (plaintext TCP, A specification). If the source address is not in any institution'sallow_ips, disconnect without reading and, inFEP_LOG(default/root/eaimw/fep/inbound.log), leave one lineDENY <주소>(DENY followed by the address). For an allowed address, return an A specification response (message type0210, the same transaction unique number, response code00, the real-name field 20 spaces, the fingerprint field 64-characters, the length including itself)./root/eaimw/fep/certcheck.py --warn-days <N>: for each institution in the profile, print one line<기관코드> <만료일 YYYY-MM-DD> <남은일수> <OK|WARN>(institution code, expiry date, days remaining, OK or WARN), in institution code order. If the remaining days are fewer than N, it is WARN, and if there is even one WARN, it ends with a non-zero code.
Notes
- Key and CSR:
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout a.key -out a.csr -subj "/O=LabHub Bank/CN=labhub-fep-a" - Checking the connection:
printf 'PING\n' | openssl s_client -connect 127.0.0.1:9441 -cert a.crt -key a.key -CAfile partners/A/ca.crt -quiet - Mutual TLS in Python:
ctx = ssl.create_default_context(cafile=CA); ctx.load_cert_chain(CERT, KEY)and thenctx.wrap_socket(sock, server_hostname="127.0.0.1"). The institution server certificate has the IP 127.0.0.1 as a SAN. For HTTPS,urllib.request.urlopen(req, context=ctx). - Certificate fingerprint:
openssl x509 -in a.crt -outform DER | sha256sum. Expiry date: read the value ofopenssl x509 -in a.crt -noout -enddatewith Python'sssl.cert_time_to_seconds(). - Common mistakes: writing institution A's length without including its own 4 bytes, turning off institution server certificate verification (
CERT_NONE), using one certificate for all institutions.
Get a certificate issued per institution
Create the two institution CAs, create a key and CSR for each institution and get them signed, and place a.crt/a.key and b.crt/b.key.
The private key stays on our side and only the CSR is sent. Institution A's must be signed by A and institution B's by B — if one CA signs both, the institution cannot reject the other's certificate.
Institution profiles as data
Look at ORGS.md and write the two institutions' address, format, encoding, length criterion, certificates and allowed IPs into /root/eaimw/fep/orgs.json.
So that no institution name appears in the code, move all the values that differ by institution into this file. The length criterion does not apply to JSON institutions, so it is null.
Connect to institution A over mutual TLS
Start the institution A server, send PING with a.crt, and save PONG to /root/eaimw/fep/a-ping.txt.
Give openssl s_client both -cert and -key (my identity) and -CAfile (the institution CA, to verify the server). With -quiet, only the response body remains. The fingerprint is my certificate as the institution saw it.
Swapping certificates gets you rejected
Try to connect to institution A with b.crt and b.key, and save the output and errors together to /root/eaimw/fep/wrong.txt.
Change only the certificate and key in the step 3 command. The rejection reason (a TLS alert) comes out on standard error, so save it together with 2>&1.
Choose per institution and send
/root/eaimw/fep/fepgw.py send sends with each institution's certificate and specification and outputs one line of JSON with the result normalized to LH-STD codes.
Look at format in the profile and pick an adapter. For A, after building the whole body, use len(body)+4 as the length (including itself). Server verification is create_default_context(cafile=institution CA), and my identity is load_cert_chain.
The inbound side — only from allowed addresses
/root/eaimw/fep/inbound.py --port P accepts only connections from the profile's allow_ips and answers in the A specification, and disconnects the rest and records DENY.
The socketserver handler's self.client_address[0] is the source address. The allow list is the allow_ips of all institution profiles combined. When rejecting, do not read; return right away.
Know about expiry first
/root/eaimw/fep/certcheck.py --warn-days N prints each institution's expiry date, days remaining and OK/WARN, and ends with a non-zero code if there is a WARN.
Read the certificate's notAfter and count the difference from now (UTC) in days. Some institutions take weeks to replace, so set the threshold generously — monitoring tools notice through the exit code.