Mutual TLS for Aviation Maintenance Data APIs

When a maintenance record crosses the wire, ordinary TLS proves only that the server is who it claims — the API has no cryptographic proof of which client is uploading. Mutual TLS closes that gap: both the MRO client and the data API present X.509 certificates and each verifies the other before a single byte of airworthiness data flows. This page implements client-certificate enforcement as one control within the Secure API Gateway Architecture, and it satisfies the non-repudiation intent behind EASA Part-145 145.A.55, which requires a maintenance organisation to retain records in a form protected from unauthorised alteration and access.

Prerequisites & inputs

Mutual TLS assumes a private certificate authority (CA) under your control, distinct from any public web CA. That internal CA issues one leaf certificate per machine client — a hangar gateway pushing records for G-TUIA, an OEM reliability feed, a subcontractor’s CAMO exporter — with the client’s stable identifier embedded in the certificate subject or a subject-alternative-name URI. The API server holds its own leaf certificate plus the CA bundle it will trust for verifying clients.

Three artifacts must be provisioned before the handshake can succeed: the server certificate and key, the client certificate and key, and the trusted CA chain on both ends. Certificate rotation is a scheduled operation, not an incident response — leaves are short-dated (30 to 90 days) and reissued from the CA before expiry. The private keys never leave the host that owns them; the encrypted-at-rest handling of that key material is covered by Securing Aviation Maintenance Databases.

The handshake

Mutual-TLS handshakeClientcertServercertmTLShandshakeTrustedchannel

Step-by-step implementation

  1. Require client auth at the socket. The server’s TLS context is configured with verify_mode = CERT_REQUIRED and loaded with the trusted CA chain. A client that presents no certificate — or one signed by an untrusted CA — is dropped during the handshake, long before any request routing.
  2. Pin the protocol floor. Disable everything below TLS 1.2. An MRO data plane has no legacy browsers to accommodate, so there is no reason to negotiate a weak protocol.
  3. Extract the verified identity. After the handshake, read the peer certificate the TLS layer already validated and map its subject to a known client. Trust the verified certificate object, never a client-supplied header claiming an identity.
  4. Check validity windows and revocation. Confirm the certificate is inside its notBefore/notAfter window and is not on the revocation list before admitting the connection to the application.
  5. Record the binding. Log the client identifier, the certificate serial, and a fingerprint against each upload so a later audit can tie record WO-34-2207 to the exact credential that delivered it.

The following module verifies the peer certificate the TLS layer has already authenticated and turns it into a structured, logged admission decision.

from __future__ import annotations

import logging
import ssl
from datetime import datetime, timezone
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("mro.gateway.mtls")


def build_server_context(certfile: str, keyfile: str, ca_bundle: str) -> ssl.SSLContext:
    """TLS 1.2+ context that mandates a client certificate.

    Records must be protected from unauthorised access per
    EASA Part-145 145.A.55 (maintenance organisation records).
    """
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.minimum_version = ssl.TLSVersion.TLSv1_2
    ctx.load_cert_chain(certfile=certfile, keyfile=keyfile)
    ctx.load_verify_locations(cafile=ca_bundle)
    ctx.verify_mode = ssl.CERT_REQUIRED
    return ctx


class PeerIdentity(BaseModel):
    """Structured view of a verified client certificate."""

    model_config = ConfigDict(strict=True, extra="forbid")

    common_name: str = Field(min_length=3, max_length=64)
    serial_number: str = Field(min_length=1)
    not_after: datetime

    @field_validator("not_after", mode="before")
    @classmethod
    def to_utc(cls, v: object) -> datetime:
        if isinstance(v, str):
            # cpython presents notAfter as e.g. "Aug 14 08:00:00 2026 GMT"
            return datetime.strptime(v, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
        if isinstance(v, datetime):
            return v.astimezone(timezone.utc)
        raise ValueError("not_after must be a certificate date string")


def admit_peer(peer_cert: Optional[dict], allowed_cns: set[str], registration: str) -> bool:
    """Map a TLS-verified peer certificate to an authorised client."""
    if not peer_cert:
        logger.error("NO_CLIENT_CERT reg=%s", registration)
        return False

    subject = dict(x[0] for x in peer_cert.get("subject", ()))
    identity = PeerIdentity(
        common_name=subject.get("commonName", ""),
        serial_number=peer_cert.get("serialNumber", ""),
        not_after=peer_cert.get("notAfter", ""),
    )

    if datetime.now(timezone.utc) >= identity.not_after:
        logger.warning("CERT_EXPIRED reg=%s cn=%s", registration, identity.common_name)
        return False
    if identity.common_name not in allowed_cns:
        logger.warning("UNKNOWN_CLIENT reg=%s cn=%s", registration, identity.common_name)
        return False

    logger.info(
        "ADMITTED reg=%s cn=%s serial=%s", registration, identity.common_name, identity.serial_number
    )
    return True

Worked example

The hangar gateway for G-TUIA opens a connection presenting a leaf certificate whose common name is client-ln-hangar-7, serial 0A:F2:19:C4, valid through Aug 14 08:00:00 2026 GMT. The TLS layer validates the chain to the internal CA during the handshake; admit_peer then reads the verified certificate, confirms the notAfter is in the future, and finds client-ln-hangar-7 in the allowed set. The log emits ADMITTED reg=G-TUIA cn=client-ln-hangar-7 serial=0A:F2:19:C4 and the ATA 34 avionics-swap record for part HG2110-08 is accepted.

A second connection arrives from a host that copied a valid bearer token but has no client key. Because verify_mode is CERT_REQUIRED, that connection never completes the handshake — admit_peer is never even reached. This is the essential property: a stolen application-layer credential is useless without the private key that anchors the channel.

Edge cases & compliance gotchas

  • Header spoofing behind a terminating proxy. If TLS is terminated at a load balancer, the application only sees whatever header the proxy injects. Verify that the proxy strips any inbound X-Client-Cert header from untrusted callers and re-sets it from the certificate it validated; otherwise a client can forge its own identity.
  • Certificate expiry as an outage. Short-dated leaves are a security win but a reliability hazard if rotation lags. Alert on certificates within seven days of notAfter and reissue from the CA automatically, the same discipline applied to credential lifetimes in Token Rotation for MRO Data APIs.
  • Revocation is not optional. When a hangar laptop is lost, expiry alone is too slow. Publish and check a revocation list (or a short-lived status response) so a compromised leaf is refused within minutes, not at end of its 90-day life.
  • Clock and locale in date parsing. OpenSSL renders notAfter in an English-month, GMT format. Parse it as UTC explicitly and store it in ISO 8601; a server in a non-English locale that parses it with the system locale will silently misread the month and admit an expired certificate.

Mutual TLS makes identity a property of the channel itself rather than a claim inside the payload. Paired with rotating access tokens for fine-grained authorization and an encrypted store for the keys, it ensures that every maintenance record entering the gateway arrives over a channel where both ends have proven, cryptographically, exactly who they are.