fix: refresh outbound HTTPS trust and installer bootstrap

This commit is contained in:
RaresKeY
2026-10-21 18:27:05 +00:00
parent 8877273e19
commit df1c2c8b19
14 changed files with 355 additions and 9 deletions
+11 -4
View File
@@ -93,7 +93,7 @@ jobs:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: "20" node-version: "22.23.4"
- run: npm ci --ignore-scripts --no-audit --no-fund - run: npm ci --ignore-scripts --no-audit --no-fund
- name: Check frontend architecture - name: Check frontend architecture
run: node scripts/check_frontend.mjs run: node scripts/check_frontend.mjs
@@ -160,15 +160,18 @@ jobs:
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
with: with:
version: "0.13.4"
enable-cache: true enable-cache: true
cache-dependency-glob: requirements.txt cache-dependency-glob: requirements.txt
prune-cache: false 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' if: steps.docs-check.outputs.docs_only != 'true'
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
with: with:
node-version: "20" node-version: "22.23.4"
cache: npm cache: npm
- run: npm ci - run: npm ci
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
@@ -190,10 +193,14 @@ jobs:
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
run: | run: |
sudo apt-get update 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 command -v ffmpeg
ffmpeg -version | head -n 1 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 - name: Establish functional bubblewrap containment
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
shell: bash shell: bash
+4
View File
@@ -70,5 +70,9 @@ jobs:
- name: Run pip-audit on requirements - name: Run pip-audit on requirements
run: | run: |
set -euo pipefail 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 install pip-audit==2.10.0
pip-audit -r requirements.txt -r requirements-optional.txt --strict pip-audit -r requirements.txt -r requirements-optional.txt --strict
+1
View File
@@ -65,6 +65,7 @@ jobs:
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
pull: true
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true 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 }} cache-from: type=gha,scope=${{ matrix.arch }}
+4
View File
@@ -76,5 +76,9 @@ jobs:
- name: Run zizmor - name: Run zizmor
run: | run: |
set -euo pipefail 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 pip install zizmor==1.25.2
zizmor --offline --min-severity=low .github/workflows/ zizmor --offline --min-severity=low .github/workflows/
+16 -1
View File
@@ -4,8 +4,13 @@
# the final image / Cookbook never has to compile the broken sdists. See # the final image / Cookbook never has to compile the broken sdists. See
# docker/build-realesrgan-wheels.sh for the full rationale. # docker/build-realesrgan-wheels.sh for the full rationale.
FROM python:3.14-slim AS realesrgan-wheels 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/* && 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 COPY docker/build-realesrgan-wheels.sh /usr/local/bin/build-realesrgan-wheels.sh
RUN bash /usr/local/bin/build-realesrgan-wheels.sh /wheels 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 \ build-essential \
cmake \ cmake \
curl \ curl \
ca-certificates \
git \ git \
nodejs \ nodejs \
npm \ npm \
@@ -48,6 +54,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libmagic1 \ libmagic1 \
&& rm -rf /var/lib/apt/lists/* && 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. # Private browser automation wrapper used by the native `private_browser` tool.
# Chromium is installed above, so agent-browser can drive the existing browser # Chromium is installed above, so agent-browser can drive the existing browser
# binary without paying `npx` startup/install overhead on each tool call. # binary without paying `npx` startup/install overhead on each tool call.
+2 -1
View File
@@ -47,7 +47,8 @@ if (-not $pyExe) {
Write-Host ("Using Python: " + $pyExe) Write-Host ("Using Python: " + $pyExe)
Write-Step "Installing build dependencies" 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 & $pyExe -m pip install -r requirements.txt pyinstaller pystray Pillow
if ($LASTEXITCODE -ne 0) { Fail "Dependency install failed." } if ($LASTEXITCODE -ne 0) { Fail "Dependency install failed." }
+34
View File
@@ -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.
+2 -1
View File
@@ -131,7 +131,8 @@ if (-not (Test-Path $venvPy)) {
# 3. Install / update dependencies # 3. Install / update dependencies
Write-Step "Installing dependencies (first run can take a few minutes)" 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 & $venvPy -m pip install -r requirements.txt
if ($LASTEXITCODE -ne 0) { Fail "Dependency install failed. Scroll up for the pip error." } if ($LASTEXITCODE -ne 0) { Fail "Dependency install failed. Scroll up for the pip error." }
+8
View File
@@ -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
+2
View File
@@ -3,6 +3,8 @@ uvicorn
python-multipart python-multipart
python-dotenv python-dotenv
httpx httpx
# HTTPS clients using certifi must distrust the compromised eMudhra CA paths.
certifi>=2026.10.19
httpcore>=1.0,<2.0 httpcore>=1.0,<2.0
pydantic>=2.13.4 pydantic>=2.13.4
pydantic-settings>=2.14.1 pydantic-settings>=2.14.1
+96
View File
@@ -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())
+2 -2
View File
@@ -140,11 +140,11 @@ if [ ! -x "$VENV_PY" ] || ! "$VENV_PY" -m pip --version >/dev/null 2>&1; then
echo "▶ Creating Python environment…" echo "▶ Creating Python environment…"
"$PY" -m venv venv "$PY" -m venv venv
fi 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" REQ_HASH_FILE="venv/.requirements_hash"
if [ ! -f "$REQ_HASH_FILE" ] || [ "$REQ_HASH" != "$(cat "$REQ_HASH_FILE" 2>/dev/null)" ]; then 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)…" 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). # Not --quiet: this is the slow step, so show progress (and any real errors).
"$VENV_PY" -m pip install -r requirements.txt "$VENV_PY" -m pip install -r requirements.txt
echo "$REQ_HASH" > "$REQ_HASH_FILE" echo "$REQ_HASH" > "$REQ_HASH_FILE"
+5
View File
@@ -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).
+168
View File
@@ -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()