fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
113
.venv/lib/python3.9/site-packages/wassima/__init__.py
Normal file
113
.venv/lib/python3.9/site-packages/wassima/__init__.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
the Wassima library is a simple library.
|
||||
It aims to provide a pythonic way to retrieve root CAs from your system without any difficulties or hazmat.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
from functools import lru_cache
|
||||
from threading import RLock
|
||||
|
||||
from ._os import (
|
||||
root_der_certificates as _root_der_certificates,
|
||||
)
|
||||
from ._os._embed import root_der_certificates as fallback_der_certificates
|
||||
from ._version import VERSION, __version__
|
||||
|
||||
# Mozilla TLS recommendations for ciphers
|
||||
# General-purpose servers with a variety of clients, recommended for almost all systems.
|
||||
MOZ_INTERMEDIATE_CIPHERS: str = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305" # noqa: E501
|
||||
#: Contain user custom CAs
|
||||
_MANUALLY_REGISTERED_CA: list[bytes] = []
|
||||
#: Lock for shared register-ca
|
||||
_USER_APPEND_CA_LOCK = RLock()
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def root_der_certificates() -> list[bytes]:
|
||||
with _USER_APPEND_CA_LOCK:
|
||||
certificates = _root_der_certificates()
|
||||
|
||||
if not certificates:
|
||||
certificates = fallback_der_certificates()
|
||||
|
||||
certificates.extend(_MANUALLY_REGISTERED_CA)
|
||||
|
||||
return certificates
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def root_pem_certificates() -> list[str]:
|
||||
"""
|
||||
Retrieve a list of root certificate from your operating system trust store.
|
||||
They will be PEM encoded.
|
||||
"""
|
||||
pem_certs = []
|
||||
|
||||
for bin_cert in root_der_certificates():
|
||||
pem_certs.append(ssl.DER_cert_to_PEM_cert(bin_cert))
|
||||
|
||||
return pem_certs
|
||||
|
||||
|
||||
def generate_ca_bundle() -> str:
|
||||
"""
|
||||
Generate an aggregated CA bundle that originate from your system trust store.
|
||||
Simply put, concatenated root PEM certificate.
|
||||
"""
|
||||
return "\n\n".join(root_pem_certificates())
|
||||
|
||||
|
||||
def register_ca(pem_or_der_certificate: bytes | str) -> None:
|
||||
"""
|
||||
You may register your own CA certificate in addition to your system trust store.
|
||||
"""
|
||||
with _USER_APPEND_CA_LOCK:
|
||||
if isinstance(pem_or_der_certificate, str):
|
||||
pem_or_der_certificate = ssl.PEM_cert_to_DER_cert(pem_or_der_certificate)
|
||||
|
||||
if pem_or_der_certificate not in _MANUALLY_REGISTERED_CA:
|
||||
_MANUALLY_REGISTERED_CA.append(pem_or_der_certificate)
|
||||
|
||||
root_pem_certificates.cache_clear()
|
||||
root_der_certificates.cache_clear()
|
||||
|
||||
|
||||
def create_default_ssl_context() -> ssl.SSLContext:
|
||||
"""
|
||||
Instantiate a native SSLContext (client purposes) that ships with your system root CAs.
|
||||
In addition to that, assign it the default OpenSSL ciphers suite and set
|
||||
TLS 1.2 as the minimum supported version. Also disable commonName check and enforce
|
||||
hostname altName verification. The Mozilla Recommended Cipher Suite is used instead of system default.
|
||||
"""
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
|
||||
ctx.load_verify_locations(cadata=generate_ca_bundle())
|
||||
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
ctx.set_ciphers(MOZ_INTERMEDIATE_CIPHERS)
|
||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
|
||||
try:
|
||||
ctx.hostname_checks_common_name = False
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
ctx.check_hostname = True
|
||||
except AttributeError: # Defensive: very old 3.7 branch
|
||||
pass
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
__all__ = (
|
||||
"root_der_certificates",
|
||||
"root_pem_certificates",
|
||||
"generate_ca_bundle",
|
||||
"create_default_ssl_context",
|
||||
"register_ca",
|
||||
"__version__",
|
||||
"VERSION",
|
||||
)
|
||||
30
.venv/lib/python3.9/site-packages/wassima/_os/__init__.py
Normal file
30
.venv/lib/python3.9/site-packages/wassima/_os/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import sys
|
||||
|
||||
# Platform detection
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
IS_MACOS = sys.platform == "darwin"
|
||||
IS_LINUX = sys.platform.startswith("linux")
|
||||
IS_BSD = sys.platform.startswith(("freebsd", "openbsd", "netbsd"))
|
||||
|
||||
# macOS version detection
|
||||
MACOS_VERSION: tuple[int, ...] | None = None
|
||||
|
||||
if IS_MACOS:
|
||||
version_str = platform.mac_ver()[0]
|
||||
MACOS_VERSION = tuple(map(int, version_str.split(".")))
|
||||
|
||||
|
||||
if IS_WINDOWS:
|
||||
from ._windows import root_der_certificates
|
||||
elif IS_MACOS and MACOS_VERSION >= (10, 15): # type: ignore[operator]
|
||||
from ._macos import root_der_certificates
|
||||
elif IS_LINUX or IS_BSD:
|
||||
from ._linux import root_der_certificates
|
||||
else:
|
||||
from ._embed import root_der_certificates
|
||||
|
||||
|
||||
__all__ = ("root_der_certificates",)
|
||||
4054
.venv/lib/python3.9/site-packages/wassima/_os/_embed.py
Normal file
4054
.venv/lib/python3.9/site-packages/wassima/_os/_embed.py
Normal file
File diff suppressed because it is too large
Load Diff
95
.venv/lib/python3.9/site-packages/wassima/_os/_linux.py
Normal file
95
.venv/lib/python3.9/site-packages/wassima/_os/_linux.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from ssl import PEM_cert_to_DER_cert
|
||||
|
||||
# source: http://gagravarr.org/writing/openssl-certs/others.shtml
|
||||
BUNDLE_TRUST_STORE_DIRECTORIES: list[str] = [
|
||||
"/var/ssl",
|
||||
"/usr/share/ssl",
|
||||
"/usr/local/ssl",
|
||||
"/usr/local/openssl",
|
||||
"/usr/local/etc/openssl",
|
||||
"/usr/local/share/certs",
|
||||
"/usr/lib/ssl",
|
||||
"/usr/ssl",
|
||||
"/etc/openssl",
|
||||
"/etc/pki/ca-trust/extracted/pem",
|
||||
"/etc/pki/tls",
|
||||
"/etc/ssl",
|
||||
"/etc/certs",
|
||||
"/opt/etc/ssl",
|
||||
"/system/etc/security/cacerts",
|
||||
"/boot/system/data/ssl",
|
||||
]
|
||||
|
||||
KNOWN_TRUST_STORE_EXTENSIONS: list[str] = [
|
||||
"pem",
|
||||
"crt",
|
||||
]
|
||||
|
||||
BANNED_KEYWORD_NOT_TLS: set[str] = {
|
||||
"email",
|
||||
"objsign",
|
||||
"trust",
|
||||
"timestamp",
|
||||
"codesign",
|
||||
"ocsp",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
||||
def root_der_certificates() -> list[bytes]:
|
||||
certificates: list[bytes] = []
|
||||
|
||||
for directory in BUNDLE_TRUST_STORE_DIRECTORIES:
|
||||
if not os.path.exists(directory):
|
||||
continue
|
||||
|
||||
# Use rglob to recursively search all files in directory and subdirectories
|
||||
for filepath in Path(directory).rglob("*"):
|
||||
try:
|
||||
if not filepath.is_file(): # Skip directories
|
||||
continue
|
||||
|
||||
extension = filepath.suffix.lstrip(".").lower()
|
||||
|
||||
if extension not in KNOWN_TRUST_STORE_EXTENSIONS and extension.isdigit() is False:
|
||||
continue
|
||||
|
||||
if any(kw in str(filepath).lower() for kw in BANNED_KEYWORD_NOT_TLS):
|
||||
continue
|
||||
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
bundle = f.read()
|
||||
|
||||
if not bundle.strip(): # Skip empty files
|
||||
continue # Defensive:
|
||||
|
||||
line_ending = "\n" if "-----END CERTIFICATE-----\r\n" not in bundle else "\r\n"
|
||||
boundary = "-----END CERTIFICATE-----" + line_ending
|
||||
|
||||
for chunk in bundle.split(boundary):
|
||||
if chunk:
|
||||
start_marker = chunk.find("-----BEGIN CERTIFICATE-----" + line_ending)
|
||||
|
||||
if start_marker == -1:
|
||||
break # Defensive: file that aren't PEM encoded in target directories(...)
|
||||
|
||||
pem_reconstructed = "".join([chunk[start_marker:], boundary])
|
||||
|
||||
try:
|
||||
der_certificate = PEM_cert_to_DER_cert(pem_reconstructed)
|
||||
except ValueError: # Defensive: malformed cert/base64?
|
||||
continue
|
||||
|
||||
if der_certificate not in certificates:
|
||||
certificates.append(der_certificate)
|
||||
|
||||
except (OSError, UnicodeDecodeError): # Defensive: Skip files we can't read
|
||||
# OSError -> e.g. PermissionError
|
||||
# UnicodeDecodeError -> DER ASN.1 encoded
|
||||
continue
|
||||
|
||||
return certificates
|
||||
142
.venv/lib/python3.9/site-packages/wassima/_os/_macos.py
Normal file
142
.venv/lib/python3.9/site-packages/wassima/_os/_macos.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
from ctypes import POINTER, byref, c_int32, c_uint32, c_void_p
|
||||
|
||||
# Load frameworks
|
||||
_core = ctypes.CDLL("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
|
||||
_sec = ctypes.CDLL("/System/Library/Frameworks/Security.framework/Security")
|
||||
|
||||
# Type aliases
|
||||
CFTypeRef = c_void_p
|
||||
CFArrayRef = c_void_p
|
||||
CFDataRef = c_void_p
|
||||
CFDictionaryRef = c_void_p
|
||||
OSStatus = c_int32
|
||||
|
||||
# CoreFoundation function prototypes
|
||||
_CFDictionaryCreate = _core.CFDictionaryCreate
|
||||
_CFDictionaryCreate.argtypes = [c_void_p, POINTER(c_void_p), POINTER(c_void_p), c_uint32, c_void_p, c_void_p]
|
||||
_CFDictionaryCreate.restype = CFDictionaryRef
|
||||
|
||||
_CFArrayGetCount = _core.CFArrayGetCount
|
||||
_CFArrayGetCount.argtypes = [CFArrayRef]
|
||||
_CFArrayGetCount.restype = c_uint32
|
||||
|
||||
_CFArrayGetValueAtIndex = _core.CFArrayGetValueAtIndex
|
||||
_CFArrayGetValueAtIndex.argtypes = [CFArrayRef, c_uint32]
|
||||
_CFArrayGetValueAtIndex.restype = CFTypeRef
|
||||
|
||||
_CFDataGetLength = _core.CFDataGetLength
|
||||
_CFDataGetLength.argtypes = [CFDataRef]
|
||||
_CFDataGetLength.restype = c_uint32
|
||||
|
||||
_CFDataGetBytePtr = _core.CFDataGetBytePtr
|
||||
_CFDataGetBytePtr.argtypes = [CFDataRef]
|
||||
_CFDataGetBytePtr.restype = ctypes.POINTER(ctypes.c_ubyte)
|
||||
|
||||
_CFRelease = _core.CFRelease
|
||||
_CFRelease.argtypes = [c_void_p]
|
||||
_CFRelease.restype = None
|
||||
|
||||
# Security function prototypes
|
||||
_SecItemCopyMatching = _sec.SecItemCopyMatching
|
||||
_SecItemCopyMatching.argtypes = [CFDictionaryRef, POINTER(CFTypeRef)]
|
||||
_SecItemCopyMatching.restype = OSStatus
|
||||
|
||||
_SecCertificateCopyData = _sec.SecCertificateCopyData
|
||||
_SecCertificateCopyData.argtypes = [CFTypeRef]
|
||||
_SecCertificateCopyData.restype = CFDataRef
|
||||
|
||||
_SecTrustSettingsCopyCertificates = _sec.SecTrustSettingsCopyCertificates
|
||||
_SecTrustSettingsCopyCertificates.argtypes = [c_int32, POINTER(CFArrayRef)]
|
||||
_SecTrustSettingsCopyCertificates.restype = OSStatus
|
||||
|
||||
# CF callbacks & boolean constants
|
||||
_kCFTypeDictKeyCallBacks = c_void_p.in_dll(_core, "kCFTypeDictionaryKeyCallBacks")
|
||||
_kCFTypeDictValueCallBacks = c_void_p.in_dll(_core, "kCFTypeDictionaryValueCallBacks")
|
||||
_kCFBooleanTrue = c_void_p.in_dll(_core, "kCFBooleanTrue")
|
||||
_kSecBooleanTrue = _kCFBooleanTrue
|
||||
|
||||
# SecItem constants
|
||||
_kSecClass = c_void_p.in_dll(_sec, "kSecClass")
|
||||
_kSecClassCertificate = c_void_p.in_dll(_sec, "kSecClassCertificate")
|
||||
_kSecMatchLimit = c_void_p.in_dll(_sec, "kSecMatchLimit")
|
||||
_kSecMatchLimitAll = c_void_p.in_dll(_sec, "kSecMatchLimitAll")
|
||||
_kSecMatchTrustedOnly = c_void_p.in_dll(_sec, "kSecMatchTrustedOnly")
|
||||
_kSecReturnRef = c_void_p.in_dll(_sec, "kSecReturnRef")
|
||||
|
||||
|
||||
# Helper: build a CFDictionary for SecItem queries
|
||||
def _make_query(keys: list[c_void_p], values: list[c_void_p]) -> CFDictionaryRef:
|
||||
count = len(keys)
|
||||
KeyArr = (c_void_p * count)(*keys)
|
||||
ValArr = (c_void_p * count)(*values)
|
||||
return _CFDictionaryCreate( # type: ignore[no-any-return]
|
||||
None,
|
||||
KeyArr,
|
||||
ValArr,
|
||||
count,
|
||||
_kCFTypeDictKeyCallBacks,
|
||||
_kCFTypeDictValueCallBacks,
|
||||
)
|
||||
|
||||
|
||||
# Helper: perform SecItemCopyMatching and return CFTypeRef list
|
||||
|
||||
|
||||
def _query_refs(query: CFDictionaryRef) -> list[CFTypeRef]:
|
||||
result = CFTypeRef()
|
||||
status = _SecItemCopyMatching(query, byref(result))
|
||||
|
||||
if status != 0:
|
||||
raise OSError(f"SecItemCopyMatching failed with status={status}") # Defensive: OOM?
|
||||
|
||||
array_ref = CFArrayRef(result.value)
|
||||
count = _CFArrayGetCount(array_ref)
|
||||
items = [_CFArrayGetValueAtIndex(array_ref, i) for i in range(count)]
|
||||
# Note: No CFRelease() calls to avoid premature deallocation
|
||||
return items
|
||||
|
||||
|
||||
# Convert CFDataRef to Python bytes
|
||||
def _data_to_bytes(data_ref: c_void_p) -> bytes:
|
||||
length = _CFDataGetLength(data_ref)
|
||||
ptr = _CFDataGetBytePtr(data_ref)
|
||||
data = bytes(ctypes.string_at(ptr, length))
|
||||
_CFRelease(data_ref)
|
||||
return data
|
||||
|
||||
|
||||
# Public: retrieve DER-encoded trusted certificates
|
||||
def root_der_certificates() -> list[bytes]:
|
||||
"""
|
||||
Returns a list of DER-encoded certificates trusted for TLS server auth,
|
||||
covering system roots, admin, user trust settings, and personal CAs.
|
||||
"""
|
||||
certificates: list[bytes] = []
|
||||
|
||||
# 1) System/user/admin trust settings
|
||||
for domain in (0, 1, 2):
|
||||
cert_array = CFArrayRef()
|
||||
status = _SecTrustSettingsCopyCertificates(domain, byref(cert_array))
|
||||
if status == 0:
|
||||
count = _CFArrayGetCount(cert_array)
|
||||
for i in range(count):
|
||||
cert_ref = _CFArrayGetValueAtIndex(cert_array, i)
|
||||
certificates.append(_data_to_bytes(_SecCertificateCopyData(cert_ref)))
|
||||
|
||||
# 2) Personal CA certificates from keychain marked trusted
|
||||
query = _make_query(
|
||||
keys=[_kSecClass, _kSecMatchLimit, _kSecMatchTrustedOnly, _kSecReturnRef],
|
||||
values=[_kSecClassCertificate, _kSecMatchLimitAll, _kSecBooleanTrue, _kSecReturnRef],
|
||||
)
|
||||
|
||||
try:
|
||||
cert_refs = _query_refs(query)
|
||||
for c in cert_refs:
|
||||
certificates.append(_data_to_bytes(_SecCertificateCopyData(c)))
|
||||
except OSError: # Defensive: OOM?
|
||||
pass
|
||||
|
||||
return certificates
|
||||
40
.venv/lib/python3.9/site-packages/wassima/_os/_windows.py
Normal file
40
.venv/lib/python3.9/site-packages/wassima/_os/_windows.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ssl import enum_certificates # type: ignore[attr-defined]
|
||||
|
||||
# ROOT: Highest level of trust. Trust anchors. Self-Signed.
|
||||
# MY: User installed/custom trust anchors. Self-Signed.
|
||||
# CA: Intermediates CA. Not trusted directly, not self-signed.
|
||||
WINDOWS_STORES: list[str] = [
|
||||
"ROOT",
|
||||
"MY",
|
||||
"CA",
|
||||
]
|
||||
SERVER_AUTH_OID: str = "1.3.6.1.5.5.7.3.1"
|
||||
|
||||
|
||||
def root_der_certificates() -> list[bytes]:
|
||||
certificates = []
|
||||
|
||||
for system_store in WINDOWS_STORES:
|
||||
try:
|
||||
for cert_bytes, encoding_type, trust in enum_certificates(system_store):
|
||||
if not trust:
|
||||
continue # Defensive: edge case, rare one.
|
||||
|
||||
# if not True, then, we MUST LOOK for SERVER_AUTH oid EKU
|
||||
if not isinstance(trust, bool) and SERVER_AUTH_OID not in trust:
|
||||
continue
|
||||
|
||||
# Check it's in X.509 ASN.1 format and is trusted
|
||||
if (
|
||||
encoding_type == "x509_asn" # X.509 ASN.1 data
|
||||
):
|
||||
certificates.append(cert_bytes)
|
||||
except PermissionError: # Defensive: we can't cover that scenario in CI.
|
||||
continue
|
||||
|
||||
return certificates
|
||||
|
||||
|
||||
__all__ = ("root_der_certificates",)
|
||||
4
.venv/lib/python3.9/site-packages/wassima/_version.py
Normal file
4
.venv/lib/python3.9/site-packages/wassima/_version.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "2.0.5"
|
||||
VERSION = __version__.split(".")
|
||||
0
.venv/lib/python3.9/site-packages/wassima/py.typed
Normal file
0
.venv/lib/python3.9/site-packages/wassima/py.typed
Normal file
Reference in New Issue
Block a user