Skip to content

Commit

Permalink
fix: fallback to hard-coded well-known ca-certificates paths if probe…
Browse files Browse the repository at this point in the history
…d paths are not usable

In some cases we apparently dlopen the bundled libcrypto.so.3 (because
the system is not having a libcrypto.so installed), so the paths could
be unreliable and absent on the running system even if they're probed.

Fall back to a list of well-known paths in this case.

Fixes: ruyisdk#103
  • Loading branch information
xen0n committed Mar 25, 2024
1 parent c91265c commit d697058
Showing 1 changed file with 75 additions and 19 deletions.
94 changes: 75 additions & 19 deletions ruyi/utils/ssl_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import ssl
import sys
from typing import NamedTuple

from .. import log

Expand All @@ -12,29 +13,38 @@
def get_system_ssl_default_verify_paths() -> ssl.DefaultVerifyPaths:
global _cached_paths

if _cached_paths is None:
_cached_paths = _get_system_ssl_default_verify_paths()
return _cached_paths


def _get_system_ssl_default_verify_paths() -> ssl.DefaultVerifyPaths:
orig_paths = _orig_get_default_verify_paths()
if sys.platform != "linux":
return orig_paths

if _cached_paths is not None:
return _cached_paths
result: ssl.DefaultVerifyPaths | None = None

# imitate the stdlib flow but with overridden data source
try:
parts = _query_linux_system_ssl_default_cert_paths()
if parts is None:
log.W("failed to probe system libcrypto")
else:
result = to_ssl_paths(parts)
except Exception as e:
log.D(f"cannot get system libssl default cert paths: {e}")
return orig_paths
log.D(f"cannot get system libcrypto default cert paths: {e}")

cafile = os.environ.get(parts[0], parts[1])
capath = os.environ.get(parts[2], parts[3])
if result is None:
log.D("falling back to probing hard-coded paths")
result = probe_fallback_verify_paths()

# must do "else None" like the stdlib, despite the type annotation being just "str"
result = ssl.DefaultVerifyPaths(
cafile if os.path.isfile(cafile) else None, # type: ignore[arg-type]
capath if os.path.isdir(capath) else None, # type: ignore[arg-type]
*parts,
)
if result is None:
# cannot proceed without certificates info (pygit2 initialization is
# bound to fail anyway)
log.F("cannot find the system libcrypto and/or TLS certificates")
log.I("TLS certificates and library are required for Ruyi to function")
raise SystemExit(1)

if result != orig_paths:
log.D(
Expand All @@ -43,10 +53,26 @@ def get_system_ssl_default_verify_paths() -> ssl.DefaultVerifyPaths:
log.D(f"bundled: {orig_paths}")
log.D(f" system: {result}")

_cached_paths = result
return result


def to_ssl_paths(parts: tuple[str, str, str, str]) -> ssl.DefaultVerifyPaths | None:
cafile = os.environ.get(parts[0], parts[1])
capath = os.environ.get(parts[2], parts[3])

is_cafile_present = os.path.isfile(cafile)
is_capath_present = os.path.isdir(capath)
if not is_cafile_present and not is_capath_present:
return None

# must do "else None" like the stdlib, despite the type annotation being just "str"
return ssl.DefaultVerifyPaths(
cafile if is_cafile_present else None, # type: ignore[arg-type]
capath if is_capath_present else None, # type: ignore[arg-type]
*parts,
)


def _decode_fsdefault_or_none(val: int | None) -> str:
if val is None:
return ""
Expand All @@ -60,7 +86,7 @@ def _decode_fsdefault_or_none(val: int | None) -> str:

def _query_linux_system_ssl_default_cert_paths(
soname: str | None = None,
) -> tuple[str, str, str, str]:
) -> tuple[str, str, str, str] | None:
if soname is None:
# check libcrypto instead of libssl, because if the system libssl is
# newer than the bundled one, the system libssl will depend on the
Expand All @@ -73,11 +99,7 @@ def _query_linux_system_ssl_default_cert_paths(
log.D(f"soname {soname} not working: {e}")
continue

# cannot proceed without certificates info (pygit2 initialization is
# bound to fail anyway)
log.F("cannot find the system libcrypto")
log.I("TLS certificates and library are required for Ruyi to function")
raise SystemExit(1)
return None

# dlopen-ing the bare soname will get us the system library
lib = ctypes.CDLL(soname)
Expand All @@ -102,4 +124,38 @@ def _query_linux_system_ssl_default_cert_paths(
return result


class WellKnownCALocation(NamedTuple):
cafile: str
capath: str


WELL_KNOWN_CA_LOCATIONS: list[WellKnownCALocation] = [
# Most others
WellKnownCALocation("/etc/ssl/cert.pem", "/etc/ssl/certs"),
# Debian-based distros
WellKnownCALocation("/usr/lib/ssl/cert.pem", "/usr/lib/ssl/certs"),
# RPM-based distros
WellKnownCALocation("/etc/pki/tls/cert.pem", "/etc/pki/tls/certs"),
]


def probe_fallback_verify_paths() -> ssl.DefaultVerifyPaths | None:
for loc in WELL_KNOWN_CA_LOCATIONS:
is_file_present = os.path.isfile(loc.cafile)
is_dir_present = os.path.isdir(loc.capath)
if not is_file_present and not is_dir_present:
continue

return ssl.DefaultVerifyPaths(
loc.cafile if is_file_present else None, # type: ignore[arg-type]
loc.capath if is_dir_present else None, # type: ignore[arg-type]
"SSL_CERT_FILE",
loc.cafile,
"SSL_CERT_DIR",
loc.capath,
)

return None


ssl.get_default_verify_paths = get_system_ssl_default_verify_paths

0 comments on commit d697058

Please sign in to comment.