168 lines
5.8 KiB
Python
168 lines
5.8 KiB
Python
"""Immutable authentication providers for DTLS sessions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from os import PathLike
|
|
from pathlib import Path
|
|
from typing import Protocol, runtime_checkable
|
|
|
|
from OpenSSL import SSL, crypto
|
|
|
|
_OCF_ROOT_CA = str(Path(__file__).with_name("ocf_root_ca.pem"))
|
|
_DTLS_CIPHERS = b"ECDHE-ECDSA-AES128-GCM-SHA256:@SECLEVEL=0"
|
|
_PEM_CERT_RE = re.compile(
|
|
rb"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def _verify_peer(_connection, _certificate, _error, _depth, ok):
|
|
"""Keep pyOpenSSL's existing verification result unchanged."""
|
|
return ok
|
|
|
|
|
|
def _load_pem_chain(ctx: SSL.Context, cert_pem: str, key_pem: str) -> None:
|
|
"""Load a PEM certificate chain and private key into a context in memory."""
|
|
certificates = _PEM_CERT_RE.findall(cert_pem.encode())
|
|
if not certificates:
|
|
raise ValueError("No certificates found in cert_pem")
|
|
ctx.use_certificate(
|
|
crypto.load_certificate(crypto.FILETYPE_PEM, certificates[0])
|
|
)
|
|
for extra in certificates[1:]:
|
|
ctx.add_extra_chain_cert(
|
|
crypto.load_certificate(crypto.FILETYPE_PEM, extra)
|
|
)
|
|
ctx.use_privatekey(
|
|
crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem.encode())
|
|
)
|
|
ctx.check_privatekey()
|
|
|
|
|
|
@runtime_checkable
|
|
class AuthenticationProvider(Protocol):
|
|
"""Configure authentication for a newly created DTLS context."""
|
|
|
|
def configure_context(self, context: SSL.Context) -> None:
|
|
"""Configure trust, verification, ciphers, and client credentials."""
|
|
|
|
|
|
class CertificateAuth:
|
|
"""Certificate authentication loaded from files or in-memory PEM data.
|
|
|
|
Use :meth:`from_files` or :meth:`from_memory` to create an instance.
|
|
Credential sources are intentionally not exposed as public attributes.
|
|
"""
|
|
|
|
__slots__ = (
|
|
"_certificate_path",
|
|
"_certificate_pem",
|
|
"_private_key_path",
|
|
"_private_key_pem",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
certificate_path: str | PathLike[str] | None = None,
|
|
private_key_path: str | PathLike[str] | None = None,
|
|
certificate_pem: str | None = None,
|
|
private_key_pem: str | None = None,
|
|
) -> None:
|
|
file_supplied = (
|
|
certificate_path is not None or private_key_path is not None
|
|
)
|
|
memory_supplied = (
|
|
certificate_pem is not None or private_key_pem is not None
|
|
)
|
|
if file_supplied and memory_supplied:
|
|
raise ValueError(
|
|
"pass either certificate_path/private_key_path or "
|
|
"certificate_pem/private_key_pem, not both"
|
|
)
|
|
if file_supplied:
|
|
if certificate_path is None or private_key_path is None:
|
|
raise ValueError(
|
|
"certificate_path and private_key_path must be passed together"
|
|
)
|
|
elif memory_supplied:
|
|
if certificate_pem is None or private_key_pem is None:
|
|
raise ValueError(
|
|
"certificate_pem and private_key_pem must be passed together"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
"must pass either certificate_path/private_key_path or "
|
|
"certificate_pem/private_key_pem"
|
|
)
|
|
object.__setattr__(
|
|
self,
|
|
"_certificate_path",
|
|
str(certificate_path) if certificate_path is not None else None,
|
|
)
|
|
object.__setattr__(
|
|
self,
|
|
"_private_key_path",
|
|
str(private_key_path) if private_key_path is not None else None,
|
|
)
|
|
object.__setattr__(self, "_certificate_pem", certificate_pem)
|
|
object.__setattr__(self, "_private_key_pem", private_key_pem)
|
|
|
|
def __setattr__(self, _name: str, _value: object) -> None:
|
|
raise AttributeError("CertificateAuth is immutable")
|
|
|
|
def __delattr__(self, _name: str) -> None:
|
|
raise AttributeError("CertificateAuth is immutable")
|
|
|
|
@classmethod
|
|
def from_files(
|
|
cls,
|
|
certificate_path: str | PathLike[str],
|
|
private_key_path: str | PathLike[str],
|
|
) -> CertificateAuth:
|
|
"""Create a provider backed by certificate-chain and key files."""
|
|
return cls(
|
|
certificate_path=certificate_path,
|
|
private_key_path=private_key_path,
|
|
)
|
|
|
|
@classmethod
|
|
def from_memory(
|
|
cls,
|
|
certificate_pem: str,
|
|
private_key_pem: str,
|
|
) -> CertificateAuth:
|
|
"""Create a provider backed by an in-memory PEM chain and key."""
|
|
return cls(
|
|
certificate_pem=certificate_pem,
|
|
private_key_pem=private_key_pem,
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
"""Return a representation that never includes credential material."""
|
|
return "CertificateAuth()"
|
|
|
|
def configure_context(self, context: SSL.Context) -> None:
|
|
"""Apply the existing certificate authentication profile to a context."""
|
|
context.load_verify_locations(_OCF_ROOT_CA)
|
|
context.set_verify(SSL.VERIFY_PEER, _verify_peer)
|
|
# @SECLEVEL=0 permits SHA-1 in Samsung's server cert chain (AC14K_M
|
|
# intermediate is SHA-1 signed). This is the only channel that reaches
|
|
# the OpenSSL instance cryptography bundles; ctypes and cffi bindings
|
|
# do not expose SSL_CTX_set_security_level on this build.
|
|
context.set_cipher_list(_DTLS_CIPHERS)
|
|
if self._certificate_pem is not None:
|
|
_load_pem_chain(
|
|
context,
|
|
self._certificate_pem,
|
|
self._private_key_pem,
|
|
)
|
|
else:
|
|
context.use_certificate_chain_file(self._certificate_path)
|
|
context.use_privatekey_file(self._private_key_path)
|
|
context.check_privatekey()
|
|
|
|
|
|
__all__ = ["AuthenticationProvider", "CertificateAuth"]
|