Compare commits
15 Commits
de4f40eb2c
...
feature/14
| Author | SHA1 | Date | |
|---|---|---|---|
| 850ff218ce | |||
| 9f4f4cf069 | |||
| b66b425512 | |||
| 363e69d11d | |||
| da6e2e515d | |||
| 4a64fa7900 | |||
| 1ce897dbff | |||
| 08281fa231 | |||
| 60796299b0 | |||
| 6de0bced1c | |||
| 8e37ef48cb | |||
| 8613cfae60 | |||
| f3934c3bf8 | |||
| 00613afdde | |||
| ea6ac27b8d |
@@ -11,12 +11,12 @@ ALLOW_CROSS_ARCH=0
|
||||
# Compose project name.
|
||||
COMPOSE_PROJECT_NAME=ours-rp-__PACKAGE_ARCH__-installer
|
||||
|
||||
# Runtime image loaded from images/*.tar.gz by install.sh.
|
||||
RPKI_IMAGE=ours-rp-runtime-__PACKAGE_ARCH__:dev
|
||||
# Runtime image rebuilt from the current source commit while packaging.
|
||||
RPKI_IMAGE=__RUNTIME_IMAGE__
|
||||
RPKI_PLATFORM=__PACKAGE_PLATFORM__
|
||||
|
||||
# Metrics image loaded from images/*.tar.gz by install.sh.
|
||||
METRICS_IMAGE=ours-rp-metrics-__PACKAGE_ARCH__:dev
|
||||
# Metrics image rebuilt from the current source commit while packaging.
|
||||
METRICS_IMAGE=__METRICS_IMAGE__
|
||||
METRICS_PLATFORM=__PACKAGE_PLATFORM__
|
||||
|
||||
# Restart policy for the soak container. Production default keeps the daemon alive.
|
||||
@@ -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
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_cir_trust_anchors",
|
||||
"expr": "max(ours_rp_cir_trust_anchors and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "RIRs",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -89,7 +89,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_duration_seconds",
|
||||
"expr": "max(ours_rp_run_duration_seconds and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "wall",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -133,7 +133,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_max_rss_bytes",
|
||||
"expr": "max(ours_rp_run_max_rss_bytes and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "rss",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_publication_points",
|
||||
"expr": "max(ours_rp_publication_points and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "publication points",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -223,7 +223,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_sequence",
|
||||
"expr": "max(ours_rp_run_sequence)",
|
||||
"legendFormat": "seq",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -287,7 +287,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "100 * sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"publication_point_cache\"}) / sum by (job, instance, exported_instance) (ours_rp_publication_points)",
|
||||
"expr": "max((100 * sum by (exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"publication_point_cache\"}) / sum by (exported_instance) (ours_rp_publication_points)) and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "PP cache hit ratio",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -332,7 +332,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_vrps{kind=\"total\"}",
|
||||
"expr": "max(ours_rp_vrps{kind=\"total\"} and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "VRPs raw",
|
||||
"refId": "A"
|
||||
}
|
||||
@@ -377,7 +377,7 @@
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_vaps",
|
||||
"expr": "max(ours_rp_vaps and on (exported_instance) topk(1, ours_rp_run_sequence))",
|
||||
"legendFormat": "VAPs",
|
||||
"refId": "A"
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -6,6 +6,8 @@ This package deploys ours RP on Linux servers whose host architecture matches th
|
||||
|
||||
The package includes four architecture-matched images: ours RP runtime, artifact metrics, Prometheus, and Grafana. Deployment does not need to pull application images on the target host. Runtime state, run artifacts, logs, Prometheus data and Grafana data are persisted through host bind mounts.
|
||||
|
||||
Every installer build rebuilds the ours RP runtime and artifact metrics images. The deliverable name is `ours-rp-installer-{arch}-{git8}-{YYYYMMDDTHHMMSSZ}.tar.gz`, and both ours RP image tags use the current source commit. `PACKAGE-MANIFEST.env` records the full commit, UTC build time, dirty state, and image archive hashes; `./scripts/status.sh --brief` displays that provenance after installation.
|
||||
|
||||
By default the host architecture must match `PACKAGE_ARCH` from `PACKAGE-MANIFEST.env`. If you intentionally want to run this package on a different host architecture through QEMU/binfmt, set:
|
||||
|
||||
```bash
|
||||
@@ -16,19 +18,19 @@ ALLOW_CROSS_ARCH=1
|
||||
|
||||
`compose/` is the single production Docker Compose template source. The Docker installer build script copies this directory directly. Do not maintain a second copy of the Compose, Prometheus, or Grafana dashboard files elsewhere.
|
||||
|
||||
The legacy direct Compose deployment entry point has been removed. For either amd64 or arm64, build the matching installer package with `scripts/docker/build_docker_installer_package.sh`, then use `install.sh`, `start.sh`, and `status.sh` as described here.
|
||||
The legacy direct Compose deployment entry point has been removed. For either amd64 or arm64, build the matching installer package with `scripts/docker/build_docker_installer_package.sh`, then use `scripts/install.sh`, `scripts/start.sh`, and `scripts/status.sh` as described here.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
tar -xzf ours-rp-docker-installer-__PACKAGE_ARCH__-*.tar.gz
|
||||
cd ours-rp-docker-installer-__PACKAGE_ARCH__-*
|
||||
tar -xzf ours-rp-installer-__PACKAGE_ARCH__-*.tar.gz
|
||||
cd ours-rp-installer-__PACKAGE_ARCH__-*
|
||||
|
||||
./install.sh
|
||||
./scripts/install.sh
|
||||
cp .env.example .env # install.sh creates .env automatically if missing
|
||||
vim .env
|
||||
./start.sh
|
||||
./status.sh
|
||||
./scripts/start.sh
|
||||
./scripts/status.sh
|
||||
```
|
||||
|
||||
Defaults:
|
||||
@@ -45,12 +47,12 @@ Defaults:
|
||||
- `PERIODIC_SNAPSHOT_MAX_DELTAS=100`
|
||||
- `HOST_DATA_DIR=__HOST_DATA_DIR__`
|
||||
- `SOAK_RESTART_POLICY=unless-stopped`
|
||||
- `RPKI_IMAGE=ours-rp-runtime-__PACKAGE_ARCH__:dev`
|
||||
- `METRICS_IMAGE=ours-rp-metrics-__PACKAGE_ARCH__:dev`
|
||||
- `RPKI_IMAGE=__RUNTIME_IMAGE__`
|
||||
- `METRICS_IMAGE=__METRICS_IMAGE__`
|
||||
- `METRICS_PLATFORM=__PACKAGE_PLATFORM__`
|
||||
- `MONITOR_PLATFORM=__PACKAGE_PLATFORM__`
|
||||
- `ALLOW_CROSS_ARCH=0`
|
||||
- `RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report`, which mounts an empty fallback directory by default; set it to a real host directory such as `/root/rpki/report` to ingest reports from a separately deployed RTR service
|
||||
- `RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report`, which mounts an empty fallback directory by default; after deploying a separate RTR service, register its same-host report directory through `scripts/register_rtr_monitor.sh`
|
||||
|
||||
## First Start Semantics
|
||||
|
||||
@@ -115,14 +117,13 @@ Change the password and restrict public access for production deployments.
|
||||
|
||||
## Optional RTR report metrics input
|
||||
|
||||
If a separately deployed RTR service on the same host continuously writes `rtr-source-*`, `rtr-runtime-*`, and `rtr-clients-*` JSON reports, configure:
|
||||
If a separately deployed RTR service on the same host continuously writes `rtr-source-*`, `rtr-runtime-*`, and `rtr-clients-*` JSON reports, run:
|
||||
|
||||
```bash
|
||||
RTR_REPORT_DIR=/root/rpki/report
|
||||
RTR_REPORT_CONTAINER_DIR=/var/lib/ours-rp/rtr-report
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
|
||||
```
|
||||
|
||||
The `artifact-metrics` container mounts the host directory read-only and reads it through `RPKI_METRICS_RTR_REPORT_DIR`, producing `ours_rp_rtr_*` metrics. By default, `RTR_REPORT_DIR` points at an empty fallback directory under the installer data root, so the metrics container also starts cleanly without an RTR service.
|
||||
The command accepts only an absolute path on the same host, validates the candidate Compose mount, and atomically updates `.env`. It recreates only `artifact-metrics`, Prometheus, and Grafana; it does not restart or recreate `ours-rp-soak`. The `artifact-metrics` container mounts the directory read-only and reads it through `RPKI_METRICS_RTR_REPORT_DIR`, producing `ours_rp_rtr_*` metrics. By default, `RTR_REPORT_DIR` points at an empty fallback directory under the installer data root, so the metrics container also starts cleanly without an RTR service.
|
||||
|
||||
## Data Directory
|
||||
|
||||
@@ -170,12 +171,12 @@ Check the latest `run-meta.json` for:
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
./status.sh
|
||||
./logs.sh ours-rp-soak --tail 200
|
||||
./restart.sh
|
||||
./stop.sh
|
||||
./cleanup.sh --keep-runs 100 --execute
|
||||
./uninstall.sh
|
||||
./scripts/status.sh
|
||||
./scripts/logs.sh ours-rp-soak --tail 200
|
||||
./scripts/restart.sh
|
||||
./scripts/stop.sh
|
||||
./scripts/cleanup.sh --keep-runs 100 --execute
|
||||
./scripts/uninstall.sh
|
||||
```
|
||||
|
||||
For finite acceptance tests, for example `MAX_RUNS=3`, also set:
|
||||
@@ -189,5 +190,5 @@ Otherwise Compose `unless-stopped` will restart the container after it exits suc
|
||||
`uninstall.sh` keeps data by default. Use the following only when you really want to delete `HOST_DATA_DIR`:
|
||||
|
||||
```bash
|
||||
./uninstall.sh --purge-data
|
||||
./scripts/uninstall.sh --purge-data
|
||||
```
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
安装包内置与包架构一致的四类镜像:ours RP runtime、artifact metrics、Prometheus、Grafana。部署时不需要现场拉取应用镜像。运行产物、状态数据库、日志、Prometheus 和 Grafana 数据均通过宿主机目录挂载保存。
|
||||
|
||||
每次构建安装包都会重新构建 ours RP runtime 与 artifact metrics 镜像。交付包命名为 `ours-rp-installer-{arch}-{git8}-{YYYYMMDDTHHMMSSZ}.tar.gz`,两类 ours RP 镜像分别使用当前源码 commit 的 tag。完整 commit、UTC 构建时间、dirty 状态和镜像归档 hash 保存在 `PACKAGE-MANIFEST.env`,可通过 `./scripts/status.sh --brief` 查看。
|
||||
|
||||
默认情况下,宿主架构必须与 `PACKAGE-MANIFEST.env` 中的 `PACKAGE_ARCH` 匹配;若确需在异构主机上通过 QEMU/binfmt 运行,必须显式设置:
|
||||
|
||||
```bash
|
||||
@@ -16,19 +18,19 @@ ALLOW_CROSS_ARCH=1
|
||||
|
||||
`compose/` 是唯一的生产 Docker Compose 模板来源,Docker 安装包构建脚本会直接复制该目录。不要在其他目录维护第二份 compose、Prometheus 或 Grafana dashboard 文件。
|
||||
|
||||
旧的直接 Compose 部署入口已经移除。需要部署 amd64 或 arm64 时,统一先用 `scripts/docker/build_docker_installer_package.sh` 构建对应架构安装包,再按本说明执行 `install.sh`、`start.sh` 和 `status.sh`。
|
||||
旧的直接 Compose 部署入口已经移除。需要部署 amd64 或 arm64 时,统一先用 `scripts/docker/build_docker_installer_package.sh` 构建对应架构安装包,再按本说明执行 `scripts/install.sh`、`scripts/start.sh` 和 `scripts/status.sh`。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
tar -xzf ours-rp-docker-installer-__PACKAGE_ARCH__-*.tar.gz
|
||||
cd ours-rp-docker-installer-__PACKAGE_ARCH__-*
|
||||
tar -xzf ours-rp-installer-__PACKAGE_ARCH__-*.tar.gz
|
||||
cd ours-rp-installer-__PACKAGE_ARCH__-*
|
||||
|
||||
./install.sh
|
||||
./scripts/install.sh
|
||||
cp .env.example .env # 如 install.sh 已自动创建,可直接编辑现有 .env
|
||||
vim .env
|
||||
./start.sh
|
||||
./status.sh
|
||||
./scripts/start.sh
|
||||
./scripts/status.sh
|
||||
```
|
||||
|
||||
默认配置:
|
||||
@@ -45,12 +47,12 @@ vim .env
|
||||
- `PERIODIC_SNAPSHOT_MAX_DELTAS=100`
|
||||
- `HOST_DATA_DIR=__HOST_DATA_DIR__`
|
||||
- `SOAK_RESTART_POLICY=unless-stopped`
|
||||
- `RPKI_IMAGE=ours-rp-runtime-__PACKAGE_ARCH__:dev`
|
||||
- `METRICS_IMAGE=ours-rp-metrics-__PACKAGE_ARCH__:dev`
|
||||
- `RPKI_IMAGE=__RUNTIME_IMAGE__`
|
||||
- `METRICS_IMAGE=__METRICS_IMAGE__`
|
||||
- `METRICS_PLATFORM=__PACKAGE_PLATFORM__`
|
||||
- `MONITOR_PLATFORM=__PACKAGE_PLATFORM__`
|
||||
- `ALLOW_CROSS_ARCH=0`
|
||||
- `RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report`,默认挂载空目录;如需接入独立 RTR 服务 report,可设置为宿主机真实目录,例如 `/root/rpki/report`
|
||||
- `RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report`,默认挂载空目录;独立 RTR 服务部署完成后,使用 `scripts/register_rtr_monitor.sh` 注册同机 report 目录
|
||||
|
||||
## 首次启动语义
|
||||
|
||||
@@ -115,14 +117,13 @@ GRAFANA_ADMIN_PASSWORD=admin
|
||||
|
||||
## 可选 RTR report 监控接入
|
||||
|
||||
如果同一台机器上已经有独立部署的 RTR 服务,并且它持续输出 `rtr-source-*`、`rtr-runtime-*`、`rtr-clients-*` JSON report,可在 `.env` 中配置:
|
||||
如果同一台机器上已经有独立部署的 RTR 服务,并且它持续输出 `rtr-source-*`、`rtr-runtime-*`、`rtr-clients-*` JSON report,执行:
|
||||
|
||||
```bash
|
||||
RTR_REPORT_DIR=/root/rpki/report
|
||||
RTR_REPORT_CONTAINER_DIR=/var/lib/ours-rp/rtr-report
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
|
||||
```
|
||||
|
||||
`artifact-metrics` 容器会以只读方式挂载该目录,并通过 `RPKI_METRICS_RTR_REPORT_DIR` 读取 report,输出 `ours_rp_rtr_*` 指标。默认 `RTR_REPORT_DIR` 指向安装包数据目录下的空 fallback 目录,因此未接入 RTR 服务时 metrics 容器也能稳定启动。
|
||||
该命令只接受本机绝对路径,会先校验候选 Compose 挂载,再原子更新 `.env`。随后只重建 `artifact-metrics`、Prometheus 和 Grafana,不重启或重建 `ours-rp-soak`。`artifact-metrics` 以只读方式挂载该目录,并通过 `RPKI_METRICS_RTR_REPORT_DIR` 读取 report,输出 `ours_rp_rtr_*` 指标。默认 `RTR_REPORT_DIR` 指向安装包数据目录下的空 fallback 目录,因此未接入 RTR 服务时 metrics 容器也能稳定启动。
|
||||
|
||||
## 数据目录
|
||||
|
||||
@@ -170,12 +171,12 @@ PERIODIC_SNAPSHOT_MAX_DELTAS=100
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
./status.sh
|
||||
./logs.sh ours-rp-soak --tail 200
|
||||
./restart.sh
|
||||
./stop.sh
|
||||
./cleanup.sh --keep-runs 100 --execute
|
||||
./uninstall.sh
|
||||
./scripts/status.sh
|
||||
./scripts/logs.sh ours-rp-soak --tail 200
|
||||
./scripts/restart.sh
|
||||
./scripts/stop.sh
|
||||
./scripts/cleanup.sh --keep-runs 100 --execute
|
||||
./scripts/uninstall.sh
|
||||
```
|
||||
|
||||
如果做有限轮次验收,例如 `MAX_RUNS=3`,建议同时设置:
|
||||
@@ -189,7 +190,7 @@ SOAK_RESTART_POLICY=no
|
||||
`uninstall.sh` 默认不删除数据。只有显式执行:
|
||||
|
||||
```bash
|
||||
./uninstall.sh --purge-data
|
||||
./scripts/uninstall.sh --purge-data
|
||||
```
|
||||
|
||||
才会删除 `HOST_DATA_DIR`。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Install
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
The installer is idempotent:
|
||||
@@ -17,13 +17,13 @@ The installer is idempotent:
|
||||
## Start
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
Start without waiting for the first snapshot:
|
||||
|
||||
```bash
|
||||
./start.sh --no-wait-first-run
|
||||
./scripts/start.sh --no-wait-first-run
|
||||
```
|
||||
|
||||
If you intentionally want to run this package on a different host architecture through QEMU/binfmt, first set:
|
||||
@@ -32,20 +32,20 @@ If you intentionally want to run this package on a different host architecture t
|
||||
ALLOW_CROSS_ARCH=1
|
||||
```
|
||||
|
||||
Then rerun `./install.sh` or `./self-check.sh`, and only then will the scripts attempt to enable the matching binfmt handler.
|
||||
Then rerun `./scripts/install.sh` or `./scripts/self-check.sh`, and only then will the scripts attempt to enable the matching binfmt handler.
|
||||
|
||||
## Stop and Restart
|
||||
|
||||
```bash
|
||||
./stop.sh
|
||||
./restart.sh
|
||||
./scripts/stop.sh
|
||||
./scripts/restart.sh
|
||||
```
|
||||
|
||||
## Status Checks
|
||||
|
||||
```bash
|
||||
./status.sh
|
||||
./self-check.sh
|
||||
./scripts/status.sh
|
||||
./scripts/self-check.sh
|
||||
```
|
||||
|
||||
Important checks:
|
||||
@@ -73,15 +73,20 @@ Important checks:
|
||||
To verify RTR report ingestion:
|
||||
|
||||
```bash
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
|
||||
./scripts/status.sh
|
||||
curl -s "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" | grep '^ours_rp_rtr_' | head
|
||||
```
|
||||
|
||||
Registration accepts only an absolute directory on the same host. The command retains a timestamped `.env` backup and updates only the metrics, Prometheus, and Grafana sidecars; the `ours-rp-soak` container ID must remain unchanged.
|
||||
|
||||
## Upgrade
|
||||
|
||||
Extract the new package into a new directory and explicitly reuse the existing `.env` through the upgrade script:
|
||||
|
||||
```bash
|
||||
./upgrade.sh --reuse-env-from /path/to/old-installer/.env
|
||||
./scripts/upgrade.sh --reuse-env-from /path/to/old-installer/.env
|
||||
```
|
||||
|
||||
If the new package directory already has a `.env`, the upgrade script keeps it.
|
||||
@@ -125,8 +130,8 @@ PERIODIC_SNAPSHOT_MAX_DELTAS=100
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
./cleanup.sh --keep-runs 100
|
||||
./cleanup.sh --keep-runs 100 --execute
|
||||
./scripts/cleanup.sh --keep-runs 100
|
||||
./scripts/cleanup.sh --keep-runs 100 --execute
|
||||
```
|
||||
|
||||
Cleanup is dry-run by default. Add `--execute` after reviewing the output.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
安装脚本是幂等的:
|
||||
@@ -17,13 +17,13 @@
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
如需后台启动后不等待首轮 snapshot:
|
||||
|
||||
```bash
|
||||
./start.sh --no-wait-first-run
|
||||
./scripts/start.sh --no-wait-first-run
|
||||
```
|
||||
|
||||
如需显式允许异构主机通过 QEMU/binfmt 跑包,先在 `.env` 中设置:
|
||||
@@ -32,20 +32,20 @@
|
||||
ALLOW_CROSS_ARCH=1
|
||||
```
|
||||
|
||||
然后重新执行 `./install.sh` 或 `./self-check.sh`,脚本才会尝试启用对应 binfmt。
|
||||
然后重新执行 `./scripts/install.sh` 或 `./scripts/self-check.sh`,脚本才会尝试启用对应 binfmt。
|
||||
|
||||
## 停止和重启
|
||||
|
||||
```bash
|
||||
./stop.sh
|
||||
./restart.sh
|
||||
./scripts/stop.sh
|
||||
./scripts/restart.sh
|
||||
```
|
||||
|
||||
## 状态检查
|
||||
|
||||
```bash
|
||||
./status.sh
|
||||
./self-check.sh
|
||||
./scripts/status.sh
|
||||
./scripts/self-check.sh
|
||||
```
|
||||
|
||||
重点检查项:
|
||||
@@ -73,15 +73,20 @@ ALLOW_CROSS_ARCH=1
|
||||
如需确认 RTR report 已接入:
|
||||
|
||||
```bash
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
|
||||
./scripts/status.sh
|
||||
curl -s "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" | grep '^ours_rp_rtr_' | head
|
||||
```
|
||||
|
||||
注册只支持同机的绝对目录。注册命令会保留一份带时间戳的 `.env` 备份,并且只更新 metrics、Prometheus、Grafana 三个旁路容器;`ours-rp-soak` 的容器 ID 应保持不变。
|
||||
|
||||
## 升级
|
||||
|
||||
把新安装包解压到新目录后,推荐通过升级脚本显式复用旧 `.env`:
|
||||
|
||||
```bash
|
||||
./upgrade.sh --reuse-env-from /path/to/old-installer/.env
|
||||
./scripts/upgrade.sh --reuse-env-from /path/to/old-installer/.env
|
||||
```
|
||||
|
||||
如果新目录已经存在 `.env`,升级脚本会保留它,不覆盖。
|
||||
@@ -125,8 +130,8 @@ PERIODIC_SNAPSHOT_MAX_DELTAS=100
|
||||
## 清理
|
||||
|
||||
```bash
|
||||
./cleanup.sh --keep-runs 100
|
||||
./cleanup.sh --keep-runs 100 --execute
|
||||
./scripts/cleanup.sh --keep-runs 100
|
||||
./scripts/cleanup.sh --keep-runs 100 --execute
|
||||
```
|
||||
|
||||
默认 dry-run,确认后加 `--execute`。
|
||||
|
||||
@@ -12,7 +12,7 @@ docker compose version
|
||||
If missing, run:
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
## Package Architecture Mismatch
|
||||
@@ -55,11 +55,11 @@ docker run --rm --platform __PACKAGE_PLATFORM__ \
|
||||
|
||||
## RTR report metrics are empty
|
||||
|
||||
Check `.env`:
|
||||
First check registration and the report directory:
|
||||
|
||||
```bash
|
||||
grep '^RTR_REPORT_DIR=' .env
|
||||
test -d "$(grep '^RTR_REPORT_DIR=' .env | cut -d= -f2-)" && echo ok
|
||||
./scripts/status.sh
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
|
||||
```
|
||||
|
||||
Check the metrics container mount:
|
||||
@@ -74,7 +74,7 @@ docker inspect "$(grep '^COMPOSE_PROJECT_NAME=' .env | cut -d= -f2-)-artifact-me
|
||||
All-five snapshot can be slow, especially under QEMU. Increase timeout:
|
||||
|
||||
```bash
|
||||
./start.sh --timeout-secs 14400
|
||||
./scripts/start.sh --timeout-secs 14400
|
||||
```
|
||||
|
||||
## Output Counts Are Too Low
|
||||
|
||||
@@ -12,7 +12,7 @@ docker compose version
|
||||
如果缺失,重新执行:
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
## 架构不匹配报错
|
||||
@@ -55,11 +55,11 @@ docker run --rm --platform __PACKAGE_PLATFORM__ \
|
||||
|
||||
## RTR report 指标为空
|
||||
|
||||
检查 `.env`:
|
||||
先检查注册状态和 report 目录:
|
||||
|
||||
```bash
|
||||
grep '^RTR_REPORT_DIR=' .env
|
||||
test -d "$(grep '^RTR_REPORT_DIR=' .env | cut -d= -f2-)" && echo ok
|
||||
./scripts/status.sh
|
||||
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
|
||||
```
|
||||
|
||||
检查 metrics 容器是否挂载该目录:
|
||||
@@ -74,7 +74,7 @@ docker inspect "$(grep '^COMPOSE_PROJECT_NAME=' .env | cut -d= -f2-)-artifact-me
|
||||
all5 snapshot 可能很慢,尤其在 QEMU 环境。可以提高超时:
|
||||
|
||||
```bash
|
||||
./start.sh --timeout-secs 14400
|
||||
./scripts/start.sh --timeout-secs 14400
|
||||
```
|
||||
|
||||
## 产物数量异常偏低
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
DRY_RUN=1
|
||||
KEEP_RUNS=""
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: ./cleanup.sh [--execute] [--keep-runs N]
|
||||
Usage: ./scripts/cleanup.sh [--execute] [--keep-runs N]
|
||||
|
||||
By default this is a dry-run. It removes old run_* directories beyond KEEP_RUNS
|
||||
and clears tmp contents.
|
||||
@@ -192,9 +192,9 @@ load_env() {
|
||||
|
||||
HOST_DATA_DIR="${HOST_DATA_DIR:-/var/lib/ours-rp-${PACKAGE_ARCH}-installer}"
|
||||
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ours-rp-${PACKAGE_ARCH}-installer}"
|
||||
RPKI_IMAGE="${RPKI_IMAGE:-ours-rp-runtime-${PACKAGE_ARCH}:dev}"
|
||||
RPKI_IMAGE="${RPKI_IMAGE:-ours-rp-runtime-${PACKAGE_ARCH}:${source_commit_short:-unknown}}"
|
||||
RPKI_PLATFORM="${RPKI_PLATFORM:-$PACKAGE_PLATFORM}"
|
||||
METRICS_IMAGE="${METRICS_IMAGE:-ours-rp-metrics-${PACKAGE_ARCH}:dev}"
|
||||
METRICS_IMAGE="${METRICS_IMAGE:-ours-rp-metrics-${PACKAGE_ARCH}:${source_commit_short:-unknown}}"
|
||||
METRICS_PLATFORM="${METRICS_PLATFORM:-$PACKAGE_PLATFORM}"
|
||||
RTR_REPORT_DIR="${RTR_REPORT_DIR:-$HOST_DATA_DIR/empty-rtr-report}"
|
||||
RTR_REPORT_CONTAINER_DIR="${RTR_REPORT_CONTAINER_DIR:-/var/lib/ours-rp/rtr-report}"
|
||||
@@ -224,6 +224,82 @@ validate_rtr_report_dir() {
|
||||
[[ -d "$RTR_REPORT_DIR" ]] || die "RTR_REPORT_DIR does not exist or is not a directory: $RTR_REPORT_DIR"
|
||||
}
|
||||
|
||||
validate_rtr_registration_dir() {
|
||||
local report_dir="$1"
|
||||
|
||||
[[ -n "$report_dir" ]] || die "--report-dir is required"
|
||||
[[ "$report_dir" = /* ]] || die "--report-dir must be an absolute host path: $report_dir"
|
||||
[[ "$report_dir" =~ ^/[A-Za-z0-9._/@:+,-]+$ ]] || die "--report-dir contains unsupported characters: $report_dir"
|
||||
[[ -d "$report_dir" ]] || die "--report-dir does not exist: $report_dir"
|
||||
[[ -r "$report_dir" ]] || die "--report-dir is not readable: $report_dir"
|
||||
}
|
||||
|
||||
rtr_report_file_count() {
|
||||
local report_dir="$1"
|
||||
{
|
||||
find "$report_dir" -maxdepth 1 -type f \( \
|
||||
-name 'rtr-source-*.json' -o \
|
||||
-name 'rtr-runtime-*.json' -o \
|
||||
-name 'rtr-clients-*.json' \
|
||||
\) -print 2>/dev/null || true
|
||||
} | wc -l | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
latest_rtr_report_file() {
|
||||
local report_dir="$1"
|
||||
{
|
||||
find "$report_dir" -maxdepth 1 -type f \( \
|
||||
-name 'rtr-source-*.json' -o \
|
||||
-name 'rtr-runtime-*.json' -o \
|
||||
-name 'rtr-clients-*.json' \
|
||||
\) -printf '%T@ %p\n' 2>/dev/null || true
|
||||
} | sort -nr | awk 'NR == 1 { sub(/^[^ ]+ /, ""); print }'
|
||||
}
|
||||
|
||||
replace_env_key_file() {
|
||||
local env_path="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local temp_path
|
||||
|
||||
[[ "$key" =~ ^[A-Z][A-Z0-9_]*$ ]] || die "invalid environment key: $key"
|
||||
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value must be single-line"
|
||||
|
||||
temp_path="$(mktemp "${env_path}.tmp.XXXXXX")"
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { replaced = 0 }
|
||||
$0 ~ "^" key "=" {
|
||||
print key "=" value
|
||||
replaced = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!replaced) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "$env_path" > "$temp_path"
|
||||
chmod --reference="$env_path" "$temp_path" 2>/dev/null || true
|
||||
mv "$temp_path" "$env_path"
|
||||
}
|
||||
|
||||
wait_for_endpoint() {
|
||||
local url="$1"
|
||||
local timeout_secs="$2"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout_secs )); do
|
||||
if endpoint_ok "$url"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
create_data_dirs() {
|
||||
load_env
|
||||
mkdir -p \
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: ./install.sh [--skip-dep-install]
|
||||
Usage: ./scripts/install.sh [--skip-dep-install]
|
||||
|
||||
Install or update the ours RP Docker installer package idempotently.
|
||||
USAGE
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
load_env
|
||||
compose_cmd --profile core --profile sidecar --profile monitor logs "$@"
|
||||
149
deploy/docker-installer/scripts/register_rtr_monitor.sh
Executable file
149
deploy/docker-installer/scripts/register_rtr_monitor.sh
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
REPORT_DIR=""
|
||||
DRY_RUN=0
|
||||
CANDIDATE_ENV=""
|
||||
CONFIG_FILE=""
|
||||
ORIGINAL_ENV_FILE=""
|
||||
ENV_BACKUP=""
|
||||
REGISTRATION_APPLIED=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/register_rtr_monitor.sh --report-dir <absolute-host-path> [--dry-run]
|
||||
|
||||
Registers one local RTR report directory for the artifact metrics sidecar.
|
||||
The directory must be on the same host as this installer and may contain
|
||||
rtr-source-*.json, rtr-runtime-*.json, and rtr-clients-*.json report files.
|
||||
|
||||
This command recreates only artifact-metrics, Prometheus, and Grafana. It does
|
||||
not restart or recreate the ours-rp-soak container.
|
||||
EOF
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
[[ -n "$CANDIDATE_ENV" && -f "$CANDIDATE_ENV" ]] && rm -f "$CANDIDATE_ENV"
|
||||
[[ -n "$CONFIG_FILE" && -f "$CONFIG_FILE" ]] && rm -f "$CONFIG_FILE"
|
||||
return 0
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
restore_failed_registration() {
|
||||
if (( REGISTRATION_APPLIED == 0 )) || [[ -z "$ENV_BACKUP" || ! -f "$ENV_BACKUP" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "registration failed after .env replacement; restoring previous metrics configuration"
|
||||
cp -p "$ENV_BACKUP" "$ORIGINAL_ENV_FILE"
|
||||
ENV_FILE="$ORIGINAL_ENV_FILE"
|
||||
load_env || warn "unable to reload restored .env"
|
||||
compose_cmd --profile sidecar up -d --force-recreate artifact-metrics \
|
||||
|| warn "unable to recreate artifact-metrics with restored configuration"
|
||||
}
|
||||
|
||||
fail_after_apply() {
|
||||
restore_failed_registration
|
||||
die "$*"
|
||||
}
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--report-dir)
|
||||
[[ $# -ge 2 ]] || die "--report-dir requires a value"
|
||||
REPORT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_cmd docker
|
||||
require_cmd curl
|
||||
require_cmd grep
|
||||
require_cmd jq
|
||||
validate_rtr_registration_dir "$REPORT_DIR"
|
||||
load_env
|
||||
assert_arch_compatibility
|
||||
|
||||
ORIGINAL_ENV_FILE="$ENV_FILE"
|
||||
REPORT_FILE_COUNT="$(rtr_report_file_count "$REPORT_DIR")"
|
||||
LATEST_REPORT_FILE="$(latest_rtr_report_file "$REPORT_DIR")"
|
||||
if (( REPORT_FILE_COUNT == 0 )); then
|
||||
warn "no recognized RTR report JSON files are currently present in $REPORT_DIR"
|
||||
fi
|
||||
|
||||
CANDIDATE_ENV="$(mktemp "$INSTALLER_ROOT/.env.rtr-register.XXXXXX")"
|
||||
cp -p "$ORIGINAL_ENV_FILE" "$CANDIDATE_ENV"
|
||||
replace_env_key_file "$CANDIDATE_ENV" "RTR_REPORT_DIR" "$REPORT_DIR"
|
||||
|
||||
ENV_FILE="$CANDIDATE_ENV"
|
||||
load_env
|
||||
CONFIG_FILE="$(mktemp "$INSTALLER_ROOT/.rtr-compose-config.XXXXXX")"
|
||||
compose_cmd --profile sidecar --profile monitor config --format json > "$CONFIG_FILE"
|
||||
jq -e --arg source "$REPORT_DIR" --arg target "$RTR_REPORT_CONTAINER_DIR" '
|
||||
.services["artifact-metrics"].volumes
|
||||
| any(.[]; .type == "bind" and .source == $source and .target == $target and .read_only == true)
|
||||
' "$CONFIG_FILE" >/dev/null || die "candidate compose configuration is missing expected read-only RTR bind mount"
|
||||
|
||||
if (( DRY_RUN == 1 )); then
|
||||
printf 'dry_run=ok\n'
|
||||
printf 'report_dir=%s\n' "$REPORT_DIR"
|
||||
printf 'recognized_report_files=%s\n' "$REPORT_FILE_COUNT"
|
||||
printf 'latest_report=%s\n' "${LATEST_REPORT_FILE:--}"
|
||||
printf 'expected_mount_source=%s\n' "$REPORT_DIR"
|
||||
printf 'expected_mount_target=%s\n' "$RTR_REPORT_CONTAINER_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ENV_FILE="$ORIGINAL_ENV_FILE"
|
||||
load_env
|
||||
soak_container_before="$(compose_cmd ps -q ours-rp-soak || true)"
|
||||
ENV_BACKUP="${ORIGINAL_ENV_FILE}.rtr-register-$(date -u +%Y%m%dT%H%M%SZ).bak"
|
||||
cp -p "$ORIGINAL_ENV_FILE" "$ENV_BACKUP"
|
||||
mv "$CANDIDATE_ENV" "$ORIGINAL_ENV_FILE"
|
||||
CANDIDATE_ENV=""
|
||||
REGISTRATION_APPLIED=1
|
||||
|
||||
load_env
|
||||
compose_cmd --profile sidecar up -d --force-recreate artifact-metrics \
|
||||
|| fail_after_apply "unable to recreate artifact-metrics"
|
||||
compose_cmd --profile sidecar --profile monitor up -d prometheus grafana \
|
||||
|| fail_after_apply "unable to start Prometheus and Grafana"
|
||||
|
||||
soak_container_after="$(compose_cmd ps -q ours-rp-soak || true)"
|
||||
if [[ -n "$soak_container_before" && "$soak_container_before" != "$soak_container_after" ]]; then
|
||||
fail_after_apply "ours-rp-soak container changed during RTR registration; investigate before continuing"
|
||||
fi
|
||||
|
||||
wait_for_endpoint "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" 60 \
|
||||
|| fail_after_apply "artifact-metrics did not become ready"
|
||||
wait_for_endpoint "http://127.0.0.1:${PROMETHEUS_PORT:-9090}/-/ready" 60 \
|
||||
|| fail_after_apply "Prometheus did not become ready"
|
||||
wait_for_endpoint "http://127.0.0.1:${GRAFANA_PORT:-3000}/api/health" 60 \
|
||||
|| fail_after_apply "Grafana did not become ready"
|
||||
|
||||
if (( REPORT_FILE_COUNT > 0 )); then
|
||||
curl -fsS "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" | grep -q '^ours_rp_rtr_' \
|
||||
|| fail_after_apply "artifact-metrics exposes no RTR metrics after registration"
|
||||
fi
|
||||
|
||||
printf 'registration=ok\n'
|
||||
printf 'report_dir=%s\n' "$REPORT_DIR"
|
||||
printf 'recognized_report_files=%s\n' "$REPORT_FILE_COUNT"
|
||||
printf 'latest_report=%s\n' "${LATEST_REPORT_FILE:--}"
|
||||
printf 'env_backup=%s\n' "$ENV_BACKUP"
|
||||
printf 'ours_rp_soak_container_unchanged=%s\n' "${soak_container_before:-not-running}"
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
QUICK=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -12,7 +12,7 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: ./self-check.sh [--quick]"
|
||||
echo "Usage: ./scripts/self-check.sh [--quick]"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
WAIT_FIRST_RUN=1
|
||||
TIMEOUT_SECS=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: ./start.sh [--no-wait-first-run] [--timeout-secs N]
|
||||
Usage: ./scripts/start.sh [--no-wait-first-run] [--timeout-secs N]
|
||||
|
||||
Start ours RP. If no successful run exists, wait for the first snapshot to succeed
|
||||
before starting metrics, Prometheus and Grafana.
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
BRIEF=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -12,7 +12,7 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: ./status.sh [--brief]"
|
||||
echo "Usage: ./scripts/status.sh [--brief]"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -26,6 +26,11 @@ host_arch="$(detect_host_arch)"
|
||||
mode="$(arch_mode)"
|
||||
echo "installer_root=$INSTALLER_ROOT"
|
||||
echo "manifest_file=$MANIFEST_FILE"
|
||||
echo "package_name=${package_name:-}"
|
||||
echo "source_commit=${source_commit:-${git_commit:-}}"
|
||||
echo "source_commit_short=${source_commit_short:-}"
|
||||
echo "source_dirty=${source_dirty:-}"
|
||||
echo "build_timestamp_utc=${build_timestamp_utc:-${created_at_utc:-}}"
|
||||
echo "host_data_dir=$HOST_DATA_DIR"
|
||||
echo "host_arch=$host_arch"
|
||||
echo "package_arch=$PACKAGE_ARCH"
|
||||
@@ -34,8 +39,12 @@ echo "allow_cross_arch=$ALLOW_CROSS_ARCH"
|
||||
echo "arch_mode=$mode"
|
||||
echo "runtime_image=$RPKI_IMAGE"
|
||||
echo "runtime_platform=$RPKI_PLATFORM"
|
||||
echo "runtime_image_revision=${runtime_image_revision:-}"
|
||||
echo "runtime_image_dirty=${runtime_image_dirty:-}"
|
||||
echo "metrics_image=$METRICS_IMAGE"
|
||||
echo "metrics_platform=$METRICS_PLATFORM"
|
||||
echo "metrics_image_revision=${metrics_image_revision:-}"
|
||||
echo "metrics_image_dirty=${metrics_image_dirty:-}"
|
||||
echo "rtr_report_dir=$RTR_REPORT_DIR"
|
||||
echo "rtr_report_container_dir=$RTR_REPORT_CONTAINER_DIR"
|
||||
echo "monitor_platform=$MONITOR_PLATFORM"
|
||||
@@ -67,4 +76,12 @@ if [[ "$BRIEF" == "0" ]]; then
|
||||
endpoint_ok "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" && echo "metrics=ok" || echo "metrics=unavailable"
|
||||
endpoint_ok "http://127.0.0.1:${PROMETHEUS_PORT:-9090}/-/ready" && echo "prometheus=ok" || echo "prometheus=unavailable"
|
||||
endpoint_ok "http://127.0.0.1:${GRAFANA_PORT:-3000}/api/health" && echo "grafana=ok" || echo "grafana=unavailable"
|
||||
echo
|
||||
if [[ "$RTR_REPORT_DIR" == "$HOST_DATA_DIR/empty-rtr-report" ]]; then
|
||||
echo "rtr_registration=fallback-empty-directory"
|
||||
else
|
||||
echo "rtr_registration=registered"
|
||||
fi
|
||||
echo "rtr_report_files=$(rtr_report_file_count "$RTR_REPORT_DIR")"
|
||||
echo "rtr_latest_report=$(latest_rtr_report_file "$RTR_REPORT_DIR" || true)"
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
load_env
|
||||
compose_cmd --profile core --profile sidecar --profile monitor stop "$@"
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
PURGE_DATA=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -12,7 +12,7 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: ./uninstall.sh [--purge-data]"
|
||||
echo "Usage: ./scripts/uninstall.sh [--purge-data]"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/scripts/common.sh"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
REUSE_ENV_FROM=""
|
||||
UPDATE_PACKAGE_IMAGE=1
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: ./upgrade.sh [--reuse-env-from /path/to/.env] [--keep-reused-image]
|
||||
Usage: ./scripts/upgrade.sh [--reuse-env-from /path/to/.env] [--keep-reused-image]
|
||||
|
||||
By default, --reuse-env-from creates the new .env from this package's
|
||||
.env.example first, then overlays existing user settings from the old .env.
|
||||
@@ -87,9 +87,6 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
|
||||
FROM --platform=$TARGETPLATFORM ${RUNTIME_IMAGE} AS runtime
|
||||
|
||||
LABEL org.opencontainers.image.title="ours-rp-metrics" \
|
||||
org.opencontainers.image.description="Ours RP artifact metrics image for multi-arch Docker Compose deployment"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@@ -100,6 +97,16 @@ WORKDIR /opt/ours-rp
|
||||
|
||||
COPY --from=builder /build-out/bin/ /opt/ours-rp/bin/
|
||||
|
||||
ARG SOURCE_COMMIT=unknown
|
||||
ARG SOURCE_DIRTY=unknown
|
||||
ARG BUILD_TIMESTAMP_UTC=unknown
|
||||
|
||||
LABEL org.opencontainers.image.title="ours-rp-metrics" \
|
||||
org.opencontainers.image.description="Ours RP artifact metrics image for multi-arch Docker Compose deployment" \
|
||||
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.created="${BUILD_TIMESTAMP_UTC}" \
|
||||
org.opencontainers.image.source-dirty="${SOURCE_DIRTY}"
|
||||
|
||||
RUN chmod +x /opt/ours-rp/bin/* \
|
||||
&& mkdir -p /var/lib/ours-rp/state /var/lib/ours-rp/runs /var/lib/ours-rp/logs
|
||||
|
||||
|
||||
@@ -94,9 +94,6 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
|
||||
FROM --platform=$TARGETPLATFORM ${RUNTIME_IMAGE} AS runtime
|
||||
|
||||
LABEL org.opencontainers.image.title="ours-rp-runtime" \
|
||||
org.opencontainers.image.description="Ours RP runtime image for multi-arch Docker Compose deployment"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
@@ -123,6 +120,16 @@ COPY tests/fixtures/ta/ /opt/ours-rp/fixtures/ta/
|
||||
COPY fixtures/live_20260619/tal/ /opt/ours-rp/fixtures/live_20260619/tal/
|
||||
COPY fixtures/live_20260619/ta/ /opt/ours-rp/fixtures/live_20260619/ta/
|
||||
|
||||
ARG SOURCE_COMMIT=unknown
|
||||
ARG SOURCE_DIRTY=unknown
|
||||
ARG BUILD_TIMESTAMP_UTC=unknown
|
||||
|
||||
LABEL org.opencontainers.image.title="ours-rp-runtime" \
|
||||
org.opencontainers.image.description="Ours RP runtime image for multi-arch Docker Compose deployment" \
|
||||
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.created="${BUILD_TIMESTAMP_UTC}" \
|
||||
org.opencontainers.image.source-dirty="${SOURCE_DIRTY}"
|
||||
|
||||
RUN chmod +x /opt/ours-rp/run_soak.sh /opt/ours-rp/bin/* \
|
||||
&& mkdir -p /var/lib/ours-rp/state /var/lib/ours-rp/runs /var/lib/ours-rp/logs /var/lib/ours-rp/tmp
|
||||
|
||||
|
||||
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"
|
||||
@@ -5,10 +5,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
TARGET_ARCH="${TARGET_ARCH:-}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-}"
|
||||
IMAGE_TAR="${IMAGE_TAR:-}"
|
||||
METRICS_IMAGE="${METRICS_IMAGE:-}"
|
||||
METRICS_IMAGE_TAR="${METRICS_IMAGE_TAR:-}"
|
||||
IMAGE_TAG=""
|
||||
IMAGE_TAR=""
|
||||
METRICS_IMAGE=""
|
||||
METRICS_IMAGE_TAR=""
|
||||
PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}"
|
||||
PROMETHEUS_IMAGE_TAR="${PROMETHEUS_IMAGE_TAR:-}"
|
||||
GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}"
|
||||
@@ -16,6 +16,7 @@ GRAFANA_IMAGE_TAR="${GRAFANA_IMAGE_TAR:-}"
|
||||
OUT_DIR="${OUT_DIR:-}"
|
||||
PACKAGE_PREFIX="${PACKAGE_PREFIX:-}"
|
||||
TEMPLATE_DIR="${TEMPLATE_DIR:-$REPO_ROOT/deploy/docker-installer}"
|
||||
ALLOW_DIRTY=0
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
@@ -24,12 +25,6 @@ Usage:
|
||||
|
||||
Options:
|
||||
--arch <arch> Target architecture: amd64|arm64
|
||||
--image <tag> Runtime image tag recorded in package manifest.
|
||||
--image-tar <path> Existing docker save tar/tar.gz to include.
|
||||
--metrics-image <tag>
|
||||
Metrics image tag recorded in package manifest.
|
||||
--metrics-image-tar <path>
|
||||
Existing metrics docker save tar/tar.gz to include.
|
||||
--prometheus-image <tag>
|
||||
Prometheus image tag to record and package.
|
||||
--prometheus-image-tar <path>
|
||||
@@ -42,10 +37,13 @@ Options:
|
||||
--prefix <name> Package directory/tar prefix.
|
||||
--template-dir <path>
|
||||
Package template directory.
|
||||
--allow-dirty Allow a development build from a dirty worktree and mark
|
||||
image tags, package name and manifest as dirty.
|
||||
-h, --help Show help.
|
||||
|
||||
If --image-tar or --metrics-image-tar is omitted, the script uses the
|
||||
matching docker-save archive under target/<arch>-docker/.
|
||||
Every package rebuilds runtime and metrics images from the current HEAD. Their
|
||||
tags are ours-rp-runtime-<arch>:<git8> and ours-rp-metrics-<arch>:<git8>.
|
||||
External runtime/metrics image tags and tar archives are intentionally rejected.
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -71,16 +69,20 @@ safe_tag_name() {
|
||||
printf '%s' "$1" | tr '/:' '--'
|
||||
}
|
||||
|
||||
default_runtime_tag() {
|
||||
printf 'ours-rp-runtime-%s:dev\n' "$1"
|
||||
runtime_image_tag() {
|
||||
local arch="$1"
|
||||
local revision="$2"
|
||||
printf 'ours-rp-runtime-%s:%s\n' "$arch" "$revision"
|
||||
}
|
||||
|
||||
default_metrics_tag() {
|
||||
printf 'ours-rp-metrics-%s:dev\n' "$1"
|
||||
metrics_image_tag() {
|
||||
local arch="$1"
|
||||
local revision="$2"
|
||||
printf 'ours-rp-metrics-%s:%s\n' "$arch" "$revision"
|
||||
}
|
||||
|
||||
default_package_prefix() {
|
||||
printf 'ours-rp-docker-installer-%s\n' "$1"
|
||||
printf 'ours-rp-installer-%s\n' "$1"
|
||||
}
|
||||
|
||||
default_host_data_dir() {
|
||||
@@ -163,20 +165,10 @@ save_image_if_needed() {
|
||||
printf '%s\n' "$tar_path"
|
||||
}
|
||||
|
||||
default_saved_image_tar() {
|
||||
image_label() {
|
||||
local image="$1"
|
||||
local out_dir="$REPO_ROOT/target/${TARGET_ARCH}-docker"
|
||||
local safe_name
|
||||
safe_name="$(safe_tag_name "$image")"
|
||||
if [[ -f "$out_dir/${safe_name}.tar.gz" ]]; then
|
||||
printf '%s\n' "$out_dir/${safe_name}.tar.gz"
|
||||
return 0
|
||||
fi
|
||||
if [[ -f "$out_dir/${safe_name}.tar" ]]; then
|
||||
printf '%s\n' "$out_dir/${safe_name}.tar"
|
||||
return 0
|
||||
fi
|
||||
printf '\n'
|
||||
local key="$2"
|
||||
docker image inspect --format "{{ index .Config.Labels \"$key\" }}" "$image"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -188,21 +180,9 @@ while [[ $# -gt 0 ]]; do
|
||||
}
|
||||
shift 2
|
||||
;;
|
||||
--image)
|
||||
IMAGE_TAG="$2"
|
||||
shift 2
|
||||
;;
|
||||
--image-tar)
|
||||
IMAGE_TAR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--metrics-image)
|
||||
METRICS_IMAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--metrics-image-tar)
|
||||
METRICS_IMAGE_TAR="$2"
|
||||
shift 2
|
||||
--image|--image-tar|--metrics-image|--metrics-image-tar)
|
||||
echo "$1 is no longer supported: installer packages always rebuild runtime and metrics images from current HEAD" >&2
|
||||
exit 2
|
||||
;;
|
||||
--prometheus-image)
|
||||
PROMETHEUS_IMAGE="$2"
|
||||
@@ -232,6 +212,10 @@ while [[ $# -gt 0 ]]; do
|
||||
TEMPLATE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--allow-dirty)
|
||||
ALLOW_DIRTY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -255,12 +239,28 @@ done
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ -z "$IMAGE_TAG" ]]; then
|
||||
IMAGE_TAG="$(default_runtime_tag "$TARGET_ARCH")"
|
||||
SOURCE_COMMIT_FULL="$(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 [[ -z "$METRICS_IMAGE" ]]; then
|
||||
METRICS_IMAGE="$(default_metrics_tag "$TARGET_ARCH")"
|
||||
if [[ "$SOURCE_DIRTY" == "true" && "$ALLOW_DIRTY" != "1" ]]; then
|
||||
cat >&2 <<EOF
|
||||
refusing to build a release installer from a dirty worktree.
|
||||
source_commit: $SOURCE_COMMIT_FULL
|
||||
Use --allow-dirty only for development verification; the package and image tags
|
||||
will be explicitly marked dirty.
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
REVISION_TOKEN="$SOURCE_COMMIT_SHORT"
|
||||
if [[ "$SOURCE_DIRTY" == "true" ]]; then
|
||||
REVISION_TOKEN="${REVISION_TOKEN}-dirty"
|
||||
fi
|
||||
IMAGE_TAG="$(runtime_image_tag "$TARGET_ARCH" "$REVISION_TOKEN")"
|
||||
METRICS_IMAGE="$(metrics_image_tag "$TARGET_ARCH" "$REVISION_TOKEN")"
|
||||
if [[ -z "$OUT_DIR" ]]; then
|
||||
OUT_DIR="$REPO_ROOT/target/${TARGET_ARCH}-installer"
|
||||
fi
|
||||
@@ -272,37 +272,35 @@ TARGET_PLATFORM="$(platform_for_arch "$TARGET_ARCH")"
|
||||
HOST_DATA_DIR_DEFAULT="$(default_host_data_dir "$TARGET_ARCH")"
|
||||
COMPOSE_PROJECT_DEFAULT="$(default_compose_project "$TARGET_ARCH")"
|
||||
METRICS_INSTANCE_DEFAULT="$(default_metrics_instance "$TARGET_ARCH")"
|
||||
|
||||
if [[ -z "$IMAGE_TAR" ]]; then
|
||||
IMAGE_TAR="$(default_saved_image_tar "$IMAGE_TAG")"
|
||||
fi
|
||||
if [[ -z "$METRICS_IMAGE_TAR" ]]; then
|
||||
METRICS_IMAGE_TAR="$(default_saved_image_tar "$METRICS_IMAGE")"
|
||||
fi
|
||||
|
||||
[[ -n "$IMAGE_TAR" && -f "$IMAGE_TAR" ]] || {
|
||||
cat >&2 <<EOF
|
||||
missing runtime image tar for $TARGET_ARCH.
|
||||
|
||||
Build one first, for example:
|
||||
scripts/docker/build_docker_runtime_image.sh --arch $TARGET_ARCH --image $IMAGE_TAG
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
[[ -n "$METRICS_IMAGE_TAR" && -f "$METRICS_IMAGE_TAR" ]] || {
|
||||
cat >&2 <<EOF
|
||||
missing metrics image tar for $TARGET_ARCH.
|
||||
|
||||
Build one first, for example:
|
||||
scripts/docker/build_docker_metrics_image.sh --arch $TARGET_ARCH --image $METRICS_IMAGE
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
IMAGE_OUT_DIR="$REPO_ROOT/target/${TARGET_ARCH}-docker"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
commit="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
package_name="${PACKAGE_PREFIX}-${timestamp}-${commit}"
|
||||
echo "rebuilding runtime image: $IMAGE_TAG"
|
||||
"$SCRIPT_DIR/build_docker_runtime_image.sh" \
|
||||
--arch "$TARGET_ARCH" \
|
||||
--image "$IMAGE_TAG" \
|
||||
--out-dir "$IMAGE_OUT_DIR"
|
||||
echo "rebuilding metrics image: $METRICS_IMAGE"
|
||||
"$SCRIPT_DIR/build_docker_metrics_image.sh" \
|
||||
--arch "$TARGET_ARCH" \
|
||||
--image "$METRICS_IMAGE" \
|
||||
--out-dir "$IMAGE_OUT_DIR"
|
||||
|
||||
IMAGE_TAR="$IMAGE_OUT_DIR/$(safe_tag_name "$IMAGE_TAG").tar.gz"
|
||||
METRICS_IMAGE_TAR="$IMAGE_OUT_DIR/$(safe_tag_name "$METRICS_IMAGE").tar.gz"
|
||||
[[ -f "$IMAGE_TAR" ]] || { echo "runtime image build did not produce $IMAGE_TAR" >&2; exit 2; }
|
||||
[[ -f "$METRICS_IMAGE_TAR" ]] || { echo "metrics image build did not produce $METRICS_IMAGE_TAR" >&2; exit 2; }
|
||||
|
||||
runtime_image_revision="$(image_label "$IMAGE_TAG" "org.opencontainers.image.revision")"
|
||||
metrics_image_revision="$(image_label "$METRICS_IMAGE" "org.opencontainers.image.revision")"
|
||||
runtime_image_dirty="$(image_label "$IMAGE_TAG" "org.opencontainers.image.source-dirty")"
|
||||
metrics_image_dirty="$(image_label "$METRICS_IMAGE" "org.opencontainers.image.source-dirty")"
|
||||
[[ "$runtime_image_revision" == "$SOURCE_COMMIT_FULL" ]] || { echo "runtime image revision mismatch: $runtime_image_revision != $SOURCE_COMMIT_FULL" >&2; exit 2; }
|
||||
[[ "$metrics_image_revision" == "$SOURCE_COMMIT_FULL" ]] || { echo "metrics image revision mismatch: $metrics_image_revision != $SOURCE_COMMIT_FULL" >&2; exit 2; }
|
||||
[[ "$runtime_image_dirty" == "$SOURCE_DIRTY" ]] || { echo "runtime image dirty flag mismatch" >&2; exit 2; }
|
||||
[[ "$metrics_image_dirty" == "$SOURCE_DIRTY" ]] || { echo "metrics image dirty flag mismatch" >&2; exit 2; }
|
||||
|
||||
package_name="${PACKAGE_PREFIX}-${REVISION_TOKEN}-${TIMESTAMP}"
|
||||
stage="$OUT_DIR/$package_name"
|
||||
tar_path="$OUT_DIR/$package_name.tar.gz"
|
||||
|
||||
@@ -312,7 +310,7 @@ mkdir -p "$stage/images"
|
||||
cp "$IMAGE_TAR" "$stage/images/"
|
||||
cp "$METRICS_IMAGE_TAR" "$stage/images/"
|
||||
|
||||
monitor_image_stage="$OUT_DIR/.monitor-images-$TARGET_ARCH-$timestamp"
|
||||
monitor_image_stage="$OUT_DIR/.monitor-images-$TARGET_ARCH-$TIMESTAMP"
|
||||
rm -rf "$monitor_image_stage"
|
||||
mkdir -p "$monitor_image_stage"
|
||||
prometheus_tar="$(save_image_if_needed "$PROMETHEUS_IMAGE" "$PROMETHEUS_IMAGE_TAR" "$monitor_image_stage" "prometheus" "$TARGET_PLATFORM")"
|
||||
@@ -354,6 +352,10 @@ replace_text_in_file "$stage/docs/operations.zh-CN.md" "__HOST_DATA_DIR__" "$HOS
|
||||
replace_text_in_file "$stage/docs/operations.en.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
|
||||
replace_text_in_file "$stage/docs/troubleshooting.zh-CN.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
|
||||
replace_text_in_file "$stage/docs/troubleshooting.en.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
|
||||
replace_text_in_file "$stage/docs/README.zh-CN.md" "__RUNTIME_IMAGE__" "$IMAGE_TAG"
|
||||
replace_text_in_file "$stage/docs/README.en.md" "__RUNTIME_IMAGE__" "$IMAGE_TAG"
|
||||
replace_text_in_file "$stage/docs/README.zh-CN.md" "__METRICS_IMAGE__" "$METRICS_IMAGE"
|
||||
replace_text_in_file "$stage/docs/README.en.md" "__METRICS_IMAGE__" "$METRICS_IMAGE"
|
||||
|
||||
runtime_tar_sha256="$(sha256sum "$IMAGE_TAR" | awk '{print $1}')"
|
||||
metrics_tar_sha256="$(sha256sum "$METRICS_IMAGE_TAR" | awk '{print $1}')"
|
||||
@@ -361,9 +363,14 @@ prometheus_tar_sha256="$(sha256sum "$prometheus_tar" | awk '{print $1}')"
|
||||
grafana_tar_sha256="$(sha256sum "$grafana_tar" | awk '{print $1}')"
|
||||
|
||||
cat > "$stage/PACKAGE-MANIFEST.env" <<EOF
|
||||
package_schema_version=2
|
||||
package_name=$package_name
|
||||
created_at_utc=$timestamp
|
||||
git_commit=$commit
|
||||
created_at_utc=$TIMESTAMP
|
||||
build_timestamp_utc=$TIMESTAMP
|
||||
source_commit=$SOURCE_COMMIT_FULL
|
||||
source_commit_short=$SOURCE_COMMIT_SHORT
|
||||
source_dirty=$SOURCE_DIRTY
|
||||
git_commit=$SOURCE_COMMIT_FULL
|
||||
git_status_count=$(git -C "$REPO_ROOT" status --short 2>/dev/null | wc -l | tr -d ' ')
|
||||
PACKAGE_ARCH=$TARGET_ARCH
|
||||
PACKAGE_PLATFORM=$TARGET_PLATFORM
|
||||
@@ -377,6 +384,9 @@ image_tag=$IMAGE_TAG
|
||||
image_tar=$(basename "$IMAGE_TAR")
|
||||
image_tar_size_bytes=$(wc -c < "$IMAGE_TAR")
|
||||
image_tar_sha256=$runtime_tar_sha256
|
||||
runtime_image=$IMAGE_TAG
|
||||
runtime_image_revision=$runtime_image_revision
|
||||
runtime_image_dirty=$runtime_image_dirty
|
||||
rpki_image=$IMAGE_TAG
|
||||
rpki_platform=$TARGET_PLATFORM
|
||||
METRICS_IMAGE=$METRICS_IMAGE
|
||||
@@ -385,6 +395,8 @@ metrics_image=$METRICS_IMAGE
|
||||
metrics_image_tar=$(basename "$METRICS_IMAGE_TAR")
|
||||
metrics_image_tar_size_bytes=$(wc -c < "$METRICS_IMAGE_TAR")
|
||||
metrics_image_tar_sha256=$metrics_tar_sha256
|
||||
metrics_image_revision=$metrics_image_revision
|
||||
metrics_image_dirty=$metrics_image_dirty
|
||||
PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE
|
||||
prometheus_image=$PROMETHEUS_IMAGE
|
||||
prometheus_image_tar=$(basename "$prometheus_tar")
|
||||
@@ -407,10 +419,16 @@ EOF
|
||||
|
||||
cat > "$stage/PACKAGE-SUMMARY.txt" <<EOF
|
||||
package_name: $package_name
|
||||
source_commit: $SOURCE_COMMIT_FULL
|
||||
source_commit_short: $SOURCE_COMMIT_SHORT
|
||||
source_dirty: $SOURCE_DIRTY
|
||||
build_timestamp_utc: $TIMESTAMP
|
||||
package_arch: $TARGET_ARCH
|
||||
package_platform: $TARGET_PLATFORM
|
||||
runtime_image: $IMAGE_TAG
|
||||
runtime_image_revision: $runtime_image_revision
|
||||
metrics_image: $METRICS_IMAGE
|
||||
metrics_image_revision: $metrics_image_revision
|
||||
prometheus_image: $PROMETHEUS_IMAGE
|
||||
grafana_image: $GRAFANA_IMAGE
|
||||
default_host_data_dir: $HOST_DATA_DIR_DEFAULT
|
||||
@@ -419,7 +437,12 @@ cross_arch_default: deny
|
||||
cross_arch_enable_var: ALLOW_CROSS_ARCH
|
||||
EOF
|
||||
|
||||
chmod +x "$stage"/*.sh "$stage/scripts"/*.sh
|
||||
chmod +x "$stage/scripts"/*.sh
|
||||
|
||||
if find "$stage" -maxdepth 1 -type f -name '*.sh' -print -quit | grep -q .; then
|
||||
echo "error: operational scripts must be installed under scripts/ only" >&2
|
||||
exit 1
|
||||
fi
|
||||
tar -C "$OUT_DIR" -czf "$tar_path" "$package_name"
|
||||
package_sha256="$(sha256sum "$tar_path" | awk '{print $1}')"
|
||||
rm -rf "$monitor_image_stage"
|
||||
@@ -429,6 +452,10 @@ rm -rf "$monitor_image_stage"
|
||||
echo "package_dir=$stage"
|
||||
echo "package_size_bytes=$(wc -c < "$tar_path")"
|
||||
echo "package_sha256=$package_sha256"
|
||||
echo "source_commit=$SOURCE_COMMIT_FULL"
|
||||
echo "source_commit_short=$SOURCE_COMMIT_SHORT"
|
||||
echo "source_dirty=$SOURCE_DIRTY"
|
||||
echo "build_timestamp_utc=$TIMESTAMP"
|
||||
echo "package_arch=$TARGET_ARCH"
|
||||
echo "package_platform=$TARGET_PLATFORM"
|
||||
echo "manifest=$stage/PACKAGE-MANIFEST.env"
|
||||
|
||||
@@ -65,7 +65,11 @@ EOF
|
||||
fi
|
||||
|
||||
if [[ -z "$IMAGE_TAG" ]]; then
|
||||
IMAGE_TAG="ours-rp-metrics-${TARGET_ARCH}:dev"
|
||||
SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 HEAD 2>/dev/null || echo unknown)"
|
||||
if [[ -n "$(git -C "$REPO_ROOT" status --short 2>/dev/null)" ]]; then
|
||||
SOURCE_COMMIT_SHORT="${SOURCE_COMMIT_SHORT}-dirty"
|
||||
fi
|
||||
IMAGE_TAG="ours-rp-metrics-${TARGET_ARCH}:${SOURCE_COMMIT_SHORT}"
|
||||
fi
|
||||
|
||||
if [[ "$dockerfile_explicit" != "1" ]]; then
|
||||
|
||||
@@ -22,7 +22,7 @@ Usage:
|
||||
|
||||
Options:
|
||||
--arch <arch> Target architecture: amd64|arm64
|
||||
--image <tag> Docker image tag (default: ours-rp-runtime-<arch>:dev)
|
||||
--image <tag> Docker image tag (default: ours-rp-runtime-<arch>:<git8>)
|
||||
--out-dir <path> Directory for docker save tar.gz (default: target/<arch>-docker)
|
||||
--dockerfile <path> Dockerfile path
|
||||
--builder <name> buildx builder name
|
||||
@@ -55,12 +55,21 @@ safe_tag_name() {
|
||||
printf '%s' "$1" | tr '/:' '--'
|
||||
}
|
||||
|
||||
local_base_image_tag() {
|
||||
local role="$1"
|
||||
local arch="$2"
|
||||
local source_image="$3"
|
||||
printf 'ours-rp-base-%s-%s:%s\n' "$role" "$arch" "$(safe_tag_name "$source_image")"
|
||||
}
|
||||
|
||||
platform_for_arch() {
|
||||
printf 'linux/%s\n' "$1"
|
||||
}
|
||||
|
||||
default_image_tag() {
|
||||
printf 'ours-rp-runtime-%s:dev\n' "$1"
|
||||
local arch="$1"
|
||||
local commit_short="$2"
|
||||
printf 'ours-rp-runtime-%s:%s\n' "$arch" "$commit_short"
|
||||
}
|
||||
|
||||
require_command() {
|
||||
@@ -142,8 +151,20 @@ done
|
||||
exit 2
|
||||
}
|
||||
|
||||
SOURCE_COMMIT_FULL="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 HEAD 2>/dev/null || echo unknown)"
|
||||
SOURCE_DIRTY="false"
|
||||
if [[ -n "$(git -C "$REPO_ROOT" status --short 2>/dev/null)" ]]; then
|
||||
SOURCE_DIRTY="true"
|
||||
fi
|
||||
BUILD_TIMESTAMP_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
if [[ -z "$IMAGE_TAG" ]]; then
|
||||
IMAGE_TAG="$(default_image_tag "$TARGET_ARCH")"
|
||||
default_revision="$SOURCE_COMMIT_SHORT"
|
||||
if [[ "$SOURCE_DIRTY" == "true" ]]; then
|
||||
default_revision="${default_revision}-dirty"
|
||||
fi
|
||||
IMAGE_TAG="$(default_image_tag "$TARGET_ARCH" "$default_revision")"
|
||||
fi
|
||||
if [[ -z "$OUT_DIR" ]]; then
|
||||
OUT_DIR="$REPO_ROOT/target/${TARGET_ARCH}-docker"
|
||||
@@ -154,12 +175,29 @@ mkdir -p "$OUT_DIR"
|
||||
|
||||
target_platform="$(platform_for_arch "$TARGET_ARCH")"
|
||||
local_host_arch="$(host_arch)"
|
||||
builder_platform="$(platform_for_arch "$local_host_arch")"
|
||||
builder_source_image="$BUILDER_IMAGE"
|
||||
runtime_source_image="$RUNTIME_IMAGE"
|
||||
builder_local_image="$(local_base_image_tag builder "$local_host_arch" "$builder_source_image")"
|
||||
runtime_local_image="$(local_base_image_tag runtime "$TARGET_ARCH" "$runtime_source_image")"
|
||||
|
||||
if [[ "$INSTALL_BINFMT" == "1" && "$local_host_arch" != "$TARGET_ARCH" ]]; then
|
||||
echo "installing binfmt/qemu for $TARGET_ARCH"
|
||||
docker run --rm --privileged tonistiigi/binfmt --install "$TARGET_ARCH"
|
||||
fi
|
||||
|
||||
# Pulling through the Docker daemon uses its configured proxy. Buildx then consumes
|
||||
# these architecture-pinned local aliases without issuing separate registry metadata
|
||||
# requests for the Dockerfile FROM instructions.
|
||||
echo "pulling builder base image: $builder_source_image ($builder_platform)"
|
||||
docker pull --platform "$builder_platform" "$builder_source_image"
|
||||
docker tag "$builder_source_image" "$builder_local_image"
|
||||
echo "pulling runtime base image: $runtime_source_image ($target_platform)"
|
||||
docker pull --platform "$target_platform" "$runtime_source_image"
|
||||
docker tag "$runtime_source_image" "$runtime_local_image"
|
||||
BUILDER_IMAGE="$builder_local_image"
|
||||
RUNTIME_IMAGE="$runtime_local_image"
|
||||
|
||||
if [[ "$BUILDER_NAME" != "default" ]] && ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
|
||||
docker buildx create --name "$BUILDER_NAME" --driver docker-container --use >/dev/null
|
||||
else
|
||||
@@ -174,18 +212,27 @@ build_log="$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").build.log"
|
||||
echo "building $target_platform image: $IMAGE_TAG"
|
||||
echo "repo: $REPO_ROOT"
|
||||
echo "dockerfile: $DOCKERFILE"
|
||||
echo "builder_image: $BUILDER_IMAGE"
|
||||
echo "runtime_image: $RUNTIME_IMAGE"
|
||||
echo "builder_source_image: $builder_source_image"
|
||||
echo "runtime_source_image: $runtime_source_image"
|
||||
echo "builder_local_image: $BUILDER_IMAGE"
|
||||
echo "runtime_local_image: $RUNTIME_IMAGE"
|
||||
echo "source_commit: $SOURCE_COMMIT_FULL"
|
||||
echo "source_dirty: $SOURCE_DIRTY"
|
||||
echo "build_timestamp_utc: $BUILD_TIMESTAMP_UTC"
|
||||
start_epoch="$(date +%s)"
|
||||
|
||||
if [[ "$LOAD_IMAGE" == "1" ]]; then
|
||||
docker buildx build \
|
||||
--platform "$target_platform" \
|
||||
--builder "$BUILDER_NAME" \
|
||||
--pull=false \
|
||||
--load \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--build-arg "BUILDER_IMAGE=$BUILDER_IMAGE" \
|
||||
--build-arg "RUNTIME_IMAGE=$RUNTIME_IMAGE" \
|
||||
--build-arg "SOURCE_COMMIT=$SOURCE_COMMIT_FULL" \
|
||||
--build-arg "SOURCE_DIRTY=$SOURCE_DIRTY" \
|
||||
--build-arg "BUILD_TIMESTAMP_UTC=$BUILD_TIMESTAMP_UTC" \
|
||||
--metadata-file "$metadata_path" \
|
||||
-t "$IMAGE_TAG" \
|
||||
-f "$DOCKERFILE" \
|
||||
@@ -195,10 +242,14 @@ else
|
||||
docker buildx build \
|
||||
--platform "$target_platform" \
|
||||
--builder "$BUILDER_NAME" \
|
||||
--pull=false \
|
||||
--output "type=docker,dest=$raw_tar_path" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--build-arg "BUILDER_IMAGE=$BUILDER_IMAGE" \
|
||||
--build-arg "RUNTIME_IMAGE=$RUNTIME_IMAGE" \
|
||||
--build-arg "SOURCE_COMMIT=$SOURCE_COMMIT_FULL" \
|
||||
--build-arg "SOURCE_DIRTY=$SOURCE_DIRTY" \
|
||||
--build-arg "BUILD_TIMESTAMP_UTC=$BUILD_TIMESTAMP_UTC" \
|
||||
--metadata-file "$metadata_path" \
|
||||
-t "$IMAGE_TAG" \
|
||||
-f "$DOCKERFILE" \
|
||||
@@ -223,16 +274,22 @@ fi
|
||||
echo "image=$IMAGE_TAG"
|
||||
echo "platform=$target_platform"
|
||||
echo "arch=$TARGET_ARCH"
|
||||
echo "builder_image=$BUILDER_IMAGE"
|
||||
echo "runtime_image=$RUNTIME_IMAGE"
|
||||
echo "builder_image=$builder_source_image"
|
||||
echo "runtime_image=$runtime_source_image"
|
||||
echo "builder_local_image=$BUILDER_IMAGE"
|
||||
echo "runtime_local_image=$RUNTIME_IMAGE"
|
||||
echo "elapsed_secs=$elapsed_secs"
|
||||
echo "metadata=$metadata_path"
|
||||
echo "tar=$tar_path"
|
||||
echo "tar_size_bytes=$(wc -c < "$tar_path" 2>/dev/null || echo 0)"
|
||||
echo "tar_sha256=$tar_sha256"
|
||||
echo "git_commit=$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
echo "source_commit=$SOURCE_COMMIT_FULL"
|
||||
echo "source_commit_short=$SOURCE_COMMIT_SHORT"
|
||||
echo "source_dirty=$SOURCE_DIRTY"
|
||||
echo "build_timestamp_utc=$BUILD_TIMESTAMP_UTC"
|
||||
echo "git_commit=$SOURCE_COMMIT_FULL"
|
||||
echo "git_status_count=$(git -C "$REPO_ROOT" status --short 2>/dev/null | wc -l | tr -d ' ')"
|
||||
echo "built_at_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "built_at_utc=$BUILD_TIMESTAMP_UTC"
|
||||
} > "$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").build-summary.env"
|
||||
|
||||
echo "build complete: elapsed=${elapsed_secs}s tar=$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"
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
|
||||
pub const OID_AUTHORITY_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.1);
|
||||
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
|
||||
pub const OID_CERTIFICATE_POLICIES_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.32);
|
||||
pub const OID_QT_CPS: &str = "1.3.6.1.5.5.7.2.1";
|
||||
|
||||
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
||||
pub const OID_AUTHORITY_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.35);
|
||||
|
||||
@@ -13,11 +13,14 @@ use crate::data_model::common::{
|
||||
use crate::data_model::oid::{
|
||||
OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST,
|
||||
OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT,
|
||||
OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS_RAW, OID_AUTHORITY_KEY_IDENTIFIER_RAW,
|
||||
OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS_RAW, OID_CERTIFICATE_POLICIES_RAW,
|
||||
OID_CP_IPADDR_ASNUMBER, OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS_RAW,
|
||||
OID_IP_ADDR_BLOCKS_RAW, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW,
|
||||
OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER_RAW,
|
||||
OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS, OID_AUTHORITY_INFO_ACCESS_RAW,
|
||||
OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_AUTONOMOUS_SYS_IDS,
|
||||
OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS, OID_BASIC_CONSTRAINTS_RAW,
|
||||
OID_CERTIFICATE_POLICIES, OID_CERTIFICATE_POLICIES_RAW, OID_CP_IPADDR_ASNUMBER,
|
||||
OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS, OID_CRL_DISTRIBUTION_POINTS_RAW,
|
||||
OID_EXTENDED_KEY_USAGE, OID_IP_ADDR_BLOCKS, OID_IP_ADDR_BLOCKS_RAW, OID_KEY_USAGE, OID_QT_CPS,
|
||||
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SUBJECT_INFO_ACCESS,
|
||||
OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER, OID_SUBJECT_KEY_IDENTIFIER_RAW,
|
||||
};
|
||||
|
||||
/// Resource Certificate kind (semantic classification).
|
||||
@@ -27,6 +30,14 @@ pub enum ResourceCertKind {
|
||||
Ee,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ResourceCertificateRole {
|
||||
TrustAnchor,
|
||||
Ca,
|
||||
SignedObjectEe,
|
||||
RouterEe,
|
||||
}
|
||||
|
||||
/// A parsed RPKI Resource Certificate (RFC 6487) data model.
|
||||
///
|
||||
/// This module intentionally focuses on the semantics needed by Signed Object validation and
|
||||
@@ -59,6 +70,7 @@ pub struct RpkixTbsCertificate {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RcExtensions {
|
||||
pub basic_constraints_ca: bool,
|
||||
pub basic_constraints: Option<BasicConstraintsProfile>,
|
||||
pub subject_key_identifier: Option<Vec<u8>>,
|
||||
/// Authority Key Identifier (AKI) keyIdentifier value.
|
||||
pub authority_key_identifier: Option<Vec<u8>>,
|
||||
@@ -68,11 +80,26 @@ pub struct RcExtensions {
|
||||
pub ca_issuers_uris: Option<Vec<String>>,
|
||||
pub subject_info_access: Option<SubjectInfoAccess>,
|
||||
pub certificate_policies_oid: Option<String>,
|
||||
pub certificate_policies: Option<CertificatePoliciesProfile>,
|
||||
pub extension_oids: Vec<String>,
|
||||
|
||||
pub ip_resources: Option<IpResourceSet>,
|
||||
pub as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BasicConstraintsProfile {
|
||||
pub ca: bool,
|
||||
pub critical: bool,
|
||||
pub path_len_constraint: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CertificatePoliciesProfile {
|
||||
pub policy_oid: String,
|
||||
pub qualifier_oids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResourceCertificateParsed {
|
||||
pub raw_der: Vec<u8>,
|
||||
@@ -114,13 +141,14 @@ impl AlgorithmIdentifierValue {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RcExtensionsParsed {
|
||||
pub basic_constraints_ca: Vec<bool>,
|
||||
pub basic_constraints: Vec<BasicConstraintsProfile>,
|
||||
pub subject_key_identifier: Vec<(Vec<u8>, bool)>,
|
||||
pub authority_key_identifier: Vec<(AuthorityKeyIdentifierParsed, bool)>,
|
||||
pub crl_distribution_points: Vec<(CrlDistributionPointsParsed, bool)>,
|
||||
pub authority_info_access: Vec<(AuthorityInfoAccessParsed, bool)>,
|
||||
pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>,
|
||||
pub certificate_policies: Vec<(Vec<String>, bool)>,
|
||||
pub certificate_policies: Vec<(Vec<CertificatePoliciesProfile>, bool)>,
|
||||
pub extension_oids: Vec<String>,
|
||||
pub ip_resources: Vec<(IpResourceSet, bool)>,
|
||||
pub as_resources: Vec<(AsResourceSet, bool)>,
|
||||
}
|
||||
@@ -406,11 +434,45 @@ pub enum ResourceCertificateProfileError {
|
||||
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
|
||||
CertificatePoliciesCriticality,
|
||||
|
||||
#[error("certificatePolicies must be present (RFC 6487 §4.8.9)")]
|
||||
CertificatePoliciesMissing,
|
||||
|
||||
#[error(
|
||||
"certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)"
|
||||
)]
|
||||
InvalidCertificatePolicy(String),
|
||||
|
||||
#[error("certificatePolicies may contain at most one CPS qualifier (RFC 6487 §4.8.9)")]
|
||||
CertificatePoliciesTooManyQualifiers,
|
||||
|
||||
#[error(
|
||||
"certificatePolicies qualifier must be id-qt-cps ({OID_QT_CPS}), got {0} (RFC 6487 §4.8.9)"
|
||||
)]
|
||||
CertificatePoliciesInvalidQualifier(String),
|
||||
|
||||
#[error("basicConstraints must be present in CA certificates (RFC 6487 §4.8.1)")]
|
||||
BasicConstraintsMissing,
|
||||
|
||||
#[error("basicConstraints criticality must be critical in CA certificates (RFC 6487 §4.8.1)")]
|
||||
BasicConstraintsCriticality,
|
||||
|
||||
#[error("basicConstraints cA must be TRUE in CA certificates (RFC 6487 §4.8.1)")]
|
||||
BasicConstraintsCaFalse,
|
||||
|
||||
#[error(
|
||||
"basicConstraints pathLenConstraint must be absent in CA certificates (RFC 6487 §4.8.1)"
|
||||
)]
|
||||
BasicConstraintsPathLenPresent,
|
||||
|
||||
#[error("basicConstraints must be absent in EE certificates (RFC 6487 §4.8.1)")]
|
||||
BasicConstraintsEeMustOmit,
|
||||
|
||||
#[error("extension {oid} is not permitted for {role} resource certificates (RFC 6487 §4.8)")]
|
||||
DisallowedExtension { role: &'static str, oid: String },
|
||||
|
||||
#[error("autonomousSysIds RDI field must be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3)")]
|
||||
AsResourcesRdiPresent,
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)"
|
||||
)]
|
||||
@@ -567,6 +629,87 @@ impl ResourceCertificate {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_rfc6487_profile(
|
||||
&self,
|
||||
role: ResourceCertificateRole,
|
||||
) -> Result<(), ResourceCertificateProfileError> {
|
||||
let role_name = match role {
|
||||
ResourceCertificateRole::TrustAnchor => "trust anchor CA",
|
||||
ResourceCertificateRole::Ca => "CA",
|
||||
ResourceCertificateRole::SignedObjectEe => "signed-object EE",
|
||||
ResourceCertificateRole::RouterEe => "router EE",
|
||||
};
|
||||
let ca_role = matches!(
|
||||
role,
|
||||
ResourceCertificateRole::TrustAnchor | ResourceCertificateRole::Ca
|
||||
);
|
||||
|
||||
if ca_role {
|
||||
let constraints = self
|
||||
.tbs
|
||||
.extensions
|
||||
.basic_constraints
|
||||
.as_ref()
|
||||
.ok_or(ResourceCertificateProfileError::BasicConstraintsMissing)?;
|
||||
if !constraints.critical {
|
||||
return Err(ResourceCertificateProfileError::BasicConstraintsCriticality);
|
||||
}
|
||||
if !constraints.ca {
|
||||
return Err(ResourceCertificateProfileError::BasicConstraintsCaFalse);
|
||||
}
|
||||
if constraints.path_len_constraint.is_some() {
|
||||
return Err(ResourceCertificateProfileError::BasicConstraintsPathLenPresent);
|
||||
}
|
||||
} else if self.tbs.extensions.basic_constraints.is_some() {
|
||||
return Err(ResourceCertificateProfileError::BasicConstraintsEeMustOmit);
|
||||
}
|
||||
|
||||
for oid in &self.tbs.extensions.extension_oids {
|
||||
if !is_permitted_extension(oid, role) {
|
||||
return Err(ResourceCertificateProfileError::DisallowedExtension {
|
||||
role: role_name,
|
||||
oid: oid.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let policies = self
|
||||
.tbs
|
||||
.extensions
|
||||
.certificate_policies
|
||||
.as_ref()
|
||||
.ok_or(ResourceCertificateProfileError::CertificatePoliciesMissing)?;
|
||||
if policies.policy_oid != OID_CP_IPADDR_ASNUMBER {
|
||||
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
||||
policies.policy_oid.clone(),
|
||||
));
|
||||
}
|
||||
if policies.qualifier_oids.len() > 1 {
|
||||
return Err(ResourceCertificateProfileError::CertificatePoliciesTooManyQualifiers);
|
||||
}
|
||||
if let Some(qualifier_oid) = policies.qualifier_oids.first() {
|
||||
if qualifier_oid != OID_QT_CPS {
|
||||
return Err(
|
||||
ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(
|
||||
qualifier_oid.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.tbs
|
||||
.extensions
|
||||
.as_resources
|
||||
.as_ref()
|
||||
.is_some_and(|resources| resources.rdi.is_some())
|
||||
{
|
||||
return Err(ResourceCertificateProfileError::AsResourcesRdiPresent);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> {
|
||||
validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?;
|
||||
validate_strict_rpki_name(&self.tbs.subject_name, "subject")?;
|
||||
@@ -590,6 +733,22 @@ impl ResourceCertificate {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_permitted_extension(oid: &str, role: ResourceCertificateRole) -> bool {
|
||||
matches!(
|
||||
oid,
|
||||
OID_BASIC_CONSTRAINTS
|
||||
| OID_KEY_USAGE
|
||||
| OID_SUBJECT_KEY_IDENTIFIER
|
||||
| OID_AUTHORITY_KEY_IDENTIFIER
|
||||
| OID_CRL_DISTRIBUTION_POINTS
|
||||
| OID_AUTHORITY_INFO_ACCESS
|
||||
| OID_SUBJECT_INFO_ACCESS
|
||||
| OID_CERTIFICATE_POLICIES
|
||||
| OID_IP_ADDR_BLOCKS
|
||||
| OID_AUTONOMOUS_SYS_IDS
|
||||
) || (role == ResourceCertificateRole::RouterEe && oid == OID_EXTENDED_KEY_USAGE)
|
||||
}
|
||||
|
||||
fn validate_strict_rpki_name(
|
||||
name: &X509NameDer,
|
||||
role: &'static str,
|
||||
@@ -744,13 +903,24 @@ mod strict_name_tests {
|
||||
#[test]
|
||||
fn profile_rejects_rfc8360_v2_policy_oid() {
|
||||
let extensions = RcExtensionsParsed {
|
||||
basic_constraints_ca: vec![true],
|
||||
basic_constraints: vec![BasicConstraintsProfile {
|
||||
ca: true,
|
||||
critical: true,
|
||||
path_len_constraint: None,
|
||||
}],
|
||||
subject_key_identifier: Vec::new(),
|
||||
authority_key_identifier: Vec::new(),
|
||||
crl_distribution_points: Vec::new(),
|
||||
authority_info_access: Vec::new(),
|
||||
subject_info_access: Vec::new(),
|
||||
certificate_policies: vec![(vec!["1.3.6.1.5.5.7.14.3".to_string()], true)],
|
||||
certificate_policies: vec![(
|
||||
vec![CertificatePoliciesProfile {
|
||||
policy_oid: "1.3.6.1.5.5.7.14.3".to_string(),
|
||||
qualifier_oids: Vec::new(),
|
||||
}],
|
||||
true,
|
||||
)],
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: Vec::new(),
|
||||
as_resources: Vec::new(),
|
||||
};
|
||||
@@ -821,23 +991,25 @@ impl RcExtensionsParsed {
|
||||
// NOTE(perf): `self` is consumed. Prefer moving decoded fields out rather than cloning,
|
||||
// especially for large resource sets and URI lists.
|
||||
let RcExtensionsParsed {
|
||||
basic_constraints_ca,
|
||||
basic_constraints,
|
||||
subject_key_identifier,
|
||||
authority_key_identifier,
|
||||
crl_distribution_points,
|
||||
authority_info_access,
|
||||
subject_info_access,
|
||||
certificate_policies,
|
||||
extension_oids,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
} = self;
|
||||
|
||||
if basic_constraints_ca.len() > 1 {
|
||||
if basic_constraints.len() > 1 {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"basicConstraints",
|
||||
));
|
||||
}
|
||||
let basic_constraints_ca = basic_constraints_ca.first().copied().unwrap_or(false);
|
||||
let basic_constraints = basic_constraints.into_iter().next();
|
||||
let basic_constraints_ca = basic_constraints.as_ref().is_some_and(|bc| bc.ca);
|
||||
|
||||
let subject_key_identifier = match subject_key_identifier.len() {
|
||||
0 => None,
|
||||
@@ -1032,25 +1204,25 @@ impl RcExtensionsParsed {
|
||||
}
|
||||
};
|
||||
|
||||
let certificate_policies_oid = match certificate_policies.len() {
|
||||
let certificate_policies = match certificate_policies.len() {
|
||||
0 => None,
|
||||
1 => {
|
||||
let (oids, critical) = certificate_policies.into_iter().next().expect("len==1");
|
||||
let (policies, critical) = certificate_policies.into_iter().next().expect("len==1");
|
||||
if !critical {
|
||||
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
|
||||
}
|
||||
if oids.len() != 1 {
|
||||
if policies.len() != 1 {
|
||||
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
||||
"expected exactly one policy".into(),
|
||||
));
|
||||
}
|
||||
let policy_oid = oids.into_iter().next().expect("len==1");
|
||||
if policy_oid != OID_CP_IPADDR_ASNUMBER {
|
||||
let policy = policies.into_iter().next().expect("len==1");
|
||||
if policy.policy_oid != OID_CP_IPADDR_ASNUMBER {
|
||||
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
||||
policy_oid,
|
||||
policy.policy_oid,
|
||||
));
|
||||
}
|
||||
Some(OID_CP_IPADDR_ASNUMBER.to_string())
|
||||
Some(policy)
|
||||
}
|
||||
_ => {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
@@ -1093,12 +1265,17 @@ impl RcExtensionsParsed {
|
||||
|
||||
Ok(RcExtensions {
|
||||
basic_constraints_ca,
|
||||
basic_constraints,
|
||||
subject_key_identifier,
|
||||
authority_key_identifier,
|
||||
crl_distribution_points_uris,
|
||||
ca_issuers_uris,
|
||||
subject_info_access,
|
||||
certificate_policies_oid,
|
||||
certificate_policies_oid: certificate_policies
|
||||
.as_ref()
|
||||
.map(|_| OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies,
|
||||
extension_oids,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
})
|
||||
@@ -1127,26 +1304,32 @@ fn algorithm_identifier_value(
|
||||
fn parse_extensions_parse(
|
||||
exts: &[X509Extension<'_>],
|
||||
) -> Result<RcExtensionsParsed, ResourceCertificateParseError> {
|
||||
let mut basic_constraints_ca: Vec<bool> = Vec::new();
|
||||
let mut basic_constraints: Vec<BasicConstraintsProfile> = Vec::new();
|
||||
let mut ski: Vec<(Vec<u8>, bool)> = Vec::new();
|
||||
let mut aki: Vec<(AuthorityKeyIdentifierParsed, bool)> = Vec::new();
|
||||
let mut crldp: Vec<(CrlDistributionPointsParsed, bool)> = Vec::new();
|
||||
let mut aia: Vec<(AuthorityInfoAccessParsed, bool)> = Vec::new();
|
||||
let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new();
|
||||
let mut cert_policies: Vec<(Vec<String>, bool)> = Vec::new();
|
||||
let mut cert_policies: Vec<(Vec<CertificatePoliciesProfile>, bool)> = Vec::new();
|
||||
let mut extension_oids: Vec<String> = Vec::with_capacity(exts.len());
|
||||
|
||||
let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new();
|
||||
let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new();
|
||||
|
||||
for ext in exts {
|
||||
let oid = ext.oid.as_bytes();
|
||||
extension_oids.push(ext.oid.to_id_string());
|
||||
if oid == OID_BASIC_CONSTRAINTS_RAW {
|
||||
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateParseError::Parse(
|
||||
"basicConstraints parse failed".into(),
|
||||
));
|
||||
};
|
||||
basic_constraints_ca.push(bc.ca);
|
||||
basic_constraints.push(BasicConstraintsProfile {
|
||||
ca: bc.ca,
|
||||
critical: ext.critical,
|
||||
path_len_constraint: bc.path_len_constraint,
|
||||
});
|
||||
} else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW {
|
||||
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateParseError::Parse(
|
||||
@@ -1195,16 +1378,30 @@ fn parse_extensions_parse(
|
||||
"certificatePolicies parse failed".into(),
|
||||
));
|
||||
};
|
||||
let mut oids: Vec<String> = Vec::with_capacity(cp.len());
|
||||
let mut policies: Vec<CertificatePoliciesProfile> = Vec::with_capacity(cp.len());
|
||||
for p in cp.iter() {
|
||||
let b = p.policy_id.as_bytes();
|
||||
if b == OID_CP_IPADDR_ASNUMBER_RAW {
|
||||
oids.push(OID_CP_IPADDR_ASNUMBER.to_string());
|
||||
let policy_oid = if b == OID_CP_IPADDR_ASNUMBER_RAW {
|
||||
OID_CP_IPADDR_ASNUMBER.to_string()
|
||||
} else {
|
||||
oids.push(p.policy_id.to_id_string());
|
||||
}
|
||||
p.policy_id.to_id_string()
|
||||
};
|
||||
let qualifier_oids = p
|
||||
.policy_qualifiers
|
||||
.as_ref()
|
||||
.map(|qualifiers| {
|
||||
qualifiers
|
||||
.iter()
|
||||
.map(|qualifier| qualifier.policy_qualifier_id.to_id_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
policies.push(CertificatePoliciesProfile {
|
||||
policy_oid,
|
||||
qualifier_oids,
|
||||
});
|
||||
}
|
||||
cert_policies.push((oids, ext.critical));
|
||||
cert_policies.push((policies, ext.critical));
|
||||
} else if oid == OID_IP_ADDR_BLOCKS_RAW {
|
||||
let parsed = IpResourceSet::decode_extn_value(ext.value)
|
||||
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
|
||||
@@ -1217,13 +1414,14 @@ fn parse_extensions_parse(
|
||||
}
|
||||
|
||||
Ok(RcExtensionsParsed {
|
||||
basic_constraints_ca,
|
||||
basic_constraints,
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points: crldp,
|
||||
authority_info_access: aia,
|
||||
subject_info_access: sia,
|
||||
certificate_policies: cert_policies,
|
||||
extension_oids,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::data_model::oid::{
|
||||
use crate::data_model::rc::{
|
||||
AsIdOrRange, AsIdentifierChoice, ResourceCertKind, ResourceCertificate,
|
||||
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
|
||||
ResourceCertificateRole,
|
||||
};
|
||||
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_predecoded_ee};
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
@@ -202,6 +203,7 @@ impl BgpsecRouterCertificateParsed {
|
||||
self,
|
||||
) -> Result<BgpsecRouterCertificate, BgpsecRouterCertificateProfileError> {
|
||||
let rc = self.rc_parsed.validate_profile()?;
|
||||
rc.validate_rfc6487_profile(ResourceCertificateRole::RouterEe)?;
|
||||
if rc.kind != ResourceCertKind::Ee {
|
||||
return Err(BgpsecRouterCertificateProfileError::NotEe);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::data_model::oid::{
|
||||
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SIGNED_DATA,
|
||||
OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS,
|
||||
};
|
||||
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
|
||||
use crate::data_model::rc::{ResourceCertificate, ResourceCertificateRole, SubjectInfoAccess};
|
||||
use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag};
|
||||
use ring::digest;
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
@@ -822,6 +822,8 @@ fn validate_ee_certificate(
|
||||
};
|
||||
}
|
||||
};
|
||||
rc.validate_rfc6487_profile(ResourceCertificateRole::SignedObjectEe)
|
||||
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
|
||||
if strict_name {
|
||||
rc.validate_strict_name_profile()
|
||||
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::data_model::rc::{
|
||||
AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate,
|
||||
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
|
||||
ResourceCertificateRole,
|
||||
};
|
||||
use crate::data_model::tal::Tal;
|
||||
|
||||
@@ -218,6 +219,7 @@ impl TaCertificateParsed {
|
||||
if rc_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(TaCertificateProfileError::NotCa);
|
||||
}
|
||||
rc_ca.validate_rfc6487_profile(ResourceCertificateRole::TrustAnchor)?;
|
||||
|
||||
if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name {
|
||||
return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject);
|
||||
@@ -244,6 +246,9 @@ pub enum TrustAnchorError {
|
||||
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
|
||||
TaCertificate(#[from] TaCertificateDecodeError),
|
||||
|
||||
#[error("TA certificate self-signature error: {0} (RFC 8630 §2.3)")]
|
||||
TaSelfSignature(#[from] TaCertificateVerifyError),
|
||||
|
||||
#[error("{0}")]
|
||||
Bind(#[from] TrustAnchorBindError),
|
||||
}
|
||||
@@ -269,6 +274,7 @@ impl TrustAnchor {
|
||||
resolved_uri: Option<&Url>,
|
||||
) -> Result<Self, TrustAnchorError> {
|
||||
let ta_certificate = TaCertificate::decode_der(ta_der)?;
|
||||
ta_certificate.verify_self_signature()?;
|
||||
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
|
||||
}
|
||||
|
||||
@@ -278,6 +284,7 @@ impl TrustAnchor {
|
||||
resolved_uri: Option<&Url>,
|
||||
) -> Result<Self, TrustAnchorError> {
|
||||
let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?;
|
||||
ta_certificate.verify_self_signature()?;
|
||||
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
|
||||
}
|
||||
|
||||
|
||||
218
src/storage.rs
218
src/storage.rs
@@ -19,7 +19,7 @@ pub use config::{
|
||||
ALL_COLUMN_FAMILY_NAMES, CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_MANIFEST_REPLAY_META,
|
||||
CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
|
||||
CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER,
|
||||
CF_TRANSPORT_PREFETCH, CF_VCIR, column_family_descriptors,
|
||||
CF_TRANSPORT_PREFETCH, CF_VCIR, CF_VCIR_FAILED_FETCH_REUSE_IDENTITY, column_family_descriptors,
|
||||
};
|
||||
use keys::*;
|
||||
use pack::compute_sha256_32;
|
||||
@@ -585,6 +585,56 @@ impl VcirInstanceGate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VcirFailedFetchReuseIdentity {
|
||||
#[serde(rename = "c")]
|
||||
#[serde(with = "serde_bytes_32")]
|
||||
pub current_ca_sha256: [u8; 32],
|
||||
#[serde(rename = "t")]
|
||||
#[serde(with = "serde_bytes_32")]
|
||||
pub ta_context_digest: [u8; 32],
|
||||
#[serde(rename = "p")]
|
||||
#[serde(with = "serde_bytes_32")]
|
||||
pub parent_context_digest: [u8; 32],
|
||||
#[serde(rename = "f")]
|
||||
#[serde(with = "serde_bytes_32")]
|
||||
pub policy_fingerprint: [u8; 32],
|
||||
#[serde(rename = "nb")]
|
||||
pub effective_not_before: PackTime,
|
||||
#[serde(rename = "nu")]
|
||||
pub effective_until: PackTime,
|
||||
}
|
||||
|
||||
impl VcirFailedFetchReuseIdentity {
|
||||
pub fn validate_internal(&self) -> StorageResult<()> {
|
||||
let effective_not_before = parse_time(
|
||||
"vcir_failed_fetch_reuse_identity.effective_not_before",
|
||||
&self.effective_not_before,
|
||||
)?;
|
||||
let effective_until = parse_time(
|
||||
"vcir_failed_fetch_reuse_identity.effective_until",
|
||||
&self.effective_until,
|
||||
)?;
|
||||
if effective_not_before >= effective_until {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "vcir_failed_fetch_reuse_identity.effective_window",
|
||||
detail: "effective_not_before must be before effective_until".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contains_validation_time(&self, validation_time: time::OffsetDateTime) -> bool {
|
||||
let Ok(effective_not_before) = self.effective_not_before.parse() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(effective_until) = self.effective_until.parse() else {
|
||||
return false;
|
||||
};
|
||||
validation_time >= effective_not_before && validation_time < effective_until
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VcirChildEntry {
|
||||
pub child_manifest_rsync_uri: String,
|
||||
@@ -1062,6 +1112,7 @@ pub struct RoaCacheObjectMeta {
|
||||
pub source_object_hash: [u8; 32],
|
||||
pub ee_serial: Vec<u8>,
|
||||
pub crl_uri: String,
|
||||
pub earliest_safe_reuse_time: PackTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -1127,6 +1178,8 @@ pub struct RoaCacheObjectProjection {
|
||||
pub ee_serial: Option<Vec<u8>>,
|
||||
#[serde(rename = "c", default, skip_serializing_if = "Option::is_none")]
|
||||
pub crl_uri: Option<String>,
|
||||
#[serde(rename = "n", default, skip_serializing_if = "Option::is_none")]
|
||||
pub earliest_safe_reuse_time_unix: Option<i64>,
|
||||
#[serde(rename = "x")]
|
||||
pub outputs_effective_until_unix: i64,
|
||||
#[serde(rename = "o")]
|
||||
@@ -1150,6 +1203,15 @@ impl RoaCacheObjectProjection {
|
||||
if let Some(crl_uri) = &self.crl_uri {
|
||||
validate_non_empty("roa_cache_projection.entries[].crl_uri", crl_uri)?;
|
||||
}
|
||||
if let Some(earliest_safe_reuse_time_unix) = self.earliest_safe_reuse_time_unix
|
||||
&& earliest_safe_reuse_time_unix >= self.outputs_effective_until_unix
|
||||
{
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "roa_cache_projection.entries[].effective_window",
|
||||
detail: "earliest_safe_reuse_time_unix must be before outputs_effective_until_unix"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
if self.outputs.is_empty() {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "roa_cache_projection.entries[]",
|
||||
@@ -1867,6 +1929,9 @@ impl RoaCacheProjection {
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
context: Option<&RoaCacheProjectionContext>,
|
||||
) -> StorageResult<Option<Self>> {
|
||||
let Some(context) = context else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut issuer_ca_sha256_hex = None;
|
||||
let mut crl_sha256_by_uri = Vec::new();
|
||||
for artifact in &vcir.related_artifacts {
|
||||
@@ -1893,13 +1958,18 @@ impl RoaCacheProjection {
|
||||
}
|
||||
crl_sha256_by_uri.sort_by(|left, right| left.uri.cmp(&right.uri));
|
||||
|
||||
let meta_by_uri = context.map(|context| {
|
||||
context
|
||||
.object_meta
|
||||
.iter()
|
||||
.map(|meta| (meta.source_object_uri.as_str(), meta))
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
let meta_by_uri = context
|
||||
.object_meta
|
||||
.iter()
|
||||
.map(|meta| (meta.source_object_uri.as_str(), meta))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let vcir_validation_time =
|
||||
vcir.last_successful_validation_time
|
||||
.parse()
|
||||
.map_err(|detail| StorageError::InvalidData {
|
||||
entity: "roa_cache_projection.entries[].earliest_safe_reuse_time_unix",
|
||||
detail,
|
||||
})?;
|
||||
let mut entries: Vec<RoaCacheObjectProjection> = Vec::new();
|
||||
let mut entry_index_by_uri: HashMap<String, usize> = HashMap::new();
|
||||
for output in &vcir.local_outputs {
|
||||
@@ -1915,23 +1985,26 @@ impl RoaCacheProjection {
|
||||
entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
|
||||
detail,
|
||||
})?;
|
||||
let meta = meta_by_uri
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.get(output.source_object_uri.as_str()).copied());
|
||||
if context.is_some() && meta.is_none() {
|
||||
let Some(meta) = meta_by_uri.get(output.source_object_uri.as_str()).copied() else {
|
||||
continue;
|
||||
};
|
||||
if meta.source_object_hash != output.source_object_hash {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "roa_cache_projection.entries[]",
|
||||
detail: format!(
|
||||
"metadata source object hash mismatch for {}",
|
||||
output.source_object_uri
|
||||
),
|
||||
});
|
||||
}
|
||||
if let Some(meta) = meta {
|
||||
if meta.source_object_hash != output.source_object_hash {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "roa_cache_projection.entries[]",
|
||||
detail: format!(
|
||||
"metadata source object hash mismatch for {}",
|
||||
output.source_object_uri
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
let earliest_safe_reuse_time_unix = meta
|
||||
.earliest_safe_reuse_time
|
||||
.parse()
|
||||
.map(|time| time.max(vcir_validation_time).unix_timestamp())
|
||||
.map_err(|detail| StorageError::InvalidData {
|
||||
entity: "roa_cache_projection.entries[].earliest_safe_reuse_time_unix",
|
||||
detail,
|
||||
})?;
|
||||
if let Some(entry_index) = entry_index_by_uri.get(output.source_object_uri.as_str()) {
|
||||
let entry = &mut entries[*entry_index];
|
||||
if entry.source_object_hash != output.source_object_hash {
|
||||
@@ -1952,8 +2025,9 @@ impl RoaCacheProjection {
|
||||
entries.push(RoaCacheObjectProjection {
|
||||
source_object_uri: output.source_object_uri.clone(),
|
||||
source_object_hash: output.source_object_hash,
|
||||
ee_serial: meta.map(|meta| meta.ee_serial.clone()),
|
||||
crl_uri: meta.map(|meta| meta.crl_uri.clone()),
|
||||
ee_serial: Some(meta.ee_serial.clone()),
|
||||
crl_uri: Some(meta.crl_uri.clone()),
|
||||
earliest_safe_reuse_time_unix: Some(earliest_safe_reuse_time_unix),
|
||||
outputs_effective_until_unix: projected_output_effective_until,
|
||||
outputs: vec![projected_output],
|
||||
});
|
||||
@@ -1968,8 +2042,8 @@ impl RoaCacheProjection {
|
||||
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
|
||||
instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
|
||||
issuer_ca_sha256_hex,
|
||||
parent_context_digest: context.map(|context| context.parent_context_digest),
|
||||
policy_fingerprint: context.map(|context| context.policy_fingerprint),
|
||||
parent_context_digest: Some(context.parent_context_digest),
|
||||
policy_fingerprint: Some(context.policy_fingerprint),
|
||||
crl_sha256_by_uri,
|
||||
entries,
|
||||
};
|
||||
@@ -2774,6 +2848,24 @@ fn write_roa_cache_projection_to_batch(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_vcir_failed_fetch_reuse_identity_to_batch(
|
||||
failed_fetch_reuse_identity_cf: &ColumnFamily,
|
||||
batch: &mut WriteBatch,
|
||||
manifest_rsync_uri: &str,
|
||||
failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
|
||||
) -> StorageResult<()> {
|
||||
let key = vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
|
||||
match failed_fetch_reuse_identity {
|
||||
Some(identity) => {
|
||||
identity.validate_internal()?;
|
||||
let value = encode_cbor(identity, "vcir_failed_fetch_reuse_identity")?;
|
||||
batch.put_cf(failed_fetch_reuse_identity_cf, key.as_bytes(), value);
|
||||
}
|
||||
None => batch.delete_cf(failed_fetch_reuse_identity_cf, key.as_bytes()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_publication_point_cache_projection_to_batch(
|
||||
projection_cf: &ColumnFamily,
|
||||
batch: &mut WriteBatch,
|
||||
@@ -3267,6 +3359,19 @@ impl RocksStore {
|
||||
self.put_vcir_with_publication_point_cache_projection(vcir, None)
|
||||
}
|
||||
|
||||
pub fn put_vcir_with_failed_fetch_reuse_identity(
|
||||
&self,
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
failed_fetch_reuse_identity: &VcirFailedFetchReuseIdentity,
|
||||
) -> StorageResult<()> {
|
||||
self.put_vcir_with_projection_action(
|
||||
vcir,
|
||||
None,
|
||||
PublicationPointCacheProjectionWriteAction::Keep,
|
||||
Some(failed_fetch_reuse_identity),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn put_vcir_with_publication_point_cache_projection(
|
||||
&self,
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
@@ -3288,6 +3393,7 @@ impl RocksStore {
|
||||
vcir,
|
||||
roa_cache_context,
|
||||
publication_point_projection_action,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3296,9 +3402,11 @@ impl RocksStore {
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
roa_cache_context: Option<&RoaCacheProjectionContext>,
|
||||
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
|
||||
failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
|
||||
) -> StorageResult<()> {
|
||||
vcir.validate_internal()?;
|
||||
let vcir_cf = self.cf(CF_VCIR)?;
|
||||
let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
|
||||
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
|
||||
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
|
||||
let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
|
||||
@@ -3308,6 +3416,12 @@ impl RocksStore {
|
||||
let key = vcir_key(&vcir.manifest_rsync_uri);
|
||||
let value = encode_cbor(vcir, "vcir")?;
|
||||
batch.put_cf(vcir_cf, key.as_bytes(), value);
|
||||
write_vcir_failed_fetch_reuse_identity_to_batch(
|
||||
failed_fetch_reuse_identity_cf,
|
||||
&mut batch,
|
||||
&vcir.manifest_rsync_uri,
|
||||
failed_fetch_reuse_identity,
|
||||
)?;
|
||||
let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
|
||||
let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
|
||||
batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
|
||||
@@ -3371,6 +3485,21 @@ impl RocksStore {
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
roa_cache_context: Option<&RoaCacheProjectionContext>,
|
||||
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
|
||||
) -> StorageResult<VcirReplaceTimingBreakdown> {
|
||||
self.replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
|
||||
vcir,
|
||||
roa_cache_context,
|
||||
publication_point_projection_action,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
|
||||
&self,
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
roa_cache_context: Option<&RoaCacheProjectionContext>,
|
||||
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
|
||||
failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
|
||||
) -> StorageResult<VcirReplaceTimingBreakdown> {
|
||||
let mut timing = VcirReplaceTimingBreakdown {
|
||||
rss_before_kb: process_vm_rss_kb(),
|
||||
@@ -3385,6 +3514,7 @@ impl RocksStore {
|
||||
|
||||
let batch_build_started = std::time::Instant::now();
|
||||
let vcir_cf = self.cf(CF_VCIR)?;
|
||||
let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
|
||||
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
|
||||
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
|
||||
let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
|
||||
@@ -3397,6 +3527,12 @@ impl RocksStore {
|
||||
timing.vcir_value_bytes = vcir_value.len() as u64;
|
||||
batch.put_cf(vcir_cf, vcir_key.as_bytes(), vcir_value);
|
||||
timing.rss_after_vcir_encode_kb = process_vm_rss_kb();
|
||||
write_vcir_failed_fetch_reuse_identity_to_batch(
|
||||
failed_fetch_reuse_identity_cf,
|
||||
&mut batch,
|
||||
&vcir.manifest_rsync_uri,
|
||||
failed_fetch_reuse_identity,
|
||||
)?;
|
||||
|
||||
let replay_meta_encode_started = std::time::Instant::now();
|
||||
let replay_meta = ManifestReplayMeta::from_vcir(vcir);
|
||||
@@ -3465,6 +3601,27 @@ impl RocksStore {
|
||||
Ok(Some(vcir))
|
||||
}
|
||||
|
||||
pub fn get_vcir_failed_fetch_reuse_identity(
|
||||
&self,
|
||||
manifest_rsync_uri: &str,
|
||||
) -> StorageResult<Option<VcirFailedFetchReuseIdentity>> {
|
||||
let cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
|
||||
let key = vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
|
||||
let Some(bytes) = self
|
||||
.db
|
||||
.get_cf(cf, key.as_bytes())
|
||||
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let identity = decode_cbor::<VcirFailedFetchReuseIdentity>(
|
||||
&bytes,
|
||||
"vcir_failed_fetch_reuse_identity",
|
||||
)?;
|
||||
identity.validate_internal()?;
|
||||
Ok(Some(identity))
|
||||
}
|
||||
|
||||
pub fn get_manifest_replay_meta(
|
||||
&self,
|
||||
manifest_rsync_uri: &str,
|
||||
@@ -4308,11 +4465,18 @@ impl RocksStore {
|
||||
|
||||
pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> {
|
||||
let vcir_cf = self.cf(CF_VCIR)?;
|
||||
let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
|
||||
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
|
||||
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
|
||||
let mut batch = WriteBatch::default();
|
||||
let key = vcir_key(manifest_rsync_uri);
|
||||
batch.delete_cf(vcir_cf, key.as_bytes());
|
||||
let failed_fetch_reuse_identity_key =
|
||||
vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
|
||||
batch.delete_cf(
|
||||
failed_fetch_reuse_identity_cf,
|
||||
failed_fetch_reuse_identity_key.as_bytes(),
|
||||
);
|
||||
let replay_key = manifest_replay_meta_key(manifest_rsync_uri);
|
||||
batch.delete_cf(replay_cf, replay_key.as_bytes());
|
||||
let projection_key = roa_cache_projection_key(manifest_rsync_uri);
|
||||
|
||||
@@ -4,6 +4,7 @@ pub const CF_REPOSITORY_VIEW: &str = "repository_view";
|
||||
pub const CF_RAW_BY_HASH: &str = "raw_by_hash";
|
||||
pub const CF_RAW_BLOB: &str = "raw_blob";
|
||||
pub const CF_VCIR: &str = "vcir";
|
||||
pub const CF_VCIR_FAILED_FETCH_REUSE_IDENTITY: &str = "vcir_failed_fetch_reuse_identity";
|
||||
pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta";
|
||||
pub const CF_ROA_CACHE_PROJECTION: &str = "roa_cache_projection";
|
||||
pub const CF_PUBLICATION_POINT_CACHE_PROJECTION: &str = "publication_point_cache_projection";
|
||||
@@ -18,6 +19,7 @@ pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
|
||||
CF_RAW_BY_HASH,
|
||||
CF_RAW_BLOB,
|
||||
CF_VCIR,
|
||||
CF_VCIR_FAILED_FETCH_REUSE_IDENTITY,
|
||||
CF_MANIFEST_REPLAY_META,
|
||||
CF_ROA_CACHE_PROJECTION,
|
||||
CF_PUBLICATION_POINT_CACHE_PROJECTION,
|
||||
@@ -32,6 +34,8 @@ pub(super) const REPOSITORY_VIEW_KEY_PREFIX: &str = "repo_view:";
|
||||
pub(super) const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
|
||||
pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
|
||||
pub(super) const VCIR_KEY_PREFIX: &str = "vcir:";
|
||||
pub(super) const VCIR_FAILED_FETCH_REUSE_IDENTITY_KEY_PREFIX: &str =
|
||||
"vcir_failed_fetch_reuse_identity:";
|
||||
pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:";
|
||||
pub(super) const ROA_CACHE_PROJECTION_KEY_PREFIX: &str = "roa_cache_projection:";
|
||||
pub(super) const PUBLICATION_POINT_CACHE_PROJECTION_KEY_PREFIX: &str =
|
||||
|
||||
@@ -26,6 +26,10 @@ pub(super) fn vcir_key(manifest_rsync_uri: &str) -> String {
|
||||
format!("{VCIR_KEY_PREFIX}{manifest_rsync_uri}")
|
||||
}
|
||||
|
||||
pub(super) fn vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri: &str) -> String {
|
||||
format!("{VCIR_FAILED_FETCH_REUSE_IDENTITY_KEY_PREFIX}{manifest_rsync_uri}")
|
||||
}
|
||||
|
||||
pub(super) fn manifest_replay_meta_key(manifest_rsync_uri: &str) -> String {
|
||||
format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}")
|
||||
}
|
||||
|
||||
@@ -284,6 +284,28 @@ fn sample_vcir(manifest_rsync_uri: &str) -> ValidatedCaInstanceResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn roa_cache_projection_context(vcir: &ValidatedCaInstanceResult) -> RoaCacheProjectionContext {
|
||||
let roa_output = vcir
|
||||
.local_outputs
|
||||
.iter()
|
||||
.find(|output| {
|
||||
output.output_type == VcirOutputType::Vrp
|
||||
&& output.source_object_type == VcirSourceObjectType::Roa
|
||||
})
|
||||
.expect("sample VCIR has a ROA output");
|
||||
RoaCacheProjectionContext {
|
||||
parent_context_digest: [0x31; 32],
|
||||
policy_fingerprint: [0x32; 32],
|
||||
object_meta: vec![RoaCacheObjectMeta {
|
||||
source_object_uri: roa_output.source_object_uri.clone(),
|
||||
source_object_hash: roa_output.source_object_hash,
|
||||
ee_serial: vec![0x01],
|
||||
crl_uri: vcir.current_crl_rsync_uri.clone(),
|
||||
earliest_safe_reuse_time: vcir.last_successful_validation_time.clone(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vcir_ccr_manifest_projection_validate_accepts_valid_projection() {
|
||||
let projection = sample_ccr_manifest_projection(
|
||||
@@ -332,7 +354,8 @@ fn vcir_ccr_manifest_projection_validate_rejects_invalid_fields() {
|
||||
#[test]
|
||||
fn roa_cache_projection_from_vcir_keeps_only_roa_vrp_outputs() {
|
||||
let vcir = sample_vcir("rsync://example.test/repo/current.mft");
|
||||
let projection = RoaCacheProjection::from_vcir(&vcir)
|
||||
let context = roa_cache_projection_context(&vcir);
|
||||
let projection = RoaCacheProjection::from_vcir_with_context(&vcir, Some(&context))
|
||||
.expect("projection build")
|
||||
.expect("projection exists");
|
||||
|
||||
@@ -366,7 +389,8 @@ fn roa_cache_projection_groups_multiple_outputs_by_roa_uri() {
|
||||
vcir.local_outputs.push(second);
|
||||
vcir.summary.local_vrp_count = 2;
|
||||
|
||||
let projection = RoaCacheProjection::from_vcir(&vcir)
|
||||
let context = roa_cache_projection_context(&vcir);
|
||||
let projection = RoaCacheProjection::from_vcir_with_context(&vcir, Some(&context))
|
||||
.expect("projection build")
|
||||
.expect("projection exists");
|
||||
|
||||
@@ -748,7 +772,8 @@ fn roa_cache_projection_rejects_duplicate_uri_with_different_hash() {
|
||||
vcir.local_outputs.push(duplicate);
|
||||
vcir.summary.local_vrp_count = 2;
|
||||
|
||||
let err = RoaCacheProjection::from_vcir(&vcir)
|
||||
let context = roa_cache_projection_context(&vcir);
|
||||
let err = RoaCacheProjection::from_vcir_with_context(&vcir, Some(&context))
|
||||
.expect_err("same ROA URI with different hash must fail");
|
||||
assert!(err.to_string().contains("source object hash mismatch"));
|
||||
}
|
||||
@@ -1206,7 +1231,10 @@ fn vcir_roundtrip_and_validation_failures_are_reported() {
|
||||
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||
|
||||
let vcir = sample_vcir("rsync://example.test/repo/current.mft");
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
let roa_context = roa_cache_projection_context(&vcir);
|
||||
store
|
||||
.put_vcir_with_projections(&vcir, Some(&roa_context), None)
|
||||
.expect("put vcir");
|
||||
let got = store
|
||||
.get_vcir(&vcir.manifest_rsync_uri)
|
||||
.expect("get vcir")
|
||||
@@ -1446,8 +1474,13 @@ fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() {
|
||||
previous.summary.local_vrp_count = 1;
|
||||
previous.summary.local_aspa_count = 0;
|
||||
previous.summary.local_router_key_count = 0;
|
||||
let previous_roa_context = roa_cache_projection_context(&previous);
|
||||
let previous_timing = store
|
||||
.replace_vcir_and_manifest_replay_meta(&previous)
|
||||
.replace_vcir_manifest_replay_meta_and_projections(
|
||||
&previous,
|
||||
Some(&previous_roa_context),
|
||||
None,
|
||||
)
|
||||
.expect("store previous vcir");
|
||||
assert!(previous_timing.vcir_value_bytes > 0);
|
||||
assert!(previous_timing.replay_meta_value_bytes > 0);
|
||||
|
||||
@@ -3,7 +3,8 @@ use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use crate::data_model::oid::OID_KEY_USAGE_RAW;
|
||||
use crate::data_model::rc::{
|
||||
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
|
||||
ResourceCertificate, ResourceCertificateDecodeError,
|
||||
ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificateProfileError,
|
||||
ResourceCertificateRole,
|
||||
};
|
||||
use crate::policy::ResourceValidationMode;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
@@ -108,6 +109,12 @@ pub enum CaPathError {
|
||||
#[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
IssuerDecode(ResourceCertificateDecodeError),
|
||||
|
||||
#[error("child CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
||||
ChildProfile(ResourceCertificateProfileError),
|
||||
|
||||
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
||||
IssuerProfile(ResourceCertificateProfileError),
|
||||
|
||||
#[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")]
|
||||
CrlDecode(#[from] CrlDecodeError),
|
||||
|
||||
@@ -244,12 +251,18 @@ pub fn validate_subordinate_ca_cert_with_resource_validation_mode(
|
||||
if child_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::ChildNotCa);
|
||||
}
|
||||
child_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::ChildProfile)?;
|
||||
|
||||
let issuer_ca =
|
||||
ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?;
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::IssuerProfile)?;
|
||||
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
|
||||
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
@@ -379,6 +392,12 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
child_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::ChildProfile)?;
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::IssuerProfile)?;
|
||||
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
@@ -1375,12 +1394,14 @@ fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::common::X509NameDer;
|
||||
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
|
||||
IpAddressOrRange, IpPrefix, IpResourceSet,
|
||||
};
|
||||
use crate::data_model::rc::{
|
||||
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use std::process::Command;
|
||||
@@ -1410,12 +1431,27 @@ mod tests {
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: kind == ResourceCertKind::Ca,
|
||||
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
|
||||
BasicConstraintsProfile {
|
||||
ca: true,
|
||||
critical: true,
|
||||
path_len_constraint: None,
|
||||
},
|
||||
),
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points_uris: crldp,
|
||||
ca_issuers_uris: aia,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
certificate_policies_oid: (kind == ResourceCertKind::Ca)
|
||||
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
|
||||
CertificatePoliciesProfile {
|
||||
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
|
||||
qualifier_oids: Vec::new(),
|
||||
},
|
||||
),
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use crate::data_model::rc::{
|
||||
ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError,
|
||||
ResourceCertificateProfileError, ResourceCertificateRole,
|
||||
};
|
||||
use crate::data_model::signed_object::{EeKeyUsageSummary, ResourceEeCertificate};
|
||||
use ring::signature;
|
||||
@@ -34,6 +35,9 @@ pub enum CertPathError {
|
||||
)]
|
||||
IssuerNotCa,
|
||||
|
||||
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
||||
IssuerProfile(ResourceCertificateProfileError),
|
||||
|
||||
#[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")]
|
||||
EeNotEe,
|
||||
|
||||
@@ -128,6 +132,9 @@ pub fn validate_ee_cert_path(
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CertPathError::IssuerNotCa);
|
||||
}
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CertPathError::IssuerProfile)?;
|
||||
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
|
||||
|
||||
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
@@ -311,6 +318,9 @@ fn validate_ee_cert_path_bindings_and_status(
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CertPathError::IssuerNotCa);
|
||||
}
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CertPathError::IssuerProfile)?;
|
||||
|
||||
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CertPathError::IssuerSubjectMismatch {
|
||||
@@ -515,8 +525,10 @@ fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::common::X509NameDer;
|
||||
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::data_model::rc::{
|
||||
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
|
||||
@@ -545,12 +557,27 @@ mod tests {
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: kind == ResourceCertKind::Ca,
|
||||
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
|
||||
BasicConstraintsProfile {
|
||||
ca: true,
|
||||
critical: true,
|
||||
path_len_constraint: None,
|
||||
},
|
||||
),
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points_uris: crldp,
|
||||
ca_issuers_uris: aia,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
certificate_policies_oid: (kind == ResourceCertKind::Ca)
|
||||
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
|
||||
CertificatePoliciesProfile {
|
||||
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
|
||||
qualifier_oids: Vec::new(),
|
||||
},
|
||||
),
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
|
||||
@@ -506,6 +506,7 @@ pub struct CachedRoaValidationResult {
|
||||
source_object_hash: [u8; 32],
|
||||
ee_serial: Option<Vec<u8>>,
|
||||
crl_uri: Option<String>,
|
||||
earliest_safe_reuse_time_unix: i64,
|
||||
outputs_effective_until_unix: i64,
|
||||
outputs: Vec<VcirLocalOutput>,
|
||||
}
|
||||
@@ -543,6 +544,12 @@ impl RoaValidationCacheView {
|
||||
}
|
||||
|
||||
for entry in &projection.entries {
|
||||
let Some(earliest_safe_reuse_time_unix) = entry.earliest_safe_reuse_time_unix else {
|
||||
continue;
|
||||
};
|
||||
if time::OffsetDateTime::from_unix_timestamp(earliest_safe_reuse_time_unix).is_err() {
|
||||
continue;
|
||||
}
|
||||
let outputs = entry
|
||||
.outputs
|
||||
.iter()
|
||||
@@ -563,6 +570,7 @@ impl RoaValidationCacheView {
|
||||
source_object_hash: entry.source_object_hash,
|
||||
ee_serial: entry.ee_serial.clone(),
|
||||
crl_uri: entry.crl_uri.clone(),
|
||||
earliest_safe_reuse_time_unix,
|
||||
outputs_effective_until_unix: entry.outputs_effective_until_unix,
|
||||
outputs,
|
||||
},
|
||||
@@ -674,7 +682,9 @@ impl RoaValidationCacheView {
|
||||
if cached.outputs.is_empty() {
|
||||
return_entry_gate!(RoaCacheLookupResult::Miss);
|
||||
}
|
||||
if cached.outputs_effective_until_unix <= validation_time.unix_timestamp() {
|
||||
if validation_time.unix_timestamp() < cached.earliest_safe_reuse_time_unix
|
||||
|| cached.outputs_effective_until_unix <= validation_time.unix_timestamp()
|
||||
{
|
||||
return_entry_gate!(RoaCacheLookupResult::ExpiredBlocked);
|
||||
}
|
||||
|
||||
@@ -760,6 +770,10 @@ impl RoaValidationCacheView {
|
||||
source_object_hash: file.sha256,
|
||||
ee_serial: ee_serial.clone(),
|
||||
crl_uri: crl_uri.to_string(),
|
||||
earliest_safe_reuse_time: PackTime::from_utc_offset_datetime(
|
||||
time::OffsetDateTime::from_unix_timestamp(cached.earliest_safe_reuse_time_unix)
|
||||
.expect("cached ROA safe reuse time must be valid"),
|
||||
),
|
||||
}),
|
||||
};
|
||||
metrics.materialize_nanos = metrics
|
||||
@@ -821,6 +835,14 @@ fn crl_valid_at_time(
|
||||
validation_time >= this_update && validation_time < next_update
|
||||
}
|
||||
|
||||
fn roa_cache_earliest_safe_reuse_time(
|
||||
ee_not_before: time::OffsetDateTime,
|
||||
crl_this_update: time::OffsetDateTime,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> PackTime {
|
||||
PackTime::from_utc_offset_datetime(ee_not_before.max(crl_this_update).max(validation_time))
|
||||
}
|
||||
|
||||
fn record_roa_cache_block(
|
||||
stats: &mut RoaValidationCacheStats,
|
||||
lookup_result: &RoaCacheLookupResult,
|
||||
@@ -2736,6 +2758,11 @@ fn process_roa_with_issuer(
|
||||
source_object_hash: file.sha256,
|
||||
ee_serial: BigUnsigned::from_biguint(&ee.resource_cert.tbs.serial_number).bytes_be,
|
||||
crl_uri: issuer_crl_rsync_uri.to_string(),
|
||||
earliest_safe_reuse_time: roa_cache_earliest_safe_reuse_time(
|
||||
ee.resource_cert.tbs.validity_not_before,
|
||||
verified_crl.crl.this_update.utc,
|
||||
validation_time,
|
||||
),
|
||||
};
|
||||
if !collect_vcir_local_outputs {
|
||||
return Ok((vrps, Vec::new(), Some(cache_object_meta)));
|
||||
@@ -2866,6 +2893,11 @@ fn process_roa_with_issuer_parallel_cached(
|
||||
source_object_hash: file.sha256,
|
||||
ee_serial: BigUnsigned::from_biguint(&ee.resource_cert.tbs.serial_number).bytes_be,
|
||||
crl_uri: issuer_crl_rsync_uri.clone(),
|
||||
earliest_safe_reuse_time: roa_cache_earliest_safe_reuse_time(
|
||||
ee.resource_cert.tbs.validity_not_before,
|
||||
verified_crl.crl.this_update.utc,
|
||||
validation_time,
|
||||
),
|
||||
};
|
||||
if !collect_vcir_local_outputs {
|
||||
return Ok((vrps, Vec::new(), Some(cache_object_meta)));
|
||||
@@ -3908,6 +3940,7 @@ mod tests {
|
||||
source_object_hash: roa_hash,
|
||||
ee_serial: vec![0x01],
|
||||
crl_uri: TEST_CRL_URI.to_string(),
|
||||
earliest_safe_reuse_time: vcir.last_successful_validation_time.clone(),
|
||||
}],
|
||||
}),
|
||||
)
|
||||
@@ -4299,7 +4332,7 @@ mod tests {
|
||||
issuer_der,
|
||||
crl_hash,
|
||||
roa_hash,
|
||||
fixed_time("2026-06-04T00:00:00Z"),
|
||||
fixed_time("2026-06-04T01:00:00Z"),
|
||||
fixed_time("2026-06-08T00:00:00Z"),
|
||||
);
|
||||
let projection = sample_roa_cache_projection(&vcir, roa_hash);
|
||||
@@ -4313,6 +4346,58 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roa_validation_cache_view_blocks_before_safe_reuse_time() {
|
||||
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
||||
let issuer_der = b"issuer-ca";
|
||||
let crl_bytes = b"current-crl".to_vec();
|
||||
let crl_hash = sha256_32(&crl_bytes);
|
||||
let roa_hash = [0x11; 32];
|
||||
let vcir = sample_roa_cache_vcir(
|
||||
issuer_der,
|
||||
crl_hash,
|
||||
roa_hash,
|
||||
fixed_time("2026-06-07T00:00:00Z"),
|
||||
fixed_time("2026-06-08T00:00:00Z"),
|
||||
);
|
||||
let mut projection = sample_roa_cache_projection(&vcir, roa_hash);
|
||||
projection.entries[0].earliest_safe_reuse_time_unix =
|
||||
Some(fixed_time("2026-06-06T00:00:00Z").unix_timestamp());
|
||||
let view = RoaValidationCacheView::from_projection(&projection, validation_time);
|
||||
let file = PackFile::from_bytes_with_sha256(TEST_ROA_URI, vec![0x01], roa_hash);
|
||||
let mut crl_cache = sample_crl_cache(crl_bytes);
|
||||
|
||||
assert!(matches!(
|
||||
view.lookup(&file, &mut crl_cache, issuer_der, validation_time),
|
||||
RoaCacheLookupResult::ExpiredBlocked
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roa_validation_cache_view_treats_legacy_entry_without_lower_bound_as_miss() {
|
||||
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
||||
let issuer_der = b"issuer-ca";
|
||||
let crl_hash = sha256_32(b"current-crl");
|
||||
let roa_hash = [0x11; 32];
|
||||
let vcir = sample_roa_cache_vcir(
|
||||
issuer_der,
|
||||
crl_hash,
|
||||
roa_hash,
|
||||
fixed_time("2026-06-07T00:00:00Z"),
|
||||
fixed_time("2026-06-08T00:00:00Z"),
|
||||
);
|
||||
let mut projection = sample_roa_cache_projection(&vcir, roa_hash);
|
||||
projection.entries[0].earliest_safe_reuse_time_unix = None;
|
||||
let view = RoaValidationCacheView::from_projection(&projection, validation_time);
|
||||
let file = PackFile::from_bytes_with_sha256(TEST_ROA_URI, vec![0x01], roa_hash);
|
||||
let mut crl_cache = sample_crl_cache(b"current-crl".to_vec());
|
||||
|
||||
assert!(matches!(
|
||||
view.lookup(&file, &mut crl_cache, issuer_der, validation_time),
|
||||
RoaCacheLookupResult::Miss
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roa_validation_cache_stats_records_vcir_miss_to_timing() {
|
||||
let stats = RoaValidationCacheStats::for_input(RoaValidationCacheInput::enabled(None), 3);
|
||||
@@ -4594,6 +4679,7 @@ mod tests {
|
||||
source_object_hash: roa_hash,
|
||||
ee_serial: Some(vec![0x01]),
|
||||
crl_uri: None,
|
||||
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
|
||||
outputs_effective_until_unix,
|
||||
outputs: vec![output.clone()],
|
||||
},
|
||||
@@ -4610,6 +4696,7 @@ mod tests {
|
||||
source_object_hash: roa_hash,
|
||||
ee_serial: None,
|
||||
crl_uri: Some(TEST_CRL_URI.to_string()),
|
||||
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
|
||||
outputs_effective_until_unix,
|
||||
outputs: vec![output.clone()],
|
||||
},
|
||||
@@ -4626,6 +4713,7 @@ mod tests {
|
||||
source_object_hash: roa_hash,
|
||||
ee_serial: Some(vec![0x01]),
|
||||
crl_uri: Some(TEST_CRL_URI.to_string()),
|
||||
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
|
||||
outputs_effective_until_unix,
|
||||
outputs: vec![output.clone()],
|
||||
},
|
||||
@@ -4652,6 +4740,7 @@ mod tests {
|
||||
source_object_hash: [0xff; 32],
|
||||
ee_serial: Some(vec![0x01]),
|
||||
crl_uri: Some(TEST_CRL_URI.to_string()),
|
||||
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
|
||||
outputs_effective_until_unix,
|
||||
outputs: vec![output.clone()],
|
||||
},
|
||||
@@ -4668,6 +4757,7 @@ mod tests {
|
||||
source_object_hash: roa_hash,
|
||||
ee_serial: Some(vec![0x01]),
|
||||
crl_uri: Some(TEST_CRL_URI.to_string()),
|
||||
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
|
||||
outputs_effective_until_unix,
|
||||
outputs: Vec::new(),
|
||||
},
|
||||
@@ -4688,6 +4778,7 @@ mod tests {
|
||||
source_object_hash: roa_hash,
|
||||
ee_serial: Some(vec![0x01]),
|
||||
crl_uri: Some(TEST_CRL_URI.to_string()),
|
||||
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
|
||||
outputs_effective_until_unix,
|
||||
outputs: vec![output],
|
||||
},
|
||||
|
||||
@@ -33,8 +33,9 @@ use crate::storage::{
|
||||
PublicationPointCacheProjectionWriteAction, RawByHashEntry, RoaCacheProjectionContext,
|
||||
RocksStore, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole,
|
||||
VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry,
|
||||
VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType, VcirRelatedArtifact,
|
||||
VcirReplaceTimingBreakdown, VcirSourceObjectType, VcirSummary,
|
||||
VcirFailedFetchReuseIdentity, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload,
|
||||
VcirOutputType, VcirRelatedArtifact, VcirReplaceTimingBreakdown, VcirSourceObjectType,
|
||||
VcirSummary,
|
||||
};
|
||||
use crate::sync::repo::{
|
||||
sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
|
||||
@@ -1751,6 +1752,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
self.store,
|
||||
ca,
|
||||
&fresh_err,
|
||||
self.policy,
|
||||
self.validation_time,
|
||||
)
|
||||
.map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
|
||||
@@ -2314,8 +2316,18 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der<
|
||||
let issuer_ca_decode_error: Option<String>;
|
||||
let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
|
||||
Ok(v) => {
|
||||
issuer_ca_decode_error = None;
|
||||
Some(v)
|
||||
match v.validate_rfc6487_profile(crate::data_model::rc::ResourceCertificateRole::Ca) {
|
||||
Ok(()) => {
|
||||
issuer_ca_decode_error = None;
|
||||
Some(v)
|
||||
}
|
||||
Err(e) => {
|
||||
issuer_ca_decode_error = Some(format!(
|
||||
"issuer CA profile validation failed: {e} (RFC 6487 §4.8)"
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
issuer_ca_decode_error = Some(format!(
|
||||
@@ -4256,6 +4268,7 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
store: &RocksStore,
|
||||
ca: &CaInstanceHandle,
|
||||
fresh_err: &ManifestFreshError,
|
||||
policy: &Policy,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<VcirReuseProjection, String> {
|
||||
let mut warnings = Vec::new();
|
||||
@@ -4264,80 +4277,57 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
.get_vcir(&ca.manifest_rsync_uri)
|
||||
.map_err(|e| format!("load VCIR failed: {e}"))?
|
||||
else {
|
||||
warnings.push(
|
||||
Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
warnings.push(
|
||||
Warning::new(
|
||||
"no latest validated result for current CA instance; no cached output reused",
|
||||
)
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
return Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: None,
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
discovered_children: Vec::new(),
|
||||
warnings,
|
||||
});
|
||||
return Ok(failed_fetch_no_cache_projection(
|
||||
ca,
|
||||
fresh_err,
|
||||
None,
|
||||
"no latest validated result for current CA instance; no cached output reused",
|
||||
));
|
||||
};
|
||||
|
||||
if !vcir.audit_summary.failed_fetch_eligible {
|
||||
warnings.push(
|
||||
Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
warnings.push(
|
||||
Warning::new(
|
||||
"latest VCIR is not marked failed-fetch eligible; no cached output reused",
|
||||
)
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
return Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: Some(vcir),
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
discovered_children: Vec::new(),
|
||||
warnings,
|
||||
});
|
||||
return Ok(failed_fetch_no_cache_projection(
|
||||
ca,
|
||||
fresh_err,
|
||||
Some(vcir),
|
||||
"latest VCIR is not marked failed-fetch eligible; no cached output reused",
|
||||
));
|
||||
}
|
||||
|
||||
let instance_effective_until =
|
||||
parse_snapshot_time_value(&vcir.instance_gate.instance_effective_until)?;
|
||||
if validation_time > instance_effective_until {
|
||||
warnings.push(
|
||||
Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
warnings.push(
|
||||
Warning::new(
|
||||
"latest VCIR instance_gate expired; current instance contributes no cached output",
|
||||
)
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
return Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: Some(vcir),
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
discovered_children: Vec::new(),
|
||||
warnings,
|
||||
});
|
||||
let reuse_identity = match store.get_vcir_failed_fetch_reuse_identity(&ca.manifest_rsync_uri) {
|
||||
Ok(Some(identity)) => identity,
|
||||
Ok(None) => {
|
||||
return Ok(failed_fetch_no_cache_projection(
|
||||
ca,
|
||||
fresh_err,
|
||||
Some(vcir),
|
||||
"latest VCIR reuse identity is missing; no cached output or child reused",
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
return Ok(failed_fetch_no_cache_projection(
|
||||
ca,
|
||||
fresh_err,
|
||||
Some(vcir),
|
||||
&format!(
|
||||
"latest VCIR reuse identity is invalid ({error}); no cached output or child reused"
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
if !failed_fetch_reuse_identity_matches_current(
|
||||
&reuse_identity,
|
||||
&vcir,
|
||||
ca,
|
||||
policy,
|
||||
validation_time,
|
||||
) {
|
||||
return Ok(failed_fetch_no_cache_projection(
|
||||
ca,
|
||||
fresh_err,
|
||||
Some(vcir),
|
||||
"latest VCIR reuse identity does not match the current CA context or time window; no cached output or child reused",
|
||||
));
|
||||
}
|
||||
|
||||
let ccr_manifest_projection = reuse_ccr_manifest_projection_from_vcir(ca, &vcir)?;
|
||||
@@ -4367,6 +4357,73 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
})
|
||||
}
|
||||
|
||||
fn failed_fetch_no_cache_projection(
|
||||
ca: &CaInstanceHandle,
|
||||
fresh_err: &ManifestFreshError,
|
||||
vcir: Option<ValidatedCaInstanceResult>,
|
||||
reason: &str,
|
||||
) -> VcirReuseProjection {
|
||||
let warnings = vec![
|
||||
Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
Warning::new(reason)
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
];
|
||||
VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir,
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
discovered_children: Vec::new(),
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
fn failed_fetch_reuse_identity_for_fresh_result(
|
||||
ca: &CaInstanceHandle,
|
||||
policy: &Policy,
|
||||
validation_time: time::OffsetDateTime,
|
||||
effective_until: PackTime,
|
||||
) -> Result<VcirFailedFetchReuseIdentity, String> {
|
||||
let current_ca_sha256 = ca.ca_certificate_sha256_32().ok_or_else(|| {
|
||||
"current CA certificate hash unavailable for VCIR reuse identity".to_string()
|
||||
})?;
|
||||
let identity = VcirFailedFetchReuseIdentity {
|
||||
current_ca_sha256,
|
||||
ta_context_digest: ta_context_digest_for_ca(ca),
|
||||
parent_context_digest: parent_context_digest_for_ca(ca),
|
||||
policy_fingerprint: publication_point_cache_policy_fingerprint(policy),
|
||||
effective_not_before: PackTime::from_utc_offset_datetime(validation_time),
|
||||
effective_until,
|
||||
};
|
||||
identity
|
||||
.validate_internal()
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(identity)
|
||||
}
|
||||
|
||||
fn failed_fetch_reuse_identity_matches_current(
|
||||
cached: &VcirFailedFetchReuseIdentity,
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
ca: &CaInstanceHandle,
|
||||
policy: &Policy,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> bool {
|
||||
let Some(current_ca_sha256) = ca.ca_certificate_sha256_32() else {
|
||||
return false;
|
||||
};
|
||||
cached.current_ca_sha256 == current_ca_sha256
|
||||
&& cached.ta_context_digest == ta_context_digest_for_ca(ca)
|
||||
&& cached.parent_context_digest == parent_context_digest_for_ca(ca)
|
||||
&& cached.policy_fingerprint == publication_point_cache_policy_fingerprint(policy)
|
||||
&& cached.effective_until == vcir.instance_gate.instance_effective_until
|
||||
&& cached.contains_validation_time(validation_time)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn reconstruct_snapshot_from_vcir(
|
||||
store: &RocksStore,
|
||||
@@ -5280,8 +5337,14 @@ fn persist_vcir_for_fresh_result_with_timing(
|
||||
.expect("publication point projection must exist when guard is not active"),
|
||||
)
|
||||
};
|
||||
let failed_fetch_reuse_identity = failed_fetch_reuse_identity_for_fresh_result(
|
||||
ca,
|
||||
policy,
|
||||
validation_time,
|
||||
vcir.instance_gate.instance_effective_until.clone(),
|
||||
)?;
|
||||
let replace_timing = store
|
||||
.replace_vcir_manifest_replay_meta_and_projection_action(
|
||||
.replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
|
||||
&vcir,
|
||||
Some(&RoaCacheProjectionContext {
|
||||
parent_context_digest: parent_context_digest_for_ca(ca),
|
||||
@@ -5289,6 +5352,7 @@ fn persist_vcir_for_fresh_result_with_timing(
|
||||
object_meta: objects.roa_cache_object_meta.clone(),
|
||||
}),
|
||||
publication_point_cache_projection_action,
|
||||
Some(&failed_fetch_reuse_identity),
|
||||
)
|
||||
.map_err(|e| format!("store VCIR and manifest replay meta failed: {e}"))?;
|
||||
timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64;
|
||||
|
||||
@@ -753,6 +753,44 @@ fn sample_vcir_for_projection(
|
||||
}
|
||||
}
|
||||
|
||||
fn put_vcir_for_failed_fetch_reuse(
|
||||
store: &RocksStore,
|
||||
ca: &CaInstanceHandle,
|
||||
policy: &Policy,
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
) {
|
||||
let validation_time = vcir
|
||||
.last_successful_validation_time
|
||||
.parse()
|
||||
.expect("parse VCIR validation time");
|
||||
let identity = failed_fetch_reuse_identity_for_fresh_result(
|
||||
ca,
|
||||
policy,
|
||||
validation_time,
|
||||
vcir.instance_gate.instance_effective_until.clone(),
|
||||
)
|
||||
.expect("build VCIR failed-fetch reuse identity");
|
||||
store
|
||||
.put_vcir_with_failed_fetch_reuse_identity(vcir, &identity)
|
||||
.expect("put reusable VCIR");
|
||||
}
|
||||
|
||||
fn sample_ca_for_failed_fetch_reuse(vcir: &ValidatedCaInstanceResult) -> CaInstanceHandle {
|
||||
CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "test-tal".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn never_http_fetcher_returns_error() {
|
||||
let f = NeverHttpFetcher;
|
||||
@@ -3827,7 +3865,6 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
|
||||
let repo_bytes_db = store_dir.path().join("repo-bytes.db");
|
||||
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
|
||||
.expect("open rocksdb with external repo bytes");
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
store
|
||||
.put_blob_bytes_batch(&[(child_cert_hash.clone(), g.child_ca_der.clone())])
|
||||
.expect("put child cert repo bytes");
|
||||
@@ -3852,6 +3889,8 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
|
||||
};
|
||||
let policy = Policy::default();
|
||||
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
|
||||
|
||||
let projection = project_current_instance_vcir_on_failed_fetch(
|
||||
&store,
|
||||
@@ -3859,6 +3898,7 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&policy,
|
||||
now,
|
||||
)
|
||||
.expect("project vcir");
|
||||
@@ -3913,6 +3953,79 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_current_instance_vcir_does_not_reuse_without_identity() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let child_cert_hash = sha256_hex(b"child-cert");
|
||||
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
store.put_vcir(&vcir).expect("put legacy-style VCIR");
|
||||
let ca = sample_ca_for_failed_fetch_reuse(&vcir);
|
||||
|
||||
let projection = project_current_instance_vcir_on_failed_fetch(
|
||||
&store,
|
||||
&ca,
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&Policy::default(),
|
||||
now,
|
||||
)
|
||||
.expect("project VCIR");
|
||||
|
||||
assert_eq!(
|
||||
projection.source,
|
||||
PublicationPointSource::FailedFetchNoCache
|
||||
);
|
||||
assert!(projection.objects.vrps.is_empty());
|
||||
assert!(projection.discovered_children.is_empty());
|
||||
assert!(
|
||||
projection
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| warning.message.contains("reuse identity is missing"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_current_instance_vcir_does_not_reuse_when_ta_context_changes() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let child_cert_hash = sha256_hex(b"child-cert");
|
||||
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy::default();
|
||||
let original_ca = sample_ca_for_failed_fetch_reuse(&vcir);
|
||||
put_vcir_for_failed_fetch_reuse(&store, &original_ca, &policy, &vcir);
|
||||
let mut changed_ca = original_ca;
|
||||
changed_ca.tal_id = "different-tal".to_string();
|
||||
|
||||
let projection = project_current_instance_vcir_on_failed_fetch(
|
||||
&store,
|
||||
&changed_ca,
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&policy,
|
||||
now,
|
||||
)
|
||||
.expect("project VCIR");
|
||||
|
||||
assert_eq!(
|
||||
projection.source,
|
||||
PublicationPointSource::FailedFetchNoCache
|
||||
);
|
||||
assert!(projection.objects.vrps.is_empty());
|
||||
assert!(projection.discovered_children.is_empty());
|
||||
assert!(
|
||||
projection
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| warning.message.contains("identity does not match"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
@@ -3950,6 +4063,7 @@ fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired()
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&Policy::default(),
|
||||
now,
|
||||
)
|
||||
.expect("project vcir");
|
||||
@@ -3975,7 +4089,6 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
|
||||
let repo_bytes_db = store_dir.path().join("repo-bytes.db");
|
||||
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
|
||||
.expect("open rocksdb with external repo bytes");
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
store
|
||||
.put_blob_bytes_batch(&[(child_cert_hash, b"child-cert".to_vec())])
|
||||
.expect("put child cert repo bytes");
|
||||
@@ -3993,6 +4106,8 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
let policy = Policy::default();
|
||||
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
|
||||
|
||||
let projection = project_current_instance_vcir_on_failed_fetch(
|
||||
&store,
|
||||
@@ -4000,6 +4115,7 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
|
||||
&ManifestFreshError::HashMismatch {
|
||||
rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
|
||||
},
|
||||
&policy,
|
||||
now,
|
||||
)
|
||||
.expect("project vcir");
|
||||
@@ -4048,6 +4164,7 @@ fn project_current_instance_vcir_returns_no_output_when_latest_result_missing()
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&Policy::default(),
|
||||
now,
|
||||
)
|
||||
.expect("project without cached vcir");
|
||||
@@ -4099,6 +4216,7 @@ fn project_current_instance_vcir_returns_no_output_when_latest_result_is_ineligi
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&Policy::default(),
|
||||
now,
|
||||
)
|
||||
.expect("project ineligible vcir");
|
||||
@@ -4128,7 +4246,6 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
|
||||
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
@@ -4143,6 +4260,8 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
let policy = Policy::default();
|
||||
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
|
||||
|
||||
let err = project_current_instance_vcir_on_failed_fetch(
|
||||
&store,
|
||||
@@ -4150,6 +4269,7 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
&policy,
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
@@ -383,7 +383,7 @@ fn metrics_for(obj_type: ObjType, der: &[u8]) -> Result<Metrics, String> {
|
||||
ObjType::Cer => {
|
||||
let parsed = ResourceCertificate::parse_der(der).map_err(|e| e.to_string())?;
|
||||
let spki_len = parsed.subject_public_key_info.len() as u32;
|
||||
let ext_count = (parsed.extensions.basic_constraints_ca.len()
|
||||
let ext_count = (parsed.extensions.basic_constraints.len()
|
||||
+ parsed.extensions.subject_key_identifier.len()
|
||||
+ parsed.extensions.authority_key_identifier.len()
|
||||
+ parsed.extensions.crl_distribution_points.len()
|
||||
|
||||
@@ -25,6 +25,7 @@ fn dummy_ee(
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
basic_constraints: None,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
@@ -35,6 +36,8 @@ fn dummy_ee(
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
certificate_policies: None,
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
|
||||
@@ -81,6 +81,7 @@ CN = Test Issuer CA
|
||||
[ v3_issuer_ca ]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
certificatePolicies = critical,1.3.6.1.5.5.7.14.2
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
|
||||
|
||||
@@ -88,6 +88,18 @@ fn offline_discovery_from_apnic_tal_and_ta_der_fixture_works() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_discovery_rejects_tampered_ta_self_signature() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let mut ta_der = apnic_ta_der();
|
||||
let last = ta_der.last_mut().expect("TA fixture must not be empty");
|
||||
*last ^= 0x01;
|
||||
|
||||
let err = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None)
|
||||
.expect_err("tampered TA signature must not form a root CA instance");
|
||||
assert!(err.to_string().contains("self-signature"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_from_tal_url_works_with_mock_fetcher() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
|
||||
131
tests/test_resource_certificate_profile.rs
Normal file
131
tests/test_resource_certificate_profile.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use rpki::data_model::common::X509NameDer;
|
||||
use rpki::data_model::oid::{
|
||||
OID_AUTONOMOUS_SYS_IDS, OID_BASIC_CONSTRAINTS, OID_CERTIFICATE_POLICIES, OID_CP_IPADDR_ASNUMBER,
|
||||
};
|
||||
use rpki::data_model::rc::{
|
||||
AsIdOrRange, AsIdentifierChoice, AsResourceSet, BasicConstraintsProfile,
|
||||
CertificatePoliciesProfile, RcExtensions, ResourceCertKind, ResourceCertificate,
|
||||
ResourceCertificateProfileError, ResourceCertificateRole, RpkixTbsCertificate,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
fn valid_ca_certificate() -> ResourceCertificate {
|
||||
let validation_time = OffsetDateTime::from_unix_timestamp(0).expect("valid epoch");
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind: ResourceCertKind::Ca,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u32),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_name: X509NameDer(b"CN=issuer".to_vec()),
|
||||
subject_name: X509NameDer(b"CN=subject".to_vec()),
|
||||
validity_not_before: validation_time,
|
||||
validity_not_after: validation_time,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
basic_constraints: Some(BasicConstraintsProfile {
|
||||
ca: true,
|
||||
critical: true,
|
||||
path_len_constraint: None,
|
||||
}),
|
||||
subject_key_identifier: None,
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies: Some(CertificatePoliciesProfile {
|
||||
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
|
||||
qualifier_oids: Vec::new(),
|
||||
}),
|
||||
extension_oids: vec![
|
||||
OID_BASIC_CONSTRAINTS.to_string(),
|
||||
OID_CERTIFICATE_POLICIES.to_string(),
|
||||
],
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_profile_accepts_required_basic_constraints_and_policy() {
|
||||
valid_ca_certificate()
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.expect("valid CA profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_profile_rejects_noncritical_basic_constraints() {
|
||||
let mut certificate = valid_ca_certificate();
|
||||
certificate
|
||||
.tbs
|
||||
.extensions
|
||||
.basic_constraints
|
||||
.as_mut()
|
||||
.expect("constraints")
|
||||
.critical = false;
|
||||
|
||||
assert!(matches!(
|
||||
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
|
||||
Err(ResourceCertificateProfileError::BasicConstraintsCriticality)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_profile_rejects_unknown_extension() {
|
||||
let mut certificate = valid_ca_certificate();
|
||||
certificate
|
||||
.tbs
|
||||
.extensions
|
||||
.extension_oids
|
||||
.push("1.2.3.4".to_string());
|
||||
|
||||
assert!(matches!(
|
||||
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
|
||||
Err(ResourceCertificateProfileError::DisallowedExtension { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_profile_rejects_non_cps_policy_qualifier() {
|
||||
let mut certificate = valid_ca_certificate();
|
||||
certificate
|
||||
.tbs
|
||||
.extensions
|
||||
.certificate_policies
|
||||
.as_mut()
|
||||
.expect("certificate policies")
|
||||
.qualifier_oids
|
||||
.push("1.2.3.4".to_string());
|
||||
|
||||
assert!(matches!(
|
||||
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
|
||||
Err(ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_profile_rejects_autonomous_system_rdi() {
|
||||
let mut certificate = valid_ca_certificate();
|
||||
certificate.tbs.extensions.as_resources = Some(AsResourceSet {
|
||||
asnum: None,
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
64512,
|
||||
)])),
|
||||
});
|
||||
certificate
|
||||
.tbs
|
||||
.extensions
|
||||
.extension_oids
|
||||
.push(OID_AUTONOMOUS_SYS_IDS.to_string());
|
||||
|
||||
assert!(matches!(
|
||||
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
|
||||
Err(ResourceCertificateProfileError::AsResourcesRdiPresent)
|
||||
));
|
||||
}
|
||||
@@ -28,6 +28,7 @@ fn dummy_ee(
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
basic_constraints: None,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
@@ -38,6 +39,8 @@ fn dummy_ee(
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
certificate_policies: None,
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
|
||||
@@ -794,13 +794,22 @@ fn ee_certificate_missing_signed_object_sia_is_rejected() {
|
||||
));
|
||||
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
let rejected_for_sia = matches!(
|
||||
&err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateMissingSia)
|
||||
| SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::EeCertificateMissingSignedObjectSia
|
||||
)
|
||||
));
|
||||
);
|
||||
let rejected_for_ee_profile = matches!(
|
||||
&err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateParse(reason))
|
||||
if reason.contains("basicConstraints must be absent")
|
||||
);
|
||||
assert!(
|
||||
rejected_for_sia || rejected_for_ee_profile,
|
||||
"unexpected rejection reason: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -834,12 +843,21 @@ fn ee_certificate_sia_without_signed_object_access_method_is_rejected() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
let rejected_for_sia = matches!(
|
||||
&err,
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::EeCertificateMissingSignedObjectSia
|
||||
)
|
||||
));
|
||||
);
|
||||
let rejected_for_ee_profile = matches!(
|
||||
&err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateParse(reason))
|
||||
if reason.contains("basicConstraints must be absent")
|
||||
);
|
||||
assert!(
|
||||
rejected_for_sia || rejected_for_ee_profile,
|
||||
"unexpected rejection reason: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -68,12 +68,15 @@ fn ta_certificate_rejects_non_self_signed_ca() {
|
||||
fn ta_constraints_require_policies_and_ski() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
basic_constraints: None,
|
||||
subject_key_identifier: None,
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
certificate_policies: None,
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
as_resources: None,
|
||||
});
|
||||
@@ -97,12 +100,15 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
// Missing both IP and AS resources.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
basic_constraints: None,
|
||||
subject_key_identifier: Some(vec![1]),
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies: None,
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use rpki::data_model::ta::{TrustAnchor, TrustAnchorBindError, TrustAnchorError};
|
||||
use rpki::data_model::ta::{
|
||||
TaCertificateVerifyError, TrustAnchor, TrustAnchorBindError, TrustAnchorError,
|
||||
};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use url::Url;
|
||||
|
||||
@@ -76,3 +78,21 @@ fn bind_rejects_resolved_uri_not_listed_in_tal() {
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_tampered_ta_self_signature() {
|
||||
let tal_raw =
|
||||
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let mut ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read TA fixture");
|
||||
|
||||
let last = ta_der.last_mut().expect("TA fixture must not be empty");
|
||||
*last ^= 0x01;
|
||||
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind_der(tal, &ta_der, None),
|
||||
Err(TrustAnchorError::TaSelfSignature(
|
||||
TaCertificateVerifyError::InvalidSelfSignature(_)
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user