mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 00:02:25 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66ffd8897d | ||
|
|
ed78beee1b |
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Exercise an already-built image through the normal Compose startup path.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
searxng_repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
searxng_image=${1:?usage: validate-searxng-image.sh IMAGE}
|
||||||
|
[[ "$searxng_image" =~ ^[a-zA-Z0-9_./:@+-]+$ ]] || {
|
||||||
|
echo "Invalid image reference" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
searxng_checks=$(mktemp -d "${TMPDIR:-/tmp}/searxng-ci-XXXXXXXX")
|
||||||
|
searxng_project=$(basename "$searxng_checks" | tr '[:upper:]' '[:lower:]')
|
||||||
|
export SEARXNG_VALIDATION_IMAGE="$searxng_image"
|
||||||
|
export SEARXNG_VALIDATION_CHECKS="$searxng_checks"
|
||||||
|
export SEARXNG_VALIDATION_VOLUME="${searxng_project}-settings"
|
||||||
|
# Never forward an operator's existing secret or load their .env for a CI probe.
|
||||||
|
export SEARXNG_SECRET=searxng-ci-validation-only
|
||||||
|
searxng_volume_created=0
|
||||||
|
searxng_started=0
|
||||||
|
searxng_compose=(docker compose --env-file /dev/null --project-name "$searxng_project"
|
||||||
|
-f "$searxng_repo/docker-compose.yml"
|
||||||
|
-f "$searxng_repo/.github/searxng-validation.compose.yml")
|
||||||
|
|
||||||
|
searxng_cleanup() {
|
||||||
|
local searxng_exit=$?
|
||||||
|
if [ "$searxng_started" = 1 ]; then
|
||||||
|
"${searxng_compose[@]}" down --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if [ "$searxng_volume_created" = 1 ]; then
|
||||||
|
docker volume rm "$SEARXNG_VALIDATION_VOLUME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
rm -rf -- "$searxng_checks"
|
||||||
|
return "$searxng_exit"
|
||||||
|
}
|
||||||
|
trap searxng_cleanup EXIT
|
||||||
|
|
||||||
|
install -m 0644 "$searxng_repo/scripts/check_ca_trust.py" \
|
||||||
|
"$searxng_repo/docker/searxng/validate_image.py" "$searxng_checks/"
|
||||||
|
install -m 0644 "$searxng_repo/tests/fixtures/ca-trust/incident.json" "$searxng_checks/incident.json"
|
||||||
|
install -m 0644 "$searxng_repo/config/searxng/settings.yml" "$searxng_checks/settings.yml"
|
||||||
|
install -m 0644 "$searxng_repo/scripts/migrate_searxng_settings.py" "$searxng_checks/migrate_searxng_settings.py"
|
||||||
|
if docker volume inspect "$SEARXNG_VALIDATION_VOLUME" >/dev/null 2>&1; then
|
||||||
|
echo "Refusing to reuse an existing validation volume" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker volume create --label "odysseus.searxng-validation=$searxng_project" \
|
||||||
|
"$SEARXNG_VALIDATION_VOLUME" >/dev/null
|
||||||
|
searxng_volume_created=1
|
||||||
|
searxng_started=1
|
||||||
|
if ! "${searxng_compose[@]}" up --detach --no-deps --no-build --pull never \
|
||||||
|
--wait --wait-timeout 150 searxng; then
|
||||||
|
"${searxng_compose[@]}" logs --no-color searxng >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"${searxng_compose[@]}" exec -T searxng /usr/local/searxng/.venv/bin/python \
|
||||||
|
/opt/odysseus-checks/validate_image.py --running --live
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# CI-only isolation; retain the service's entrypoint, healthcheck and capabilities.
|
||||||
|
services:
|
||||||
|
searxng:
|
||||||
|
image: ${SEARXNG_VALIDATION_IMAGE:?set the image being validated}
|
||||||
|
ports: !reset []
|
||||||
|
volumes: !override
|
||||||
|
- searxng-validation-data:/etc/searxng
|
||||||
|
- ${SEARXNG_VALIDATION_CHECKS:?}/settings.yml:/tmp/searxng-settings.yml.template:ro
|
||||||
|
- ${SEARXNG_VALIDATION_CHECKS:?}/migrate_searxng_settings.py:/tmp/migrate-searxng-settings.py:ro
|
||||||
|
- ${SEARXNG_VALIDATION_CHECKS:?}:/opt/odysseus-checks:ro
|
||||||
|
networks: !override
|
||||||
|
- default
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default: {}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
searxng-validation-data:
|
||||||
|
external: true
|
||||||
|
name: ${SEARXNG_VALIDATION_VOLUME:?}
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
# Byte-compile sources — catches syntax errors without installing deps.
|
# Byte-compile sources — catches syntax errors without installing deps.
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
with:
|
with:
|
||||||
node-version: "22.23.4"
|
node-version: "22.23.4"
|
||||||
- run: npm ci --ignore-scripts --no-audit --no-fund
|
- run: npm ci --ignore-scripts --no-audit --no-fund
|
||||||
@@ -151,7 +151,7 @@ jobs:
|
|||||||
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
if: steps.docs-check.outputs.docs_only != 'true'
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
@@ -168,10 +168,10 @@ jobs:
|
|||||||
|
|
||||||
# uv resolves the same versions as pip but installs ~5x faster, and its
|
# uv resolves the same versions as pip but installs ~5x faster, and its
|
||||||
# cache keeps each shard from re-downloading the whole requirement set.
|
# cache keeps each shard from re-downloading the whole requirement set.
|
||||||
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
|
||||||
if: steps.docs-check.outputs.docs_only != 'true'
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
with:
|
with:
|
||||||
version: "0.13.4"
|
version: "0.13.5"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-dependency-glob: requirements.txt
|
cache-dependency-glob: requirements.txt
|
||||||
prune-cache: false
|
prune-cache: false
|
||||||
@@ -180,7 +180,7 @@ jobs:
|
|||||||
python scripts/check_ca_trust.py --python
|
python scripts/check_ca_trust.py --python
|
||||||
uv pip install --system -r requirements.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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
if: steps.docs-check.outputs.docs_only != 'true'
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
with:
|
with:
|
||||||
node-version: "22.23.4"
|
node-version: "22.23.4"
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: none
|
build-mode: none
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
with:
|
with:
|
||||||
category: "/language:${{ matrix.language }}"
|
category: "/language:${{ matrix.language }}"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Lint Dockerfile
|
- name: Lint Dockerfile
|
||||||
uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0
|
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
|
||||||
with:
|
with:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
# DL3008: pinning apt package versions is impractical on a -slim base
|
# DL3008: pinning apt package versions is impractical on a -slim base
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
with:
|
with:
|
||||||
driver: docker
|
driver: docker
|
||||||
|
|
||||||
# Build without pushing so a broken Dockerfile is caught here, and the
|
# Build without pushing so a broken Dockerfile is caught here, and the
|
||||||
# exact image we ship is what gets scanned.
|
# exact image we ship is what gets scanned.
|
||||||
- name: Build image
|
- name: Build image
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
@@ -107,12 +107,12 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
with:
|
with:
|
||||||
driver: docker
|
driver: docker
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
@@ -133,7 +133,7 @@ jobs:
|
|||||||
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
|
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
|
||||||
|
|
||||||
- name: Upload Trivy results
|
- name: Upload Trivy results
|
||||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
with:
|
with:
|
||||||
sarif_file: trivy-results.sarif
|
sarif_file: trivy-results.sarif
|
||||||
category: trivy-image
|
category: trivy-image
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
|
run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
pull: true
|
pull: true
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: digest-${{ matrix.arch }}
|
name: digest-${{ matrix.arch }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
@@ -107,13 +107,13 @@ jobs:
|
|||||||
echo "version=$v" >> "$GITHUB_OUTPUT"
|
echo "version=$v" >> "$GITHUB_OUTPUT"
|
||||||
echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digest-*
|
pattern: digest-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: ci / SearXNG trust
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'docker/searxng/**'
|
||||||
|
- 'scripts/check_ca_trust.py'
|
||||||
|
- 'tests/fixtures/ca-trust/**'
|
||||||
|
- 'tests/test_searxng*'
|
||||||
|
- 'docker-compose*.yml'
|
||||||
|
- 'config/searxng/**'
|
||||||
|
- 'scripts/migrate_searxng_settings.py'
|
||||||
|
- '.github/scripts/*searxng*'
|
||||||
|
- '.github/searxng-validation.compose.yml'
|
||||||
|
- '.github/workflows/searxng-image.yml'
|
||||||
|
push:
|
||||||
|
branches: [dev, main]
|
||||||
|
paths:
|
||||||
|
- 'docker/searxng/**'
|
||||||
|
- 'scripts/check_ca_trust.py'
|
||||||
|
- 'tests/fixtures/ca-trust/**'
|
||||||
|
- 'docker-compose*.yml'
|
||||||
|
- 'config/searxng/**'
|
||||||
|
- 'scripts/migrate_searxng_settings.py'
|
||||||
|
- '.github/scripts/*searxng*'
|
||||||
|
- '.github/searxng-validation.compose.yml'
|
||||||
|
- '.github/workflows/searxng-image.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: searxng-image-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
name: SearXNG validate (${{ matrix.arch }})
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
timeout-minutes: 25
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- arch: arm64
|
||||||
|
platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Select the immutable upstream image from Compose
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
docker compose --env-file /dev/null -f docker-compose.yml config --format json |
|
||||||
|
python3 -c 'import json, re, sys; image = json.load(sys.stdin)["services"]["searxng"]["image"]; re.fullmatch(r"docker\.io/searxng/searxng:[0-9]{4}\.[^@\s]+@sha256:[0-9a-f]{64}", image) or sys.exit("Invalid SearXNG pin"); print("image=" + image)' >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Pull the upstream image without registry credentials
|
||||||
|
env:
|
||||||
|
SEARXNG_TEST_IMAGE: ${{ steps.image.outputs.image }}
|
||||||
|
run: docker pull "$SEARXNG_TEST_IMAGE"
|
||||||
|
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
|
- name: Check unmodified upstream trust and actual entrypoint offline
|
||||||
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/searxng/Dockerfile
|
||||||
|
target: validation
|
||||||
|
build-args: SEARXNG_IMAGE=${{ steps.image.outputs.image }}
|
||||||
|
network: none
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
no-cache-filters: validation
|
||||||
|
- name: Validate the selected upstream runtime through Compose and live HTTPS
|
||||||
|
env:
|
||||||
|
SEARXNG_TEST_IMAGE: ${{ steps.image.outputs.image }}
|
||||||
|
run: bash .github/scripts/validate-searxng-image.sh "$SEARXNG_TEST_IMAGE"
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -3,7 +3,7 @@
|
|||||||
# which raises KeyError on Python 3.13+ (PEP 667). Build patched wheels here so
|
# which raises KeyError on Python 3.13+ (PEP 667). Build patched wheels here so
|
||||||
# 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.9-slim AS realesrgan-wheels
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
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
|
COPY scripts/check_ca_trust.py /usr/local/bin/check-ca-trust.py
|
||||||
@@ -14,7 +14,7 @@ RUN python -m pip install --no-cache-dir --require-hashes --only-binary=:all: --
|
|||||||
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
|
||||||
|
|
||||||
FROM python:3.14-slim
|
FROM python:3.14.9-slim
|
||||||
|
|
||||||
# System deps. tmux is required by Cookbook for background downloads/serves.
|
# System deps. tmux is required by Cookbook for background downloads/serves.
|
||||||
# openssh-client is required for Cookbook remote server tests, setup, probes,
|
# openssh-client is required for Cookbook remote server tests, setup, probes,
|
||||||
@@ -92,7 +92,8 @@ RUN npm install -g agent-browser@0.35.0 --omit=dev --loglevel=error
|
|||||||
# /var/run/docker.sock mount). The Debian `docker.io` package ships
|
# /var/run/docker.sock mount). The Debian `docker.io` package ships
|
||||||
# dockerd but not the client binary on slim, so grab the static client
|
# dockerd but not the client binary on slim, so grab the static client
|
||||||
# tarball from download.docker.com instead.
|
# tarball from download.docker.com instead.
|
||||||
ARG DOCKER_CLI_VERSION=29.6.2
|
# 29.9.2 includes Go 1.26.10's eMudhra intermediate-key distrust.
|
||||||
|
ARG DOCKER_CLI_VERSION=29.9.2
|
||||||
RUN ARCH="$(dpkg --print-architecture)" \
|
RUN ARCH="$(dpkg --print-architecture)" \
|
||||||
&& case "$ARCH" in \
|
&& case "$ARCH" in \
|
||||||
amd64) DARCH=x86_64 ;; \
|
amd64) DARCH=x86_64 ;; \
|
||||||
@@ -129,7 +130,8 @@ RUN pip install --no-cache-dir --no-deps /tmp/odysseus-wheels/*.whl \
|
|||||||
&& rm -rf /tmp/odysseus-wheels
|
&& rm -rf /tmp/odysseus-wheels
|
||||||
|
|
||||||
# Check gRPC's effective defaults after all Python dependencies are installed.
|
# Check gRPC's effective defaults after all Python dependencies are installed.
|
||||||
# Its wheel can retain affected roots even when pip/certifi are current.
|
# The application minimum fixes its bundled roots; the checked override also
|
||||||
|
# keeps the effective Docker selection consistent with system trust.
|
||||||
RUN python /usr/local/bin/check-ca-trust.py --system --python --grpc
|
RUN python /usr/local/bin/check-ca-trust.py --system --python --grpc
|
||||||
|
|
||||||
# Copy app code
|
# Copy app code
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ services:
|
|||||||
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
||||||
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
||||||
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
||||||
# Bump this deliberately after verifying a newer tag boots clean.
|
# Update all default/GPU pins together after native startup and trust checks.
|
||||||
image: docker.io/searxng/searxng:2026.9.25-12f8b6515@sha256:5286edb35782454ab8a102c5eff6b54bff745853191b46aeead95f225aa6dfb6
|
image: docker.io/searxng/searxng:2026.10.24-ef0ec03da@sha256:7c7cbe509429e82755104b2a95f52085a98633c95d53f44e4e905fa3a59f08c8
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ services:
|
|||||||
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
||||||
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
||||||
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
||||||
# Bump this deliberately after verifying a newer tag boots clean.
|
# Update all default/GPU pins together after native startup and trust checks.
|
||||||
image: docker.io/searxng/searxng:2026.9.25-12f8b6515@sha256:5286edb35782454ab8a102c5eff6b54bff745853191b46aeead95f225aa6dfb6
|
image: docker.io/searxng/searxng:2026.10.24-ef0ec03da@sha256:7c7cbe509429e82755104b2a95f52085a98633c95d53f44e4e905fa3a59f08c8
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
|
|||||||
+2
-2
@@ -126,8 +126,8 @@ services:
|
|||||||
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
||||||
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
||||||
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
||||||
# Bump this deliberately after verifying a newer tag boots clean.
|
# Update all default/GPU pins together after native startup and trust checks.
|
||||||
image: docker.io/searxng/searxng:2026.9.25-12f8b6515@sha256:5286edb35782454ab8a102c5eff6b54bff745853191b46aeead95f225aa6dfb6
|
image: docker.io/searxng/searxng:2026.10.24-ef0ec03da@sha256:7c7cbe509429e82755104b2a95f52085a98633c95d53f44e4e905fa3a59f08c8
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Validation only. Deploy the unmodified upstream image selected by Compose.
|
||||||
|
ARG SEARXNG_IMAGE
|
||||||
|
FROM ${SEARXNG_IMAGE} AS validation
|
||||||
|
COPY scripts/check_ca_trust.py docker/searxng/validate_image.py /opt/odysseus-trust/
|
||||||
|
COPY tests/fixtures/ca-trust/incident.json /opt/odysseus-trust/incident.json
|
||||||
|
RUN /usr/local/searxng/.venv/bin/python /opt/odysseus-trust/validate_image.py
|
||||||
|
|
||||||
|
FROM validation AS live-validation
|
||||||
|
RUN /usr/local/searxng/.venv/bin/python /opt/odysseus-trust/validate_image.py --live
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
**
|
||||||
|
!scripts/
|
||||||
|
!scripts/check_ca_trust.py
|
||||||
|
!docker/
|
||||||
|
!docker/searxng/
|
||||||
|
!docker/searxng/**
|
||||||
|
!tests/
|
||||||
|
!tests/fixtures/
|
||||||
|
!tests/fixtures/ca-trust/
|
||||||
|
!tests/fixtures/ca-trust/incident.json
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Verify incident distrust and startup in the unmodified upstream image."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import certifi
|
||||||
|
|
||||||
|
from check_ca_trust import check_bundle
|
||||||
|
|
||||||
|
|
||||||
|
def check_chains():
|
||||||
|
incident = json.loads(Path(__file__).with_name("incident.json").read_text())
|
||||||
|
system = Path(os.environ["SSL_CERT_FILE"])
|
||||||
|
capath = os.environ["SSL_CERT_DIR"]
|
||||||
|
stores = [["-CAfile", str(system), "-no-CApath"],
|
||||||
|
["-CAfile", certifi.where(), "-no-CApath"],
|
||||||
|
["-no-CAfile", "-CApath", capath]]
|
||||||
|
check_bundle(system)
|
||||||
|
check_bundle(Path(certifi.where()))
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
directory = Path(directory)
|
||||||
|
for fingerprint, pem in incident["certificates"].items():
|
||||||
|
if hashlib.sha256(ssl.PEM_cert_to_DER_cert(pem)).hexdigest() != fingerprint:
|
||||||
|
raise ValueError(f"incident fixture fingerprint mismatch: {fingerprint}")
|
||||||
|
(directory / fingerprint).write_text(pem)
|
||||||
|
positive = directory / "affected.pem"
|
||||||
|
positive.write_text("\n".join(incident["certificates"][fp] for fp in incident["roots"]))
|
||||||
|
for hostname, chain in incident["chains"].items():
|
||||||
|
untrusted = directory / "untrusted.pem"
|
||||||
|
untrusted.write_text("\n".join(incident["certificates"][fp] for fp in chain[1:]))
|
||||||
|
base = ["openssl", "verify", "-no-CAstore", "-attime", str(incident["verification_time"]),
|
||||||
|
"-purpose", "sslserver", "-verify_hostname", hostname,
|
||||||
|
"-x509_strict", "-partial_chain", "-trusted_first", "-untrusted", str(untrusted)]
|
||||||
|
expectations = [(selection, False) for selection in stores]
|
||||||
|
for selection, expected in expectations + [
|
||||||
|
(["-CAfile", str(positive), "-no-CApath"], True)]:
|
||||||
|
result = subprocess.run(base + selection + [str(directory / chain[0])],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if (result.returncode == 0) != expected:
|
||||||
|
raise RuntimeError(f"unexpected chain result: {hostname}: {selection}: {result.stderr}")
|
||||||
|
# Accept issuer lookup, self-signed and explicitly untrusted
|
||||||
|
# certificate errors, rather than tool or fixture failures.
|
||||||
|
if not expected and not re.search(r"error (?:2|18|19|20|21|27) at \d+ depth lookup", result.stderr):
|
||||||
|
raise RuntimeError(f"not a certificate trust rejection: {hostname}: {result.stderr}")
|
||||||
|
print(f"{len(incident['chains'])}/{len(incident['chains'])} incident chains rejected by system PEM, hashed directory and certifi; affected controls accepted")
|
||||||
|
|
||||||
|
|
||||||
|
def check_boot(live=False):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
settings = Path(directory) / "settings.yml"
|
||||||
|
settings.write_text("use_default_settings: true\nserver:\n secret_key: offline-build-validation-only\n limiter: false\nsearch:\n formats: [html, json]\n")
|
||||||
|
cache = Path(directory) / "cache"
|
||||||
|
cache.mkdir()
|
||||||
|
env = dict(os.environ, SEARXNG_SETTINGS_PATH=str(settings),
|
||||||
|
__SEARXNG_CONFIG_PATH=directory, __SEARXNG_DATA_PATH=str(cache),
|
||||||
|
GRANIAN_HOST="127.0.0.1", GRANIAN_PORT="18888")
|
||||||
|
# Do not expose a port or issue an outbound search during offline builds.
|
||||||
|
with tempfile.TemporaryFile() as log:
|
||||||
|
process = subprocess.Popen(["/usr/local/searxng/entrypoint.sh"],
|
||||||
|
cwd="/usr/local/searxng", env=env, stdout=log,
|
||||||
|
stderr=subprocess.STDOUT, start_new_session=True)
|
||||||
|
try:
|
||||||
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||||
|
healthy = False
|
||||||
|
for _ in range(60):
|
||||||
|
if process.poll() is not None:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
with opener.open("http://127.0.0.1:18888/healthz", timeout=1) as response:
|
||||||
|
healthy = response.status == 200
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if healthy:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
if not healthy:
|
||||||
|
log.seek(0)
|
||||||
|
raise RuntimeError("SearXNG failed boot smoke: " + log.read().decode(errors="replace")[-6000:])
|
||||||
|
print("SearXNG loopback healthcheck passed")
|
||||||
|
# Its root entrypoint regenerates system trust. Repeat the
|
||||||
|
# behavioral checks after that real startup path completes.
|
||||||
|
check_running(live=live, port=18888)
|
||||||
|
finally:
|
||||||
|
if process.poll() is None:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
process.wait(timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def check_running(live=False, port=None):
|
||||||
|
"""Verify an existing service without repairing it or starting another one."""
|
||||||
|
port = int(os.environ.get("GRANIAN_PORT", "8080")) if port is None else port
|
||||||
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||||
|
with opener.open(f"http://127.0.0.1:{port}/healthz", timeout=5) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
raise RuntimeError("SearXNG running-service healthcheck failed")
|
||||||
|
check_chains()
|
||||||
|
for path in Path(os.environ["SSL_CERT_DIR"]).iterdir():
|
||||||
|
if re.fullmatch(r"[0-9a-fA-F]{8}\.[0-9]+", path.name):
|
||||||
|
check_bundle(path)
|
||||||
|
if live:
|
||||||
|
asyncio.run(check_https())
|
||||||
|
with opener.open(f"http://127.0.0.1:{port}/search?q=python+documentation&format=json", timeout=90) as search:
|
||||||
|
payload = json.load(search)
|
||||||
|
results = payload.get("results", [])
|
||||||
|
if not results:
|
||||||
|
raise RuntimeError("live search returned no results")
|
||||||
|
print(f"SearXNG live JSON search passed: {len(results)} results")
|
||||||
|
|
||||||
|
|
||||||
|
async def check_https():
|
||||||
|
# Exercise the actual pinned SearXNG client factory with its default CA
|
||||||
|
# selection, rather than a different HTTP library's trust configuration.
|
||||||
|
sys.path.insert(0, "/usr/local/searxng")
|
||||||
|
from curl_cffi.requests.exceptions import RequestException
|
||||||
|
from searx.network.client import new_client
|
||||||
|
|
||||||
|
client = new_client(enable_http=False, verify=True, enable_http2=True,
|
||||||
|
enable_http3=False, max_connections=2, proxies={},
|
||||||
|
local_address=None, max_redirects=3)
|
||||||
|
try:
|
||||||
|
response = await client.get("https://pypi.org/simple/certifi/", timeout=30)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise RuntimeError(f"ordinary HTTPS failed: {response.status_code}")
|
||||||
|
print("SearXNG actual curl client ordinary HTTPS passed with verification enabled")
|
||||||
|
incident = json.loads(Path(__file__).with_name("incident.json").read_text())
|
||||||
|
for hostname in incident["chains"]:
|
||||||
|
try:
|
||||||
|
await client.get(f"https://{hostname}/", timeout=30, allow_redirects=False)
|
||||||
|
except RequestException as exc:
|
||||||
|
# libcurl code 60 is peer certificate verification failure.
|
||||||
|
# DNS failures, timeouts and handshake EOF do not prove distrust.
|
||||||
|
if exc.code != 60:
|
||||||
|
raise RuntimeError(f"inconclusive incident TLS failure: {hostname}: {exc}") from exc
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"incident HTTPS unexpectedly accepted: {hostname}")
|
||||||
|
print(f"SearXNG actual curl client rejects {len(incident['chains'])}/{len(incident['chains'])} incident hosts with certificate errors after startup")
|
||||||
|
finally:
|
||||||
|
await client.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
live = "--live" in sys.argv
|
||||||
|
if "--running" in sys.argv:
|
||||||
|
check_running(live=live)
|
||||||
|
else:
|
||||||
|
check_chains()
|
||||||
|
check_boot(live=live)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Upstream SearXNG trust validation
|
||||||
|
|
||||||
|
SearXNG owns independent system and Python certificate stores. Updating Odysseus's Python environment does not update this sidecar. The base and both standalone GPU Compose files now select the official `docker.io/searxng/searxng:2026.10.24-ef0ec03da` image at immutable multiarch digest `sha256:7c7cbe509429e82755104b2a95f52085a98633c95d53f44e4e905fa3a59f08c8`. Its system and certifi stores remove the affected eMudhra trust. The [upstream certifi update](https://github.com/searxng/searxng/pull/6881) is included in this release.
|
||||||
|
|
||||||
|
Deploy the upstream image directly. The validation Dockerfile adds public fixtures and diagnostics only; no repair layer, custom runtime image or project registry publisher is required. `scripts/check_ca_trust.py` owns the known-certificate inventory for both the application and SearXNG checks.
|
||||||
|
|
||||||
|
## Validate a pin update
|
||||||
|
|
||||||
|
Run from the repository root on a disposable Docker host. The helper uses a fresh configuration volume, omits published ports and does not load the operator's `.env`. It retains the base Compose settings wrapper, healthcheck and capabilities. Outbound network access and working search engines are required for the live checks.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
image=$(docker compose --env-file /dev/null -f docker-compose.yml config --format json |
|
||||||
|
python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["searxng"]["image"])')
|
||||||
|
|
||||||
|
# Offline build-step checks, including actual entrypoint and loopback health.
|
||||||
|
# Pulling the selected base still requires registry access.
|
||||||
|
docker build --network=none --target validation \
|
||||||
|
--build-arg "SEARXNG_IMAGE=$image" -f docker/searxng/Dockerfile .
|
||||||
|
|
||||||
|
# Exercise the exact unmodified deployment image after an anonymous pull.
|
||||||
|
docker pull "$image"
|
||||||
|
bash .github/scripts/validate-searxng-image.sh "$image"
|
||||||
|
```
|
||||||
|
|
||||||
|
The offline checks require all fourteen incident chains to fail certificate verification through the system PEM bundle, hashed CA directory and certifi. The affected positive-control roots must accept those same chains. Each fixture's DER fingerprint is verified first, and a fixed verification timestamp prevents expiry from hiding a broken mitigation. Checks run again after the real upstream root entrypoint regenerates trust.
|
||||||
|
|
||||||
|
The live checks use SearXNG's actual curl client factory. Ordinary verified HTTPS must succeed, all fourteen incident hosts must fail with curl certificate error 60, and a JSON search must return results. DNS failures, timeouts and generic handshake failures are inconclusive and fail validation. The checks inspect the already-running service without changing its trust stores.
|
||||||
|
|
||||||
|
The `ci / SearXNG trust` workflow runs these offline and Compose/live checks on native amd64 and arm64 runners. It selects the same immutable image as base Compose, pulls without registry credentials, and has read-only repository permissions. It neither publishes a replacement image nor requires package-write credentials. Update all three deployment pins together; the alignment regression guards the standalone GPU files.
|
||||||
|
|
||||||
|
## Rollout and scope
|
||||||
|
|
||||||
|
Changing a Compose pin does not replace running containers. After updating the checkout, run `docker compose pull searxng` and `docker compose up -d --no-deps searxng`, then inspect health, retained settings and actual Odysseus search integration. Use the appropriate standalone GPU Compose file when that is the deployment being managed.
|
||||||
|
|
||||||
|
Operator-mounted trust, custom engines, persisted packages and independently managed clients need separate effective-client checks. An amd64 result does not establish arm64 behavior. The fixture-based check detects known certificate fingerprints; it does not model every native revocation mechanism or arbitrary reissued certificates sharing a compromised key. See [outbound TLS trust](tls-trust.md) for the application, bootstrap, Chromium and other runtime boundaries.
|
||||||
|
|
||||||
|
## Public fixture provenance
|
||||||
|
|
||||||
|
The public CA inventory comes from the incident's [roots](https://emudhra.we-have-a-problem.com/roots.json) and [intermediates](https://emudhra.we-have-a-problem.com/impacted-intermediates.json). The additional cross-signed issuer and fourteen chain fixtures come from [chains.zip](https://emudhra.we-have-a-problem.com/chains.zip), SHA-256 `7b949956955fc501a30f4d98d2f513d34c0bbbe92e29a968194c96e097ce2345`. Fixtures contain public certificates only. See [the fixture notes](../tests/fixtures/ca-trust/README.md).
|
||||||
+13
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
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.
|
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/).
|
For the October 2026 eMudhra incident, use certifi 2026.10.23 or newer, pip 26.2.2 or newer, and uv 0.13.5 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:
|
Before other pip installs, bootstrap the selected environment with the integrity-pinned wheels:
|
||||||
|
|
||||||
@@ -27,7 +27,9 @@ Explicit PEM bundles can also be passed as positional arguments. The check rejec
|
|||||||
|
|
||||||
This is an offline inventory check of known certificate fingerprints. It does not modify stores, reject requests at runtime, model native OS revocation, or detect arbitrary reissued certificates sharing the compromised keys. Chromium's policy separately matches public keys. Check mounted or replaced CA files again after deployment, remove affected trust through the owning platform/client configuration, and restart clients that cache SSL contexts. A passing build cannot validate custom files introduced later.
|
This is an offline inventory check of known certificate fingerprints. It does not modify stores, reject requests at runtime, model native OS revocation, or detect arbitrary reissued certificates sharing the compromised keys. Chromium's policy separately matches public keys. Check mounted or replaced CA files again after deployment, remove affected trust through the owning platform/client configuration, and restart clients that cache SSL contexts. A passing build cannot validate custom files introduced later.
|
||||||
|
|
||||||
Both Docker build stages refresh system CA certificates and bootstrap pip/certifi before dependency installation. The build fails if known affected CA certificates remain. Node uses the corrected system store through `--use-openssl-ca`. Python gRPC uses that same PEM file through `GRPC_DEFAULT_SSL_ROOTS_FILE_PATH`; updating certifi alone does not replace gRPC's independent wheel bundle. After dependency installation, the image checks gRPC's selected file as well. An explicit gRPC override must be readable and free of known affected CA certificates: gRPC can otherwise fall back to its affected bundled defaults. Upstream gRPC has [merged a bundle update](https://github.com/grpc/grpc/commit/8b32af3bf498c2ba9667410eaa8c836e700e97e6); as checked on 2026-10-21, its [release backport](https://github.com/grpc/grpc/pull/43700) is still open and the latest published grpcio wheel is 1.84.0. Retain the checked override until a released replacement's actual trust behavior has been verified.
|
Both Docker build stages use Python 3.14.9, refresh system CA certificates and bootstrap pip/certifi before dependency installation. The build fails if known affected CA certificates remain. Node uses the corrected system store through `--use-openssl-ca`. Python gRPC uses that same PEM file through `GRPC_DEFAULT_SSL_ROOTS_FILE_PATH`; updating certifi alone does not replace gRPC's independent wheel bundle. After dependency installation, the image checks gRPC's selected file as well. An explicit gRPC override must be readable and free of known affected CA certificates. [grpcio 1.84.1](https://github.com/grpc/grpc/releases/tag/v1.84.1) includes the upstream root-bundle correction and is the application minimum. Retain the checked override as a consistent image trust selection; older persisted Python environments and explicit client roots still need independent checks.
|
||||||
|
|
||||||
|
The image also bundles Docker CLI [29.9.2](https://github.com/moby/moby/releases/tag/docker-v29.9.2), built with Go 1.26.10's distrust of the compromised intermediate keys. Cookbook uses this client for optional Docker operations. Default Compose does not mount the host Docker socket; local daemon access requires the explicit [host-Docker overlay](../docker/host-docker.yml), whose usual Unix-socket connection does not use TLS. The client update is maintenance of a shipped tool, not evidence that these local calls were exposed. It does not update the host daemon or other containers' clients and trust stores; assess those separately.
|
||||||
|
|
||||||
Chromium uses a separate trust store. The Docker image installs a managed `CADistrustedCertificates` policy for the eight compromised intermediate public keys, covering their cross-signs without depending on background revocation updates. It adds no trusted certificate and does not disable certificate verification. The policy and public-certificate provenance are documented in [the browser policy note](../docker/chromium-ca-policy.md). Native browser installations and independently managed profiles require their own platform policy or vendor update; this image policy does not configure host browsers.
|
Chromium uses a separate trust store. The Docker image installs a managed `CADistrustedCertificates` policy for the eight compromised intermediate public keys, covering their cross-signs without depending on background revocation updates. It adds no trusted certificate and does not disable certificate verification. The policy and public-certificate provenance are documented in [the browser policy note](../docker/chromium-ca-policy.md). Native browser installations and independently managed profiles require their own platform policy or vendor update; this image policy does not configure host browsers.
|
||||||
|
|
||||||
@@ -62,4 +64,12 @@ For containers started non-root or without the SETUID/SETGID capabilities needed
|
|||||||
|
|
||||||
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.
|
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. In Chromium, confirm the managed policy is loaded at `chrome://policy` and test the incident endpoints through the actual browser path with certificate verification enabled. The PEM checker cannot inspect Chromium's policy or its active verifier. Package versions and a successful image build alone do not prove that a running deployment selects the corrected store.
|
For behavioral validation, all fourteen [published test chains](https://emudhra.we-have-a-problem.com/chains.zip), including the AAA cross-signs and two chains without Certificate Transparency entries, must fail certificate verification through the effective stores. Confirm that an ordinary known-good HTTPS request still succeeds. In Chromium, confirm the managed policy is loaded at `chrome://policy` and test the incident endpoints through the actual browser path with certificate verification enabled. The PEM checker cannot inspect Chromium's policy or its active verifier. Package versions and a successful image build alone do not prove that a running deployment selects the corrected store.
|
||||||
|
|
||||||
|
## Follow-up releases
|
||||||
|
|
||||||
|
The maintained bootstrap selects certifi 2026.10.23 with its verified wheel hash; pip remains 26.2.2. The certifi update removes the C1/C3 roots in addition to the earlier incident changes. CI uses uv 0.13.5, which adds verifier-level rejection of the compromised intermediate keys and updated managed CPython builds. Existing virtual environments do not update automatically. See the [certifi change](https://github.com/certifi/python-certifi/commit/b60466c9f2882d9cf088943c650b0faac06c080c), [uv release](https://github.com/astral-sh/uv/releases/tag/0.13.5), and [Python security releases](https://blog.python.org/2026/10/python-3151-3149-31317/). The pinned bootstrap still replaces the selected versions exactly; preserving newer installed versions and private-mirror support remain separate maintenance.
|
||||||
|
|
||||||
|
SearXNG remains an independent trust owner. Use the [upstream-image validation and rollout instructions](searxng-trust-image.md); updating these application packages does not repair an existing sidecar. Host daemons, native browser policy, optional compiled clients and independently managed integrations require their own effective-client checks.
|
||||||
|
|
||||||
|
CPython's [intermediate-key verifier block](https://github.com/python/cpython/pull/160042) and its [3.14 backport](https://github.com/python/cpython/pull/160569) merged on October 24. They are distinct from Python 3.14.9's bundled-pip update. No released 3.14 interpreter containing that verifier change was identified when this image pin was selected; retain the corrected effective trust stores and track the subsequent security release. hf-xet's [trust fix](https://github.com/huggingface/xet-core/pull/1038) is also merged but not included in the latest published 1.7.0 release at that check. Agent-browser 0.39.2 and primp 2.0.1 are not established incident fixes for their independent native trust paths. A version bump alone does not establish that the active client uses corrected trust.
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Direct universal wheels avoid index resolution through the old trust store.
|
# 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 \
|
pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl \
|
||||||
--hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70
|
--hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70
|
||||||
certifi @ https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl \
|
certifi @ https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl \
|
||||||
--hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b
|
--hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,7 @@ python-multipart
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
httpx
|
httpx
|
||||||
# HTTPS clients using certifi must distrust the compromised eMudhra CA paths.
|
# HTTPS clients using certifi must distrust the compromised eMudhra CA paths.
|
||||||
certifi>=2026.10.19
|
certifi>=2026.10.23
|
||||||
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
|
||||||
@@ -23,6 +23,8 @@ numpy
|
|||||||
# chromadb-client is the lightweight HTTP client (talks to a standalone
|
# chromadb-client is the lightweight HTTP client (talks to a standalone
|
||||||
# ChromaDB service); fastembed runs local ONNX embeddings.
|
# ChromaDB service); fastembed runs local ONNX embeddings.
|
||||||
chromadb-client
|
chromadb-client
|
||||||
|
# gRPC vendors independent roots; 1.84.1 removes the incident trust anchors.
|
||||||
|
grpcio>=1.84.1
|
||||||
fastembed
|
fastembed
|
||||||
youtube-transcript-api
|
youtube-transcript-api
|
||||||
yt-dlp
|
yt-dlp
|
||||||
|
|||||||
Vendored
+5
-3
@@ -4,12 +4,14 @@ The public certificates embedded in `tests/test_ca_trust.py` exercise the Octobe
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
`incident.json` contains the four published roots, sixteen [compromised intermediate certificates and cross-signs](https://emudhra.we-have-a-problem.com/impacted-intermediates.json), and the [twelve published server chains](https://emudhra.we-have-a-problem.com/chains.zip), retrieved on 2026-10-21. Certificates are deduplicated by SHA-256 of DER; each chain lists its leaf first. The additional cross-signed `emSign Root TLS CA - G3` issuer comes from those chains and has fingerprint `3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd`. Like the intermediates, it must not be accepted as a custom trust anchor. All data is public certificate material, with no private keys.
|
`incident.json` contains the four published roots, sixteen [compromised intermediate certificates and cross-signs](https://emudhra.we-have-a-problem.com/impacted-intermediates.json), and the [fourteen published server chains](https://emudhra.we-have-a-problem.com/chains.zip), retrieved on 2026-10-21 with chains refreshed on 2026-10-23. The refresh adds the two DV G3B hosts without Certificate Transparency entries and preserves the original twelve chains. Certificates are deduplicated by SHA-256 of DER; each chain lists its leaf first. The additional cross-signed `emSign Root TLS CA - G3` issuer comes from those chains and has fingerprint `3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd`. Like the intermediates, it must not be accepted as a custom trust anchor. All data is public certificate material, with no private keys.
|
||||||
|
|
||||||
Downloaded source SHA-256 values:
|
Downloaded source SHA-256 values:
|
||||||
|
|
||||||
- `roots.json`: `119f32cd81a6b5a4e3c4c2df5fe07daa90dc43cafa123a62f0394f1895d92380`
|
- `roots.json`: `119f32cd81a6b5a4e3c4c2df5fe07daa90dc43cafa123a62f0394f1895d92380`
|
||||||
- `impacted-intermediates.json`: `f67b3abe9b4879f4e91874ed041beccd71257f1db41c588c724ccf12aa90c59e`
|
- `impacted-intermediates.json`: `f67b3abe9b4879f4e91874ed041beccd71257f1db41c588c724ccf12aa90c59e`
|
||||||
- `chains.zip`: `8a2edc81b6c51e3ad27d45ca1e441ec84c961e2dee0fc547f2a02c0f74315f95`
|
- `chains.zip`: `7b949956955fc501a30f4d98d2f513d34c0bbbe92e29a968194c96e097ce2345`
|
||||||
|
|
||||||
The offline OpenSSL regression verifies server purpose, hostname and strict certificate rules with partial-chain trust, at `2026-10-21T12:00:00Z` (`1792584000`). Fixing the verification date keeps later certificate expiry from masking a broken mitigation. All twelve chains must verify against the affected positive-control roots and fail against the unaffected G1-only store. Adding the compromised DV G3B intermediate restores two paths, which the checker must reject. The regression makes no network requests and skips chain verification if the OpenSSL executable is unavailable; certificate-inventory tests still run using Python's standard library.
|
The offline OpenSSL regression verifies server purpose, hostname and strict certificate rules with partial-chain trust, at `2026-10-23T12:00:00Z` (`1792756800`). Fixing the verification date keeps later certificate expiry from masking a broken mitigation. All fourteen chains must verify against the affected positive-control roots and fail against the unaffected G1-only store. Adding the compromised DV G3B intermediate restores four paths, including the two new hosts, which the checker must reject. The regression makes no network requests and skips chain verification if the OpenSSL executable is unavailable; certificate-inventory tests still run using Python's standard library.
|
||||||
|
|
||||||
|
The upstream SearXNG image validation uses the same `incident.json` inventory and fixed verification timestamp, checking each stored DER fingerprint before use. See [the image documentation](../../../docs/searxng-trust-image.md) for its validation targets and source provenance.
|
||||||
|
|||||||
+12
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"verification_time": 1792584000,
|
"verification_time": 1792756800,
|
||||||
"roots": {
|
"roots": {
|
||||||
"7dd78d5f4f13459a83dff9abbba62edbaf6f2d102bf257fd712f4d9f2746ed8d": "emSign Root TLS CA - G3",
|
"7dd78d5f4f13459a83dff9abbba62edbaf6f2d102bf257fd712f4d9f2746ed8d": "emSign Root TLS CA - G3",
|
||||||
"f2edaaff3d9b701fb4d14c9f285f8c0bad90fc3bcc1fa07d47f4718edaba34a5": "emSign Root CS CA - G3",
|
"f2edaaff3d9b701fb4d14c9f285f8c0bad90fc3bcc1fa07d47f4718edaba34a5": "emSign Root CS CA - G3",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"certificates": {
|
"certificates": {
|
||||||
"0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1\nMDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk\naHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy\npDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV\nbMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg\nyuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd\nAEKwaJ0mVzScy2+6\n-----END CERTIFICATE-----\n",
|
"0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1\nMDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk\naHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy\npDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV\nbMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg\nyuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd\nAEKwaJ0mVzScy2+6\n-----END CERTIFICATE-----\n",
|
||||||
|
"0cbfe22b824385823fd3bf4be4d131e8901342933a7feef1cf8b3110b8e44a70": "-----BEGIN CERTIFICATE-----\nMIICQTCCAcigAwIBAgIUG01Z/jOotSvB49K4K27kFjpdnWMwCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjIxMjU3MzFaFw0yNzA1MDUxMzAyMzFa\nMDQxMjAwBgNVBAMMKWR2LWczYi1ub2N0LWVtdWRocmEud2UtaGF2ZS1hLXByb2Js\nZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEI805yWr+RxanILifET+/\nGwHsXo7dp8ghA3OgwR0Mg11Bl6iYyOzldy4iU7kM+3qfDNsUrzxvkUkJZQpYieuC\n+aOBrDCBqTAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK\nBggrBgEFBQcDATA0BgNVHREELTArgilkdi1nM2Itbm9jdC1lbXVkaHJhLndlLWhh\ndmUtYS1wcm9ibGVtLmNvbTAdBgNVHQ4EFgQUOlRrLQGIczrmr4H+Oak06aUmJ4Yw\nHwYDVR0jBBgwFoAU4h4v5xwZOQ5VS2LBICRXNSSKJRQwCgYIKoZIzj0EAwIDZwAw\nZAIwf4ekmRgXAxXP2AjpVY2f1bO42GC7Nnc80t4OpaWJeVdXpT+X3dOsLRwlIUm0\nYbMwAjBihTx7oHfaYB3KRnFuhKqlHYmUcOe9Hjxqs6bubZohhrqMQzPocyasDhJL\nUx3HsgQ=\n-----END CERTIFICATE-----\n",
|
||||||
"21f57f83631a93c98e867050075746b3361e0433c9e0e969462852476784f6f0": "-----BEGIN CERTIFICATE-----\nMIICPzCCAcagAwIBAgIUH0gWfBRnOiSjcduti/cukjDGAjIwCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDJaFw0yNzA1MDMwMjI3NDJa\nMDMxMTAvBgNVBAMMKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl\nbS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ3EfjVE+6fUm9gr7UoUv6N\nYPttIdILR9OMZnAdTj+RWT1Z4fp7dpC5mat1gbIo4J6EDelcOQQz5/oGeB3gFJKw\no4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG\nCCsGAQUFBwMBMDMGA1UdEQQsMCqCKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl\nLWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFKzQPp6gTBOWb0TBxZbkL5zsQn3oMB8G\nA1UdIwQYMBaAFJvYZVGv5+csN60I7mkZ2b83hK4KMAoGCCqGSM49BAMCA2cAMGQC\nMARKrRRBEWY2Ix1d80DGUvC7bWENP1Q9pf6BOGF4+84UiwV5+nAuht/+hdpAanWB\nsQIwJF2dW8tpPN6lTm1kDerzG6yJAJOz4drotaTthMF2+efuv5W2FL7Dr8qZSd1z\n201o\n-----END CERTIFICATE-----\n",
|
"21f57f83631a93c98e867050075746b3361e0433c9e0e969462852476784f6f0": "-----BEGIN CERTIFICATE-----\nMIICPzCCAcagAwIBAgIUH0gWfBRnOiSjcduti/cukjDGAjIwCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDJaFw0yNzA1MDMwMjI3NDJa\nMDMxMTAvBgNVBAMMKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl\nbS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ3EfjVE+6fUm9gr7UoUv6N\nYPttIdILR9OMZnAdTj+RWT1Z4fp7dpC5mat1gbIo4J6EDelcOQQz5/oGeB3gFJKw\no4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG\nCCsGAQUFBwMBMDMGA1UdEQQsMCqCKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl\nLWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFKzQPp6gTBOWb0TBxZbkL5zsQn3oMB8G\nA1UdIwQYMBaAFJvYZVGv5+csN60I7mkZ2b83hK4KMAoGCCqGSM49BAMCA2cAMGQC\nMARKrRRBEWY2Ix1d80DGUvC7bWENP1Q9pf6BOGF4+84UiwV5+nAuht/+hdpAanWB\nsQIwJF2dW8tpPN6lTm1kDerzG6yJAJOz4drotaTthMF2+efuv5W2FL7Dr8qZSd1z\n201o\n-----END CERTIFICATE-----\n",
|
||||||
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd": "-----BEGIN CERTIFICATE-----\nMIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw\nCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy\nYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290\nIENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD\nVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg\nMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr\ngQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m\nWO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw\nLKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj\nQjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB\nFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi\nBggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4\naHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290\nQ0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t\nP1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB\nATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg\nTroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0\n1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV\n-----END CERTIFICATE-----\n",
|
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd": "-----BEGIN CERTIFICATE-----\nMIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw\nCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy\nYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290\nIENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD\nVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg\nMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr\ngQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m\nWO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw\nLKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj\nQjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB\nFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi\nBggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4\naHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290\nQ0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t\nP1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB\nATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg\nTroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0\n1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV\n-----END CERTIFICATE-----\n",
|
||||||
"346cdd962e97189b59aee5526d1ae8fa714bb1a64094dfa67bef768ee93f36b5": "-----BEGIN CERTIFICATE-----\nMIICODCCAb+gAwIBAgIUbWvtwSNK/Qro223vAqyz323+VBAwCgYIKoZIzj0EAwIw\nQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF\nTSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM2WhcNMjcwNTAzMDEwNDM2\nWjAvMS0wKwYDVQQDDCRkdi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j\nb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAScdBQpNUOOCcag6YEVFccNRSFe\nUIl02iH53vDfoWPJFS005WzqF3CPDclB/kygP2gWF4pg/r5gPYdnI3/6Vdzro4Gn\nMIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG\nAQUFBwMBMC8GA1UdEQQoMCaCJGR2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i\nbGVtLmNvbTAdBgNVHQ4EFgQUBj2yGO/TzDOE/u/6QeUvcW8y1iMwHwYDVR0jBBgw\nFoAUrn4QXDwplsGoRycFWfHgPdTlCvMwCgYIKoZIzj0EAwIDZwAwZAIwBQOOQqG9\nccdzM/7XIlXLtota1esALaLmgEN/ZweosBIvk4xabHcERZInWVIwO45NAjBos8Ue\nNwDe1X3ahdK2lkxqBEoBMtUHJN18TnJKs2nK1gmXpMbmY5oKnAQQgCEZPA4=\n-----END CERTIFICATE-----\n",
|
"346cdd962e97189b59aee5526d1ae8fa714bb1a64094dfa67bef768ee93f36b5": "-----BEGIN CERTIFICATE-----\nMIICODCCAb+gAwIBAgIUbWvtwSNK/Qro223vAqyz323+VBAwCgYIKoZIzj0EAwIw\nQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF\nTSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM2WhcNMjcwNTAzMDEwNDM2\nWjAvMS0wKwYDVQQDDCRkdi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j\nb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAScdBQpNUOOCcag6YEVFccNRSFe\nUIl02iH53vDfoWPJFS005WzqF3CPDclB/kygP2gWF4pg/r5gPYdnI3/6Vdzro4Gn\nMIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG\nAQUFBwMBMC8GA1UdEQQoMCaCJGR2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i\nbGVtLmNvbTAdBgNVHQ4EFgQUBj2yGO/TzDOE/u/6QeUvcW8y1iMwHwYDVR0jBBgw\nFoAUrn4QXDwplsGoRycFWfHgPdTlCvMwCgYIKoZIzj0EAwIDZwAwZAIwBQOOQqG9\nccdzM/7XIlXLtota1esALaLmgEN/ZweosBIvk4xabHcERZInWVIwO45NAjBos8Ue\nNwDe1X3ahdK2lkxqBEoBMtUHJN18TnJKs2nK1gmXpMbmY5oKnAQQgCEZPA4=\n-----END CERTIFICATE-----\n",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl\nZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz\nOTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11\nZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+\nbSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm\nxRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9\ni1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU\nBggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq\nMCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG\nAQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc\n9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f\nAal1GNxsOutLnr0Y\n-----END CERTIFICATE-----\n",
|
"432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl\nZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz\nOTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11\nZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+\nbSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm\nxRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9\ni1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU\nBggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq\nMCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG\nAQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc\n9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f\nAal1GNxsOutLnr0Y\n-----END CERTIFICATE-----\n",
|
||||||
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO\nL4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2\nPM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE\nwqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM\nQkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ\n7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl\nbN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V\nGUyRCEIDhF5TBUuKF4E3kf1h\n-----END CERTIFICATE-----\n",
|
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO\nL4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2\nPM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE\nwqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM\nQkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ\n7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl\nbN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V\nGUyRCEIDhF5TBUuKF4E3kf1h\n-----END CERTIFICATE-----\n",
|
||||||
"4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1": "-----BEGIN CERTIFICATE-----\nMIIDWzCCAuGgAwIBAgIPVEt/kiwxriIKSpIpFvbmMAoGCCqGSM49BAMDMFUxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2Mzg0\nMFoXDTQwMDQyODE4MzAwMFowPzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRo\ncmEgSW5jMRowGAYDVQQDDBFFTSBPViBDUyBDQSAtIEczQzB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABLnw4GNHAqacpqrdRDz9YsQbm6SyCSRpfXl052kaEm+5avs+NML9\nO8X0gwYfJl3CpYQ2NHMjUQQ2so8E84YjYwZ5gOJQPgJTLWVGI1pxZaJZoe9Jb3Qb\nX97neHII5Lj676OCAYswggGHMB8GA1UdIwQYMBaAFGh0A/iFyo1UUFcdFuDI3kUN\nq80tMB0GA1UdDgQWBBTub3MpjW+/p7Mbw5U09hVWRHhmtzATBgNVHSUEDDAKBggr\nBgEFBQcDAzBeBgNVHSAEVzBVMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEW\nHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDgYMKwYBBAGDjiEBAoFIMAgG\nBmeBDAEEATB0BggrBgEFBQcBAQRoMGYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw\nLWMuZW1zaWduLmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3JlcG9zaXRvcnkuZW1z\naWduLmNvbS9jZXJ0cy9FTU9WQ1NDQUczQy5wN2MwNgYDVR0fBC8wLTAroCmgJ4Yl\naHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290Y3NjYWczLmNybDAOBgNVHQ8BAf8E\nBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAwNoADBlAjEA8yPg\n8sntOhLwV2QXJ5LEaKSpo/fnE3uOgTadK73fDhyjiqr0Io9GVuQTYqBPbQ2TAjBg\neuApmBS486+iei03lRrp139dPw6k28jjGNvWsAU7A1N86J1zPJVDEyVH+nMAWC0=\n-----END CERTIFICATE-----\n",
|
"4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1": "-----BEGIN CERTIFICATE-----\nMIIDWzCCAuGgAwIBAgIPVEt/kiwxriIKSpIpFvbmMAoGCCqGSM49BAMDMFUxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2Mzg0\nMFoXDTQwMDQyODE4MzAwMFowPzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRo\ncmEgSW5jMRowGAYDVQQDDBFFTSBPViBDUyBDQSAtIEczQzB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABLnw4GNHAqacpqrdRDz9YsQbm6SyCSRpfXl052kaEm+5avs+NML9\nO8X0gwYfJl3CpYQ2NHMjUQQ2so8E84YjYwZ5gOJQPgJTLWVGI1pxZaJZoe9Jb3Qb\nX97neHII5Lj676OCAYswggGHMB8GA1UdIwQYMBaAFGh0A/iFyo1UUFcdFuDI3kUN\nq80tMB0GA1UdDgQWBBTub3MpjW+/p7Mbw5U09hVWRHhmtzATBgNVHSUEDDAKBggr\nBgEFBQcDAzBeBgNVHSAEVzBVMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEW\nHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDgYMKwYBBAGDjiEBAoFIMAgG\nBmeBDAEEATB0BggrBgEFBQcBAQRoMGYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw\nLWMuZW1zaWduLmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3JlcG9zaXRvcnkuZW1z\naWduLmNvbS9jZXJ0cy9FTU9WQ1NDQUczQy5wN2MwNgYDVR0fBC8wLTAroCmgJ4Yl\naHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290Y3NjYWczLmNybDAOBgNVHQ8BAf8E\nBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAwNoADBlAjEA8yPg\n8sntOhLwV2QXJ5LEaKSpo/fnE3uOgTadK73fDhyjiqr0Io9GVuQTYqBPbQ2TAjBg\neuApmBS486+iei03lRrp139dPw6k28jjGNvWsAU7A1N86J1zPJVDEyVH+nMAWC0=\n-----END CERTIFICATE-----\n",
|
||||||
|
"5510ecf5764dee8dfe9468ae2d7323da21fcd5530889906540a2f8725c6f74db": "-----BEGIN CERTIFICATE-----\nMIICSTCCAdCgAwIBAgIUO7oRaIOhMWD0jFgvxusAGNi5CI0wCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjIxMzE0NDhaFw0yNzA1MDUxMzE5NDha\nMDgxNjA0BgNVBAMMLWR2LWczYi1hYWEtbm9jdC1lbXVkaHJhLndlLWhhdmUtYS1w\ncm9ibGVtLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ0yLMf28vw2E6pO\nc74doinU47pwTYD6xZ60nkrGkqI05DxVKXblos+V5S0+bgDjT2qw1F39EW9EUj8b\nNQmmqv6jgbAwga0wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l\nBAwwCgYIKwYBBQUHAwEwOAYDVR0RBDEwL4ItZHYtZzNiLWFhYS1ub2N0LWVtdWRo\ncmEud2UtaGF2ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBTruPPaZ1PiEFfpqdLo\n8Ra5Ah5a2jAfBgNVHSMEGDAWgBTiHi/nHBk5DlVLYsEgJFc1JIolFDAKBggqhkjO\nPQQDAgNnADBkAjBUTUTymJylZiZRV/RqMayZdUPJKRDIcn9+sUPpG66AjVgx8M+h\nDGzxxgeroOSxzSwCMBM/r1y7g371RI10nDU16T0WEAO/Udqyyuc8mJ2LCQ+Gxv3c\njU2NXGQLsvosv6T0ww==\n-----END CERTIFICATE-----\n",
|
||||||
"579480aea166290bd3c643589e795fc0d0e54e86505d180f1dff99efd3199512": "-----BEGIN CERTIFICATE-----\nMIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL\nzWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W\nSvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB\nAngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB\nQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF\nBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB\nAJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM\n7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf\nFVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk\n2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM\nm7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet\nbAlwhwFT6AP8uIVMGUHNw24=\n-----END CERTIFICATE-----\n",
|
"579480aea166290bd3c643589e795fc0d0e54e86505d180f1dff99efd3199512": "-----BEGIN CERTIFICATE-----\nMIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL\nzWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W\nSvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB\nAngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB\nQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF\nBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB\nAJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM\n7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf\nFVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk\n2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM\nm7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet\nbAlwhwFT6AP8uIVMGUHNw24=\n-----END CERTIFICATE-----\n",
|
||||||
"59cf0386b56bc2414a0c06241ea9e25e01a9c93bac7a596648747f9d0ded4906": "-----BEGIN CERTIFICATE-----\nMIICQTCCAcegAwIBAgIULuWnWW6kFV6QZxp2ls8r18NCjXkwCgYIKoZIzj0EAwIw\nQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF\nTSBPViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy\nWjAzMTEwLwYDVQQDDChvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js\nZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb7+5g2YHNYwH2eQERrZc\nTmVuf/RmvzsGGMSwXmZke6vn35Ut5HME7UOOU3Muy+BSfjfpo6fjL5KOs3Ndz67d\nOKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK\nBggrBgEFBQcDATAzBgNVHREELDAqgihvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2\nZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBQ7oBzq3jpTuvntbIkzMV12Ps3O0TAf\nBgNVHSMEGDAWgBQ06uoM68kK2vnQxeLtm3jJh54bbDAKBggqhkjOPQQDAgNoADBl\nAjEA/Y7AZ4M2MYbN5Od9tREaUfFjZKoOjczfIKPjKNMjtGxTdL9WUXvAT4QDgktn\n/IJdAjAbORTkAXD5hggyG6K7p0MBz3lFZJb2l/LndZlrttJWBLP8QGrc44hrZZtK\nIq0d1dE=\n-----END CERTIFICATE-----\n",
|
"59cf0386b56bc2414a0c06241ea9e25e01a9c93bac7a596648747f9d0ded4906": "-----BEGIN CERTIFICATE-----\nMIICQTCCAcegAwIBAgIULuWnWW6kFV6QZxp2ls8r18NCjXkwCgYIKoZIzj0EAwIw\nQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF\nTSBPViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy\nWjAzMTEwLwYDVQQDDChvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js\nZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb7+5g2YHNYwH2eQERrZc\nTmVuf/RmvzsGGMSwXmZke6vn35Ut5HME7UOOU3Muy+BSfjfpo6fjL5KOs3Ndz67d\nOKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK\nBggrBgEFBQcDATAzBgNVHREELDAqgihvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2\nZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBQ7oBzq3jpTuvntbIkzMV12Ps3O0TAf\nBgNVHSMEGDAWgBQ06uoM68kK2vnQxeLtm3jJh54bbDAKBggqhkjOPQQDAgNoADBl\nAjEA/Y7AZ4M2MYbN5Od9tREaUfFjZKoOjczfIKPjKNMjtGxTdL9WUXvAT4QDgktn\n/IJdAjAbORTkAXD5hggyG6K7p0MBz3lFZJb2l/LndZlrttJWBLP8QGrc44hrZZtK\nIq0d1dE=\n-----END CERTIFICATE-----\n",
|
||||||
"5f1ded4dd590af74631290cf073b122a2aac6a1fa479778a0bd43579bd832699": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy\nKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv\nTPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj\nCeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y\nmtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb\nbERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl\nCCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x\n0k9mldz2dqI9nhCiY+TTIOOR\n-----END CERTIFICATE-----\n",
|
"5f1ded4dd590af74631290cf073b122a2aac6a1fa479778a0bd43579bd832699": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy\nKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv\nTPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj\nCeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y\nmtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb\nbERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl\nCCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x\n0k9mldz2dqI9nhCiY+TTIOOR\n-----END CERTIFICATE-----\n",
|
||||||
@@ -64,11 +66,20 @@
|
|||||||
"7e02622ab6d745c1edb4b431bb1c551e667305c3dd3eb0c7cc6dfdb523807424",
|
"7e02622ab6d745c1edb4b431bb1c551e667305c3dd3eb0c7cc6dfdb523807424",
|
||||||
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe"
|
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe"
|
||||||
],
|
],
|
||||||
|
"dv-g3b-aaa-noct-emudhra.we-have-a-problem.com": [
|
||||||
|
"5510ecf5764dee8dfe9468ae2d7323da21fcd5530889906540a2f8725c6f74db",
|
||||||
|
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe"
|
||||||
|
],
|
||||||
"dv-g3b-emudhra.we-have-a-problem.com": [
|
"dv-g3b-emudhra.we-have-a-problem.com": [
|
||||||
"9dcb21b1841d75313b3c888f4bb9cc01f8f51379a76fbaa241ca492b668839e7",
|
"9dcb21b1841d75313b3c888f4bb9cc01f8f51379a76fbaa241ca492b668839e7",
|
||||||
"42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493",
|
"42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493",
|
||||||
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd"
|
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd"
|
||||||
],
|
],
|
||||||
|
"dv-g3b-noct-emudhra.we-have-a-problem.com": [
|
||||||
|
"0cbfe22b824385823fd3bf4be4d131e8901342933a7feef1cf8b3110b8e44a70",
|
||||||
|
"42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493",
|
||||||
|
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd"
|
||||||
|
],
|
||||||
"dv-g3c-aaa-emudhra.we-have-a-problem.com": [
|
"dv-g3c-aaa-emudhra.we-have-a-problem.com": [
|
||||||
"9200d153c3cac272cc06bf68f9ea163fc24659fc3331e77668236fe00645ae52",
|
"9200d153c3cac272cc06bf68f9ea163fc24659fc3331e77668236fe00645ae52",
|
||||||
"62b7f9868263f72f6911fc78ceaf72c1979158b7368bf648683bb2aa39b02e72"
|
"62b7f9868263f72f6911fc78ceaf72c1979158b7368bf648683bb2aa39b02e72"
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class CATrustTests(unittest.TestCase):
|
|||||||
custom = directory / "custom.pem"
|
custom = directory / "custom.pem"
|
||||||
fingerprint = next(iter(INCIDENT["intermediates"]))
|
fingerprint = next(iter(INCIDENT["intermediates"]))
|
||||||
custom.write_text(clean.read_text() + INCIDENT["certificates"][fingerprint])
|
custom.write_text(clean.read_text() + INCIDENT["certificates"][fingerprint])
|
||||||
self.assertEqual(len(INCIDENT["chains"]), 12)
|
self.assertEqual(len(INCIDENT["chains"]), 14)
|
||||||
custom_accepted = []
|
custom_accepted = []
|
||||||
for hostname, chain in INCIDENT["chains"].items():
|
for hostname, chain in INCIDENT["chains"].items():
|
||||||
leaf, intermediates = directory / "leaf.pem", directory / "chain.pem"
|
leaf, intermediates = directory / "leaf.pem", directory / "chain.pem"
|
||||||
@@ -156,9 +156,11 @@ class CATrustTests(unittest.TestCase):
|
|||||||
self.assertIn("unable to get local issuer certificate", negative.stderr)
|
self.assertIn("unable to get local issuer certificate", negative.stderr)
|
||||||
if verify(custom).returncode == 0:
|
if verify(custom).returncode == 0:
|
||||||
custom_accepted.append(hostname)
|
custom_accepted.append(hostname)
|
||||||
self.assertEqual(custom_accepted, [
|
self.assertCountEqual(custom_accepted, [
|
||||||
"dv-g3b-aaa-emudhra.we-have-a-problem.com",
|
"dv-g3b-aaa-emudhra.we-have-a-problem.com",
|
||||||
|
"dv-g3b-aaa-noct-emudhra.we-have-a-problem.com",
|
||||||
"dv-g3b-emudhra.we-have-a-problem.com",
|
"dv-g3b-emudhra.we-have-a-problem.com",
|
||||||
|
"dv-g3b-noct-emudhra.we-have-a-problem.com",
|
||||||
])
|
])
|
||||||
checker.check_bundle(clean)
|
checker.check_bundle(clean)
|
||||||
for bundle in (affected, custom):
|
for bundle in (affected, custom):
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""A network failure must not be mistaken for rejection of an incident chain."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from types import ModuleType, SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
INCIDENT = json.loads((ROOT / "tests/fixtures/ca-trust/incident.json").read_text())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def validator():
|
||||||
|
checker = ModuleType("check_ca_trust")
|
||||||
|
checker.check_bundle = lambda path: None
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"searxng_validation", ROOT / "docker/searxng/validate_image.py"
|
||||||
|
)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
with patch.dict(sys.modules, {"check_ca_trust": checker}):
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
module.__file__ = str(ROOT / "tests/fixtures/ca-trust/validate_image.py")
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class RequestFailure(Exception):
|
||||||
|
def __init__(self, code):
|
||||||
|
super().__init__(f"curl error {code}")
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("code", [60, 6, 28, 35, None])
|
||||||
|
def test_live_probe_requires_certificate_errors_for_every_host(validator, code):
|
||||||
|
hosts = []
|
||||||
|
|
||||||
|
async def get(url, **kwargs):
|
||||||
|
if url == "https://pypi.org/simple/certifi/":
|
||||||
|
return SimpleNamespace(status_code=200)
|
||||||
|
hosts.append(url.removeprefix("https://").removesuffix("/"))
|
||||||
|
assert kwargs["allow_redirects"] is False
|
||||||
|
if code is not None:
|
||||||
|
raise RequestFailure(code)
|
||||||
|
return SimpleNamespace(status_code=200)
|
||||||
|
|
||||||
|
client = SimpleNamespace(get=get, aclose=AsyncMock())
|
||||||
|
factory = ModuleType("searx.network.client")
|
||||||
|
|
||||||
|
def new_client(**kwargs):
|
||||||
|
assert kwargs["verify"] is True
|
||||||
|
return client
|
||||||
|
|
||||||
|
factory.new_client = new_client
|
||||||
|
errors = ModuleType("curl_cffi.requests.exceptions")
|
||||||
|
errors.RequestException = RequestFailure
|
||||||
|
modules = {"searx.network.client": factory, "curl_cffi.requests.exceptions": errors}
|
||||||
|
with patch.dict(sys.modules, modules), patch.object(sys, "path", list(sys.path)):
|
||||||
|
if code == 60:
|
||||||
|
asyncio.run(validator.check_https())
|
||||||
|
assert set(hosts) == set(INCIDENT["chains"])
|
||||||
|
else:
|
||||||
|
expected = "unexpectedly accepted" if code is None else "inconclusive"
|
||||||
|
with pytest.raises(RuntimeError, match=expected):
|
||||||
|
asyncio.run(validator.check_https())
|
||||||
|
client.aclose.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_offline_probe_rejects_openssl_execution_failure(validator):
|
||||||
|
result = SimpleNamespace(returncode=1, stderr="openssl: unknown option", stdout="")
|
||||||
|
with patch.dict(validator.os.environ, {"SSL_CERT_FILE": "/unused.pem", "SSL_CERT_DIR": "/unused"}), \
|
||||||
|
patch.object(validator.subprocess, "run", return_value=result):
|
||||||
|
with pytest.raises(RuntimeError, match="not a certificate trust rejection"):
|
||||||
|
validator.check_chains()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("results", [[{"title": "ordinary result"}], []])
|
||||||
|
def test_running_probe_checks_existing_service_without_repair_or_second_boot(validator, tmp_path, results):
|
||||||
|
certificate = tmp_path / "01234567.0"
|
||||||
|
certificate.write_text("certificate double")
|
||||||
|
(tmp_path / "unrelated").write_text("ignored")
|
||||||
|
health = io.BytesIO(b"OK")
|
||||||
|
health.status = 200
|
||||||
|
search = io.BytesIO(json.dumps({"results": results}).encode())
|
||||||
|
with patch.dict(validator.os.environ, {"SSL_CERT_DIR": str(tmp_path), "GRANIAN_PORT": "8080"}), \
|
||||||
|
patch.object(validator.urllib.request, "build_opener") as opener, \
|
||||||
|
patch.object(validator, "check_chains") as chains, \
|
||||||
|
patch.object(validator, "check_bundle") as bundle, \
|
||||||
|
patch.object(validator, "check_https", new_callable=AsyncMock) as https, \
|
||||||
|
patch.object(validator.subprocess, "Popen") as boot:
|
||||||
|
opener.return_value.open.side_effect = [health, search]
|
||||||
|
if results:
|
||||||
|
validator.check_running(live=True)
|
||||||
|
else:
|
||||||
|
with pytest.raises(RuntimeError, match="no results"):
|
||||||
|
validator.check_running(live=True)
|
||||||
|
assert opener.return_value.open.call_args_list[0].args == ("http://127.0.0.1:8080/healthz",)
|
||||||
|
chains.assert_called_once_with()
|
||||||
|
bundle.assert_called_once_with(certificate)
|
||||||
|
https.assert_awaited_once()
|
||||||
|
boot.assert_not_called()
|
||||||
@@ -10,17 +10,22 @@ startup. This guards that the pin stays in place.
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
COMPOSE = Path(__file__).resolve().parent.parent / "docker-compose.yml"
|
import yaml
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
COMPOSE = ROOT / "docker-compose.yml"
|
||||||
|
|
||||||
|
|
||||||
def test_searxng_image_is_pinned_not_latest():
|
def test_searxng_image_is_pinned_not_latest():
|
||||||
text = COMPOSE.read_text(encoding="utf-8")
|
text = COMPOSE.read_text(encoding="utf-8")
|
||||||
m = re.search(r"image:\s*\S*searxng/searxng:(\S+)", text)
|
image = yaml.safe_load(text)["services"]["searxng"]["image"]
|
||||||
assert m, "searxng image line not found in docker-compose.yml"
|
assert re.fullmatch(
|
||||||
tag = m.group(1)
|
r"docker\.io/searxng/searxng:\d{4}\.\d[^\s@]*@sha256:[0-9a-f]{64}", image
|
||||||
assert tag != "latest", (
|
), f"SearXNG must use a versioned immutable upstream image: {image!r}"
|
||||||
"SearXNG must be pinned, not ':latest' — odysseus startup depends on its "
|
|
||||||
"healthcheck, so a broken upstream latest tag blocks the app (issue #1414)"
|
|
||||||
)
|
def test_all_standalone_compose_files_select_the_same_searxng_image():
|
||||||
# A real version tag (date-based, e.g. 2026.5.31-7159b8aed), not a moving ref.
|
expected = yaml.safe_load(COMPOSE.read_text())["services"]["searxng"]["image"]
|
||||||
assert re.match(r"\d{4}\.\d", tag), f"expected a versioned tag, got {tag!r}"
|
for name in ("docker-compose.gpu-amd.yml", "docker-compose.gpu-nvidia.yml"):
|
||||||
|
actual = yaml.safe_load((ROOT / name).read_text())["services"]["searxng"]["image"]
|
||||||
|
assert actual == expected, f"{name} selects a different SearXNG image"
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""Native checks must validate the selected runtime without registry credentials."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ci_checks_the_pinned_upstream_image_on_both_native_architectures():
|
||||||
|
workflow = yaml.safe_load((ROOT / ".github/workflows/searxng-image.yml").read_text())
|
||||||
|
events = workflow.get("on", workflow.get(True))
|
||||||
|
assert "pull_request_target" not in events
|
||||||
|
assert workflow["permissions"] == {"contents": "read"}
|
||||||
|
assert set(workflow["jobs"]) == {"validate"}
|
||||||
|
job = workflow["jobs"]["validate"]
|
||||||
|
matrix = job["strategy"]["matrix"]["include"]
|
||||||
|
assert {(row["arch"], row["runner"]) for row in matrix} == {
|
||||||
|
("amd64", "ubuntu-latest"), ("arm64", "ubuntu-24.04-arm")
|
||||||
|
}
|
||||||
|
steps = job["steps"]
|
||||||
|
assert not any("login-action" in step.get("uses", "") for step in steps)
|
||||||
|
assert all(step.get("with", {}).get("persist-credentials") is False
|
||||||
|
for step in steps if "actions/checkout@" in step.get("uses", ""))
|
||||||
|
offline = next(step for step in steps if "docker/build-push-action@" in step.get("uses", ""))
|
||||||
|
assert offline["with"]["network"] == "none"
|
||||||
|
assert offline["with"]["build-args"] == "SEARXNG_IMAGE=${{ steps.image.outputs.image }}"
|
||||||
|
live = next(step for step in steps if "validate-searxng-image.sh" in step.get("run", ""))
|
||||||
|
assert live["env"]["SEARXNG_TEST_IMAGE"] == "${{ steps.image.outputs.image }}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("failure,expected", [("none", 0), ("exec", 7), ("up", 1), ("existing-volume", 1)])
|
||||||
|
def test_compose_probe_owns_only_its_temporary_resources(tmp_path, failure, expected):
|
||||||
|
"""Exercise the real helper with a Docker double, including failure cleanup."""
|
||||||
|
binary = tmp_path / "docker"
|
||||||
|
calls = tmp_path / "calls.jsonl"
|
||||||
|
binary.write_text('''#!/usr/bin/env python3
|
||||||
|
import json, os, sys
|
||||||
|
args = sys.argv[1:]
|
||||||
|
with open(os.environ["DOCKER_CALLS"], "a") as log:
|
||||||
|
log.write(json.dumps({"args": args, "volume": os.environ["SEARXNG_VALIDATION_VOLUME"], "secret": os.environ["SEARXNG_SECRET"]}) + "\\n")
|
||||||
|
failure = os.environ["DOCKER_FAILURE"]
|
||||||
|
if args[:2] == ["volume", "inspect"]:
|
||||||
|
sys.exit(0 if failure == "existing-volume" else 1)
|
||||||
|
if "exec" in args and failure == "exec":
|
||||||
|
sys.exit(7)
|
||||||
|
if "up" in args and failure == "up":
|
||||||
|
sys.exit(9)
|
||||||
|
''')
|
||||||
|
binary.chmod(0o755)
|
||||||
|
result = subprocess.run(["bash", str(ROOT / ".github/scripts/validate-searxng-image.sh"), "localhost/searxng:validated"], env={**os.environ, "PATH": str(tmp_path) + ":" + os.environ["PATH"], "DOCKER_CALLS": str(calls), "DOCKER_FAILURE": failure, "SEARXNG_SECRET": "operator-secret-must-not-propagate"}, capture_output=True, text=True)
|
||||||
|
assert result.returncode == expected, result.stderr
|
||||||
|
entries = [json.loads(line) for line in calls.read_text().splitlines()]
|
||||||
|
assert all(entry["secret"] == "searxng-ci-validation-only" for entry in entries)
|
||||||
|
commands = [entry["args"] for entry in entries]
|
||||||
|
if failure == "existing-volume":
|
||||||
|
assert len(commands) == 1
|
||||||
|
return
|
||||||
|
up = next(command for command in commands if "up" in command)
|
||||||
|
assert up[up.index("--env-file") + 1] == "/dev/null"
|
||||||
|
assert "--no-deps" in up and "--no-build" in up and "--wait" in up
|
||||||
|
assert up[up.index("--pull") + 1] == "never"
|
||||||
|
down = next(command for command in commands if "down" in command)
|
||||||
|
assert "--volumes" not in down
|
||||||
|
assert commands[-1] == ["volume", "rm", entries[-1]["volume"]]
|
||||||
|
if failure != "up":
|
||||||
|
execute = next(command for command in commands if "exec" in command)
|
||||||
|
assert execute[-2:] == ["--running", "--live"]
|
||||||
Reference in New Issue
Block a user