diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42f0651f0..29f303ca4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "20" + node-version: "22.23.4" - run: npm ci --ignore-scripts --no-audit --no-fund - name: Check frontend architecture run: node scripts/check_frontend.mjs @@ -160,15 +160,18 @@ jobs: - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 if: steps.docs-check.outputs.docs_only != 'true' with: + version: "0.13.4" enable-cache: true cache-dependency-glob: requirements.txt prune-cache: false - - run: uv pip install --system -r requirements.txt + - run: | + uv pip install --system --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt + uv pip install --system -r requirements.txt if: steps.docs-check.outputs.docs_only != 'true' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 if: steps.docs-check.outputs.docs_only != 'true' with: - node-version: "20" + node-version: "22.23.4" cache: npm - run: npm ci if: steps.docs-check.outputs.docs_only != 'true' @@ -190,10 +193,14 @@ jobs: if: steps.docs-check.outputs.docs_only != 'true' run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends ffmpeg bubblewrap + sudo apt-get install -y --no-install-recommends ffmpeg bubblewrap ca-certificates command -v ffmpeg ffmpeg -version | head -n 1 + - name: Validate Python and system CA trust + if: steps.docs-check.outputs.docs_only != 'true' + run: python scripts/check_ca_trust.py --system --python + - name: Establish functional bubblewrap containment if: steps.docs-check.outputs.docs_only != 'true' shell: bash diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0e4f9a1f1..e78bba158 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -70,5 +70,9 @@ jobs: - name: Run pip-audit on requirements run: | set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends ca-certificates + python scripts/check_ca_trust.py --system + python -m pip install --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt pip install pip-audit==2.10.0 pip-audit -r requirements.txt -r requirements-optional.txt --strict diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index da41c7140..2cb8221ec 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -65,6 +65,7 @@ jobs: uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . + pull: true platforms: ${{ matrix.platform }} outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=gha,scope=${{ matrix.arch }} diff --git a/.github/workflows/workflow-security.yml b/.github/workflows/workflow-security.yml index b00cd03a4..18a197eb0 100644 --- a/.github/workflows/workflow-security.yml +++ b/.github/workflows/workflow-security.yml @@ -76,5 +76,9 @@ jobs: - name: Run zizmor run: | set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends ca-certificates + python scripts/check_ca_trust.py --system + python -m pip install --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt pip install zizmor==1.25.2 zizmor --offline --min-severity=low .github/workflows/ diff --git a/Dockerfile b/Dockerfile index d6e7ca48f..2e49c6ed6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,13 @@ # the final image / Cookbook never has to compile the broken sdists. See # docker/build-realesrgan-wheels.sh for the full rationale. FROM python:3.14-slim AS realesrgan-wheels -RUN apt-get update && apt-get install -y --no-install-recommends curl \ +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* +COPY scripts/check_ca_trust.py /usr/local/bin/check-ca-trust.py +RUN python /usr/local/bin/check-ca-trust.py --system +COPY requirements-bootstrap.txt /tmp/requirements-bootstrap.txt +RUN python -m pip install --no-cache-dir --require-hashes --only-binary=:all: --no-deps -r /tmp/requirements-bootstrap.txt \ + && python /usr/local/bin/check-ca-trust.py --python COPY docker/build-realesrgan-wheels.sh /usr/local/bin/build-realesrgan-wheels.sh RUN bash /usr/local/bin/build-realesrgan-wheels.sh /wheels @@ -28,6 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ cmake \ curl \ + ca-certificates \ git \ nodejs \ npm \ @@ -48,6 +54,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libmagic1 \ && rm -rf /var/lib/apt/lists/* +# Refresh both Python trust bundles before installing application dependencies. +# System roots also serve web fetches, webhooks, curl, and Node's OpenSSL mode. +COPY scripts/check_ca_trust.py /usr/local/bin/check-ca-trust.py +RUN python /usr/local/bin/check-ca-trust.py --system +COPY requirements-bootstrap.txt /tmp/requirements-bootstrap.txt +RUN python -m pip install --no-cache-dir --require-hashes --only-binary=:all: --no-deps -r /tmp/requirements-bootstrap.txt \ + && python /usr/local/bin/check-ca-trust.py --python +ENV NODE_OPTIONS=--use-openssl-ca + # Private browser automation wrapper used by the native `private_browser` tool. # Chromium is installed above, so agent-browser can drive the existing browser # binary without paying `npx` startup/install overhead on each tool call. diff --git a/build-windows-portable.ps1 b/build-windows-portable.ps1 index 7da912f73..634ae8068 100644 --- a/build-windows-portable.ps1 +++ b/build-windows-portable.ps1 @@ -47,7 +47,8 @@ if (-not $pyExe) { Write-Host ("Using Python: " + $pyExe) Write-Step "Installing build dependencies" -& $pyExe -m pip install --upgrade pip --quiet +& $pyExe -m pip install --quiet --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt +if ($LASTEXITCODE -ne 0) { Fail "TLS build dependency bootstrap failed." } & $pyExe -m pip install -r requirements.txt pyinstaller pystray Pillow if ($LASTEXITCODE -ne 0) { Fail "Dependency install failed." } diff --git a/docs/tls-trust.md b/docs/tls-trust.md new file mode 100644 index 000000000..3d91e08cd --- /dev/null +++ b/docs/tls-trust.md @@ -0,0 +1,34 @@ +# Outbound HTTPS trust + +Odysseus uses both Python package CA bundles and system trust. HTTPX uses certifi by default; the guarded web/PDF fetcher and webhook delivery explicitly use Python's system SSL context. A configured `LLM_CA_BUNDLE` adds certificates to system trust for model calls. Upgrading certifi alone cannot repair every outbound HTTPS path. + +For the October 2026 eMudhra incident, use certifi 2026.10.19 or newer, pip 26.2.2 or newer, and uv 0.13.4 or newer wherever uv is used. Pip has its own vendored certifi and also loads system roots. Update the operating system's certificate package separately. See the [Python security announcement](https://discuss.python.org/t/109576) and [incident details](https://emudhra.we-have-a-problem.com/). + +Before other pip installs, bootstrap the selected environment with the integrity-pinned wheels: + +```sh +python -m pip install --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt +python -m pip install -r requirements.txt +``` + +The pinned bootstrap protects package integrity even when the old installer's certificate bundle is affected. It is not an operating-system trust-store update. Native Windows/macOS trust and command-line OpenSSL PEM stores may differ; follow the platform vendor's remediation for every store in use. Restart Odysseus and its subprocesses after updating trust. + +On Linux/OpenSSL deployments, inspect both the effective CA file and hashed CA directory, plus the two Python bundles: + +```sh +python scripts/check_ca_trust.py --system --python +``` + +Explicit PEM bundles can also be passed as positional arguments. The check rejects the incident's known root fingerprints and malformed/empty bundles. It does not model native OS intermediate revocation, custom TLS implementations, or arbitrary certificates added by an operator. + +Both Docker build stages refresh system CA certificates and bootstrap pip/certifi before dependency installation. The build fails if affected roots remain. Node uses the corrected system store through `--use-openssl-ca`. Rebuild with a fresh base and dependency layers when remediating an existing deployment: + +```sh +docker compose build --pull --no-cache odysseus +docker compose up -d odysseus +docker compose exec odysseus python scripts/check_ca_trust.py --system --python +``` + +SearXNG is a separate HTTPS client and image; updating the Odysseus image does not patch it. Inspect its effective CA path and update or rebuild that image independently. Also inspect persisted `/app/.local` packages, remote Cookbook environments, and externally configured MCP runtimes. Image replacement does not necessarily replace those environments. + +For behavioral validation, all twelve [published test chains](https://emudhra.we-have-a-problem.com/chains.zip), including the AAA cross-signs, must fail certificate verification through the effective stores. Confirm that an ordinary known-good HTTPS request still succeeds. Package versions and a successful image build alone do not prove that a running deployment selects the corrected store. diff --git a/launch-windows.ps1 b/launch-windows.ps1 index ab0e3542b..31c45a5fb 100644 --- a/launch-windows.ps1 +++ b/launch-windows.ps1 @@ -131,7 +131,8 @@ if (-not (Test-Path $venvPy)) { # 3. Install / update dependencies Write-Step "Installing dependencies (first run can take a few minutes)" -& $venvPy -m pip install --upgrade pip --quiet +& $venvPy -m pip install --quiet --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt +if ($LASTEXITCODE -ne 0) { Fail "TLS dependency bootstrap failed." } & $venvPy -m pip install -r requirements.txt if ($LASTEXITCODE -ne 0) { Fail "Dependency install failed. Scroll up for the pip error." } diff --git a/requirements-bootstrap.txt b/requirements-bootstrap.txt new file mode 100644 index 000000000..8b39dc69b --- /dev/null +++ b/requirements-bootstrap.txt @@ -0,0 +1,8 @@ +# Integrity-pinned bootstrap for the October 2026 eMudhra CA incident. +# Install with --require-hashes --only-binary=:all: --no-deps before other pip work. +# pip vendors its own certifi; updating the application bundle cannot update it. +# Direct universal wheels avoid index resolution through the old trust store. +pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl \ + --hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70 +certifi @ https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl \ + --hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b diff --git a/requirements.txt b/requirements.txt index 7fabaae93..4d794537d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,8 @@ uvicorn python-multipart python-dotenv httpx +# HTTPS clients using certifi must distrust the compromised eMudhra CA paths. +certifi>=2026.10.19 httpcore>=1.0,<2.0 pydantic>=2.13.4 pydantic-settings>=2.14.1 diff --git a/scripts/check_ca_trust.py b/scripts/check_ca_trust.py new file mode 100644 index 000000000..c675dc6f3 --- /dev/null +++ b/scripts/check_ca_trust.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Reject the eMudhra incident's root certificates in PEM trust stores. + +This checks certificate contents, including OpenSSL's lazily loaded CA directory. +It does not model native OS certificate revocation or arbitrary custom clients. +Incident fingerprints: https://emudhra.we-have-a-problem.com/roots.json +""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib +import os +from pathlib import Path +import re +import ssl +import sys + + +BLOCKED_ROOTS = { + "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b": "emSign ECC Root CA - G3", + "d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4": "AAA Certificate Services", + "7dd78d5f4f13459a83dff9abbba62edbaf6f2d102bf257fd712f4d9f2746ed8d": "emSign Root TLS CA - G3", + "f2edaaff3d9b701fb4d14c9f285f8c0bad90fc3bcc1fa07d47f4718edaba34a5": "emSign Root CS CA - G3", +} + + +def check_bundle(path: Path) -> None: + """Raise on an unreadable, empty, malformed, or affected CA bundle.""" + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.load_verify_locations(cafile=str(path)) + certificates = context.get_ca_certs(binary_form=True) + if not certificates: + raise ValueError("no CA certificates") + blocked = { + BLOCKED_ROOTS[fingerprint] + for certificate in certificates + if (fingerprint := hashlib.sha256(certificate).hexdigest()) in BLOCKED_ROOTS + } + if blocked: + raise ValueError("affected roots: " + ", ".join(sorted(blocked))) + + +def system_paths() -> list[Path]: + """Inspect both the effective OpenSSL CA file and hashed directory entries.""" + defaults = ssl.get_default_verify_paths() + paths = [Path(defaults.cafile)] if defaults.cafile else [] + # get_default_verify_paths() can report None for a colon-separated + # SSL_CERT_DIR even though OpenSSL consumes each directory in that list. + capath = os.environ.get(defaults.openssl_capath_env, defaults.capath) + if capath: + for directory in filter(None, capath.split(os.pathsep)): + paths.extend( + path + for path in Path(directory).iterdir() + if re.fullmatch(r"[0-9a-fA-F]{8}\.[0-9]+", path.name) and path.is_file() + ) + if not paths: + raise ValueError("no effective OpenSSL PEM CA store found") + return paths + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("bundles", nargs="*", type=Path) + parser.add_argument("--system", action="store_true", help="check effective OpenSSL PEM stores") + parser.add_argument("--python", action="store_true", help="check certifi and pip's separate vendored bundle") + args = parser.parse_args(argv) + if not (args.bundles or args.system or args.python): + parser.error("select --system, --python, or explicit PEM bundle paths") + try: + paths = list(args.bundles) + if args.system: + paths.extend(system_paths()) + if args.python: + for name in ("certifi", "pip._vendor.certifi"): + paths.append(Path(importlib.import_module(name).where())) + paths = list(dict.fromkeys(path.resolve() for path in paths)) + errors = [] + for path in paths: + try: + check_bundle(path) + except (OSError, ValueError, ssl.SSLError) as exc: + errors.append(f"{path}: {exc}") + if errors: + raise ValueError("\n".join(errors)) + except (ImportError, OSError, ValueError, ssl.SSLError) as exc: + print(str(exc), file=sys.stderr) + return 1 + print(f"Validated {len(paths)} CA bundle file(s): no eMudhra incident roots") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/start-macos.sh b/start-macos.sh index 3e9048547..b9178792f 100755 --- a/start-macos.sh +++ b/start-macos.sh @@ -140,11 +140,11 @@ if [ ! -x "$VENV_PY" ] || ! "$VENV_PY" -m pip --version >/dev/null 2>&1; then echo "▶ Creating Python environment…" "$PY" -m venv venv fi -REQ_HASH="$(md5 -q requirements.txt 2>/dev/null || md5sum requirements.txt | cut -d' ' -f1)" +REQ_HASH="$("$VENV_PY" -c 'import hashlib, pathlib; print(hashlib.sha256(b"".join(pathlib.Path(p).read_bytes() for p in ("requirements.txt", "requirements-bootstrap.txt"))).hexdigest())')" REQ_HASH_FILE="venv/.requirements_hash" if [ ! -f "$REQ_HASH_FILE" ] || [ "$REQ_HASH" != "$(cat "$REQ_HASH_FILE" 2>/dev/null)" ]; then echo "▶ Installing Python packages (first run downloads a few — can take a few minutes)…" - "$VENV_PY" -m pip install --quiet --upgrade pip + "$VENV_PY" -m pip install --quiet --require-hashes --only-binary=:all: --no-deps -r requirements-bootstrap.txt # Not --quiet: this is the slow step, so show progress (and any real errors). "$VENV_PY" -m pip install -r requirements.txt echo "$REQ_HASH" > "$REQ_HASH_FILE" diff --git a/tests/fixtures/ca-trust/README.md b/tests/fixtures/ca-trust/README.md new file mode 100644 index 000000000..47e440ac1 --- /dev/null +++ b/tests/fixtures/ca-trust/README.md @@ -0,0 +1,5 @@ +# CA trust regression fixtures + +The public CA certificates embedded in `tests/test_ca_trust.py` exercise the October 2026 eMudhra trust-store check. They contain no private keys. `emsign-g3.pem` and `aaa.pem` are the incident's affected trust anchors; `emsign-g1.pem` is an unaffected emSign anchor that must remain usable. The test creates temporary PEM files from these public constants. The certificate fingerprints are the factual identity of each fixture. + +Sources: the [incident root list](https://emudhra.we-have-a-problem.com/roots.json) and the public Mozilla root certificates distributed in Debian's `ca-certificates` package. Behavioral chain verification should also cover the incident site's separate [twelve published server chains](https://emudhra.we-have-a-problem.com/chains.zip). diff --git a/tests/test_ca_trust.py b/tests/test_ca_trust.py new file mode 100644 index 000000000..b8e7b1bb0 --- /dev/null +++ b/tests/test_ca_trust.py @@ -0,0 +1,168 @@ +"""Offline regressions for incident roots, unaffected roots, and CA directories.""" + +import contextlib +import importlib.util +import io +import os +from pathlib import Path +import ssl +import tempfile +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("check_ca_trust", ROOT / "scripts/check_ca_trust.py") +checker = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(checker) + + +class CATrustTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Public certificate data is embedded below. Tests create temporary + # PEM files from these constants; no fixture contains a private key. + cls.temporary = tempfile.TemporaryDirectory() + cls.fixtures = Path(cls.temporary.name) + for name, pem in PUBLIC_CERTIFICATES.items(): + (cls.fixtures / name).write_text(pem) + + @classmethod + def tearDownClass(cls): + cls.temporary.cleanup() + + def test_unaffected_emsign_root_remains_usable(self): + checker.check_bundle(self.fixtures / "emsign-g1.pem") + + def test_compromised_emsign_path_is_rejected(self): + with self.assertRaisesRegex(ValueError, "emSign ECC Root CA - G3"): + checker.check_bundle(self.fixtures / "emsign-g3.pem") + + def test_aaa_cross_sign_path_is_rejected(self): + with self.assertRaisesRegex(ValueError, "AAA Certificate Services"): + checker.check_bundle(self.fixtures / "aaa.pem") + + def test_mixed_bundle_does_not_hide_an_affected_root(self): + with tempfile.TemporaryDirectory() as tmp: + bundle = Path(tmp) / "mixed.pem" + bundle.write_bytes((self.fixtures / "emsign-g1.pem").read_bytes() + (self.fixtures / "aaa.pem").read_bytes()) + with self.assertRaisesRegex(ValueError, "AAA Certificate Services"): + checker.check_bundle(bundle) + + def test_empty_malformed_and_missing_bundles_fail_closed(self): + with tempfile.TemporaryDirectory() as tmp: + for content in ("", "not a certificate"): + path = Path(tmp) / "bad.pem" + path.write_text(content) + with self.subTest(content=content), self.assertRaises((ssl.SSLError, ValueError)): + checker.check_bundle(path) + with self.assertRaises(OSError): + checker.check_bundle(Path(tmp) / "missing.pem") + + def test_safe_cafile_does_not_hide_stale_hashed_directory(self): + with tempfile.TemporaryDirectory() as tmp: + directory = Path(tmp) + (directory / "14bc7599.0").symlink_to(self.fixtures / "emsign-g3.pem") + defaults = ssl.get_default_verify_paths()._replace( + cafile=str(self.fixtures / "emsign-g1.pem"), capath=str(directory) + ) + errors = io.StringIO() + with patch.object(checker.ssl, "get_default_verify_paths", return_value=defaults), patch.dict( + os.environ, {defaults.openssl_capath_env: str(directory)} + ): + with contextlib.redirect_stderr(errors): + result = checker.main(["--system"]) + self.assertEqual(result, 1) + self.assertIn("emSign ECC Root CA - G3", errors.getvalue()) + + def test_multiple_ca_directories_are_all_checked(self): + with tempfile.TemporaryDirectory() as tmp: + clean, stale = Path(tmp) / "clean", Path(tmp) / "stale" + clean.mkdir() + stale.mkdir() + (stale / "ee64a828.0").symlink_to(self.fixtures / "aaa.pem") + defaults = ssl.get_default_verify_paths()._replace( + cafile=str(self.fixtures / "emsign-g1.pem"), capath=None + ) + errors = io.StringIO() + with patch.object(checker.ssl, "get_default_verify_paths", return_value=defaults), patch.dict( + os.environ, {defaults.openssl_capath_env: os.pathsep.join(map(str, (clean, stale)))} + ): + with contextlib.redirect_stderr(errors): + result = checker.main(["--system"]) + self.assertEqual(result, 1) + self.assertIn("AAA Certificate Services", errors.getvalue()) + + +# Public Mozilla CA certificates; provenance: tests/fixtures/ca-trust/README.md. +PUBLIC_CERTIFICATES = { + 'aaa.pem': """-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +""", + 'emsign-g1.pem': """-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- +""", + 'emsign-g3.pem': """-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +""", +} + + +if __name__ == "__main__": + unittest.main()