Source code for oso.framework.auth.mtls

#
# (c) Copyright IBM Corp. 2025
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


"""mTLS (Mutual Transport Layer Security) Authentication Handler.

Attributes
----------
NAME : str
    Constaint equal to ``mtls``, the name of this handler.

HEADER_SSL_VERIFY : str
    Constant equal to ``X-SSL-VERIFY`` header key. This header's value should be
    set by the TLS terminator, with a `MTLS.SSL_VERIFY_SUCCESS` value being
    authorized.

HEADER_SSL_CERT : str
    Constant equal to ``X-SSL-CERT`` header key. This header's value should be
    set by the TLS terminator, with a url-encoded certificate string.

SSL_VERIFY_SUCCESS : str
    Constant equal to ``SUCCESS``. This is the authorized value.

SSL_VERIFY_MISSING : str
    Constant equal to ``FAILED: Header missing from request``. This is the
    default header value.

    .. note:

        Nginx tags the reason for failure after the ``FAILED: `` prefix on
        versions 1.11.7 and onwards. This class is mimicing that behavior. See
        https://nginx.org/en/docs/http/ngx_http_ssl_module.html for more
        details.

OPENSSH_FINGERPRINT_HEADER : str
    Constant equal to ``SHA256:``, which is the prefix for the OpenSSH fingerprint type.

MD5_FINGERPRINT_HEADER : str
    Constant equal to ``MD5:``, which is the prefix for the MD5 fingerprint type.
"""

from __future__ import annotations


from base64 import b64decode
from typing import Final
from urllib.parse import unquote_to_bytes

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.x509 import Certificate, load_pem_x509_certificate
from flask import Request

from oso.framework.core.logging import get_logger
from .common import EXT_NAME, BaseParserConfig


NAME: Final[str] = __name__.split(".")[-1]
HEADER_SSL_VERIFY: Final[str] = "X-SSL-CLIENT-VERIFY"
HEADER_SSL_CERT: Final[str] = "X-SSL-CERT"
SSL_VERIFY_SUCCESS: Final[str] = "SUCCESS"
SSL_VERIFY_MISSING: Final[str] = "FAILED: Header missing from request"
OPENSSH_FINGERPRINT_HEADER: Final[str] = "SHA256:"
MD5_FINGERPRINT_HEADER: Final[str] = "MD5:"

logger = get_logger(EXT_NAME + "-" + NAME)


class _MtlsConfig(BaseParserConfig):
    pass


[docs] def parse_allowlist(allowlist: list[str]) -> list[bytes]: """Parse allowlist.""" return [load_fingerprint(fp) for fp in allowlist]
[docs] def load_fingerprint(hash: str) -> bytes: """Load fingerprint.""" logger.debug(f"Loading {hash}") if hash.startswith(OPENSSH_FINGERPRINT_HEADER): hash = hash.removeprefix(OPENSSH_FINGERPRINT_HEADER) pad = "=" * (len(hash) % 4) return b64decode(hash + pad) raise TypeError("Invalid fingerprint format. Supported: OpenSSH.")
[docs] def parse(request: Request) -> dict: """Return an AuthResult.""" data = {} data["errors"] = [] data["authorized_header"] = request.headers.get( HEADER_SSL_VERIFY, SSL_VERIFY_MISSING, ) data["authorized"] = data["authorized_header"] == SSL_VERIFY_SUCCESS if not data["authorized"]: data["errors"].append("Invalid or missing Nginx SSL verification") return data cert_string = request.headers.get(HEADER_SSL_CERT, "") try: data["cert"] = load_pem_x509_certificate(unquote_to_bytes(cert_string)) except TypeError: data["errors"].append("Invalid or missing certificate") return data except Exception: data["errors"].append("Internal Server Error") return data data["fingerprint"] = data["_user"] = parse_user_fingerprint(data["cert"]) data["subject"] = parse_user_subject(data["cert"]) return data
[docs] def parse_user_fingerprint(cert: Certificate) -> bytes: """Calculate the user's public key fingerprint. Parameters ---------- cert : `~cryptography.x509.Certificate` The user's public X.509 certificate. Returns ------- str The user's public key fingerprint in OpenSSH format. """ hash = hashes.Hash(hashes.SHA256()) hash.update( b64decode( cert.public_key() .public_bytes( encoding=serialization.Encoding.OpenSSH, format=serialization.PublicFormat.OpenSSH, ) .split(b" ")[1] ) ) return hash.finalize()
[docs] def parse_user_subject(cert: Certificate) -> str: """Retrive the certificate's subject line as a string. Parameters ---------- cert : `cryptography.x509.Certificate` The user's public X.509 certificate. Returns ------- str: The user's subject line in string format. """ return cert.subject.rfc4514_string()