Skip to main content

Python Callback Server Example

server.py

if __name__ == '__main__':
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)


# 1. using self signed certificate
ssl_context.load_cert_chain(certfile='new-server.crt', keyfile='new-server.key')


# 2. using converted .pem bundle file from GTR
ssl_context.load_verify_locations(cafile='ca_bundle.pem')


# 3. the incomining request required SSL Client CERT or not
ssl_context.verify_mode = ssl.CERT_REQUIRED

app.run(ssl_context=ssl_context, host="0.0.0.0", port=8443)

Where new-server.crt and new-server.key are signed by yourself. The following OpenSSL command is valid to provide the certificate by yourself, or you can reach a CA to sign a trust-root CA validated certificate, but it is not necessary. The client to your callback server should only be GTR, not public.

The following command provides an example of generating self-signed certificates.

openssl genrsa -des3 -out new-server.key 2048

openssl req -new -key new-server.key -out new-server.csr

cp new-server.key new-server.key.org

openssl rsa -in new-server.key.org -out new-server.key

openssl x509 -req -days 365 -in new-server.csr -signkey new-server.key -out new-server.crt

For further validation, ca_bundle.pem set in verify_mode only verifies the root-CA, not the GTR certificate. A simple way to verify by yourself is using string comparison to match SSL_CLIENT_CERT with ca_bundle.pem; if the string exists, then it passes. Or if strong verification is required, you could verify that the SSL_CLIENT_CERT is valid and matches the common name. Please check the example below to demonstrate how to verify the CN matches and valid dates.

middlewares.py

from flask import Flask, request
import ssl
from OpenSSL.crypto import X509, X509Store, X509StoreContextError, FILETYPE_PEM, X509StoreContext, load_certificate
import datetime

# load global trusted ca
store = X509Store()
ca_certs = load_certificates('ca_bundle.pem')
for ca in ca_certs:
print(ca.get_subject())
store.add_cert(ca)

# customize Common Name (CN) verification function
def is_match_cn(cert):
verified = False
for ca in ca_certs:
# print(get_CN_prefix(cert))
# print(get_CN_prefix(ca))
verified = verified or get_CN_prefix(cert) in get_CN_prefix(ca)
return verified

# get CN from certificate struct
def get_CN_prefix(cert):
for component in cert.get_subject().get_components():
if component[0] == b'CN' :
return component[1]
return "NO_CN"

def is_valid_certificate(cert):
not_before_str = cert.get_notBefore().decode('utf-8')
not_after_str = cert.get_notAfter().decode('utf-8')

not_before = datetime.datetime.strptime(not_before_str, "%Y%m%d%H%M%SZ")
not_after = datetime.datetime.strptime(not_after_str, "%Y%m%d%H%M%SZ")

current_time = datetime.datetime.utcnow()

if not_before <= current_time <= not_after:
print('Certificate is valid.')
return True
else:
print('Certificate is not valid.')
return False

# define middleware to check the request
@app.before_request
def check_client_cert():
client_cert = request.environ.get("SSL_CLIENT_CERT")
if client_cert:
cert = load_certificate(FILETYPE_PEM, client_cert)
store_context = X509StoreContext(store, cert)

# check current timestamp is valid between valid date range
if not is_valid_certificate(cert):
return "Invalid certificate", 403

# check client cert is match trusted certificate's CN
if not is_match_cn(cert):
return "Invalid client certificate", 403
else:
return "Client certificate required.", 401
Copyright (C) 2024 Global Travel Rule. All Rights Reserved
General
Developer