add custom TAL installer patch
This commit is contained in:
parent
9f4f4cf069
commit
850ff218ce
@ -37,12 +37,27 @@ RETAIN_RUNS=100
|
||||
# TAL/TA input mode:
|
||||
# file-with-ta: use packaged fixture TAL + TA only.
|
||||
# file-live-ta: use packaged fixture TAL; snapshot waits for live TA refresh, delta refreshes TA in background.
|
||||
# custom-file-with-ta: use one custom TAL + TA from the read-only custom fixture mount.
|
||||
# url: pass TAL URL to child process.
|
||||
TAL_INPUT_MODE=file-live-ta
|
||||
LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1
|
||||
LIVE_TA_REFRESH_CONNECT_TIMEOUT_SECS=15
|
||||
LIVE_TA_REFRESH_MAX_TIME_SECS=120
|
||||
|
||||
# Custom TAL patch mode. Keep TAL_INPUT_MODE=file-live-ta and the five-RIR RIRS
|
||||
# list above for the original package behavior. For a private fixture, set:
|
||||
# RIRS=custom
|
||||
# TAL_INPUT_MODE=custom-file-with-ta
|
||||
# The paths below are container paths; CUSTOM_FIXTURE_HOST_DIR is a host path
|
||||
# resolved relative to the compose directory when left at its default.
|
||||
CUSTOM_FIXTURE_HOST_DIR=../custom-fixtures
|
||||
CUSTOM_TAL_PATH=/opt/ours-rp/custom-fixtures/tal/custom.tal
|
||||
CUSTOM_TA_PATH=/opt/ours-rp/custom-fixtures/ta/custom-ta.cer
|
||||
CUSTOM_TAL_URI=https://host.docker.internal:18443/tal/custom.tal
|
||||
# Space-separated container paths; paths must not contain spaces. Set this in
|
||||
# custom mode when the RRDP server uses a private/self-signed CA.
|
||||
HTTP_ROOT_CERT_PATHS=
|
||||
|
||||
# Sync and runtime behavior.
|
||||
RSYNC_SCOPE=module-root
|
||||
DISABLE_COMPETING_RPS=0
|
||||
|
||||
@ -11,8 +11,15 @@ services:
|
||||
RUN_ROOT: /var/lib/ours-rp
|
||||
BIN_DIR: /opt/ours-rp/bin
|
||||
FIXTURE_DIR: /opt/ours-rp/fixtures
|
||||
CUSTOM_FIXTURE_DIR: /opt/ours-rp/custom-fixtures
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ../.env:/opt/ours-rp/.env:ro
|
||||
# Overlay the patched runner while keeping the original runtime image
|
||||
# and its exact rpki binaries unchanged.
|
||||
- ../scripts/soak/run_soak.sh:/opt/ours-rp/run_soak.sh:ro
|
||||
- ${CUSTOM_FIXTURE_HOST_DIR:-../custom-fixtures}:/opt/ours-rp/custom-fixtures:ro
|
||||
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/state:/var/lib/ours-rp/state
|
||||
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/runs:/var/lib/ours-rp/runs
|
||||
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/logs:/var/lib/ours-rp/logs
|
||||
|
||||
14
deploy/docker-installer/custom-fixtures/README.md
Normal file
14
deploy/docker-installer/custom-fixtures/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Custom TAL fixture mount
|
||||
|
||||
This directory is intentionally shipped as a mount point, not as a repository
|
||||
of private keys. Run the patch tool `tools/generate_custom_fixture.py` to create
|
||||
the test-only TAL, TA, RPKI objects, HTTPS CA/server certificates, RRDP files,
|
||||
and rsync module data below it.
|
||||
|
||||
The generated fixture contains `baseline-v1`, `sync-hash-mismatch` and
|
||||
`baseline-v2` for snapshot / hash / delta tests, plus
|
||||
`validation-expired`, `validation-max-length`,
|
||||
`validation-max-length-invalid`, `validation-roa-prefix-outside`,
|
||||
`validation-out-of-resource`, `validation-ca-over-resource`, and
|
||||
`validation-nonstandard` for validation cases. Generated keys are test-only
|
||||
and must not be used in a production RPKI hierarchy.
|
||||
30
deploy/docker-installer/custom-tal-patch/README.zh-CN.md
Normal file
30
deploy/docker-installer/custom-tal-patch/README.zh-CN.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 自建 TAL/TA 补丁
|
||||
|
||||
本目录描述并生成一个叠加到客户 Arm64 Ours RP 组件包上的测试补丁。补丁基线是源码 commit `9f4f4cf069e9fa8f6f97ed0f1065adac38abe82f`,不会替换客户包中的 runtime image 或 `bin/rpki`、`bin/rpki_daemon` 二进制。
|
||||
|
||||
补丁做三件事:
|
||||
|
||||
1. 在 runner 中增加 `RIRS=custom`、`TAL_INPUT_MODE=custom-file-with-ta`、自定义 TAL/TA 路径和 `HTTP_ROOT_CERT_PATHS`;
|
||||
2. 通过 Compose 只读挂载 patched runner、`custom-fixtures/` 和 `host.docker.internal`,连接固定 HTTPS RRDP 与 rsync 服务;
|
||||
3. 提供固定证书链、RPKI 对象生成器和服务启动脚本,不依赖 Barry、Rapport 或公网 RIR。
|
||||
|
||||
先停止 Ours RP,再执行:
|
||||
|
||||
```bash
|
||||
./apply_custom_tal_patch.sh \
|
||||
--component-root /opt/ours-rp-rtr-stack/components/ours-rp \
|
||||
--enable-custom
|
||||
```
|
||||
|
||||
`--enable-custom` 会修改组件 `.env` 的 custom 模式入口;不加该选项只安装 overlay,不改变当前 `.env`。回滚:
|
||||
|
||||
```bash
|
||||
./rollback_custom_tal_patch.sh \
|
||||
--component-root /opt/ours-rp-rtr-stack/components/ours-rp
|
||||
```
|
||||
|
||||
补丁应用脚本会校验 `PACKAGE-MANIFEST.env` 的源码 commit 和 `arm64` 架构,并留下 `CUSTOM-TAL-PATCH-MANIFEST.env` 与 `.custom-tal-patch-state/` 备份。升级组件后需要重新检查 patch marker;不要把该 overlay 误当成客户原始安装包。
|
||||
|
||||
生成器默认把容器访问地址写成 `host.docker.internal`,固定服务端口为 HTTPS RRDP `18443`、rsync `1873`,rsync module 为 `custom`。可用 `--host localhost` 生成给宿主机直接运行 `bin/rpki` 的本地变体。
|
||||
|
||||
用例包括:`baseline-v1`、`sync-hash-mismatch`、`baseline-v2`、`validation-expired`、`validation-max-length`、`validation-max-length-invalid`、`validation-roa-prefix-outside`、`validation-out-of-resource`、`validation-ca-over-resource`、`validation-nonstandard`。其中 `validation-ca-over-resource` 在 `RESOURCE_VALIDATION_MODE=rfc6487` 下验证严格拒绝;`validation-update-03` 是原包默认的兼容处理模式。
|
||||
@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PATCH_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PAYLOAD_ROOT="$SCRIPT_DIR/payload"
|
||||
STATE_DIR_NAME=".custom-tal-patch-state"
|
||||
EXPECTED_SOURCE_COMMIT="9f4f4cf069e9fa8f6f97ed0f1065adac38abe82f"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
./apply_custom_tal_patch.sh --component-root <installed-ours-rp-root>
|
||||
./apply_custom_tal_patch.sh --component-root <root> --enable-custom
|
||||
|
||||
The default operation installs the overlay but does not change the existing
|
||||
.env. --enable-custom writes the custom TAL/TA/root-cert example values into
|
||||
the component .env so the next run uses the fixed local fixture.
|
||||
Stop the ours RP service before applying or rolling back the overlay.
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
COMPONENT_ROOT=""
|
||||
ENABLE_CUSTOM=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--component-root)
|
||||
COMPONENT_ROOT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--enable-custom)
|
||||
ENABLE_CUSTOM=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$COMPONENT_ROOT" ]] || die "--component-root is required"
|
||||
COMPONENT_ROOT="$(cd "$COMPONENT_ROOT" 2>/dev/null && pwd)" \
|
||||
|| die "component root does not exist: $COMPONENT_ROOT"
|
||||
[[ -f "$COMPONENT_ROOT/PACKAGE-MANIFEST.env" ]] \
|
||||
|| die "missing component PACKAGE-MANIFEST.env: $COMPONENT_ROOT"
|
||||
[[ -d "$PAYLOAD_ROOT" ]] || die "missing patch payload: $PAYLOAD_ROOT"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$COMPONENT_ROOT/PACKAGE-MANIFEST.env"
|
||||
[[ "${source_commit:-}" == "$EXPECTED_SOURCE_COMMIT" ]] \
|
||||
|| die "base source commit mismatch: ${source_commit:-missing} != $EXPECTED_SOURCE_COMMIT"
|
||||
[[ "${package_arch:-${PACKAGE_ARCH:-}}" == "arm64" ]] \
|
||||
|| die "this patch is for the customer Arm64 component, got ${package_arch:-${PACKAGE_ARCH:-missing}}"
|
||||
|
||||
STATE_ROOT="$COMPONENT_ROOT/$STATE_DIR_NAME"
|
||||
BACKUP_ROOT="$STATE_ROOT/original"
|
||||
marker="$COMPONENT_ROOT/CUSTOM-TAL-PATCH-MANIFEST.env"
|
||||
|
||||
set_env_value() {
|
||||
local env_path="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_path="${env_path}.custom-tal-patch.tmp"
|
||||
[[ -f "$env_path" ]] || die "missing component .env: $env_path"
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { found = 0 }
|
||||
$0 ~ "^" key "=" { print key "=" value; found = 1; next }
|
||||
{ print }
|
||||
END { if (!found) print key "=" value }
|
||||
' "$env_path" > "$tmp_path"
|
||||
mv "$tmp_path" "$env_path"
|
||||
}
|
||||
|
||||
copy_payload_file() {
|
||||
local source_path="$1"
|
||||
local target_path="$2"
|
||||
mkdir -p "$(dirname "$target_path")"
|
||||
cp "$source_path" "$target_path"
|
||||
chmod --reference="$source_path" "$target_path" 2>/dev/null || true
|
||||
}
|
||||
|
||||
append_custom_env_example() {
|
||||
local target_path="$1"
|
||||
if ! grep -Eq '^CUSTOM_TAL_URI=' "$target_path"; then
|
||||
{
|
||||
printf '\n# #142 custom TAL patch settings; original package values above are preserved.\n'
|
||||
cat "$SCRIPT_DIR/custom-tal.env.example"
|
||||
} >> "$target_path"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -e "$STATE_ROOT" ]]; then
|
||||
die "patch is already applied or an incomplete state exists: $STATE_ROOT"
|
||||
fi
|
||||
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
for relative_path in compose/docker-compose.yml .env.example; do
|
||||
[[ -f "$COMPONENT_ROOT/$relative_path" ]] \
|
||||
|| die "missing base file: $COMPONENT_ROOT/$relative_path"
|
||||
mkdir -p "$BACKUP_ROOT/$(dirname "$relative_path")"
|
||||
cp -a "$COMPONENT_ROOT/$relative_path" "$BACKUP_ROOT/$relative_path"
|
||||
done
|
||||
|
||||
runner_path="$COMPONENT_ROOT/scripts/soak/run_soak.sh"
|
||||
if [[ -e "$runner_path" ]]; then
|
||||
mkdir -p "$BACKUP_ROOT/scripts/soak"
|
||||
cp -a "$runner_path" "$BACKUP_ROOT/scripts/soak/run_soak.sh"
|
||||
printf 'runner_was_present=1\n' > "$STATE_ROOT/state.env"
|
||||
else
|
||||
printf 'runner_was_present=0\n' > "$STATE_ROOT/state.env"
|
||||
fi
|
||||
|
||||
if [[ -d "$COMPONENT_ROOT/custom-fixtures" ]]; then
|
||||
cp -a "$COMPONENT_ROOT/custom-fixtures" "$BACKUP_ROOT/custom-fixtures"
|
||||
printf 'custom_fixtures_were_present=1\n' >> "$STATE_ROOT/state.env"
|
||||
else
|
||||
printf 'custom_fixtures_were_present=0\n' >> "$STATE_ROOT/state.env"
|
||||
fi
|
||||
|
||||
if [[ -d "$COMPONENT_ROOT/custom-tal-patch" ]]; then
|
||||
cp -a "$COMPONENT_ROOT/custom-tal-patch" "$BACKUP_ROOT/custom-tal-patch"
|
||||
printf 'patch_tools_were_present=1\n' >> "$STATE_ROOT/state.env"
|
||||
else
|
||||
printf 'patch_tools_were_present=0\n' >> "$STATE_ROOT/state.env"
|
||||
fi
|
||||
|
||||
if [[ -f "$COMPONENT_ROOT/custom-tal.env.example" ]]; then
|
||||
cp -a "$COMPONENT_ROOT/custom-tal.env.example" "$BACKUP_ROOT/custom-tal.env.example"
|
||||
printf 'custom_env_example_was_present=1\n' >> "$STATE_ROOT/state.env"
|
||||
else
|
||||
printf 'custom_env_example_was_present=0\n' >> "$STATE_ROOT/state.env"
|
||||
fi
|
||||
|
||||
if [[ "$ENABLE_CUSTOM" == "1" ]]; then
|
||||
[[ -f "$COMPONENT_ROOT/.env" ]] || die "missing component .env: $COMPONENT_ROOT/.env"
|
||||
cp -a "$COMPONENT_ROOT/.env" "$BACKUP_ROOT/.env"
|
||||
fi
|
||||
|
||||
copy_payload_file "$PAYLOAD_ROOT/compose/docker-compose.yml" "$COMPONENT_ROOT/compose/docker-compose.yml"
|
||||
append_custom_env_example "$COMPONENT_ROOT/.env.example"
|
||||
copy_payload_file "$PAYLOAD_ROOT/scripts/soak/run_soak.sh" "$runner_path"
|
||||
mkdir -p "$COMPONENT_ROOT/custom-fixtures/tal" \
|
||||
"$COMPONENT_ROOT/custom-fixtures/ta" \
|
||||
"$COMPONENT_ROOT/custom-fixtures/certs" \
|
||||
"$COMPONENT_ROOT/custom-fixtures/services"
|
||||
if [[ -d "$PAYLOAD_ROOT/custom-fixtures" ]]; then
|
||||
cp -a "$PAYLOAD_ROOT/custom-fixtures/." "$COMPONENT_ROOT/custom-fixtures/"
|
||||
fi
|
||||
mkdir -p "$COMPONENT_ROOT/custom-tal-patch/tools"
|
||||
cp -a "$PAYLOAD_ROOT/tools/." "$COMPONENT_ROOT/custom-tal-patch/tools/"
|
||||
copy_payload_file "$SCRIPT_DIR/custom-tal.env.example" \
|
||||
"$COMPONENT_ROOT/custom-tal.env.example"
|
||||
|
||||
if [[ "$ENABLE_CUSTOM" == "1" ]]; then
|
||||
set_env_value "$COMPONENT_ROOT/.env" RIRS custom
|
||||
set_env_value "$COMPONENT_ROOT/.env" TAL_INPUT_MODE custom-file-with-ta
|
||||
set_env_value "$COMPONENT_ROOT/.env" CUSTOM_FIXTURE_HOST_DIR ../custom-fixtures
|
||||
set_env_value "$COMPONENT_ROOT/.env" CUSTOM_TAL_PATH /opt/ours-rp/custom-fixtures/tal/custom.tal
|
||||
set_env_value "$COMPONENT_ROOT/.env" CUSTOM_TA_PATH /opt/ours-rp/custom-fixtures/ta/custom-ta.cer
|
||||
set_env_value "$COMPONENT_ROOT/.env" CUSTOM_TAL_URI https://host.docker.internal:18443/tal/custom.tal
|
||||
set_env_value "$COMPONENT_ROOT/.env" HTTP_ROOT_CERT_PATHS /opt/ours-rp/custom-fixtures/certs/rrdp-ca.pem
|
||||
printf 'env_was_enabled=1\n' >> "$STATE_ROOT/state.env"
|
||||
else
|
||||
printf 'env_was_enabled=0\n' >> "$STATE_ROOT/state.env"
|
||||
fi
|
||||
|
||||
[[ -f "$SCRIPT_DIR/PATCH-MANIFEST.env" ]] \
|
||||
|| die "missing PATCH-MANIFEST.env"
|
||||
cp "$SCRIPT_DIR/PATCH-MANIFEST.env" "$marker"
|
||||
printf 'patch_root=%s\n' "$PATCH_ROOT" >> "$STATE_ROOT/state.env"
|
||||
printf 'applied_at_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$STATE_ROOT/state.env"
|
||||
|
||||
echo "custom TAL patch applied to $COMPONENT_ROOT"
|
||||
if [[ "$ENABLE_CUSTOM" == "1" ]]; then
|
||||
echo "custom mode enabled in $COMPONENT_ROOT/.env"
|
||||
else
|
||||
echo "custom mode remains disabled; review custom-tal.env.example before enabling it"
|
||||
fi
|
||||
@ -0,0 +1,15 @@
|
||||
# Copy these values into the component .env after applying the patch.
|
||||
# The patch's --enable-custom option writes the same values automatically.
|
||||
RIRS=custom
|
||||
TAL_INPUT_MODE=custom-file-with-ta
|
||||
CUSTOM_FIXTURE_HOST_DIR=../custom-fixtures
|
||||
CUSTOM_TAL_PATH=/opt/ours-rp/custom-fixtures/tal/custom.tal
|
||||
CUSTOM_TA_PATH=/opt/ours-rp/custom-fixtures/ta/custom-ta.cer
|
||||
CUSTOM_TAL_URI=https://host.docker.internal:18443/tal/custom.tal
|
||||
HTTP_ROOT_CERT_PATHS=/opt/ours-rp/custom-fixtures/certs/rrdp-ca.pem
|
||||
|
||||
# Recommended for a finite acceptance run; keep the customer default for a
|
||||
# long-running test only after the fixture service is supervised.
|
||||
MAX_RUNS=1
|
||||
INTERVAL_SECS=0
|
||||
SOAK_RESTART_POLICY=no
|
||||
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
./rollback_custom_tal_patch.sh --component-root <installed-ours-rp-root>
|
||||
|
||||
Stop the ours RP service before rolling back the overlay.
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
COMPONENT_ROOT=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--component-root)
|
||||
COMPONENT_ROOT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$COMPONENT_ROOT" ]] || die "--component-root is required"
|
||||
COMPONENT_ROOT="$(cd "$COMPONENT_ROOT" 2>/dev/null && pwd)" \
|
||||
|| die "component root does not exist: $COMPONENT_ROOT"
|
||||
STATE_ROOT="$COMPONENT_ROOT/.custom-tal-patch-state"
|
||||
BACKUP_ROOT="$STATE_ROOT/original"
|
||||
[[ -d "$STATE_ROOT" ]] || die "patch state not found: $STATE_ROOT"
|
||||
[[ -f "$BACKUP_ROOT/compose/docker-compose.yml" ]] \
|
||||
|| die "patch backup is incomplete: $BACKUP_ROOT"
|
||||
|
||||
cp -a "$BACKUP_ROOT/compose/docker-compose.yml" "$COMPONENT_ROOT/compose/docker-compose.yml"
|
||||
cp -a "$BACKUP_ROOT/.env.example" "$COMPONENT_ROOT/.env.example"
|
||||
|
||||
if [[ -f "$BACKUP_ROOT/scripts/soak/run_soak.sh" ]]; then
|
||||
mkdir -p "$COMPONENT_ROOT/scripts/soak"
|
||||
cp -a "$BACKUP_ROOT/scripts/soak/run_soak.sh" "$COMPONENT_ROOT/scripts/soak/run_soak.sh"
|
||||
else
|
||||
rm -f "$COMPONENT_ROOT/scripts/soak/run_soak.sh"
|
||||
rmdir "$COMPONENT_ROOT/scripts/soak" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [[ -d "$BACKUP_ROOT/custom-fixtures" ]]; then
|
||||
rm -rf "$COMPONENT_ROOT/custom-fixtures"
|
||||
cp -a "$BACKUP_ROOT/custom-fixtures" "$COMPONENT_ROOT/custom-fixtures"
|
||||
else
|
||||
rm -rf "$COMPONENT_ROOT/custom-fixtures"
|
||||
fi
|
||||
|
||||
if [[ -d "$BACKUP_ROOT/custom-tal-patch" ]]; then
|
||||
rm -rf "$COMPONENT_ROOT/custom-tal-patch"
|
||||
cp -a "$BACKUP_ROOT/custom-tal-patch" "$COMPONENT_ROOT/custom-tal-patch"
|
||||
else
|
||||
rm -rf "$COMPONENT_ROOT/custom-tal-patch"
|
||||
fi
|
||||
|
||||
if [[ -f "$BACKUP_ROOT/custom-tal.env.example" ]]; then
|
||||
cp -a "$BACKUP_ROOT/custom-tal.env.example" "$COMPONENT_ROOT/custom-tal.env.example"
|
||||
else
|
||||
rm -f "$COMPONENT_ROOT/custom-tal.env.example"
|
||||
fi
|
||||
|
||||
if [[ -f "$BACKUP_ROOT/.env" ]]; then
|
||||
cp -a "$BACKUP_ROOT/.env" "$COMPONENT_ROOT/.env"
|
||||
fi
|
||||
rm -f "$COMPONENT_ROOT/CUSTOM-TAL-PATCH-MANIFEST.env"
|
||||
rm -rf "$STATE_ROOT"
|
||||
echo "custom TAL patch rolled back from $COMPONENT_ROOT"
|
||||
@ -0,0 +1,636 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a small self-contained RPKI repository for installer tests.
|
||||
|
||||
The generator uses Python cryptography and the OpenSSL CLI for CMS signing.
|
||||
It does not invoke Barry, Rapport, or a public RIR service.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
|
||||
from cryptography.x509 import AccessDescription, AuthorityInformationAccess, DistributionPoint, UniformResourceIdentifier
|
||||
from cryptography.x509.oid import AuthorityInformationAccessOID, NameOID, ObjectIdentifier
|
||||
|
||||
|
||||
RRDP_HOST = "host.docker.internal"
|
||||
RRDP_PORT = 18443
|
||||
RSYNC_PORT = 1873
|
||||
TAL_URI = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/ta.cer"
|
||||
TAL_HTTPS_URI = f"https://{RRDP_HOST}:{RRDP_PORT}/ta/custom-ta.cer"
|
||||
RRDP_URI = f"https://{RRDP_HOST}:{RRDP_PORT}/rrdp/notification.xml"
|
||||
ROOT_REPO_URI = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/root/"
|
||||
ROOT_MFT_URI = f"{ROOT_REPO_URI}root.mft"
|
||||
CHILD_REPO_URI = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/child/"
|
||||
CHILD_MFT_URI = f"{CHILD_REPO_URI}child.mft"
|
||||
OID_SIA = ObjectIdentifier("1.3.6.1.5.5.7.1.11")
|
||||
OID_RESOURCES_IP = ObjectIdentifier("1.3.6.1.5.5.7.1.7")
|
||||
OID_RESOURCES_AS = ObjectIdentifier("1.3.6.1.5.5.7.1.8")
|
||||
OID_POLICY_IP_AS = ObjectIdentifier("1.3.6.1.5.5.7.14.2")
|
||||
OID_ROA_ECONTENT = "1.2.840.113549.1.9.16.1.24"
|
||||
OID_MANIFEST_ECONTENT = "1.2.840.113549.1.9.16.1.26"
|
||||
OID_SHA256 = "2.16.840.1.101.3.4.2.1"
|
||||
SIA_CA_REPOSITORY = "1.3.6.1.5.5.7.48.5"
|
||||
SIA_RPKI_MANIFEST = "1.3.6.1.5.5.7.48.10"
|
||||
SIA_SIGNED_OBJECT = "1.3.6.1.5.5.7.48.11"
|
||||
SIA_RPKI_NOTIFY = "1.3.6.1.5.5.7.48.13"
|
||||
UTC = timezone.utc
|
||||
VALID_FROM = datetime(2026, 1, 1, tzinfo=UTC)
|
||||
VALID_TO = datetime(2027, 12, 31, 23, 59, 59, tzinfo=UTC)
|
||||
CHILD_PREFIX = ipaddress.ip_network("203.0.113.0/24")
|
||||
SECOND_PREFIX = ipaddress.ip_network("203.0.113.128/25")
|
||||
OUTSIDE_PREFIX = ipaddress.ip_network("203.0.114.0/24")
|
||||
TEST_ASN = 64496
|
||||
SESSION_ID = "11111111-2222-4333-8444-555555555555"
|
||||
|
||||
|
||||
def tlv(tag: int, value: bytes) -> bytes:
|
||||
length = len(value)
|
||||
if length < 128:
|
||||
encoded_length = bytes([length])
|
||||
else:
|
||||
raw = length.to_bytes((length.bit_length() + 7) // 8, "big")
|
||||
encoded_length = bytes([0x80 | len(raw)]) + raw
|
||||
return bytes([tag]) + encoded_length + value
|
||||
|
||||
|
||||
def seq(*values: bytes) -> bytes:
|
||||
return tlv(0x30, b"".join(values))
|
||||
|
||||
|
||||
def integer(value: int) -> bytes:
|
||||
raw = value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big")
|
||||
if raw[0] & 0x80:
|
||||
raw = b"\x00" + raw
|
||||
return tlv(0x02, raw)
|
||||
|
||||
|
||||
def oid(oid_value: str) -> bytes:
|
||||
parts = [int(part) for part in oid_value.split(".")]
|
||||
encoded = bytearray([40 * parts[0] + parts[1]])
|
||||
for part in parts[2:]:
|
||||
chunks = [part & 0x7F]
|
||||
part >>= 7
|
||||
while part:
|
||||
chunks.append(0x80 | (part & 0x7F))
|
||||
part >>= 7
|
||||
encoded.extend(reversed(chunks))
|
||||
return tlv(0x06, bytes(encoded))
|
||||
|
||||
|
||||
def octet(value: bytes) -> bytes:
|
||||
return tlv(0x04, value)
|
||||
|
||||
|
||||
def uri(value: str) -> bytes:
|
||||
return tlv(0x86, value.encode("ascii"))
|
||||
|
||||
|
||||
def bit_string(value: bytes, unused: int = 0) -> bytes:
|
||||
return tlv(0x03, bytes([unused]) + value)
|
||||
|
||||
|
||||
def generalized_time(value: datetime) -> bytes:
|
||||
return tlv(0x18, value.astimezone(UTC).strftime("%Y%m%d%H%M%SZ").encode("ascii"))
|
||||
|
||||
|
||||
def context_zero(value: bytes) -> bytes:
|
||||
return tlv(0xA0, value)
|
||||
|
||||
|
||||
def ip_choice(networks: list[ipaddress._BaseNetwork]) -> bytes:
|
||||
entries = []
|
||||
for network in networks:
|
||||
length = (network.prefixlen + 7) // 8
|
||||
raw = network.network_address.packed[:length]
|
||||
entries.append(bit_string(raw, length * 8 - network.prefixlen))
|
||||
return seq(*entries)
|
||||
|
||||
|
||||
def ip_resources(networks: list[ipaddress._BaseNetwork]) -> bytes:
|
||||
families: dict[int, list[ipaddress._BaseNetwork]] = {1: [], 2: []}
|
||||
for network in networks:
|
||||
families[1 if network.version == 4 else 2].append(network)
|
||||
return seq(
|
||||
*(
|
||||
seq(octet(afi.to_bytes(2, "big")), ip_choice(items))
|
||||
for afi, items in families.items()
|
||||
if items
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def ip_inherit() -> bytes:
|
||||
return seq(
|
||||
seq(octet(b"\x00\x01"), tlv(0x05, b"")),
|
||||
seq(octet(b"\x00\x02"), tlv(0x05, b"")),
|
||||
)
|
||||
|
||||
|
||||
def as_resources(asns: list[int] | None) -> bytes:
|
||||
if asns is None:
|
||||
return seq(context_zero(tlv(0x05, b"")))
|
||||
return seq(context_zero(seq(*(integer(asn) for asn in asns))))
|
||||
|
||||
|
||||
def sia(entries: list[tuple[str, str]]) -> bytes:
|
||||
return seq(*(seq(oid(method), uri(location)) for method, location in entries))
|
||||
|
||||
|
||||
def write(path: Path, data: bytes, mode: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
if mode is not None:
|
||||
path.chmod(mode)
|
||||
|
||||
|
||||
def pem_cert(cert: x509.Certificate) -> bytes:
|
||||
return cert.public_bytes(Encoding.PEM)
|
||||
|
||||
|
||||
def pem_key(key: rsa.RSAPrivateKey) -> bytes:
|
||||
return key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption())
|
||||
|
||||
|
||||
def write_key(path: Path, key: rsa.RSAPrivateKey) -> None:
|
||||
write(path, pem_key(key), 0o600)
|
||||
|
||||
|
||||
def sign_cert(
|
||||
subject: x509.Name,
|
||||
issuer: x509.Name,
|
||||
public_key,
|
||||
signing_key: rsa.RSAPrivateKey,
|
||||
serial: int,
|
||||
is_ca: bool,
|
||||
ski_key: rsa.RSAPrivateKey,
|
||||
aki_key: rsa.RSAPrivateKey | None = None,
|
||||
ip_ext: bytes | None = None,
|
||||
as_ext: bytes | None = None,
|
||||
ca_repository: str | None = None,
|
||||
manifest_uri: str | None = None,
|
||||
notify_uri: str | None = None,
|
||||
signed_object_uri: str | None = None,
|
||||
crl_uri: str | None = None,
|
||||
ca_issuer_uri: str | None = None,
|
||||
not_before: datetime = VALID_FROM,
|
||||
not_after: datetime = VALID_TO,
|
||||
) -> x509.Certificate:
|
||||
builder = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(public_key)
|
||||
.serial_number(serial)
|
||||
.not_valid_before(not_before)
|
||||
.not_valid_after(not_after)
|
||||
)
|
||||
if is_ca:
|
||||
builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
||||
if is_ca:
|
||||
usage = x509.KeyUsage(False, False, False, False, False, True, True, False, False)
|
||||
else:
|
||||
usage = x509.KeyUsage(True, False, False, False, False, False, False, False, False)
|
||||
builder = builder.add_extension(usage, critical=True)
|
||||
builder = builder.add_extension(x509.SubjectKeyIdentifier.from_public_key(ski_key.public_key()), critical=False)
|
||||
if aki_key is not None:
|
||||
builder = builder.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(aki_key.public_key()), critical=False)
|
||||
builder = builder.add_extension(
|
||||
x509.CertificatePolicies([x509.PolicyInformation(OID_POLICY_IP_AS, None)]),
|
||||
critical=True,
|
||||
)
|
||||
if crl_uri is not None:
|
||||
builder = builder.add_extension(
|
||||
x509.CRLDistributionPoints(
|
||||
[DistributionPoint([UniformResourceIdentifier(crl_uri)], None, None, None)]
|
||||
),
|
||||
critical=False,
|
||||
)
|
||||
if ca_issuer_uri is not None:
|
||||
builder = builder.add_extension(
|
||||
AuthorityInformationAccess(
|
||||
[AccessDescription(AuthorityInformationAccessOID.CA_ISSUERS, UniformResourceIdentifier(ca_issuer_uri))]
|
||||
),
|
||||
critical=False,
|
||||
)
|
||||
sia_entries: list[tuple[str, str]] = []
|
||||
if ca_repository is not None:
|
||||
sia_entries.append((SIA_CA_REPOSITORY, ca_repository))
|
||||
if manifest_uri is not None:
|
||||
sia_entries.append((SIA_RPKI_MANIFEST, manifest_uri))
|
||||
if notify_uri is not None:
|
||||
sia_entries.append((SIA_RPKI_NOTIFY, notify_uri))
|
||||
if signed_object_uri is not None:
|
||||
sia_entries.append((SIA_SIGNED_OBJECT, signed_object_uri))
|
||||
if sia_entries:
|
||||
builder = builder.add_extension(x509.UnrecognizedExtension(OID_SIA, sia(sia_entries)), critical=False)
|
||||
if ip_ext is not None:
|
||||
builder = builder.add_extension(x509.UnrecognizedExtension(OID_RESOURCES_IP, ip_ext), critical=True)
|
||||
if as_ext is not None:
|
||||
builder = builder.add_extension(x509.UnrecognizedExtension(OID_RESOURCES_AS, as_ext), critical=True)
|
||||
return builder.sign(signing_key, hashes.SHA256())
|
||||
|
||||
|
||||
def make_crl(subject: x509.Name, key: rsa.RSAPrivateKey, number: int) -> x509.CertificateRevocationList:
|
||||
return (
|
||||
x509.CertificateRevocationListBuilder()
|
||||
.issuer_name(subject)
|
||||
.last_update(VALID_FROM + timedelta(days=30))
|
||||
.next_update(VALID_TO - timedelta(days=30))
|
||||
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()), critical=False)
|
||||
.add_extension(x509.CRLNumber(number), critical=False)
|
||||
.sign(key, hashes.SHA256())
|
||||
)
|
||||
|
||||
|
||||
def roa_content(prefix: ipaddress._BaseNetwork, max_length: int) -> bytes:
|
||||
length = (prefix.prefixlen + 7) // 8
|
||||
raw = prefix.network_address.packed[:length]
|
||||
address = seq(bit_string(raw, length * 8 - prefix.prefixlen), integer(max_length))
|
||||
family = seq(octet((1 if prefix.version == 4 else 2).to_bytes(2, "big")), seq(address))
|
||||
return seq(integer(TEST_ASN), seq(family))
|
||||
|
||||
|
||||
def manifest_content(number: int, files: list[tuple[str, bytes]]) -> bytes:
|
||||
entries = [
|
||||
seq(tlv(0x16, name.encode("ascii")), bit_string(hashlib.sha256(content).digest()))
|
||||
for name, content in files
|
||||
]
|
||||
return seq(
|
||||
integer(number),
|
||||
generalized_time(VALID_FROM + timedelta(days=30)),
|
||||
generalized_time(VALID_TO - timedelta(days=30)),
|
||||
oid(OID_SHA256),
|
||||
seq(*entries),
|
||||
)
|
||||
|
||||
|
||||
def write_cms(content: bytes, content_type: str, signer_cert: x509.Certificate, signer_key: rsa.RSAPrivateKey, output: Path, work: Path) -> None:
|
||||
content_path = work / f"{output.name}.content.der"
|
||||
signer_path = work / f"{output.name}.signer.pem"
|
||||
key_path = work / f"{output.name}.key.pem"
|
||||
write(content_path, content)
|
||||
write(signer_path, pem_cert(signer_cert))
|
||||
write_key(key_path, signer_key)
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl", "cms", "-sign", "-binary", "-in", str(content_path),
|
||||
"-signer", str(signer_path), "-inkey", str(key_path),
|
||||
"-outform", "DER", "-nodetach", "-nosmimecap", "-keyid", "-md", "sha256",
|
||||
"-econtent_type", content_type, "-out", str(output),
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
|
||||
def make_roa(
|
||||
name: str,
|
||||
prefix,
|
||||
max_length: int,
|
||||
not_before: datetime,
|
||||
not_after: datetime,
|
||||
child_name,
|
||||
child_key,
|
||||
common: Path,
|
||||
work: Path,
|
||||
certificate_prefix=None,
|
||||
) -> None:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
cert = sign_cert(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, f"#142 EE {name}")]),
|
||||
child_name,
|
||||
key.public_key(),
|
||||
child_key,
|
||||
x509.random_serial_number(),
|
||||
False,
|
||||
key,
|
||||
child_key,
|
||||
ip_ext=ip_resources([certificate_prefix or prefix]),
|
||||
signed_object_uri=f"{CHILD_REPO_URI}{name}",
|
||||
crl_uri=f"{CHILD_REPO_URI}child.crl",
|
||||
ca_issuer_uri=f"{CHILD_REPO_URI}child.cer",
|
||||
not_before=not_before,
|
||||
not_after=not_after,
|
||||
)
|
||||
write_cms(roa_content(prefix, max_length), OID_ROA_ECONTENT, cert, key, common / name, work)
|
||||
|
||||
|
||||
def make_manifest(name: str, number: int, files: list[tuple[str, bytes]], child_name, child_key, common: Path, work: Path) -> bytes:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
cert = sign_cert(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, f"#142 Manifest EE {name}")]),
|
||||
child_name,
|
||||
key.public_key(),
|
||||
child_key,
|
||||
x509.random_serial_number(),
|
||||
False,
|
||||
key,
|
||||
child_key,
|
||||
ip_ext=ip_inherit(),
|
||||
as_ext=as_resources(None),
|
||||
signed_object_uri=CHILD_MFT_URI if name != "root" else ROOT_MFT_URI,
|
||||
crl_uri=f"{CHILD_REPO_URI}child.crl" if name != "root" else f"{ROOT_REPO_URI}root.crl",
|
||||
ca_issuer_uri=f"{CHILD_REPO_URI}child.cer" if name != "root" else f"{ROOT_REPO_URI}ta.cer",
|
||||
)
|
||||
output = common / f"{name}.mft"
|
||||
write_cms(manifest_content(number, files), OID_MANIFEST_ECONTENT, cert, key, output, work)
|
||||
return output.read_bytes()
|
||||
|
||||
|
||||
def publish(uri: str, path: Path) -> str:
|
||||
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
|
||||
return f' <publish uri="{uri}">{encoded}</publish>\n'
|
||||
|
||||
|
||||
def snapshot(case_root: Path, names: list[str], serial: int, extra_root_names: list[str] | None = None) -> bytes:
|
||||
repository = case_root / "repository"
|
||||
objects = [
|
||||
(f"{ROOT_REPO_URI}root.crl", repository / "root/root.crl"),
|
||||
(f"{ROOT_REPO_URI}child.cer", repository / "root/child.cer"),
|
||||
*[
|
||||
(f"{ROOT_REPO_URI}{name}", repository / f"root/{name}")
|
||||
for name in extra_root_names or []
|
||||
],
|
||||
(f"{CHILD_REPO_URI}child.crl", repository / "child/child.crl"),
|
||||
*[(f"{CHILD_REPO_URI}{name}", repository / f"child/{name}") for name in names],
|
||||
(ROOT_MFT_URI, repository / "root/root.mft"),
|
||||
(CHILD_MFT_URI, repository / "child/child.mft"),
|
||||
]
|
||||
body = "".join(publish(uri, path) for uri, path in objects)
|
||||
return (
|
||||
f'<snapshot xmlns="http://www.ripe.net/rpki/rrdp" session_id="{SESSION_ID}" serial="{serial}">\n'
|
||||
f"{body}</snapshot>\n"
|
||||
).encode()
|
||||
|
||||
|
||||
def notification(serial: int, snap: bytes, delta: bytes | None) -> bytes:
|
||||
lines = [
|
||||
f'<notification xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{SESSION_ID}" serial="{serial}">',
|
||||
f' <snapshot uri="https://{RRDP_HOST}:{RRDP_PORT}/rrdp/snapshot.xml" hash="{hashlib.sha256(snap).hexdigest()}"/>',
|
||||
]
|
||||
if delta is not None:
|
||||
lines.append(
|
||||
f' <delta serial="{serial}" uri="https://{RRDP_HOST}:{RRDP_PORT}/rrdp/delta-{serial}.xml" hash="{hashlib.sha256(delta).hexdigest()}"/>'
|
||||
)
|
||||
return ("\n".join(lines) + "\n</notification>\n").encode()
|
||||
|
||||
|
||||
def corrupt_first_hash(xml: bytes) -> bytes:
|
||||
marker = b'hash="'
|
||||
start = xml.index(marker) + len(marker)
|
||||
end = xml.index(b'"', start)
|
||||
value = bytearray(xml[start:end])
|
||||
value[0] = ord("0") if value[0] != ord("0") else ord("1")
|
||||
return xml[:start] + bytes(value) + xml[end:]
|
||||
|
||||
|
||||
def delta(add_name: str, add_path: Path, new_mft: Path) -> bytes:
|
||||
body = publish(CHILD_MFT_URI, new_mft)
|
||||
body += publish(f"{CHILD_REPO_URI}{add_name}", add_path)
|
||||
return f'<delta xmlns="http://www.ripe.net/rpki/rrdp" session_id="{SESSION_ID}" serial="2">\n{body}</delta>\n'.encode()
|
||||
|
||||
|
||||
def build_case(
|
||||
output: Path,
|
||||
common: Path,
|
||||
tal: bytes,
|
||||
ta: bytes,
|
||||
name: str,
|
||||
number: int,
|
||||
names: list[str],
|
||||
child_mft: bytes,
|
||||
change: bytes | None = None,
|
||||
root_mft: bytes | None = None,
|
||||
extra_root_objects: list[tuple[str, bytes]] | None = None,
|
||||
notification_hash_mismatch: bool = False,
|
||||
) -> None:
|
||||
case_root = output / "cases" / name
|
||||
repo = case_root / "repository"
|
||||
(repo / "root").mkdir(parents=True, exist_ok=True)
|
||||
(repo / "child").mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(common / "ta.cer", repo / "ta.cer")
|
||||
for item in ("root.crl", "child.cer"):
|
||||
shutil.copy2(common / item, repo / "root" / item)
|
||||
(repo / "root/root.mft").write_bytes(root_mft if root_mft is not None else (common / "root.mft").read_bytes())
|
||||
for item, content in extra_root_objects or []:
|
||||
(repo / "root" / item).write_bytes(content)
|
||||
for item in (
|
||||
"child.crl", "valid.roa", "valid2.roa", "expired.roa", "max-length.roa",
|
||||
"max-length-invalid.roa", "roa-prefix-outside.roa", "out-of-resource.roa", "bad-format.roa",
|
||||
):
|
||||
if (common / item).exists():
|
||||
shutil.copy2(common / item, repo / "child" / item)
|
||||
(repo / "child/child.mft").write_bytes(child_mft)
|
||||
snap = snapshot(case_root, names, number, [item for item, _ in extra_root_objects or []])
|
||||
(case_root / "http/rrdp").mkdir(parents=True, exist_ok=True)
|
||||
(case_root / "http/tal").mkdir(parents=True, exist_ok=True)
|
||||
(case_root / "http/ta").mkdir(parents=True, exist_ok=True)
|
||||
(case_root / "http/rrdp/snapshot.xml").write_bytes(snap)
|
||||
notification_xml = notification(number, snap, change)
|
||||
if notification_hash_mismatch:
|
||||
notification_xml = corrupt_first_hash(notification_xml)
|
||||
(case_root / "http/rrdp/notification.xml").write_bytes(notification_xml)
|
||||
if change is not None:
|
||||
(case_root / "http/rrdp/delta-2.xml").write_bytes(change)
|
||||
(case_root / "http/tal/custom.tal").write_bytes(tal)
|
||||
(case_root / "http/ta/custom-ta.cer").write_bytes(ta)
|
||||
(case_root / "CASE-MANIFEST.txt").write_text(
|
||||
f"case={name}\nserial={number}\nobjects={','.join(names)}\nsnapshot_sha256={hashlib.sha256(snap).hexdigest()}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def make_https_certificates(output: Path) -> None:
|
||||
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "#142 fixed RRDP test CA")])
|
||||
ca_cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(ca_name).issuer_name(ca_name).public_key(ca_key.public_key())
|
||||
.serial_number(x509.random_serial_number()).not_valid_before(VALID_FROM).not_valid_after(VALID_TO)
|
||||
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
|
||||
.add_extension(x509.KeyUsage(False, False, False, False, False, True, True, False, False), critical=True)
|
||||
.sign(ca_key, hashes.SHA256())
|
||||
)
|
||||
server_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
server_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, RRDP_HOST)])
|
||||
try:
|
||||
server_host = x509.IPAddress(ipaddress.ip_address(RRDP_HOST))
|
||||
except ValueError:
|
||||
server_host = x509.DNSName(RRDP_HOST)
|
||||
san_entries = [server_host]
|
||||
if RRDP_HOST != "localhost":
|
||||
san_entries.append(x509.DNSName("localhost"))
|
||||
server_cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(server_name).issuer_name(ca_cert.subject).public_key(server_key.public_key())
|
||||
.serial_number(x509.random_serial_number()).not_valid_before(VALID_FROM).not_valid_after(VALID_TO)
|
||||
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
||||
.add_extension(x509.KeyUsage(True, False, False, False, False, False, False, False, False), critical=True)
|
||||
.add_extension(x509.SubjectAlternativeName(san_entries), critical=False)
|
||||
.sign(ca_key, hashes.SHA256())
|
||||
)
|
||||
write(output / "certs/rrdp-ca.pem", pem_cert(ca_cert))
|
||||
write_key(output / "certs/rrdp-ca.key", ca_key)
|
||||
write(output / "certs/rrdp-server.pem", pem_cert(server_cert))
|
||||
write_key(output / "certs/rrdp-server.key", server_key)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global RRDP_HOST, TAL_URI, TAL_HTTPS_URI, RRDP_URI
|
||||
global ROOT_REPO_URI, ROOT_MFT_URI, CHILD_REPO_URI, CHILD_MFT_URI
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=RRDP_HOST,
|
||||
help="host embedded in TAL, certificate SIA and RRDP data (default: host.docker.internal)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
RRDP_HOST = args.host
|
||||
TAL_URI = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/ta.cer"
|
||||
TAL_HTTPS_URI = f"https://{RRDP_HOST}:{RRDP_PORT}/ta/custom-ta.cer"
|
||||
RRDP_URI = f"https://{RRDP_HOST}:{RRDP_PORT}/rrdp/notification.xml"
|
||||
ROOT_REPO_URI = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/root/"
|
||||
ROOT_MFT_URI = f"{ROOT_REPO_URI}root.mft"
|
||||
CHILD_REPO_URI = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/child/"
|
||||
CHILD_MFT_URI = f"{CHILD_REPO_URI}child.mft"
|
||||
output = args.output.resolve()
|
||||
if output.exists():
|
||||
shutil.rmtree(output)
|
||||
for directory in ("tal", "ta", "certs", "services", "keys"):
|
||||
(output / directory).mkdir(parents=True)
|
||||
|
||||
root_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
child_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
root_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "#142 Custom RPKI Root")])
|
||||
child_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "#142 Custom RPKI Child")])
|
||||
root_cert = sign_cert(
|
||||
root_name, root_name, root_key.public_key(), root_key, 0x14200001, True, root_key,
|
||||
ip_ext=ip_resources([CHILD_PREFIX]),
|
||||
as_ext=as_resources([TEST_ASN]), ca_repository=ROOT_REPO_URI, manifest_uri=ROOT_MFT_URI,
|
||||
notify_uri=RRDP_URI,
|
||||
)
|
||||
child_cert = sign_cert(
|
||||
child_name, root_name, child_key.public_key(), root_key, 0x14200002, True, child_key, root_key,
|
||||
ip_ext=ip_resources([CHILD_PREFIX]), as_ext=as_resources([TEST_ASN]),
|
||||
ca_repository=CHILD_REPO_URI, manifest_uri=CHILD_MFT_URI, notify_uri=RRDP_URI,
|
||||
crl_uri=f"{ROOT_REPO_URI}root.crl", ca_issuer_uri=f"{ROOT_REPO_URI}child.cer",
|
||||
)
|
||||
bad_child_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "#142 Custom RPKI Overclaiming Child")])
|
||||
bad_child_repo_uri = f"rsync://{RRDP_HOST}:{RSYNC_PORT}/custom/bad-child/"
|
||||
bad_child_cert = sign_cert(
|
||||
bad_child_name, root_name, child_key.public_key(), root_key, 0x14200003, True, child_key, root_key,
|
||||
ip_ext=ip_resources([OUTSIDE_PREFIX]), as_ext=as_resources([TEST_ASN]),
|
||||
ca_repository=bad_child_repo_uri, manifest_uri=f"{bad_child_repo_uri}bad-child.mft", notify_uri=RRDP_URI,
|
||||
crl_uri=f"{ROOT_REPO_URI}root.crl", ca_issuer_uri=f"{ROOT_REPO_URI}child.cer",
|
||||
)
|
||||
root_crl = make_crl(root_name, root_key, 1)
|
||||
child_crl = make_crl(child_name, child_key, 1)
|
||||
tal = (
|
||||
f"{TAL_URI}\n{TAL_HTTPS_URI}\n\n"
|
||||
+ base64.b64encode(root_key.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)).decode()
|
||||
+ "\n"
|
||||
).encode()
|
||||
write(output / "tal/custom.tal", tal)
|
||||
write(output / "ta/custom-ta.cer", root_cert.public_bytes(Encoding.DER))
|
||||
with tempfile.TemporaryDirectory(prefix="custom-rpki-cms-") as temp:
|
||||
work = Path(temp)
|
||||
common = output / ".common"
|
||||
common.mkdir()
|
||||
write(common / "ta.cer", root_cert.public_bytes(Encoding.DER))
|
||||
write(common / "child.cer", child_cert.public_bytes(Encoding.DER))
|
||||
write(common / "bad-child.cer", bad_child_cert.public_bytes(Encoding.DER))
|
||||
write(common / "root.crl", root_crl.public_bytes(Encoding.DER))
|
||||
write(common / "child.crl", child_crl.public_bytes(Encoding.DER))
|
||||
for name, prefix, max_length, before, after, certificate_prefix in (
|
||||
("valid.roa", CHILD_PREFIX, 24, VALID_FROM + timedelta(days=31), VALID_TO - timedelta(days=31), None),
|
||||
("valid2.roa", SECOND_PREFIX, 25, VALID_FROM + timedelta(days=31), VALID_TO - timedelta(days=31), None),
|
||||
("expired.roa", CHILD_PREFIX, 24, VALID_FROM - timedelta(days=30), VALID_FROM - timedelta(days=1), None),
|
||||
("max-length.roa", CHILD_PREFIX, 25, VALID_FROM + timedelta(days=31), VALID_TO - timedelta(days=31), None),
|
||||
("max-length-invalid.roa", CHILD_PREFIX, 33, VALID_FROM + timedelta(days=31), VALID_TO - timedelta(days=31), None),
|
||||
("roa-prefix-outside.roa", OUTSIDE_PREFIX, 24, VALID_FROM + timedelta(days=31), VALID_TO - timedelta(days=31), CHILD_PREFIX),
|
||||
("out-of-resource.roa", OUTSIDE_PREFIX, 24, VALID_FROM + timedelta(days=31), VALID_TO - timedelta(days=31), None),
|
||||
):
|
||||
make_roa(
|
||||
name, prefix, max_length, before, after, child_name, child_key, common, work,
|
||||
certificate_prefix=certificate_prefix,
|
||||
)
|
||||
bad = bytearray((common / "valid.roa").read_bytes())
|
||||
bad[-1] ^= 1
|
||||
write(common / "bad-format.roa", bytes(bad))
|
||||
|
||||
root_mft = make_manifest(
|
||||
"root", 1, [
|
||||
("root.crl", (common / "root.crl").read_bytes()),
|
||||
("child.cer", (common / "child.cer").read_bytes()),
|
||||
], root_name, root_key, common, work,
|
||||
)
|
||||
write(common / "root.mft", root_mft)
|
||||
bad_root_mft = make_manifest(
|
||||
"root", 1, [
|
||||
("root.crl", (common / "root.crl").read_bytes()),
|
||||
("child.cer", (common / "child.cer").read_bytes()),
|
||||
("bad-child.cer", (common / "bad-child.cer").read_bytes()),
|
||||
], root_name, root_key, common, work,
|
||||
)
|
||||
write(common / "root.mft", root_mft)
|
||||
case_specs = (
|
||||
("baseline-v1", 1, ["child.crl", "valid.roa"]),
|
||||
("sync-hash-mismatch", 1, ["child.crl", "valid.roa"]),
|
||||
("baseline-v2", 2, ["child.crl", "valid.roa", "valid2.roa"]),
|
||||
("validation-expired", 1, ["child.crl", "expired.roa"]),
|
||||
("validation-max-length", 1, ["child.crl", "max-length.roa"]),
|
||||
("validation-max-length-invalid", 1, ["child.crl", "max-length-invalid.roa"]),
|
||||
("validation-roa-prefix-outside", 1, ["child.crl", "roa-prefix-outside.roa"]),
|
||||
("validation-out-of-resource", 1, ["child.crl", "out-of-resource.roa"]),
|
||||
("validation-ca-over-resource", 1, ["child.crl", "valid.roa"]),
|
||||
("validation-nonstandard", 1, ["child.crl", "bad-format.roa"]),
|
||||
)
|
||||
child_mfts = {}
|
||||
for name, number, names in case_specs:
|
||||
child_mfts[name] = make_manifest(
|
||||
name, number, [(item, (common / item).read_bytes()) for item in names],
|
||||
child_name, child_key, common, work,
|
||||
)
|
||||
for name, number, names in case_specs:
|
||||
change = None
|
||||
if name == "baseline-v2":
|
||||
change = delta("valid2.roa", common / "valid2.roa", common / "baseline-v2.mft")
|
||||
build_case(
|
||||
output, common, tal, root_cert.public_bytes(Encoding.DER), name, number, names,
|
||||
child_mfts[name], change,
|
||||
root_mft=bad_root_mft if name == "validation-ca-over-resource" else None,
|
||||
extra_root_objects=[("bad-child.cer", (common / "bad-child.cer").read_bytes())]
|
||||
if name == "validation-ca-over-resource" else None,
|
||||
notification_hash_mismatch=name == "sync-hash-mismatch",
|
||||
)
|
||||
write_key(output / "keys/root.key", root_key)
|
||||
write_key(output / "keys/child.key", child_key)
|
||||
make_https_certificates(output)
|
||||
shutil.rmtree(output / ".common")
|
||||
(output / "FIXTURE-MANIFEST.txt").write_text(
|
||||
"fixture_schema_version=1\nfixture_kind=custom-tal-fixed-local-rpki\n"
|
||||
f"tal_uri={TAL_URI}\nrrdp_uri={RRDP_URI}\nrsync_module=custom\n"
|
||||
f"rrdp_port={RRDP_PORT}\nrsync_port={RSYNC_PORT}\n"
|
||||
"cases=baseline-v1,sync-hash-mismatch,baseline-v2,validation-expired,validation-max-length,validation-max-length-invalid,"
|
||||
"validation-roa-prefix-outside,validation-out-of-resource,validation-ca-over-resource,validation-nonstandard\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"generated custom fixture: {output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Serve a fixture directory over HTTPS with request logging."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import http.server
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format: str, *args) -> None:
|
||||
print("%s %s" % (self.log_date_time_string(), format % args), flush=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--directory", type=Path, required=True)
|
||||
parser.add_argument("--certfile", type=Path, required=True)
|
||||
parser.add_argument("--keyfile", type=Path, required=True)
|
||||
parser.add_argument("--host", default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=18443)
|
||||
args = parser.parse_args()
|
||||
handler = lambda *handler_args, directory=str(args.directory): RequestHandler(
|
||||
*handler_args, directory=directory
|
||||
)
|
||||
server = http.server.ThreadingHTTPServer((args.host, args.port), handler)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=args.certfile, keyfile=args.keyfile)
|
||||
server.socket = context.wrap_socket(server.socket, server_side=True)
|
||||
print(f"https fixture server listening on {args.host}:{args.port} root={args.directory}", flush=True)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
|
||||
FIXTURE_ROOT=""
|
||||
CASE_NAME="baseline-v1"
|
||||
RRDP_PORT="$(printenv RRDP_PORT 2>/dev/null || true)"
|
||||
RSYNC_PORT="$(printenv RSYNC_PORT 2>/dev/null || true)"
|
||||
PID_ROOT=""
|
||||
|
||||
[[ -n "$RRDP_PORT" ]] || RRDP_PORT=18443
|
||||
[[ -n "$RSYNC_PORT" ]] || RSYNC_PORT=1873
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
./start_fixed_services.sh --fixture-root <generated-fixtures> [--case NAME]
|
||||
|
||||
Cases: baseline-v1, sync-hash-mismatch, baseline-v2, validation-expired,
|
||||
validation-max-length, validation-max-length-invalid,
|
||||
validation-roa-prefix-outside, validation-out-of-resource,
|
||||
validation-ca-over-resource, validation-nonstandard
|
||||
Environment: RRDP_PORT (default 18443), RSYNC_PORT (default 1873)
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--fixture-root) FIXTURE_ROOT="$2"; shift 2 ;;
|
||||
--case) CASE_NAME="$2"; shift 2 ;;
|
||||
--pid-root) PID_ROOT="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$FIXTURE_ROOT" ]] || die "--fixture-root is required"
|
||||
FIXTURE_ROOT="$(cd "$FIXTURE_ROOT" 2>/dev/null && pwd)" || die "fixture root not found"
|
||||
CASE_ROOT="$FIXTURE_ROOT/cases/$CASE_NAME"
|
||||
[[ -d "$CASE_ROOT/repository" && -d "$CASE_ROOT/http" ]] || die "fixture case not found: $CASE_NAME"
|
||||
if [[ -z "$PID_ROOT" ]]; then
|
||||
PID_ROOT="$FIXTURE_ROOT/services/$CASE_NAME"
|
||||
fi
|
||||
mkdir -p "$PID_ROOT"
|
||||
[[ ! -f "$PID_ROOT/pids.env" ]] || die "services already appear to be running: $PID_ROOT/pids.env"
|
||||
rm -f "$PID_ROOT/rsyncd.pid" "$PID_ROOT/rsyncd.lock"
|
||||
|
||||
RSYNCD_CONF="$PID_ROOT/rsyncd.conf"
|
||||
RSYNC_UID="$(id -un)"
|
||||
RSYNC_GID="$(id -gn)"
|
||||
cat > "$RSYNCD_CONF" <<EOF
|
||||
uid = $RSYNC_UID
|
||||
gid = $RSYNC_GID
|
||||
use chroot = no
|
||||
read only = yes
|
||||
port = $RSYNC_PORT
|
||||
pid file = $PID_ROOT/rsyncd.pid
|
||||
lock file = $PID_ROOT/rsyncd.lock
|
||||
log file = $PID_ROOT/rsyncd.log
|
||||
|
||||
[custom]
|
||||
path = $CASE_ROOT/repository
|
||||
comment = #142 fixed custom RPKI repository
|
||||
read only = yes
|
||||
EOF
|
||||
|
||||
rsync --daemon --no-detach --config="$RSYNCD_CONF" >"$PID_ROOT/rsyncd.stdout.log" 2>&1 &
|
||||
RSYNC_PID=$!
|
||||
python3 "$SCRIPT_DIR/serve_https.py" \
|
||||
--directory "$CASE_ROOT/http" \
|
||||
--certfile "$FIXTURE_ROOT/certs/rrdp-server.pem" \
|
||||
--keyfile "$FIXTURE_ROOT/certs/rrdp-server.key" \
|
||||
--port "$RRDP_PORT" \
|
||||
>"$PID_ROOT/https.log" 2>&1 &
|
||||
HTTPS_PID=$!
|
||||
|
||||
printf 'case=%s\nrrdp_pid=%s\nrsync_pid=%s\nrrdp_port=%s\nrsync_port=%s\n' \
|
||||
"$CASE_NAME" "$HTTPS_PID" "$RSYNC_PID" "$RRDP_PORT" "$RSYNC_PORT" > "$PID_ROOT/pids.env"
|
||||
sleep 1
|
||||
kill -0 "$HTTPS_PID" 2>/dev/null || die "HTTPS fixture server exited; see $PID_ROOT/https.log"
|
||||
kill -0 "$RSYNC_PID" 2>/dev/null || die "rsync fixture server exited; see $PID_ROOT/rsyncd.stdout.log"
|
||||
echo "fixed RRDP/rsync services started case=$CASE_NAME rrdp=$RRDP_PORT rsync=$RSYNC_PORT pid_root=$PID_ROOT"
|
||||
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PID_ROOT="$1"
|
||||
[[ -n "$PID_ROOT" ]] || { echo "Usage: $0 <pid-root>" >&2; exit 2; }
|
||||
[[ -f "$PID_ROOT/pids.env" ]] || { echo "services are not running: $PID_ROOT" >&2; exit 2; }
|
||||
# shellcheck disable=SC1090
|
||||
source "$PID_ROOT/pids.env"
|
||||
service_pids=("${rrdp_pid:-}" "${rsync_pid:-}")
|
||||
for pid in "${service_pids[@]}"; do
|
||||
if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
for pid in "${service_pids[@]}"; do
|
||||
[[ "$pid" =~ ^[0-9]+$ ]] || continue
|
||||
for _ in $(seq 1 50); do
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
done
|
||||
rm -f "$PID_ROOT/pids.env"
|
||||
echo "fixed RRDP/rsync services stopped: $PID_ROOT"
|
||||
146
scripts/docker/build_custom_tal_patch.sh
Normal file
146
scripts/docker/build_custom_tal_patch.sh
Normal file
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
PATCH_SOURCE="$REPO_ROOT/deploy/docker-installer/custom-tal-patch"
|
||||
BASE_COMPONENT_PACKAGE=""
|
||||
OUT_DIR="$(printenv OUT_DIR 2>/dev/null || true)"
|
||||
PREFIX="$(printenv PREFIX 2>/dev/null || true)"
|
||||
ALLOW_DIRTY=0
|
||||
[[ -n "$OUT_DIR" ]] || OUT_DIR="$REPO_ROOT/target/custom-tal-patch"
|
||||
[[ -n "$PREFIX" ]] || PREFIX="ours-rp-custom-tal-patch"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
scripts/docker/build_custom_tal_patch.sh \
|
||||
--base-component-package <ours-rp-installer-*.tar.gz> \
|
||||
[--out-dir <dir>] [--prefix <name>] [--allow-dirty]
|
||||
|
||||
The patch targets the Arm64 component package from the customer stack. It
|
||||
overlays the runner, Compose, fixture mount and test-service tools; it does not
|
||||
rebuild or replace the runtime image.
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--base-component-package) BASE_COMPONENT_PACKAGE="$2"; shift 2 ;;
|
||||
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||
--prefix) PREFIX="$2"; shift 2 ;;
|
||||
--allow-dirty) ALLOW_DIRTY=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -f "$BASE_COMPONENT_PACKAGE" ]] || die "missing base component package: $BASE_COMPONENT_PACKAGE"
|
||||
[[ -d "$PATCH_SOURCE" ]] || die "missing patch source: $PATCH_SOURCE"
|
||||
|
||||
SOURCE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||||
SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 HEAD)"
|
||||
SOURCE_DIRTY=false
|
||||
if [[ -n "$(git -C "$REPO_ROOT" status --short)" ]]; then
|
||||
SOURCE_DIRTY=true
|
||||
fi
|
||||
if [[ "$SOURCE_DIRTY" == true && "$ALLOW_DIRTY" != 1 ]]; then
|
||||
die "source worktree is dirty; use --allow-dirty for a development patch"
|
||||
fi
|
||||
|
||||
package_root="$(tar -tzf "$BASE_COMPONENT_PACKAGE" | awk -F/ '!seen { print $1; seen = 1 }')"
|
||||
[[ -n "$package_root" ]] || die "cannot determine component package root"
|
||||
manifest_text="$(tar -xOf "$BASE_COMPONENT_PACKAGE" "$package_root/PACKAGE-MANIFEST.env")" \
|
||||
|| die "base component package has no PACKAGE-MANIFEST.env"
|
||||
read_manifest_value() {
|
||||
printf '%s\n' "$manifest_text" | awk -F= -v key="$1" '$1 == key { print substr($0, index($0, "=") + 1); exit }'
|
||||
}
|
||||
base_source_commit="$(read_manifest_value source_commit)"
|
||||
base_package_arch="$(read_manifest_value package_arch)"
|
||||
base_runtime_image="$(read_manifest_value runtime_image)"
|
||||
[[ "$base_source_commit" == 9f4f4cf069e9fa8f6f97ed0f1065adac38abe82f ]] \
|
||||
|| die "base source commit mismatch: $base_source_commit"
|
||||
[[ "$base_package_arch" == arm64 ]] || die "base package must be arm64: $base_package_arch"
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
patch_name="$PREFIX-$SOURCE_COMMIT_SHORT"
|
||||
if [[ "$SOURCE_DIRTY" == true ]]; then
|
||||
patch_name="$patch_name-dirty"
|
||||
fi
|
||||
patch_name="$patch_name-$timestamp"
|
||||
stage="$OUT_DIR/$patch_name"
|
||||
tar_path="$OUT_DIR/$patch_name.tar.gz"
|
||||
base_sha256="$(sha256sum "$BASE_COMPONENT_PACKAGE" | awk '{ print $1 }')"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage/payload/compose" "$stage/payload/scripts/soak" \
|
||||
"$stage/payload/custom-fixtures" "$stage/payload/tools"
|
||||
|
||||
cp "$PATCH_SOURCE/apply_custom_tal_patch.sh" "$stage/"
|
||||
cp "$PATCH_SOURCE/rollback_custom_tal_patch.sh" "$stage/"
|
||||
cp "$PATCH_SOURCE/custom-tal.env.example" "$stage/"
|
||||
cp "$REPO_ROOT/deploy/docker-installer/compose/docker-compose.yml" "$stage/payload/compose/"
|
||||
cp "$REPO_ROOT/scripts/soak/run_soak.sh" "$stage/payload/scripts/soak/"
|
||||
cp "$REPO_ROOT/deploy/docker-installer/custom-fixtures/README.md" "$stage/payload/custom-fixtures/"
|
||||
cp "$PATCH_SOURCE/tools/generate_custom_fixture.py" "$stage/payload/tools/"
|
||||
cp "$PATCH_SOURCE/tools/serve_https.py" "$stage/payload/tools/"
|
||||
cp "$PATCH_SOURCE/tools/start_fixed_services.sh" "$stage/payload/tools/"
|
||||
cp "$PATCH_SOURCE/tools/stop_fixed_services.sh" "$stage/payload/tools/"
|
||||
chmod +x "$stage/apply_custom_tal_patch.sh" "$stage/rollback_custom_tal_patch.sh" \
|
||||
"$stage/payload/scripts/soak/run_soak.sh" "$stage/payload/tools/"*.sh \
|
||||
"$stage/payload/tools/"*.py
|
||||
|
||||
{
|
||||
echo "patch_schema_version=1"
|
||||
echo "patch_name=$patch_name"
|
||||
echo "created_at_utc=$timestamp"
|
||||
echo "source_commit=$SOURCE_COMMIT"
|
||||
echo "source_commit_short=$SOURCE_COMMIT_SHORT"
|
||||
echo "source_dirty=$SOURCE_DIRTY"
|
||||
echo "base_component_archive=$(basename "$BASE_COMPONENT_PACKAGE")"
|
||||
echo "base_component_archive_sha256=$base_sha256"
|
||||
echo "base_component_root=$package_root"
|
||||
echo "base_source_commit=$base_source_commit"
|
||||
echo "base_package_arch=$base_package_arch"
|
||||
echo "base_runtime_image=$base_runtime_image"
|
||||
echo "overlay_runtime_image_unchanged=true"
|
||||
echo "custom_mode=RIRS=custom,TAL_INPUT_MODE=custom-file-with-ta"
|
||||
echo "rrdp_port=18443"
|
||||
echo "rsync_port=1873"
|
||||
echo "payload_root=payload"
|
||||
while IFS= read -r relative_path; do
|
||||
safe_name="$(printf '%s' "$relative_path" | tr '/.' '__')"
|
||||
file_sha256="$(sha256sum "$stage/payload/$relative_path" | awk '{ print $1 }')"
|
||||
echo "payload_"$safe_name"_sha256=$file_sha256"
|
||||
done < <(cd "$stage/payload" && find . -type f -printf '%P\n' | sort)
|
||||
} > "$stage/PATCH-MANIFEST.env"
|
||||
|
||||
cat > "$stage/PATCH-SUMMARY.txt" <<EOF
|
||||
patch_name: $patch_name
|
||||
base_component_archive: $(basename "$BASE_COMPONENT_PACKAGE")
|
||||
base_component_archive_sha256: $base_sha256
|
||||
base_source_commit: $base_source_commit
|
||||
source_commit: $SOURCE_COMMIT
|
||||
source_dirty: $SOURCE_DIRTY
|
||||
runtime_image: $base_runtime_image
|
||||
runtime_image_changed: false
|
||||
custom_fixture_generator: payload/tools/generate_custom_fixture.py
|
||||
fixed_services: payload/tools/start_fixed_services.sh
|
||||
EOF
|
||||
|
||||
tar -C "$OUT_DIR" -czf "$tar_path" "$patch_name"
|
||||
patch_sha256="$(sha256sum "$tar_path" | awk '{ print $1 }')"
|
||||
{
|
||||
echo "patch=$tar_path"
|
||||
echo "patch_dir=$stage"
|
||||
echo "patch_sha256=$patch_sha256"
|
||||
echo "manifest=$stage/PATCH-MANIFEST.env"
|
||||
echo "base_component_archive_sha256=$base_sha256"
|
||||
echo "source_commit=$SOURCE_COMMIT"
|
||||
echo "source_dirty=$SOURCE_DIRTY"
|
||||
} > "$OUT_DIR/$patch_name.summary.env"
|
||||
echo "patch built: $tar_path"
|
||||
@ -15,6 +15,10 @@ INTERVAL_SECS="${INTERVAL_SECS:-0}"
|
||||
STOP_AFTER_SECS="${STOP_AFTER_SECS:-0}"
|
||||
RIRS="${RIRS:-afrinic,apnic,arin,lacnic,ripe}"
|
||||
TAL_INPUT_MODE="${TAL_INPUT_MODE:-file-with-ta}"
|
||||
CUSTOM_TAL_PATH="${CUSTOM_TAL_PATH:-$PACKAGE_ROOT/custom-fixtures/tal/custom.tal}"
|
||||
CUSTOM_TA_PATH="${CUSTOM_TA_PATH:-$PACKAGE_ROOT/custom-fixtures/ta/custom-ta.cer}"
|
||||
CUSTOM_TAL_URI="${CUSTOM_TAL_URI:-https://host.docker.internal:18443/tal/custom.tal}"
|
||||
HTTP_ROOT_CERT_PATHS="${HTTP_ROOT_CERT_PATHS:-}"
|
||||
RESOURCE_VALIDATION_MODE="${RESOURCE_VALIDATION_MODE:-validation-update-03}"
|
||||
RUN_ROOT="${RUN_ROOT:-$PACKAGE_ROOT}"
|
||||
RETAIN_RUNS="${RETAIN_RUNS:-10}"
|
||||
@ -118,10 +122,10 @@ validate_rsync_scope() {
|
||||
|
||||
validate_tal_input_mode() {
|
||||
case "$TAL_INPUT_MODE" in
|
||||
file-with-ta|file-live-ta|url)
|
||||
file-with-ta|file-live-ta|custom-file-with-ta|url)
|
||||
;;
|
||||
*)
|
||||
die "TAL_INPUT_MODE must be file-with-ta, file-live-ta or url: $TAL_INPUT_MODE"
|
||||
die "TAL_INPUT_MODE must be file-with-ta, file-live-ta, custom-file-with-ta or url: $TAL_INPUT_MODE"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@ -155,14 +159,46 @@ parse_rirs() {
|
||||
afrinic|apnic|arin|lacnic|ripe)
|
||||
RIR_LIST+=("$normalized")
|
||||
;;
|
||||
custom)
|
||||
[[ "$TAL_INPUT_MODE" == "custom-file-with-ta" ]] \
|
||||
|| die "RIRS=custom requires TAL_INPUT_MODE=custom-file-with-ta"
|
||||
RIR_LIST+=("$normalized")
|
||||
;;
|
||||
*)
|
||||
die "invalid RIRS entry: $raw_token; allowed: afrinic,apnic,arin,lacnic,ripe"
|
||||
die "invalid RIRS entry: $raw_token; allowed: afrinic,apnic,arin,lacnic,ripe,custom (custom mode only)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ "${#RIR_LIST[@]}" -gt 0 ]] || die "RIRS must contain at least one RIR"
|
||||
}
|
||||
|
||||
validate_custom_inputs() {
|
||||
local root_cert_path
|
||||
local -a root_cert_paths=()
|
||||
|
||||
if [[ "$TAL_INPUT_MODE" == "custom-file-with-ta" ]]; then
|
||||
[[ "${#RIR_LIST[@]}" -eq 1 && "${RIR_LIST[0]}" == "custom" ]] \
|
||||
|| die "custom-file-with-ta requires exactly RIRS=custom"
|
||||
[[ -s "$CUSTOM_TAL_PATH" ]] \
|
||||
|| die "missing custom TAL: $CUSTOM_TAL_PATH"
|
||||
[[ -s "$CUSTOM_TA_PATH" ]] \
|
||||
|| die "missing custom TA DER: $CUSTOM_TA_PATH"
|
||||
[[ -n "$CUSTOM_TAL_URI" ]] \
|
||||
|| die "CUSTOM_TAL_URI is required in custom-file-with-ta mode"
|
||||
elif [[ "$RIRS" == *custom* ]]; then
|
||||
die "RIRS=custom requires TAL_INPUT_MODE=custom-file-with-ta"
|
||||
fi
|
||||
|
||||
if [[ -n "$HTTP_ROOT_CERT_PATHS" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
root_cert_paths=( $HTTP_ROOT_CERT_PATHS )
|
||||
for root_cert_path in "${root_cert_paths[@]}"; do
|
||||
[[ -s "$root_cert_path" ]] \
|
||||
|| die "missing HTTP root certificate: $root_cert_path"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
tal_file_for_rir() {
|
||||
case "$1" in
|
||||
afrinic) printf '%s' "$FIXTURE_DIR/tal/afrinic.tal" ;;
|
||||
@ -170,6 +206,7 @@ tal_file_for_rir() {
|
||||
arin) printf '%s' "$FIXTURE_DIR/tal/arin.tal" ;;
|
||||
lacnic) printf '%s' "$FIXTURE_DIR/tal/lacnic.tal" ;;
|
||||
ripe) printf '%s' "$FIXTURE_DIR/tal/ripe-ncc.tal" ;;
|
||||
custom) printf '%s' "$CUSTOM_TAL_PATH" ;;
|
||||
*) die "unknown RIR: $1" ;;
|
||||
esac
|
||||
}
|
||||
@ -181,6 +218,7 @@ ta_file_for_rir() {
|
||||
arin) printf '%s' "$FIXTURE_DIR/ta/arin-ta.cer" ;;
|
||||
lacnic) printf '%s' "$FIXTURE_DIR/ta/lacnic-ta.cer" ;;
|
||||
ripe) printf '%s' "$FIXTURE_DIR/ta/ripe-ncc-ta.cer" ;;
|
||||
custom) printf '%s' "$CUSTOM_TA_PATH" ;;
|
||||
*) die "unknown RIR: $1" ;;
|
||||
esac
|
||||
}
|
||||
@ -192,6 +230,7 @@ tal_url_for_rir() {
|
||||
arin) printf '%s' "https://www.arin.net/resources/manage/rpki/arin.tal" ;;
|
||||
lacnic) printf '%s' "https://www.lacnic.net/innovaportal/file/4983/1/lacnic.tal" ;;
|
||||
ripe) printf '%s' "https://tal.rpki.ripe.net/ripe-ncc.tal" ;;
|
||||
custom) printf '%s' "$CUSTOM_TAL_URI" ;;
|
||||
*) die "unknown RIR: $1" ;;
|
||||
esac
|
||||
}
|
||||
@ -1057,6 +1096,16 @@ build_child_args() {
|
||||
CHILD_ARGS+=(--rsync-mirror-root "$TMP_DIR/rsync-mirror-{run_id}")
|
||||
fi
|
||||
|
||||
local root_cert_path
|
||||
local -a root_cert_paths=()
|
||||
if [[ -n "$HTTP_ROOT_CERT_PATHS" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
root_cert_paths=( $HTTP_ROOT_CERT_PATHS )
|
||||
for root_cert_path in "${root_cert_paths[@]}"; do
|
||||
CHILD_ARGS+=(--http-root-cert "$root_cert_path")
|
||||
done
|
||||
fi
|
||||
|
||||
CHILD_ARGS+=(
|
||||
--parallel-phase2-ready-batch-size 256
|
||||
--parallel-phase2-ready-batch-wall-time-budget-ms 100
|
||||
@ -1344,6 +1393,7 @@ main() {
|
||||
validate_positive_int "DB_STATS_EXACT_EVERY" "$DB_STATS_EXACT_EVERY"
|
||||
fi
|
||||
parse_rirs
|
||||
validate_custom_inputs
|
||||
[[ -x "$RPKI_BIN" ]] || die "missing executable: $RPKI_BIN"
|
||||
[[ -x "$RPKI_DAEMON_BIN" ]] || die "missing executable: $RPKI_DAEMON_BIN"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user