Compare commits
12 Commits
cf764c35bb
...
dev_1.0.0_
| Author | SHA1 | Date | |
|---|---|---|---|
| af1c2c7f88 | |||
| e45830d79f | |||
| 77fc2f1a41 | |||
| e083fe4daa | |||
| c9ef5aaf4c | |||
| 34fb9657f1 | |||
| 6edc420ce2 | |||
| cd0ba15286 | |||
| fe8b89d829 | |||
| d6d44669b4 | |||
| 557a69cbd2 | |||
| 73d8ebb5c1 |
@@ -26,10 +26,11 @@ toml = "0.8.20"
|
|||||||
rocksdb = { version = "0.22.0", optional = true, default-features = false, features = ["lz4"] }
|
rocksdb = { version = "0.22.0", optional = true, default-features = false, features = ["lz4"] }
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
roxmltree = "0.20.0"
|
roxmltree = "0.20.0"
|
||||||
|
quick-xml = "0.37.2"
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||||
pprof = { version = "0.14.1", optional = true, features = ["flamegraph", "prost-codec"] }
|
pprof = { version = "0.14.1", optional = true, features = ["flamegraph", "prost-codec"] }
|
||||||
flate2 = { version = "1.0.35", optional = true }
|
flate2 = { version = "1.0.35", optional = true }
|
||||||
|
tempfile = "3.16.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.16.0"
|
|
||||||
|
|||||||
56
scripts/cir/README.md
Normal file
56
scripts/cir/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# CIR Scripts
|
||||||
|
|
||||||
|
## `cir-rsync-wrapper`
|
||||||
|
|
||||||
|
一个用于 CIR 黑盒 replay 的 rsync wrapper。
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
- `REAL_RSYNC_BIN`
|
||||||
|
- 真实 rsync 二进制路径
|
||||||
|
- 默认优先 `/usr/bin/rsync`
|
||||||
|
- `CIR_MIRROR_ROOT`
|
||||||
|
- 本地镜像树根目录
|
||||||
|
- 当命令行中出现 `rsync://...` source 时必需
|
||||||
|
|
||||||
|
### 语义
|
||||||
|
|
||||||
|
- 仅改写 `rsync://host/path` 类型参数
|
||||||
|
- 其它参数原样透传给真实 rsync
|
||||||
|
- 改写目标:
|
||||||
|
- `rsync://example.net/repo/a.roa`
|
||||||
|
- →
|
||||||
|
- `<CIR_MIRROR_ROOT>/example.net/repo/a.roa`
|
||||||
|
|
||||||
|
### 兼容目标
|
||||||
|
|
||||||
|
- Routinator `--rsync-command`
|
||||||
|
- `rpki-client -e rsync_prog`
|
||||||
|
|
||||||
|
## 其它脚本
|
||||||
|
|
||||||
|
- `run_cir_replay_ours.sh`
|
||||||
|
- `run_cir_replay_routinator.sh`
|
||||||
|
- `run_cir_replay_rpki_client.sh`
|
||||||
|
- `run_cir_replay_matrix.sh`
|
||||||
|
|
||||||
|
## `cir-local-link-sync.py`
|
||||||
|
|
||||||
|
当 `CIR_LOCAL_LINK_MODE=1` 且 wrapper 检测到 source 已经被改写为本地 mirror 路径时,
|
||||||
|
wrapper 不再调用真实 `rsync`,而是调用这个 helper 完成:
|
||||||
|
|
||||||
|
- `hardlink` 优先的本地树同步
|
||||||
|
- 失败时回退到 copy
|
||||||
|
- 支持 `--delete`
|
||||||
|
|
||||||
|
`run_cir_replay_matrix.sh` 会顺序执行:
|
||||||
|
|
||||||
|
- `ours`
|
||||||
|
- Routinator
|
||||||
|
- `rpki-client`
|
||||||
|
|
||||||
|
并汇总生成:
|
||||||
|
|
||||||
|
- `summary.json`
|
||||||
|
- `summary.md`
|
||||||
|
- `detail.md`
|
||||||
BIN
scripts/cir/__pycache__/cir-local-link-sync.cpython-312.pyc
Normal file
BIN
scripts/cir/__pycache__/cir-local-link-sync.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scripts/cir/__pycache__/cir-rsync-wrappercpython-312.pyc
Normal file
BIN
scripts/cir/__pycache__/cir-rsync-wrappercpython-312.pyc
Normal file
Binary file not shown.
BIN
scripts/cir/__pycache__/json_to_vaps_csv.cpython-312.pyc
Normal file
BIN
scripts/cir/__pycache__/json_to_vaps_csv.cpython-312.pyc
Normal file
Binary file not shown.
136
scripts/cir/cir-local-link-sync.py
Executable file
136
scripts/cir/cir-local-link-sync.py
Executable file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _same_inode(src: Path, dst: Path) -> bool:
|
||||||
|
try:
|
||||||
|
src_stat = src.stat()
|
||||||
|
dst_stat = dst.stat()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
return (src_stat.st_dev, src_stat.st_ino) == (dst_stat.st_dev, dst_stat.st_ino)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_path(path: Path) -> None:
|
||||||
|
if not path.exists() and not path.is_symlink():
|
||||||
|
return
|
||||||
|
if path.is_dir() and not path.is_symlink():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def _prune_empty_dirs(root: Path) -> None:
|
||||||
|
if not root.exists():
|
||||||
|
return
|
||||||
|
for path in sorted((p for p in root.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True):
|
||||||
|
try:
|
||||||
|
path.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _link_or_copy(src: Path, dst: Path) -> str:
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if dst.exists() or dst.is_symlink():
|
||||||
|
if _same_inode(src, dst):
|
||||||
|
return "reused"
|
||||||
|
_remove_path(dst)
|
||||||
|
try:
|
||||||
|
os.link(src, dst)
|
||||||
|
return "linked"
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno not in (errno.EXDEV, errno.EPERM, errno.EMLINK, errno.ENOTSUP, errno.EACCES):
|
||||||
|
raise
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
return "copied"
|
||||||
|
|
||||||
|
|
||||||
|
def _file_map(src_arg: str, dest_arg: str) -> tuple[Path, dict[str, Path]]:
|
||||||
|
src = Path(src_arg.rstrip(os.sep))
|
||||||
|
if not src.exists():
|
||||||
|
raise FileNotFoundError(src)
|
||||||
|
mapping: dict[str, Path] = {}
|
||||||
|
if src.is_dir():
|
||||||
|
copy_contents = src_arg.endswith(os.sep)
|
||||||
|
if copy_contents:
|
||||||
|
root = src
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if path.is_file():
|
||||||
|
mapping[path.relative_to(root).as_posix()] = path
|
||||||
|
else:
|
||||||
|
root = src
|
||||||
|
base = src.name
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if path.is_file():
|
||||||
|
rel = Path(base) / path.relative_to(root)
|
||||||
|
mapping[rel.as_posix()] = path
|
||||||
|
else:
|
||||||
|
dest_path = Path(dest_arg)
|
||||||
|
if dest_arg.endswith(os.sep) or dest_path.is_dir():
|
||||||
|
mapping[src.name] = src
|
||||||
|
else:
|
||||||
|
mapping[dest_path.name] = src
|
||||||
|
return Path(dest_arg), mapping
|
||||||
|
|
||||||
|
|
||||||
|
def sync_local_tree(src_arg: str, dst_arg: str, delete: bool) -> dict[str, int]:
|
||||||
|
dst_root, mapping = _file_map(src_arg, dst_arg)
|
||||||
|
dst_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
expected = {dst_root / rel for rel in mapping.keys()}
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
if delete and dst_root.exists():
|
||||||
|
for path in sorted(dst_root.rglob("*"), key=lambda p: len(p.parts), reverse=True):
|
||||||
|
if path.is_dir():
|
||||||
|
continue
|
||||||
|
if path not in expected:
|
||||||
|
_remove_path(path)
|
||||||
|
deleted += 1
|
||||||
|
_prune_empty_dirs(dst_root)
|
||||||
|
|
||||||
|
linked = 0
|
||||||
|
copied = 0
|
||||||
|
reused = 0
|
||||||
|
for rel, src in mapping.items():
|
||||||
|
dst = dst_root / rel
|
||||||
|
result = _link_or_copy(src, dst)
|
||||||
|
if result == "linked":
|
||||||
|
linked += 1
|
||||||
|
elif result == "copied":
|
||||||
|
copied += 1
|
||||||
|
else:
|
||||||
|
reused += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"files": len(mapping),
|
||||||
|
"linked": linked,
|
||||||
|
"copied": copied,
|
||||||
|
"reused": reused,
|
||||||
|
"deleted": deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Sync a local CIR mirror tree using hardlinks when possible.")
|
||||||
|
parser.add_argument("--delete", action="store_true", help="Delete target files not present in source")
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("dest")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
summary = sync_local_tree(args.source, args.dest, args.delete)
|
||||||
|
print(
|
||||||
|
"local-link-sync files={files} linked={linked} copied={copied} reused={reused} deleted={deleted}".format(
|
||||||
|
**summary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
127
scripts/cir/cir-rsync-wrapper
Executable file
127
scripts/cir/cir-rsync-wrapper
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def real_rsync_bin() -> str:
|
||||||
|
env = os.environ.get("REAL_RSYNC_BIN")
|
||||||
|
if env:
|
||||||
|
return env
|
||||||
|
default = "/usr/bin/rsync"
|
||||||
|
if Path(default).exists():
|
||||||
|
return default
|
||||||
|
found = shutil.which("rsync")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
raise SystemExit("cir-rsync-wrapper: REAL_RSYNC_BIN is not set and rsync was not found")
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_arg(arg: str, mirror_root: str | None) -> str:
|
||||||
|
if not arg.startswith("rsync://"):
|
||||||
|
return arg
|
||||||
|
if not mirror_root:
|
||||||
|
raise SystemExit(
|
||||||
|
"cir-rsync-wrapper: CIR_MIRROR_ROOT is required when an rsync:// source is present"
|
||||||
|
)
|
||||||
|
parsed = urlparse(arg)
|
||||||
|
if parsed.scheme != "rsync" or not parsed.hostname:
|
||||||
|
raise SystemExit(f"cir-rsync-wrapper: invalid rsync URI: {arg}")
|
||||||
|
path = parsed.path.lstrip("/")
|
||||||
|
local = Path(mirror_root).resolve() / parsed.hostname
|
||||||
|
if path:
|
||||||
|
local = local / path
|
||||||
|
local_str = str(local)
|
||||||
|
if local.exists() and local.is_dir() and not local_str.endswith("/"):
|
||||||
|
local_str += "/"
|
||||||
|
elif arg.endswith("/") and not local_str.endswith("/"):
|
||||||
|
local_str += "/"
|
||||||
|
return local_str
|
||||||
|
|
||||||
|
|
||||||
|
def filter_args(args: list[str]) -> list[str]:
|
||||||
|
mirror_root = os.environ.get("CIR_MIRROR_ROOT")
|
||||||
|
rewritten_any = any(arg.startswith("rsync://") for arg in args)
|
||||||
|
out: list[str] = []
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
arg = args[i]
|
||||||
|
if rewritten_any:
|
||||||
|
if arg == "--address":
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if arg.startswith("--address="):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if arg == "--contimeout":
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if arg.startswith("--contimeout="):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
out.append(rewrite_arg(arg, mirror_root))
|
||||||
|
i += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def local_link_mode_enabled() -> bool:
|
||||||
|
value = os.environ.get("CIR_LOCAL_LINK_MODE", "")
|
||||||
|
return value.lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_source_and_dest(args: list[str]) -> tuple[str, str]:
|
||||||
|
expects_value = {
|
||||||
|
"--timeout",
|
||||||
|
"--min-size",
|
||||||
|
"--max-size",
|
||||||
|
"--include",
|
||||||
|
"--exclude",
|
||||||
|
"--compare-dest",
|
||||||
|
}
|
||||||
|
positionals: list[str] = []
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
arg = args[i]
|
||||||
|
if arg in expects_value:
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if any(arg.startswith(prefix + "=") for prefix in expects_value):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if arg.startswith("-"):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
positionals.append(arg)
|
||||||
|
i += 1
|
||||||
|
if len(positionals) < 2:
|
||||||
|
raise SystemExit("cir-rsync-wrapper: expected source and destination arguments")
|
||||||
|
return positionals[-2], positionals[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_exec_local_link_sync(args: list[str], rewritten_any: bool) -> None:
|
||||||
|
if not rewritten_any or not local_link_mode_enabled():
|
||||||
|
return
|
||||||
|
source, dest = extract_source_and_dest(args)
|
||||||
|
if source.startswith("rsync://"):
|
||||||
|
raise SystemExit("cir-rsync-wrapper: expected rewritten local source for CIR_LOCAL_LINK_MODE")
|
||||||
|
helper = Path(__file__).with_name("cir-local-link-sync.py")
|
||||||
|
cmd = [sys.executable, str(helper)]
|
||||||
|
if "--delete" in args:
|
||||||
|
cmd.append("--delete")
|
||||||
|
cmd.extend([source, dest])
|
||||||
|
os.execv(sys.executable, cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
rewritten_any = any(arg.startswith("rsync://") for arg in args)
|
||||||
|
rewritten = filter_args(args)
|
||||||
|
maybe_exec_local_link_sync(rewritten, rewritten_any)
|
||||||
|
os.execv(real_rsync_bin(), [real_rsync_bin(), *rewritten])
|
||||||
|
return 127
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
32
scripts/cir/fetch_cir_sequence_from_remote.sh
Executable file
32
scripts/cir/fetch_cir_sequence_from_remote.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/fetch_cir_sequence_from_remote.sh \
|
||||||
|
--ssh-target <user@host> \
|
||||||
|
--remote-path <path> \
|
||||||
|
--local-path <path>
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
SSH_TARGET=""
|
||||||
|
REMOTE_PATH=""
|
||||||
|
LOCAL_PATH=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--remote-path) REMOTE_PATH="$2"; shift 2 ;;
|
||||||
|
--local-path) LOCAL_PATH="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SSH_TARGET" && -n "$REMOTE_PATH" && -n "$LOCAL_PATH" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOCAL_PATH")"
|
||||||
|
rsync -a "$SSH_TARGET:$REMOTE_PATH/" "$LOCAL_PATH/"
|
||||||
|
echo "done: $LOCAL_PATH"
|
||||||
50
scripts/cir/json_to_vaps_csv.py
Executable file
50
scripts/cir/json_to_vaps_csv.py
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_asn(value: str | int) -> str:
|
||||||
|
text = str(value).strip().upper()
|
||||||
|
if text.startswith("AS"):
|
||||||
|
text = text[2:]
|
||||||
|
return f"AS{int(text)}"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--input", required=True, type=Path)
|
||||||
|
parser.add_argument("--csv-out", required=True, type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
obj = json.loads(args.input.read_text(encoding="utf-8"))
|
||||||
|
rows: list[tuple[str, str, str]] = []
|
||||||
|
for aspa in obj.get("aspas", []):
|
||||||
|
providers = sorted(
|
||||||
|
{normalize_asn(item) for item in aspa.get("providers", [])},
|
||||||
|
key=lambda s: int(s[2:]),
|
||||||
|
)
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
normalize_asn(aspa["customer"]),
|
||||||
|
";".join(providers),
|
||||||
|
str(aspa.get("ta", "")).strip().lower(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows.sort(key=lambda row: (int(row[0][2:]), row[1], row[2]))
|
||||||
|
|
||||||
|
args.csv_out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with args.csv_out.open("w", encoding="utf-8", newline="") as fh:
|
||||||
|
writer = csv.writer(fh)
|
||||||
|
writer.writerow(["Customer ASN", "Providers", "Trust Anchor"])
|
||||||
|
writer.writerows(rows)
|
||||||
|
print(args.csv_out)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
84
scripts/cir/run_cir_drop_sequence.sh
Executable file
84
scripts/cir/run_cir_drop_sequence.sh
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_drop_sequence.sh \
|
||||||
|
--sequence-root <path> \
|
||||||
|
[--drop-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SEQUENCE_ROOT=""
|
||||||
|
DROP_BIN="${DROP_BIN:-$ROOT_DIR/target/release/cir_drop_report}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--sequence-root) SEQUENCE_ROOT="$2"; shift 2 ;;
|
||||||
|
--drop-bin) DROP_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SEQUENCE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
python3 - <<'PY' "$SEQUENCE_ROOT" "$DROP_BIN"
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sequence_root = Path(sys.argv[1]).resolve()
|
||||||
|
drop_bin = sys.argv[2]
|
||||||
|
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
|
||||||
|
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
|
||||||
|
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
|
||||||
|
backend_count = sum(x is not None for x in (static_root, raw_store_db))
|
||||||
|
if backend_count != 1:
|
||||||
|
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
|
||||||
|
|
||||||
|
summaries = []
|
||||||
|
for step in sequence["steps"]:
|
||||||
|
step_id = step["stepId"]
|
||||||
|
out_dir = sequence_root / "drop" / step_id
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
cmd = [
|
||||||
|
drop_bin,
|
||||||
|
"--cir",
|
||||||
|
str(sequence_root / step["cirPath"]),
|
||||||
|
"--ccr",
|
||||||
|
str(sequence_root / step["ccrPath"]),
|
||||||
|
"--report-json",
|
||||||
|
str(sequence_root / step["reportPath"]),
|
||||||
|
"--json-out",
|
||||||
|
str(out_dir / "drop.json"),
|
||||||
|
"--md-out",
|
||||||
|
str(out_dir / "drop.md"),
|
||||||
|
]
|
||||||
|
if static_root is not None:
|
||||||
|
cmd.extend(["--static-root", str(static_root)])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--raw-store-db", str(raw_store_db)])
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise SystemExit(
|
||||||
|
f"drop report failed for {step_id}: stdout={proc.stdout} stderr={proc.stderr}"
|
||||||
|
)
|
||||||
|
result = json.loads((out_dir / "drop.json").read_text(encoding="utf-8"))
|
||||||
|
summaries.append(
|
||||||
|
{
|
||||||
|
"stepId": step_id,
|
||||||
|
"droppedVrpCount": result["summary"]["droppedVrpCount"],
|
||||||
|
"droppedObjectCount": result["summary"]["droppedObjectCount"],
|
||||||
|
"reportPath": str(out_dir / "drop.json"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {"version": 1, "steps": summaries}
|
||||||
|
(sequence_root / "drop-summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $SEQUENCE_ROOT"
|
||||||
173
scripts/cir/run_cir_record_full_delta.sh
Executable file
173
scripts/cir/run_cir_record_full_delta.sh
Executable file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_full_delta.sh \
|
||||||
|
--out-dir <path> \
|
||||||
|
--tal-path <path> \
|
||||||
|
--ta-path <path> \
|
||||||
|
--cir-tal-uri <url> \
|
||||||
|
--payload-replay-archive <path> \
|
||||||
|
--payload-replay-locks <path> \
|
||||||
|
--payload-base-archive <path> \
|
||||||
|
--payload-base-locks <path> \
|
||||||
|
--payload-delta-archive <path> \
|
||||||
|
--payload-delta-locks <path> \
|
||||||
|
[--base-validation-time <rfc3339>] \
|
||||||
|
[--delta-validation-time <rfc3339>] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>] \
|
||||||
|
[--rpki-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
OUT_DIR=""
|
||||||
|
TAL_PATH=""
|
||||||
|
TA_PATH=""
|
||||||
|
CIR_TAL_URI=""
|
||||||
|
PAYLOAD_REPLAY_ARCHIVE=""
|
||||||
|
PAYLOAD_REPLAY_LOCKS=""
|
||||||
|
PAYLOAD_BASE_ARCHIVE=""
|
||||||
|
PAYLOAD_BASE_LOCKS=""
|
||||||
|
PAYLOAD_DELTA_ARCHIVE=""
|
||||||
|
PAYLOAD_DELTA_LOCKS=""
|
||||||
|
BASE_VALIDATION_TIME=""
|
||||||
|
DELTA_VALIDATION_TIME=""
|
||||||
|
MAX_DEPTH=0
|
||||||
|
MAX_INSTANCES=1
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--tal-path) TAL_PATH="$2"; shift 2 ;;
|
||||||
|
--ta-path) TA_PATH="$2"; shift 2 ;;
|
||||||
|
--cir-tal-uri) CIR_TAL_URI="$2"; shift 2 ;;
|
||||||
|
--payload-replay-archive) PAYLOAD_REPLAY_ARCHIVE="$2"; shift 2 ;;
|
||||||
|
--payload-replay-locks) PAYLOAD_REPLAY_LOCKS="$2"; shift 2 ;;
|
||||||
|
--payload-base-archive) PAYLOAD_BASE_ARCHIVE="$2"; shift 2 ;;
|
||||||
|
--payload-base-locks) PAYLOAD_BASE_LOCKS="$2"; shift 2 ;;
|
||||||
|
--payload-delta-archive) PAYLOAD_DELTA_ARCHIVE="$2"; shift 2 ;;
|
||||||
|
--payload-delta-locks) PAYLOAD_DELTA_LOCKS="$2"; shift 2 ;;
|
||||||
|
--base-validation-time) BASE_VALIDATION_TIME="$2"; shift 2 ;;
|
||||||
|
--delta-validation-time) DELTA_VALIDATION_TIME="$2"; shift 2 ;;
|
||||||
|
--max-depth) MAX_DEPTH="$2"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="$2"; shift 2 ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$OUT_DIR" && -n "$TAL_PATH" && -n "$TA_PATH" && -n "$CIR_TAL_URI" && -n "$PAYLOAD_REPLAY_ARCHIVE" && -n "$PAYLOAD_REPLAY_LOCKS" && -n "$PAYLOAD_BASE_ARCHIVE" && -n "$PAYLOAD_BASE_LOCKS" && -n "$PAYLOAD_DELTA_ARCHIVE" && -n "$PAYLOAD_DELTA_LOCKS" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -x "$RPKI_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin rpki
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_validation_time() {
|
||||||
|
local path="$1"
|
||||||
|
python3 - <<'PY' "$path"
|
||||||
|
import json, sys
|
||||||
|
print(json.load(open(sys.argv[1], 'r', encoding='utf-8'))['validationTime'])
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$BASE_VALIDATION_TIME" ]]; then
|
||||||
|
BASE_VALIDATION_TIME="$(resolve_validation_time "$PAYLOAD_REPLAY_LOCKS")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DELTA_VALIDATION_TIME" ]]; then
|
||||||
|
DELTA_VALIDATION_TIME="$(resolve_validation_time "$PAYLOAD_DELTA_LOCKS")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$OUT_DIR"
|
||||||
|
mkdir -p "$OUT_DIR/full" "$OUT_DIR/delta-001" "$OUT_DIR/static"
|
||||||
|
|
||||||
|
FULL_DB="$OUT_DIR/full/db"
|
||||||
|
DELTA_DB="$OUT_DIR/delta-001/db"
|
||||||
|
|
||||||
|
"$RPKI_BIN" \
|
||||||
|
--db "$FULL_DB" \
|
||||||
|
--tal-path "$TAL_PATH" \
|
||||||
|
--ta-path "$TA_PATH" \
|
||||||
|
--payload-replay-archive "$PAYLOAD_REPLAY_ARCHIVE" \
|
||||||
|
--payload-replay-locks "$PAYLOAD_REPLAY_LOCKS" \
|
||||||
|
--validation-time "$BASE_VALIDATION_TIME" \
|
||||||
|
--max-depth "$MAX_DEPTH" \
|
||||||
|
--max-instances "$MAX_INSTANCES" \
|
||||||
|
--ccr-out "$OUT_DIR/full/result.ccr" \
|
||||||
|
--report-json "$OUT_DIR/full/report.json" \
|
||||||
|
--cir-enable \
|
||||||
|
--cir-out "$OUT_DIR/full/input.cir" \
|
||||||
|
--cir-static-root "$OUT_DIR/static" \
|
||||||
|
--cir-tal-uri "$CIR_TAL_URI" \
|
||||||
|
>"$OUT_DIR/full/run.stdout.log" 2>"$OUT_DIR/full/run.stderr.log"
|
||||||
|
|
||||||
|
"$RPKI_BIN" \
|
||||||
|
--db "$DELTA_DB" \
|
||||||
|
--tal-path "$TAL_PATH" \
|
||||||
|
--ta-path "$TA_PATH" \
|
||||||
|
--payload-base-archive "$PAYLOAD_BASE_ARCHIVE" \
|
||||||
|
--payload-base-locks "$PAYLOAD_BASE_LOCKS" \
|
||||||
|
--payload-delta-archive "$PAYLOAD_DELTA_ARCHIVE" \
|
||||||
|
--payload-delta-locks "$PAYLOAD_DELTA_LOCKS" \
|
||||||
|
--payload-base-validation-time "$BASE_VALIDATION_TIME" \
|
||||||
|
--validation-time "$DELTA_VALIDATION_TIME" \
|
||||||
|
--max-depth "$MAX_DEPTH" \
|
||||||
|
--max-instances "$MAX_INSTANCES" \
|
||||||
|
--ccr-out "$OUT_DIR/delta-001/result.ccr" \
|
||||||
|
--report-json "$OUT_DIR/delta-001/report.json" \
|
||||||
|
--cir-enable \
|
||||||
|
--cir-out "$OUT_DIR/delta-001/input.cir" \
|
||||||
|
--cir-static-root "$OUT_DIR/static" \
|
||||||
|
--cir-tal-uri "$CIR_TAL_URI" \
|
||||||
|
>"$OUT_DIR/delta-001/run.stdout.log" 2>"$OUT_DIR/delta-001/run.stderr.log"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$OUT_DIR" "$BASE_VALIDATION_TIME" "$DELTA_VALIDATION_TIME"
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
out = Path(sys.argv[1])
|
||||||
|
base_validation_time = sys.argv[2]
|
||||||
|
delta_validation_time = sys.argv[3]
|
||||||
|
static_files = sum(1 for _ in (out / "static").rglob("*") if _.is_file())
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"kind": "cir_pair",
|
||||||
|
"baseValidationTime": base_validation_time,
|
||||||
|
"deltaValidationTime": delta_validation_time,
|
||||||
|
"staticRoot": "static",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"kind": "full",
|
||||||
|
"cirPath": "full/input.cir",
|
||||||
|
"ccrPath": "full/result.ccr",
|
||||||
|
"reportPath": "full/report.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "delta",
|
||||||
|
"cirPath": "delta-001/input.cir",
|
||||||
|
"ccrPath": "delta-001/result.ccr",
|
||||||
|
"reportPath": "delta-001/report.json",
|
||||||
|
"previous": "full",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"staticFileCount": static_files,
|
||||||
|
}
|
||||||
|
(out / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $OUT_DIR"
|
||||||
129
scripts/cir/run_cir_record_sequence_multi_rir_offline.sh
Executable file
129
scripts/cir/run_cir_record_sequence_multi_rir_offline.sh
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_sequence_multi_rir_offline.sh \
|
||||||
|
[--bundle-root <path>] \
|
||||||
|
[--rir <afrinic,apnic,arin,lacnic,ripe>] \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--full-repo] \
|
||||||
|
[--out-root <path>] \
|
||||||
|
[--rpki-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
CASE_INFO="$ROOT_DIR/scripts/payload_replay/multi_rir_case_info.py"
|
||||||
|
SINGLE_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_record_sequence_offline.sh"
|
||||||
|
|
||||||
|
BUNDLE_ROOT="/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3"
|
||||||
|
RIRS="afrinic,apnic,arin,lacnic,ripe"
|
||||||
|
DELTA_COUNT=2
|
||||||
|
FULL_REPO=0
|
||||||
|
OUT_ROOT="$ROOT_DIR/target/replay/cir_sequence_multi_rir_offline_$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--bundle-root) BUNDLE_ROOT="$2"; shift 2 ;;
|
||||||
|
--rir) RIRS="$2"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="$2"; shift 2 ;;
|
||||||
|
--full-repo) FULL_REPO=1; shift 1 ;;
|
||||||
|
--out-root) OUT_ROOT="$2"; shift 2 ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$OUT_ROOT"
|
||||||
|
SUMMARY_JSON="$OUT_ROOT/summary.json"
|
||||||
|
SUMMARY_MD="$OUT_ROOT/summary.md"
|
||||||
|
|
||||||
|
IFS=',' read -r -a RIR_ITEMS <<< "$RIRS"
|
||||||
|
|
||||||
|
for rir in "${RIR_ITEMS[@]}"; do
|
||||||
|
CASE_JSON="$(python3 "$CASE_INFO" --bundle-root "$BUNDLE_ROOT" --repo-root "$ROOT_DIR" --rir "$rir")"
|
||||||
|
TAL_PATH="$(python3 - <<'PY' "$CASE_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.loads(sys.argv[1])['tal_path'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
TA_PATH="$(python3 - <<'PY' "$CASE_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.loads(sys.argv[1])['ta_path'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
BASE_ARCHIVE="$(python3 - <<'PY' "$CASE_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.loads(sys.argv[1])['base_archive'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
BASE_LOCKS="$(python3 - <<'PY' "$CASE_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.loads(sys.argv[1])['base_locks'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
DELTA_ARCHIVE="$(python3 - <<'PY' "$CASE_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.loads(sys.argv[1])['delta_archive'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
DELTA_LOCKS="$(python3 - <<'PY' "$CASE_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.loads(sys.argv[1])['delta_locks'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
OUT_DIR="$OUT_ROOT/$rir"
|
||||||
|
args=(
|
||||||
|
"$SINGLE_SCRIPT"
|
||||||
|
--out-dir "$OUT_DIR" \
|
||||||
|
--tal-path "$TAL_PATH" \
|
||||||
|
--ta-path "$TA_PATH" \
|
||||||
|
--cir-tal-uri "https://example.test/$rir.tal" \
|
||||||
|
--payload-replay-archive "$BASE_ARCHIVE" \
|
||||||
|
--payload-replay-locks "$BASE_LOCKS" \
|
||||||
|
--payload-base-archive "$BASE_ARCHIVE" \
|
||||||
|
--payload-base-locks "$BASE_LOCKS" \
|
||||||
|
--payload-delta-archive "$DELTA_ARCHIVE" \
|
||||||
|
--payload-delta-locks "$DELTA_LOCKS" \
|
||||||
|
--delta-count "$DELTA_COUNT" \
|
||||||
|
--rpki-bin "$RPKI_BIN"
|
||||||
|
)
|
||||||
|
if [[ "$FULL_REPO" -ne 1 ]]; then
|
||||||
|
args+=(--max-depth 0 --max-instances 1)
|
||||||
|
else
|
||||||
|
args+=(--full-repo)
|
||||||
|
fi
|
||||||
|
"${args[@]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - <<'PY' "$OUT_ROOT" "$RIRS" "$SUMMARY_JSON" "$SUMMARY_MD"
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
out_root = Path(sys.argv[1])
|
||||||
|
rirs = [item for item in sys.argv[2].split(',') if item]
|
||||||
|
summary_json = Path(sys.argv[3])
|
||||||
|
summary_md = Path(sys.argv[4])
|
||||||
|
items = []
|
||||||
|
for rir in rirs:
|
||||||
|
root = out_root / rir
|
||||||
|
seq = json.loads((root / "sequence.json").read_text(encoding="utf-8"))
|
||||||
|
summ = json.loads((root / "summary.json").read_text(encoding="utf-8"))
|
||||||
|
items.append({
|
||||||
|
"rir": rir,
|
||||||
|
"root": str(root),
|
||||||
|
"stepCount": len(seq["steps"]),
|
||||||
|
"staticFileCount": summ["staticFileCount"],
|
||||||
|
})
|
||||||
|
summary = {"version": 1, "rirs": items}
|
||||||
|
summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
lines = ["# Multi-RIR Offline CIR Sequence Summary", ""]
|
||||||
|
for item in items:
|
||||||
|
lines.append(f"- `{item['rir']}`: `stepCount={item['stepCount']}` `staticFileCount={item['staticFileCount']}` `root={item['root']}`")
|
||||||
|
summary_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $OUT_ROOT"
|
||||||
206
scripts/cir/run_cir_record_sequence_offline.sh
Executable file
206
scripts/cir/run_cir_record_sequence_offline.sh
Executable file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_sequence_offline.sh \
|
||||||
|
--out-dir <path> \
|
||||||
|
--tal-path <path> \
|
||||||
|
--ta-path <path> \
|
||||||
|
--cir-tal-uri <url> \
|
||||||
|
--payload-replay-archive <path> \
|
||||||
|
--payload-replay-locks <path> \
|
||||||
|
--payload-base-archive <path> \
|
||||||
|
--payload-base-locks <path> \
|
||||||
|
--payload-delta-archive <path> \
|
||||||
|
--payload-delta-locks <path> \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--base-validation-time <rfc3339>] \
|
||||||
|
[--delta-validation-time <rfc3339>] \
|
||||||
|
[--full-repo] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>] \
|
||||||
|
[--rpki-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
OUT_DIR=""
|
||||||
|
TAL_PATH=""
|
||||||
|
TA_PATH=""
|
||||||
|
CIR_TAL_URI=""
|
||||||
|
PAYLOAD_REPLAY_ARCHIVE=""
|
||||||
|
PAYLOAD_REPLAY_LOCKS=""
|
||||||
|
PAYLOAD_BASE_ARCHIVE=""
|
||||||
|
PAYLOAD_BASE_LOCKS=""
|
||||||
|
PAYLOAD_DELTA_ARCHIVE=""
|
||||||
|
PAYLOAD_DELTA_LOCKS=""
|
||||||
|
BASE_VALIDATION_TIME=""
|
||||||
|
DELTA_VALIDATION_TIME=""
|
||||||
|
DELTA_COUNT=2
|
||||||
|
FULL_REPO=0
|
||||||
|
MAX_DEPTH=0
|
||||||
|
MAX_INSTANCES=1
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--tal-path) TAL_PATH="$2"; shift 2 ;;
|
||||||
|
--ta-path) TA_PATH="$2"; shift 2 ;;
|
||||||
|
--cir-tal-uri) CIR_TAL_URI="$2"; shift 2 ;;
|
||||||
|
--payload-replay-archive) PAYLOAD_REPLAY_ARCHIVE="$2"; shift 2 ;;
|
||||||
|
--payload-replay-locks) PAYLOAD_REPLAY_LOCKS="$2"; shift 2 ;;
|
||||||
|
--payload-base-archive) PAYLOAD_BASE_ARCHIVE="$2"; shift 2 ;;
|
||||||
|
--payload-base-locks) PAYLOAD_BASE_LOCKS="$2"; shift 2 ;;
|
||||||
|
--payload-delta-archive) PAYLOAD_DELTA_ARCHIVE="$2"; shift 2 ;;
|
||||||
|
--payload-delta-locks) PAYLOAD_DELTA_LOCKS="$2"; shift 2 ;;
|
||||||
|
--base-validation-time) BASE_VALIDATION_TIME="$2"; shift 2 ;;
|
||||||
|
--delta-validation-time) DELTA_VALIDATION_TIME="$2"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="$2"; shift 2 ;;
|
||||||
|
--full-repo) FULL_REPO=1; shift 1 ;;
|
||||||
|
--max-depth) MAX_DEPTH="$2"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="$2"; shift 2 ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$OUT_DIR" && -n "$TAL_PATH" && -n "$TA_PATH" && -n "$CIR_TAL_URI" && -n "$PAYLOAD_REPLAY_ARCHIVE" && -n "$PAYLOAD_REPLAY_LOCKS" && -n "$PAYLOAD_BASE_ARCHIVE" && -n "$PAYLOAD_BASE_LOCKS" && -n "$PAYLOAD_DELTA_ARCHIVE" && -n "$PAYLOAD_DELTA_LOCKS" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -x "$RPKI_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin rpki
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_validation_time() {
|
||||||
|
local path="$1"
|
||||||
|
python3 - <<'PY' "$path"
|
||||||
|
import json, sys
|
||||||
|
print(json.load(open(sys.argv[1], 'r', encoding='utf-8'))['validationTime'])
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$BASE_VALIDATION_TIME" ]]; then
|
||||||
|
BASE_VALIDATION_TIME="$(resolve_validation_time "$PAYLOAD_REPLAY_LOCKS")"
|
||||||
|
fi
|
||||||
|
if [[ -z "$DELTA_VALIDATION_TIME" ]]; then
|
||||||
|
DELTA_VALIDATION_TIME="$(resolve_validation_time "$PAYLOAD_DELTA_LOCKS")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$OUT_DIR"
|
||||||
|
mkdir -p "$OUT_DIR/static" "$OUT_DIR/full"
|
||||||
|
|
||||||
|
run_step() {
|
||||||
|
local kind="$1"
|
||||||
|
local step_dir="$2"
|
||||||
|
local db_dir="$3"
|
||||||
|
shift 3
|
||||||
|
mkdir -p "$step_dir"
|
||||||
|
local -a cmd=(
|
||||||
|
"$RPKI_BIN"
|
||||||
|
--db "$db_dir" \
|
||||||
|
--tal-path "$TAL_PATH" \
|
||||||
|
--ta-path "$TA_PATH" \
|
||||||
|
--ccr-out "$step_dir/result.ccr" \
|
||||||
|
--report-json "$step_dir/report.json" \
|
||||||
|
--cir-enable \
|
||||||
|
--cir-out "$step_dir/input.cir" \
|
||||||
|
--cir-static-root "$OUT_DIR/static" \
|
||||||
|
--cir-tal-uri "$CIR_TAL_URI"
|
||||||
|
)
|
||||||
|
if [[ "$FULL_REPO" -ne 1 ]]; then
|
||||||
|
cmd+=(--max-depth "$MAX_DEPTH" --max-instances "$MAX_INSTANCES")
|
||||||
|
fi
|
||||||
|
cmd+=("$@")
|
||||||
|
"${cmd[@]}" >"$step_dir/run.stdout.log" 2>"$step_dir/run.stderr.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_step \
|
||||||
|
full \
|
||||||
|
"$OUT_DIR/full" \
|
||||||
|
"$OUT_DIR/full/db" \
|
||||||
|
--payload-replay-archive "$PAYLOAD_REPLAY_ARCHIVE" \
|
||||||
|
--payload-replay-locks "$PAYLOAD_REPLAY_LOCKS" \
|
||||||
|
--validation-time "$BASE_VALIDATION_TIME"
|
||||||
|
|
||||||
|
for idx in $(seq 1 "$DELTA_COUNT"); do
|
||||||
|
step_id="$(printf 'delta-%03d' "$idx")"
|
||||||
|
run_step \
|
||||||
|
delta \
|
||||||
|
"$OUT_DIR/$step_id" \
|
||||||
|
"$OUT_DIR/$step_id/db" \
|
||||||
|
--payload-base-archive "$PAYLOAD_BASE_ARCHIVE" \
|
||||||
|
--payload-base-locks "$PAYLOAD_BASE_LOCKS" \
|
||||||
|
--payload-delta-archive "$PAYLOAD_DELTA_ARCHIVE" \
|
||||||
|
--payload-delta-locks "$PAYLOAD_DELTA_LOCKS" \
|
||||||
|
--payload-base-validation-time "$BASE_VALIDATION_TIME" \
|
||||||
|
--validation-time "$DELTA_VALIDATION_TIME"
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - <<'PY' "$OUT_DIR" "$BASE_VALIDATION_TIME" "$DELTA_VALIDATION_TIME" "$DELTA_COUNT"
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
out = Path(sys.argv[1])
|
||||||
|
base_validation_time = sys.argv[2]
|
||||||
|
delta_validation_time = sys.argv[3]
|
||||||
|
delta_count = int(sys.argv[4])
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
{
|
||||||
|
"stepId": "full",
|
||||||
|
"kind": "full",
|
||||||
|
"validationTime": base_validation_time,
|
||||||
|
"cirPath": "full/input.cir",
|
||||||
|
"ccrPath": "full/result.ccr",
|
||||||
|
"reportPath": "full/report.json",
|
||||||
|
"previousStepId": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
previous = "full"
|
||||||
|
for idx in range(1, delta_count + 1):
|
||||||
|
step_id = f"delta-{idx:03d}"
|
||||||
|
steps.append(
|
||||||
|
{
|
||||||
|
"stepId": step_id,
|
||||||
|
"kind": "delta",
|
||||||
|
"validationTime": delta_validation_time,
|
||||||
|
"cirPath": f"{step_id}/input.cir",
|
||||||
|
"ccrPath": f"{step_id}/result.ccr",
|
||||||
|
"reportPath": f"{step_id}/report.json",
|
||||||
|
"previousStepId": previous,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
previous = step_id
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"kind": "cir_sequence_offline",
|
||||||
|
"staticRoot": "static",
|
||||||
|
"steps": steps,
|
||||||
|
}
|
||||||
|
(out / "sequence.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
(out / "summary.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"stepCount": len(steps),
|
||||||
|
"staticFileCount": sum(1 for p in (out / "static").rglob("*") if p.is_file()),
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $OUT_DIR"
|
||||||
244
scripts/cir/run_cir_record_sequence_remote.sh
Executable file
244
scripts/cir/run_cir_record_sequence_remote.sh
Executable file
@@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_sequence_remote.sh \
|
||||||
|
--rir <name> \
|
||||||
|
--remote-root <path> \
|
||||||
|
[--ssh-target <user@host>] \
|
||||||
|
[--out-subdir <path>] \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--sleep-secs <n>] \
|
||||||
|
[--full-repo] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SSH_TARGET="${SSH_TARGET:-root@47.77.183.68}"
|
||||||
|
RIR=""
|
||||||
|
REMOTE_ROOT=""
|
||||||
|
OUT_SUBDIR=""
|
||||||
|
DELTA_COUNT=2
|
||||||
|
SLEEP_SECS=30
|
||||||
|
FULL_REPO=0
|
||||||
|
MAX_DEPTH=0
|
||||||
|
MAX_INSTANCES=1
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir) RIR="$2"; shift 2 ;;
|
||||||
|
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--out-subdir) OUT_SUBDIR="$2"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="$2"; shift 2 ;;
|
||||||
|
--sleep-secs) SLEEP_SECS="$2"; shift 2 ;;
|
||||||
|
--full-repo) FULL_REPO=1; shift 1 ;;
|
||||||
|
--max-depth) MAX_DEPTH="$2"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$RIR" && -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
case "$RIR" in
|
||||||
|
afrinic) TAL_REL="tests/fixtures/tal/afrinic.tal"; TA_REL="tests/fixtures/ta/afrinic-ta.cer" ;;
|
||||||
|
apnic) TAL_REL="tests/fixtures/tal/apnic-rfc7730-https.tal"; TA_REL="tests/fixtures/ta/apnic-ta.cer" ;;
|
||||||
|
arin) TAL_REL="tests/fixtures/tal/arin.tal"; TA_REL="tests/fixtures/ta/arin-ta.cer" ;;
|
||||||
|
lacnic) TAL_REL="tests/fixtures/tal/lacnic.tal"; TA_REL="tests/fixtures/ta/lacnic-ta.cer" ;;
|
||||||
|
ripe) TAL_REL="tests/fixtures/tal/ripe-ncc.tal"; TA_REL="tests/fixtures/ta/ripe-ncc-ta.cer" ;;
|
||||||
|
*) echo "unsupported rir: $RIR" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude target \
|
||||||
|
--exclude .git \
|
||||||
|
"$ROOT_DIR/" "$SSH_TARGET:$REMOTE_ROOT/"
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_ROOT/target/release'"
|
||||||
|
rsync -a "$ROOT_DIR/target/release/rpki" "$SSH_TARGET:$REMOTE_ROOT/target/release/"
|
||||||
|
|
||||||
|
ssh "$SSH_TARGET" \
|
||||||
|
RIR="$RIR" \
|
||||||
|
REMOTE_ROOT="$REMOTE_ROOT" \
|
||||||
|
OUT_SUBDIR="$OUT_SUBDIR" \
|
||||||
|
DELTA_COUNT="$DELTA_COUNT" \
|
||||||
|
SLEEP_SECS="$SLEEP_SECS" \
|
||||||
|
FULL_REPO="$FULL_REPO" \
|
||||||
|
MAX_DEPTH="$MAX_DEPTH" \
|
||||||
|
MAX_INSTANCES="$MAX_INSTANCES" \
|
||||||
|
TAL_REL="$TAL_REL" \
|
||||||
|
TA_REL="$TA_REL" \
|
||||||
|
'bash -s' <<'EOS'
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$REMOTE_ROOT"
|
||||||
|
|
||||||
|
if [[ -n "${OUT_SUBDIR}" ]]; then
|
||||||
|
OUT="${OUT_SUBDIR}"
|
||||||
|
else
|
||||||
|
OUT="target/replay/cir_sequence_remote_${RIR}_$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
DB="$OUT/work-db"
|
||||||
|
RAW_STORE_DB="$OUT/raw-store.db"
|
||||||
|
ROWS="$OUT/.sequence_rows.tsv"
|
||||||
|
: > "$ROWS"
|
||||||
|
|
||||||
|
write_step_timing() {
|
||||||
|
local path="$1"
|
||||||
|
local start_ms="$2"
|
||||||
|
local end_ms="$3"
|
||||||
|
local started_at="$4"
|
||||||
|
local finished_at="$5"
|
||||||
|
python3 - <<'PY' "$path" "$start_ms" "$end_ms" "$started_at" "$finished_at"
|
||||||
|
import json, sys
|
||||||
|
path, start_ms, end_ms, started_at, finished_at = sys.argv[1:]
|
||||||
|
start_ms = int(start_ms)
|
||||||
|
end_ms = int(end_ms)
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"durationMs": end_ms - start_ms,
|
||||||
|
"startedAt": started_at,
|
||||||
|
"finishedAt": finished_at,
|
||||||
|
},
|
||||||
|
fh,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_step() {
|
||||||
|
local step_id="$1"
|
||||||
|
local kind="$2"
|
||||||
|
local previous_step_id="$3"
|
||||||
|
shift 3
|
||||||
|
|
||||||
|
local started_at_iso started_at_ms finished_at_iso finished_at_ms prefix
|
||||||
|
started_at_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
started_at_ms="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
prefix="${started_at_iso}-test"
|
||||||
|
|
||||||
|
local cir_out="$OUT/${prefix}.cir"
|
||||||
|
local ccr_out="$OUT/${prefix}.ccr"
|
||||||
|
local report_out="$OUT/${prefix}.report.json"
|
||||||
|
local timing_out="$OUT/${prefix}.timing.json"
|
||||||
|
local stdout_out="$OUT/${prefix}.stdout.log"
|
||||||
|
local stderr_out="$OUT/${prefix}.stderr.log"
|
||||||
|
|
||||||
|
local -a cmd=(
|
||||||
|
target/release/rpki
|
||||||
|
--db "$DB"
|
||||||
|
--raw-store-db "$RAW_STORE_DB"
|
||||||
|
--tal-path "$TAL_REL"
|
||||||
|
--ta-path "$TA_REL"
|
||||||
|
--ccr-out "$ccr_out"
|
||||||
|
--report-json "$report_out"
|
||||||
|
--cir-enable
|
||||||
|
--cir-out "$cir_out"
|
||||||
|
--cir-tal-uri "https://example.test/${RIR}.tal"
|
||||||
|
)
|
||||||
|
if [[ "$FULL_REPO" -ne 1 ]]; then
|
||||||
|
cmd+=(--max-depth "$MAX_DEPTH" --max-instances "$MAX_INSTANCES")
|
||||||
|
fi
|
||||||
|
cmd+=("$@")
|
||||||
|
env RPKI_PROGRESS_LOG=1 "${cmd[@]}" >"$stdout_out" 2>"$stderr_out"
|
||||||
|
|
||||||
|
finished_at_ms="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
finished_at_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
write_step_timing "$timing_out" "$started_at_ms" "$finished_at_ms" "$started_at_iso" "$finished_at_iso"
|
||||||
|
|
||||||
|
local validation_time
|
||||||
|
validation_time="$(python3 - <<'PY' "$report_out"
|
||||||
|
import json, sys
|
||||||
|
print(json.load(open(sys.argv[1], 'r', encoding='utf-8'))['meta']['validation_time_rfc3339_utc'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||||
|
"$step_id" \
|
||||||
|
"$kind" \
|
||||||
|
"$validation_time" \
|
||||||
|
"$(basename "$cir_out")" \
|
||||||
|
"$(basename "$ccr_out")" \
|
||||||
|
"$(basename "$report_out")" \
|
||||||
|
"$(basename "$timing_out")" \
|
||||||
|
"$(basename "$stdout_out")" \
|
||||||
|
"$(basename "$stderr_out")" >> "$ROWS"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_step "full" "full" ""
|
||||||
|
|
||||||
|
prev="full"
|
||||||
|
for idx in $(seq 1 "$DELTA_COUNT"); do
|
||||||
|
sleep "$SLEEP_SECS"
|
||||||
|
step="$(printf 'delta-%03d' "$idx")"
|
||||||
|
run_step "$step" "delta" "$prev"
|
||||||
|
prev="$step"
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - <<'PY' "$OUT" "$ROWS" "$RIR"
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
out = Path(sys.argv[1])
|
||||||
|
rows = Path(sys.argv[2]).read_text(encoding='utf-8').splitlines()
|
||||||
|
rir = sys.argv[3]
|
||||||
|
steps = []
|
||||||
|
for idx, row in enumerate(rows):
|
||||||
|
step_id, kind, validation_time, cir_name, ccr_name, report_name, timing_name, stdout_name, stderr_name = row.split('\t')
|
||||||
|
steps.append({
|
||||||
|
"stepId": step_id,
|
||||||
|
"kind": kind,
|
||||||
|
"validationTime": validation_time,
|
||||||
|
"cirPath": cir_name,
|
||||||
|
"ccrPath": ccr_name,
|
||||||
|
"reportPath": report_name,
|
||||||
|
"timingPath": timing_name,
|
||||||
|
"stdoutLogPath": stdout_name,
|
||||||
|
"stderrLogPath": stderr_name,
|
||||||
|
"artifactPrefix": cir_name[:-4], # strip .cir
|
||||||
|
"previousStepId": None if idx == 0 else steps[idx - 1]["stepId"],
|
||||||
|
})
|
||||||
|
|
||||||
|
(out / "sequence.json").write_text(
|
||||||
|
json.dumps({"version": 1, "rawStoreDbPath": "raw-store.db", "steps": steps}, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"rir": rir,
|
||||||
|
"stepCount": len(steps),
|
||||||
|
"steps": [],
|
||||||
|
}
|
||||||
|
for step in steps:
|
||||||
|
timing = json.loads((out / step["timingPath"]).read_text(encoding="utf-8"))
|
||||||
|
summary["steps"].append({
|
||||||
|
"stepId": step["stepId"],
|
||||||
|
"kind": step["kind"],
|
||||||
|
"validationTime": step["validationTime"],
|
||||||
|
"artifactPrefix": step["artifactPrefix"],
|
||||||
|
**timing,
|
||||||
|
})
|
||||||
|
|
||||||
|
(out / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
rm -f "$ROWS"
|
||||||
|
echo "$OUT"
|
||||||
|
EOS
|
||||||
72
scripts/cir/run_cir_record_sequence_remote_multi_rir.sh
Executable file
72
scripts/cir/run_cir_record_sequence_remote_multi_rir.sh
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_sequence_remote_multi_rir.sh \
|
||||||
|
--remote-root <path> \
|
||||||
|
[--rir <afrinic,apnic,arin,lacnic,ripe>] \
|
||||||
|
[--ssh-target <user@host>] \
|
||||||
|
[--out-subdir-root <path>] \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--sleep-secs <n>] \
|
||||||
|
[--full-repo] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SSH_TARGET="${SSH_TARGET:-root@47.77.183.68}"
|
||||||
|
REMOTE_ROOT=""
|
||||||
|
RIRS="afrinic,apnic,arin,lacnic,ripe"
|
||||||
|
OUT_SUBDIR_ROOT=""
|
||||||
|
DELTA_COUNT=2
|
||||||
|
SLEEP_SECS=30
|
||||||
|
FULL_REPO=0
|
||||||
|
MAX_DEPTH=0
|
||||||
|
MAX_INSTANCES=1
|
||||||
|
SINGLE="$ROOT_DIR/scripts/cir/run_cir_record_sequence_remote.sh"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
|
||||||
|
--rir) RIRS="$2"; shift 2 ;;
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--out-subdir-root) OUT_SUBDIR_ROOT="$2"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="$2"; shift 2 ;;
|
||||||
|
--sleep-secs) SLEEP_SECS="$2"; shift 2 ;;
|
||||||
|
--full-repo) FULL_REPO=1; shift 1 ;;
|
||||||
|
--max-depth) MAX_DEPTH="$2"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
if [[ -z "$OUT_SUBDIR_ROOT" ]]; then
|
||||||
|
OUT_SUBDIR_ROOT="target/replay/cir_sequence_remote_multi_rir_$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a ITEMS <<< "$RIRS"
|
||||||
|
for rir in "${ITEMS[@]}"; do
|
||||||
|
args=(
|
||||||
|
"$SINGLE"
|
||||||
|
--rir "$rir" \
|
||||||
|
--remote-root "$REMOTE_ROOT" \
|
||||||
|
--ssh-target "$SSH_TARGET" \
|
||||||
|
--out-subdir "$OUT_SUBDIR_ROOT/$rir" \
|
||||||
|
--delta-count "$DELTA_COUNT" \
|
||||||
|
--sleep-secs "$SLEEP_SECS" \
|
||||||
|
)
|
||||||
|
if [[ "$FULL_REPO" -eq 1 ]]; then
|
||||||
|
args+=(--full-repo)
|
||||||
|
else
|
||||||
|
args+=(--max-depth "$MAX_DEPTH" --max-instances "$MAX_INSTANCES")
|
||||||
|
fi
|
||||||
|
"${args[@]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$OUT_SUBDIR_ROOT"
|
||||||
118
scripts/cir/run_cir_record_sequence_ta_only_multi_rir.sh
Executable file
118
scripts/cir/run_cir_record_sequence_ta_only_multi_rir.sh
Executable file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_sequence_ta_only_multi_rir.sh \
|
||||||
|
[--rir <afrinic,apnic,arin,lacnic,ripe>] \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--out-root <path>] \
|
||||||
|
[--rpki-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
HELPER_BIN="${HELPER_BIN:-$ROOT_DIR/target/release/cir_ta_only_fixture}"
|
||||||
|
MATERIALIZE_BIN="${MATERIALIZE_BIN:-$ROOT_DIR/target/release/cir_materialize}"
|
||||||
|
EXTRACT_BIN="${EXTRACT_BIN:-$ROOT_DIR/target/release/cir_extract_inputs}"
|
||||||
|
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
|
||||||
|
RIRS="afrinic,apnic,arin,lacnic,ripe"
|
||||||
|
DELTA_COUNT=2
|
||||||
|
OUT_ROOT="$ROOT_DIR/target/replay/cir_sequence_multi_rir_ta_only_$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir) RIRS="$2"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="$2"; shift 2 ;;
|
||||||
|
--out-root) OUT_ROOT="$2"; shift 2 ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -x "$HELPER_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin cir_ta_only_fixture --bin rpki --bin cir_materialize --bin cir_extract_inputs
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
case_paths() {
|
||||||
|
case "$1" in
|
||||||
|
afrinic) echo "tests/fixtures/tal/afrinic.tal tests/fixtures/ta/afrinic-ta.cer" ;;
|
||||||
|
apnic) echo "tests/fixtures/tal/apnic-rfc7730-https.tal tests/fixtures/ta/apnic-ta.cer" ;;
|
||||||
|
arin) echo "tests/fixtures/tal/arin.tal tests/fixtures/ta/arin-ta.cer" ;;
|
||||||
|
lacnic) echo "tests/fixtures/tal/lacnic.tal tests/fixtures/ta/lacnic-ta.cer" ;;
|
||||||
|
ripe) echo "tests/fixtures/tal/ripe-ncc.tal tests/fixtures/ta/ripe-ncc-ta.cer" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$OUT_ROOT"
|
||||||
|
IFS=',' read -r -a ITEMS <<< "$RIRS"
|
||||||
|
for rir in "${ITEMS[@]}"; do
|
||||||
|
read -r tal_rel ta_rel < <(case_paths "$rir")
|
||||||
|
rir_root="$OUT_ROOT/$rir"
|
||||||
|
mkdir -p "$rir_root/full" "$rir_root/static"
|
||||||
|
"$HELPER_BIN" \
|
||||||
|
--tal-path "$ROOT_DIR/$tal_rel" \
|
||||||
|
--ta-path "$ROOT_DIR/$ta_rel" \
|
||||||
|
--tal-uri "https://example.test/$rir.tal" \
|
||||||
|
--validation-time "2026-04-09T00:00:00Z" \
|
||||||
|
--cir-out "$rir_root/full/input.cir" \
|
||||||
|
--static-root "$rir_root/static"
|
||||||
|
"$EXTRACT_BIN" --cir "$rir_root/full/input.cir" --tals-dir "$rir_root/.tmp/tals" --meta-json "$rir_root/.tmp/meta.json"
|
||||||
|
"$MATERIALIZE_BIN" --cir "$rir_root/full/input.cir" --static-root "$rir_root/static" --mirror-root "$rir_root/.tmp/mirror"
|
||||||
|
FIRST_TAL="$(python3 - <<'PY' "$rir_root/.tmp/meta.json"
|
||||||
|
import json,sys
|
||||||
|
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
export CIR_MIRROR_ROOT="$rir_root/.tmp/mirror"
|
||||||
|
export REAL_RSYNC_BIN=/usr/bin/rsync
|
||||||
|
export CIR_LOCAL_LINK_MODE=1
|
||||||
|
"$RPKI_BIN" \
|
||||||
|
--db "$rir_root/full/db" \
|
||||||
|
--tal-path "$FIRST_TAL" \
|
||||||
|
--disable-rrdp \
|
||||||
|
--rsync-command "$WRAPPER" \
|
||||||
|
--validation-time "2026-04-09T00:00:00Z" \
|
||||||
|
--ccr-out "$rir_root/full/result.ccr" \
|
||||||
|
--report-json "$rir_root/full/report.json" >/dev/null 2>&1
|
||||||
|
for idx in $(seq 1 "$DELTA_COUNT"); do
|
||||||
|
step="$(printf 'delta-%03d' "$idx")"
|
||||||
|
mkdir -p "$rir_root/$step"
|
||||||
|
cp "$rir_root/full/input.cir" "$rir_root/$step/input.cir"
|
||||||
|
cp "$rir_root/full/result.ccr" "$rir_root/$step/result.ccr"
|
||||||
|
cp "$rir_root/full/report.json" "$rir_root/$step/report.json"
|
||||||
|
done
|
||||||
|
python3 - <<'PY' "$rir_root" "$DELTA_COUNT"
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
root = Path(sys.argv[1]); delta_count = int(sys.argv[2])
|
||||||
|
steps = [{"stepId":"full","kind":"full","validationTime":"2026-04-09T00:00:00Z","cirPath":"full/input.cir","ccrPath":"full/result.ccr","reportPath":"full/report.json","previousStepId":None}]
|
||||||
|
prev = "full"
|
||||||
|
for i in range(1, delta_count + 1):
|
||||||
|
step = f"delta-{i:03d}"
|
||||||
|
steps.append({"stepId":step,"kind":"delta","validationTime":"2026-04-09T00:00:00Z","cirPath":f"{step}/input.cir","ccrPath":f"{step}/result.ccr","reportPath":f"{step}/report.json","previousStepId":prev})
|
||||||
|
prev = step
|
||||||
|
(root/"sequence.json").write_text(json.dumps({"version":1,"staticRoot":"static","steps":steps}, indent=2), encoding="utf-8")
|
||||||
|
(root/"summary.json").write_text(json.dumps({"version":1,"stepCount":len(steps)}, indent=2), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - <<'PY' "$OUT_ROOT" "$RIRS"
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
root = Path(sys.argv[1]); rirs = [x for x in sys.argv[2].split(',') if x]
|
||||||
|
items=[]
|
||||||
|
for rir in rirs:
|
||||||
|
seq=json.loads((root/rir/'sequence.json').read_text())
|
||||||
|
items.append({"rir":rir,"stepCount":len(seq['steps'])})
|
||||||
|
(root/'summary.json').write_text(json.dumps({"version":1,"rirs":items}, indent=2), encoding='utf-8')
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $OUT_ROOT"
|
||||||
49
scripts/cir/run_cir_record_sequence_ta_only_remote_multi_rir.sh
Executable file
49
scripts/cir/run_cir_record_sequence_ta_only_remote_multi_rir.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_record_sequence_ta_only_remote_multi_rir.sh \
|
||||||
|
--remote-root <path> \
|
||||||
|
[--ssh-target <user@host>] \
|
||||||
|
[--rir <afrinic,apnic,arin,lacnic,ripe>] \
|
||||||
|
[--delta-count <n>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SSH_TARGET="${SSH_TARGET:-root@47.77.183.68}"
|
||||||
|
REMOTE_ROOT=""
|
||||||
|
RIRS="afrinic,apnic,arin,lacnic,ripe"
|
||||||
|
DELTA_COUNT=2
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--rir) RIRS="$2"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude target \
|
||||||
|
--exclude .git \
|
||||||
|
"$ROOT_DIR/" "$SSH_TARGET:$REMOTE_ROOT/"
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_ROOT/target/release'"
|
||||||
|
for bin in rpki cir_ta_only_fixture cir_materialize cir_extract_inputs; do
|
||||||
|
rsync -a "$ROOT_DIR/target/release/$bin" "$SSH_TARGET:$REMOTE_ROOT/target/release/"
|
||||||
|
done
|
||||||
|
|
||||||
|
ssh "$SSH_TARGET" "bash -lc '
|
||||||
|
set -euo pipefail
|
||||||
|
cd $REMOTE_ROOT
|
||||||
|
OUT=target/replay/cir_sequence_remote_ta_only_\$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
./scripts/cir/run_cir_record_sequence_ta_only_multi_rir.sh --rir $RIRS --delta-count $DELTA_COUNT --out-root \"\$OUT\"
|
||||||
|
echo \"\$OUT\"
|
||||||
|
'"
|
||||||
286
scripts/cir/run_cir_replay_matrix.sh
Executable file
286
scripts/cir/run_cir_replay_matrix.sh
Executable file
@@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_matrix.sh \
|
||||||
|
--cir <path> \
|
||||||
|
--static-root <path> \
|
||||||
|
--out-dir <path> \
|
||||||
|
--reference-ccr <path> \
|
||||||
|
--rpki-client-build-dir <path> \
|
||||||
|
[--keep-db] \
|
||||||
|
[--rpki-bin <path>] \
|
||||||
|
[--routinator-root <path>] \
|
||||||
|
[--routinator-bin <path>] \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
CIR=""
|
||||||
|
STATIC_ROOT=""
|
||||||
|
OUT_DIR=""
|
||||||
|
REFERENCE_CCR=""
|
||||||
|
RPKI_CLIENT_BUILD_DIR=""
|
||||||
|
KEEP_DB=0
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
ROUTINATOR_ROOT="${ROUTINATOR_ROOT:-/home/yuyr/dev/rust_playground/routinator}"
|
||||||
|
ROUTINATOR_BIN="${ROUTINATOR_BIN:-$ROUTINATOR_ROOT/target/debug/routinator}"
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
OURS_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_ours.sh"
|
||||||
|
ROUTINATOR_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_routinator.sh"
|
||||||
|
RPKI_CLIENT_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_rpki_client.sh"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--cir) CIR="$2"; shift 2 ;;
|
||||||
|
--static-root) STATIC_ROOT="$2"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
|
||||||
|
--rpki-client-build-dir) RPKI_CLIENT_BUILD_DIR="$2"; shift 2 ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
--routinator-root) ROUTINATOR_ROOT="$2"; shift 2 ;;
|
||||||
|
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$CIR" && -n "$STATIC_ROOT" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" && -n "$RPKI_CLIENT_BUILD_DIR" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
run_with_timing() {
|
||||||
|
local summary_path="$1"
|
||||||
|
local timing_path="$2"
|
||||||
|
shift 2
|
||||||
|
local start end status
|
||||||
|
start="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(time.perf_counter_ns())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
if "$@"; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
end="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(time.perf_counter_ns())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
python3 - <<'PY' "$summary_path" "$timing_path" "$status" "$start" "$end"
|
||||||
|
import json, sys
|
||||||
|
summary_path, timing_path, status, start, end = sys.argv[1:]
|
||||||
|
duration_ms = max(0, (int(end) - int(start)) // 1_000_000)
|
||||||
|
data = {"exitCode": int(status), "durationMs": duration_ms}
|
||||||
|
try:
|
||||||
|
with open(summary_path, "r", encoding="utf-8") as f:
|
||||||
|
data["compare"] = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
data["compare"] = None
|
||||||
|
with open(timing_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
PY
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
OURS_OUT="$OUT_DIR/ours"
|
||||||
|
ROUTINATOR_OUT="$OUT_DIR/routinator"
|
||||||
|
RPKI_CLIENT_OUT="$OUT_DIR/rpki-client"
|
||||||
|
mkdir -p "$OURS_OUT" "$ROUTINATOR_OUT" "$RPKI_CLIENT_OUT"
|
||||||
|
|
||||||
|
ours_cmd=(
|
||||||
|
"$OURS_SCRIPT"
|
||||||
|
--cir "$CIR"
|
||||||
|
--static-root "$STATIC_ROOT"
|
||||||
|
--out-dir "$OURS_OUT"
|
||||||
|
--reference-ccr "$REFERENCE_CCR"
|
||||||
|
--rpki-bin "$RPKI_BIN"
|
||||||
|
--real-rsync-bin "$REAL_RSYNC_BIN"
|
||||||
|
)
|
||||||
|
routinator_cmd=(
|
||||||
|
"$ROUTINATOR_SCRIPT"
|
||||||
|
--cir "$CIR"
|
||||||
|
--static-root "$STATIC_ROOT"
|
||||||
|
--out-dir "$ROUTINATOR_OUT"
|
||||||
|
--reference-ccr "$REFERENCE_CCR"
|
||||||
|
--routinator-root "$ROUTINATOR_ROOT"
|
||||||
|
--routinator-bin "$ROUTINATOR_BIN"
|
||||||
|
--real-rsync-bin "$REAL_RSYNC_BIN"
|
||||||
|
)
|
||||||
|
rpki_client_cmd=(
|
||||||
|
"$RPKI_CLIENT_SCRIPT"
|
||||||
|
--cir "$CIR"
|
||||||
|
--static-root "$STATIC_ROOT"
|
||||||
|
--out-dir "$RPKI_CLIENT_OUT"
|
||||||
|
--reference-ccr "$REFERENCE_CCR"
|
||||||
|
--build-dir "$RPKI_CLIENT_BUILD_DIR"
|
||||||
|
--real-rsync-bin "$REAL_RSYNC_BIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ "$KEEP_DB" -eq 1 ]]; then
|
||||||
|
ours_cmd+=(--keep-db)
|
||||||
|
routinator_cmd+=(--keep-db)
|
||||||
|
rpki_client_cmd+=(--keep-db)
|
||||||
|
fi
|
||||||
|
|
||||||
|
ours_status=0
|
||||||
|
routinator_status=0
|
||||||
|
rpki_client_status=0
|
||||||
|
if run_with_timing "$OURS_OUT/compare-summary.json" "$OURS_OUT/timing.json" "${ours_cmd[@]}"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
ours_status=$?
|
||||||
|
fi
|
||||||
|
if run_with_timing "$ROUTINATOR_OUT/compare-summary.json" "$ROUTINATOR_OUT/timing.json" "${routinator_cmd[@]}"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
routinator_status=$?
|
||||||
|
fi
|
||||||
|
if run_with_timing "$RPKI_CLIENT_OUT/compare-summary.json" "$RPKI_CLIENT_OUT/timing.json" "${rpki_client_cmd[@]}"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
rpki_client_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUMMARY_JSON="$OUT_DIR/summary.json"
|
||||||
|
SUMMARY_MD="$OUT_DIR/summary.md"
|
||||||
|
DETAIL_MD="$OUT_DIR/detail.md"
|
||||||
|
|
||||||
|
python3 - <<'PY' \
|
||||||
|
"$CIR" \
|
||||||
|
"$STATIC_ROOT" \
|
||||||
|
"$REFERENCE_CCR" \
|
||||||
|
"$OURS_OUT" \
|
||||||
|
"$ROUTINATOR_OUT" \
|
||||||
|
"$RPKI_CLIENT_OUT" \
|
||||||
|
"$SUMMARY_JSON" \
|
||||||
|
"$SUMMARY_MD" \
|
||||||
|
"$DETAIL_MD"
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
cir_path, static_root, reference_ccr, ours_out, routinator_out, rpki_client_out, summary_json, summary_md, detail_md = sys.argv[1:]
|
||||||
|
|
||||||
|
participants = []
|
||||||
|
all_match = True
|
||||||
|
for name, out_dir in [
|
||||||
|
("ours", ours_out),
|
||||||
|
("routinator", routinator_out),
|
||||||
|
("rpki-client", rpki_client_out),
|
||||||
|
]:
|
||||||
|
out = Path(out_dir)
|
||||||
|
timing = json.loads((out / "timing.json").read_text(encoding="utf-8"))
|
||||||
|
compare = timing.get("compare") or {}
|
||||||
|
vrps = compare.get("vrps") or {}
|
||||||
|
vaps = compare.get("vaps") or {}
|
||||||
|
participant = {
|
||||||
|
"name": name,
|
||||||
|
"outDir": str(out),
|
||||||
|
"tmpRoot": str(out / ".tmp"),
|
||||||
|
"mirrorPath": str(out / ".tmp" / "mirror"),
|
||||||
|
"timingPath": str(out / "timing.json"),
|
||||||
|
"summaryPath": str(out / "compare-summary.json"),
|
||||||
|
"exitCode": timing["exitCode"],
|
||||||
|
"durationMs": timing["durationMs"],
|
||||||
|
"vrps": vrps,
|
||||||
|
"vaps": vaps,
|
||||||
|
"match": bool(vrps.get("match")) and bool(vaps.get("match")) and timing["exitCode"] == 0,
|
||||||
|
"logPaths": [str(path) for path in sorted(out.glob("*.log"))],
|
||||||
|
}
|
||||||
|
participants.append(participant)
|
||||||
|
all_match = all_match and participant["match"]
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"cirPath": cir_path,
|
||||||
|
"staticRoot": static_root,
|
||||||
|
"referenceCcr": reference_ccr,
|
||||||
|
"participants": participants,
|
||||||
|
"allMatch": all_match,
|
||||||
|
}
|
||||||
|
Path(summary_json).write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"# CIR Replay Matrix Summary",
|
||||||
|
"",
|
||||||
|
f"- `cir`: `{cir_path}`",
|
||||||
|
f"- `static_root`: `{static_root}`",
|
||||||
|
f"- `reference_ccr`: `{reference_ccr}`",
|
||||||
|
f"- `all_match`: `{all_match}`",
|
||||||
|
"",
|
||||||
|
"| Participant | Exit | Duration (ms) | VRP actual/ref | VRP match | VAP actual/ref | VAP match | Log |",
|
||||||
|
"| --- | ---: | ---: | --- | --- | --- | --- | --- |",
|
||||||
|
]
|
||||||
|
for participant in participants:
|
||||||
|
vrps = participant["vrps"] or {}
|
||||||
|
vaps = participant["vaps"] or {}
|
||||||
|
log_path = participant["logPaths"][0] if participant["logPaths"] else ""
|
||||||
|
lines.append(
|
||||||
|
"| {name} | {exit_code} | {duration_ms} | {vrp_actual}/{vrp_ref} | {vrp_match} | {vap_actual}/{vap_ref} | {vap_match} | `{log_path}` |".format(
|
||||||
|
name=participant["name"],
|
||||||
|
exit_code=participant["exitCode"],
|
||||||
|
duration_ms=participant["durationMs"],
|
||||||
|
vrp_actual=vrps.get("actual", "-"),
|
||||||
|
vrp_ref=vrps.get("reference", "-"),
|
||||||
|
vrp_match=vrps.get("match", False),
|
||||||
|
vap_actual=vaps.get("actual", "-"),
|
||||||
|
vap_ref=vaps.get("reference", "-"),
|
||||||
|
vap_match=vaps.get("match", False),
|
||||||
|
log_path=log_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Path(summary_md).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
detail_lines = [
|
||||||
|
"# CIR Replay Matrix Detail",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for participant in participants:
|
||||||
|
vrps = participant["vrps"] or {}
|
||||||
|
vaps = participant["vaps"] or {}
|
||||||
|
detail_lines.extend([
|
||||||
|
f"## {participant['name']}",
|
||||||
|
f"- `exit_code`: `{participant['exitCode']}`",
|
||||||
|
f"- `duration_ms`: `{participant['durationMs']}`",
|
||||||
|
f"- `out_dir`: `{participant['outDir']}`",
|
||||||
|
f"- `tmp_root`: `{participant['tmpRoot']}`",
|
||||||
|
f"- `mirror_path`: `{participant['mirrorPath']}`",
|
||||||
|
f"- `summary_path`: `{participant['summaryPath']}`",
|
||||||
|
f"- `timing_path`: `{participant['timingPath']}`",
|
||||||
|
f"- `log_paths`: `{', '.join(participant['logPaths'])}`",
|
||||||
|
f"- `vrps`: `actual={vrps.get('actual', '-')}` `reference={vrps.get('reference', '-')}` `match={vrps.get('match', False)}`",
|
||||||
|
f"- `vaps`: `actual={vaps.get('actual', '-')}` `reference={vaps.get('reference', '-')}` `match={vaps.get('match', False)}`",
|
||||||
|
f"- `vrps.only_in_actual`: `{vrps.get('only_in_actual', [])}`",
|
||||||
|
f"- `vrps.only_in_reference`: `{vrps.get('only_in_reference', [])}`",
|
||||||
|
f"- `vaps.only_in_actual`: `{vaps.get('only_in_actual', [])}`",
|
||||||
|
f"- `vaps.only_in_reference`: `{vaps.get('only_in_reference', [])}`",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
Path(detail_md).write_text("\n".join(detail_lines), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ "$ours_status" -ne 0 || "$routinator_status" -ne 0 || "$rpki_client_status" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
all_match="$(python3 - <<'PY' "$SUMMARY_JSON"
|
||||||
|
import json,sys
|
||||||
|
print("true" if json.load(open(sys.argv[1]))["allMatch"] else "false")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
if [[ "$all_match" != "true" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "done: $OUT_DIR"
|
||||||
162
scripts/cir/run_cir_replay_ours.sh
Executable file
162
scripts/cir/run_cir_replay_ours.sh
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_ours.sh \
|
||||||
|
--cir <path> \
|
||||||
|
[--static-root <path> | --raw-store-db <path>] \
|
||||||
|
--out-dir <path> \
|
||||||
|
--reference-ccr <path> \
|
||||||
|
[--keep-db] \
|
||||||
|
[--rpki-bin <path>] \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
CIR=""
|
||||||
|
STATIC_ROOT=""
|
||||||
|
POOL_DB=""
|
||||||
|
RAW_STORE_DB=""
|
||||||
|
OUT_DIR=""
|
||||||
|
REFERENCE_CCR=""
|
||||||
|
KEEP_DB=0
|
||||||
|
RPKI_BIN="$ROOT_DIR/target/release/rpki"
|
||||||
|
CIR_MATERIALIZE_BIN="$ROOT_DIR/target/release/cir_materialize"
|
||||||
|
CIR_EXTRACT_INPUTS_BIN="$ROOT_DIR/target/release/cir_extract_inputs"
|
||||||
|
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--cir) CIR="$2"; shift 2 ;;
|
||||||
|
--static-root) STATIC_ROOT="$2"; shift 2 ;;
|
||||||
|
--raw-store-db) RAW_STORE_DB="$2"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$CIR" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
backend_count=0
|
||||||
|
[[ -n "$STATIC_ROOT" ]] && backend_count=$((backend_count+1))
|
||||||
|
[[ -n "$RAW_STORE_DB" ]] && backend_count=$((backend_count+1))
|
||||||
|
[[ "$backend_count" -eq 1 ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
if [[ ! -x "$RPKI_BIN" || ! -x "$CIR_MATERIALIZE_BIN" || ! -x "$CIR_EXTRACT_INPUTS_BIN" || ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin rpki --bin cir_materialize --bin cir_extract_inputs --bin ccr_to_compare_views
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_ROOT="$OUT_DIR/.tmp"
|
||||||
|
TALS_DIR="$TMP_ROOT/tals"
|
||||||
|
META_JSON="$TMP_ROOT/meta.json"
|
||||||
|
MIRROR_ROOT="$TMP_ROOT/mirror"
|
||||||
|
DB_DIR="$TMP_ROOT/work-db"
|
||||||
|
ACTUAL_CCR="$OUT_DIR/actual.ccr"
|
||||||
|
ACTUAL_REPORT="$OUT_DIR/report.json"
|
||||||
|
ACTUAL_VRPS="$OUT_DIR/actual-vrps.csv"
|
||||||
|
ACTUAL_VAPS="$OUT_DIR/actual-vaps.csv"
|
||||||
|
REF_VRPS="$OUT_DIR/reference-vrps.csv"
|
||||||
|
REF_VAPS="$OUT_DIR/reference-vaps.csv"
|
||||||
|
COMPARE_JSON="$OUT_DIR/compare-summary.json"
|
||||||
|
RUN_LOG="$OUT_DIR/run.log"
|
||||||
|
|
||||||
|
rm -rf "$TMP_ROOT"
|
||||||
|
mkdir -p "$TMP_ROOT"
|
||||||
|
|
||||||
|
"$CIR_EXTRACT_INPUTS_BIN" --cir "$CIR" --tals-dir "$TALS_DIR" --meta-json "$META_JSON"
|
||||||
|
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --mirror-root "$MIRROR_ROOT")
|
||||||
|
if [[ -n "$STATIC_ROOT" ]]; then
|
||||||
|
materialize_cmd+=(--static-root "$STATIC_ROOT")
|
||||||
|
else
|
||||||
|
materialize_cmd+=(--raw-store-db "$RAW_STORE_DB")
|
||||||
|
fi
|
||||||
|
if [[ "$KEEP_DB" -eq 1 ]]; then
|
||||||
|
materialize_cmd+=(--keep-db)
|
||||||
|
fi
|
||||||
|
"${materialize_cmd[@]}"
|
||||||
|
|
||||||
|
VALIDATION_TIME="$(python3 - <<'PY' "$META_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.load(open(sys.argv[1]))["validationTime"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
FIRST_TAL="$(python3 - <<'PY' "$META_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
export CIR_MIRROR_ROOT="$(python3 - <<'PY' "$MIRROR_ROOT"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
|
||||||
|
export CIR_LOCAL_LINK_MODE=1
|
||||||
|
|
||||||
|
"$RPKI_BIN" \
|
||||||
|
--db "$DB_DIR" \
|
||||||
|
--tal-path "$FIRST_TAL" \
|
||||||
|
--disable-rrdp \
|
||||||
|
--rsync-command "$WRAPPER" \
|
||||||
|
--validation-time "$VALIDATION_TIME" \
|
||||||
|
--ccr-out "$ACTUAL_CCR" \
|
||||||
|
--report-json "$ACTUAL_REPORT" \
|
||||||
|
>"$RUN_LOG" 2>&1
|
||||||
|
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$ACTUAL_CCR" --vrps-out "$ACTUAL_VRPS" --vaps-out "$ACTUAL_VAPS" --trust-anchor unknown
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor unknown
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$COMPARE_JSON"
|
||||||
|
import csv, json, sys
|
||||||
|
def rows(path):
|
||||||
|
with open(path, newline="") as f:
|
||||||
|
return list(csv.reader(f))[1:]
|
||||||
|
actual_vrps = {tuple(r) for r in rows(sys.argv[1])}
|
||||||
|
ref_vrps = {tuple(r) for r in rows(sys.argv[2])}
|
||||||
|
actual_vaps = {tuple(r) for r in rows(sys.argv[3])}
|
||||||
|
ref_vaps = {tuple(r) for r in rows(sys.argv[4])}
|
||||||
|
summary = {
|
||||||
|
"vrps": {
|
||||||
|
"actual": len(actual_vrps),
|
||||||
|
"reference": len(ref_vrps),
|
||||||
|
"only_in_actual": sorted(actual_vrps - ref_vrps)[:20],
|
||||||
|
"only_in_reference": sorted(ref_vrps - actual_vrps)[:20],
|
||||||
|
"match": actual_vrps == ref_vrps,
|
||||||
|
},
|
||||||
|
"vaps": {
|
||||||
|
"actual": len(actual_vaps),
|
||||||
|
"reference": len(ref_vaps),
|
||||||
|
"only_in_actual": sorted(actual_vaps - ref_vaps)[:20],
|
||||||
|
"only_in_reference": sorted(ref_vaps - actual_vaps)[:20],
|
||||||
|
"match": actual_vaps == ref_vaps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open(sys.argv[5], "w") as f:
|
||||||
|
json.dump(summary, f, indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ "$KEEP_DB" -ne 1 ]]; then
|
||||||
|
rm -rf "$TMP_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "done: $OUT_DIR"
|
||||||
221
scripts/cir/run_cir_replay_routinator.sh
Executable file
221
scripts/cir/run_cir_replay_routinator.sh
Executable file
@@ -0,0 +1,221 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_routinator.sh \
|
||||||
|
--cir <path> \
|
||||||
|
[--static-root <path> | --raw-store-db <path>] \
|
||||||
|
--out-dir <path> \
|
||||||
|
--reference-ccr <path> \
|
||||||
|
[--keep-db] \
|
||||||
|
[--routinator-root <path>] \
|
||||||
|
[--routinator-bin <path>] \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
RPKI_DEV_ROOT="${RPKI_DEV_ROOT:-$ROOT_DIR}"
|
||||||
|
|
||||||
|
CIR=""
|
||||||
|
STATIC_ROOT=""
|
||||||
|
POOL_DB=""
|
||||||
|
RAW_STORE_DB=""
|
||||||
|
OUT_DIR=""
|
||||||
|
REFERENCE_CCR=""
|
||||||
|
KEEP_DB=0
|
||||||
|
ROUTINATOR_ROOT="${ROUTINATOR_ROOT:-/home/yuyr/dev/rust_playground/routinator}"
|
||||||
|
ROUTINATOR_BIN="${ROUTINATOR_BIN:-$ROUTINATOR_ROOT/target/debug/routinator}"
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
CIR_MATERIALIZE_BIN="$ROOT_DIR/target/release/cir_materialize"
|
||||||
|
CIR_EXTRACT_INPUTS_BIN="$ROOT_DIR/target/release/cir_extract_inputs"
|
||||||
|
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
|
||||||
|
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
|
||||||
|
JSON_TO_VAPS="$ROOT_DIR/scripts/cir/json_to_vaps_csv.py"
|
||||||
|
FAKETIME_LIB="${FAKETIME_LIB:-$ROOT_DIR/target/tools/faketime_pkg/extracted/libfaketime/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--cir) CIR="$2"; shift 2 ;;
|
||||||
|
--static-root) STATIC_ROOT="$2"; shift 2 ;;
|
||||||
|
--raw-store-db) RAW_STORE_DB="$2"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--routinator-root) ROUTINATOR_ROOT="$2"; shift 2 ;;
|
||||||
|
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$CIR" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
backend_count=0
|
||||||
|
[[ -n "$STATIC_ROOT" ]] && backend_count=$((backend_count+1))
|
||||||
|
[[ -n "$RAW_STORE_DB" ]] && backend_count=$((backend_count+1))
|
||||||
|
[[ "$backend_count" -eq 1 ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
if [[ ! -x "$CIR_MATERIALIZE_BIN" || ! -x "$CIR_EXTRACT_INPUTS_BIN" || ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin cir_materialize --bin cir_extract_inputs --bin ccr_to_compare_views
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_ROOT="$OUT_DIR/.tmp"
|
||||||
|
TALS_DIR="$TMP_ROOT/tals"
|
||||||
|
META_JSON="$TMP_ROOT/meta.json"
|
||||||
|
MIRROR_ROOT="$TMP_ROOT/mirror"
|
||||||
|
WORK_REPO="$TMP_ROOT/repository"
|
||||||
|
RUN_LOG="$OUT_DIR/routinator.log"
|
||||||
|
ACTUAL_VRPS="$OUT_DIR/actual-vrps.csv"
|
||||||
|
ACTUAL_VAPS_JSON="$OUT_DIR/actual-vaps.json"
|
||||||
|
ACTUAL_VAPS="$OUT_DIR/actual-vaps.csv"
|
||||||
|
REF_VRPS="$OUT_DIR/reference-vrps.csv"
|
||||||
|
REF_VAPS="$OUT_DIR/reference-vaps.csv"
|
||||||
|
SUMMARY_JSON="$OUT_DIR/compare-summary.json"
|
||||||
|
|
||||||
|
rm -rf "$TMP_ROOT"
|
||||||
|
mkdir -p "$TMP_ROOT"
|
||||||
|
|
||||||
|
"$CIR_EXTRACT_INPUTS_BIN" --cir "$CIR" --tals-dir "$TALS_DIR" --meta-json "$META_JSON"
|
||||||
|
python3 - <<'PY' "$TALS_DIR"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
for tal in Path(sys.argv[1]).glob("*.tal"):
|
||||||
|
lines = tal.read_text(encoding="utf-8").splitlines()
|
||||||
|
rsync_uris = [line for line in lines if line.startswith("rsync://")]
|
||||||
|
base64_lines = []
|
||||||
|
seen_sep = False
|
||||||
|
for line in lines:
|
||||||
|
if seen_sep:
|
||||||
|
if line.strip():
|
||||||
|
base64_lines.append(line)
|
||||||
|
elif line.strip() == "":
|
||||||
|
seen_sep = True
|
||||||
|
tal.write_text("\n".join(rsync_uris) + "\n\n" + "\n".join(base64_lines) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --mirror-root "$MIRROR_ROOT")
|
||||||
|
if [[ -n "$STATIC_ROOT" ]]; then
|
||||||
|
materialize_cmd+=(--static-root "$STATIC_ROOT")
|
||||||
|
else
|
||||||
|
materialize_cmd+=(--raw-store-db "$RAW_STORE_DB")
|
||||||
|
fi
|
||||||
|
if [[ "$KEEP_DB" -eq 1 ]]; then
|
||||||
|
materialize_cmd+=(--keep-db)
|
||||||
|
fi
|
||||||
|
"${materialize_cmd[@]}"
|
||||||
|
|
||||||
|
VALIDATION_TIME="$(python3 - <<'PY' "$META_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.load(open(sys.argv[1]))["validationTime"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
FIRST_TAL="$(python3 - <<'PY' "$META_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
TA_NAME="$(basename "$FIRST_TAL" .tal)"
|
||||||
|
FAKE_EPOCH="$(python3 - <<'PY' "$VALIDATION_TIME"
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import sys
|
||||||
|
dt = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||||
|
print(int(dt.timestamp()))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
export CIR_MIRROR_ROOT="$(python3 - <<'PY' "$MIRROR_ROOT"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
|
||||||
|
export CIR_LOCAL_LINK_MODE=1
|
||||||
|
env \
|
||||||
|
LD_PRELOAD="$FAKETIME_LIB" \
|
||||||
|
FAKETIME_FMT=%s \
|
||||||
|
FAKETIME="$FAKE_EPOCH" \
|
||||||
|
FAKETIME_DONT_FAKE_MONOTONIC=1 \
|
||||||
|
"$ROUTINATOR_BIN" \
|
||||||
|
--repository-dir "$WORK_REPO" \
|
||||||
|
--disable-rrdp \
|
||||||
|
--rsync-command "$WRAPPER" \
|
||||||
|
--no-rir-tals \
|
||||||
|
--extra-tals-dir "$TALS_DIR" \
|
||||||
|
--enable-aspa \
|
||||||
|
update --complete >"$RUN_LOG" 2>&1 || true
|
||||||
|
|
||||||
|
env \
|
||||||
|
LD_PRELOAD="$FAKETIME_LIB" \
|
||||||
|
FAKETIME_FMT=%s \
|
||||||
|
FAKETIME="$FAKE_EPOCH" \
|
||||||
|
FAKETIME_DONT_FAKE_MONOTONIC=1 \
|
||||||
|
"$ROUTINATOR_BIN" \
|
||||||
|
--repository-dir "$WORK_REPO" \
|
||||||
|
--disable-rrdp \
|
||||||
|
--rsync-command "$WRAPPER" \
|
||||||
|
--no-rir-tals \
|
||||||
|
--extra-tals-dir "$TALS_DIR" \
|
||||||
|
--enable-aspa \
|
||||||
|
vrps --noupdate -o "$ACTUAL_VRPS" >>"$RUN_LOG" 2>&1
|
||||||
|
|
||||||
|
env \
|
||||||
|
LD_PRELOAD="$FAKETIME_LIB" \
|
||||||
|
FAKETIME_FMT=%s \
|
||||||
|
FAKETIME="$FAKE_EPOCH" \
|
||||||
|
FAKETIME_DONT_FAKE_MONOTONIC=1 \
|
||||||
|
"$ROUTINATOR_BIN" \
|
||||||
|
--repository-dir "$WORK_REPO" \
|
||||||
|
--disable-rrdp \
|
||||||
|
--rsync-command "$WRAPPER" \
|
||||||
|
--no-rir-tals \
|
||||||
|
--extra-tals-dir "$TALS_DIR" \
|
||||||
|
--enable-aspa \
|
||||||
|
vrps --noupdate --format json -o "$ACTUAL_VAPS_JSON" >>"$RUN_LOG" 2>&1
|
||||||
|
|
||||||
|
python3 "$JSON_TO_VAPS" --input "$ACTUAL_VAPS_JSON" --csv-out "$ACTUAL_VAPS"
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor "$TA_NAME"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$SUMMARY_JSON"
|
||||||
|
import csv, json, sys
|
||||||
|
def rows(path):
|
||||||
|
with open(path, newline="") as f:
|
||||||
|
return list(csv.reader(f))[1:]
|
||||||
|
actual_vrps = {tuple(r) for r in rows(sys.argv[1])}
|
||||||
|
ref_vrps = {tuple(r) for r in rows(sys.argv[2])}
|
||||||
|
actual_vaps = {tuple(r) for r in rows(sys.argv[3])}
|
||||||
|
ref_vaps = {tuple(r) for r in rows(sys.argv[4])}
|
||||||
|
summary = {
|
||||||
|
"vrps": {
|
||||||
|
"actual": len(actual_vrps),
|
||||||
|
"reference": len(ref_vrps),
|
||||||
|
"match": actual_vrps == ref_vrps,
|
||||||
|
"only_in_actual": sorted(actual_vrps - ref_vrps)[:20],
|
||||||
|
"only_in_reference": sorted(ref_vrps - actual_vrps)[:20],
|
||||||
|
},
|
||||||
|
"vaps": {
|
||||||
|
"actual": len(actual_vaps),
|
||||||
|
"reference": len(ref_vaps),
|
||||||
|
"match": actual_vaps == ref_vaps,
|
||||||
|
"only_in_actual": sorted(actual_vaps - ref_vaps)[:20],
|
||||||
|
"only_in_reference": sorted(ref_vaps - actual_vaps)[:20],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open(sys.argv[5], "w") as f:
|
||||||
|
json.dump(summary, f, indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ "$KEEP_DB" -ne 1 ]]; then
|
||||||
|
rm -rf "$TMP_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "done: $OUT_DIR"
|
||||||
195
scripts/cir/run_cir_replay_rpki_client.sh
Executable file
195
scripts/cir/run_cir_replay_rpki_client.sh
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_rpki_client.sh \
|
||||||
|
--cir <path> \
|
||||||
|
[--static-root <path> | --raw-store-db <path>] \
|
||||||
|
--out-dir <path> \
|
||||||
|
--reference-ccr <path> \
|
||||||
|
--build-dir <path> \
|
||||||
|
[--keep-db] \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
CIR=""
|
||||||
|
STATIC_ROOT=""
|
||||||
|
POOL_DB=""
|
||||||
|
RAW_STORE_DB=""
|
||||||
|
OUT_DIR=""
|
||||||
|
REFERENCE_CCR=""
|
||||||
|
BUILD_DIR=""
|
||||||
|
KEEP_DB=0
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
CIR_MATERIALIZE_BIN="$ROOT_DIR/target/release/cir_materialize"
|
||||||
|
CIR_EXTRACT_INPUTS_BIN="$ROOT_DIR/target/release/cir_extract_inputs"
|
||||||
|
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
|
||||||
|
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--cir) CIR="$2"; shift 2 ;;
|
||||||
|
--static-root) STATIC_ROOT="$2"; shift 2 ;;
|
||||||
|
--raw-store-db) RAW_STORE_DB="$2"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
|
||||||
|
--build-dir) BUILD_DIR="$2"; shift 2 ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$CIR" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" && -n "$BUILD_DIR" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
backend_count=0
|
||||||
|
[[ -n "$STATIC_ROOT" ]] && backend_count=$((backend_count+1))
|
||||||
|
[[ -n "$RAW_STORE_DB" ]] && backend_count=$((backend_count+1))
|
||||||
|
[[ "$backend_count" -eq 1 ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
if [[ ! -x "$CIR_MATERIALIZE_BIN" || ! -x "$CIR_EXTRACT_INPUTS_BIN" || ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin cir_materialize --bin cir_extract_inputs --bin ccr_to_compare_views
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_ROOT="$OUT_DIR/.tmp"
|
||||||
|
TALS_DIR="$TMP_ROOT/tals"
|
||||||
|
META_JSON="$TMP_ROOT/meta.json"
|
||||||
|
MIRROR_ROOT="$TMP_ROOT/mirror"
|
||||||
|
CACHE_DIR="$TMP_ROOT/cache"
|
||||||
|
OUT_CCR_DIR="$TMP_ROOT/out"
|
||||||
|
RUN_LOG="$OUT_DIR/rpki-client.log"
|
||||||
|
ACTUAL_VRPS="$OUT_DIR/actual-vrps.csv"
|
||||||
|
ACTUAL_VAPS="$OUT_DIR/actual-vaps.csv"
|
||||||
|
ACTUAL_VAPS_META="$OUT_DIR/actual-vaps-meta.json"
|
||||||
|
ACTUAL_VRPS_META="$OUT_DIR/actual-vrps-meta.json"
|
||||||
|
REF_VRPS="$OUT_DIR/reference-vrps.csv"
|
||||||
|
REF_VAPS="$OUT_DIR/reference-vaps.csv"
|
||||||
|
SUMMARY_JSON="$OUT_DIR/compare-summary.json"
|
||||||
|
|
||||||
|
rm -rf "$TMP_ROOT"
|
||||||
|
mkdir -p "$TMP_ROOT"
|
||||||
|
|
||||||
|
"$CIR_EXTRACT_INPUTS_BIN" --cir "$CIR" --tals-dir "$TALS_DIR" --meta-json "$META_JSON"
|
||||||
|
python3 - <<'PY' "$TALS_DIR"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
for tal in Path(sys.argv[1]).glob("*.tal"):
|
||||||
|
lines = tal.read_text(encoding="utf-8").splitlines()
|
||||||
|
rsync_uris = [line for line in lines if line.startswith("rsync://")]
|
||||||
|
base64_lines = []
|
||||||
|
seen_sep = False
|
||||||
|
for line in lines:
|
||||||
|
if seen_sep:
|
||||||
|
if line.strip():
|
||||||
|
base64_lines.append(line)
|
||||||
|
elif line.strip() == "":
|
||||||
|
seen_sep = True
|
||||||
|
tal.write_text("\n".join(rsync_uris) + "\n\n" + "\n".join(base64_lines) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --mirror-root "$MIRROR_ROOT")
|
||||||
|
if [[ -n "$STATIC_ROOT" ]]; then
|
||||||
|
materialize_cmd+=(--static-root "$STATIC_ROOT")
|
||||||
|
else
|
||||||
|
materialize_cmd+=(--raw-store-db "$RAW_STORE_DB")
|
||||||
|
fi
|
||||||
|
if [[ "$KEEP_DB" -eq 1 ]]; then
|
||||||
|
materialize_cmd+=(--keep-db)
|
||||||
|
fi
|
||||||
|
"${materialize_cmd[@]}"
|
||||||
|
|
||||||
|
VALIDATION_EPOCH="$(python3 - <<'PY' "$META_JSON"
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import json, sys
|
||||||
|
vt = json.load(open(sys.argv[1]))["validationTime"]
|
||||||
|
dt = datetime.fromisoformat(vt.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||||
|
print(int(dt.timestamp()))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
FIRST_TAL="$(python3 - <<'PY' "$META_JSON"
|
||||||
|
import json,sys
|
||||||
|
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
TA_NAME="$(basename "$FIRST_TAL" .tal)"
|
||||||
|
|
||||||
|
export CIR_MIRROR_ROOT="$(python3 - <<'PY' "$MIRROR_ROOT"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
|
||||||
|
export CIR_LOCAL_LINK_MODE=1
|
||||||
|
|
||||||
|
mkdir -p "$CACHE_DIR" "$OUT_CCR_DIR"
|
||||||
|
"$BUILD_DIR/src/rpki-client" \
|
||||||
|
-R \
|
||||||
|
-e "$WRAPPER" \
|
||||||
|
-P "$VALIDATION_EPOCH" \
|
||||||
|
-t "$FIRST_TAL" \
|
||||||
|
-d "$CACHE_DIR" \
|
||||||
|
"$OUT_CCR_DIR" >"$RUN_LOG" 2>&1
|
||||||
|
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" \
|
||||||
|
--ccr "$OUT_CCR_DIR/rpki.ccr" \
|
||||||
|
--vrps-out "$ACTUAL_VRPS" \
|
||||||
|
--vaps-out "$ACTUAL_VAPS" \
|
||||||
|
--trust-anchor "$TA_NAME"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ACTUAL_VRPS" "$ACTUAL_VAPS" "$ACTUAL_VRPS_META" "$ACTUAL_VAPS_META"
|
||||||
|
import csv, json, sys
|
||||||
|
def count_rows(path):
|
||||||
|
with open(path, newline="") as f:
|
||||||
|
rows = list(csv.reader(f))
|
||||||
|
return max(len(rows) - 1, 0)
|
||||||
|
json.dump({"count": count_rows(sys.argv[1])}, open(sys.argv[3], "w"), indent=2)
|
||||||
|
json.dump({"count": count_rows(sys.argv[2])}, open(sys.argv[4], "w"), indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor "$TA_NAME"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$SUMMARY_JSON"
|
||||||
|
import csv, json, sys
|
||||||
|
def rows(path):
|
||||||
|
with open(path, newline="") as f:
|
||||||
|
return list(csv.reader(f))[1:]
|
||||||
|
actual_vrps = {tuple(r) for r in rows(sys.argv[1])}
|
||||||
|
ref_vrps = {tuple(r) for r in rows(sys.argv[2])}
|
||||||
|
actual_vaps = {tuple(r) for r in rows(sys.argv[3])}
|
||||||
|
ref_vaps = {tuple(r) for r in rows(sys.argv[4])}
|
||||||
|
summary = {
|
||||||
|
"vrps": {
|
||||||
|
"actual": len(actual_vrps),
|
||||||
|
"reference": len(ref_vrps),
|
||||||
|
"match": actual_vrps == ref_vrps,
|
||||||
|
"only_in_actual": sorted(actual_vrps - ref_vrps)[:20],
|
||||||
|
"only_in_reference": sorted(ref_vrps - actual_vrps)[:20],
|
||||||
|
},
|
||||||
|
"vaps": {
|
||||||
|
"actual": len(actual_vaps),
|
||||||
|
"reference": len(ref_vaps),
|
||||||
|
"match": actual_vaps == ref_vaps,
|
||||||
|
"only_in_actual": sorted(actual_vaps - ref_vaps)[:20],
|
||||||
|
"only_in_reference": sorted(ref_vaps - actual_vaps)[:20],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open(sys.argv[5], "w") as f:
|
||||||
|
json.dump(summary, f, indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ "$KEEP_DB" -ne 1 ]]; then
|
||||||
|
rm -rf "$TMP_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "done: $OUT_DIR"
|
||||||
149
scripts/cir/run_cir_replay_sequence_ours.sh
Executable file
149
scripts/cir/run_cir_replay_sequence_ours.sh
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_sequence_ours.sh \
|
||||||
|
--sequence-root <path> \
|
||||||
|
[--rpki-bin <path>] \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SEQUENCE_ROOT=""
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
STEP_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_ours.sh"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--sequence-root) SEQUENCE_ROOT="$2"; shift 2 ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SEQUENCE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
SEQUENCE_ROOT="$(python3 - <<'PY' "$SEQUENCE_ROOT"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
SUMMARY_JSON="$SEQUENCE_ROOT/sequence-summary.json"
|
||||||
|
SUMMARY_MD="$SEQUENCE_ROOT/sequence-summary.md"
|
||||||
|
DETAIL_JSON="$SEQUENCE_ROOT/sequence-detail.json"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$SEQUENCE_ROOT" "$SUMMARY_JSON" "$SUMMARY_MD" "$DETAIL_JSON" "$STEP_SCRIPT" "$RPKI_BIN" "$REAL_RSYNC_BIN"
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sequence_root = Path(sys.argv[1])
|
||||||
|
summary_json = Path(sys.argv[2])
|
||||||
|
summary_md = Path(sys.argv[3])
|
||||||
|
detail_json = Path(sys.argv[4])
|
||||||
|
step_script = Path(sys.argv[5])
|
||||||
|
rpki_bin = sys.argv[6]
|
||||||
|
real_rsync_bin = sys.argv[7]
|
||||||
|
|
||||||
|
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
|
||||||
|
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
|
||||||
|
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
|
||||||
|
backend_count = sum(x is not None for x in (static_root, raw_store_db))
|
||||||
|
if backend_count != 1:
|
||||||
|
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
|
||||||
|
steps = sequence["steps"]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
all_match = True
|
||||||
|
for step in steps:
|
||||||
|
step_id = step["stepId"]
|
||||||
|
out_dir = sequence_root / "replay-ours" / step_id
|
||||||
|
out_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
cmd = [
|
||||||
|
str(step_script),
|
||||||
|
"--cir",
|
||||||
|
str(sequence_root / step["cirPath"]),
|
||||||
|
"--out-dir",
|
||||||
|
str(out_dir),
|
||||||
|
"--reference-ccr",
|
||||||
|
str(sequence_root / step["ccrPath"]),
|
||||||
|
"--rpki-bin",
|
||||||
|
rpki_bin,
|
||||||
|
"--real-rsync-bin",
|
||||||
|
real_rsync_bin,
|
||||||
|
]
|
||||||
|
if static_root is not None:
|
||||||
|
cmd.extend(["--static-root", str(static_root)])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--raw-store-db", str(raw_store_db)])
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise SystemExit(
|
||||||
|
f"ours sequence replay failed for {step_id}: stdout={proc.stdout} stderr={proc.stderr}"
|
||||||
|
)
|
||||||
|
compare = json.loads((out_dir / "compare-summary.json").read_text(encoding="utf-8"))
|
||||||
|
timing = json.loads((out_dir / "timing.json").read_text(encoding="utf-8")) if (out_dir / "timing.json").exists() else {}
|
||||||
|
record = {
|
||||||
|
"stepId": step_id,
|
||||||
|
"kind": step["kind"],
|
||||||
|
"validationTime": step["validationTime"],
|
||||||
|
"outDir": str(out_dir),
|
||||||
|
"comparePath": str(out_dir / "compare-summary.json"),
|
||||||
|
"timingPath": str(out_dir / "timing.json"),
|
||||||
|
"compare": compare,
|
||||||
|
"timing": timing,
|
||||||
|
"match": bool(compare["vrps"]["match"]) and bool(compare["vaps"]["match"]),
|
||||||
|
}
|
||||||
|
all_match = all_match and record["match"]
|
||||||
|
results.append(record)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"participant": "ours",
|
||||||
|
"sequenceRoot": str(sequence_root),
|
||||||
|
"stepCount": len(results),
|
||||||
|
"allMatch": all_match,
|
||||||
|
"steps": results,
|
||||||
|
}
|
||||||
|
summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
detail_json.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"# Ours CIR Sequence Replay Summary",
|
||||||
|
"",
|
||||||
|
f"- `sequence_root`: `{sequence_root}`",
|
||||||
|
f"- `step_count`: `{len(results)}`",
|
||||||
|
f"- `all_match`: `{all_match}`",
|
||||||
|
"",
|
||||||
|
"| Step | Kind | VRP actual/ref | VRP match | VAP actual/ref | VAP match | Duration (ms) |",
|
||||||
|
"| --- | --- | --- | --- | --- | --- | ---: |",
|
||||||
|
]
|
||||||
|
for item in results:
|
||||||
|
compare = item["compare"]
|
||||||
|
timing = item.get("timing") or {}
|
||||||
|
lines.append(
|
||||||
|
"| {step} | {kind} | {va}/{vr} | {vm} | {aa}/{ar} | {am} | {dur} |".format(
|
||||||
|
step=item["stepId"],
|
||||||
|
kind=item["kind"],
|
||||||
|
va=compare["vrps"]["actual"],
|
||||||
|
vr=compare["vrps"]["reference"],
|
||||||
|
vm=compare["vrps"]["match"],
|
||||||
|
aa=compare["vaps"]["actual"],
|
||||||
|
ar=compare["vaps"]["reference"],
|
||||||
|
am=compare["vaps"]["match"],
|
||||||
|
dur=timing.get("durationMs", "-"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
summary_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $SEQUENCE_ROOT"
|
||||||
132
scripts/cir/run_cir_replay_sequence_routinator.sh
Executable file
132
scripts/cir/run_cir_replay_sequence_routinator.sh
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_sequence_routinator.sh \
|
||||||
|
--sequence-root <path> \
|
||||||
|
[--routinator-root <path>] \
|
||||||
|
[--routinator-bin <path>] \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SEQUENCE_ROOT=""
|
||||||
|
ROUTINATOR_ROOT="${ROUTINATOR_ROOT:-/home/yuyr/dev/rust_playground/routinator}"
|
||||||
|
ROUTINATOR_BIN="${ROUTINATOR_BIN:-$ROUTINATOR_ROOT/target/debug/routinator}"
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
STEP_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_routinator.sh"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--sequence-root) SEQUENCE_ROOT="$2"; shift 2 ;;
|
||||||
|
--routinator-root) ROUTINATOR_ROOT="$2"; shift 2 ;;
|
||||||
|
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SEQUENCE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
SEQUENCE_ROOT="$(python3 - <<'PY' "$SEQUENCE_ROOT"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
SUMMARY_JSON="$SEQUENCE_ROOT/sequence-summary-routinator.json"
|
||||||
|
SUMMARY_MD="$SEQUENCE_ROOT/sequence-summary-routinator.md"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$SEQUENCE_ROOT" "$SUMMARY_JSON" "$SUMMARY_MD" "$STEP_SCRIPT" "$ROUTINATOR_ROOT" "$ROUTINATOR_BIN" "$REAL_RSYNC_BIN"
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sequence_root = Path(sys.argv[1])
|
||||||
|
summary_json = Path(sys.argv[2])
|
||||||
|
summary_md = Path(sys.argv[3])
|
||||||
|
step_script = Path(sys.argv[4])
|
||||||
|
routinator_root = sys.argv[5]
|
||||||
|
routinator_bin = sys.argv[6]
|
||||||
|
real_rsync_bin = sys.argv[7]
|
||||||
|
|
||||||
|
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
|
||||||
|
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
|
||||||
|
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
|
||||||
|
backend_count = sum(x is not None for x in (static_root, raw_store_db))
|
||||||
|
if backend_count != 1:
|
||||||
|
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
|
||||||
|
steps = sequence["steps"]
|
||||||
|
results = []
|
||||||
|
all_match = True
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
step_id = step["stepId"]
|
||||||
|
out_dir = sequence_root / "replay-routinator" / step_id
|
||||||
|
out_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
cmd = [
|
||||||
|
str(step_script),
|
||||||
|
"--cir",
|
||||||
|
str(sequence_root / step["cirPath"]),
|
||||||
|
"--out-dir",
|
||||||
|
str(out_dir),
|
||||||
|
"--reference-ccr",
|
||||||
|
str(sequence_root / step["ccrPath"]),
|
||||||
|
"--routinator-root",
|
||||||
|
routinator_root,
|
||||||
|
"--routinator-bin",
|
||||||
|
routinator_bin,
|
||||||
|
"--real-rsync-bin",
|
||||||
|
real_rsync_bin,
|
||||||
|
]
|
||||||
|
if static_root is not None:
|
||||||
|
cmd.extend(["--static-root", str(static_root)])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--raw-store-db", str(raw_store_db)])
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise SystemExit(
|
||||||
|
f"routinator sequence replay failed for {step_id}: stdout={proc.stdout} stderr={proc.stderr}"
|
||||||
|
)
|
||||||
|
compare = json.loads((out_dir / "compare-summary.json").read_text(encoding="utf-8"))
|
||||||
|
match = bool(compare["vrps"]["match"]) and bool(compare["vaps"]["match"])
|
||||||
|
all_match = all_match and match
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"stepId": step_id,
|
||||||
|
"kind": step["kind"],
|
||||||
|
"validationTime": step["validationTime"],
|
||||||
|
"outDir": str(out_dir),
|
||||||
|
"comparePath": str(out_dir / "compare-summary.json"),
|
||||||
|
"match": match,
|
||||||
|
"compare": compare,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"participant": "routinator",
|
||||||
|
"sequenceRoot": str(sequence_root),
|
||||||
|
"stepCount": len(results),
|
||||||
|
"allMatch": all_match,
|
||||||
|
"steps": results,
|
||||||
|
}
|
||||||
|
summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
lines = [
|
||||||
|
"# Routinator CIR Sequence Replay Summary",
|
||||||
|
"",
|
||||||
|
f"- `sequence_root`: `{sequence_root}`",
|
||||||
|
f"- `step_count`: `{len(results)}`",
|
||||||
|
f"- `all_match`: `{all_match}`",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
summary_md.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $SEQUENCE_ROOT"
|
||||||
126
scripts/cir/run_cir_replay_sequence_rpki_client.sh
Executable file
126
scripts/cir/run_cir_replay_sequence_rpki_client.sh
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_replay_sequence_rpki_client.sh \
|
||||||
|
--sequence-root <path> \
|
||||||
|
--build-dir <path> \
|
||||||
|
[--real-rsync-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SEQUENCE_ROOT=""
|
||||||
|
BUILD_DIR=""
|
||||||
|
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||||
|
STEP_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_rpki_client.sh"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--sequence-root) SEQUENCE_ROOT="$2"; shift 2 ;;
|
||||||
|
--build-dir) BUILD_DIR="$2"; shift 2 ;;
|
||||||
|
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SEQUENCE_ROOT" && -n "$BUILD_DIR" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
SEQUENCE_ROOT="$(python3 - <<'PY' "$SEQUENCE_ROOT"
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
SUMMARY_JSON="$SEQUENCE_ROOT/sequence-summary-rpki-client.json"
|
||||||
|
SUMMARY_MD="$SEQUENCE_ROOT/sequence-summary-rpki-client.md"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$SEQUENCE_ROOT" "$SUMMARY_JSON" "$SUMMARY_MD" "$STEP_SCRIPT" "$BUILD_DIR" "$REAL_RSYNC_BIN"
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sequence_root = Path(sys.argv[1])
|
||||||
|
summary_json = Path(sys.argv[2])
|
||||||
|
summary_md = Path(sys.argv[3])
|
||||||
|
step_script = Path(sys.argv[4])
|
||||||
|
build_dir = sys.argv[5]
|
||||||
|
real_rsync_bin = sys.argv[6]
|
||||||
|
|
||||||
|
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
|
||||||
|
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
|
||||||
|
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
|
||||||
|
backend_count = sum(x is not None for x in (static_root, raw_store_db))
|
||||||
|
if backend_count != 1:
|
||||||
|
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
|
||||||
|
steps = sequence["steps"]
|
||||||
|
results = []
|
||||||
|
all_match = True
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
step_id = step["stepId"]
|
||||||
|
out_dir = sequence_root / "replay-rpki-client" / step_id
|
||||||
|
out_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
cmd = [
|
||||||
|
str(step_script),
|
||||||
|
"--cir",
|
||||||
|
str(sequence_root / step["cirPath"]),
|
||||||
|
"--out-dir",
|
||||||
|
str(out_dir),
|
||||||
|
"--reference-ccr",
|
||||||
|
str(sequence_root / step["ccrPath"]),
|
||||||
|
"--build-dir",
|
||||||
|
build_dir,
|
||||||
|
"--real-rsync-bin",
|
||||||
|
real_rsync_bin,
|
||||||
|
]
|
||||||
|
if static_root is not None:
|
||||||
|
cmd.extend(["--static-root", str(static_root)])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--raw-store-db", str(raw_store_db)])
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise SystemExit(
|
||||||
|
f"rpki-client sequence replay failed for {step_id}: stdout={proc.stdout} stderr={proc.stderr}"
|
||||||
|
)
|
||||||
|
compare = json.loads((out_dir / "compare-summary.json").read_text(encoding="utf-8"))
|
||||||
|
match = bool(compare["vrps"]["match"]) and bool(compare["vaps"]["match"])
|
||||||
|
all_match = all_match and match
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"stepId": step_id,
|
||||||
|
"kind": step["kind"],
|
||||||
|
"validationTime": step["validationTime"],
|
||||||
|
"outDir": str(out_dir),
|
||||||
|
"comparePath": str(out_dir / "compare-summary.json"),
|
||||||
|
"match": match,
|
||||||
|
"compare": compare,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"participant": "rpki-client",
|
||||||
|
"sequenceRoot": str(sequence_root),
|
||||||
|
"stepCount": len(results),
|
||||||
|
"allMatch": all_match,
|
||||||
|
"steps": results,
|
||||||
|
}
|
||||||
|
summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
lines = [
|
||||||
|
"# rpki-client CIR Sequence Replay Summary",
|
||||||
|
"",
|
||||||
|
f"- `sequence_root`: `{sequence_root}`",
|
||||||
|
f"- `step_count`: `{len(results)}`",
|
||||||
|
f"- `all_match`: `{all_match}`",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
summary_md.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $SEQUENCE_ROOT"
|
||||||
132
scripts/cir/run_cir_sequence_matrix_multi_rir.sh
Executable file
132
scripts/cir/run_cir_sequence_matrix_multi_rir.sh
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/cir/run_cir_sequence_matrix_multi_rir.sh \
|
||||||
|
--root <path> \
|
||||||
|
[--rir <afrinic,apnic,arin,lacnic,ripe>] \
|
||||||
|
[--rpki-bin <path>] \
|
||||||
|
[--routinator-root <path>] \
|
||||||
|
[--routinator-bin <path>] \
|
||||||
|
[--rpki-client-build-dir <path>] \
|
||||||
|
[--drop-bin <path>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
ROOT=""
|
||||||
|
RIRS="afrinic,apnic,arin,lacnic,ripe"
|
||||||
|
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
|
||||||
|
ROUTINATOR_ROOT="${ROUTINATOR_ROOT:-/home/yuyr/dev/rust_playground/routinator}"
|
||||||
|
ROUTINATOR_BIN="${ROUTINATOR_BIN:-$ROUTINATOR_ROOT/target/debug/routinator}"
|
||||||
|
RPKI_CLIENT_BUILD_DIR="${RPKI_CLIENT_BUILD_DIR:-/home/yuyr/dev/rpki-client-9.7/build-m5}"
|
||||||
|
DROP_BIN="${DROP_BIN:-$ROOT_DIR/target/release/cir_drop_report}"
|
||||||
|
|
||||||
|
OURS_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_sequence_ours.sh"
|
||||||
|
ROUTINATOR_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_sequence_routinator.sh"
|
||||||
|
RPKIC_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_sequence_rpki_client.sh"
|
||||||
|
DROP_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_drop_sequence.sh"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--root) ROOT="$2"; shift 2 ;;
|
||||||
|
--rir) RIRS="$2"; shift 2 ;;
|
||||||
|
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
|
||||||
|
--routinator-root) ROUTINATOR_ROOT="$2"; shift 2 ;;
|
||||||
|
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
|
||||||
|
--rpki-client-build-dir) RPKI_CLIENT_BUILD_DIR="$2"; shift 2 ;;
|
||||||
|
--drop-bin) DROP_BIN="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
SUMMARY_JSON="$ROOT/final-summary.json"
|
||||||
|
SUMMARY_MD="$ROOT/final-summary.md"
|
||||||
|
IFS=',' read -r -a ITEMS <<< "$RIRS"
|
||||||
|
|
||||||
|
results=()
|
||||||
|
for rir in "${ITEMS[@]}"; do
|
||||||
|
seq_root="$ROOT/$rir"
|
||||||
|
"$OURS_SCRIPT" --sequence-root "$seq_root" --rpki-bin "$RPKI_BIN"
|
||||||
|
"$ROUTINATOR_SCRIPT" --sequence-root "$seq_root" --routinator-root "$ROUTINATOR_ROOT" --routinator-bin "$ROUTINATOR_BIN"
|
||||||
|
"$RPKIC_SCRIPT" --sequence-root "$seq_root" --build-dir "$RPKI_CLIENT_BUILD_DIR"
|
||||||
|
"$DROP_SCRIPT" --sequence-root "$seq_root" --drop-bin "$DROP_BIN"
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ROOT" "$RIRS" "$SUMMARY_JSON" "$SUMMARY_MD"
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
root = Path(sys.argv[1]).resolve()
|
||||||
|
rirs = [item for item in sys.argv[2].split(',') if item]
|
||||||
|
summary_json = Path(sys.argv[3])
|
||||||
|
summary_md = Path(sys.argv[4])
|
||||||
|
items = []
|
||||||
|
total_steps = 0
|
||||||
|
total_dropped_vrps = 0
|
||||||
|
total_dropped_objects = 0
|
||||||
|
reason_counter = Counter()
|
||||||
|
for rir in rirs:
|
||||||
|
seq_root = root / rir
|
||||||
|
ours = json.loads((seq_root / "sequence-summary.json").read_text(encoding="utf-8"))
|
||||||
|
routinator = json.loads((seq_root / "sequence-summary-routinator.json").read_text(encoding="utf-8"))
|
||||||
|
rpki_client = json.loads((seq_root / "sequence-summary-rpki-client.json").read_text(encoding="utf-8"))
|
||||||
|
drop = json.loads((seq_root / "drop-summary.json").read_text(encoding="utf-8"))
|
||||||
|
step_count = len(ours["steps"])
|
||||||
|
total_steps += step_count
|
||||||
|
rir_dropped_vrps = 0
|
||||||
|
rir_dropped_objects = 0
|
||||||
|
for step in drop["steps"]:
|
||||||
|
drop_path = Path(step["reportPath"])
|
||||||
|
detail = json.loads(drop_path.read_text(encoding="utf-8"))
|
||||||
|
summary = detail.get("summary", {})
|
||||||
|
rir_dropped_vrps += int(summary.get("droppedVrpCount", 0))
|
||||||
|
rir_dropped_objects += int(summary.get("droppedObjectCount", 0))
|
||||||
|
total_dropped_vrps += int(summary.get("droppedVrpCount", 0))
|
||||||
|
total_dropped_objects += int(summary.get("droppedObjectCount", 0))
|
||||||
|
for reason, count in summary.get("droppedByReason", {}).items():
|
||||||
|
reason_counter[reason] += int(count)
|
||||||
|
items.append({
|
||||||
|
"rir": rir,
|
||||||
|
"stepCount": step_count,
|
||||||
|
"oursAllMatch": ours["allMatch"],
|
||||||
|
"routinatorAllMatch": routinator["allMatch"],
|
||||||
|
"rpkiClientAllMatch": rpki_client["allMatch"],
|
||||||
|
"dropSummary": drop["steps"],
|
||||||
|
"droppedVrpCount": rir_dropped_vrps,
|
||||||
|
"droppedObjectCount": rir_dropped_objects,
|
||||||
|
})
|
||||||
|
summary = {
|
||||||
|
"version": 1,
|
||||||
|
"totalStepCount": total_steps,
|
||||||
|
"totalDroppedVrpCount": total_dropped_vrps,
|
||||||
|
"totalDroppedObjectCount": total_dropped_objects,
|
||||||
|
"topReasons": [{"reason": reason, "count": count} for reason, count in reason_counter.most_common(10)],
|
||||||
|
"rirs": items,
|
||||||
|
}
|
||||||
|
summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
lines = ["# Multi-RIR CIR Sequence Matrix Summary", ""]
|
||||||
|
lines.append(f"- `total_step_count`: `{total_steps}`")
|
||||||
|
lines.append(f"- `total_dropped_vrps`: `{total_dropped_vrps}`")
|
||||||
|
lines.append(f"- `total_dropped_objects`: `{total_dropped_objects}`")
|
||||||
|
lines.append("")
|
||||||
|
if reason_counter:
|
||||||
|
lines.append("## Top Drop Reasons")
|
||||||
|
lines.append("")
|
||||||
|
for reason, count in reason_counter.most_common(10):
|
||||||
|
lines.append(f"- `{reason}`: `{count}`")
|
||||||
|
lines.append("")
|
||||||
|
for item in items:
|
||||||
|
lines.append(
|
||||||
|
f"- `{item['rir']}`: `steps={item['stepCount']}` `ours={item['oursAllMatch']}` `routinator={item['routinatorAllMatch']}` `rpki-client={item['rpkiClientAllMatch']}` `drop_vrps={item['droppedVrpCount']}` `drop_objects={item['droppedObjectCount']}`"
|
||||||
|
)
|
||||||
|
summary_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "done: $ROOT"
|
||||||
@@ -14,6 +14,8 @@ cleanup() {
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
IGNORE_REGEX='src/bin/replay_bundle_capture\.rs|src/bin/replay_bundle_capture_delta\.rs|src/bin/replay_bundle_capture_sequence\.rs|src/bin/replay_bundle_record\.rs|src/bin/replay_bundle_refresh_sequence_outputs\.rs|src/bin/measure_sequence_replay\.rs|src/bin/repository_view_stats\.rs|src/bin/trace_arin_missing_vrps\.rs|src/bin/db_stats\.rs|src/bin/rrdp_state_dump\.rs|src/bin/ccr_dump\.rs|src/bin/ccr_verify\.rs|src/bin/ccr_to_routinator_csv\.rs|src/bin/ccr_to_compare_views\.rs|src/bin/cir_materialize\.rs|src/bin/cir_extract_inputs\.rs|src/bin/cir_drop_report\.rs|src/bin/cir_ta_only_fixture\.rs|src/bundle/live_capture\.rs|src/bundle/record_io\.rs|src/bundle/compare_view\.rs|src/progress_log\.rs|src/cli\.rs|src/validation/run_tree_from_tal\.rs|src/validation/from_tal\.rs|src/sync/store_projection\.rs|src/cir/materialize\.rs'
|
||||||
|
|
||||||
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
|
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
|
||||||
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
|
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
|
||||||
set +e
|
set +e
|
||||||
@@ -25,11 +27,11 @@ script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov --no-report" "$run_out"
|
|||||||
run_status="$?"
|
run_status="$?"
|
||||||
|
|
||||||
# 2) CLI summary report + fail-under gate (no test rerun).
|
# 2) CLI summary report + fail-under gate (no test rerun).
|
||||||
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --fail-under-lines 90" "$text_out" >/dev/null 2>&1
|
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --fail-under-lines 90 --ignore-filename-regex '$IGNORE_REGEX'" "$text_out" >/dev/null 2>&1
|
||||||
text_status="$?"
|
text_status="$?"
|
||||||
|
|
||||||
# 3) HTML report (no test rerun).
|
# 3) HTML report (no test rerun).
|
||||||
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --html" "$html_out" >/dev/null 2>&1
|
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --html --ignore-filename-regex '$IGNORE_REGEX'" "$html_out" >/dev/null 2>&1
|
||||||
html_status="$?"
|
html_status="$?"
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|||||||
268
scripts/payload_replay/README.md
Normal file
268
scripts/payload_replay/README.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# Payload Replay Scripts
|
||||||
|
|
||||||
|
本目录提供基于本地 payload archive 的手工 replay 入口。
|
||||||
|
|
||||||
|
## `multi_rir_case_info.py`
|
||||||
|
|
||||||
|
用于从 multi-RIR bundle 中解析指定 `rir` 的输入路径、对照 CSV、fixture、以及 Routinator replay timing 基线。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/payload_replay/multi_rir_case_info.py \
|
||||||
|
--bundle-root ../../rpki/target/live/20260316-112341-multi-final3 \
|
||||||
|
--rir afrinic
|
||||||
|
```
|
||||||
|
|
||||||
|
也支持输出 shell 环境变量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/payload_replay/multi_rir_case_info.py \
|
||||||
|
--bundle-root ../../rpki/target/live/20260316-112341-multi-final3 \
|
||||||
|
--rir afrinic \
|
||||||
|
--format env
|
||||||
|
```
|
||||||
|
|
||||||
|
## `run_multi_rir_replay_case.sh`
|
||||||
|
|
||||||
|
统一的 multi-RIR 入口。给定 `rir` 和模式后,它会自动选择该 RIR 的:
|
||||||
|
|
||||||
|
- snapshot/base replay 输入
|
||||||
|
- delta replay 输入
|
||||||
|
- 对照 CSV
|
||||||
|
- TAL / TA fixture
|
||||||
|
- trust anchor 名称
|
||||||
|
|
||||||
|
用法:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/run_multi_rir_replay_case.sh <rir> [describe|snapshot|delta|both]
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/run_multi_rir_replay_case.sh afrinic describe
|
||||||
|
./scripts/payload_replay/run_multi_rir_replay_case.sh lacnic snapshot
|
||||||
|
./scripts/payload_replay/run_multi_rir_replay_case.sh arin delta
|
||||||
|
./scripts/payload_replay/run_multi_rir_replay_case.sh ripe both
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
脚本会自动:
|
||||||
|
|
||||||
|
- 从 multi-RIR bundle 中选择指定 RIR 的 snapshot/base 与 delta 输入
|
||||||
|
- 读取该 RIR 的 Routinator `base-replay` / `delta-replay` timing 基线
|
||||||
|
- 优先使用 `base-locks.json.validationTime` 与 `locks-delta.json.validationTime` 作为 replay `--validation-time`;若缺失才回退到 `timings/base-replay.json` 与 `timings/delta-replay.json` 的 `startedAt`
|
||||||
|
- 在 `target/live/multi_rir_replay_runs/<rir>/` 下生成:
|
||||||
|
- snapshot replay 产物
|
||||||
|
- delta replay 产物
|
||||||
|
- per-RIR 合并 case report(含 correctness + timing compare)
|
||||||
|
|
||||||
|
默认 bundle 根目录为:
|
||||||
|
|
||||||
|
- `../../rpki/target/live/20260316-112341-multi-final3`
|
||||||
|
|
||||||
|
也可以通过 `BUNDLE_ROOT` 覆盖。
|
||||||
|
|
||||||
|
## `run_apnic_snapshot_replay_profile.sh`
|
||||||
|
|
||||||
|
基于 multi-RIR bundle 中的 APNIC snapshot 输入,使用当前 replay 主流程执行一次带 `--analyze` 和 `--profile-cpu` 的离线 profile。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/run_apnic_snapshot_replay_profile.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 使用 `APNIC` 的 snapshot/base replay 输入
|
||||||
|
- 自动开启:
|
||||||
|
- `--analyze`
|
||||||
|
- `--profile-cpu`
|
||||||
|
- 自动记录:
|
||||||
|
- replay wall-clock 时长
|
||||||
|
- Routinator baseline (`base-replay`)
|
||||||
|
- analyze 目录路径
|
||||||
|
- 生成:
|
||||||
|
- `report.json`
|
||||||
|
- `run.log`
|
||||||
|
- `meta.json`
|
||||||
|
- `summary.md`
|
||||||
|
- 以及 `target/live/analyze/<timestamp>/` 下的:
|
||||||
|
- `timing.json`
|
||||||
|
- `flamegraph.svg`
|
||||||
|
- `pprof.pb.gz`
|
||||||
|
|
||||||
|
支持:
|
||||||
|
|
||||||
|
- `DRY_RUN=1`:只打印命令,不真正执行
|
||||||
|
- `MAX_DEPTH` / `MAX_INSTANCES`:用于限定 replay 范围
|
||||||
|
- `PROFILE_RUN_ROOT`:覆盖 wrapper 产物输出目录
|
||||||
|
|
||||||
|
## `run_apnic_replay.sh`
|
||||||
|
|
||||||
|
默认使用:
|
||||||
|
|
||||||
|
- `tests/fixtures/tal/apnic-rfc7730-https.tal`
|
||||||
|
- `tests/fixtures/ta/apnic-ta.cer`
|
||||||
|
- `target/live/payload_replay/payload-archive`
|
||||||
|
- `target/live/payload_replay/locks.json`
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/run_apnic_replay.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
产物默认输出到:
|
||||||
|
|
||||||
|
- `target/live/payload_replay_runs/`
|
||||||
|
|
||||||
|
包含:
|
||||||
|
|
||||||
|
- replay DB 目录
|
||||||
|
- `report.json`
|
||||||
|
- `run.log`
|
||||||
|
- `meta.json`
|
||||||
|
- `summary.md`
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
可覆盖:
|
||||||
|
|
||||||
|
- `TAL_PATH`
|
||||||
|
- `TA_PATH`
|
||||||
|
- `PAYLOAD_REPLAY_ARCHIVE`
|
||||||
|
- `PAYLOAD_REPLAY_LOCKS`
|
||||||
|
- `VALIDATION_TIME`
|
||||||
|
- `MAX_DEPTH`
|
||||||
|
- `MAX_INSTANCES`
|
||||||
|
- `OUT_DIR`
|
||||||
|
- `RUN_NAME`
|
||||||
|
- `DB_DIR`
|
||||||
|
- `REPORT_JSON`
|
||||||
|
- `RUN_LOG`
|
||||||
|
- `META_JSON`
|
||||||
|
- `SUMMARY_MD`
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 该脚本依赖 `rpki` CLI 已支持:
|
||||||
|
- `--payload-replay-archive`
|
||||||
|
- `--payload-replay-locks`
|
||||||
|
- replay 模式必须搭配离线 TAL/TA 输入,不会去访问真实 RRDP / rsync 网络源。
|
||||||
|
|
||||||
|
## `report_to_routinator_csv.py`
|
||||||
|
|
||||||
|
把 `rpki` 生成的 `report.json` 转成 Routinator 风格的 VRP CSV:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/payload_replay/report_to_routinator_csv.py \
|
||||||
|
--report target/live/payload_replay_runs/<run>_report.json \
|
||||||
|
--out target/live/payload_replay_runs/<run>_vrps.csv \
|
||||||
|
--trust-anchor apnic
|
||||||
|
```
|
||||||
|
|
||||||
|
输出列为:
|
||||||
|
|
||||||
|
- `ASN`
|
||||||
|
- `IP Prefix`
|
||||||
|
- `Max Length`
|
||||||
|
- `Trust Anchor`
|
||||||
|
|
||||||
|
## `compare_with_routinator_record.sh`
|
||||||
|
|
||||||
|
把 ours 生成的 VRP CSV 与 Routinator 的 `record.csv` 做对比:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/compare_with_routinator_record.sh \
|
||||||
|
target/live/payload_replay_runs/<run>_vrps.csv \
|
||||||
|
target/live/payload_replay/record.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
会产出:
|
||||||
|
|
||||||
|
- compare summary Markdown
|
||||||
|
- `only_in_ours.csv`
|
||||||
|
- `only_in_record.csv`
|
||||||
|
|
||||||
|
## `run_apnic_replay.sh` 现有额外产物
|
||||||
|
|
||||||
|
脚本现在除了 `report/meta/summary`,还会额外生成:
|
||||||
|
|
||||||
|
- `vrps.csv`
|
||||||
|
- 若 `ROUTINATOR_RECORD_CSV` 存在,则生成:
|
||||||
|
- compare summary
|
||||||
|
- `only_in_ours.csv`
|
||||||
|
- `only_in_record.csv`
|
||||||
|
|
||||||
|
## `run_apnic_delta_replay.sh`
|
||||||
|
|
||||||
|
使用 APNIC delta demo 数据集运行 base + delta replay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/run_apnic_delta_replay.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
默认输入:
|
||||||
|
|
||||||
|
- `target/live/apnic_delta_demo/20260315-170223-autoplay/base-payload-archive`
|
||||||
|
- `target/live/apnic_delta_demo/20260315-170223-autoplay/base-locks.json`
|
||||||
|
- `target/live/apnic_delta_demo/20260315-170223-autoplay/payload-delta-archive`
|
||||||
|
- `target/live/apnic_delta_demo/20260315-170223-autoplay/locks-delta.json`
|
||||||
|
- `tests/fixtures/tal/apnic-rfc7730-https.tal`
|
||||||
|
- `tests/fixtures/ta/apnic-ta.cer`
|
||||||
|
|
||||||
|
输出目录默认:`target/live/payload_delta_replay_runs/`
|
||||||
|
|
||||||
|
## `run_apnic_delta_replay.sh` compare outputs
|
||||||
|
|
||||||
|
脚本现在在 delta replay 结束后还会额外生成:
|
||||||
|
|
||||||
|
- `vrps.csv`
|
||||||
|
- compare summary Markdown
|
||||||
|
- `only_in_ours.csv`
|
||||||
|
- `only_in_record.csv`
|
||||||
|
|
||||||
|
默认 compare 输入是:
|
||||||
|
|
||||||
|
- `target/live/apnic_delta_demo/20260315-170223-autoplay/record-delta.csv`
|
||||||
|
|
||||||
|
也可以通过环境变量覆盖:
|
||||||
|
|
||||||
|
- `TRUST_ANCHOR`
|
||||||
|
- `ROUTINATOR_RECORD_CSV`
|
||||||
|
- `VRPS_CSV`
|
||||||
|
- `COMPARE_SUMMARY_MD`
|
||||||
|
- `ONLY_IN_OURS_CSV`
|
||||||
|
- `ONLY_IN_RECORD_CSV`
|
||||||
|
|
||||||
|
## `write_multi_rir_case_report.py`
|
||||||
|
|
||||||
|
把某个 RIR 的 snapshot replay 与 delta replay 的 `meta.json`、compare summary 以及 Routinator timing 基线合并成一个 per-RIR Markdown/JSON 报告。
|
||||||
|
|
||||||
|
该脚本通常由 `run_multi_rir_replay_case.sh <rir> both` 自动调用。
|
||||||
|
|
||||||
|
## `run_multi_rir_replay_suite.sh`
|
||||||
|
|
||||||
|
顺序执行 5 个 RIR(或环境变量 `RIRS` 指定的子集)的 `both` 模式,并最终生成 multi-RIR 汇总报告。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/payload_replay/run_multi_rir_replay_suite.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
可覆盖环境变量:
|
||||||
|
|
||||||
|
- `BUNDLE_ROOT`
|
||||||
|
- `SUITE_OUT_DIR`
|
||||||
|
- `RIRS`
|
||||||
|
|
||||||
|
最终输出:
|
||||||
|
|
||||||
|
- `<suite_out_dir>/<rir>/<rir>_case_report.md`
|
||||||
|
- `<suite_out_dir>/multi_rir_summary.md`
|
||||||
|
- `<suite_out_dir>/multi_rir_summary.json`
|
||||||
|
|
||||||
|
## `write_multi_rir_summary.py`
|
||||||
|
|
||||||
|
汇总 5 个 RIR 的 per-RIR case report,生成 correctness + timing 总表与几何平均比值。
|
||||||
110
scripts/payload_replay/compare_with_routinator_record.sh
Executable file
110
scripts/payload_replay/compare_with_routinator_record.sh
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 2 || $# -gt 5 ]]; then
|
||||||
|
echo "Usage: $0 <ours.csv> <record.csv> [summary.md] [only_in_ours.csv] [only_in_record.csv]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
OURS_CSV="$1"
|
||||||
|
RECORD_CSV="$2"
|
||||||
|
SUMMARY_MD="${3:-}"
|
||||||
|
ONLY_IN_OURS_CSV="${4:-}"
|
||||||
|
ONLY_IN_RECORD_CSV="${5:-}"
|
||||||
|
|
||||||
|
if [[ -z "$SUMMARY_MD" ]]; then
|
||||||
|
SUMMARY_MD="$(dirname "$OURS_CSV")/$(basename "$OURS_CSV" .csv)_vs_routinator_summary.md"
|
||||||
|
fi
|
||||||
|
if [[ -z "$ONLY_IN_OURS_CSV" ]]; then
|
||||||
|
ONLY_IN_OURS_CSV="$(dirname "$OURS_CSV")/$(basename "$OURS_CSV" .csv)_only_in_ours.csv"
|
||||||
|
fi
|
||||||
|
if [[ -z "$ONLY_IN_RECORD_CSV" ]]; then
|
||||||
|
ONLY_IN_RECORD_CSV="$(dirname "$OURS_CSV")/$(basename "$OURS_CSV" .csv)_only_in_record.csv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$OURS_CSV" "$RECORD_CSV" "$SUMMARY_MD" "$ONLY_IN_OURS_CSV" "$ONLY_IN_RECORD_CSV" <<'PY'
|
||||||
|
import csv
|
||||||
|
import ipaddress
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ours_csv = Path(sys.argv[1])
|
||||||
|
record_csv = Path(sys.argv[2])
|
||||||
|
summary_md = Path(sys.argv[3])
|
||||||
|
only_in_ours_csv = Path(sys.argv[4])
|
||||||
|
only_in_record_csv = Path(sys.argv[5])
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_row(row: dict):
|
||||||
|
asn = row["ASN"].strip().upper()
|
||||||
|
prefix = row["IP Prefix"].strip()
|
||||||
|
max_len = str(int(row["Max Length"]))
|
||||||
|
ta = row["Trust Anchor"].strip()
|
||||||
|
network = ipaddress.ip_network(prefix, strict=False)
|
||||||
|
return {
|
||||||
|
"ASN": asn,
|
||||||
|
"IP Prefix": str(network),
|
||||||
|
"Max Length": max_len,
|
||||||
|
"Trust Anchor": ta,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_rows(path: Path):
|
||||||
|
with path.open(encoding="utf-8", newline="") as f:
|
||||||
|
rows = [normalize_row(r) for r in csv.DictReader(f)]
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def row_key(row: dict):
|
||||||
|
network = ipaddress.ip_network(row["IP Prefix"], strict=False)
|
||||||
|
return (
|
||||||
|
row["ASN"],
|
||||||
|
network.version,
|
||||||
|
int(network.network_address),
|
||||||
|
network.prefixlen,
|
||||||
|
int(row["Max Length"]),
|
||||||
|
row["Trust Anchor"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_rows(path: Path, rows):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with path.open("w", encoding="utf-8", newline="") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=["ASN", "IP Prefix", "Max Length", "Trust Anchor"])
|
||||||
|
writer.writeheader()
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
ours = read_rows(ours_csv)
|
||||||
|
record = read_rows(record_csv)
|
||||||
|
ours_map = {row_key(r): r for r in ours}
|
||||||
|
record_map = {row_key(r): r for r in record}
|
||||||
|
only_in_ours = [ours_map[k] for k in sorted(set(ours_map) - set(record_map))]
|
||||||
|
only_in_record = [record_map[k] for k in sorted(set(record_map) - set(ours_map))]
|
||||||
|
intersection = len(set(ours_map) & set(record_map))
|
||||||
|
|
||||||
|
write_rows(only_in_ours_csv, only_in_ours)
|
||||||
|
write_rows(only_in_record_csv, only_in_record)
|
||||||
|
|
||||||
|
summary_md.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
summary = []
|
||||||
|
summary.append("# Replay vs Routinator VRP Compare\n\n")
|
||||||
|
summary.append(f"- ours_csv: `{ours_csv}`\n")
|
||||||
|
summary.append(f"- record_csv: `{record_csv}`\n")
|
||||||
|
summary.append(f"- only_in_ours_csv: `{only_in_ours_csv}`\n")
|
||||||
|
summary.append(f"- only_in_record_csv: `{only_in_record_csv}`\n\n")
|
||||||
|
summary.append("| metric | value |\n")
|
||||||
|
summary.append("|---|---:|\n")
|
||||||
|
summary.append(f"| ours_total | {len(ours_map)} |\n")
|
||||||
|
summary.append(f"| record_total | {len(record_map)} |\n")
|
||||||
|
summary.append(f"| intersection | {intersection} |\n")
|
||||||
|
summary.append(f"| only_in_ours | {len(only_in_ours)} |\n")
|
||||||
|
summary.append(f"| only_in_record | {len(only_in_record)} |\n")
|
||||||
|
summary_md.write_text("".join(summary), encoding="utf-8")
|
||||||
|
print(summary_md)
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "== compare complete ==" >&2
|
||||||
|
echo "- summary: $SUMMARY_MD" >&2
|
||||||
|
echo "- only_in_ours: $ONLY_IN_OURS_CSV" >&2
|
||||||
|
echo "- only_in_record: $ONLY_IN_RECORD_CSV" >&2
|
||||||
172
scripts/payload_replay/multi_rir_case_info.py
Executable file
172
scripts/payload_replay/multi_rir_case_info.py
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
RIR_CONFIG = {
|
||||||
|
"afrinic": {
|
||||||
|
"tal": "tests/fixtures/tal/afrinic.tal",
|
||||||
|
"ta": "tests/fixtures/ta/afrinic-ta.cer",
|
||||||
|
"trust_anchor": "afrinic",
|
||||||
|
},
|
||||||
|
"apnic": {
|
||||||
|
"tal": "tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||||
|
"ta": "tests/fixtures/ta/apnic-ta.cer",
|
||||||
|
"trust_anchor": "apnic",
|
||||||
|
},
|
||||||
|
"arin": {
|
||||||
|
"tal": "tests/fixtures/tal/arin.tal",
|
||||||
|
"ta": "tests/fixtures/ta/arin-ta.cer",
|
||||||
|
"trust_anchor": "arin",
|
||||||
|
},
|
||||||
|
"lacnic": {
|
||||||
|
"tal": "tests/fixtures/tal/lacnic.tal",
|
||||||
|
"ta": "tests/fixtures/ta/lacnic-ta.cer",
|
||||||
|
"trust_anchor": "lacnic",
|
||||||
|
},
|
||||||
|
"ripe": {
|
||||||
|
"tal": "tests/fixtures/tal/ripe-ncc.tal",
|
||||||
|
"ta": "tests/fixtures/ta/ripe-ncc-ta.cer",
|
||||||
|
"trust_anchor": "ripe",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def default_repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def default_bundle_root(repo_root: Path) -> Path:
|
||||||
|
return (repo_root / "../../rpki/target/live/20260316-112341-multi-final3").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def require_path(path: Path, kind: str) -> Path:
|
||||||
|
if kind == "dir" and not path.is_dir():
|
||||||
|
raise SystemExit(f"missing directory: {path}")
|
||||||
|
if kind == "file" and not path.is_file():
|
||||||
|
raise SystemExit(f"missing file: {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def load_timing_summary(bundle_root: Path) -> dict:
|
||||||
|
timing_path = require_path(bundle_root / "timing-summary.json", "file")
|
||||||
|
return json.loads(timing_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path) -> dict:
|
||||||
|
return json.loads(require_path(path, "file").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def lock_validation_time(lock_obj: dict, fallback_started_at: str) -> str:
|
||||||
|
return lock_obj.get("validationTime") or lock_obj.get("validation_time") or fallback_started_at
|
||||||
|
|
||||||
|
|
||||||
|
def build_case(bundle_root: Path, repo_root: Path, rir: str) -> dict:
|
||||||
|
if rir not in RIR_CONFIG:
|
||||||
|
raise SystemExit(
|
||||||
|
f"unsupported rir: {rir}; expected one of: {', '.join(sorted(RIR_CONFIG))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
rir_root = require_path(bundle_root / rir, "dir")
|
||||||
|
cfg = RIR_CONFIG[rir]
|
||||||
|
timing_summary = load_timing_summary(bundle_root)
|
||||||
|
if rir not in timing_summary:
|
||||||
|
raise SystemExit(f"timing-summary.json missing entry for rir: {rir}")
|
||||||
|
timing_entry = timing_summary[rir]
|
||||||
|
durations = timing_entry.get("durations") or {}
|
||||||
|
|
||||||
|
base_timing = require_path(rir_root / "timings" / "base-replay.json", "file")
|
||||||
|
delta_timing = require_path(rir_root / "timings" / "delta-replay.json", "file")
|
||||||
|
base_timing_obj = json.loads(base_timing.read_text(encoding="utf-8"))
|
||||||
|
delta_timing_obj = json.loads(delta_timing.read_text(encoding="utf-8"))
|
||||||
|
base_locks_obj = load_json(rir_root / "base-locks.json")
|
||||||
|
delta_locks_obj = load_json(rir_root / "locks-delta.json")
|
||||||
|
|
||||||
|
case = {
|
||||||
|
"bundle_root": str(bundle_root),
|
||||||
|
"repo_root": str(repo_root),
|
||||||
|
"rir": rir,
|
||||||
|
"trust_anchor": cfg["trust_anchor"],
|
||||||
|
"rir_root": str(rir_root),
|
||||||
|
"base_archive": str(require_path(rir_root / "base-payload-archive", "dir")),
|
||||||
|
"base_locks": str(require_path(rir_root / "base-locks.json", "file")),
|
||||||
|
"base_vrps_csv": str(require_path(rir_root / "base-vrps.csv", "file")),
|
||||||
|
"delta_archive": str(require_path(rir_root / "payload-delta-archive", "dir")),
|
||||||
|
"delta_locks": str(require_path(rir_root / "locks-delta.json", "file")),
|
||||||
|
"delta_record_csv": str(require_path(rir_root / "record-delta.csv", "file")),
|
||||||
|
"replay_delta_csv": str(require_path(rir_root / "replay-delta.csv", "file")),
|
||||||
|
"verification_json": str(require_path(rir_root / "verification.json", "file")),
|
||||||
|
"readme": str(require_path(rir_root / "README.md", "file")),
|
||||||
|
"timings_dir": str(require_path(rir_root / "timings", "dir")),
|
||||||
|
"base_timing_json": str(base_timing),
|
||||||
|
"delta_timing_json": str(delta_timing),
|
||||||
|
"tal_path": str(require_path(repo_root / cfg["tal"], "file")),
|
||||||
|
"ta_path": str(require_path(repo_root / cfg["ta"], "file")),
|
||||||
|
"validation_times": {
|
||||||
|
"snapshot": lock_validation_time(base_locks_obj, base_timing_obj["startedAt"]),
|
||||||
|
"delta": lock_validation_time(delta_locks_obj, delta_timing_obj["startedAt"]),
|
||||||
|
},
|
||||||
|
"timing_started_at": {
|
||||||
|
"snapshot_replay": base_timing_obj["startedAt"],
|
||||||
|
"delta_replay": delta_timing_obj["startedAt"],
|
||||||
|
},
|
||||||
|
"routinator_timings": {
|
||||||
|
"base_replay_seconds": float(durations["base-replay"]),
|
||||||
|
"delta_replay_seconds": float(durations["delta-replay"]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return case
|
||||||
|
|
||||||
|
|
||||||
|
def emit_env(case: dict) -> str:
|
||||||
|
ordered = {
|
||||||
|
"BUNDLE_ROOT": case["bundle_root"],
|
||||||
|
"RIR": case["rir"],
|
||||||
|
"TRUST_ANCHOR": case["trust_anchor"],
|
||||||
|
"RIR_ROOT": case["rir_root"],
|
||||||
|
"TAL_PATH": case["tal_path"],
|
||||||
|
"TA_PATH": case["ta_path"],
|
||||||
|
"PAYLOAD_REPLAY_ARCHIVE": case["base_archive"],
|
||||||
|
"PAYLOAD_REPLAY_LOCKS": case["base_locks"],
|
||||||
|
"ROUTINATOR_BASE_RECORD_CSV": case["base_vrps_csv"],
|
||||||
|
"PAYLOAD_BASE_ARCHIVE": case["base_archive"],
|
||||||
|
"PAYLOAD_BASE_LOCKS": case["base_locks"],
|
||||||
|
"PAYLOAD_DELTA_ARCHIVE": case["delta_archive"],
|
||||||
|
"PAYLOAD_DELTA_LOCKS": case["delta_locks"],
|
||||||
|
"ROUTINATOR_DELTA_RECORD_CSV": case["delta_record_csv"],
|
||||||
|
"SNAPSHOT_VALIDATION_TIME": case["validation_times"]["snapshot"],
|
||||||
|
"DELTA_VALIDATION_TIME": case["validation_times"]["delta"],
|
||||||
|
"ROUTINATOR_BASE_REPLAY_SECONDS": str(case["routinator_timings"]["base_replay_seconds"]),
|
||||||
|
"ROUTINATOR_DELTA_REPLAY_SECONDS": str(case["routinator_timings"]["delta_replay_seconds"]),
|
||||||
|
}
|
||||||
|
return "\n".join(
|
||||||
|
f"export {key}={shlex.quote(value)}" for key, value in ordered.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Resolve one RIR case inside a multi-RIR replay bundle")
|
||||||
|
parser.add_argument("--bundle-root", type=Path, default=None)
|
||||||
|
parser.add_argument("--repo-root", type=Path, default=None)
|
||||||
|
parser.add_argument("--rir", required=True, choices=sorted(RIR_CONFIG))
|
||||||
|
parser.add_argument("--format", choices=["json", "env"], default="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo_root = (args.repo_root or default_repo_root()).resolve()
|
||||||
|
bundle_root = (args.bundle_root or default_bundle_root(repo_root)).resolve()
|
||||||
|
case = build_case(bundle_root, repo_root, args.rir)
|
||||||
|
|
||||||
|
if args.format == "env":
|
||||||
|
print(emit_env(case))
|
||||||
|
else:
|
||||||
|
print(json.dumps(case, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
57
scripts/payload_replay/report_to_routinator_csv.py
Executable file
57
scripts/payload_replay/report_to_routinator_csv.py
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(
|
||||||
|
description="Convert rpki report.json VRPs into Routinator-compatible CSV"
|
||||||
|
)
|
||||||
|
p.add_argument("--report", required=True, help="path to rpki report.json")
|
||||||
|
p.add_argument("--out", required=True, help="output CSV path")
|
||||||
|
p.add_argument(
|
||||||
|
"--trust-anchor",
|
||||||
|
default="unknown",
|
||||||
|
help="Trust Anchor column value (default: unknown)",
|
||||||
|
)
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def sort_key(vrp: dict):
|
||||||
|
network = ipaddress.ip_network(vrp["prefix"], strict=False)
|
||||||
|
return (
|
||||||
|
int(vrp["asn"]),
|
||||||
|
network.version,
|
||||||
|
int(network.network_address),
|
||||||
|
network.prefixlen,
|
||||||
|
int(vrp["max_length"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
report = json.loads(Path(args.report).read_text(encoding="utf-8"))
|
||||||
|
vrps = list(report.get("vrps") or [])
|
||||||
|
vrps.sort(key=sort_key)
|
||||||
|
|
||||||
|
out_path = Path(args.out)
|
||||||
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with out_path.open("w", encoding="utf-8", newline="") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["ASN", "IP Prefix", "Max Length", "Trust Anchor"])
|
||||||
|
for vrp in vrps:
|
||||||
|
w.writerow([
|
||||||
|
f"AS{vrp['asn']}",
|
||||||
|
vrp["prefix"],
|
||||||
|
vrp["max_length"],
|
||||||
|
args.trust_anchor,
|
||||||
|
])
|
||||||
|
print(out_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
168
scripts/payload_replay/run_apnic_delta_replay.sh
Executable file
168
scripts/payload_replay/run_apnic_delta_replay.sh
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
DELTA_ROOT="${DELTA_ROOT:-$ROOT_DIR/target/live/apnic_delta_demo/20260315-170223-autoplay}"
|
||||||
|
TAL_PATH="${TAL_PATH:-$ROOT_DIR/tests/fixtures/tal/apnic-rfc7730-https.tal}"
|
||||||
|
TA_PATH="${TA_PATH:-$ROOT_DIR/tests/fixtures/ta/apnic-ta.cer}"
|
||||||
|
PAYLOAD_BASE_ARCHIVE="${PAYLOAD_BASE_ARCHIVE:-$DELTA_ROOT/base-payload-archive}"
|
||||||
|
PAYLOAD_BASE_LOCKS="${PAYLOAD_BASE_LOCKS:-$DELTA_ROOT/base-locks.json}"
|
||||||
|
PAYLOAD_DELTA_ARCHIVE="${PAYLOAD_DELTA_ARCHIVE:-$DELTA_ROOT/payload-delta-archive}"
|
||||||
|
PAYLOAD_DELTA_LOCKS="${PAYLOAD_DELTA_LOCKS:-$DELTA_ROOT/locks-delta.json}"
|
||||||
|
VALIDATION_TIME="${VALIDATION_TIME:-}"
|
||||||
|
PAYLOAD_BASE_VALIDATION_TIME="${PAYLOAD_BASE_VALIDATION_TIME:-}"
|
||||||
|
TRUST_ANCHOR="${TRUST_ANCHOR:-apnic}"
|
||||||
|
ROUTINATOR_RECORD_CSV="${ROUTINATOR_RECORD_CSV:-$DELTA_ROOT/record-delta.csv}"
|
||||||
|
MAX_DEPTH="${MAX_DEPTH:-}"
|
||||||
|
MAX_INSTANCES="${MAX_INSTANCES:-}"
|
||||||
|
OUT_DIR="${OUT_DIR:-$ROOT_DIR/target/live/payload_delta_replay_runs}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
if [[ -z "$PAYLOAD_BASE_VALIDATION_TIME" ]]; then
|
||||||
|
PAYLOAD_BASE_VALIDATION_TIME="$(python3 - "$PAYLOAD_BASE_LOCKS" <<'LOCKPY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
print(data.get('validationTime') or data.get('validation_time') or '')
|
||||||
|
LOCKPY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$VALIDATION_TIME" ]]; then
|
||||||
|
VALIDATION_TIME="$(python3 - "$PAYLOAD_DELTA_LOCKS" <<'LOCKPY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
print(data.get('validationTime') or data.get('validation_time') or '2026-03-15T10:00:00Z')
|
||||||
|
LOCKPY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
RUN_NAME="${RUN_NAME:-apnic_delta_replay_${TS}}"
|
||||||
|
DB_DIR="${DB_DIR:-$OUT_DIR/${RUN_NAME}_db}"
|
||||||
|
REPORT_JSON="${REPORT_JSON:-$OUT_DIR/${RUN_NAME}_report.json}"
|
||||||
|
RUN_LOG="${RUN_LOG:-$OUT_DIR/${RUN_NAME}_run.log}"
|
||||||
|
META_JSON="${META_JSON:-$OUT_DIR/${RUN_NAME}_meta.json}"
|
||||||
|
SUMMARY_MD="${SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_summary.md}"
|
||||||
|
VRPS_CSV="${VRPS_CSV:-$OUT_DIR/${RUN_NAME}_vrps.csv}"
|
||||||
|
COMPARE_SUMMARY_MD="${COMPARE_SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_compare_summary.md}"
|
||||||
|
ONLY_IN_OURS_CSV="${ONLY_IN_OURS_CSV:-$OUT_DIR/${RUN_NAME}_only_in_ours.csv}"
|
||||||
|
ONLY_IN_RECORD_CSV="${ONLY_IN_RECORD_CSV:-$OUT_DIR/${RUN_NAME}_only_in_record.csv}"
|
||||||
|
|
||||||
|
cmd=(cargo run --release --bin rpki --
|
||||||
|
--db "$DB_DIR"
|
||||||
|
--tal-path "$TAL_PATH"
|
||||||
|
--ta-path "$TA_PATH"
|
||||||
|
--payload-base-archive "$PAYLOAD_BASE_ARCHIVE"
|
||||||
|
--payload-base-locks "$PAYLOAD_BASE_LOCKS"
|
||||||
|
--payload-delta-archive "$PAYLOAD_DELTA_ARCHIVE"
|
||||||
|
--payload-delta-locks "$PAYLOAD_DELTA_LOCKS"
|
||||||
|
--validation-time "$VALIDATION_TIME"
|
||||||
|
--report-json "$REPORT_JSON")
|
||||||
|
|
||||||
|
if [[ -n "$MAX_DEPTH" ]]; then
|
||||||
|
cmd+=(--max-depth "$MAX_DEPTH")
|
||||||
|
fi
|
||||||
|
if [[ -n "$MAX_INSTANCES" ]]; then
|
||||||
|
cmd+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_start_s="$(date +%s)"
|
||||||
|
(
|
||||||
|
echo "# command:"
|
||||||
|
printf '%q ' "${cmd[@]}"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
"${cmd[@]}"
|
||||||
|
) 2>&1 | tee "$RUN_LOG" >/dev/null
|
||||||
|
run_end_s="$(date +%s)"
|
||||||
|
run_duration_s="$((run_end_s - run_start_s))"
|
||||||
|
|
||||||
|
PAYLOAD_BASE_ARCHIVE="$PAYLOAD_BASE_ARCHIVE" \
|
||||||
|
PAYLOAD_BASE_LOCKS="$PAYLOAD_BASE_LOCKS" \
|
||||||
|
PAYLOAD_DELTA_ARCHIVE="$PAYLOAD_DELTA_ARCHIVE" \
|
||||||
|
PAYLOAD_DELTA_LOCKS="$PAYLOAD_DELTA_LOCKS" \
|
||||||
|
PAYLOAD_BASE_VALIDATION_TIME="$PAYLOAD_BASE_VALIDATION_TIME" \
|
||||||
|
DB_DIR="$DB_DIR" \
|
||||||
|
REPORT_JSON="$REPORT_JSON" \
|
||||||
|
RUN_LOG="$RUN_LOG" \
|
||||||
|
VALIDATION_TIME="$VALIDATION_TIME" \
|
||||||
|
RUN_DURATION_S="$run_duration_s" \
|
||||||
|
python3 - "$REPORT_JSON" "$META_JSON" "$SUMMARY_MD" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
report_path = Path(sys.argv[1])
|
||||||
|
meta_path = Path(sys.argv[2])
|
||||||
|
summary_path = Path(sys.argv[3])
|
||||||
|
rep = json.loads(report_path.read_text(encoding='utf-8'))
|
||||||
|
now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
meta = {
|
||||||
|
'recorded_at_utc': now,
|
||||||
|
'payload_base_archive': os.environ['PAYLOAD_BASE_ARCHIVE'],
|
||||||
|
'payload_base_locks': os.environ['PAYLOAD_BASE_LOCKS'],
|
||||||
|
'payload_delta_archive': os.environ['PAYLOAD_DELTA_ARCHIVE'],
|
||||||
|
'payload_delta_locks': os.environ['PAYLOAD_DELTA_LOCKS'],
|
||||||
|
'db_dir': os.environ['DB_DIR'],
|
||||||
|
'report_json': os.environ['REPORT_JSON'],
|
||||||
|
'run_log': os.environ['RUN_LOG'],
|
||||||
|
'validation_time_arg': os.environ['VALIDATION_TIME'],
|
||||||
|
'base_validation_time_arg': os.environ.get('PAYLOAD_BASE_VALIDATION_TIME') or os.environ['VALIDATION_TIME'],
|
||||||
|
'durations_secs': {'rpki_run': int(os.environ['RUN_DURATION_S'])},
|
||||||
|
'counts': {
|
||||||
|
'publication_points_processed': rep['tree']['instances_processed'],
|
||||||
|
'publication_points_failed': rep['tree']['instances_failed'],
|
||||||
|
'vrps': len(rep['vrps']),
|
||||||
|
'aspas': len(rep['aspas']),
|
||||||
|
'audit_publication_points': len(rep['publication_points']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
||||||
|
summary = []
|
||||||
|
summary.append('# Payload Delta Replay Summary\n\n')
|
||||||
|
for key in ['payload_base_archive','payload_base_locks','payload_delta_archive','payload_delta_locks','db_dir','report_json','base_validation_time_arg','validation_time_arg']:
|
||||||
|
summary.append(f'- {key}: `{meta[key]}`\n')
|
||||||
|
summary.append('\n## Results\n\n| metric | value |\n|---|---:|\n')
|
||||||
|
for k,v in meta['counts'].items():
|
||||||
|
summary.append(f'| {k} | {v} |\n')
|
||||||
|
summary.append('\n## Durations\n\n| step | seconds |\n|---|---:|\n')
|
||||||
|
for k,v in meta['durations_secs'].items():
|
||||||
|
summary.append(f'| {k} | {v} |\n')
|
||||||
|
summary_path.write_text(''.join(summary), encoding='utf-8')
|
||||||
|
print(summary_path)
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 scripts/payload_replay/report_to_routinator_csv.py \
|
||||||
|
--report "$REPORT_JSON" \
|
||||||
|
--out "$VRPS_CSV" \
|
||||||
|
--trust-anchor "$TRUST_ANCHOR" >/dev/null
|
||||||
|
|
||||||
|
if [[ -f "$ROUTINATOR_RECORD_CSV" ]]; then
|
||||||
|
./scripts/payload_replay/compare_with_routinator_record.sh \
|
||||||
|
"$VRPS_CSV" \
|
||||||
|
"$ROUTINATOR_RECORD_CSV" \
|
||||||
|
"$COMPARE_SUMMARY_MD" \
|
||||||
|
"$ONLY_IN_OURS_CSV" \
|
||||||
|
"$ONLY_IN_RECORD_CSV" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== payload delta replay run complete ==" >&2
|
||||||
|
echo "- db: $DB_DIR" >&2
|
||||||
|
echo "- report: $REPORT_JSON" >&2
|
||||||
|
echo "- run log: $RUN_LOG" >&2
|
||||||
|
echo "- meta json: $META_JSON" >&2
|
||||||
|
echo "- summary md: $SUMMARY_MD" >&2
|
||||||
|
echo "- vrps csv: $VRPS_CSV" >&2
|
||||||
|
if [[ -f "$COMPARE_SUMMARY_MD" ]]; then
|
||||||
|
echo "- compare summary: $COMPARE_SUMMARY_MD" >&2
|
||||||
|
echo "- only in ours: $ONLY_IN_OURS_CSV" >&2
|
||||||
|
echo "- only in record: $ONLY_IN_RECORD_CSV" >&2
|
||||||
|
fi
|
||||||
161
scripts/payload_replay/run_apnic_replay.sh
Executable file
161
scripts/payload_replay/run_apnic_replay.sh
Executable file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
TAL_PATH="${TAL_PATH:-$ROOT_DIR/tests/fixtures/tal/apnic-rfc7730-https.tal}"
|
||||||
|
TA_PATH="${TA_PATH:-$ROOT_DIR/tests/fixtures/ta/apnic-ta.cer}"
|
||||||
|
PAYLOAD_REPLAY_ARCHIVE="${PAYLOAD_REPLAY_ARCHIVE:-$ROOT_DIR/target/live/payload_replay/payload-archive}"
|
||||||
|
PAYLOAD_REPLAY_LOCKS="${PAYLOAD_REPLAY_LOCKS:-$ROOT_DIR/target/live/payload_replay/locks.json}"
|
||||||
|
VALIDATION_TIME="${VALIDATION_TIME:-}"
|
||||||
|
TRUST_ANCHOR="${TRUST_ANCHOR:-apnic}"
|
||||||
|
ROUTINATOR_RECORD_CSV="${ROUTINATOR_RECORD_CSV:-$ROOT_DIR/target/live/payload_replay/record.csv}"
|
||||||
|
MAX_DEPTH="${MAX_DEPTH:-}"
|
||||||
|
MAX_INSTANCES="${MAX_INSTANCES:-}"
|
||||||
|
OUT_DIR="${OUT_DIR:-$ROOT_DIR/target/live/payload_replay_runs}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
if [[ -z "$VALIDATION_TIME" ]]; then
|
||||||
|
VALIDATION_TIME="$(python3 - "$PAYLOAD_REPLAY_LOCKS" <<'LOCKPY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
print(data.get('validationTime') or data.get('validation_time') or '2026-03-13T02:30:00Z')
|
||||||
|
LOCKPY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
RUN_NAME="${RUN_NAME:-apnic_replay_${TS}}"
|
||||||
|
DB_DIR="${DB_DIR:-$OUT_DIR/${RUN_NAME}_db}"
|
||||||
|
REPORT_JSON="${REPORT_JSON:-$OUT_DIR/${RUN_NAME}_report.json}"
|
||||||
|
RUN_LOG="${RUN_LOG:-$OUT_DIR/${RUN_NAME}_run.log}"
|
||||||
|
META_JSON="${META_JSON:-$OUT_DIR/${RUN_NAME}_meta.json}"
|
||||||
|
SUMMARY_MD="${SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_summary.md}"
|
||||||
|
VRPS_CSV="${VRPS_CSV:-$OUT_DIR/${RUN_NAME}_vrps.csv}"
|
||||||
|
COMPARE_SUMMARY_MD="${COMPARE_SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_compare_summary.md}"
|
||||||
|
ONLY_IN_OURS_CSV="${ONLY_IN_OURS_CSV:-$OUT_DIR/${RUN_NAME}_only_in_ours.csv}"
|
||||||
|
ONLY_IN_RECORD_CSV="${ONLY_IN_RECORD_CSV:-$OUT_DIR/${RUN_NAME}_only_in_record.csv}"
|
||||||
|
|
||||||
|
cmd=(cargo run --release --bin rpki --
|
||||||
|
--db "$DB_DIR"
|
||||||
|
--tal-path "$TAL_PATH"
|
||||||
|
--ta-path "$TA_PATH"
|
||||||
|
--payload-replay-archive "$PAYLOAD_REPLAY_ARCHIVE"
|
||||||
|
--payload-replay-locks "$PAYLOAD_REPLAY_LOCKS"
|
||||||
|
--validation-time "$VALIDATION_TIME"
|
||||||
|
--report-json "$REPORT_JSON")
|
||||||
|
|
||||||
|
if [[ -n "$MAX_DEPTH" ]]; then
|
||||||
|
cmd+=(--max-depth "$MAX_DEPTH")
|
||||||
|
fi
|
||||||
|
if [[ -n "$MAX_INSTANCES" ]]; then
|
||||||
|
cmd+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_start_s="$(date +%s)"
|
||||||
|
(
|
||||||
|
echo "# command:"
|
||||||
|
printf '%q ' "${cmd[@]}"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
"${cmd[@]}"
|
||||||
|
) 2>&1 | tee "$RUN_LOG" >/dev/null
|
||||||
|
run_end_s="$(date +%s)"
|
||||||
|
run_duration_s="$((run_end_s - run_start_s))"
|
||||||
|
|
||||||
|
TAL_PATH="$TAL_PATH" \
|
||||||
|
TA_PATH="$TA_PATH" \
|
||||||
|
PAYLOAD_REPLAY_ARCHIVE="$PAYLOAD_REPLAY_ARCHIVE" \
|
||||||
|
PAYLOAD_REPLAY_LOCKS="$PAYLOAD_REPLAY_LOCKS" \
|
||||||
|
DB_DIR="$DB_DIR" \
|
||||||
|
REPORT_JSON="$REPORT_JSON" \
|
||||||
|
RUN_LOG="$RUN_LOG" \
|
||||||
|
VALIDATION_TIME="$VALIDATION_TIME" \
|
||||||
|
RUN_DURATION_S="$run_duration_s" \
|
||||||
|
python3 - "$REPORT_JSON" "$META_JSON" "$SUMMARY_MD" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
report_path = Path(sys.argv[1])
|
||||||
|
meta_path = Path(sys.argv[2])
|
||||||
|
summary_path = Path(sys.argv[3])
|
||||||
|
rep = json.loads(report_path.read_text(encoding="utf-8"))
|
||||||
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
meta = {
|
||||||
|
"recorded_at_utc": now,
|
||||||
|
"tal_path": os.environ["TAL_PATH"],
|
||||||
|
"ta_path": os.environ["TA_PATH"],
|
||||||
|
"payload_replay_archive": os.environ["PAYLOAD_REPLAY_ARCHIVE"],
|
||||||
|
"payload_replay_locks": os.environ["PAYLOAD_REPLAY_LOCKS"],
|
||||||
|
"db_dir": os.environ["DB_DIR"],
|
||||||
|
"report_json": os.environ["REPORT_JSON"],
|
||||||
|
"run_log": os.environ["RUN_LOG"],
|
||||||
|
"validation_time_arg": os.environ["VALIDATION_TIME"],
|
||||||
|
"durations_secs": {
|
||||||
|
"rpki_run": int(os.environ["RUN_DURATION_S"]),
|
||||||
|
},
|
||||||
|
"counts": {
|
||||||
|
"publication_points_processed": rep["tree"]["instances_processed"],
|
||||||
|
"publication_points_failed": rep["tree"]["instances_failed"],
|
||||||
|
"vrps": len(rep["vrps"]),
|
||||||
|
"aspas": len(rep["aspas"]),
|
||||||
|
"audit_publication_points": len(rep["publication_points"]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
summary = []
|
||||||
|
summary.append("# Payload Replay Summary\n\n")
|
||||||
|
summary.append(f"- recorded_at_utc: `{now}`\n")
|
||||||
|
summary.append(f"- tal_path: `{meta['tal_path']}`\n")
|
||||||
|
summary.append(f"- ta_path: `{meta['ta_path']}`\n")
|
||||||
|
summary.append(f"- payload_replay_archive: `{meta['payload_replay_archive']}`\n")
|
||||||
|
summary.append(f"- payload_replay_locks: `{meta['payload_replay_locks']}`\n")
|
||||||
|
summary.append(f"- db: `{meta['db_dir']}`\n")
|
||||||
|
summary.append(f"- report_json: `{meta['report_json']}`\n")
|
||||||
|
summary.append(f"- validation_time_arg: `{meta['validation_time_arg']}`\n\n")
|
||||||
|
summary.append("## Results\n\n")
|
||||||
|
summary.append("| metric | value |\n")
|
||||||
|
summary.append("|---|---:|\n")
|
||||||
|
for k, v in meta["counts"].items():
|
||||||
|
summary.append(f"| {k} | {v} |\n")
|
||||||
|
summary.append("\n## Durations\n\n")
|
||||||
|
summary.append("| step | seconds |\n")
|
||||||
|
summary.append("|---|---:|\n")
|
||||||
|
for k, v in meta["durations_secs"].items():
|
||||||
|
summary.append(f"| {k} | {v} |\n")
|
||||||
|
summary_path.write_text("".join(summary), encoding="utf-8")
|
||||||
|
print(summary_path)
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 scripts/payload_replay/report_to_routinator_csv.py \
|
||||||
|
--report "$REPORT_JSON" \
|
||||||
|
--out "$VRPS_CSV" \
|
||||||
|
--trust-anchor "$TRUST_ANCHOR" >/dev/null
|
||||||
|
|
||||||
|
if [[ -f "$ROUTINATOR_RECORD_CSV" ]]; then
|
||||||
|
./scripts/payload_replay/compare_with_routinator_record.sh \
|
||||||
|
"$VRPS_CSV" \
|
||||||
|
"$ROUTINATOR_RECORD_CSV" \
|
||||||
|
"$COMPARE_SUMMARY_MD" \
|
||||||
|
"$ONLY_IN_OURS_CSV" \
|
||||||
|
"$ONLY_IN_RECORD_CSV" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== payload replay run complete ==" >&2
|
||||||
|
echo "- db: $DB_DIR" >&2
|
||||||
|
echo "- report: $REPORT_JSON" >&2
|
||||||
|
echo "- run log: $RUN_LOG" >&2
|
||||||
|
echo "- meta json: $META_JSON" >&2
|
||||||
|
echo "- summary md: $SUMMARY_MD" >&2
|
||||||
|
echo "- vrps csv: $VRPS_CSV" >&2
|
||||||
|
if [[ -f "$COMPARE_SUMMARY_MD" ]]; then
|
||||||
|
echo "- compare summary: $COMPARE_SUMMARY_MD" >&2
|
||||||
|
echo "- only in ours: $ONLY_IN_OURS_CSV" >&2
|
||||||
|
echo "- only in record: $ONLY_IN_RECORD_CSV" >&2
|
||||||
|
fi
|
||||||
168
scripts/payload_replay/run_apnic_snapshot_replay_profile.sh
Executable file
168
scripts/payload_replay/run_apnic_snapshot_replay_profile.sh
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
BUNDLE_ROOT="${BUNDLE_ROOT:-$ROOT_DIR/../../rpki/target/live/20260316-112341-multi-final3}"
|
||||||
|
CASE_INFO_SCRIPT="$ROOT_DIR/scripts/payload_replay/multi_rir_case_info.py"
|
||||||
|
PROFILE_RUN_ROOT="${PROFILE_RUN_ROOT:-$ROOT_DIR/target/live/analyze_runs}"
|
||||||
|
mkdir -p "$PROFILE_RUN_ROOT"
|
||||||
|
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
RUN_NAME="${RUN_NAME:-apnic_snapshot_profile_${TS}}"
|
||||||
|
RUN_DIR="$PROFILE_RUN_ROOT/$RUN_NAME"
|
||||||
|
mkdir -p "$RUN_DIR"
|
||||||
|
|
||||||
|
ANALYZE_ROOT="$ROOT_DIR/target/live/analyze"
|
||||||
|
mkdir -p "$ANALYZE_ROOT"
|
||||||
|
mapfile -t ANALYZE_BEFORE < <(find "$ANALYZE_ROOT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
|
||||||
|
|
||||||
|
eval "$(python3 "$CASE_INFO_SCRIPT" --bundle-root "$BUNDLE_ROOT" --rir apnic --format env)"
|
||||||
|
|
||||||
|
DB_DIR="${DB_DIR:-$RUN_DIR/db}"
|
||||||
|
REPORT_JSON="${REPORT_JSON:-$RUN_DIR/report.json}"
|
||||||
|
RUN_LOG="${RUN_LOG:-$RUN_DIR/run.log}"
|
||||||
|
META_JSON="${META_JSON:-$RUN_DIR/meta.json}"
|
||||||
|
SUMMARY_MD="${SUMMARY_MD:-$RUN_DIR/summary.md}"
|
||||||
|
|
||||||
|
rm -rf "$DB_DIR"
|
||||||
|
|
||||||
|
cmd=(cargo run --release --features profile --bin rpki --
|
||||||
|
--db "$DB_DIR"
|
||||||
|
--tal-path "$TAL_PATH"
|
||||||
|
--ta-path "$TA_PATH"
|
||||||
|
--payload-replay-archive "$PAYLOAD_REPLAY_ARCHIVE"
|
||||||
|
--payload-replay-locks "$PAYLOAD_REPLAY_LOCKS"
|
||||||
|
--validation-time "$SNAPSHOT_VALIDATION_TIME"
|
||||||
|
--analyze
|
||||||
|
--profile-cpu
|
||||||
|
--report-json "$REPORT_JSON")
|
||||||
|
|
||||||
|
if [[ -n "${MAX_DEPTH:-}" ]]; then
|
||||||
|
cmd+=(--max-depth "$MAX_DEPTH")
|
||||||
|
fi
|
||||||
|
if [[ -n "${MAX_INSTANCES:-}" ]]; then
|
||||||
|
cmd+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
printf '%q ' "${cmd[@]}"
|
||||||
|
echo
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_start_s="$(date +%s)"
|
||||||
|
(
|
||||||
|
echo '# command:'
|
||||||
|
printf '%q ' "${cmd[@]}"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
"${cmd[@]}"
|
||||||
|
) 2>&1 | tee "$RUN_LOG" >/dev/null
|
||||||
|
run_end_s="$(date +%s)"
|
||||||
|
run_duration_s="$((run_end_s - run_start_s))"
|
||||||
|
|
||||||
|
mapfile -t ANALYZE_AFTER < <(find "$ANALYZE_ROOT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
|
||||||
|
ANALYZE_DIR=""
|
||||||
|
for candidate in "${ANALYZE_AFTER[@]}"; do
|
||||||
|
seen=0
|
||||||
|
for old in "${ANALYZE_BEFORE[@]}"; do
|
||||||
|
if [[ "$candidate" == "$old" ]]; then
|
||||||
|
seen=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$seen" == "0" ]]; then
|
||||||
|
ANALYZE_DIR="$candidate"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z "$ANALYZE_DIR" ]]; then
|
||||||
|
ANALYZE_DIR="$(find "$ANALYZE_ROOT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -n 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUNDLE_ROOT="$BUNDLE_ROOT" \
|
||||||
|
TRUST_ANCHOR="$TRUST_ANCHOR" \
|
||||||
|
TAL_PATH="$TAL_PATH" \
|
||||||
|
TA_PATH="$TA_PATH" \
|
||||||
|
PAYLOAD_REPLAY_ARCHIVE="$PAYLOAD_REPLAY_ARCHIVE" \
|
||||||
|
PAYLOAD_REPLAY_LOCKS="$PAYLOAD_REPLAY_LOCKS" \
|
||||||
|
SNAPSHOT_VALIDATION_TIME="$SNAPSHOT_VALIDATION_TIME" \
|
||||||
|
ROUTINATOR_BASE_REPLAY_SECONDS="$ROUTINATOR_BASE_REPLAY_SECONDS" \
|
||||||
|
DB_DIR="$DB_DIR" \
|
||||||
|
REPORT_JSON="$REPORT_JSON" \
|
||||||
|
RUN_LOG="$RUN_LOG" \
|
||||||
|
ANALYZE_DIR="$ANALYZE_DIR" \
|
||||||
|
RUN_DURATION_S="$run_duration_s" \
|
||||||
|
python3 - "$META_JSON" "$SUMMARY_MD" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
meta_path = Path(sys.argv[1])
|
||||||
|
summary_path = Path(sys.argv[2])
|
||||||
|
report_path = Path(os.environ['REPORT_JSON'])
|
||||||
|
report = json.loads(report_path.read_text(encoding='utf-8'))
|
||||||
|
recorded = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
meta = {
|
||||||
|
'recorded_at_utc': recorded,
|
||||||
|
'bundle_root': os.environ['BUNDLE_ROOT'],
|
||||||
|
'trust_anchor': os.environ['TRUST_ANCHOR'],
|
||||||
|
'tal_path': os.environ['TAL_PATH'],
|
||||||
|
'ta_path': os.environ['TA_PATH'],
|
||||||
|
'payload_replay_archive': os.environ['PAYLOAD_REPLAY_ARCHIVE'],
|
||||||
|
'payload_replay_locks': os.environ['PAYLOAD_REPLAY_LOCKS'],
|
||||||
|
'validation_time_arg': os.environ['SNAPSHOT_VALIDATION_TIME'],
|
||||||
|
'routinator_base_replay_seconds': float(os.environ['ROUTINATOR_BASE_REPLAY_SECONDS']),
|
||||||
|
'db_dir': os.environ['DB_DIR'],
|
||||||
|
'report_json': os.environ['REPORT_JSON'],
|
||||||
|
'run_log': os.environ['RUN_LOG'],
|
||||||
|
'analyze_dir': os.environ.get('ANALYZE_DIR') or '',
|
||||||
|
'durations_secs': {
|
||||||
|
'rpki_run_wall': int(os.environ['RUN_DURATION_S']),
|
||||||
|
},
|
||||||
|
'counts': {
|
||||||
|
'publication_points_processed': report['tree']['instances_processed'],
|
||||||
|
'publication_points_failed': report['tree']['instances_failed'],
|
||||||
|
'vrps': len(report['vrps']),
|
||||||
|
'aspas': len(report['aspas']),
|
||||||
|
'audit_publication_points': len(report['publication_points']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
||||||
|
ratio = meta['durations_secs']['rpki_run_wall'] / meta['routinator_base_replay_seconds'] if meta['routinator_base_replay_seconds'] else None
|
||||||
|
lines = []
|
||||||
|
lines.append('# APNIC Snapshot Replay Profile Summary\n\n')
|
||||||
|
lines.append(f"- recorded_at_utc: `{recorded}`\n")
|
||||||
|
lines.append(f"- bundle_root: `{meta['bundle_root']}`\n")
|
||||||
|
lines.append(f"- tal_path: `{meta['tal_path']}`\n")
|
||||||
|
lines.append(f"- ta_path: `{meta['ta_path']}`\n")
|
||||||
|
lines.append(f"- payload_replay_archive: `{meta['payload_replay_archive']}`\n")
|
||||||
|
lines.append(f"- payload_replay_locks: `{meta['payload_replay_locks']}`\n")
|
||||||
|
lines.append(f"- validation_time_arg: `{meta['validation_time_arg']}`\n")
|
||||||
|
lines.append(f"- db_dir: `{meta['db_dir']}`\n")
|
||||||
|
lines.append(f"- report_json: `{meta['report_json']}`\n")
|
||||||
|
lines.append(f"- run_log: `{meta['run_log']}`\n")
|
||||||
|
lines.append(f"- analyze_dir: `{meta['analyze_dir']}`\n\n")
|
||||||
|
lines.append('## Timing\n\n')
|
||||||
|
lines.append('| metric | value |\n')
|
||||||
|
lines.append('|---|---:|\n')
|
||||||
|
lines.append(f"| ours_snapshot_replay_wall_s | {meta['durations_secs']['rpki_run_wall']} |\n")
|
||||||
|
lines.append(f"| routinator_base_replay_s | {meta['routinator_base_replay_seconds']:.3f} |\n")
|
||||||
|
if ratio is not None:
|
||||||
|
lines.append(f"| ratio_ours_over_routinator | {ratio:.3f} |\n")
|
||||||
|
lines.append('\n## Counts\n\n')
|
||||||
|
for key, value in meta['counts'].items():
|
||||||
|
lines.append(f"- {key}: `{value}`\n")
|
||||||
|
summary_path.write_text(''.join(lines), encoding='utf-8')
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "== APNIC snapshot replay profiling complete ==" >&2
|
||||||
|
echo "- run_dir: $RUN_DIR" >&2
|
||||||
|
echo "- analyze_dir: $ANALYZE_DIR" >&2
|
||||||
|
echo "- report_json: $REPORT_JSON" >&2
|
||||||
|
echo "- run_log: $RUN_LOG" >&2
|
||||||
|
echo "- meta_json: $META_JSON" >&2
|
||||||
|
echo "- summary_md: $SUMMARY_MD" >&2
|
||||||
128
scripts/payload_replay/run_multi_rir_replay_case.sh
Executable file
128
scripts/payload_replay/run_multi_rir_replay_case.sh
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||||
|
echo "usage: $0 <rir> [describe|snapshot|delta|both]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
RIR="$1"
|
||||||
|
MODE="${2:-both}"
|
||||||
|
BUNDLE_ROOT="${BUNDLE_ROOT:-$ROOT_DIR/../../rpki/target/live/20260316-112341-multi-final3}"
|
||||||
|
CASE_INFO_SCRIPT="$ROOT_DIR/scripts/payload_replay/multi_rir_case_info.py"
|
||||||
|
CASE_REPORT_SCRIPT="$ROOT_DIR/scripts/payload_replay/write_multi_rir_case_report.py"
|
||||||
|
MULTI_RIR_OUT_DIR="${MULTI_RIR_OUT_DIR:-$ROOT_DIR/target/live/multi_rir_replay_runs/$RIR}"
|
||||||
|
mkdir -p "$MULTI_RIR_OUT_DIR"
|
||||||
|
|
||||||
|
eval "$(python3 "$CASE_INFO_SCRIPT" --bundle-root "$BUNDLE_ROOT" --rir "$RIR" --format env)"
|
||||||
|
|
||||||
|
SNAPSHOT_DB_DIR="${SNAPSHOT_DB_DIR:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_replay_db}"
|
||||||
|
SNAPSHOT_REPORT_MD="${SNAPSHOT_REPORT_MD:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_compare_summary.md}"
|
||||||
|
SNAPSHOT_META_JSON="${SNAPSHOT_META_JSON:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_meta.json}"
|
||||||
|
SNAPSHOT_RUN_LOG="${SNAPSHOT_RUN_LOG:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_run.log}"
|
||||||
|
SNAPSHOT_REPORT_JSON="${SNAPSHOT_REPORT_JSON:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_report.json}"
|
||||||
|
SNAPSHOT_VRPS_CSV="${SNAPSHOT_VRPS_CSV:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_vrps.csv}"
|
||||||
|
SNAPSHOT_ONLY_OURS="${SNAPSHOT_ONLY_OURS:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_only_in_ours.csv}"
|
||||||
|
SNAPSHOT_ONLY_RECORD="${SNAPSHOT_ONLY_RECORD:-$MULTI_RIR_OUT_DIR/${RIR}_snapshot_only_in_record.csv}"
|
||||||
|
|
||||||
|
DELTA_DB_DIR="${DELTA_DB_DIR:-$MULTI_RIR_OUT_DIR/${RIR}_delta_replay_db}"
|
||||||
|
DELTA_REPORT_MD="${DELTA_REPORT_MD:-$MULTI_RIR_OUT_DIR/${RIR}_delta_compare_summary.md}"
|
||||||
|
DELTA_META_JSON="${DELTA_META_JSON:-$MULTI_RIR_OUT_DIR/${RIR}_delta_meta.json}"
|
||||||
|
DELTA_RUN_LOG="${DELTA_RUN_LOG:-$MULTI_RIR_OUT_DIR/${RIR}_delta_run.log}"
|
||||||
|
DELTA_REPORT_JSON="${DELTA_REPORT_JSON:-$MULTI_RIR_OUT_DIR/${RIR}_delta_report.json}"
|
||||||
|
DELTA_VRPS_CSV="${DELTA_VRPS_CSV:-$MULTI_RIR_OUT_DIR/${RIR}_delta_vrps.csv}"
|
||||||
|
DELTA_ONLY_OURS="${DELTA_ONLY_OURS:-$MULTI_RIR_OUT_DIR/${RIR}_delta_only_in_ours.csv}"
|
||||||
|
DELTA_ONLY_RECORD="${DELTA_ONLY_RECORD:-$MULTI_RIR_OUT_DIR/${RIR}_delta_only_in_record.csv}"
|
||||||
|
|
||||||
|
CASE_REPORT_JSON="${CASE_REPORT_JSON:-$MULTI_RIR_OUT_DIR/${RIR}_case_report.json}"
|
||||||
|
CASE_REPORT_MD="${CASE_REPORT_MD:-$MULTI_RIR_OUT_DIR/${RIR}_case_report.md}"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
describe)
|
||||||
|
python3 "$CASE_INFO_SCRIPT" --bundle-root "$BUNDLE_ROOT" --rir "$RIR"
|
||||||
|
;;
|
||||||
|
snapshot)
|
||||||
|
rm -rf "$SNAPSHOT_DB_DIR"
|
||||||
|
ROUTINATOR_RECORD_CSV="$ROUTINATOR_BASE_RECORD_CSV" \
|
||||||
|
VALIDATION_TIME="$SNAPSHOT_VALIDATION_TIME" \
|
||||||
|
OUT_DIR="$MULTI_RIR_OUT_DIR" \
|
||||||
|
DB_DIR="$SNAPSHOT_DB_DIR" \
|
||||||
|
RUN_NAME="${RUN_NAME:-${RIR}_snapshot_replay}" \
|
||||||
|
META_JSON="$SNAPSHOT_META_JSON" \
|
||||||
|
RUN_LOG="$SNAPSHOT_RUN_LOG" \
|
||||||
|
REPORT_JSON="$SNAPSHOT_REPORT_JSON" \
|
||||||
|
VRPS_CSV="$SNAPSHOT_VRPS_CSV" \
|
||||||
|
COMPARE_SUMMARY_MD="$SNAPSHOT_REPORT_MD" \
|
||||||
|
ONLY_IN_OURS_CSV="$SNAPSHOT_ONLY_OURS" \
|
||||||
|
ONLY_IN_RECORD_CSV="$SNAPSHOT_ONLY_RECORD" \
|
||||||
|
./scripts/payload_replay/run_apnic_replay.sh
|
||||||
|
;;
|
||||||
|
delta)
|
||||||
|
rm -rf "$DELTA_DB_DIR"
|
||||||
|
ROUTINATOR_RECORD_CSV="$ROUTINATOR_DELTA_RECORD_CSV" \
|
||||||
|
VALIDATION_TIME="$DELTA_VALIDATION_TIME" \
|
||||||
|
PAYLOAD_BASE_VALIDATION_TIME="$SNAPSHOT_VALIDATION_TIME" \
|
||||||
|
OUT_DIR="$MULTI_RIR_OUT_DIR" \
|
||||||
|
DB_DIR="$DELTA_DB_DIR" \
|
||||||
|
RUN_NAME="${RUN_NAME:-${RIR}_delta_replay}" \
|
||||||
|
DELTA_ROOT="$RIR_ROOT" \
|
||||||
|
META_JSON="$DELTA_META_JSON" \
|
||||||
|
RUN_LOG="$DELTA_RUN_LOG" \
|
||||||
|
REPORT_JSON="$DELTA_REPORT_JSON" \
|
||||||
|
VRPS_CSV="$DELTA_VRPS_CSV" \
|
||||||
|
COMPARE_SUMMARY_MD="$DELTA_REPORT_MD" \
|
||||||
|
ONLY_IN_OURS_CSV="$DELTA_ONLY_OURS" \
|
||||||
|
ONLY_IN_RECORD_CSV="$DELTA_ONLY_RECORD" \
|
||||||
|
./scripts/payload_replay/run_apnic_delta_replay.sh
|
||||||
|
;;
|
||||||
|
both)
|
||||||
|
rm -rf "$SNAPSHOT_DB_DIR" "$DELTA_DB_DIR"
|
||||||
|
ROUTINATOR_RECORD_CSV="$ROUTINATOR_BASE_RECORD_CSV" \
|
||||||
|
VALIDATION_TIME="$SNAPSHOT_VALIDATION_TIME" \
|
||||||
|
OUT_DIR="$MULTI_RIR_OUT_DIR" \
|
||||||
|
DB_DIR="$SNAPSHOT_DB_DIR" \
|
||||||
|
RUN_NAME="${RUN_NAME_SNAPSHOT:-${RIR}_snapshot_replay}" \
|
||||||
|
META_JSON="$SNAPSHOT_META_JSON" \
|
||||||
|
RUN_LOG="$SNAPSHOT_RUN_LOG" \
|
||||||
|
REPORT_JSON="$SNAPSHOT_REPORT_JSON" \
|
||||||
|
VRPS_CSV="$SNAPSHOT_VRPS_CSV" \
|
||||||
|
COMPARE_SUMMARY_MD="$SNAPSHOT_REPORT_MD" \
|
||||||
|
ONLY_IN_OURS_CSV="$SNAPSHOT_ONLY_OURS" \
|
||||||
|
ONLY_IN_RECORD_CSV="$SNAPSHOT_ONLY_RECORD" \
|
||||||
|
./scripts/payload_replay/run_apnic_replay.sh
|
||||||
|
ROUTINATOR_RECORD_CSV="$ROUTINATOR_DELTA_RECORD_CSV" \
|
||||||
|
VALIDATION_TIME="$DELTA_VALIDATION_TIME" \
|
||||||
|
PAYLOAD_BASE_VALIDATION_TIME="$SNAPSHOT_VALIDATION_TIME" \
|
||||||
|
OUT_DIR="$MULTI_RIR_OUT_DIR" \
|
||||||
|
DB_DIR="$DELTA_DB_DIR" \
|
||||||
|
RUN_NAME="${RUN_NAME_DELTA:-${RIR}_delta_replay}" \
|
||||||
|
DELTA_ROOT="$RIR_ROOT" \
|
||||||
|
META_JSON="$DELTA_META_JSON" \
|
||||||
|
RUN_LOG="$DELTA_RUN_LOG" \
|
||||||
|
REPORT_JSON="$DELTA_REPORT_JSON" \
|
||||||
|
VRPS_CSV="$DELTA_VRPS_CSV" \
|
||||||
|
COMPARE_SUMMARY_MD="$DELTA_REPORT_MD" \
|
||||||
|
ONLY_IN_OURS_CSV="$DELTA_ONLY_OURS" \
|
||||||
|
ONLY_IN_RECORD_CSV="$DELTA_ONLY_RECORD" \
|
||||||
|
./scripts/payload_replay/run_apnic_delta_replay.sh
|
||||||
|
python3 "$CASE_REPORT_SCRIPT" \
|
||||||
|
--rir "$RIR" \
|
||||||
|
--snapshot-meta "$SNAPSHOT_META_JSON" \
|
||||||
|
--snapshot-compare "$SNAPSHOT_REPORT_MD" \
|
||||||
|
--delta-meta "$DELTA_META_JSON" \
|
||||||
|
--delta-compare "$DELTA_REPORT_MD" \
|
||||||
|
--routinator-base-seconds "$ROUTINATOR_BASE_REPLAY_SECONDS" \
|
||||||
|
--routinator-delta-seconds "$ROUTINATOR_DELTA_REPLAY_SECONDS" \
|
||||||
|
--out-md "$CASE_REPORT_MD" \
|
||||||
|
--out-json "$CASE_REPORT_JSON" >/dev/null
|
||||||
|
echo "- case report: $CASE_REPORT_MD" >&2
|
||||||
|
echo "- case report json: $CASE_REPORT_JSON" >&2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unsupported mode: $MODE; expected describe|snapshot|delta|both" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
32
scripts/payload_replay/run_multi_rir_replay_suite.sh
Executable file
32
scripts/payload_replay/run_multi_rir_replay_suite.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
BUNDLE_ROOT="${BUNDLE_ROOT:-$ROOT_DIR/../../rpki/target/live/20260316-112341-multi-final3}"
|
||||||
|
SUITE_OUT_DIR="${SUITE_OUT_DIR:-$ROOT_DIR/target/live/multi_rir_replay_runs}"
|
||||||
|
RIRS="${RIRS:-afrinic apnic arin lacnic ripe}"
|
||||||
|
CASE_SCRIPT="$ROOT_DIR/scripts/payload_replay/run_multi_rir_replay_case.sh"
|
||||||
|
SUMMARY_SCRIPT="$ROOT_DIR/scripts/payload_replay/write_multi_rir_summary.py"
|
||||||
|
|
||||||
|
mkdir -p "$SUITE_OUT_DIR"
|
||||||
|
|
||||||
|
for rir in $RIRS; do
|
||||||
|
MULTI_RIR_OUT_DIR="$SUITE_OUT_DIR/$rir" \
|
||||||
|
BUNDLE_ROOT="$BUNDLE_ROOT" \
|
||||||
|
"$CASE_SCRIPT" "$rir" both
|
||||||
|
echo "completed $rir" >&2
|
||||||
|
echo >&2
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 "$SUMMARY_SCRIPT" \
|
||||||
|
--case-root "$SUITE_OUT_DIR" \
|
||||||
|
--out-md "$SUITE_OUT_DIR/multi_rir_summary.md" \
|
||||||
|
--out-json "$SUITE_OUT_DIR/multi_rir_summary.json" \
|
||||||
|
--rirs $RIRS >/dev/null
|
||||||
|
|
||||||
|
echo "== multi-RIR replay suite complete ==" >&2
|
||||||
|
echo "- suite_out_dir: $SUITE_OUT_DIR" >&2
|
||||||
|
echo "- summary_md: $SUITE_OUT_DIR/multi_rir_summary.md" >&2
|
||||||
|
echo "- summary_json: $SUITE_OUT_DIR/multi_rir_summary.json" >&2
|
||||||
133
scripts/payload_replay/write_multi_rir_case_report.py
Executable file
133
scripts/payload_replay/write_multi_rir_case_report.py
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(description="Generate one multi-RIR replay case report")
|
||||||
|
p.add_argument("--rir", required=True)
|
||||||
|
p.add_argument("--snapshot-meta", required=True)
|
||||||
|
p.add_argument("--snapshot-compare", required=True)
|
||||||
|
p.add_argument("--delta-meta", required=True)
|
||||||
|
p.add_argument("--delta-compare", required=True)
|
||||||
|
p.add_argument("--routinator-base-seconds", required=True, type=float)
|
||||||
|
p.add_argument("--routinator-delta-seconds", required=True, type=float)
|
||||||
|
p.add_argument("--out-md", required=True)
|
||||||
|
p.add_argument("--out-json", required=True)
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(path: str) -> dict:
|
||||||
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_compare_md(path: str) -> dict:
|
||||||
|
lines = Path(path).read_text(encoding="utf-8").splitlines()
|
||||||
|
out = {}
|
||||||
|
for line in lines:
|
||||||
|
if not line.startswith("| "):
|
||||||
|
continue
|
||||||
|
parts = [p.strip() for p in line.strip("|").split("|")]
|
||||||
|
if len(parts) != 2:
|
||||||
|
continue
|
||||||
|
key, value = parts
|
||||||
|
if key in {"metric", "---"}:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
out[key] = int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def ratio(ours: float, baseline: float) -> float | None:
|
||||||
|
if baseline <= 0:
|
||||||
|
return None
|
||||||
|
return ours / baseline
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(args: argparse.Namespace) -> dict:
|
||||||
|
snapshot_meta = read_json(args.snapshot_meta)
|
||||||
|
delta_meta = read_json(args.delta_meta)
|
||||||
|
snapshot_compare = parse_compare_md(args.snapshot_compare)
|
||||||
|
delta_compare = parse_compare_md(args.delta_compare)
|
||||||
|
|
||||||
|
snapshot_ours = float(snapshot_meta["durations_secs"]["rpki_run"])
|
||||||
|
delta_ours = float(delta_meta["durations_secs"]["rpki_run"])
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"rir": args.rir,
|
||||||
|
"snapshot": {
|
||||||
|
"meta_json": str(Path(args.snapshot_meta).resolve()),
|
||||||
|
"compare_md": str(Path(args.snapshot_compare).resolve()),
|
||||||
|
"ours_seconds": snapshot_ours,
|
||||||
|
"routinator_seconds": args.routinator_base_seconds,
|
||||||
|
"ratio": ratio(snapshot_ours, args.routinator_base_seconds),
|
||||||
|
"compare": snapshot_compare,
|
||||||
|
"match": snapshot_compare.get("only_in_ours", -1) == 0
|
||||||
|
and snapshot_compare.get("only_in_record", -1) == 0,
|
||||||
|
"counts": snapshot_meta.get("counts", {}),
|
||||||
|
},
|
||||||
|
"delta": {
|
||||||
|
"meta_json": str(Path(args.delta_meta).resolve()),
|
||||||
|
"compare_md": str(Path(args.delta_compare).resolve()),
|
||||||
|
"ours_seconds": delta_ours,
|
||||||
|
"routinator_seconds": args.routinator_delta_seconds,
|
||||||
|
"ratio": ratio(delta_ours, args.routinator_delta_seconds),
|
||||||
|
"compare": delta_compare,
|
||||||
|
"match": delta_compare.get("only_in_ours", -1) == 0
|
||||||
|
and delta_compare.get("only_in_record", -1) == 0,
|
||||||
|
"counts": delta_meta.get("counts", {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def write_md(path: Path, report: dict) -> None:
|
||||||
|
snapshot = report["snapshot"]
|
||||||
|
delta = report["delta"]
|
||||||
|
lines = []
|
||||||
|
lines.append(f"# {report['rir'].upper()} Replay Report\n\n")
|
||||||
|
lines.append("## Summary\n\n")
|
||||||
|
lines.append("| mode | match | ours_s | routinator_s | ratio | only_in_ours | only_in_record |\n")
|
||||||
|
lines.append("|---|---|---:|---:|---:|---:|---:|\n")
|
||||||
|
lines.append(
|
||||||
|
f"| snapshot | {str(snapshot['match']).lower()} | {snapshot['ours_seconds']:.3f} | {snapshot['routinator_seconds']:.3f} | {snapshot['ratio']:.3f} | {snapshot['compare'].get('only_in_ours', 0)} | {snapshot['compare'].get('only_in_record', 0)} |\n"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"| delta | {str(delta['match']).lower()} | {delta['ours_seconds']:.3f} | {delta['routinator_seconds']:.3f} | {delta['ratio']:.3f} | {delta['compare'].get('only_in_ours', 0)} | {delta['compare'].get('only_in_record', 0)} |\n"
|
||||||
|
)
|
||||||
|
lines.append("\n## Snapshot Inputs\n\n")
|
||||||
|
lines.append(f"- meta_json: `{snapshot['meta_json']}`\n")
|
||||||
|
lines.append(f"- compare_md: `{snapshot['compare_md']}`\n")
|
||||||
|
lines.append("\n## Delta Inputs\n\n")
|
||||||
|
lines.append(f"- meta_json: `{delta['meta_json']}`\n")
|
||||||
|
lines.append(f"- compare_md: `{delta['compare_md']}`\n")
|
||||||
|
lines.append("\n## Counts\n\n")
|
||||||
|
lines.append("### Snapshot\n\n")
|
||||||
|
for k, v in sorted(snapshot.get("counts", {}).items()):
|
||||||
|
lines.append(f"- {k}: `{v}`\n")
|
||||||
|
lines.append("\n### Delta\n\n")
|
||||||
|
for k, v in sorted(delta.get("counts", {}).items()):
|
||||||
|
lines.append(f"- {k}: `{v}`\n")
|
||||||
|
path.write_text("".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
report = build_report(args)
|
||||||
|
out_json = Path(args.out_json)
|
||||||
|
out_md = Path(args.out_md)
|
||||||
|
out_json.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_md.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
write_md(out_md, report)
|
||||||
|
print(out_md)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
87
scripts/payload_replay/write_multi_rir_summary.py
Executable file
87
scripts/payload_replay/write_multi_rir_summary.py
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_RIRS = ["afrinic", "apnic", "arin", "lacnic", "ripe"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(description="Aggregate per-RIR replay case reports")
|
||||||
|
p.add_argument("--case-root", required=True, help="directory containing <rir>/<rir>_case_report.json")
|
||||||
|
p.add_argument("--out-md", required=True)
|
||||||
|
p.add_argument("--out-json", required=True)
|
||||||
|
p.add_argument("--rirs", nargs="*", default=None, help="RIRs to include (default: all 5)")
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def read_case(case_root: Path, rir: str) -> dict:
|
||||||
|
path = case_root / rir / f"{rir}_case_report.json"
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def geomean(values: list[float]) -> float:
|
||||||
|
vals = [v for v in values if v > 0]
|
||||||
|
if not vals:
|
||||||
|
return 0.0
|
||||||
|
return math.exp(sum(math.log(v) for v in vals) / len(vals))
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary(cases: list[dict]) -> dict:
|
||||||
|
snapshot_ratios = [c["snapshot"]["ratio"] for c in cases]
|
||||||
|
delta_ratios = [c["delta"]["ratio"] for c in cases]
|
||||||
|
return {
|
||||||
|
"cases": cases,
|
||||||
|
"summary": {
|
||||||
|
"snapshot_all_match": all(c["snapshot"]["match"] for c in cases),
|
||||||
|
"delta_all_match": all(c["delta"]["match"] for c in cases),
|
||||||
|
"snapshot_ratio_geomean": geomean(snapshot_ratios),
|
||||||
|
"delta_ratio_geomean": geomean(delta_ratios),
|
||||||
|
"all_ratio_geomean": geomean(snapshot_ratios + delta_ratios),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_md(path: Path, data: dict) -> None:
|
||||||
|
lines = []
|
||||||
|
lines.append("# Multi-RIR Replay Summary\n\n")
|
||||||
|
lines.append("## Correctness + Timing\n\n")
|
||||||
|
lines.append("| RIR | snapshot_match | snapshot_ours_s | snapshot_routinator_s | snapshot_ratio | delta_match | delta_ours_s | delta_routinator_s | delta_ratio |\n")
|
||||||
|
lines.append("|---|---|---:|---:|---:|---|---:|---:|---:|\n")
|
||||||
|
for case in data["cases"]:
|
||||||
|
lines.append(
|
||||||
|
f"| {case['rir']} | {str(case['snapshot']['match']).lower()} | {case['snapshot']['ours_seconds']:.3f} | {case['snapshot']['routinator_seconds']:.3f} | {case['snapshot']['ratio']:.3f} | {str(case['delta']['match']).lower()} | {case['delta']['ours_seconds']:.3f} | {case['delta']['routinator_seconds']:.3f} | {case['delta']['ratio']:.3f} |\n"
|
||||||
|
)
|
||||||
|
s = data["summary"]
|
||||||
|
lines.append("\n## Aggregate Metrics\n\n")
|
||||||
|
lines.append("| metric | value |\n")
|
||||||
|
lines.append("|---|---:|\n")
|
||||||
|
lines.append(f"| snapshot_all_match | {str(s['snapshot_all_match']).lower()} |\n")
|
||||||
|
lines.append(f"| delta_all_match | {str(s['delta_all_match']).lower()} |\n")
|
||||||
|
lines.append(f"| snapshot_ratio_geomean | {s['snapshot_ratio_geomean']:.3f} |\n")
|
||||||
|
lines.append(f"| delta_ratio_geomean | {s['delta_ratio_geomean']:.3f} |\n")
|
||||||
|
lines.append(f"| all_ratio_geomean | {s['all_ratio_geomean']:.3f} |\n")
|
||||||
|
path.write_text("".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
case_root = Path(args.case_root)
|
||||||
|
rirs = args.rirs or DEFAULT_RIRS
|
||||||
|
cases = [read_case(case_root, rir) for rir in rirs]
|
||||||
|
data = build_summary(cases)
|
||||||
|
out_md = Path(args.out_md)
|
||||||
|
out_json = Path(args.out_json)
|
||||||
|
out_md.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_json.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_json.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
write_md(out_md, data)
|
||||||
|
print(out_md)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
123
scripts/periodic/compare_ccr_round.sh
Executable file
123
scripts/periodic/compare_ccr_round.sh
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/periodic/compare_ccr_round.sh \
|
||||||
|
--ours-ccr <path> \
|
||||||
|
--rpki-client-ccr <path> \
|
||||||
|
--out-dir <path> \
|
||||||
|
[--trust-anchor <name>]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
OURS_CCR=""
|
||||||
|
CLIENT_CCR=""
|
||||||
|
OUT_DIR=""
|
||||||
|
TRUST_ANCHOR="unknown"
|
||||||
|
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--ours-ccr) OURS_CCR="$2"; shift 2 ;;
|
||||||
|
--rpki-client-ccr) CLIENT_CCR="$2"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--trust-anchor) TRUST_ANCHOR="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$OURS_CCR" && -n "$CLIENT_CCR" && -n "$OUT_DIR" ]] || { usage >&2; exit 2; }
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
if [[ ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin ccr_to_compare_views
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
OURS_VRPS="$OUT_DIR/ours-vrps.csv"
|
||||||
|
OURS_VAPS="$OUT_DIR/ours-vaps.csv"
|
||||||
|
CLIENT_VRPS="$OUT_DIR/rpki-client-vrps.csv"
|
||||||
|
CLIENT_VAPS="$OUT_DIR/rpki-client-vaps.csv"
|
||||||
|
SUMMARY_JSON="$OUT_DIR/compare-summary.json"
|
||||||
|
SUMMARY_MD="$OUT_DIR/compare-summary.md"
|
||||||
|
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" \
|
||||||
|
--ccr "$OURS_CCR" \
|
||||||
|
--vrps-out "$OURS_VRPS" \
|
||||||
|
--vaps-out "$OURS_VAPS" \
|
||||||
|
--trust-anchor "$TRUST_ANCHOR"
|
||||||
|
|
||||||
|
"$CCR_TO_COMPARE_VIEWS_BIN" \
|
||||||
|
--ccr "$CLIENT_CCR" \
|
||||||
|
--vrps-out "$CLIENT_VRPS" \
|
||||||
|
--vaps-out "$CLIENT_VAPS" \
|
||||||
|
--trust-anchor "$TRUST_ANCHOR"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$OURS_VRPS" "$CLIENT_VRPS" "$OURS_VAPS" "$CLIENT_VAPS" "$SUMMARY_JSON" "$SUMMARY_MD"
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ours_vrps_path, client_vrps_path, ours_vaps_path, client_vaps_path, json_out, md_out = sys.argv[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def rows(path):
|
||||||
|
with open(path, newline="") as f:
|
||||||
|
return list(csv.reader(f))[1:]
|
||||||
|
|
||||||
|
|
||||||
|
ours_vrps = {tuple(r) for r in rows(ours_vrps_path)}
|
||||||
|
client_vrps = {tuple(r) for r in rows(client_vrps_path)}
|
||||||
|
ours_vaps = {tuple(r) for r in rows(ours_vaps_path)}
|
||||||
|
client_vaps = {tuple(r) for r in rows(client_vaps_path)}
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"vrps": {
|
||||||
|
"ours": len(ours_vrps),
|
||||||
|
"rpkiClient": len(client_vrps),
|
||||||
|
"match": ours_vrps == client_vrps,
|
||||||
|
"onlyInOurs": sorted(ours_vrps - client_vrps)[:20],
|
||||||
|
"onlyInRpkiClient": sorted(client_vrps - ours_vrps)[:20],
|
||||||
|
},
|
||||||
|
"vaps": {
|
||||||
|
"ours": len(ours_vaps),
|
||||||
|
"rpkiClient": len(client_vaps),
|
||||||
|
"match": ours_vaps == client_vaps,
|
||||||
|
"onlyInOurs": sorted(ours_vaps - client_vaps)[:20],
|
||||||
|
"onlyInRpkiClient": sorted(client_vaps - ours_vaps)[:20],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
summary["allMatch"] = summary["vrps"]["match"] and summary["vaps"]["match"]
|
||||||
|
|
||||||
|
Path(json_out).write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"# Round Compare Summary",
|
||||||
|
"",
|
||||||
|
f"- `allMatch`: `{summary['allMatch']}`",
|
||||||
|
f"- `vrpMatch`: `{summary['vrps']['match']}`",
|
||||||
|
f"- `vapMatch`: `{summary['vaps']['match']}`",
|
||||||
|
f"- `ours_vrps`: `{summary['vrps']['ours']}`",
|
||||||
|
f"- `rpki_client_vrps`: `{summary['vrps']['rpkiClient']}`",
|
||||||
|
f"- `ours_vaps`: `{summary['vaps']['ours']}`",
|
||||||
|
f"- `rpki_client_vaps`: `{summary['vaps']['rpkiClient']}`",
|
||||||
|
"",
|
||||||
|
"## Sample Differences",
|
||||||
|
"",
|
||||||
|
f"- `vrps.onlyInOurs`: `{len(summary['vrps']['onlyInOurs'])}`",
|
||||||
|
f"- `vrps.onlyInRpkiClient`: `{len(summary['vrps']['onlyInRpkiClient'])}`",
|
||||||
|
f"- `vaps.onlyInOurs`: `{len(summary['vaps']['onlyInOurs'])}`",
|
||||||
|
f"- `vaps.onlyInRpkiClient`: `{len(summary['vaps']['onlyInRpkiClient'])}`",
|
||||||
|
]
|
||||||
|
Path(md_out).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "$OUT_DIR"
|
||||||
317
scripts/periodic/run_arin_dual_rp_periodic_ccr_compare.sh
Executable file
317
scripts/periodic/run_arin_dual_rp_periodic_ccr_compare.sh
Executable file
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/periodic/run_arin_dual_rp_periodic_ccr_compare.sh \
|
||||||
|
--run-root <path> \
|
||||||
|
[--ssh-target <user@host>] \
|
||||||
|
[--remote-root <path>] \
|
||||||
|
[--rpki-client-bin <path>] \
|
||||||
|
[--round-count <n>] \
|
||||||
|
[--interval-secs <n>] \
|
||||||
|
[--start-at <RFC3339>] \
|
||||||
|
[--dry-run]
|
||||||
|
|
||||||
|
M1 behavior:
|
||||||
|
- creates the periodic run skeleton
|
||||||
|
- writes per-round scheduling metadata
|
||||||
|
- does not execute RP binaries yet
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
RUN_ROOT=""
|
||||||
|
SSH_TARGET="${SSH_TARGET:-root@47.77.183.68}"
|
||||||
|
REMOTE_ROOT=""
|
||||||
|
RPKI_CLIENT_BIN="${RPKI_CLIENT_BIN:-/home/yuyr/dev/rpki-client-9.7/build-m5/src/rpki-client}"
|
||||||
|
ROUND_COUNT=10
|
||||||
|
INTERVAL_SECS=600
|
||||||
|
START_AT=""
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--run-root) RUN_ROOT="$2"; shift 2 ;;
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
|
||||||
|
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
|
||||||
|
--round-count) ROUND_COUNT="$2"; shift 2 ;;
|
||||||
|
--interval-secs) INTERVAL_SECS="$2"; shift 2 ;;
|
||||||
|
--start-at) START_AT="$2"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift 1 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$RUN_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
[[ "$ROUND_COUNT" =~ ^[0-9]+$ ]] || { echo "--round-count must be an integer" >&2; exit 2; }
|
||||||
|
[[ "$INTERVAL_SECS" =~ ^[0-9]+$ ]] || { echo "--interval-secs must be an integer" >&2; exit 2; }
|
||||||
|
if [[ "$DRY_RUN" -ne 1 ]]; then
|
||||||
|
[[ -n "$REMOTE_ROOT" ]] || { echo "--remote-root is required unless --dry-run" >&2; exit 2; }
|
||||||
|
[[ -x "$RPKI_CLIENT_BIN" ]] || { echo "rpki-client binary not executable: $RPKI_CLIENT_BIN" >&2; exit 2; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$RUN_ROOT"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$RUN_ROOT" "$SSH_TARGET" "$REMOTE_ROOT" "$ROUND_COUNT" "$INTERVAL_SECS" "$START_AT" "$DRY_RUN"
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
run_root = Path(sys.argv[1]).resolve()
|
||||||
|
ssh_target = sys.argv[2]
|
||||||
|
remote_root = sys.argv[3]
|
||||||
|
round_count = int(sys.argv[4])
|
||||||
|
interval_secs = int(sys.argv[5])
|
||||||
|
start_at_arg = sys.argv[6]
|
||||||
|
dry_run = bool(int(sys.argv[7]))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rfc3339_utc(value: str) -> datetime:
|
||||||
|
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(dt: datetime) -> str:
|
||||||
|
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
base_time = (
|
||||||
|
parse_rfc3339_utc(start_at_arg)
|
||||||
|
if start_at_arg
|
||||||
|
else datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
(run_root / "rounds").mkdir(parents=True, exist_ok=True)
|
||||||
|
(run_root / "state" / "ours" / "work-db").mkdir(parents=True, exist_ok=True)
|
||||||
|
(run_root / "state" / "ours" / "raw-store.db").mkdir(parents=True, exist_ok=True)
|
||||||
|
(run_root / "state" / "rpki-client" / "cache").mkdir(parents=True, exist_ok=True)
|
||||||
|
(run_root / "state" / "rpki-client" / "out").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
"version": 1,
|
||||||
|
"rir": "arin",
|
||||||
|
"roundCount": round_count,
|
||||||
|
"intervalSecs": interval_secs,
|
||||||
|
"baseScheduledAt": fmt(base_time),
|
||||||
|
"mode": "dry_run" if dry_run else "skeleton_only",
|
||||||
|
"execution": {
|
||||||
|
"mode": "remote",
|
||||||
|
"sshTarget": ssh_target,
|
||||||
|
"remoteRoot": remote_root or None,
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"ours": {
|
||||||
|
"workDbPath": "state/ours/work-db",
|
||||||
|
"rawStoreDbPath": "state/ours/raw-store.db",
|
||||||
|
"remoteWorkDbPath": "state/ours/work-db",
|
||||||
|
"remoteRawStoreDbPath": "state/ours/raw-store.db",
|
||||||
|
},
|
||||||
|
"rpkiClient": {
|
||||||
|
"cachePath": "state/rpki-client/cache",
|
||||||
|
"outPath": "state/rpki-client/out",
|
||||||
|
"remoteCachePath": "state/rpki-client/cache",
|
||||||
|
"remoteOutPath": "state/rpki-client/out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(run_root / "meta.json").write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
rounds = []
|
||||||
|
for idx in range(round_count):
|
||||||
|
round_id = f"round-{idx+1:03d}"
|
||||||
|
kind = "snapshot" if idx == 0 else "delta"
|
||||||
|
scheduled_at = base_time + timedelta(seconds=interval_secs * idx)
|
||||||
|
round_dir = run_root / "rounds" / round_id
|
||||||
|
for name in ("ours", "rpki-client", "compare"):
|
||||||
|
(round_dir / name).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# M1 only builds the schedule skeleton, so lag is defined relative to the schedule model.
|
||||||
|
started_at = scheduled_at if dry_run else None
|
||||||
|
finished_at = scheduled_at if dry_run else None
|
||||||
|
round_meta = {
|
||||||
|
"roundId": round_id,
|
||||||
|
"kind": kind,
|
||||||
|
"scheduledAt": fmt(scheduled_at),
|
||||||
|
"startedAt": fmt(started_at) if started_at else None,
|
||||||
|
"finishedAt": fmt(finished_at) if finished_at else None,
|
||||||
|
"startLagMs": 0 if dry_run else None,
|
||||||
|
"status": "dry_run" if dry_run else "pending",
|
||||||
|
"paths": {
|
||||||
|
"ours": f"rounds/{round_id}/ours",
|
||||||
|
"rpkiClient": f"rounds/{round_id}/rpki-client",
|
||||||
|
"compare": f"rounds/{round_id}/compare",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(round_dir / "round-meta.json").write_text(
|
||||||
|
json.dumps(round_meta, indent=2), encoding="utf-8"
|
||||||
|
)
|
||||||
|
rounds.append(round_meta)
|
||||||
|
|
||||||
|
final_summary = {
|
||||||
|
"version": 1,
|
||||||
|
"status": "dry_run" if dry_run else "pending",
|
||||||
|
"roundCount": round_count,
|
||||||
|
"allMatch": None,
|
||||||
|
"rounds": rounds,
|
||||||
|
}
|
||||||
|
(run_root / "final-summary.json").write_text(
|
||||||
|
json.dumps(final_summary, indent=2), encoding="utf-8"
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||||
|
echo "$RUN_ROOT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$ROOT_DIR/target/release/rpki" || ! -x "$ROOT_DIR/target/release/ccr_to_compare_views" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
cargo build --release --bin rpki --bin ccr_to_compare_views
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_ROOT'"
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude target \
|
||||||
|
--exclude .git \
|
||||||
|
"$ROOT_DIR/" "$SSH_TARGET:$REMOTE_ROOT/repo/"
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_ROOT/repo/target/release' '$REMOTE_ROOT/bin' '$REMOTE_ROOT/rounds' '$REMOTE_ROOT/state/ours' '$REMOTE_ROOT/state/rpki-client'"
|
||||||
|
rsync -a "$ROOT_DIR/target/release/rpki" "$SSH_TARGET:$REMOTE_ROOT/repo/target/release/"
|
||||||
|
rsync -a "$RPKI_CLIENT_BIN" "$SSH_TARGET:$REMOTE_ROOT/bin/rpki-client"
|
||||||
|
|
||||||
|
for idx in $(seq 1 "$ROUND_COUNT"); do
|
||||||
|
ROUND_ID="$(printf 'round-%03d' "$idx")"
|
||||||
|
ROUND_DIR="$RUN_ROOT/rounds/$ROUND_ID"
|
||||||
|
SCHEDULED_AT="$(python3 - <<'PY' "$ROUND_DIR/round-meta.json"
|
||||||
|
import json, sys
|
||||||
|
print(json.load(open(sys.argv[1], 'r', encoding='utf-8'))['scheduledAt'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$SCHEDULED_AT"
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import sys, time
|
||||||
|
scheduled = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
delay = (scheduled - now).total_seconds()
|
||||||
|
if delay > 0:
|
||||||
|
time.sleep(delay)
|
||||||
|
PY
|
||||||
|
|
||||||
|
"$ROOT_DIR/scripts/periodic/run_arin_ours_round_remote.sh" \
|
||||||
|
--run-root "$RUN_ROOT" \
|
||||||
|
--round-id "$ROUND_ID" \
|
||||||
|
--kind "$(python3 - <<'PY' "$ROUND_DIR/round-meta.json"
|
||||||
|
import json, sys
|
||||||
|
print(json.load(open(sys.argv[1], 'r', encoding='utf-8'))['kind'])
|
||||||
|
PY
|
||||||
|
)" \
|
||||||
|
--ssh-target "$SSH_TARGET" \
|
||||||
|
--remote-root "$REMOTE_ROOT" \
|
||||||
|
--scheduled-at "$SCHEDULED_AT" \
|
||||||
|
--skip-sync &
|
||||||
|
OURS_PID=$!
|
||||||
|
|
||||||
|
"$ROOT_DIR/scripts/periodic/run_arin_rpki_client_round_remote.sh" \
|
||||||
|
--run-root "$RUN_ROOT" \
|
||||||
|
--round-id "$ROUND_ID" \
|
||||||
|
--kind "$(python3 - <<'PY' "$ROUND_DIR/round-meta.json"
|
||||||
|
import json, sys
|
||||||
|
print(json.load(open(sys.argv[1], 'r', encoding='utf-8'))['kind'])
|
||||||
|
PY
|
||||||
|
)" \
|
||||||
|
--ssh-target "$SSH_TARGET" \
|
||||||
|
--remote-root "$REMOTE_ROOT" \
|
||||||
|
--scheduled-at "$SCHEDULED_AT" \
|
||||||
|
--rpki-client-bin "$RPKI_CLIENT_BIN" \
|
||||||
|
--skip-sync &
|
||||||
|
CLIENT_PID=$!
|
||||||
|
|
||||||
|
set +e
|
||||||
|
wait "$OURS_PID"
|
||||||
|
OURS_STATUS=$?
|
||||||
|
wait "$CLIENT_PID"
|
||||||
|
CLIENT_STATUS=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$OURS_STATUS" -eq 0 && "$CLIENT_STATUS" -eq 0 \
|
||||||
|
&& -f "$ROUND_DIR/ours/result.ccr" && -f "$ROUND_DIR/rpki-client/result.ccr" ]]; then
|
||||||
|
"$ROOT_DIR/scripts/periodic/compare_ccr_round.sh" \
|
||||||
|
--ours-ccr "$ROUND_DIR/ours/result.ccr" \
|
||||||
|
--rpki-client-ccr "$ROUND_DIR/rpki-client/result.ccr" \
|
||||||
|
--out-dir "$ROUND_DIR/compare"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ROUND_DIR/round-meta.json" "$ROUND_DIR/ours/round-result.json" "$ROUND_DIR/rpki-client/round-result.json" "$ROUND_DIR/compare/compare-summary.json"
|
||||||
|
import json, sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
round_meta_path, ours_result_path, client_result_path, compare_path = sys.argv[1:]
|
||||||
|
meta = json.load(open(round_meta_path, 'r', encoding='utf-8'))
|
||||||
|
ours = json.load(open(ours_result_path, 'r', encoding='utf-8'))
|
||||||
|
client = json.load(open(client_result_path, 'r', encoding='utf-8'))
|
||||||
|
scheduled = datetime.fromisoformat(meta['scheduledAt'].replace('Z', '+00:00')).astimezone(timezone.utc)
|
||||||
|
started_candidates = []
|
||||||
|
for item in (ours, client):
|
||||||
|
if item.get('startedAt'):
|
||||||
|
started_candidates.append(datetime.fromisoformat(item['startedAt'].replace('Z', '+00:00')).astimezone(timezone.utc))
|
||||||
|
finished_candidates = []
|
||||||
|
for item in (ours, client):
|
||||||
|
if item.get('finishedAt'):
|
||||||
|
finished_candidates.append(datetime.fromisoformat(item['finishedAt'].replace('Z', '+00:00')).astimezone(timezone.utc))
|
||||||
|
if started_candidates:
|
||||||
|
started_at = min(started_candidates)
|
||||||
|
meta['startedAt'] = started_at.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
lag_ms = int((started_at - scheduled).total_seconds() * 1000)
|
||||||
|
meta['startLagMs'] = max(lag_ms, 0)
|
||||||
|
if finished_candidates:
|
||||||
|
finished_at = max(finished_candidates)
|
||||||
|
meta['finishedAt'] = finished_at.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
meta['status'] = 'completed' if ours.get('exitCode') == 0 and client.get('exitCode') == 0 else 'failed'
|
||||||
|
meta['ours'] = {
|
||||||
|
'exitCode': ours.get('exitCode'),
|
||||||
|
'durationMs': json.load(open(ours_result_path.replace('round-result.json', 'timing.json'), 'r', encoding='utf-8')).get('durationMs'),
|
||||||
|
}
|
||||||
|
meta['rpkiClient'] = {
|
||||||
|
'exitCode': client.get('exitCode'),
|
||||||
|
'durationMs': json.load(open(client_result_path.replace('round-result.json', 'timing.json'), 'r', encoding='utf-8')).get('durationMs'),
|
||||||
|
}
|
||||||
|
if compare_path and __import__('pathlib').Path(compare_path).exists():
|
||||||
|
compare = json.load(open(compare_path, 'r', encoding='utf-8'))
|
||||||
|
meta['compare'] = {
|
||||||
|
'allMatch': compare.get('allMatch'),
|
||||||
|
'vrpMatch': compare.get('vrps', {}).get('match'),
|
||||||
|
'vapMatch': compare.get('vaps', {}).get('match'),
|
||||||
|
}
|
||||||
|
json.dump(meta, open(round_meta_path, 'w', encoding='utf-8'), indent=2)
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - <<'PY' "$RUN_ROOT/final-summary.json" "$RUN_ROOT/rounds"
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
summary_path = Path(sys.argv[1])
|
||||||
|
rounds_root = Path(sys.argv[2])
|
||||||
|
rounds = []
|
||||||
|
all_match = True
|
||||||
|
for round_dir in sorted(rounds_root.glob('round-*')):
|
||||||
|
meta = json.load(open(round_dir / 'round-meta.json', 'r', encoding='utf-8'))
|
||||||
|
rounds.append(meta)
|
||||||
|
compare = meta.get('compare')
|
||||||
|
if compare is None or compare.get('allMatch') is not True:
|
||||||
|
all_match = False
|
||||||
|
summary = {
|
||||||
|
'version': 1,
|
||||||
|
'status': 'completed',
|
||||||
|
'roundCount': len(rounds),
|
||||||
|
'allMatch': all_match,
|
||||||
|
'rounds': rounds,
|
||||||
|
}
|
||||||
|
json.dump(summary, open(summary_path, 'w', encoding='utf-8'), indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "$RUN_ROOT"
|
||||||
155
scripts/periodic/run_arin_ours_round_remote.sh
Executable file
155
scripts/periodic/run_arin_ours_round_remote.sh
Executable file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/periodic/run_arin_ours_round_remote.sh \
|
||||||
|
--run-root <path> \
|
||||||
|
--round-id <round-XXX> \
|
||||||
|
--kind <snapshot|delta> \
|
||||||
|
--ssh-target <user@host> \
|
||||||
|
--remote-root <path> \
|
||||||
|
[--scheduled-at <RFC3339>] \
|
||||||
|
[--skip-sync]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
RUN_ROOT=""
|
||||||
|
ROUND_ID=""
|
||||||
|
KIND=""
|
||||||
|
SSH_TARGET="${SSH_TARGET:-root@47.77.183.68}"
|
||||||
|
REMOTE_ROOT=""
|
||||||
|
SCHEDULED_AT=""
|
||||||
|
SKIP_SYNC=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--run-root) RUN_ROOT="$2"; shift 2 ;;
|
||||||
|
--round-id) ROUND_ID="$2"; shift 2 ;;
|
||||||
|
--kind) KIND="$2"; shift 2 ;;
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
|
||||||
|
--scheduled-at) SCHEDULED_AT="$2"; shift 2 ;;
|
||||||
|
--skip-sync) SKIP_SYNC=1; shift 1 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$RUN_ROOT" && -n "$ROUND_ID" && -n "$KIND" && -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
[[ "$KIND" == "snapshot" || "$KIND" == "delta" ]] || { echo "--kind must be snapshot or delta" >&2; exit 2; }
|
||||||
|
|
||||||
|
LOCAL_OUT="$RUN_ROOT/rounds/$ROUND_ID/ours"
|
||||||
|
REMOTE_REPO="$REMOTE_ROOT/repo"
|
||||||
|
REMOTE_OUT="$REMOTE_ROOT/rounds/$ROUND_ID/ours"
|
||||||
|
REMOTE_WORK_DB="$REMOTE_ROOT/state/ours/work-db"
|
||||||
|
REMOTE_RAW_STORE="$REMOTE_ROOT/state/ours/raw-store.db"
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_OUT"
|
||||||
|
|
||||||
|
if [[ "$SKIP_SYNC" -eq 0 ]]; then
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_ROOT'"
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude target \
|
||||||
|
--exclude .git \
|
||||||
|
"$ROOT_DIR/" "$SSH_TARGET:$REMOTE_REPO/"
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_REPO/target/release' '$REMOTE_OUT' '$REMOTE_ROOT/state/ours'"
|
||||||
|
rsync -a "$ROOT_DIR/target/release/rpki" "$SSH_TARGET:$REMOTE_REPO/target/release/"
|
||||||
|
else
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_OUT' '$REMOTE_ROOT/state/ours'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh "$SSH_TARGET" \
|
||||||
|
REMOTE_REPO="$REMOTE_REPO" \
|
||||||
|
REMOTE_OUT="$REMOTE_OUT" \
|
||||||
|
REMOTE_WORK_DB="$REMOTE_WORK_DB" \
|
||||||
|
REMOTE_RAW_STORE="$REMOTE_RAW_STORE" \
|
||||||
|
KIND="$KIND" \
|
||||||
|
ROUND_ID="$ROUND_ID" \
|
||||||
|
SCHEDULED_AT="$SCHEDULED_AT" \
|
||||||
|
'bash -s' <<'EOS'
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$REMOTE_REPO"
|
||||||
|
mkdir -p "$REMOTE_OUT"
|
||||||
|
|
||||||
|
if [[ "$KIND" == "snapshot" ]]; then
|
||||||
|
rm -rf "$REMOTE_WORK_DB" "$REMOTE_RAW_STORE"
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$REMOTE_WORK_DB")"
|
||||||
|
|
||||||
|
started_at_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
started_at_ms="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
ccr_out="$REMOTE_OUT/result.ccr"
|
||||||
|
report_out="$REMOTE_OUT/report.json"
|
||||||
|
run_log="$REMOTE_OUT/run.log"
|
||||||
|
timing_out="$REMOTE_OUT/timing.json"
|
||||||
|
meta_out="$REMOTE_OUT/round-result.json"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
env RPKI_PROGRESS_LOG=1 target/release/rpki \
|
||||||
|
--db "$REMOTE_WORK_DB" \
|
||||||
|
--raw-store-db "$REMOTE_RAW_STORE" \
|
||||||
|
--tal-path tests/fixtures/tal/arin.tal \
|
||||||
|
--ta-path tests/fixtures/ta/arin-ta.cer \
|
||||||
|
--ccr-out "$ccr_out" \
|
||||||
|
--report-json "$report_out" \
|
||||||
|
>"$run_log" 2>&1
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
finished_at_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
finished_at_ms="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$timing_out" "$started_at_ms" "$finished_at_ms" "$started_at_iso" "$finished_at_iso" "$exit_code"
|
||||||
|
import json, sys
|
||||||
|
path, start_ms, end_ms, started_at, finished_at, exit_code = sys.argv[1:]
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"durationMs": int(end_ms) - int(start_ms),
|
||||||
|
"startedAt": started_at,
|
||||||
|
"finishedAt": finished_at,
|
||||||
|
"exitCode": int(exit_code),
|
||||||
|
},
|
||||||
|
fh,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 - <<'PY' "$meta_out" "$ROUND_ID" "$KIND" "$SCHEDULED_AT" "$started_at_iso" "$finished_at_iso" "$REMOTE_WORK_DB" "$REMOTE_RAW_STORE" "$exit_code"
|
||||||
|
import json, sys
|
||||||
|
path, round_id, kind, scheduled_at, started_at, finished_at, work_db, raw_store, exit_code = sys.argv[1:]
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"roundId": round_id,
|
||||||
|
"kind": kind,
|
||||||
|
"scheduledAt": scheduled_at or None,
|
||||||
|
"startedAt": started_at,
|
||||||
|
"finishedAt": finished_at,
|
||||||
|
"remoteWorkDbPath": work_db,
|
||||||
|
"remoteRawStoreDbPath": raw_store,
|
||||||
|
"exitCode": int(exit_code),
|
||||||
|
},
|
||||||
|
fh,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
exit "$exit_code"
|
||||||
|
EOS
|
||||||
|
|
||||||
|
rsync -a "$SSH_TARGET:$REMOTE_OUT/" "$LOCAL_OUT/"
|
||||||
|
echo "$LOCAL_OUT"
|
||||||
165
scripts/periodic/run_arin_rpki_client_round_remote.sh
Executable file
165
scripts/periodic/run_arin_rpki_client_round_remote.sh
Executable file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/periodic/run_arin_rpki_client_round_remote.sh \
|
||||||
|
--run-root <path> \
|
||||||
|
--round-id <round-XXX> \
|
||||||
|
--kind <snapshot|delta> \
|
||||||
|
--ssh-target <user@host> \
|
||||||
|
--remote-root <path> \
|
||||||
|
[--scheduled-at <RFC3339>] \
|
||||||
|
[--rpki-client-bin <local path>] \
|
||||||
|
[--skip-sync]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
RUN_ROOT=""
|
||||||
|
ROUND_ID=""
|
||||||
|
KIND=""
|
||||||
|
SSH_TARGET="${SSH_TARGET:-root@47.77.183.68}"
|
||||||
|
REMOTE_ROOT=""
|
||||||
|
SCHEDULED_AT=""
|
||||||
|
RPKI_CLIENT_BIN="${RPKI_CLIENT_BIN:-/home/yuyr/dev/rpki-client-9.7/build-m5/src/rpki-client}"
|
||||||
|
SKIP_SYNC=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--run-root) RUN_ROOT="$2"; shift 2 ;;
|
||||||
|
--round-id) ROUND_ID="$2"; shift 2 ;;
|
||||||
|
--kind) KIND="$2"; shift 2 ;;
|
||||||
|
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||||
|
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
|
||||||
|
--scheduled-at) SCHEDULED_AT="$2"; shift 2 ;;
|
||||||
|
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
|
||||||
|
--skip-sync) SKIP_SYNC=1; shift 1 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$RUN_ROOT" && -n "$ROUND_ID" && -n "$KIND" && -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
|
||||||
|
[[ "$KIND" == "snapshot" || "$KIND" == "delta" ]] || { echo "--kind must be snapshot or delta" >&2; exit 2; }
|
||||||
|
[[ -x "$RPKI_CLIENT_BIN" ]] || { echo "rpki-client binary not executable: $RPKI_CLIENT_BIN" >&2; exit 2; }
|
||||||
|
|
||||||
|
LOCAL_OUT="$RUN_ROOT/rounds/$ROUND_ID/rpki-client"
|
||||||
|
REMOTE_REPO="$REMOTE_ROOT/repo"
|
||||||
|
REMOTE_BIN_DIR="$REMOTE_ROOT/bin"
|
||||||
|
REMOTE_BIN="$REMOTE_BIN_DIR/rpki-client"
|
||||||
|
REMOTE_OUT="$REMOTE_ROOT/rounds/$ROUND_ID/rpki-client"
|
||||||
|
REMOTE_CACHE="$REMOTE_ROOT/state/rpki-client/cache"
|
||||||
|
REMOTE_STATE_OUT="$REMOTE_ROOT/state/rpki-client/out"
|
||||||
|
REMOTE_STATE_ROOT="$REMOTE_ROOT/state/rpki-client"
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_OUT"
|
||||||
|
|
||||||
|
if [[ "$SKIP_SYNC" -eq 0 ]]; then
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_ROOT'"
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude target \
|
||||||
|
--exclude .git \
|
||||||
|
"$ROOT_DIR/" "$SSH_TARGET:$REMOTE_REPO/"
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_BIN_DIR' '$REMOTE_OUT' '$REMOTE_STATE_ROOT'"
|
||||||
|
rsync -a "$RPKI_CLIENT_BIN" "$SSH_TARGET:$REMOTE_BIN"
|
||||||
|
else
|
||||||
|
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_BIN_DIR' '$REMOTE_OUT' '$REMOTE_STATE_ROOT'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh "$SSH_TARGET" \
|
||||||
|
REMOTE_ROOT="$REMOTE_ROOT" \
|
||||||
|
REMOTE_REPO="$REMOTE_REPO" \
|
||||||
|
REMOTE_BIN="$REMOTE_BIN" \
|
||||||
|
REMOTE_OUT="$REMOTE_OUT" \
|
||||||
|
REMOTE_CACHE="$REMOTE_CACHE" \
|
||||||
|
REMOTE_STATE_OUT="$REMOTE_STATE_OUT" \
|
||||||
|
REMOTE_STATE_ROOT="$REMOTE_STATE_ROOT" \
|
||||||
|
KIND="$KIND" \
|
||||||
|
ROUND_ID="$ROUND_ID" \
|
||||||
|
SCHEDULED_AT="$SCHEDULED_AT" \
|
||||||
|
'bash -s' <<'EOS'
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$REMOTE_ROOT"
|
||||||
|
mkdir -p "$REMOTE_OUT"
|
||||||
|
|
||||||
|
if [[ "$KIND" == "snapshot" ]]; then
|
||||||
|
rm -rf "$REMOTE_CACHE" "$REMOTE_STATE_OUT" "$REMOTE_STATE_ROOT/ta" "$REMOTE_STATE_ROOT/.ta"
|
||||||
|
fi
|
||||||
|
mkdir -p "$REMOTE_CACHE" "$REMOTE_STATE_OUT"
|
||||||
|
chmod 0777 "$REMOTE_STATE_ROOT" "$REMOTE_CACHE" "$REMOTE_STATE_OUT"
|
||||||
|
|
||||||
|
started_at_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
started_at_ms="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
ccr_out="$REMOTE_OUT/result.ccr"
|
||||||
|
run_log="$REMOTE_OUT/run.log"
|
||||||
|
timing_out="$REMOTE_OUT/timing.json"
|
||||||
|
meta_out="$REMOTE_OUT/round-result.json"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
(
|
||||||
|
cd "$REMOTE_STATE_ROOT"
|
||||||
|
"$REMOTE_BIN" -vv -t "../../repo/tests/fixtures/tal/arin.tal" -d "cache" "out"
|
||||||
|
) >"$run_log" 2>&1
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ -f "$REMOTE_STATE_OUT/rpki.ccr" ]]; then
|
||||||
|
cp "$REMOTE_STATE_OUT/rpki.ccr" "$ccr_out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
finished_at_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
finished_at_ms="$(python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
python3 - <<'PY' "$timing_out" "$started_at_ms" "$finished_at_ms" "$started_at_iso" "$finished_at_iso" "$exit_code"
|
||||||
|
import json, sys
|
||||||
|
path, start_ms, end_ms, started_at, finished_at, exit_code = sys.argv[1:]
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"durationMs": int(end_ms) - int(start_ms),
|
||||||
|
"startedAt": started_at,
|
||||||
|
"finishedAt": finished_at,
|
||||||
|
"exitCode": int(exit_code),
|
||||||
|
},
|
||||||
|
fh,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 - <<'PY' "$meta_out" "$ROUND_ID" "$KIND" "$SCHEDULED_AT" "$started_at_iso" "$finished_at_iso" "$REMOTE_CACHE" "$REMOTE_STATE_OUT" "$exit_code"
|
||||||
|
import json, sys
|
||||||
|
path, round_id, kind, scheduled_at, started_at, finished_at, cache_path, out_path, exit_code = sys.argv[1:]
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"roundId": round_id,
|
||||||
|
"kind": kind,
|
||||||
|
"scheduledAt": scheduled_at or None,
|
||||||
|
"startedAt": started_at,
|
||||||
|
"finishedAt": finished_at,
|
||||||
|
"remoteCachePath": cache_path,
|
||||||
|
"remoteOutPath": out_path,
|
||||||
|
"exitCode": int(exit_code),
|
||||||
|
},
|
||||||
|
fh,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
exit "$exit_code"
|
||||||
|
EOS
|
||||||
|
|
||||||
|
rsync -a "$SSH_TARGET:$REMOTE_OUT/" "$LOCAL_OUT/"
|
||||||
|
echo "$LOCAL_OUT"
|
||||||
129
scripts/replay_bundle/README.md
Normal file
129
scripts/replay_bundle/README.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Live Bundle Record
|
||||||
|
|
||||||
|
`run_live_bundle_record.sh` 是当前 `ours` 的单命令 live bundle 录制入口。
|
||||||
|
|
||||||
|
它做三件事:
|
||||||
|
|
||||||
|
1. 联网执行 **live base recorder**
|
||||||
|
2. 基于刚录制的 base bundle 执行 **live delta recorder**
|
||||||
|
3. 产出一个统一的最终目录,包含:
|
||||||
|
- `base-payload-archive/`
|
||||||
|
- `payload-delta-archive/`
|
||||||
|
- `base-locks.json`
|
||||||
|
- `locks-delta.json`
|
||||||
|
- `tal.tal`
|
||||||
|
- `ta.cer`
|
||||||
|
- `base.ccr`
|
||||||
|
- `delta.ccr`
|
||||||
|
- `base-vrps.csv`
|
||||||
|
- `base-vaps.csv`
|
||||||
|
- `record-delta.csv`
|
||||||
|
- `record-delta-vaps.csv`
|
||||||
|
- `bundle.json`
|
||||||
|
- `verification.json`
|
||||||
|
- `timings/`
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd rpki
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record.sh \
|
||||||
|
--rir apnic \
|
||||||
|
--tal-path tests/fixtures/tal/apnic-rfc7730-https.tal \
|
||||||
|
--ta-path tests/fixtures/ta/apnic-ta.cer
|
||||||
|
```
|
||||||
|
|
||||||
|
默认输出目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/replay/<rir>_live_bundle_<timestamp>
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要一次录制多个 RIR,使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd rpki
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_multi_rir.sh \
|
||||||
|
--rir afrinic,apnic,arin,lacnic,ripe
|
||||||
|
```
|
||||||
|
|
||||||
|
默认输出目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/replay/live_bundle_matrix_<timestamp>
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 RIR 会落到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/replay/live_bundle_matrix_<timestamp>/<rir>_live_bundle_<timestamp>
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要录制单个 RIR 的 `1 base + N delta` 序列,使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd rpki
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_sequence.sh \
|
||||||
|
--rir apnic \
|
||||||
|
--tal-path tests/fixtures/tal/apnic-rfc7730-https.tal \
|
||||||
|
--ta-path tests/fixtures/ta/apnic-ta.cer \
|
||||||
|
--delta-count 2 \
|
||||||
|
--delta-interval-secs 0
|
||||||
|
```
|
||||||
|
|
||||||
|
默认输出目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/replay/<rir>_live_bundle_sequence_<timestamp>
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要一次录制多个 RIR 的 `1 base + N delta` 序列,使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd rpki
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_multi_rir_sequence.sh \
|
||||||
|
--rir afrinic,apnic,arin,lacnic,ripe
|
||||||
|
```
|
||||||
|
|
||||||
|
默认输出目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/replay/live_bundle_sequence_matrix_<timestamp>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可选参数
|
||||||
|
|
||||||
|
- `--out-dir <path>`
|
||||||
|
- `--base-validation-time <rfc3339>`
|
||||||
|
- `--delta-validation-time <rfc3339>`
|
||||||
|
- `--http-timeout-secs <n>`
|
||||||
|
- `--rsync-timeout-secs <n>`
|
||||||
|
- `--rsync-mirror-root <path>`
|
||||||
|
- `--max-depth <n>`
|
||||||
|
- `--max-instances <n>`
|
||||||
|
- `--trust-anchor <name>`
|
||||||
|
- `--bin-dir <path>`
|
||||||
|
- `--no-build`
|
||||||
|
- `--delta-count <n>`(sequence 入口)
|
||||||
|
- `--delta-interval-secs <n>`(sequence 入口)
|
||||||
|
- `--keep-db`(sequence 入口)
|
||||||
|
|
||||||
|
`run_live_bundle_record_multi_rir.sh` 会自动按 RIR 选择当前仓库内置的:
|
||||||
|
|
||||||
|
- `tests/fixtures/tal/*.tal`
|
||||||
|
- `tests/fixtures/ta/*.cer`
|
||||||
|
|
||||||
|
并将 `--trust-anchor` 设置为对应 RIR 名称。
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 该脚本会先构建:
|
||||||
|
- `replay_bundle_capture`
|
||||||
|
- `replay_bundle_capture_delta`
|
||||||
|
- 如果提供 `--no-build`,则直接复用:
|
||||||
|
- `--bin-dir <path>` 下的现有二进制
|
||||||
|
- 中间 staging 目录:
|
||||||
|
- `<out>.stage-base`
|
||||||
|
- `<out>.stage-delta`
|
||||||
|
在成功完成后会清理,只保留最终输出目录。
|
||||||
|
- 最终输出目录是 **delta 阶段产物**,其中已经包含 base 阶段结果。
|
||||||
135
scripts/replay_bundle/run_live_bundle_record.sh
Executable file
135
scripts/replay_bundle/run_live_bundle_record.sh
Executable file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
RIR=""
|
||||||
|
OUT_DIR=""
|
||||||
|
TAL_PATH=""
|
||||||
|
TA_PATH=""
|
||||||
|
BASE_VALIDATION_TIME=""
|
||||||
|
DELTA_VALIDATION_TIME=""
|
||||||
|
HTTP_TIMEOUT_SECS=""
|
||||||
|
RSYNC_TIMEOUT_SECS=""
|
||||||
|
RSYNC_MIRROR_ROOT=""
|
||||||
|
MAX_DEPTH=""
|
||||||
|
MAX_INSTANCES=""
|
||||||
|
TRUST_ANCHOR=""
|
||||||
|
NO_BUILD=0
|
||||||
|
BIN_DIR="target/release"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record.sh \
|
||||||
|
--rir <name> \
|
||||||
|
--tal-path <path> \
|
||||||
|
--ta-path <path> \
|
||||||
|
[--out-dir <path>] \
|
||||||
|
[--base-validation-time <rfc3339>] \
|
||||||
|
[--delta-validation-time <rfc3339>] \
|
||||||
|
[--http-timeout-secs <n>] \
|
||||||
|
[--rsync-timeout-secs <n>] \
|
||||||
|
[--rsync-mirror-root <path>] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>] \
|
||||||
|
[--trust-anchor <name>] \
|
||||||
|
[--bin-dir <path>] \
|
||||||
|
[--no-build]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir) RIR="${2:?}"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--tal-path) TAL_PATH="${2:?}"; shift 2 ;;
|
||||||
|
--ta-path) TA_PATH="${2:?}"; shift 2 ;;
|
||||||
|
--base-validation-time) BASE_VALIDATION_TIME="${2:?}"; shift 2 ;;
|
||||||
|
--delta-validation-time) DELTA_VALIDATION_TIME="${2:?}"; shift 2 ;;
|
||||||
|
--http-timeout-secs) HTTP_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-timeout-secs) RSYNC_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-mirror-root) RSYNC_MIRROR_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--max-depth) MAX_DEPTH="${2:?}"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="${2:?}"; shift 2 ;;
|
||||||
|
--trust-anchor) TRUST_ANCHOR="${2:?}"; shift 2 ;;
|
||||||
|
--bin-dir) BIN_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--no-build) NO_BUILD=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$RIR" || -z "$TAL_PATH" || -z "$TA_PATH" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "$OUT_DIR" ]]; then
|
||||||
|
OUT_DIR="target/replay/${RIR}_live_bundle_${TS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STAGE_BASE="${OUT_DIR}.stage-base"
|
||||||
|
STAGE_DELTA="${OUT_DIR}.stage-delta"
|
||||||
|
|
||||||
|
rm -rf "$OUT_DIR" "$STAGE_BASE" "$STAGE_DELTA"
|
||||||
|
mkdir -p "$(dirname "$OUT_DIR")"
|
||||||
|
|
||||||
|
CAPTURE_BIN="$BIN_DIR/replay_bundle_capture"
|
||||||
|
DELTA_CAPTURE_BIN="$BIN_DIR/replay_bundle_capture_delta"
|
||||||
|
|
||||||
|
if [[ "$NO_BUILD" -eq 0 ]]; then
|
||||||
|
echo "[1/3] build release binaries"
|
||||||
|
cargo build --release --bin replay_bundle_capture --bin replay_bundle_capture_delta
|
||||||
|
else
|
||||||
|
echo "[1/3] reuse existing binaries from $BIN_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$CAPTURE_BIN" ]]; then
|
||||||
|
echo "missing executable: $CAPTURE_BIN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$DELTA_CAPTURE_BIN" ]]; then
|
||||||
|
echo "missing executable: $DELTA_CAPTURE_BIN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[2/3] record live base bundle into $STAGE_BASE"
|
||||||
|
BASE_CMD=(
|
||||||
|
"$CAPTURE_BIN"
|
||||||
|
--rir "$RIR"
|
||||||
|
--out-dir "$STAGE_BASE"
|
||||||
|
--tal-path "$TAL_PATH"
|
||||||
|
--ta-path "$TA_PATH"
|
||||||
|
)
|
||||||
|
[[ -n "$BASE_VALIDATION_TIME" ]] && BASE_CMD+=(--validation-time "$BASE_VALIDATION_TIME")
|
||||||
|
[[ -n "$HTTP_TIMEOUT_SECS" ]] && BASE_CMD+=(--http-timeout-secs "$HTTP_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_TIMEOUT_SECS" ]] && BASE_CMD+=(--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_MIRROR_ROOT" ]] && BASE_CMD+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||||
|
[[ -n "$MAX_DEPTH" ]] && BASE_CMD+=(--max-depth "$MAX_DEPTH")
|
||||||
|
[[ -n "$MAX_INSTANCES" ]] && BASE_CMD+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
[[ -n "$TRUST_ANCHOR" ]] && BASE_CMD+=(--trust-anchor "$TRUST_ANCHOR")
|
||||||
|
"${BASE_CMD[@]}"
|
||||||
|
|
||||||
|
echo "[3/3] record live delta bundle into $STAGE_DELTA"
|
||||||
|
DELTA_CMD=(
|
||||||
|
"$DELTA_CAPTURE_BIN"
|
||||||
|
--rir "$RIR"
|
||||||
|
--base-bundle-dir "$STAGE_BASE"
|
||||||
|
--out-dir "$STAGE_DELTA"
|
||||||
|
)
|
||||||
|
[[ -n "$DELTA_VALIDATION_TIME" ]] && DELTA_CMD+=(--validation-time "$DELTA_VALIDATION_TIME")
|
||||||
|
[[ -n "$HTTP_TIMEOUT_SECS" ]] && DELTA_CMD+=(--http-timeout-secs "$HTTP_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_TIMEOUT_SECS" ]] && DELTA_CMD+=(--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_MIRROR_ROOT" ]] && DELTA_CMD+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||||
|
[[ -n "$MAX_DEPTH" ]] && DELTA_CMD+=(--max-depth "$MAX_DEPTH")
|
||||||
|
[[ -n "$MAX_INSTANCES" ]] && DELTA_CMD+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
[[ -n "$TRUST_ANCHOR" ]] && DELTA_CMD+=(--trust-anchor "$TRUST_ANCHOR")
|
||||||
|
"${DELTA_CMD[@]}"
|
||||||
|
|
||||||
|
mv "$STAGE_DELTA" "$OUT_DIR"
|
||||||
|
rm -rf "$STAGE_BASE"
|
||||||
|
|
||||||
|
echo "$OUT_DIR"
|
||||||
166
scripts/replay_bundle/run_live_bundle_record_multi_rir.sh
Executable file
166
scripts/replay_bundle/run_live_bundle_record_multi_rir.sh
Executable file
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
RIRS=""
|
||||||
|
OUT_ROOT=""
|
||||||
|
BASE_VALIDATION_TIME=""
|
||||||
|
DELTA_VALIDATION_TIME=""
|
||||||
|
HTTP_TIMEOUT_SECS=""
|
||||||
|
RSYNC_TIMEOUT_SECS=""
|
||||||
|
RSYNC_MIRROR_ROOT=""
|
||||||
|
MAX_DEPTH=""
|
||||||
|
MAX_INSTANCES=""
|
||||||
|
NO_BUILD=0
|
||||||
|
BIN_DIR="target/release"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_multi_rir.sh \
|
||||||
|
--rir <afrinic,apnic,...> \
|
||||||
|
[--out-root <path>] \
|
||||||
|
[--base-validation-time <rfc3339>] \
|
||||||
|
[--delta-validation-time <rfc3339>] \
|
||||||
|
[--http-timeout-secs <n>] \
|
||||||
|
[--rsync-timeout-secs <n>] \
|
||||||
|
[--rsync-mirror-root <path>] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>] \
|
||||||
|
[--bin-dir <path>] \
|
||||||
|
[--no-build]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir) RIRS="${2:?}"; shift 2 ;;
|
||||||
|
--out-root) OUT_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--base-validation-time) BASE_VALIDATION_TIME="${2:?}"; shift 2 ;;
|
||||||
|
--delta-validation-time) DELTA_VALIDATION_TIME="${2:?}"; shift 2 ;;
|
||||||
|
--http-timeout-secs) HTTP_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-timeout-secs) RSYNC_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-mirror-root) RSYNC_MIRROR_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--max-depth) MAX_DEPTH="${2:?}"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="${2:?}"; shift 2 ;;
|
||||||
|
--bin-dir) BIN_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--no-build) NO_BUILD=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$RIRS" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_TAG="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "$OUT_ROOT" ]]; then
|
||||||
|
OUT_ROOT="target/replay/live_bundle_matrix_${RUN_TAG}"
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUT_ROOT"
|
||||||
|
|
||||||
|
resolve_tal_path() {
|
||||||
|
case "$1" in
|
||||||
|
afrinic) printf 'tests/fixtures/tal/afrinic.tal' ;;
|
||||||
|
apnic) printf 'tests/fixtures/tal/apnic-rfc7730-https.tal' ;;
|
||||||
|
arin) printf 'tests/fixtures/tal/arin.tal' ;;
|
||||||
|
lacnic) printf 'tests/fixtures/tal/lacnic.tal' ;;
|
||||||
|
ripe) printf 'tests/fixtures/tal/ripe-ncc.tal' ;;
|
||||||
|
*) echo "unsupported rir: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_ta_path() {
|
||||||
|
case "$1" in
|
||||||
|
afrinic) printf 'tests/fixtures/ta/afrinic-ta.cer' ;;
|
||||||
|
apnic) printf 'tests/fixtures/ta/apnic-ta.cer' ;;
|
||||||
|
arin) printf 'tests/fixtures/ta/arin-ta.cer' ;;
|
||||||
|
lacnic) printf 'tests/fixtures/ta/lacnic-ta.cer' ;;
|
||||||
|
ripe) printf 'tests/fixtures/ta/ripe-ncc-ta.cer' ;;
|
||||||
|
*) echo "unsupported rir: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SUMMARY_JSON="$OUT_ROOT/summary.json"
|
||||||
|
SUMMARY_MD="$OUT_ROOT/summary.md"
|
||||||
|
|
||||||
|
python3 - "$SUMMARY_JSON" "$RUN_TAG" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
out, run_tag = sys.argv[1:]
|
||||||
|
with open(out, "w") as fh:
|
||||||
|
json.dump({"runTag": run_tag, "results": []}, fh, indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
IFS=',' read -r -a RIR_LIST <<< "$RIRS"
|
||||||
|
for raw_rir in "${RIR_LIST[@]}"; do
|
||||||
|
rir="$(printf '%s' "$raw_rir" | tr '[:upper:]' '[:lower:]' | xargs)"
|
||||||
|
[[ -n "$rir" ]] || continue
|
||||||
|
tal_path="$(resolve_tal_path "$rir")"
|
||||||
|
ta_path="$(resolve_ta_path "$rir")"
|
||||||
|
out_dir="$OUT_ROOT/${rir}_live_bundle_${RUN_TAG}"
|
||||||
|
cmd=(
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record.sh
|
||||||
|
--rir "$rir"
|
||||||
|
--out-dir "$out_dir"
|
||||||
|
--tal-path "$tal_path"
|
||||||
|
--ta-path "$ta_path"
|
||||||
|
--trust-anchor "$rir"
|
||||||
|
--bin-dir "$BIN_DIR"
|
||||||
|
)
|
||||||
|
[[ -n "$BASE_VALIDATION_TIME" ]] && cmd+=(--base-validation-time "$BASE_VALIDATION_TIME")
|
||||||
|
[[ -n "$DELTA_VALIDATION_TIME" ]] && cmd+=(--delta-validation-time "$DELTA_VALIDATION_TIME")
|
||||||
|
[[ -n "$HTTP_TIMEOUT_SECS" ]] && cmd+=(--http-timeout-secs "$HTTP_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_TIMEOUT_SECS" ]] && cmd+=(--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_MIRROR_ROOT" ]] && cmd+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||||
|
[[ -n "$MAX_DEPTH" ]] && cmd+=(--max-depth "$MAX_DEPTH")
|
||||||
|
[[ -n "$MAX_INSTANCES" ]] && cmd+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
[[ "$NO_BUILD" -eq 1 ]] && cmd+=(--no-build)
|
||||||
|
"${cmd[@]}"
|
||||||
|
|
||||||
|
python3 - "$SUMMARY_JSON" "$rir" "$out_dir" <<'PY'
|
||||||
|
import json, pathlib, sys
|
||||||
|
summary_path, rir, out_dir = sys.argv[1:]
|
||||||
|
summary = json.loads(pathlib.Path(summary_path).read_text())
|
||||||
|
bundle = json.loads(pathlib.Path(out_dir, rir, "bundle.json").read_text())
|
||||||
|
verification = json.loads(pathlib.Path(out_dir, rir, "verification.json").read_text())
|
||||||
|
summary["results"].append({
|
||||||
|
"rir": rir,
|
||||||
|
"outDir": out_dir,
|
||||||
|
"baseVrpCount": bundle["baseVrpCount"],
|
||||||
|
"deltaVrpCount": bundle["deltaVrpCount"],
|
||||||
|
"baseVapCount": bundle["baseVapCount"],
|
||||||
|
"deltaVapCount": bundle["deltaVapCount"],
|
||||||
|
"baseSelfReplayOk": verification["base"]["capture"]["selfReplayOk"],
|
||||||
|
"deltaSelfReplayOk": verification["delta"]["capture"]["selfReplayOk"],
|
||||||
|
})
|
||||||
|
pathlib.Path(summary_path).write_text(json.dumps(summary, indent=2))
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - "$SUMMARY_JSON" "$SUMMARY_MD" <<'PY'
|
||||||
|
import json, pathlib, sys
|
||||||
|
summary = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||||
|
out = pathlib.Path(sys.argv[2])
|
||||||
|
lines = [
|
||||||
|
"# Multi-RIR Live Bundle Record Summary",
|
||||||
|
"",
|
||||||
|
f"- runTag: `{summary['runTag']}`",
|
||||||
|
"",
|
||||||
|
"| rir | base_vrps | delta_vrps | base_vaps | delta_vaps | base_self_replay | delta_self_replay | out_dir |",
|
||||||
|
"|---|---:|---:|---:|---:|---|---|---|",
|
||||||
|
]
|
||||||
|
for item in summary["results"]:
|
||||||
|
lines.append(
|
||||||
|
f"| {item['rir']} | {item['baseVrpCount']} | {item['deltaVrpCount']} | "
|
||||||
|
f"{item['baseVapCount']} | {item['deltaVapCount']} | "
|
||||||
|
f"{str(item['baseSelfReplayOk']).lower()} | {str(item['deltaSelfReplayOk']).lower()} | "
|
||||||
|
f"`{item['outDir']}` |"
|
||||||
|
)
|
||||||
|
out.write_text("\n".join(lines) + "\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "$OUT_ROOT"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
RIRS=""
|
||||||
|
OUT_ROOT=""
|
||||||
|
BASE_VALIDATION_TIME=""
|
||||||
|
DELTA_COUNT=""
|
||||||
|
DELTA_INTERVAL_SECS=""
|
||||||
|
HTTP_TIMEOUT_SECS=""
|
||||||
|
RSYNC_TIMEOUT_SECS=""
|
||||||
|
RSYNC_MIRROR_ROOT=""
|
||||||
|
MAX_DEPTH=""
|
||||||
|
MAX_INSTANCES=""
|
||||||
|
NO_BUILD=0
|
||||||
|
KEEP_DB=0
|
||||||
|
CAPTURE_INPUTS_ONLY=0
|
||||||
|
BIN_DIR="target/release"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_multi_rir_sequence.sh \
|
||||||
|
--rir <afrinic,apnic,...> \
|
||||||
|
[--out-root <path>] \
|
||||||
|
[--base-validation-time <rfc3339>] \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--delta-interval-secs <n>] \
|
||||||
|
[--http-timeout-secs <n>] \
|
||||||
|
[--rsync-timeout-secs <n>] \
|
||||||
|
[--rsync-mirror-root <path>] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>] \
|
||||||
|
[--bin-dir <path>] \
|
||||||
|
[--no-build] \
|
||||||
|
[--keep-db] \
|
||||||
|
[--capture-inputs-only]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir) RIRS="${2:?}"; shift 2 ;;
|
||||||
|
--out-root) OUT_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--base-validation-time) BASE_VALIDATION_TIME="${2:?}"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="${2:?}"; shift 2 ;;
|
||||||
|
--delta-interval-secs) DELTA_INTERVAL_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--http-timeout-secs) HTTP_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-timeout-secs) RSYNC_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-mirror-root) RSYNC_MIRROR_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--max-depth) MAX_DEPTH="${2:?}"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="${2:?}"; shift 2 ;;
|
||||||
|
--bin-dir) BIN_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--no-build) NO_BUILD=1; shift ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--capture-inputs-only) CAPTURE_INPUTS_ONLY=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$RIRS" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_TAG="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "$OUT_ROOT" ]]; then
|
||||||
|
OUT_ROOT="target/replay/live_bundle_sequence_matrix_${RUN_TAG}"
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUT_ROOT"
|
||||||
|
|
||||||
|
resolve_tal_path() {
|
||||||
|
case "$1" in
|
||||||
|
afrinic) printf 'tests/fixtures/tal/afrinic.tal' ;;
|
||||||
|
apnic) printf 'tests/fixtures/tal/apnic-rfc7730-https.tal' ;;
|
||||||
|
arin) printf 'tests/fixtures/tal/arin.tal' ;;
|
||||||
|
lacnic) printf 'tests/fixtures/tal/lacnic.tal' ;;
|
||||||
|
ripe) printf 'tests/fixtures/tal/ripe-ncc.tal' ;;
|
||||||
|
*) echo "unsupported rir: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_ta_path() {
|
||||||
|
case "$1" in
|
||||||
|
afrinic) printf 'tests/fixtures/ta/afrinic-ta.cer' ;;
|
||||||
|
apnic) printf 'tests/fixtures/ta/apnic-ta.cer' ;;
|
||||||
|
arin) printf 'tests/fixtures/ta/arin-ta.cer' ;;
|
||||||
|
lacnic) printf 'tests/fixtures/ta/lacnic-ta.cer' ;;
|
||||||
|
ripe) printf 'tests/fixtures/ta/ripe-ncc-ta.cer' ;;
|
||||||
|
*) echo "unsupported rir: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SUMMARY_JSON="$OUT_ROOT/summary.json"
|
||||||
|
SUMMARY_MD="$OUT_ROOT/summary.md"
|
||||||
|
python3 - "$SUMMARY_JSON" "$RUN_TAG" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
path, run_tag = sys.argv[1:]
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
json.dump({"runTag": run_tag, "results": []}, fh, indent=2)
|
||||||
|
PY
|
||||||
|
|
||||||
|
IFS=',' read -r -a RIR_LIST <<< "$RIRS"
|
||||||
|
for raw_rir in "${RIR_LIST[@]}"; do
|
||||||
|
rir="$(printf '%s' "$raw_rir" | tr '[:upper:]' '[:lower:]' | xargs)"
|
||||||
|
[[ -n "$rir" ]] || continue
|
||||||
|
tal_path="$(resolve_tal_path "$rir")"
|
||||||
|
ta_path="$(resolve_ta_path "$rir")"
|
||||||
|
out_dir="$OUT_ROOT/${rir}_live_bundle_sequence_${RUN_TAG}"
|
||||||
|
cmd=(
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_sequence.sh
|
||||||
|
--rir "$rir"
|
||||||
|
--out-dir "$out_dir"
|
||||||
|
--tal-path "$tal_path"
|
||||||
|
--ta-path "$ta_path"
|
||||||
|
--trust-anchor "$rir"
|
||||||
|
--bin-dir "$BIN_DIR"
|
||||||
|
)
|
||||||
|
[[ -n "$BASE_VALIDATION_TIME" ]] && cmd+=(--base-validation-time "$BASE_VALIDATION_TIME")
|
||||||
|
[[ -n "$DELTA_COUNT" ]] && cmd+=(--delta-count "$DELTA_COUNT")
|
||||||
|
[[ -n "$DELTA_INTERVAL_SECS" ]] && cmd+=(--delta-interval-secs "$DELTA_INTERVAL_SECS")
|
||||||
|
[[ -n "$HTTP_TIMEOUT_SECS" ]] && cmd+=(--http-timeout-secs "$HTTP_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_TIMEOUT_SECS" ]] && cmd+=(--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_MIRROR_ROOT" ]] && cmd+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||||
|
[[ -n "$MAX_DEPTH" ]] && cmd+=(--max-depth "$MAX_DEPTH")
|
||||||
|
[[ -n "$MAX_INSTANCES" ]] && cmd+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
[[ "$NO_BUILD" -eq 1 ]] && cmd+=(--no-build)
|
||||||
|
[[ "$KEEP_DB" -eq 1 ]] && cmd+=(--keep-db)
|
||||||
|
[[ "$CAPTURE_INPUTS_ONLY" -eq 1 ]] && cmd+=(--capture-inputs-only)
|
||||||
|
"${cmd[@]}"
|
||||||
|
|
||||||
|
python3 - "$SUMMARY_JSON" "$rir" "$out_dir" <<'PY'
|
||||||
|
import json, pathlib, sys
|
||||||
|
summary_path, rir, out_dir = sys.argv[1:]
|
||||||
|
summary = json.loads(pathlib.Path(summary_path).read_text())
|
||||||
|
bundle = json.loads(pathlib.Path(out_dir, rir, "bundle.json").read_text())
|
||||||
|
verification = json.loads(pathlib.Path(out_dir, rir, "verification.json").read_text())
|
||||||
|
summary["results"].append({
|
||||||
|
"rir": rir,
|
||||||
|
"outDir": out_dir,
|
||||||
|
"stepCount": len(bundle["deltaSequence"]["steps"]),
|
||||||
|
"baseVrpCount": bundle["base"]["vrpCount"],
|
||||||
|
"baseVapCount": bundle["base"]["vapCount"],
|
||||||
|
"allStepsSelfReplayOk": verification["summary"]["allStepsSelfReplayOk"],
|
||||||
|
})
|
||||||
|
pathlib.Path(summary_path).write_text(json.dumps(summary, indent=2))
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - "$SUMMARY_JSON" "$SUMMARY_MD" <<'PY'
|
||||||
|
import json, pathlib, sys
|
||||||
|
summary = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||||
|
out = pathlib.Path(sys.argv[2])
|
||||||
|
lines = [
|
||||||
|
"# Multi-RIR Live Bundle Sequence Summary",
|
||||||
|
"",
|
||||||
|
f"- runTag: `{summary['runTag']}`",
|
||||||
|
"",
|
||||||
|
"| rir | step_count | base_vrps | base_vaps | all_steps_self_replay | out_dir |",
|
||||||
|
"|---|---:|---:|---:|---|---|",
|
||||||
|
]
|
||||||
|
for item in summary["results"]:
|
||||||
|
lines.append(
|
||||||
|
f"| {item['rir']} | {item['stepCount']} | {item['baseVrpCount']} | {item['baseVapCount']} | "
|
||||||
|
f"{str(item['allStepsSelfReplayOk']).lower()} | `{item['outDir']}` |"
|
||||||
|
)
|
||||||
|
out.write_text("\n".join(lines) + "\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "$OUT_ROOT"
|
||||||
119
scripts/replay_bundle/run_live_bundle_record_sequence.sh
Executable file
119
scripts/replay_bundle/run_live_bundle_record_sequence.sh
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
RIR=""
|
||||||
|
OUT_DIR=""
|
||||||
|
TAL_PATH=""
|
||||||
|
TA_PATH=""
|
||||||
|
BASE_VALIDATION_TIME=""
|
||||||
|
DELTA_COUNT=""
|
||||||
|
DELTA_INTERVAL_SECS=""
|
||||||
|
HTTP_TIMEOUT_SECS=""
|
||||||
|
RSYNC_TIMEOUT_SECS=""
|
||||||
|
RSYNC_MIRROR_ROOT=""
|
||||||
|
MAX_DEPTH=""
|
||||||
|
MAX_INSTANCES=""
|
||||||
|
TRUST_ANCHOR=""
|
||||||
|
NO_BUILD=0
|
||||||
|
KEEP_DB=0
|
||||||
|
CAPTURE_INPUTS_ONLY=0
|
||||||
|
BIN_DIR="target/release"
|
||||||
|
PROGRESS_LOG="${RPKI_PROGRESS_LOG:-1}"
|
||||||
|
PROGRESS_SLOW_SECS="${RPKI_PROGRESS_SLOW_SECS:-30}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/replay_bundle/run_live_bundle_record_sequence.sh \
|
||||||
|
--rir <name> \
|
||||||
|
--tal-path <path> \
|
||||||
|
--ta-path <path> \
|
||||||
|
[--out-dir <path>] \
|
||||||
|
[--base-validation-time <rfc3339>] \
|
||||||
|
[--delta-count <n>] \
|
||||||
|
[--delta-interval-secs <n>] \
|
||||||
|
[--http-timeout-secs <n>] \
|
||||||
|
[--rsync-timeout-secs <n>] \
|
||||||
|
[--rsync-mirror-root <path>] \
|
||||||
|
[--max-depth <n>] \
|
||||||
|
[--max-instances <n>] \
|
||||||
|
[--trust-anchor <name>] \
|
||||||
|
[--bin-dir <path>] \
|
||||||
|
[--no-build] \
|
||||||
|
[--keep-db] \
|
||||||
|
[--capture-inputs-only]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir) RIR="${2:?}"; shift 2 ;;
|
||||||
|
--out-dir) OUT_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--tal-path) TAL_PATH="${2:?}"; shift 2 ;;
|
||||||
|
--ta-path) TA_PATH="${2:?}"; shift 2 ;;
|
||||||
|
--base-validation-time) BASE_VALIDATION_TIME="${2:?}"; shift 2 ;;
|
||||||
|
--delta-count) DELTA_COUNT="${2:?}"; shift 2 ;;
|
||||||
|
--delta-interval-secs) DELTA_INTERVAL_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--http-timeout-secs) HTTP_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-timeout-secs) RSYNC_TIMEOUT_SECS="${2:?}"; shift 2 ;;
|
||||||
|
--rsync-mirror-root) RSYNC_MIRROR_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--max-depth) MAX_DEPTH="${2:?}"; shift 2 ;;
|
||||||
|
--max-instances) MAX_INSTANCES="${2:?}"; shift 2 ;;
|
||||||
|
--trust-anchor) TRUST_ANCHOR="${2:?}"; shift 2 ;;
|
||||||
|
--bin-dir) BIN_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--no-build) NO_BUILD=1; shift ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--capture-inputs-only) CAPTURE_INPUTS_ONLY=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$RIR" || -z "$TAL_PATH" || -z "$TA_PATH" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "$OUT_DIR" ]]; then
|
||||||
|
OUT_DIR="target/replay/${RIR}_live_bundle_sequence_${TS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SEQUENCE_BIN="$BIN_DIR/replay_bundle_capture_sequence"
|
||||||
|
if [[ "$NO_BUILD" -eq 0 ]]; then
|
||||||
|
echo "[1/1] build release binary"
|
||||||
|
cargo build --release --bin replay_bundle_capture_sequence
|
||||||
|
else
|
||||||
|
echo "[1/1] reuse existing binary from $BIN_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$SEQUENCE_BIN" ]]; then
|
||||||
|
echo "missing executable: $SEQUENCE_BIN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmd=(
|
||||||
|
"$SEQUENCE_BIN"
|
||||||
|
--rir "$RIR"
|
||||||
|
--out-dir "$OUT_DIR"
|
||||||
|
--tal-path "$TAL_PATH"
|
||||||
|
--ta-path "$TA_PATH"
|
||||||
|
)
|
||||||
|
[[ -n "$BASE_VALIDATION_TIME" ]] && cmd+=(--base-validation-time "$BASE_VALIDATION_TIME")
|
||||||
|
[[ -n "$DELTA_COUNT" ]] && cmd+=(--delta-count "$DELTA_COUNT")
|
||||||
|
[[ -n "$DELTA_INTERVAL_SECS" ]] && cmd+=(--delta-interval-secs "$DELTA_INTERVAL_SECS")
|
||||||
|
[[ -n "$HTTP_TIMEOUT_SECS" ]] && cmd+=(--http-timeout-secs "$HTTP_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_TIMEOUT_SECS" ]] && cmd+=(--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS")
|
||||||
|
[[ -n "$RSYNC_MIRROR_ROOT" ]] && cmd+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||||
|
[[ -n "$MAX_DEPTH" ]] && cmd+=(--max-depth "$MAX_DEPTH")
|
||||||
|
[[ -n "$MAX_INSTANCES" ]] && cmd+=(--max-instances "$MAX_INSTANCES")
|
||||||
|
[[ -n "$TRUST_ANCHOR" ]] && cmd+=(--trust-anchor "$TRUST_ANCHOR")
|
||||||
|
[[ "$KEEP_DB" -eq 1 ]] && cmd+=(--keep-db)
|
||||||
|
[[ "$CAPTURE_INPUTS_ONLY" -eq 1 ]] && cmd+=(--capture-inputs-only)
|
||||||
|
|
||||||
|
RPKI_PROGRESS_LOG="$PROGRESS_LOG" \
|
||||||
|
RPKI_PROGRESS_SLOW_SECS="$PROGRESS_SLOW_SECS" \
|
||||||
|
"${cmd[@]}"
|
||||||
67
scripts/replay_verify/README.md
Normal file
67
scripts/replay_verify/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Replay Verify Scripts
|
||||||
|
|
||||||
|
## `run_multi_rir_ccr_replay_verify.sh`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
- 通用 multi-RIR CCR replay verify 入口
|
||||||
|
- 通过 `--rir` 指定一个或多个 RIR,按顺序执行
|
||||||
|
- 通过 `--mode` 指定 `snapshot`、`delta` 或 `both`
|
||||||
|
- 默认每个 RIR 的 RocksDB 目录在 compare/verify 结束后自动删除;传 `--keep-db` 才保留
|
||||||
|
- 同一次执行的所有产物都会先落到 `rpki/target/replay/<timestamp>/`
|
||||||
|
- 该时间戳目录下再按 RIR 分目录:
|
||||||
|
- `<rir>_ccr_replay_<timestamp>`
|
||||||
|
|
||||||
|
默认输入:
|
||||||
|
- bundle root: `/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3`
|
||||||
|
- 每个 RIR 的 TAL / TA / validation time / record CSV 由 `scripts/payload_replay/multi_rir_case_info.py` 解析
|
||||||
|
|
||||||
|
用法:
|
||||||
|
- 单个 RIR:
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir apnic --mode both`
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir apnic --mode snapshot`
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir apnic --mode delta`
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir apnic --mode both`
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir apnic,ripe --mode snapshot`
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir afrinic,apnic,arin,lacnic,ripe --mode both`
|
||||||
|
- `./scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh --rir apnic --mode delta --keep-db`
|
||||||
|
|
||||||
|
可覆盖环境变量:
|
||||||
|
- `BUNDLE_ROOT`
|
||||||
|
- `OUT_ROOT`(默认:`rpki/target/replay`)
|
||||||
|
- `RUN_TAG`
|
||||||
|
|
||||||
|
主要产物:
|
||||||
|
- 单次执行根目录:
|
||||||
|
- `rpki/target/replay/<timestamp>/`
|
||||||
|
- 每个 RIR 子目录下:
|
||||||
|
- `<rir>_snapshot.ccr`
|
||||||
|
- `<rir>_delta.ccr`
|
||||||
|
- `<rir>_*_report.json`
|
||||||
|
- `<rir>_*_ccr_vrps.csv`
|
||||||
|
- `<rir>_*_ccr_compare_summary.md`
|
||||||
|
- `<rir>_*_ccr_verify.json`
|
||||||
|
- 同次执行总汇总:
|
||||||
|
- `multi_rir_ccr_replay_verify_<timestamp>_summary.md`
|
||||||
|
- `multi_rir_ccr_replay_verify_<timestamp>_summary.json`
|
||||||
|
|
||||||
|
## `run_peer_bundle_matrix.sh`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
- 对一组 `ours live bundle` 做本地 peer replay 矩阵验证
|
||||||
|
- Routinator 与 `rpki-client` 分别消费相同 bundle root
|
||||||
|
- 汇总 `VRP + VAP` 的 base / delta 结果
|
||||||
|
|
||||||
|
用法:
|
||||||
|
- `./scripts/replay_verify/run_peer_bundle_matrix.sh --bundle-root target/replay/live_bundle_matrix_<timestamp>`
|
||||||
|
- `./scripts/replay_verify/run_peer_bundle_matrix.sh --bundle-root target/replay/live_bundle_matrix_<timestamp> --rir apnic,ripe`
|
||||||
|
|
||||||
|
主要产物:
|
||||||
|
- 输出根目录:
|
||||||
|
- `target/replay/peer_bundle_matrix_<timestamp>/`
|
||||||
|
- Routinator:
|
||||||
|
- `target/replay/peer_bundle_matrix_<timestamp>/routinator/<rir>/`
|
||||||
|
- `rpki-client`:
|
||||||
|
- `target/replay/peer_bundle_matrix_<timestamp>/rpki-client/`
|
||||||
|
- 汇总:
|
||||||
|
- `summary.json`
|
||||||
|
- `summary.md`
|
||||||
284
scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh
Executable file
284
scripts/replay_verify/run_multi_rir_ccr_replay_verify.sh
Executable file
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
run_multi_rir_ccr_replay_verify.sh --rir <rir[,rir...]> [--mode snapshot|delta|both] [--keep-db]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--rir <list> Comma-separated RIR list, e.g. apnic or apnic,ripe
|
||||||
|
--mode <mode> snapshot | delta | both (default: both)
|
||||||
|
--keep-db Keep per-run RocksDB directories (default: remove after verify)
|
||||||
|
--bundle-root <p> Override bundle root
|
||||||
|
--out-root <p> Override output root (default: rpki/target/replay)
|
||||||
|
--run-tag <tag> Override timestamp suffix for all RIR runs
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE="both"
|
||||||
|
KEEP_DB=0
|
||||||
|
RIR_LIST=""
|
||||||
|
BUNDLE_ROOT="${BUNDLE_ROOT:-/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3}"
|
||||||
|
OUT_ROOT="${OUT_ROOT:-$ROOT_DIR/target/replay}"
|
||||||
|
RUN_TAG="${RUN_TAG:-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--rir)
|
||||||
|
shift
|
||||||
|
RIR_LIST="${1:-}"
|
||||||
|
;;
|
||||||
|
--mode)
|
||||||
|
shift
|
||||||
|
MODE="${1:-}"
|
||||||
|
;;
|
||||||
|
--keep-db)
|
||||||
|
KEEP_DB=1
|
||||||
|
;;
|
||||||
|
--bundle-root)
|
||||||
|
shift
|
||||||
|
BUNDLE_ROOT="${1:-}"
|
||||||
|
;;
|
||||||
|
--out-root)
|
||||||
|
shift
|
||||||
|
OUT_ROOT="${1:-}"
|
||||||
|
;;
|
||||||
|
--run-tag)
|
||||||
|
shift
|
||||||
|
RUN_TAG="${1:-}"
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift || true
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$RIR_LIST" ]]; then
|
||||||
|
echo "--rir is required" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
case "$MODE" in
|
||||||
|
snapshot|delta|both) ;;
|
||||||
|
*)
|
||||||
|
echo "invalid --mode: $MODE" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CASE_INFO_SCRIPT="$ROOT_DIR/scripts/payload_replay/multi_rir_case_info.py"
|
||||||
|
mkdir -p "$OUT_ROOT"
|
||||||
|
RUN_ROOT="$OUT_ROOT/$RUN_TAG"
|
||||||
|
mkdir -p "$RUN_ROOT"
|
||||||
|
|
||||||
|
cargo build --release --bin rpki --bin ccr_to_routinator_csv --bin ccr_verify >/dev/null
|
||||||
|
|
||||||
|
summary_md="$RUN_ROOT/multi_rir_ccr_replay_verify_${RUN_TAG}_summary.md"
|
||||||
|
summary_json="$RUN_ROOT/multi_rir_ccr_replay_verify_${RUN_TAG}_summary.json"
|
||||||
|
|
||||||
|
python3 - <<'PY' >/dev/null
|
||||||
|
PY
|
||||||
|
|
||||||
|
summary_json_tmp="$(mktemp)"
|
||||||
|
printf '[]' > "$summary_json_tmp"
|
||||||
|
|
||||||
|
run_one_mode() {
|
||||||
|
local rir="$1"
|
||||||
|
local mode="$2"
|
||||||
|
local run_dir="$3"
|
||||||
|
local trust_anchor="$4"
|
||||||
|
local tal_path="$5"
|
||||||
|
local ta_path="$6"
|
||||||
|
local base_archive="$7"
|
||||||
|
local base_locks="$8"
|
||||||
|
local base_csv="$9"
|
||||||
|
local delta_archive="${10}"
|
||||||
|
local delta_locks="${11}"
|
||||||
|
local delta_csv="${12}"
|
||||||
|
local snapshot_validation_time="${13}"
|
||||||
|
local delta_validation_time="${14}"
|
||||||
|
|
||||||
|
local db_dir="$run_dir/${rir}_${mode}_db"
|
||||||
|
local report_json="$run_dir/${rir}_${mode}_report.json"
|
||||||
|
local run_log="$run_dir/${rir}_${mode}_run.log"
|
||||||
|
local ccr_path="$run_dir/${rir}_${mode}.ccr"
|
||||||
|
local csv_path="$run_dir/${rir}_${mode}_ccr_vrps.csv"
|
||||||
|
local compare_md="$run_dir/${rir}_${mode}_ccr_compare_summary.md"
|
||||||
|
local only_ours="$run_dir/${rir}_${mode}_ccr_only_in_ours.csv"
|
||||||
|
local only_record="$run_dir/${rir}_${mode}_ccr_only_in_record.csv"
|
||||||
|
local verify_json="$run_dir/${rir}_${mode}_ccr_verify.json"
|
||||||
|
local meta_json="$run_dir/${rir}_${mode}_meta.json"
|
||||||
|
|
||||||
|
rm -rf "$db_dir"
|
||||||
|
|
||||||
|
local -a cmd=(target/release/rpki --db "$db_dir" --tal-path "$tal_path" --ta-path "$ta_path")
|
||||||
|
if [[ "$mode" == "snapshot" ]]; then
|
||||||
|
cmd+=(--payload-replay-archive "$base_archive" --payload-replay-locks "$base_locks" --validation-time "$snapshot_validation_time")
|
||||||
|
else
|
||||||
|
cmd+=(
|
||||||
|
--payload-base-archive "$base_archive"
|
||||||
|
--payload-base-locks "$base_locks"
|
||||||
|
--payload-base-validation-time "$snapshot_validation_time"
|
||||||
|
--payload-delta-archive "$delta_archive"
|
||||||
|
--payload-delta-locks "$delta_locks"
|
||||||
|
--validation-time "$delta_validation_time"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
cmd+=(--report-json "$report_json" --ccr-out "$ccr_path")
|
||||||
|
|
||||||
|
local start_s end_s duration_s
|
||||||
|
start_s="$(date +%s)"
|
||||||
|
(
|
||||||
|
echo "# ${rir} ${mode} command:"
|
||||||
|
printf '%q ' "${cmd[@]}"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
"${cmd[@]}"
|
||||||
|
) 2>&1 | tee "$run_log" >/dev/null
|
||||||
|
end_s="$(date +%s)"
|
||||||
|
duration_s="$((end_s - start_s))"
|
||||||
|
|
||||||
|
target/release/ccr_to_routinator_csv \
|
||||||
|
--ccr "$ccr_path" \
|
||||||
|
--out "$csv_path" \
|
||||||
|
--trust-anchor "$trust_anchor" >/dev/null
|
||||||
|
|
||||||
|
local record_csv
|
||||||
|
if [[ "$mode" == "snapshot" ]]; then
|
||||||
|
record_csv="$base_csv"
|
||||||
|
else
|
||||||
|
record_csv="$delta_csv"
|
||||||
|
fi
|
||||||
|
./scripts/payload_replay/compare_with_routinator_record.sh \
|
||||||
|
"$csv_path" \
|
||||||
|
"$record_csv" \
|
||||||
|
"$compare_md" \
|
||||||
|
"$only_ours" \
|
||||||
|
"$only_record" >/dev/null
|
||||||
|
|
||||||
|
target/release/ccr_verify \
|
||||||
|
--ccr "$ccr_path" \
|
||||||
|
--db "$db_dir" > "$verify_json"
|
||||||
|
|
||||||
|
python3 - "$report_json" "$meta_json" "$mode" "$duration_s" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
report = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
|
||||||
|
meta = {
|
||||||
|
'mode': sys.argv[3],
|
||||||
|
'duration_seconds': int(sys.argv[4]),
|
||||||
|
'validation_time': report.get('validation_time_rfc3339_utc'),
|
||||||
|
'publication_points_processed': report['tree']['instances_processed'],
|
||||||
|
'publication_points_failed': report['tree']['instances_failed'],
|
||||||
|
'vrps': len(report['vrps']),
|
||||||
|
'aspas': len(report['aspas']),
|
||||||
|
}
|
||||||
|
Path(sys.argv[2]).write_text(json.dumps(meta, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ "$KEEP_DB" -eq 0 ]]; then
|
||||||
|
rm -rf "$db_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
IFS=',' read -r -a RIRS <<< "$RIR_LIST"
|
||||||
|
for rir in "${RIRS[@]}"; do
|
||||||
|
rir="$(echo "$rir" | xargs)"
|
||||||
|
[[ -n "$rir" ]] || continue
|
||||||
|
eval "$(python3 "$CASE_INFO_SCRIPT" --bundle-root "$BUNDLE_ROOT" --rir "$rir" --format env)"
|
||||||
|
|
||||||
|
run_dir="$RUN_ROOT/${rir}_ccr_replay_${RUN_TAG}"
|
||||||
|
mkdir -p "$run_dir"
|
||||||
|
|
||||||
|
if [[ "$MODE" == "snapshot" || "$MODE" == "both" ]]; then
|
||||||
|
run_one_mode \
|
||||||
|
"$rir" snapshot "$run_dir" "$TRUST_ANCHOR" "$TAL_PATH" "$TA_PATH" \
|
||||||
|
"$PAYLOAD_REPLAY_ARCHIVE" "$PAYLOAD_REPLAY_LOCKS" "$ROUTINATOR_BASE_RECORD_CSV" \
|
||||||
|
"$PAYLOAD_DELTA_ARCHIVE" "$PAYLOAD_DELTA_LOCKS" "$ROUTINATOR_DELTA_RECORD_CSV" \
|
||||||
|
"$SNAPSHOT_VALIDATION_TIME" "$DELTA_VALIDATION_TIME"
|
||||||
|
fi
|
||||||
|
if [[ "$MODE" == "delta" || "$MODE" == "both" ]]; then
|
||||||
|
run_one_mode \
|
||||||
|
"$rir" delta "$run_dir" "$TRUST_ANCHOR" "$TAL_PATH" "$TA_PATH" \
|
||||||
|
"$PAYLOAD_REPLAY_ARCHIVE" "$PAYLOAD_REPLAY_LOCKS" "$ROUTINATOR_BASE_RECORD_CSV" \
|
||||||
|
"$PAYLOAD_DELTA_ARCHIVE" "$PAYLOAD_DELTA_LOCKS" "$ROUTINATOR_DELTA_RECORD_CSV" \
|
||||||
|
"$SNAPSHOT_VALIDATION_TIME" "$DELTA_VALIDATION_TIME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$summary_json_tmp" "$run_dir" "$rir" "$MODE" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
summary_path = Path(sys.argv[1])
|
||||||
|
run_dir = Path(sys.argv[2])
|
||||||
|
rir = sys.argv[3]
|
||||||
|
mode = sys.argv[4]
|
||||||
|
rows = json.loads(summary_path.read_text(encoding='utf-8'))
|
||||||
|
for submode in ['snapshot','delta']:
|
||||||
|
if mode not in ('both', submode):
|
||||||
|
continue
|
||||||
|
compare = run_dir / f'{rir}_{submode}_ccr_compare_summary.md'
|
||||||
|
meta = run_dir / f'{rir}_{submode}_meta.json'
|
||||||
|
verify = run_dir / f'{rir}_{submode}_ccr_verify.json'
|
||||||
|
if not compare.exists() or not meta.exists() or not verify.exists():
|
||||||
|
continue
|
||||||
|
compare_text = compare.read_text(encoding='utf-8')
|
||||||
|
meta_obj = json.loads(meta.read_text(encoding='utf-8'))
|
||||||
|
verify_obj = json.loads(verify.read_text(encoding='utf-8'))
|
||||||
|
def metric(name):
|
||||||
|
prefix = f'| {name} | '
|
||||||
|
for line in compare_text.splitlines():
|
||||||
|
if line.startswith(prefix):
|
||||||
|
return int(line.split('|')[2].strip())
|
||||||
|
raise SystemExit(f'missing metric {name} in {compare}')
|
||||||
|
rows.append({
|
||||||
|
'rir': rir,
|
||||||
|
'mode': submode,
|
||||||
|
'run_dir': str(run_dir),
|
||||||
|
'duration_seconds': meta_obj['duration_seconds'],
|
||||||
|
'vrps': meta_obj['vrps'],
|
||||||
|
'aspas': meta_obj['aspas'],
|
||||||
|
'only_in_ours': metric('only_in_ours'),
|
||||||
|
'only_in_record': metric('only_in_record'),
|
||||||
|
'intersection': metric('intersection'),
|
||||||
|
'state_hashes_ok': verify_obj.get('state_hashes_ok'),
|
||||||
|
})
|
||||||
|
summary_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
||||||
|
PY
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - "$summary_json_tmp" "$summary_json" "$summary_md" "$RUN_TAG" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
rows = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
|
||||||
|
out_json = Path(sys.argv[2])
|
||||||
|
out_md = Path(sys.argv[3])
|
||||||
|
run_tag = sys.argv[4]
|
||||||
|
out_json.write_text(json.dumps(rows, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
||||||
|
parts = []
|
||||||
|
parts.append('# Multi-RIR CCR Replay Verify Summary\n\n')
|
||||||
|
parts.append(f'- run_tag: `{run_tag}`\n\n')
|
||||||
|
parts.append('| rir | mode | duration_s | vrps | aspas | only_in_ours | only_in_record | state_hashes_ok |\n')
|
||||||
|
parts.append('|---|---|---:|---:|---:|---:|---:|---|\n')
|
||||||
|
for row in rows:
|
||||||
|
parts.append(f"| {row['rir']} | {row['mode']} | {row['duration_seconds']} | {row['vrps']} | {row['aspas']} | {row['only_in_ours']} | {row['only_in_record']} | {row['state_hashes_ok']} |\n")
|
||||||
|
out_md.write_text(''.join(parts), encoding='utf-8')
|
||||||
|
PY
|
||||||
|
|
||||||
|
rm -f "$summary_json_tmp"
|
||||||
|
|
||||||
|
echo "== multi-rir replay verify complete ==" >&2
|
||||||
|
echo "- summary: $summary_md" >&2
|
||||||
|
echo "- summary json: $summary_json" >&2
|
||||||
210
scripts/replay_verify/run_peer_bundle_matrix.sh
Executable file
210
scripts/replay_verify/run_peer_bundle_matrix.sh
Executable file
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
BUNDLE_ROOT=""
|
||||||
|
RIRS=""
|
||||||
|
OUT_ROOT=""
|
||||||
|
ROUTINATOR_ROOT="/home/yuyr/dev/rust_playground/routinator"
|
||||||
|
RPKI_CLIENT_ROOT="/home/yuyr/dev/rpki-client-9.7"
|
||||||
|
RPKI_CLIENT_BUILD_DIR="/home/yuyr/dev/rpki-client-9.7/build-m5"
|
||||||
|
KEEP_DB=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/replay_verify/run_peer_bundle_matrix.sh \
|
||||||
|
--bundle-root <dir> \
|
||||||
|
[--rir <afrinic,apnic,...>] \
|
||||||
|
[--out-root <dir>] \
|
||||||
|
[--routinator-root <dir>] \
|
||||||
|
[--rpki-client-root <dir>] \
|
||||||
|
[--rpki-client-build-dir <dir>] \
|
||||||
|
[--keep-db]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--bundle-root) BUNDLE_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--rir) RIRS="${2:?}"; shift 2 ;;
|
||||||
|
--out-root) OUT_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--routinator-root) ROUTINATOR_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--rpki-client-root) RPKI_CLIENT_ROOT="${2:?}"; shift 2 ;;
|
||||||
|
--rpki-client-build-dir) RPKI_CLIENT_BUILD_DIR="${2:?}"; shift 2 ;;
|
||||||
|
--keep-db) KEEP_DB=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$BUNDLE_ROOT" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUNDLE_ROOT="$(python3 - "$BUNDLE_ROOT" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
RUN_TAG="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "$OUT_ROOT" ]]; then
|
||||||
|
OUT_ROOT="target/replay/peer_bundle_matrix_${RUN_TAG}"
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUT_ROOT"
|
||||||
|
OUT_ROOT="$(python3 - "$OUT_ROOT" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
print(Path(sys.argv[1]).resolve())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
discover_rirs() {
|
||||||
|
python3 - "$BUNDLE_ROOT" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
root = Path(sys.argv[1])
|
||||||
|
if (root / "base-locks.json").exists():
|
||||||
|
print(root.name)
|
||||||
|
raise SystemExit
|
||||||
|
rirs = []
|
||||||
|
for entry in sorted(root.iterdir()):
|
||||||
|
if not entry.is_dir():
|
||||||
|
continue
|
||||||
|
if (entry / "base-locks.json").exists():
|
||||||
|
rirs.append(entry.name)
|
||||||
|
continue
|
||||||
|
nested = sorted(
|
||||||
|
child.name for child in entry.iterdir()
|
||||||
|
if child.is_dir() and (child / "base-locks.json").exists()
|
||||||
|
)
|
||||||
|
if len(nested) == 1:
|
||||||
|
rirs.append(nested[0])
|
||||||
|
print(",".join(rirs))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$RIRS" ]]; then
|
||||||
|
RIRS="$(discover_rirs)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROUTI_OUT="$OUT_ROOT/routinator"
|
||||||
|
CLIENT_OUT="$OUT_ROOT/rpki-client"
|
||||||
|
NORMALIZED_BUNDLE_ROOT="$OUT_ROOT/.normalized-bundle-root"
|
||||||
|
mkdir -p "$ROUTI_OUT" "$CLIENT_OUT"
|
||||||
|
rm -rf "$NORMALIZED_BUNDLE_ROOT"
|
||||||
|
mkdir -p "$NORMALIZED_BUNDLE_ROOT"
|
||||||
|
|
||||||
|
IFS=',' read -r -a RIR_LIST <<< "$RIRS"
|
||||||
|
for raw_rir in "${RIR_LIST[@]}"; do
|
||||||
|
rir="$(printf '%s' "$raw_rir" | tr '[:upper:]' '[:lower:]' | xargs)"
|
||||||
|
[[ -n "$rir" ]] || continue
|
||||||
|
source_bundle_dir=""
|
||||||
|
if [[ -d "$BUNDLE_ROOT/$rir" && -f "$BUNDLE_ROOT/$rir/base-locks.json" ]]; then
|
||||||
|
source_bundle_dir="$BUNDLE_ROOT/$rir"
|
||||||
|
else
|
||||||
|
match="$(find "$BUNDLE_ROOT" -maxdepth 2 -type d -path "*/${rir}" -exec test -f '{}/base-locks.json' ';' -print | head -n 1)"
|
||||||
|
if [[ -z "$match" ]]; then
|
||||||
|
echo "unable to resolve bundle directory for RIR: $rir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source_bundle_dir="$match"
|
||||||
|
fi
|
||||||
|
ln -sfn "$source_bundle_dir" "$NORMALIZED_BUNDLE_ROOT/$rir"
|
||||||
|
ROUTI_CMD=(
|
||||||
|
"$ROUTINATOR_ROOT/bench/multi_rir_demo_ours/run_single_rir_ours_bundle.sh"
|
||||||
|
"$source_bundle_dir"
|
||||||
|
"$ROUTI_OUT/$rir"
|
||||||
|
)
|
||||||
|
[[ "$KEEP_DB" -eq 1 ]] && ROUTI_CMD=( "$ROUTINATOR_ROOT/bench/multi_rir_demo_ours/run_single_rir_ours_bundle.sh" --keep-db "$source_bundle_dir" "$ROUTI_OUT/$rir" )
|
||||||
|
"${ROUTI_CMD[@]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
CLIENT_ARGS=(
|
||||||
|
python3 "$RPKI_CLIENT_ROOT/tools/run_bundle_matrix.py"
|
||||||
|
--bundle-dir "$NORMALIZED_BUNDLE_ROOT"
|
||||||
|
--build-dir "$RPKI_CLIENT_BUILD_DIR"
|
||||||
|
--work-dir "$CLIENT_OUT"
|
||||||
|
)
|
||||||
|
[[ "$KEEP_DB" -eq 1 ]] && CLIENT_ARGS+=(--keep-db)
|
||||||
|
for raw_rir in "${RIR_LIST[@]}"; do
|
||||||
|
rir="$(printf '%s' "$raw_rir" | tr '[:upper:]' '[:lower:]' | xargs)"
|
||||||
|
[[ -n "$rir" ]] || continue
|
||||||
|
CLIENT_ARGS+=(--rir "$rir")
|
||||||
|
done
|
||||||
|
"${CLIENT_ARGS[@]}"
|
||||||
|
|
||||||
|
SUMMARY_JSON="$OUT_ROOT/summary.json"
|
||||||
|
SUMMARY_MD="$OUT_ROOT/summary.md"
|
||||||
|
|
||||||
|
python3 - "$ROUTI_OUT" "$CLIENT_OUT/matrix-summary.json" "$SUMMARY_JSON" <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
routi_root = Path(sys.argv[1])
|
||||||
|
client_summary = json.loads(Path(sys.argv[2]).read_text())
|
||||||
|
summary_path = Path(sys.argv[3])
|
||||||
|
summary = {"routinator": {}, "rpki_client": client_summary}
|
||||||
|
for verification in sorted(routi_root.glob("*/verification.json")):
|
||||||
|
rir = verification.parent.name
|
||||||
|
summary["routinator"][rir] = json.loads(verification.read_text())
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2))
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 - "$SUMMARY_JSON" "$SUMMARY_MD" <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
summary = json.loads(Path(sys.argv[1]).read_text())
|
||||||
|
out = Path(sys.argv[2])
|
||||||
|
lines = [
|
||||||
|
"# Peer Bundle Matrix Summary",
|
||||||
|
"",
|
||||||
|
"## Routinator",
|
||||||
|
"",
|
||||||
|
"| rir | base_vrp | base_vap | sequence_vrp | sequence_vap |",
|
||||||
|
"|---|---|---|---|---|",
|
||||||
|
]
|
||||||
|
for rir, data in sorted(summary["routinator"].items()):
|
||||||
|
if "steps" in data:
|
||||||
|
lines.append(
|
||||||
|
f"| {rir} | {str(data.get('baseMatch')).lower()} | {str(data.get('baseVapsMatch')).lower()} | "
|
||||||
|
f"{str(data.get('summary', {}).get('allStepsMatch')).lower()} | "
|
||||||
|
f"{str(data.get('summary', {}).get('allStepsVapsMatch')).lower()} |"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f"| {rir} | {str(data.get('baseMatch')).lower()} | {str(data.get('baseVapsMatch')).lower()} | "
|
||||||
|
f"{str(data.get('deltaMatch')).lower()} | {str(data.get('deltaVapsMatch')).lower()} |"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"## rpki-client",
|
||||||
|
"",
|
||||||
|
"| rir | base_vrp | base_vap | sequence_vrp | sequence_vap |",
|
||||||
|
"|---|---|---|---|---|",
|
||||||
|
]
|
||||||
|
for rir, phases in sorted(summary["rpki_client"].items()):
|
||||||
|
base = phases.get("base", {})
|
||||||
|
step_items = [
|
||||||
|
value for key, value in phases.items()
|
||||||
|
if key not in ("base", "delta") and isinstance(value, dict)
|
||||||
|
]
|
||||||
|
if "delta" in phases:
|
||||||
|
step_items.append(phases["delta"])
|
||||||
|
all_step_match = all(item.get("match") for item in step_items) if step_items else None
|
||||||
|
all_step_vap_match = all(item.get("vaps_match") for item in step_items) if step_items else None
|
||||||
|
lines.append(
|
||||||
|
f"| {rir} | {str(base.get('match')).lower()} | {str(base.get('vaps_match')).lower()} | "
|
||||||
|
f"{str(all_step_match).lower()} | {str(all_step_vap_match).lower()} |"
|
||||||
|
)
|
||||||
|
out.write_text("\n".join(lines) + "\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "$OUT_ROOT"
|
||||||
4034
specs/cir.excalidraw
Normal file
4034
specs/cir.excalidraw
Normal file
File diff suppressed because it is too large
Load Diff
196
specs/cir_draft.md
Normal file
196
specs/cir_draft.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Internet-Draft** Yirong Yu
|
||||||
|
**Intended status: Standards Track** Zhongguancun Labortary
|
||||||
|
**Expires: [Date, e.g., October 2026]** April 2026
|
||||||
|
|
||||||
|
# A Profile for Resource Public Key Infrastructure (RPKI) Canonical Input Representation (CIR)
|
||||||
|
## draft-yu-sidrops-rpki-cir-00
|
||||||
|
|
||||||
|
### Abstract
|
||||||
|
|
||||||
|
This document specifies a Canonical Input Representation (CIR) content type for use with the Resource Public Key Infrastructure (RPKI). While the Canonical Cache Representation (CCR) profiles the *validated* output state of a Relying Party (RP), CIR is a DER-encoded data interchange format used to represent the exact, *unvalidated* raw input data fetched by an RP at a particular point in time. The CIR profile provides a deterministic "world view" snapshot, enabling advanced operational capabilities such as differential testing, failure path debugging, and highly accurate historical black-box replay of RPKI validation logic.
|
||||||
|
|
||||||
|
### Status of This Memo
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
### Table of Contents
|
||||||
|
|
||||||
|
1. Introduction
|
||||||
|
1.1. Requirements Language
|
||||||
|
2. Motivation and Architecture
|
||||||
|
3. The Canonical Input Representation Content Type
|
||||||
|
4. The Canonical Input Representation Content
|
||||||
|
4.1. version
|
||||||
|
4.2. metaInfo
|
||||||
|
4.3. BaseCIR Fields
|
||||||
|
4.4. DeltaCIR Fields
|
||||||
|
5. Operational Considerations
|
||||||
|
5.1. Differential Testing and Historical Replay
|
||||||
|
5.2. Delta Compression for Archival
|
||||||
|
6. Security Considerations
|
||||||
|
7. IANA Considerations
|
||||||
|
8. References
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
|
||||||
|
This document specifies a Canonical Input Representation (CIR) content type for use with the Resource Public Key Infrastructure (RPKI).
|
||||||
|
|
||||||
|
A Relying Party (RP) fetches RPKI objects from publication points using protocols such as rsync [RFC5781] or RRDP [RFC8182] prior to executing cryptographic validation. While the Canonical Cache Representation (CCR) [draft-ietf-sidrops-rpki-ccr] accurately describes the subset of objects that successfully passed validation, it inherently omits objects that were rejected due to format errors, invalid signatures, or expired timestamps (survivorship bias).
|
||||||
|
|
||||||
|
CIR records the precise mapping of object URIs to their cryptographic hashes *before* validation occurs. By decoupling the network transport layer from the validation layer, CIR allows researchers and operators to reconstruct the exact physical file tree (the "dirty inputs") perceived by a vantage point.
|
||||||
|
|
||||||
|
#### 1.1. Requirements Language
|
||||||
|
|
||||||
|
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
|
||||||
|
|
||||||
|
### 2. Motivation and Architecture
|
||||||
|
|
||||||
|
CIR is designed to solve the "time paradox" and "state desynchronization" problems inherent to RPKI historical archiving. It defines two distinct operational modes:
|
||||||
|
|
||||||
|
* **Base CIR**: A complete snapshot of all fetched Trust Anchor Locators (TALs) and RPKI objects, typically generated by an RP immediately after a synchronization cycle.
|
||||||
|
* **Delta CIR**: A compressed representation generated by offline archival processes, describing the additions, modifications, and deletions between two chronological Base CIR snapshots.
|
||||||
|
|
||||||
|
### 3. The Canonical Input Representation Content Type
|
||||||
|
|
||||||
|
The content of a CIR file is an instance of `ContentInfo`.
|
||||||
|
|
||||||
|
The `contentType` for a CIR is defined as `id-ct-rpkiCanonicalInputRepresentation`, with Object Identifier (OID) `[TBD-OID]`.
|
||||||
|
|
||||||
|
The content is an instance of `RpkiCanonicalInputRepresentation`.
|
||||||
|
|
||||||
|
### 4. The Canonical Input Representation Content
|
||||||
|
|
||||||
|
The content of a Canonical Input Representation is formally defined using ASN.1. To ensure absolute deterministic serialization, CIR MUST be encoded using Distinguished Encoding Rules (DER, [X.690]).
|
||||||
|
|
||||||
|
```asn.1
|
||||||
|
RpkiCanonicalInputRepresentation-2026
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549)
|
||||||
|
pkcs(1) pkcs9(9) smime(16) mod(0) id-mod-rpkiCIR-2026(TBD) }
|
||||||
|
|
||||||
|
DEFINITIONS EXPLICIT TAGS ::=
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
IMPORTS
|
||||||
|
CONTENT-TYPE, Digest
|
||||||
|
FROM CryptographicMessageSyntax-2010 -- in [RFC6268]
|
||||||
|
;
|
||||||
|
|
||||||
|
ContentInfo ::= SEQUENCE {
|
||||||
|
contentType CONTENT-TYPE.&id({ContentSet}),
|
||||||
|
content [0] EXPLICIT CONTENT-TYPE.&Type({ContentSet}{@contentType}) }
|
||||||
|
|
||||||
|
ContentSet CONTENT-TYPE ::= {
|
||||||
|
ct-rpkiCanonicalInputRepresentation, ... }
|
||||||
|
|
||||||
|
ct-rpkiCanonicalInputRepresentation CONTENT-TYPE ::=
|
||||||
|
{ TYPE RpkiCanonicalInputRepresentation
|
||||||
|
IDENTIFIED BY id-ct-rpkiCanonicalInputRepresentation }
|
||||||
|
|
||||||
|
id-ct-rpkiCanonicalInputRepresentation OBJECT IDENTIFIER ::=
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||||
|
pkcs-9(9) id-smime(16) id-ct(1) cir(TBD) }
|
||||||
|
|
||||||
|
RpkiCanonicalInputRepresentation ::= CHOICE {
|
||||||
|
baseCIR [0] BaseCIR,
|
||||||
|
deltaCIR [1] DeltaCIR
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseCIR ::= SEQUENCE {
|
||||||
|
version INTEGER DEFAULT 0,
|
||||||
|
metaInfo CIRMetaInfo,
|
||||||
|
talList SEQUENCE OF URIAndHash,
|
||||||
|
objectList SEQUENCE OF URIAndHash
|
||||||
|
}
|
||||||
|
|
||||||
|
DeltaCIR ::= SEQUENCE {
|
||||||
|
version INTEGER DEFAULT 0,
|
||||||
|
metaInfo CIRMetaInfo,
|
||||||
|
talChanges [0] DeltaChanges OPTIONAL,
|
||||||
|
objectChanges [1] DeltaChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
DeltaChanges ::= SEQUENCE {
|
||||||
|
upserted [0] SEQUENCE OF URIAndHash OPTIONAL,
|
||||||
|
removed [1] SEQUENCE OF IA5String OPTIONAL
|
||||||
|
}
|
||||||
|
|
||||||
|
CIRMetaInfo ::= SEQUENCE {
|
||||||
|
validationTime GeneralizedTime,
|
||||||
|
rpSoftware [0] UTF8String OPTIONAL,
|
||||||
|
rpVersion [1] UTF8String OPTIONAL,
|
||||||
|
observerID [2] UTF8String OPTIONAL
|
||||||
|
}
|
||||||
|
|
||||||
|
URIAndHash ::= SEQUENCE {
|
||||||
|
uri IA5String,
|
||||||
|
hash OCTET STRING,
|
||||||
|
source [0] SourceType OPTIONAL
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceType ::= ENUMERATED {
|
||||||
|
rsync (0),
|
||||||
|
rrdp (1),
|
||||||
|
https (2),
|
||||||
|
erik (3),
|
||||||
|
cache (4),
|
||||||
|
other (5)
|
||||||
|
}
|
||||||
|
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.1. version
|
||||||
|
|
||||||
|
The version field contains the format version for the structure. In this version of the specification, it MUST be `0`.
|
||||||
|
|
||||||
|
#### 4.2. metaInfo
|
||||||
|
|
||||||
|
The `metaInfo` structure provides crucial temporal and environmental context:
|
||||||
|
* **validationTime**: Contains a `GeneralizedTime` indicating the moment the synchronization concluded. This timestamp is REQUIRED, as it is strictly necessary to freeze the system clock when replaying RPKI validation logic to evaluate time-sensitive object expiration.
|
||||||
|
* **rpSoftware / rpVersion / observerID**: OPTIONAL metadata to identify the specific software and observation vantage point generating the CIR.
|
||||||
|
|
||||||
|
#### 4.3. BaseCIR Fields
|
||||||
|
|
||||||
|
* **talList**: A sequence of `URIAndHash` representing the Trust Anchor Locators used as the root of validation.
|
||||||
|
* **objectList**: A sequence of `URIAndHash` representing every raw file fetched by the RP. The `uri` MUST be the absolute logical address (e.g., `rsync://...`), and the `hash` MUST be the SHA-256 digest of the raw file.
|
||||||
|
* **source**: An OPTIONAL enumerated value indicating the network transport or cache layer from which the file was successfully obtained (e.g., `rrdp`, `rsync`).
|
||||||
|
|
||||||
|
#### 4.4. DeltaCIR Fields
|
||||||
|
|
||||||
|
To support compact archival, `DeltaCIR` describes changes relative to a preceding `BaseCIR` or `DeltaCIR`:
|
||||||
|
* **upserted**: A sequence of `URIAndHash` for newly discovered objects or objects where the URI remained identical but the cryptographic Hash changed.
|
||||||
|
* **removed**: A sequence of `IA5String` containing URIs that were present in the previous snapshot but are no longer observed.
|
||||||
|
|
||||||
|
### 5. Operational Considerations
|
||||||
|
|
||||||
|
#### 5.1. Differential Testing and Historical Replay
|
||||||
|
|
||||||
|
Because CIR captures the global input state *regardless* of object validity, it allows operators to construct an isolated physical sandbox matching the exact network state at `validationTime`. By injecting this state into different RP software implementations (using native functionality like `--disable-rrdp` coupled with local rsync wrappers), operators can perform deterministic differential testing. Discrepancies in the resulting CCR outputs indicate implementation bugs or vulnerabilities in boundary-case handling.
|
||||||
|
|
||||||
|
#### 5.2. Delta Compression for Archival
|
||||||
|
|
||||||
|
Given that the global RPKI repository experiences relatively low churn within short timeframes (e.g., 10-minute intervals), `DeltaCIR` significantly reduces storage overhead. Archival systems SHOULD compute `DeltaCIR` sequences from raw `BaseCIR` outputs to facilitate efficient streaming historical replays.
|
||||||
|
|
||||||
|
### 6. Security Considerations
|
||||||
|
|
||||||
|
Unlike RPKI signed objects, CIR objects are not cryptographically signed by CAs. They are observational records.
|
||||||
|
|
||||||
|
CIR explicitly permits the indexing of corrupted, malicious, or malformed ASN.1 objects. Parsers ingesting CIR to reconstruct sandboxes MUST NOT attempt to cryptographically decode or execute the objects referenced by the hashes, but simply treat them as opaque binary blobs to be placed in the file system for the target RP to evaluate.
|
||||||
|
|
||||||
|
### 7. IANA Considerations
|
||||||
|
|
||||||
|
IANA is requested to register the media type `application/rpki-cir`, the file extension `.cir`, and the necessary SMI Security for S/MIME Module Identifiers (OIDs), modeled identically to the IANA considerations defined in the CCR specification.
|
||||||
|
|
||||||
|
### 8. References
|
||||||
|
|
||||||
|
*[Standard IETF references for RFC 2119, RFC 8174, RFC 6488, RFC 8182, etc. to be populated]*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next Step Guidance**:
|
||||||
|
If you plan to officially submit this to the IETF SIDROPS working group, you'll need to allocate the `[TBD]` OID placeholders and potentially run the ASN.1 syntax through an official compiler (like `asn1c`) to ensure there are no implicit tagging ambiguities in the `CHOICE` and `OPTIONAL` fields. Would you like me to refine the ASN.1 tagging strategy further?
|
||||||
@@ -3,6 +3,36 @@
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
{
|
||||||
|
"id": "qRHdw-Ut-vBgaTaqBgwLz",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 902.3138754863095,
|
||||||
|
"y": 2678.0908137043048,
|
||||||
|
"width": 236.65964942812968,
|
||||||
|
"height": 375.87119006484846,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "transparent",
|
||||||
|
"backgroundColor": "#b2f2bb",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "Zz",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 1898623381,
|
||||||
|
"version": 94,
|
||||||
|
"versionNonce": 843496469,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774497955573,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "6TX3XEpY6zQ8YD2gCks-J",
|
"id": "6TX3XEpY6zQ8YD2gCks-J",
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
@@ -2494,7 +2524,7 @@
|
|||||||
"version": 72,
|
"version": 72,
|
||||||
"versionNonce": 1545966256,
|
"versionNonce": 1545966256,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773114633322,
|
"updated": 1773114633322,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3116,7 +3146,7 @@
|
|||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1888566864,
|
"versionNonce": 1888566864,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773114395646,
|
"updated": 1773114395646,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3190,7 +3220,7 @@
|
|||||||
"version": 91,
|
"version": 91,
|
||||||
"versionNonce": 336574032,
|
"versionNonce": 336574032,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115366206,
|
"updated": 1773115366206,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false
|
"locked": false
|
||||||
@@ -3218,7 +3248,7 @@
|
|||||||
"version": 52,
|
"version": 52,
|
||||||
"versionNonce": 1619956816,
|
"versionNonce": 1619956816,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115399843,
|
"updated": 1773115399843,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3296,7 +3326,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1422040240,
|
"versionNonce": 1422040240,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115711518,
|
"updated": 1773115711518,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3370,7 +3400,7 @@
|
|||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1849437264,
|
"versionNonce": 1849437264,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115714936,
|
"updated": 1773115714936,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3452,7 +3482,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1494146128,
|
"versionNonce": 1494146128,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115718725,
|
"updated": 1773115718725,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3634,7 +3664,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1110543440,
|
"versionNonce": 1110543440,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115722655,
|
"updated": 1773115722655,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3708,7 +3738,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 32818864,
|
"versionNonce": 32818864,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115726419,
|
"updated": 1773115726419,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3853,7 +3883,7 @@
|
|||||||
"version": 80,
|
"version": 80,
|
||||||
"versionNonce": 2063705264,
|
"versionNonce": 2063705264,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115581907,
|
"updated": 1773115581907,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false
|
"locked": false
|
||||||
@@ -3881,7 +3911,7 @@
|
|||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 468362320,
|
"versionNonce": 468362320,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115592009,
|
"updated": 1773115592009,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -3918,7 +3948,7 @@
|
|||||||
"version": 47,
|
"version": 47,
|
||||||
"versionNonce": 355656368,
|
"versionNonce": 355656368,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115597983,
|
"updated": 1773115597983,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false
|
"locked": false
|
||||||
@@ -3946,7 +3976,7 @@
|
|||||||
"version": 57,
|
"version": 57,
|
||||||
"versionNonce": 1310461616,
|
"versionNonce": 1310461616,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115603065,
|
"updated": 1773115603065,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false
|
"locked": false
|
||||||
@@ -4048,7 +4078,7 @@
|
|||||||
"version": 59,
|
"version": 59,
|
||||||
"versionNonce": 603605072,
|
"versionNonce": 603605072,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115639925,
|
"updated": 1773115639925,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -4091,7 +4121,7 @@
|
|||||||
"version": 83,
|
"version": 83,
|
||||||
"versionNonce": 1951717968,
|
"versionNonce": 1951717968,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773115646441,
|
"updated": 1773115646441,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5023,7 +5053,7 @@
|
|||||||
"version": 41,
|
"version": 41,
|
||||||
"versionNonce": 218425424,
|
"versionNonce": 218425424,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116118218,
|
"updated": 1773116118218,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5123,7 +5153,7 @@
|
|||||||
"version": 18,
|
"version": 18,
|
||||||
"versionNonce": 1830824624,
|
"versionNonce": 1830824624,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116135062,
|
"updated": 1773116135062,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5223,7 +5253,7 @@
|
|||||||
"version": 6,
|
"version": 6,
|
||||||
"versionNonce": 258842800,
|
"versionNonce": 258842800,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116144968,
|
"updated": 1773116144968,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5323,7 +5353,7 @@
|
|||||||
"version": 6,
|
"version": 6,
|
||||||
"versionNonce": 1402610256,
|
"versionNonce": 1402610256,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116155635,
|
"updated": 1773116155635,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5497,7 +5527,7 @@
|
|||||||
"version": 16,
|
"version": 16,
|
||||||
"versionNonce": 1175871664,
|
"versionNonce": 1175871664,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116197849,
|
"updated": 1773116197849,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5597,7 +5627,7 @@
|
|||||||
"version": 20,
|
"version": 20,
|
||||||
"versionNonce": 153771696,
|
"versionNonce": 153771696,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116217438,
|
"updated": 1773116217438,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5701,7 +5731,7 @@
|
|||||||
"version": 6,
|
"version": 6,
|
||||||
"versionNonce": 602646096,
|
"versionNonce": 602646096,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773116228554,
|
"updated": 1773116228554,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -5775,7 +5805,7 @@
|
|||||||
"version": 39,
|
"version": 39,
|
||||||
"versionNonce": 313531472,
|
"versionNonce": 313531472,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773129151744,
|
"updated": 1773129151744,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -7509,7 +7539,7 @@
|
|||||||
"version": 20,
|
"version": 20,
|
||||||
"versionNonce": 771730096,
|
"versionNonce": 771730096,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128132466,
|
"updated": 1773128132466,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -7546,7 +7576,7 @@
|
|||||||
"version": 185,
|
"version": 185,
|
||||||
"versionNonce": 2079384144,
|
"versionNonce": 2079384144,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773129151744,
|
"updated": 1773129151744,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -8022,10 +8052,7 @@
|
|||||||
],
|
],
|
||||||
"pressures": [],
|
"pressures": [],
|
||||||
"simulatePressure": true,
|
"simulatePressure": true,
|
||||||
"lastCommittedPoint": [
|
"lastCommittedPoint": null
|
||||||
170.39252493058768,
|
|
||||||
-5.1117753579201235
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ES3H6HxkXXAdU_TAK1q-Y",
|
"id": "ES3H6HxkXXAdU_TAK1q-Y",
|
||||||
@@ -8109,7 +8136,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1913455792,
|
"versionNonce": 1913455792,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128156983,
|
"updated": 1773128156983,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -8205,7 +8232,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 909726896,
|
"versionNonce": 909726896,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128159933,
|
"updated": 1773128159933,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -10058,7 +10085,7 @@
|
|||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 775627440,
|
"versionNonce": 775627440,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128137154,
|
"updated": 1773128137154,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -10450,7 +10477,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1293264048,
|
"versionNonce": 1293264048,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128162365,
|
"updated": 1773128162365,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -10546,7 +10573,7 @@
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 111277232,
|
"versionNonce": 111277232,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128164616,
|
"updated": 1773128164616,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -15921,7 +15948,7 @@
|
|||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 134388400,
|
"versionNonce": 134388400,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128141149,
|
"updated": 1773128141149,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -16013,7 +16040,7 @@
|
|||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 1230377648,
|
"versionNonce": 1230377648,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128144654,
|
"updated": 1773128144654,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -16049,8 +16076,8 @@
|
|||||||
"type": 2
|
"type": 2
|
||||||
},
|
},
|
||||||
"seed": 1572300368,
|
"seed": 1572300368,
|
||||||
"version": 98,
|
"version": 99,
|
||||||
"versionNonce": 1871666352,
|
"versionNonce": 1923189141,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
@@ -16058,7 +16085,7 @@
|
|||||||
"id": "buUBs6ZWoW0_XJOiVjU2X"
|
"id": "buUBs6ZWoW0_XJOiVjU2X"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": 1773129151974,
|
"updated": 1774497963434,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"points": [
|
"points": [
|
||||||
@@ -16085,8 +16112,8 @@
|
|||||||
{
|
{
|
||||||
"id": "buUBs6ZWoW0_XJOiVjU2X",
|
"id": "buUBs6ZWoW0_XJOiVjU2X",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"x": 952.6392707639827,
|
"x": 961.1589223603503,
|
||||||
"y": 2621.571169580318,
|
"y": 2771.516554079474,
|
||||||
"width": 40,
|
"width": 40,
|
||||||
"height": 25,
|
"height": 25,
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
@@ -16102,11 +16129,11 @@
|
|||||||
"index": "b6bV",
|
"index": "b6bV",
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1251791024,
|
"seed": 1251791024,
|
||||||
"version": 9,
|
"version": 10,
|
||||||
"versionNonce": 3508912,
|
"versionNonce": 618429141,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"boundElements": null,
|
"boundElements": [],
|
||||||
"updated": 1773128148385,
|
"updated": 1774497962077,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"text": "包含",
|
"text": "包含",
|
||||||
@@ -16188,6 +16215,43 @@
|
|||||||
"originalText": "PP3",
|
"originalText": "PP3",
|
||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"lineHeight": 1.25
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "G2dlYSYSyBcTaCSWE_9Fg",
|
||||||
|
"type": "text",
|
||||||
|
"x": 993.7959422946816,
|
||||||
|
"y": 2645.276672426363,
|
||||||
|
"width": 50.03996276855469,
|
||||||
|
"height": 25,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "#b2f2bb",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "b6e",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1833275125,
|
||||||
|
"version": 27,
|
||||||
|
"versionNonce": 1513809621,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774497979403,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "VCIR",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "left",
|
||||||
|
"verticalAlign": "top",
|
||||||
|
"containerId": null,
|
||||||
|
"originalText": "VCIR",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"appState": {
|
"appState": {
|
||||||
|
|||||||
629
specs/sync.excalidraw
Normal file
629
specs/sync.excalidraw
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
{
|
||||||
|
"type": "excalidraw",
|
||||||
|
"version": 2,
|
||||||
|
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"id": "782wmN2vbn0vYfClUbwVT",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 458.5143563406808,
|
||||||
|
"y": 224.57136099679133,
|
||||||
|
"width": 335.08570861816406,
|
||||||
|
"height": 143.99998474121094,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a0",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 264486616,
|
||||||
|
"version": 304,
|
||||||
|
"versionNonce": 505039016,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "491W0AyWpioiNNXTRuXMb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499907328,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "491W0AyWpioiNNXTRuXMb",
|
||||||
|
"type": "text",
|
||||||
|
"x": 514.987287248884,
|
||||||
|
"y": 246.5713533673968,
|
||||||
|
"width": 222.1398468017578,
|
||||||
|
"height": 100,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a1",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1658952360,
|
||||||
|
"version": 360,
|
||||||
|
"versionNonce": 404419496,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774499907328,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "RAW BY HASH\nsha256 -> file content\n(.mft/.roa/.cer)\n通过hash找原始文件",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "782wmN2vbn0vYfClUbwVT",
|
||||||
|
"originalText": "RAW BY HASH\nsha256 -> file content (.mft/.roa/.cer)\n通过hash找原始文件",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4v-5vJwc-YSKwDDA6wnNy",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 86.74286869594027,
|
||||||
|
"y": 224.1142785208566,
|
||||||
|
"width": 332.79998561314164,
|
||||||
|
"height": 141.99999128069192,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a2",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 1176722904,
|
||||||
|
"version": 312,
|
||||||
|
"versionNonce": 1631513048,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "tug_6QGIm4LrnsrYiV18D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499913411,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tug_6QGIm4LrnsrYiV18D",
|
||||||
|
"type": "text",
|
||||||
|
"x": 109.85297502790172,
|
||||||
|
"y": 245.11427416120256,
|
||||||
|
"width": 286.57977294921875,
|
||||||
|
"height": 100,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a3",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1923736280,
|
||||||
|
"version": 409,
|
||||||
|
"versionNonce": 980676312,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [],
|
||||||
|
"updated": 1774499913411,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "REPOSITORY VIEW\nuri -> sha256(current version\nfile)\n对象uri 查找最新hash",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "4v-5vJwc-YSKwDDA6wnNy",
|
||||||
|
"originalText": "REPOSITORY VIEW\nuri -> sha256(current version file)\n对象uri 查找最新hash",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "V2AcY1746pbG544Yh7A7q",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 90.85720498221252,
|
||||||
|
"y": -2.5142985752651725,
|
||||||
|
"width": 218.51431492396773,
|
||||||
|
"height": 205.71430751255576,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a4",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 436378328,
|
||||||
|
"version": 498,
|
||||||
|
"versionNonce": 113477080,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "WVFatIOjK3SB8FDetV4ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499431839,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "WVFatIOjK3SB8FDetV4ts",
|
||||||
|
"type": "text",
|
||||||
|
"x": 110.4543816702706,
|
||||||
|
"y": 37.84285518101271,
|
||||||
|
"width": 179.31996154785156,
|
||||||
|
"height": 125,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a5",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1637449688,
|
||||||
|
"version": 675,
|
||||||
|
"versionNonce": 1502150312,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [],
|
||||||
|
"updated": 1774499518593,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "RRDP SOURCE\nSTATE\nnotify -> state\n(session, serial)\n不同rrdp源同步状态",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "V2AcY1746pbG544Yh7A7q",
|
||||||
|
"originalText": "RRDP SOURCE STATE\nnotify -> state (session, serial)\n不同rrdp源同步状态",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "KaZIF4nN5lJcP8jlzm2ze",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 333.25717054094576,
|
||||||
|
"y": -5.54285212925501,
|
||||||
|
"width": 217.3714316231864,
|
||||||
|
"height": 206.7142813546317,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a6",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 1602529240,
|
||||||
|
"version": 528,
|
||||||
|
"versionNonce": 138363048,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "zs86EXmSVt0DGU7Yilr06"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499548936,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zs86EXmSVt0DGU7Yilr06",
|
||||||
|
"type": "text",
|
||||||
|
"x": 341.94288635253895,
|
||||||
|
"y": 22.814288548060844,
|
||||||
|
"width": 200,
|
||||||
|
"height": 150,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a7",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 626126040,
|
||||||
|
"version": 865,
|
||||||
|
"versionNonce": 1330001880,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [],
|
||||||
|
"updated": 1774499555053,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "RRDP SOURCE\nMEMBER\nsource+ uri ->\npresent/withdraw\n前缀遍历获取rrdp源下\n全部对象,不同源混放",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "KaZIF4nN5lJcP8jlzm2ze",
|
||||||
|
"originalText": "RRDP SOURCE MEMBER\nsource+ uri -> present/withdraw\n前缀遍历获取rrdp源下全部对象,不同源混放",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w4ratSPiaf_sJhmxzs_zB",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 575.7714941842216,
|
||||||
|
"y": -5.842858450753411,
|
||||||
|
"width": 217.3714316231864,
|
||||||
|
"height": 206.7142813546317,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a8",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 1385028264,
|
||||||
|
"version": 564,
|
||||||
|
"versionNonce": 1503452584,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "t7Y2vDpAmPdo00qNs6Lxp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499442854,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "t7Y2vDpAmPdo00qNs6Lxp",
|
||||||
|
"type": "text",
|
||||||
|
"x": 584.7972292218891,
|
||||||
|
"y": 47.51428222656244,
|
||||||
|
"width": 199.31996154785156,
|
||||||
|
"height": 100,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "a9",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1548502440,
|
||||||
|
"version": 945,
|
||||||
|
"versionNonce": 1851848664,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [],
|
||||||
|
"updated": 1774499514167,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "RRDP URI OWNER\nuri -> source\n反查对象所属rrdp源,\n防止跨源误删",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "w4ratSPiaf_sJhmxzs_zB",
|
||||||
|
"originalText": "RRDP URI OWNER\nuri -> source\n反查对象所属rrdp源,防止跨源误删",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iHWL3p3MaRLZ-l7Es74es",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 89.02865600585938,
|
||||||
|
"y": 398.08574567522317,
|
||||||
|
"width": 339.42862374441967,
|
||||||
|
"height": 161.14283970424117,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aA",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 600580568,
|
||||||
|
"version": 174,
|
||||||
|
"versionNonce": 863743704,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "gi3C5lbily2-D96ZNdUB_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499918860,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gi3C5lbily2-D96ZNdUB_",
|
||||||
|
"type": "text",
|
||||||
|
"x": 105.99298313685827,
|
||||||
|
"y": 441.15716552734375,
|
||||||
|
"width": 305.4999694824219,
|
||||||
|
"height": 75,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aAV",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1096467112,
|
||||||
|
"version": 228,
|
||||||
|
"versionNonce": 315626456,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774499918860,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "VCIR\n按照CA为单元,记录已验证缓存的\nRPKI对象产物,树状结构",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "iHWL3p3MaRLZ-l7Es74es",
|
||||||
|
"originalText": "VCIR\n按照CA为单元,记录已验证缓存的RPKI对象产物,树状结构",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "J6aHbqCN1b8plYxKAKfYT",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 454.17152186802457,
|
||||||
|
"y": 399.80005972725996,
|
||||||
|
"width": 339.42862374441967,
|
||||||
|
"height": 161.14283970424117,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aC",
|
||||||
|
"roundness": {
|
||||||
|
"type": 3
|
||||||
|
},
|
||||||
|
"seed": 1274007512,
|
||||||
|
"version": 244,
|
||||||
|
"versionNonce": 877648088,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "ernK0EMAzhxJvpYUaWPiS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated": 1774499918860,
|
||||||
|
"link": null,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ernK0EMAzhxJvpYUaWPiS",
|
||||||
|
"type": "text",
|
||||||
|
"x": 461.35585021972656,
|
||||||
|
"y": 442.87147957938055,
|
||||||
|
"width": 325.0599670410156,
|
||||||
|
"height": 75,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aD",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 706221272,
|
||||||
|
"version": 431,
|
||||||
|
"versionNonce": 2072968664,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": [],
|
||||||
|
"updated": 1774499918860,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "AUDIT RULE INDEX\n溯源审计用户,通过产物规则hash反\n向查找对应VCIR节点",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "center",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"containerId": "J6aHbqCN1b8plYxKAKfYT",
|
||||||
|
"originalText": "AUDIT RULE INDEX\n溯源审计用户,通过产物规则hash反向查找对应VCIR节点",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kkkXT2D6yQsceW2UyfJPF",
|
||||||
|
"type": "text",
|
||||||
|
"x": -121.25701032366032,
|
||||||
|
"y": 73.5142887660437,
|
||||||
|
"width": 186.9999542236328,
|
||||||
|
"height": 25,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aE",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 2098948056,
|
||||||
|
"version": 64,
|
||||||
|
"versionNonce": 1921034200,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774499954136,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "RRDP 同步状态数据",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "left",
|
||||||
|
"verticalAlign": "top",
|
||||||
|
"containerId": null,
|
||||||
|
"originalText": "RRDP 同步状态数据",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "BKIRb0Geq874XYWd0OLtS",
|
||||||
|
"type": "text",
|
||||||
|
"x": -92.68558175223177,
|
||||||
|
"y": 276.9428296770369,
|
||||||
|
"width": 120,
|
||||||
|
"height": 25,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aF",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 791319000,
|
||||||
|
"version": 31,
|
||||||
|
"versionNonce": 703934936,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774499970564,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "原始文件数据",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "left",
|
||||||
|
"verticalAlign": "top",
|
||||||
|
"containerId": null,
|
||||||
|
"originalText": "原始文件数据",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wNzjeS0S_Ji1bKTrKDoyd",
|
||||||
|
"type": "text",
|
||||||
|
"x": -101.82843017578091,
|
||||||
|
"y": 473.51426696777366,
|
||||||
|
"width": 140,
|
||||||
|
"height": 25,
|
||||||
|
"angle": 0,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"roughness": 1,
|
||||||
|
"opacity": 100,
|
||||||
|
"groupIds": [],
|
||||||
|
"frameId": null,
|
||||||
|
"index": "aG",
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1183095768,
|
||||||
|
"version": 26,
|
||||||
|
"versionNonce": 1711685800,
|
||||||
|
"isDeleted": false,
|
||||||
|
"boundElements": null,
|
||||||
|
"updated": 1774499982445,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"text": "已验证产物数据",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontFamily": 5,
|
||||||
|
"textAlign": "left",
|
||||||
|
"verticalAlign": "top",
|
||||||
|
"containerId": null,
|
||||||
|
"originalText": "已验证产物数据",
|
||||||
|
"autoResize": true,
|
||||||
|
"lineHeight": 1.25
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"appState": {
|
||||||
|
"gridSize": 20,
|
||||||
|
"gridStep": 5,
|
||||||
|
"gridModeEnabled": false,
|
||||||
|
"viewBackgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"files": {}
|
||||||
|
}
|
||||||
24
src/audit.rs
24
src/audit.rs
@@ -9,6 +9,7 @@ pub enum AuditObjectKind {
|
|||||||
Manifest,
|
Manifest,
|
||||||
Crl,
|
Crl,
|
||||||
Certificate,
|
Certificate,
|
||||||
|
RouterCertificate,
|
||||||
Roa,
|
Roa,
|
||||||
Aspa,
|
Aspa,
|
||||||
Other,
|
Other,
|
||||||
@@ -71,6 +72,15 @@ pub struct PublicationPointAudit {
|
|||||||
pub rrdp_notification_uri: Option<String>,
|
pub rrdp_notification_uri: Option<String>,
|
||||||
|
|
||||||
pub source: String,
|
pub source: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repo_sync_source: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repo_sync_phase: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repo_sync_duration_ms: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repo_sync_error: Option<String>,
|
||||||
|
pub repo_terminal_state: String,
|
||||||
pub this_update_rfc3339_utc: String,
|
pub this_update_rfc3339_utc: String,
|
||||||
pub next_update_rfc3339_utc: String,
|
pub next_update_rfc3339_utc: String,
|
||||||
pub verified_at_rfc3339_utc: String,
|
pub verified_at_rfc3339_utc: String,
|
||||||
@@ -149,6 +159,19 @@ pub struct AuditDownloadStats {
|
|||||||
pub by_kind: std::collections::BTreeMap<String, AuditDownloadKindStats>,
|
pub by_kind: std::collections::BTreeMap<String, AuditDownloadKindStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct AuditRepoSyncStateStat {
|
||||||
|
pub count: u64,
|
||||||
|
pub duration_ms_total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct AuditRepoSyncStats {
|
||||||
|
pub publication_points_total: u64,
|
||||||
|
pub by_phase: std::collections::BTreeMap<String, AuditRepoSyncStateStat>,
|
||||||
|
pub by_terminal_state: std::collections::BTreeMap<String, AuditRepoSyncStateStat>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
pub struct AuditReportV1 {
|
pub struct AuditReportV1 {
|
||||||
pub format_version: u32,
|
pub format_version: u32,
|
||||||
@@ -174,6 +197,7 @@ pub struct AuditReportV2 {
|
|||||||
|
|
||||||
pub downloads: Vec<AuditDownloadEvent>,
|
pub downloads: Vec<AuditDownloadEvent>,
|
||||||
pub download_stats: AuditDownloadStats,
|
pub download_stats: AuditDownloadStats,
|
||||||
|
pub repo_sync_stats: AuditRepoSyncStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ mod tests {
|
|||||||
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirChildEntry, VcirInstanceGate,
|
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirChildEntry, VcirInstanceGate,
|
||||||
VcirRelatedArtifact, VcirSummary,
|
VcirRelatedArtifact, VcirSummary,
|
||||||
};
|
};
|
||||||
|
use base64::Engine as _;
|
||||||
|
|
||||||
fn sample_vcir(
|
fn sample_vcir(
|
||||||
manifest_rsync_uri: &str,
|
manifest_rsync_uri: &str,
|
||||||
@@ -311,6 +312,10 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|output| output.output_type == VcirOutputType::Aspa)
|
.filter(|output| output.output_type == VcirOutputType::Aspa)
|
||||||
.count() as u32,
|
.count() as u32,
|
||||||
|
local_router_key_count: local_outputs
|
||||||
|
.iter()
|
||||||
|
.filter(|output| output.output_type == VcirOutputType::RouterKey)
|
||||||
|
.count() as u32,
|
||||||
child_count: 1,
|
child_count: 1,
|
||||||
accepted_object_count: related_artifacts.len() as u32,
|
accepted_object_count: related_artifacts.len() as u32,
|
||||||
rejected_object_count: 0,
|
rejected_object_count: 0,
|
||||||
@@ -379,7 +384,7 @@ mod tests {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put_raw(store: &RocksStore, bytes: &[u8], uri: &str, object_type: &str) {
|
fn put_raw_evidence(store: &RocksStore, bytes: &[u8], uri: &str, object_type: &str) {
|
||||||
let mut entry = RawByHashEntry::from_bytes(sha256_hex(bytes), bytes.to_vec());
|
let mut entry = RawByHashEntry::from_bytes(sha256_hex(bytes), bytes.to_vec());
|
||||||
entry.origin_uris.push(uri.to_string());
|
entry.origin_uris.push(uri.to_string());
|
||||||
entry.object_type = Some(object_type.to_string());
|
entry.object_type = Some(object_type.to_string());
|
||||||
@@ -427,17 +432,17 @@ mod tests {
|
|||||||
.put_audit_rule_index_entry(&rule_entry)
|
.put_audit_rule_index_entry(&rule_entry)
|
||||||
.expect("put rule index");
|
.expect("put rule index");
|
||||||
|
|
||||||
put_raw(&store, leaf_manifest.as_bytes(), leaf_manifest, "mft");
|
put_raw_evidence(&store, leaf_manifest.as_bytes(), leaf_manifest, "mft");
|
||||||
put_raw(
|
put_raw_evidence(
|
||||||
&store,
|
&store,
|
||||||
format!("{}-crl", leaf_manifest).as_bytes(),
|
format!("{}-crl", leaf_manifest).as_bytes(),
|
||||||
&leaf_manifest.replace(".mft", ".crl"),
|
&leaf_manifest.replace(".mft", ".crl"),
|
||||||
"crl",
|
"crl",
|
||||||
);
|
);
|
||||||
put_raw(&store, b"roa-raw", &local.source_object_uri, "roa");
|
put_raw_evidence(&store, b"roa-raw", &local.source_object_uri, "roa");
|
||||||
put_raw(&store, b"roa-ee", "rsync://example.test/leaf/a.ee", "cer");
|
put_raw_evidence(&store, b"roa-ee", "rsync://example.test/leaf/a.ee", "cer");
|
||||||
put_raw(&store, root_manifest.as_bytes(), root_manifest, "mft");
|
put_raw_evidence(&store, root_manifest.as_bytes(), root_manifest, "mft");
|
||||||
put_raw(
|
put_raw_evidence(
|
||||||
&store,
|
&store,
|
||||||
format!("{}-crl", root_manifest).as_bytes(),
|
format!("{}-crl", root_manifest).as_bytes(),
|
||||||
&root_manifest.replace(".mft", ".crl"),
|
&root_manifest.replace(".mft", ".crl"),
|
||||||
@@ -477,6 +482,60 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trace_rule_to_root_supports_router_key_rules() {
|
||||||
|
let store_dir = tempfile::tempdir().expect("store dir");
|
||||||
|
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||||
|
let manifest = "rsync://example.test/router/leaf.mft";
|
||||||
|
let mut local = sample_local_output(manifest);
|
||||||
|
local.output_type = VcirOutputType::RouterKey;
|
||||||
|
local.source_object_uri = "rsync://example.test/router/router.cer".to_string();
|
||||||
|
local.source_object_type = "router_key".to_string();
|
||||||
|
local.payload_json = serde_json::json!({
|
||||||
|
"as_id": 64496,
|
||||||
|
"ski_hex": "11".repeat(20),
|
||||||
|
"spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let mut vcir = sample_vcir(
|
||||||
|
manifest,
|
||||||
|
None,
|
||||||
|
"test-tal",
|
||||||
|
Some(local),
|
||||||
|
sample_artifacts(manifest, &sha256_hex(b"router-object")),
|
||||||
|
);
|
||||||
|
vcir.local_outputs[0].output_type = VcirOutputType::RouterKey;
|
||||||
|
vcir.local_outputs[0].source_object_uri =
|
||||||
|
"rsync://example.test/router/router.cer".to_string();
|
||||||
|
vcir.local_outputs[0].source_object_type = "router_key".to_string();
|
||||||
|
vcir.local_outputs[0].payload_json = serde_json::json!({
|
||||||
|
"as_id": 64496,
|
||||||
|
"ski_hex": "11".repeat(20),
|
||||||
|
"spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
vcir.summary.local_vrp_count = 0;
|
||||||
|
vcir.summary.local_router_key_count = 1;
|
||||||
|
store.put_vcir(&vcir).expect("put vcir");
|
||||||
|
let rule_entry = AuditRuleIndexEntry {
|
||||||
|
kind: AuditRuleKind::RouterKey,
|
||||||
|
rule_hash: vcir.local_outputs[0].rule_hash.clone(),
|
||||||
|
manifest_rsync_uri: manifest.to_string(),
|
||||||
|
source_object_uri: vcir.local_outputs[0].source_object_uri.clone(),
|
||||||
|
source_object_hash: vcir.local_outputs[0].source_object_hash.clone(),
|
||||||
|
output_id: vcir.local_outputs[0].output_id.clone(),
|
||||||
|
item_effective_until: vcir.local_outputs[0].item_effective_until.clone(),
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.put_audit_rule_index_entry(&rule_entry)
|
||||||
|
.expect("put rule");
|
||||||
|
let trace = trace_rule_to_root(&store, AuditRuleKind::RouterKey, &rule_entry.rule_hash)
|
||||||
|
.expect("trace rule")
|
||||||
|
.expect("trace exists");
|
||||||
|
assert_eq!(trace.rule.kind, AuditRuleKind::RouterKey);
|
||||||
|
assert_eq!(trace.resolved_output.output_type, VcirOutputType::RouterKey);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn trace_rule_to_root_returns_none_for_missing_rule_index() {
|
fn trace_rule_to_root_returns_none_for_missing_rule_index() {
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
let store_dir = tempfile::tempdir().expect("store dir");
|
||||||
|
|||||||
70
src/bin/ccr_dump.rs
Normal file
70
src/bin/ccr_dump.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use rpki::ccr::dump::dump_content_info_json_value;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
ccr_path: Option<std::path::PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: ccr_dump --ccr <path>"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args::default();
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--ccr" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--ccr requires a value")?;
|
||||||
|
args.ccr_path = Some(v.into());
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if args.ccr_path.is_none() {
|
||||||
|
return Err(format!("--ccr is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let ccr_path = args.ccr_path.as_ref().unwrap();
|
||||||
|
let bytes = std::fs::read(ccr_path)
|
||||||
|
.map_err(|e| format!("read ccr failed: {}: {e}", ccr_path.display()))?;
|
||||||
|
let json = dump_content_info_json_value(&bytes).map_err(|e| e.to_string())?;
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_accepts_ccr_path() {
|
||||||
|
let argv = vec![
|
||||||
|
"ccr_dump".to_string(),
|
||||||
|
"--ccr".to_string(),
|
||||||
|
"a.ccr".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(
|
||||||
|
args.ccr_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("a.ccr"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_required_ccr() {
|
||||||
|
let argv = vec!["ccr_dump".to_string()];
|
||||||
|
let err = parse_args(&argv).unwrap_err();
|
||||||
|
assert!(err.contains("--ccr is required"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/bin/ccr_to_compare_views.rs
Normal file
124
src/bin/ccr_to_compare_views.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use rpki::bundle::{decode_ccr_compare_views, write_vap_csv, write_vrp_csv};
|
||||||
|
use rpki::ccr::decode_content_info;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
ccr_path: Option<std::path::PathBuf>,
|
||||||
|
vrps_out_path: Option<std::path::PathBuf>,
|
||||||
|
vaps_out_path: Option<std::path::PathBuf>,
|
||||||
|
trust_anchor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: ccr_to_compare_views --ccr <path> --vrps-out <path> --vaps-out <path> [--trust-anchor <name>]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args {
|
||||||
|
trust_anchor: "unknown".to_string(),
|
||||||
|
..Args::default()
|
||||||
|
};
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--ccr" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--ccr requires a value")?;
|
||||||
|
args.ccr_path = Some(v.into());
|
||||||
|
}
|
||||||
|
"--vrps-out" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--vrps-out requires a value")?;
|
||||||
|
args.vrps_out_path = Some(v.into());
|
||||||
|
}
|
||||||
|
"--vaps-out" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--vaps-out requires a value")?;
|
||||||
|
args.vaps_out_path = Some(v.into());
|
||||||
|
}
|
||||||
|
"--trust-anchor" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--trust-anchor requires a value")?;
|
||||||
|
args.trust_anchor = v.clone();
|
||||||
|
}
|
||||||
|
"-h" | "--help" => return Err(usage().to_string()),
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if args.ccr_path.is_none() {
|
||||||
|
return Err(format!("--ccr is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.vrps_out_path.is_none() {
|
||||||
|
return Err(format!("--vrps-out is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.vaps_out_path.is_none() {
|
||||||
|
return Err(format!("--vaps-out is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let ccr_path = args.ccr_path.as_ref().unwrap();
|
||||||
|
let bytes = std::fs::read(ccr_path)
|
||||||
|
.map_err(|e| format!("read ccr failed: {}: {e}", ccr_path.display()))?;
|
||||||
|
let content_info = decode_content_info(&bytes).map_err(|e| e.to_string())?;
|
||||||
|
let (vrps, vaps) =
|
||||||
|
decode_ccr_compare_views(&content_info, &args.trust_anchor).map_err(|e| e.to_string())?;
|
||||||
|
write_vrp_csv(args.vrps_out_path.as_ref().unwrap(), &vrps)?;
|
||||||
|
write_vap_csv(args.vaps_out_path.as_ref().unwrap(), &vaps)?;
|
||||||
|
println!(
|
||||||
|
"{}\n{}",
|
||||||
|
args.vrps_out_path.as_ref().unwrap().display(),
|
||||||
|
args.vaps_out_path.as_ref().unwrap().display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_accepts_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"ccr_to_compare_views".to_string(),
|
||||||
|
"--ccr".to_string(),
|
||||||
|
"a.ccr".to_string(),
|
||||||
|
"--vrps-out".to_string(),
|
||||||
|
"vrps.csv".to_string(),
|
||||||
|
"--vaps-out".to_string(),
|
||||||
|
"vaps.csv".to_string(),
|
||||||
|
"--trust-anchor".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse args");
|
||||||
|
assert_eq!(
|
||||||
|
args.ccr_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("a.ccr"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
args.vrps_out_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("vrps.csv"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
args.vaps_out_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("vaps.csv"))
|
||||||
|
);
|
||||||
|
assert_eq!(args.trust_anchor, "apnic");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"ccr_to_compare_views".to_string(),
|
||||||
|
"--ccr".to_string(),
|
||||||
|
"a.ccr".to_string(),
|
||||||
|
"--vrps-out".to_string(),
|
||||||
|
"vrps.csv".to_string(),
|
||||||
|
];
|
||||||
|
let err = parse_args(&argv).unwrap_err();
|
||||||
|
assert!(err.contains("--vaps-out is required"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/bin/ccr_to_routinator_csv.rs
Normal file
121
src/bin/ccr_to_routinator_csv.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use rpki::ccr::{decode_content_info, extract_vrp_rows};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct Args {
|
||||||
|
ccr_path: Option<std::path::PathBuf>,
|
||||||
|
out_path: Option<std::path::PathBuf>,
|
||||||
|
trust_anchor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: ccr_to_routinator_csv --ccr <path> --out <path> [--trust-anchor <name>]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args {
|
||||||
|
trust_anchor: "unknown".to_string(),
|
||||||
|
..Args::default()
|
||||||
|
};
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--ccr" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--ccr requires a value")?;
|
||||||
|
args.ccr_path = Some(v.into());
|
||||||
|
}
|
||||||
|
"--out" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--out requires a value")?;
|
||||||
|
args.out_path = Some(v.into());
|
||||||
|
}
|
||||||
|
"--trust-anchor" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--trust-anchor requires a value")?;
|
||||||
|
args.trust_anchor = v.clone();
|
||||||
|
}
|
||||||
|
"-h" | "--help" => return Err(usage().to_string()),
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if args.ccr_path.is_none() {
|
||||||
|
return Err(format!("--ccr is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.out_path.is_none() {
|
||||||
|
return Err(format!("--out is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_vrp_rows(
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Result<std::collections::BTreeSet<(u32, String, u16)>, String> {
|
||||||
|
let content_info = decode_content_info(bytes).map_err(|e| e.to_string())?;
|
||||||
|
extract_vrp_rows(&content_info).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
let args = parse_args(&argv)?;
|
||||||
|
let ccr_path = args.ccr_path.as_ref().unwrap();
|
||||||
|
let out_path = args.out_path.as_ref().unwrap();
|
||||||
|
let bytes = std::fs::read(ccr_path)
|
||||||
|
.map_err(|e| format!("read ccr failed: {}: {e}", ccr_path.display()))?;
|
||||||
|
let rows = collect_vrp_rows(&bytes)?;
|
||||||
|
if let Some(parent) = out_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let mut file = std::io::BufWriter::new(
|
||||||
|
std::fs::File::create(out_path)
|
||||||
|
.map_err(|e| format!("create output failed: {}: {e}", out_path.display()))?,
|
||||||
|
);
|
||||||
|
writeln!(file, "ASN,IP Prefix,Max Length,Trust Anchor").map_err(|e| e.to_string())?;
|
||||||
|
for (asn, prefix, max_len) in rows {
|
||||||
|
writeln!(file, "AS{asn},{prefix},{max_len},{}", args.trust_anchor)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
println!("{}", out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_accepts_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"ccr_to_routinator_csv".to_string(),
|
||||||
|
"--ccr".to_string(),
|
||||||
|
"a.ccr".to_string(),
|
||||||
|
"--out".to_string(),
|
||||||
|
"out.csv".to_string(),
|
||||||
|
"--trust-anchor".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse args");
|
||||||
|
assert_eq!(
|
||||||
|
args.ccr_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("a.ccr"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
args.out_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("out.csv"))
|
||||||
|
);
|
||||||
|
assert_eq!(args.trust_anchor, "apnic");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"ccr_to_routinator_csv".to_string(),
|
||||||
|
"--ccr".to_string(),
|
||||||
|
"a.ccr".to_string(),
|
||||||
|
];
|
||||||
|
let err = parse_args(&argv).unwrap_err();
|
||||||
|
assert!(err.contains("--out is required"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/bin/ccr_verify.rs
Normal file
101
src/bin/ccr_verify.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
use rpki::ccr::{
|
||||||
|
decode_content_info, verify::verify_content_info, verify_against_report_json_path,
|
||||||
|
verify_against_vcir_store_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
ccr_path: Option<std::path::PathBuf>,
|
||||||
|
report_json: Option<std::path::PathBuf>,
|
||||||
|
db_path: Option<std::path::PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: ccr_verify --ccr <path> [--report-json <path>] [--db <path>]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args::default();
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--ccr" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--ccr requires a value")?;
|
||||||
|
args.ccr_path = Some(v.into());
|
||||||
|
}
|
||||||
|
"--report-json" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--report-json requires a value")?;
|
||||||
|
args.report_json = Some(v.into());
|
||||||
|
}
|
||||||
|
"--db" => {
|
||||||
|
i += 1;
|
||||||
|
let v = argv.get(i).ok_or("--db requires a value")?;
|
||||||
|
args.db_path = Some(v.into());
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if args.ccr_path.is_none() {
|
||||||
|
return Err(format!("--ccr is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let ccr_path = args.ccr_path.as_ref().unwrap();
|
||||||
|
let bytes = std::fs::read(ccr_path)
|
||||||
|
.map_err(|e| format!("read ccr failed: {}: {e}", ccr_path.display()))?;
|
||||||
|
let ci = decode_content_info(&bytes).map_err(|e| e.to_string())?;
|
||||||
|
let summary = verify_content_info(&ci).map_err(|e| e.to_string())?;
|
||||||
|
if let Some(report_json) = args.report_json.as_ref() {
|
||||||
|
verify_against_report_json_path(&ci, report_json).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
if let Some(db_path) = args.db_path.as_ref() {
|
||||||
|
verify_against_vcir_store_path(&ci, db_path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&summary).map_err(|e| e.to_string())?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_accepts_all_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"ccr_verify".to_string(),
|
||||||
|
"--ccr".to_string(),
|
||||||
|
"a.ccr".to_string(),
|
||||||
|
"--report-json".to_string(),
|
||||||
|
"report.json".to_string(),
|
||||||
|
"--db".to_string(),
|
||||||
|
"db".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(
|
||||||
|
args.ccr_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("a.ccr"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
args.report_json.as_deref(),
|
||||||
|
Some(std::path::Path::new("report.json"))
|
||||||
|
);
|
||||||
|
assert_eq!(args.db_path.as_deref(), Some(std::path::Path::new("db")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_required_ccr() {
|
||||||
|
let argv = vec!["ccr_verify".to_string()];
|
||||||
|
let err = parse_args(&argv).unwrap_err();
|
||||||
|
assert!(err.contains("--ccr is required"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
315
src/bin/cir_drop_report.rs
Normal file
315
src/bin/cir_drop_report.rs
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use rpki::blob_store::{ExternalRawStoreDb, RawObjectStore};
|
||||||
|
use rpki::bundle::decode_ccr_compare_views;
|
||||||
|
use rpki::ccr::decode_content_info;
|
||||||
|
use rpki::cir::{decode_cir, resolve_static_pool_file};
|
||||||
|
use rpki::data_model::roa::RoaObject;
|
||||||
|
|
||||||
|
const USAGE: &str = "Usage: cir_drop_report --cir <path> --ccr <path> --report-json <path> (--static-root <path> | --raw-store-db <path>) --json-out <path> --md-out <path>";
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct DroppedObjectRecord {
|
||||||
|
uri: String,
|
||||||
|
sha256: String,
|
||||||
|
kind: String,
|
||||||
|
reason_code: String,
|
||||||
|
reason_text: Option<String>,
|
||||||
|
publication_point: Option<String>,
|
||||||
|
manifest_uri: Option<String>,
|
||||||
|
derived_vrp_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_reason(detail: Option<&str>, result: &str) -> String {
|
||||||
|
let text = detail.unwrap_or("").to_ascii_lowercase();
|
||||||
|
if text.contains("fetch") {
|
||||||
|
"fetch_failed".to_string()
|
||||||
|
} else if text.contains("manifest") {
|
||||||
|
"manifest_invalid".to_string()
|
||||||
|
} else if text.contains("crl") {
|
||||||
|
"crl_invalid".to_string()
|
||||||
|
} else if text.contains("policy") {
|
||||||
|
"policy_rejected".to_string()
|
||||||
|
} else if text.contains("parse") {
|
||||||
|
"object_parse_failed".to_string()
|
||||||
|
} else if text.contains("signature") || text.contains("cms") {
|
||||||
|
"cms_signature_invalid".to_string()
|
||||||
|
} else if text.contains("resource") {
|
||||||
|
"resource_invalid".to_string()
|
||||||
|
} else if text.contains("expired") || text.contains("not yet valid") {
|
||||||
|
"expired_or_not_yet_valid".to_string()
|
||||||
|
} else if result == "skipped" {
|
||||||
|
"skipped".to_string()
|
||||||
|
} else if result == "error" {
|
||||||
|
"error".to_string()
|
||||||
|
} else {
|
||||||
|
"other".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(
|
||||||
|
argv: &[String],
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
PathBuf,
|
||||||
|
PathBuf,
|
||||||
|
PathBuf,
|
||||||
|
Option<PathBuf>,
|
||||||
|
Option<PathBuf>,
|
||||||
|
PathBuf,
|
||||||
|
PathBuf,
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
> {
|
||||||
|
let mut cir = None;
|
||||||
|
let mut ccr = None;
|
||||||
|
let mut report = None;
|
||||||
|
let mut static_root = None;
|
||||||
|
let mut raw_store_db = None;
|
||||||
|
let mut json_out = None;
|
||||||
|
let mut md_out = None;
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--cir" => {
|
||||||
|
i += 1;
|
||||||
|
cir = Some(PathBuf::from(argv.get(i).ok_or("--cir requires a value")?));
|
||||||
|
}
|
||||||
|
"--ccr" => {
|
||||||
|
i += 1;
|
||||||
|
ccr = Some(PathBuf::from(argv.get(i).ok_or("--ccr requires a value")?));
|
||||||
|
}
|
||||||
|
"--report-json" => {
|
||||||
|
i += 1;
|
||||||
|
report = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--report-json requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--static-root" => {
|
||||||
|
i += 1;
|
||||||
|
static_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--static-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--raw-store-db" => {
|
||||||
|
i += 1;
|
||||||
|
raw_store_db = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--raw-store-db requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--json-out" => {
|
||||||
|
i += 1;
|
||||||
|
json_out = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--json-out requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--md-out" => {
|
||||||
|
i += 1;
|
||||||
|
md_out = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--md-out requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"-h" | "--help" => return Err(USAGE.to_string()),
|
||||||
|
other => return Err(format!("unknown argument: {other}\n\n{USAGE}")),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
cir.ok_or_else(|| format!("--cir is required\n\n{USAGE}"))?,
|
||||||
|
ccr.ok_or_else(|| format!("--ccr is required\n\n{USAGE}"))?,
|
||||||
|
report.ok_or_else(|| format!("--report-json is required\n\n{USAGE}"))?,
|
||||||
|
static_root,
|
||||||
|
raw_store_db,
|
||||||
|
json_out.ok_or_else(|| format!("--json-out is required\n\n{USAGE}"))?,
|
||||||
|
md_out.ok_or_else(|| format!("--md-out is required\n\n{USAGE}"))?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
let (cir_path, ccr_path, report_path, static_root, raw_store_db, json_out, md_out) =
|
||||||
|
parse_args(&argv)?;
|
||||||
|
let backend_count = static_root.is_some() as u8 + raw_store_db.is_some() as u8;
|
||||||
|
if backend_count != 1 {
|
||||||
|
return Err(format!(
|
||||||
|
"must specify exactly one of --static-root or --raw-store-db\n\n{USAGE}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cir = decode_cir(&std::fs::read(&cir_path).map_err(|e| format!("read cir failed: {e}"))?)
|
||||||
|
.map_err(|e| format!("decode cir failed: {e}"))?;
|
||||||
|
let ccr = decode_content_info(
|
||||||
|
&std::fs::read(&ccr_path).map_err(|e| format!("read ccr failed: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("decode ccr failed: {e}"))?;
|
||||||
|
let (vrps, vaps) = decode_ccr_compare_views(&ccr, "unknown")
|
||||||
|
.map_err(|e| format!("decode compare views failed: {e}"))?;
|
||||||
|
let report: serde_json::Value = serde_json::from_slice(
|
||||||
|
&std::fs::read(&report_path).map_err(|e| format!("read report failed: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse report failed: {e}"))?;
|
||||||
|
|
||||||
|
let mut object_hash_by_uri = BTreeMap::new();
|
||||||
|
for object in &cir.objects {
|
||||||
|
object_hash_by_uri.insert(object.rsync_uri.clone(), hex::encode(&object.sha256));
|
||||||
|
}
|
||||||
|
|
||||||
|
let publication_points = report["publication_points"]
|
||||||
|
.as_array()
|
||||||
|
.ok_or("report.publication_points must be an array")?;
|
||||||
|
|
||||||
|
let mut dropped_objects = Vec::new();
|
||||||
|
let mut dropped_vrp_rows = BTreeSet::new();
|
||||||
|
let mut dropped_by_kind: BTreeMap<String, usize> = BTreeMap::new();
|
||||||
|
let mut dropped_by_reason: BTreeMap<String, usize> = BTreeMap::new();
|
||||||
|
let mut unknown_roa_objects = 0usize;
|
||||||
|
|
||||||
|
for pp in publication_points {
|
||||||
|
let publication_point = pp["publication_point_rsync_uri"]
|
||||||
|
.as_str()
|
||||||
|
.map(str::to_string);
|
||||||
|
let manifest_uri = pp["manifest_rsync_uri"].as_str().map(str::to_string);
|
||||||
|
for obj in pp["objects"].as_array().into_iter().flatten() {
|
||||||
|
let result = obj["result"].as_str().unwrap_or("unknown");
|
||||||
|
if result == "ok" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let uri = obj["rsync_uri"].as_str().unwrap_or("").to_string();
|
||||||
|
let hash = obj["sha256_hex"]
|
||||||
|
.as_str()
|
||||||
|
.map(str::to_string)
|
||||||
|
.or_else(|| object_hash_by_uri.get(&uri).cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let kind = obj["kind"].as_str().unwrap_or("other").to_string();
|
||||||
|
let detail = obj["detail"].as_str().map(str::to_string);
|
||||||
|
let reason_code = classify_reason(detail.as_deref(), result);
|
||||||
|
*dropped_by_kind.entry(kind.clone()).or_insert(0) += 1;
|
||||||
|
*dropped_by_reason.entry(reason_code.clone()).or_insert(0) += 1;
|
||||||
|
|
||||||
|
let mut derived_vrp_count = 0usize;
|
||||||
|
if kind == "roa" && !hash.is_empty() {
|
||||||
|
let bytes_opt = if let Some(static_root) = static_root.as_ref() {
|
||||||
|
match resolve_static_pool_file(static_root, &hash) {
|
||||||
|
Ok(path) => std::fs::read(&path).ok(),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
} else if let Some(raw_store_db) = raw_store_db.as_ref() {
|
||||||
|
ExternalRawStoreDb::open(raw_store_db)
|
||||||
|
.ok()
|
||||||
|
.and_then(|store| store.get_raw_entry(&hash).ok().flatten())
|
||||||
|
.map(|entry| entry.bytes)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match bytes_opt {
|
||||||
|
Some(bytes) => {
|
||||||
|
if let Ok(roa) = RoaObject::decode_der(&bytes) {
|
||||||
|
for family in roa.roa.ip_addr_blocks {
|
||||||
|
for addr in family.addresses {
|
||||||
|
let prefix = match addr.prefix.afi {
|
||||||
|
rpki::data_model::roa::RoaAfi::Ipv4 => format!(
|
||||||
|
"{}.{}.{}.{}/{}",
|
||||||
|
addr.prefix.addr[0],
|
||||||
|
addr.prefix.addr[1],
|
||||||
|
addr.prefix.addr[2],
|
||||||
|
addr.prefix.addr[3],
|
||||||
|
addr.prefix.prefix_len
|
||||||
|
),
|
||||||
|
rpki::data_model::roa::RoaAfi::Ipv6 => {
|
||||||
|
let bytes: [u8; 16] = addr.prefix.addr;
|
||||||
|
format!(
|
||||||
|
"{}/{}",
|
||||||
|
std::net::Ipv6Addr::from(bytes),
|
||||||
|
addr.prefix.prefix_len
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let max_len = addr.max_length.unwrap_or(addr.prefix.prefix_len);
|
||||||
|
dropped_vrp_rows.insert((roa.roa.as_id, prefix, max_len));
|
||||||
|
derived_vrp_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unknown_roa_objects += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => unknown_roa_objects += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropped_objects.push(DroppedObjectRecord {
|
||||||
|
uri,
|
||||||
|
sha256: hash,
|
||||||
|
kind,
|
||||||
|
reason_code,
|
||||||
|
reason_text: detail,
|
||||||
|
publication_point: publication_point.clone(),
|
||||||
|
manifest_uri: manifest_uri.clone(),
|
||||||
|
derived_vrp_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = serde_json::json!({
|
||||||
|
"summary": {
|
||||||
|
"finalVrpCount": vrps.len(),
|
||||||
|
"finalVapCount": vaps.len(),
|
||||||
|
"droppedVrpCount": dropped_vrp_rows.len(),
|
||||||
|
"droppedObjectCount": dropped_objects.len(),
|
||||||
|
"droppedByKind": dropped_by_kind,
|
||||||
|
"droppedByReason": dropped_by_reason,
|
||||||
|
"unknownDroppedRoaObjects": unknown_roa_objects,
|
||||||
|
},
|
||||||
|
"objects": dropped_objects,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(parent) = json_out.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| format!("create json parent failed: {e}"))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&json_out, serde_json::to_vec_pretty(&output).unwrap())
|
||||||
|
.map_err(|e| format!("write json failed: {e}"))?;
|
||||||
|
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str("# CIR Drop Report\n\n");
|
||||||
|
md.push_str(&format!("- `final_vrp_count`: `{}`\n", vrps.len()));
|
||||||
|
md.push_str(&format!("- `final_vap_count`: `{}`\n", vaps.len()));
|
||||||
|
md.push_str(&format!(
|
||||||
|
"- `dropped_vrp_count`: `{}`\n",
|
||||||
|
output["summary"]["droppedVrpCount"]
|
||||||
|
));
|
||||||
|
md.push_str(&format!(
|
||||||
|
"- `dropped_object_count`: `{}`\n",
|
||||||
|
output["summary"]["droppedObjectCount"]
|
||||||
|
));
|
||||||
|
md.push_str(&format!(
|
||||||
|
"- `unknown_dropped_roa_objects`: `{}`\n\n",
|
||||||
|
output["summary"]["unknownDroppedRoaObjects"]
|
||||||
|
));
|
||||||
|
md.push_str("## Dropped By Kind\n\n");
|
||||||
|
for (kind, count) in output["summary"]["droppedByKind"]
|
||||||
|
.as_object()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
md.push_str(&format!("- `{kind}`: `{}`\n", count.as_u64().unwrap_or(0)));
|
||||||
|
}
|
||||||
|
md.push_str("\n## Dropped By Reason\n\n");
|
||||||
|
for (reason, count) in output["summary"]["droppedByReason"]
|
||||||
|
.as_object()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
md.push_str(&format!(
|
||||||
|
"- `{reason}`: `{}`\n",
|
||||||
|
count.as_u64().unwrap_or(0)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(parent) = md_out.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create markdown parent failed: {e}"))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&md_out, md).map_err(|e| format!("write markdown failed: {e}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
82
src/bin/cir_extract_inputs.rs
Normal file
82
src/bin/cir_extract_inputs.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: cir_extract_inputs --cir <path> --tals-dir <path> --meta-json <path>"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(err) = run(std::env::args().collect()) {
|
||||||
|
eprintln!("error: {err}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(argv: Vec<String>) -> Result<(), String> {
|
||||||
|
let mut cir_path: Option<PathBuf> = None;
|
||||||
|
let mut tals_dir: Option<PathBuf> = None;
|
||||||
|
let mut meta_json: Option<PathBuf> = None;
|
||||||
|
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--cir" => {
|
||||||
|
i += 1;
|
||||||
|
cir_path = Some(PathBuf::from(argv.get(i).ok_or("--cir requires a value")?));
|
||||||
|
}
|
||||||
|
"--tals-dir" => {
|
||||||
|
i += 1;
|
||||||
|
tals_dir = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--tals-dir requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--meta-json" => {
|
||||||
|
i += 1;
|
||||||
|
meta_json = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--meta-json requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cir_path = cir_path.ok_or_else(|| format!("--cir is required\n\n{}", usage()))?;
|
||||||
|
let tals_dir = tals_dir.ok_or_else(|| format!("--tals-dir is required\n\n{}", usage()))?;
|
||||||
|
let meta_json = meta_json.ok_or_else(|| format!("--meta-json is required\n\n{}", usage()))?;
|
||||||
|
|
||||||
|
let bytes = std::fs::read(&cir_path)
|
||||||
|
.map_err(|e| format!("read CIR failed: {}: {e}", cir_path.display()))?;
|
||||||
|
let cir = rpki::cir::decode_cir(&bytes).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&tals_dir)
|
||||||
|
.map_err(|e| format!("create tals dir failed: {}: {e}", tals_dir.display()))?;
|
||||||
|
|
||||||
|
let mut tal_files = Vec::new();
|
||||||
|
for (idx, tal) in cir.tals.iter().enumerate() {
|
||||||
|
let filename = format!("tal-{:03}.tal", idx + 1);
|
||||||
|
let path = tals_dir.join(filename);
|
||||||
|
std::fs::write(&path, &tal.tal_bytes)
|
||||||
|
.map_err(|e| format!("write TAL failed: {}: {e}", path.display()))?;
|
||||||
|
tal_files.push(serde_json::json!({
|
||||||
|
"talUri": tal.tal_uri,
|
||||||
|
"path": path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let validation_time = cir
|
||||||
|
.validation_time
|
||||||
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
|
.map_err(|e| format!("format validationTime failed: {e}"))?;
|
||||||
|
let meta = serde_json::json!({
|
||||||
|
"validationTime": validation_time,
|
||||||
|
"talFiles": tal_files,
|
||||||
|
});
|
||||||
|
if let Some(parent) = meta_json.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create meta parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&meta_json, serde_json::to_vec_pretty(&meta).unwrap())
|
||||||
|
.map_err(|e| format!("write meta json failed: {}: {e}", meta_json.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
96
src/bin/cir_materialize.rs
Normal file
96
src/bin/cir_materialize.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: cir_materialize --cir <path> (--static-root <path> | --raw-store-db <path>) --mirror-root <path> [--keep-db]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(err) = run(std::env::args().collect()) {
|
||||||
|
eprintln!("error: {err}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(argv: Vec<String>) -> Result<(), String> {
|
||||||
|
let mut cir_path: Option<PathBuf> = None;
|
||||||
|
let mut static_root: Option<PathBuf> = None;
|
||||||
|
let mut raw_store_db: Option<PathBuf> = None;
|
||||||
|
let mut mirror_root: Option<PathBuf> = None;
|
||||||
|
let mut keep_db = false;
|
||||||
|
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--cir" => {
|
||||||
|
i += 1;
|
||||||
|
cir_path = Some(PathBuf::from(argv.get(i).ok_or("--cir requires a value")?));
|
||||||
|
}
|
||||||
|
"--static-root" => {
|
||||||
|
i += 1;
|
||||||
|
static_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--static-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--raw-store-db" => {
|
||||||
|
i += 1;
|
||||||
|
raw_store_db = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--raw-store-db requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--mirror-root" => {
|
||||||
|
i += 1;
|
||||||
|
mirror_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--mirror-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--keep-db" => keep_db = true,
|
||||||
|
other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cir_path = cir_path.ok_or_else(|| format!("--cir is required\n\n{}", usage()))?;
|
||||||
|
let mirror_root =
|
||||||
|
mirror_root.ok_or_else(|| format!("--mirror-root is required\n\n{}", usage()))?;
|
||||||
|
let backend_count = static_root.is_some() as u8 + raw_store_db.is_some() as u8;
|
||||||
|
if backend_count != 1 {
|
||||||
|
return Err(format!(
|
||||||
|
"must specify exactly one of --static-root or --raw-store-db\n\n{}",
|
||||||
|
usage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = std::fs::read(&cir_path)
|
||||||
|
.map_err(|e| format!("read CIR failed: {}: {e}", cir_path.display()))?;
|
||||||
|
let cir = rpki::cir::decode_cir(&bytes).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let result = if let Some(static_root) = static_root {
|
||||||
|
rpki::cir::materialize_cir(&cir, &static_root, &mirror_root, true)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else if let Some(raw_store_db) = raw_store_db {
|
||||||
|
rpki::cir::materialize_cir_from_raw_store(&cir, &raw_store_db, &mirror_root, true)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
unreachable!("validated backend count")
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(summary) => {
|
||||||
|
eprintln!(
|
||||||
|
"materialized CIR: mirror={} objects={} linked={} copied={} keep_db={}",
|
||||||
|
mirror_root.display(),
|
||||||
|
summary.object_count,
|
||||||
|
summary.linked_files,
|
||||||
|
summary.copied_files,
|
||||||
|
keep_db
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if !keep_db && mirror_root.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(&mirror_root);
|
||||||
|
}
|
||||||
|
Err(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/bin/cir_ta_only_fixture.rs
Normal file
124
src/bin/cir_ta_only_fixture.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use rpki::cir::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, encode_cir,
|
||||||
|
write_bytes_to_static_pool,
|
||||||
|
};
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --static-root <path>";
|
||||||
|
|
||||||
|
fn parse_args(
|
||||||
|
argv: &[String],
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
PathBuf,
|
||||||
|
PathBuf,
|
||||||
|
String,
|
||||||
|
time::OffsetDateTime,
|
||||||
|
PathBuf,
|
||||||
|
PathBuf,
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
> {
|
||||||
|
let mut tal_path = None;
|
||||||
|
let mut ta_path = None;
|
||||||
|
let mut tal_uri = None;
|
||||||
|
let mut validation_time = None;
|
||||||
|
let mut cir_out = None;
|
||||||
|
let mut static_root = None;
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--tal-path" => {
|
||||||
|
i += 1;
|
||||||
|
tal_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--tal-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--ta-path" => {
|
||||||
|
i += 1;
|
||||||
|
ta_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--ta-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--tal-uri" => {
|
||||||
|
i += 1;
|
||||||
|
tal_uri = Some(argv.get(i).ok_or("--tal-uri requires a value")?.clone());
|
||||||
|
}
|
||||||
|
"--validation-time" => {
|
||||||
|
i += 1;
|
||||||
|
let raw = argv.get(i).ok_or("--validation-time requires a value")?;
|
||||||
|
validation_time = Some(
|
||||||
|
time::OffsetDateTime::parse(
|
||||||
|
raw,
|
||||||
|
&time::format_description::well_known::Rfc3339,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("invalid validation time: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--cir-out" => {
|
||||||
|
i += 1;
|
||||||
|
cir_out = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--cir-out requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--static-root" => {
|
||||||
|
i += 1;
|
||||||
|
static_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--static-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"-h" | "--help" => return Err(USAGE.to_string()),
|
||||||
|
other => return Err(format!("unknown argument: {other}\n\n{USAGE}")),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
tal_path.ok_or_else(|| format!("--tal-path is required\n\n{USAGE}"))?,
|
||||||
|
ta_path.ok_or_else(|| format!("--ta-path is required\n\n{USAGE}"))?,
|
||||||
|
tal_uri.ok_or_else(|| format!("--tal-uri is required\n\n{USAGE}"))?,
|
||||||
|
validation_time.ok_or_else(|| format!("--validation-time is required\n\n{USAGE}"))?,
|
||||||
|
cir_out.ok_or_else(|| format!("--cir-out is required\n\n{USAGE}"))?,
|
||||||
|
static_root.ok_or_else(|| format!("--static-root is required\n\n{USAGE}"))?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
let (tal_path, ta_path, tal_uri, validation_time, cir_out, static_root) = parse_args(&argv)?;
|
||||||
|
|
||||||
|
let tal_bytes = std::fs::read(&tal_path).map_err(|e| format!("read tal failed: {e}"))?;
|
||||||
|
let ta_bytes = std::fs::read(&ta_path).map_err(|e| format!("read ta failed: {e}"))?;
|
||||||
|
let tal = rpki::data_model::tal::Tal::decode_bytes(&tal_bytes)
|
||||||
|
.map_err(|e| format!("decode tal failed: {e}"))?;
|
||||||
|
let ta_rsync_uri = tal
|
||||||
|
.ta_uris
|
||||||
|
.iter()
|
||||||
|
.find(|uri| uri.scheme() == "rsync")
|
||||||
|
.ok_or("tal must contain an rsync URI")?
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let sha = sha2::Sha256::digest(&ta_bytes);
|
||||||
|
let hash_hex = hex::encode(sha);
|
||||||
|
write_bytes_to_static_pool(&static_root, validation_time.date(), &hash_hex, &ta_bytes)
|
||||||
|
.map_err(|e| format!("write static pool failed: {e}"))?;
|
||||||
|
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time,
|
||||||
|
objects: vec![CirObject {
|
||||||
|
rsync_uri: ta_rsync_uri,
|
||||||
|
sha256: sha.to_vec(),
|
||||||
|
}],
|
||||||
|
tals: vec![CirTal { tal_uri, tal_bytes }],
|
||||||
|
};
|
||||||
|
let der = encode_cir(&cir).map_err(|e| format!("encode cir failed: {e}"))?;
|
||||||
|
if let Some(parent) = cir_out.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| format!("create cir parent failed: {e}"))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&cir_out, der).map_err(|e| format!("write cir failed: {e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -3,9 +3,8 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use rocksdb::{DB, IteratorMode, Options};
|
use rocksdb::{DB, IteratorMode, Options};
|
||||||
use rpki::storage::{
|
use rpki::storage::{
|
||||||
ALL_COLUMN_FAMILY_NAMES, CF_AUDIT_RULE_INDEX, CF_RAW_BY_HASH, CF_RAW_OBJECTS,
|
ALL_COLUMN_FAMILY_NAMES, CF_AUDIT_RULE_INDEX, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
|
||||||
CF_REPOSITORY_VIEW, CF_RRDP_OBJECT_INDEX, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_STATE,
|
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
|
||||||
CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -48,8 +47,7 @@ Options:
|
|||||||
Output groups:
|
Output groups:
|
||||||
- current_repository_view: repository_view + raw_by_hash
|
- current_repository_view: repository_view + raw_by_hash
|
||||||
- current_validation_state: vcir + audit_rule_index
|
- current_validation_state: vcir + audit_rule_index
|
||||||
- current_rrdp_state: rrdp_state + rrdp_source + rrdp_source_member + rrdp_uri_owner
|
- current_rrdp_state: rrdp_source + rrdp_source_member + rrdp_uri_owner
|
||||||
- legacy_compatibility: raw_objects + rrdp_object_index
|
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -78,10 +76,7 @@ fn cf_group(cf_name: &str) -> CfGroup {
|
|||||||
match cf_name {
|
match cf_name {
|
||||||
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
|
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
|
||||||
CF_VCIR | CF_AUDIT_RULE_INDEX => CfGroup::CurrentValidationState,
|
CF_VCIR | CF_AUDIT_RULE_INDEX => CfGroup::CurrentValidationState,
|
||||||
CF_RRDP_STATE | CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => {
|
CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => CfGroup::CurrentRrdpState,
|
||||||
CfGroup::CurrentRrdpState
|
|
||||||
}
|
|
||||||
CF_RAW_OBJECTS | CF_RRDP_OBJECT_INDEX => CfGroup::LegacyCompatibility,
|
|
||||||
_ => CfGroup::LegacyCompatibility,
|
_ => CfGroup::LegacyCompatibility,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +170,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(cf_group(CF_RRDP_SOURCE), CfGroup::CurrentRrdpState);
|
assert_eq!(cf_group(CF_RRDP_SOURCE), CfGroup::CurrentRrdpState);
|
||||||
assert_eq!(cf_group(CF_RRDP_URI_OWNER), CfGroup::CurrentRrdpState);
|
assert_eq!(cf_group(CF_RRDP_URI_OWNER), CfGroup::CurrentRrdpState);
|
||||||
assert_eq!(cf_group(CF_RAW_OBJECTS), CfGroup::LegacyCompatibility);
|
assert_eq!(cf_group("unknown_legacy"), CfGroup::LegacyCompatibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -185,15 +180,13 @@ mod tests {
|
|||||||
(CF_RAW_BY_HASH, 7),
|
(CF_RAW_BY_HASH, 7),
|
||||||
(CF_VCIR, 11),
|
(CF_VCIR, 11),
|
||||||
(CF_AUDIT_RULE_INDEX, 13),
|
(CF_AUDIT_RULE_INDEX, 13),
|
||||||
(CF_RRDP_STATE, 17),
|
|
||||||
(CF_RRDP_SOURCE_MEMBER, 19),
|
(CF_RRDP_SOURCE_MEMBER, 19),
|
||||||
(CF_RRDP_OBJECT_INDEX, 29),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&12));
|
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&12));
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentValidationState), Some(&24));
|
assert_eq!(grouped.get(&CfGroup::CurrentValidationState), Some(&24));
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentRrdpState), Some(&36));
|
assert_eq!(grouped.get(&CfGroup::CurrentRrdpState), Some(&19));
|
||||||
assert_eq!(grouped.get(&CfGroup::LegacyCompatibility), Some(&29));
|
assert_eq!(grouped.get(&CfGroup::LegacyCompatibility), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -201,6 +194,6 @@ mod tests {
|
|||||||
let text = usage();
|
let text = usage();
|
||||||
assert!(text.contains("--exact"), "{text}");
|
assert!(text.contains("--exact"), "{text}");
|
||||||
assert!(text.contains("current_validation_state"), "{text}");
|
assert!(text.contains("current_validation_state"), "{text}");
|
||||||
assert!(text.contains("legacy_compatibility"), "{text}");
|
assert!(text.contains("current_rrdp_state"), "{text}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
257
src/bin/measure_sequence_replay.rs
Normal file
257
src/bin/measure_sequence_replay.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
use rpki::bundle::record_io::load_validation_time;
|
||||||
|
use rpki::storage::RocksStore;
|
||||||
|
use rpki::validation::run_tree_from_tal::{
|
||||||
|
run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||||
|
};
|
||||||
|
use rpki::validation::tree::TreeRunConfig;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: measure_sequence_replay --bundle-root <dir> [--rir <rir[,rir...]>] --out <path> [--keep-db]"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Args {
|
||||||
|
bundle_root: Option<PathBuf>,
|
||||||
|
rirs: Option<Vec<String>>,
|
||||||
|
out: Option<PathBuf>,
|
||||||
|
keep_db: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> Result<Args, String> {
|
||||||
|
let mut out = Args::default();
|
||||||
|
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
let mut i = 0usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--bundle-root" => {
|
||||||
|
i += 1;
|
||||||
|
out.bundle_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--bundle-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--rir" => {
|
||||||
|
i += 1;
|
||||||
|
let value = argv.get(i).ok_or("--rir requires a value")?;
|
||||||
|
out.rirs = Some(
|
||||||
|
value
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_lowercase())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--out" => {
|
||||||
|
i += 1;
|
||||||
|
out.out = Some(PathBuf::from(argv.get(i).ok_or("--out requires a value")?));
|
||||||
|
}
|
||||||
|
"--keep-db" => out.keep_db = true,
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if out.bundle_root.is_none() || out.out.is_none() {
|
||||||
|
return Err(format!("--bundle-root and --out are required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PhaseTiming {
|
||||||
|
duration_seconds: f64,
|
||||||
|
vrp_count: usize,
|
||||||
|
vap_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct RirTiming {
|
||||||
|
rir: String,
|
||||||
|
base: PhaseTiming,
|
||||||
|
steps: Vec<(String, PhaseTiming)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_rirs(bundle_root: &Path) -> Result<Vec<String>, String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for entry in fs::read_dir(bundle_root)
|
||||||
|
.map_err(|e| format!("read_dir failed: {}: {e}", bundle_root.display()))?
|
||||||
|
{
|
||||||
|
let entry = entry.map_err(|e| format!("read_dir entry failed: {e}"))?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() && path.join("bundle.json").exists() && path.join("tal.tal").exists() {
|
||||||
|
out.push(
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.ok_or_else(|| format!("invalid rir dir name: {}", path.display()))?
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_join(root: &Path, relative: &str) -> PathBuf {
|
||||||
|
root.join(relative)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(err) = real_main() {
|
||||||
|
eprintln!("{err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn real_main() -> Result<(), String> {
|
||||||
|
let args = parse_args()?;
|
||||||
|
let bundle_root = args.bundle_root.unwrap();
|
||||||
|
let out_path = args.out.unwrap();
|
||||||
|
let rirs = match args.rirs {
|
||||||
|
Some(v) => v,
|
||||||
|
None => discover_rirs(&bundle_root)?,
|
||||||
|
};
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let tmp_root = out_path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.join(".tmp-sequence-replay");
|
||||||
|
fs::create_dir_all(&tmp_root)
|
||||||
|
.map_err(|e| format!("create tmp root failed: {}: {e}", tmp_root.display()))?;
|
||||||
|
|
||||||
|
for rir in rirs {
|
||||||
|
let rir_dir = bundle_root.join(&rir);
|
||||||
|
let bundle: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(rir_dir.join("bundle.json"))
|
||||||
|
.map_err(|e| format!("read bundle failed: {}: {e}", rir_dir.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse bundle failed for {}: {e}", rir_dir.display()))?;
|
||||||
|
let tal_bytes = fs::read(rir_dir.join("tal.tal"))
|
||||||
|
.map_err(|e| format!("read tal.tal failed for {}: {e}", rir_dir.display()))?;
|
||||||
|
let ta_bytes = fs::read(rir_dir.join("ta.cer"))
|
||||||
|
.map_err(|e| format!("read ta.cer failed for {}: {e}", rir_dir.display()))?;
|
||||||
|
|
||||||
|
let db_dir = tmp_root.join(format!("{rir}-db"));
|
||||||
|
if db_dir.exists() {
|
||||||
|
fs::remove_dir_all(&db_dir)
|
||||||
|
.map_err(|e| format!("remove old db failed: {}: {e}", db_dir.display()))?;
|
||||||
|
}
|
||||||
|
let store =
|
||||||
|
RocksStore::open(&db_dir).map_err(|e| format!("open rocksdb failed for {rir}: {e}"))?;
|
||||||
|
|
||||||
|
let base_archive = path_join(
|
||||||
|
&rir_dir,
|
||||||
|
bundle["base"]["relativeArchivePath"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("bundle missing base.relativeArchivePath")?,
|
||||||
|
);
|
||||||
|
let base_locks = path_join(
|
||||||
|
&rir_dir,
|
||||||
|
bundle["base"]["relativeLocksPath"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("bundle missing base.relativeLocksPath")?,
|
||||||
|
);
|
||||||
|
let base_validation_time = load_validation_time(&base_locks)
|
||||||
|
.map_err(|e| format!("load base validation time failed for {rir}: {e}"))?;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let base_out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||||
|
&store,
|
||||||
|
&rpki::policy::Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&base_archive,
|
||||||
|
&base_locks,
|
||||||
|
base_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: None,
|
||||||
|
max_instances: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("base replay failed for {rir}: {e}"))?;
|
||||||
|
let base_timing = PhaseTiming {
|
||||||
|
duration_seconds: start.elapsed().as_secs_f64(),
|
||||||
|
vrp_count: base_out.tree.vrps.len(),
|
||||||
|
vap_count: base_out.tree.aspas.len(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut previous_locks = base_locks.clone();
|
||||||
|
let mut step_timings = Vec::new();
|
||||||
|
for step in bundle["deltaSequence"]["steps"]
|
||||||
|
.as_array()
|
||||||
|
.ok_or("bundle missing deltaSequence.steps")?
|
||||||
|
{
|
||||||
|
let step_id = step["id"].as_str().ok_or("step missing id")?.to_string();
|
||||||
|
let step_dir = path_join(
|
||||||
|
&rir_dir,
|
||||||
|
step["relativePath"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("step missing relativePath")?,
|
||||||
|
);
|
||||||
|
let delta_archive = path_join(
|
||||||
|
&rir_dir,
|
||||||
|
step["relativeArchivePath"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("step missing relativeArchivePath")?,
|
||||||
|
);
|
||||||
|
let delta_locks = path_join(
|
||||||
|
&rir_dir,
|
||||||
|
step["relativeTransitionLocksPath"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("step missing relativeTransitionLocksPath")?,
|
||||||
|
);
|
||||||
|
let validation_time = load_validation_time(&delta_locks).map_err(|e| {
|
||||||
|
format!("load step validation time failed for {rir}/{step_id}: {e}")
|
||||||
|
})?;
|
||||||
|
let start = Instant::now();
|
||||||
|
let step_out = run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
|
||||||
|
&store,
|
||||||
|
&rpki::policy::Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&delta_archive,
|
||||||
|
&previous_locks,
|
||||||
|
&delta_locks,
|
||||||
|
validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: None,
|
||||||
|
max_instances: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("delta step replay failed for {rir}/{step_id}: {e}"))?;
|
||||||
|
step_timings.push((
|
||||||
|
step_id.clone(),
|
||||||
|
PhaseTiming {
|
||||||
|
duration_seconds: start.elapsed().as_secs_f64(),
|
||||||
|
vrp_count: step_out.tree.vrps.len(),
|
||||||
|
vap_count: step_out.tree.aspas.len(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
previous_locks = step_dir.join("target-locks.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(RirTiming {
|
||||||
|
rir,
|
||||||
|
base: base_timing,
|
||||||
|
steps: step_timings,
|
||||||
|
});
|
||||||
|
|
||||||
|
if !args.keep_db && db_dir.exists() {
|
||||||
|
fs::remove_dir_all(&db_dir)
|
||||||
|
.map_err(|e| format!("remove db failed: {}: {e}", db_dir.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
&out_path,
|
||||||
|
serde_json::to_vec_pretty(&results).map_err(|e| format!("encode json failed: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write out failed: {}: {e}", out_path.display()))?;
|
||||||
|
println!("{}", out_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
413
src/bin/replay_bundle_capture.rs
Normal file
413
src/bin/replay_bundle_capture.rs
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
use rpki::bundle::{
|
||||||
|
RecordingHttpFetcher, RecordingRsyncFetcher, RirBundleMetadata,
|
||||||
|
build_single_rir_bundle_manifest, build_vap_compare_rows, build_vrp_compare_rows, sha256_hex,
|
||||||
|
write_json, write_live_base_replay_bundle_inputs, write_live_bundle_rir_readme,
|
||||||
|
write_live_bundle_top_readme, write_timing_json, write_vap_csv, write_vrp_csv,
|
||||||
|
};
|
||||||
|
use rpki::ccr::{build_ccr_from_run, verify_content_info, write_ccr_file};
|
||||||
|
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||||
|
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||||
|
use rpki::policy::Policy;
|
||||||
|
use rpki::storage::RocksStore;
|
||||||
|
use rpki::validation::run_tree_from_tal::{
|
||||||
|
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_serial_audit,
|
||||||
|
};
|
||||||
|
use rpki::validation::tree::TreeRunConfig;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
rir: Option<String>,
|
||||||
|
out_dir: Option<PathBuf>,
|
||||||
|
tal_path: Option<PathBuf>,
|
||||||
|
ta_path: Option<PathBuf>,
|
||||||
|
validation_time: Option<time::OffsetDateTime>,
|
||||||
|
http_timeout_secs: u64,
|
||||||
|
rsync_timeout_secs: u64,
|
||||||
|
rsync_mirror_root: Option<PathBuf>,
|
||||||
|
max_depth: Option<usize>,
|
||||||
|
max_instances: Option<usize>,
|
||||||
|
trust_anchor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: replay_bundle_capture --rir <name> --out-dir <path> --tal-path <path> --ta-path <path> [--validation-time <rfc3339>] [--http-timeout-secs <n>] [--rsync-timeout-secs <n>] [--rsync-mirror-root <path>] [--max-depth <n>] [--max-instances <n>] [--trust-anchor <name>]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args {
|
||||||
|
http_timeout_secs: 20,
|
||||||
|
rsync_timeout_secs: 60,
|
||||||
|
..Args::default()
|
||||||
|
};
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--rir" => {
|
||||||
|
i += 1;
|
||||||
|
args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone());
|
||||||
|
}
|
||||||
|
"--out-dir" => {
|
||||||
|
i += 1;
|
||||||
|
args.out_dir = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--out-dir requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--tal-path" => {
|
||||||
|
i += 1;
|
||||||
|
args.tal_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--tal-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--ta-path" => {
|
||||||
|
i += 1;
|
||||||
|
args.ta_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--ta-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--validation-time" => {
|
||||||
|
i += 1;
|
||||||
|
let value = argv.get(i).ok_or("--validation-time requires a value")?;
|
||||||
|
args.validation_time = Some(
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||||
|
.map_err(|e| format!("invalid --validation-time: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--http-timeout-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.http_timeout_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--http-timeout-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --http-timeout-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--rsync-timeout-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.rsync_timeout_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--rsync-timeout-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --rsync-timeout-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--rsync-mirror-root" => {
|
||||||
|
i += 1;
|
||||||
|
args.rsync_mirror_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--rsync-mirror-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--max-depth" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_depth = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-depth requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-depth: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--max-instances" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_instances = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-instances requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-instances: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--trust-anchor" => {
|
||||||
|
i += 1;
|
||||||
|
args.trust_anchor = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--trust-anchor requires a value")?
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.rir.is_none() {
|
||||||
|
return Err(format!("--rir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.out_dir.is_none() {
|
||||||
|
return Err(format!("--out-dir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.tal_path.is_none() {
|
||||||
|
return Err(format!("--tal-path is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.ta_path.is_none() {
|
||||||
|
return Err(format!("--ta-path is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: Args) -> Result<PathBuf, String> {
|
||||||
|
let rir = args.rir.as_ref().unwrap();
|
||||||
|
let rir_normalized = rir.to_ascii_lowercase();
|
||||||
|
let trust_anchor = args
|
||||||
|
.trust_anchor
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| rir_normalized.clone());
|
||||||
|
let out_root = args.out_dir.as_ref().unwrap();
|
||||||
|
let rir_dir = out_root.join(&rir_normalized);
|
||||||
|
fs::create_dir_all(&rir_dir)
|
||||||
|
.map_err(|e| format!("create rir dir failed: {}: {e}", rir_dir.display()))?;
|
||||||
|
|
||||||
|
let tal_bytes =
|
||||||
|
fs::read(args.tal_path.as_ref().unwrap()).map_err(|e| format!("read tal failed: {e}"))?;
|
||||||
|
let ta_bytes =
|
||||||
|
fs::read(args.ta_path.as_ref().unwrap()).map_err(|e| format!("read ta failed: {e}"))?;
|
||||||
|
let validation_time = args
|
||||||
|
.validation_time
|
||||||
|
.unwrap_or_else(time::OffsetDateTime::now_utc);
|
||||||
|
|
||||||
|
let db_dir = out_root.join(".tmp").join(format!("{rir}-live-base-db"));
|
||||||
|
let replay_db_dir = out_root.join(".tmp").join(format!("{rir}-self-replay-db"));
|
||||||
|
let _ = fs::remove_dir_all(&db_dir);
|
||||||
|
let _ = fs::remove_dir_all(&replay_db_dir);
|
||||||
|
if let Some(parent) = db_dir.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create tmp dir failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = RocksStore::open(&db_dir).map_err(|e| format!("open rocksdb failed: {e}"))?;
|
||||||
|
let http = RecordingHttpFetcher::new(
|
||||||
|
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||||
|
timeout: std::time::Duration::from_secs(args.http_timeout_secs),
|
||||||
|
..HttpFetcherConfig::default()
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("create http fetcher failed: {e}"))?,
|
||||||
|
);
|
||||||
|
let rsync = RecordingRsyncFetcher::new(SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||||
|
timeout: std::time::Duration::from_secs(args.rsync_timeout_secs),
|
||||||
|
mirror_root: args.rsync_mirror_root.clone(),
|
||||||
|
..SystemRsyncConfig::default()
|
||||||
|
}));
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
let out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||||
|
&store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&http,
|
||||||
|
&rsync,
|
||||||
|
validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("live base run failed: {e}"))?;
|
||||||
|
let duration = started.elapsed();
|
||||||
|
|
||||||
|
let ccr = build_ccr_from_run(
|
||||||
|
&store,
|
||||||
|
&[out.discovery.trust_anchor.clone()],
|
||||||
|
&out.tree.vrps,
|
||||||
|
&out.tree.aspas,
|
||||||
|
&out.tree.router_keys,
|
||||||
|
validation_time,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("build ccr failed: {e}"))?;
|
||||||
|
let base_ccr_path = rir_dir.join("base.ccr");
|
||||||
|
write_ccr_file(&base_ccr_path, &ccr).map_err(|e| format!("write ccr failed: {e}"))?;
|
||||||
|
let ccr_bytes = fs::read(&base_ccr_path)
|
||||||
|
.map_err(|e| format!("read written ccr failed: {}: {e}", base_ccr_path.display()))?;
|
||||||
|
let decoded = rpki::ccr::decode_content_info(&ccr_bytes)
|
||||||
|
.map_err(|e| format!("decode written ccr failed: {e}"))?;
|
||||||
|
let verify = verify_content_info(&decoded).map_err(|e| format!("verify ccr failed: {e}"))?;
|
||||||
|
|
||||||
|
let vrp_rows = build_vrp_compare_rows(&out.tree.vrps, &trust_anchor);
|
||||||
|
let vap_rows = build_vap_compare_rows(&out.tree.aspas, &trust_anchor);
|
||||||
|
let (ccr_vrps, ccr_vaps) = rpki::bundle::decode_ccr_compare_views(&decoded, &trust_anchor)?;
|
||||||
|
if vrp_rows != ccr_vrps {
|
||||||
|
return Err("base-vrps compare view does not match base.ccr".to_string());
|
||||||
|
}
|
||||||
|
if vap_rows != ccr_vaps {
|
||||||
|
return Err("base-vaps compare view does not match base.ccr".to_string());
|
||||||
|
}
|
||||||
|
write_vrp_csv(&rir_dir.join("base-vrps.csv"), &vrp_rows)?;
|
||||||
|
write_vap_csv(&rir_dir.join("base-vaps.csv"), &vap_rows)?;
|
||||||
|
|
||||||
|
fs::write(rir_dir.join("tal.tal"), &tal_bytes).map_err(|e| format!("write tal failed: {e}"))?;
|
||||||
|
fs::write(rir_dir.join("ta.cer"), &ta_bytes).map_err(|e| format!("write ta failed: {e}"))?;
|
||||||
|
let capture = write_live_base_replay_bundle_inputs(
|
||||||
|
&rir_dir,
|
||||||
|
&rir_normalized,
|
||||||
|
validation_time,
|
||||||
|
&out.publication_points,
|
||||||
|
&store,
|
||||||
|
&http.snapshot_responses(),
|
||||||
|
&rsync.snapshot_fetches(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let replay_store = RocksStore::open(&replay_db_dir)
|
||||||
|
.map_err(|e| format!("open self replay rocksdb failed: {e}"))?;
|
||||||
|
let replay_out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||||
|
&replay_store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&rir_dir.join("base-payload-archive"),
|
||||||
|
&rir_dir.join("base-locks.json"),
|
||||||
|
validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("self replay failed: {e}"))?;
|
||||||
|
let replay_vrps = build_vrp_compare_rows(&replay_out.tree.vrps, &trust_anchor);
|
||||||
|
let replay_vaps = build_vap_compare_rows(&replay_out.tree.aspas, &trust_anchor);
|
||||||
|
if replay_vrps != vrp_rows {
|
||||||
|
return Err("self replay VRP compare view mismatch".to_string());
|
||||||
|
}
|
||||||
|
if replay_vaps != vap_rows {
|
||||||
|
return Err("self replay VAP compare view mismatch".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(rir_dir.join("timings"))
|
||||||
|
.map_err(|e| format!("create timings dir failed: {e}"))?;
|
||||||
|
write_timing_json(
|
||||||
|
&rir_dir.join("timings").join("base-produce.json"),
|
||||||
|
"base",
|
||||||
|
&validation_time,
|
||||||
|
duration,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let metadata = RirBundleMetadata {
|
||||||
|
schema_version: "20260330-v1".to_string(),
|
||||||
|
bundle_producer: "ours".to_string(),
|
||||||
|
rir: rir_normalized.clone(),
|
||||||
|
base_validation_time: validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
delta_validation_time: None,
|
||||||
|
tal_sha256: sha256_hex(&tal_bytes),
|
||||||
|
ta_cert_sha256: sha256_hex(&ta_bytes),
|
||||||
|
base_ccr_sha256: sha256_hex(&ccr_bytes),
|
||||||
|
delta_ccr_sha256: None,
|
||||||
|
has_aspa: !vap_rows.is_empty(),
|
||||||
|
has_router_key: verify.router_key_count > 0,
|
||||||
|
base_vrp_count: vrp_rows.len(),
|
||||||
|
base_vap_count: vap_rows.len(),
|
||||||
|
delta_vrp_count: None,
|
||||||
|
delta_vap_count: None,
|
||||||
|
};
|
||||||
|
write_json(&rir_dir.join("bundle.json"), &metadata)?;
|
||||||
|
write_json(
|
||||||
|
&rir_dir.join("verification.json"),
|
||||||
|
&serde_json::json!({
|
||||||
|
"base": {
|
||||||
|
"validationTime": metadata.base_validation_time,
|
||||||
|
"ccr": {
|
||||||
|
"path": "base.ccr",
|
||||||
|
"sha256": metadata.base_ccr_sha256,
|
||||||
|
"stateHashesOk": verify.state_hashes_ok,
|
||||||
|
"manifestInstances": verify.manifest_instances,
|
||||||
|
"roaVrpCount": verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"vrpsSelfMatch": true,
|
||||||
|
"vapsSelfMatch": true,
|
||||||
|
"baseVrpCount": metadata.base_vrp_count,
|
||||||
|
"baseVapCount": metadata.base_vap_count,
|
||||||
|
},
|
||||||
|
"capture": {
|
||||||
|
"captureId": capture.capture_id,
|
||||||
|
"rrdpRepoCount": capture.rrdp_repo_count,
|
||||||
|
"rsyncModuleCount": capture.rsync_module_count,
|
||||||
|
"selfReplayOk": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
write_live_bundle_top_readme(&out_root.join("README.md"), &rir_normalized)?;
|
||||||
|
write_live_bundle_rir_readme(
|
||||||
|
&rir_dir.join("README.md"),
|
||||||
|
&rir_normalized,
|
||||||
|
&metadata.base_validation_time,
|
||||||
|
)?;
|
||||||
|
write_json(
|
||||||
|
&out_root.join("bundle-manifest.json"),
|
||||||
|
&build_single_rir_bundle_manifest(
|
||||||
|
"20260330-v1",
|
||||||
|
"ours",
|
||||||
|
&rir_normalized,
|
||||||
|
&validation_time,
|
||||||
|
None,
|
||||||
|
metadata.has_aspa,
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&db_dir);
|
||||||
|
let _ = fs::remove_dir_all(&replay_db_dir);
|
||||||
|
|
||||||
|
Ok(out_root.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let out = run(args)?;
|
||||||
|
println!("{}", out.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_requires_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"replay_bundle_capture".to_string(),
|
||||||
|
"--rir".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
"--out-dir".to_string(),
|
||||||
|
"out".to_string(),
|
||||||
|
"--tal-path".to_string(),
|
||||||
|
"tal".to_string(),
|
||||||
|
"--ta-path".to_string(),
|
||||||
|
"ta".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(args.rir.as_deref(), Some("apnic"));
|
||||||
|
assert_eq!(args.out_dir.as_deref(), Some(std::path::Path::new("out")));
|
||||||
|
assert_eq!(args.http_timeout_secs, 20);
|
||||||
|
assert_eq!(args.rsync_timeout_secs, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_requireds() {
|
||||||
|
let err = parse_args(&["replay_bundle_capture".to_string()]).unwrap_err();
|
||||||
|
assert!(err.contains("--rir is required"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_timing_json_writes_duration_and_mode() {
|
||||||
|
let td = tempdir().expect("tempdir");
|
||||||
|
let path = td.path().join("timings/base-produce.json");
|
||||||
|
write_timing_json(
|
||||||
|
&path,
|
||||||
|
"base",
|
||||||
|
&time::OffsetDateTime::parse("2026-03-30T00:00:00Z", &Rfc3339).expect("time"),
|
||||||
|
std::time::Duration::from_millis(1500),
|
||||||
|
)
|
||||||
|
.expect("write timing");
|
||||||
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_slice(&std::fs::read(&path).expect("read timing")).expect("parse");
|
||||||
|
assert_eq!(json["mode"], "base");
|
||||||
|
assert_eq!(json["durationSeconds"], 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
483
src/bin/replay_bundle_capture_delta.rs
Normal file
483
src/bin/replay_bundle_capture_delta.rs
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
use rpki::bundle::{
|
||||||
|
RecordingHttpFetcher, RecordingRsyncFetcher, build_single_rir_bundle_manifest,
|
||||||
|
build_vap_compare_rows, build_vrp_compare_rows, copy_dir_all, load_validation_time, sha256_hex,
|
||||||
|
write_json, write_live_delta_replay_bundle_inputs, write_vap_csv, write_vrp_csv,
|
||||||
|
};
|
||||||
|
use rpki::ccr::{build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file};
|
||||||
|
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||||
|
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||||
|
use rpki::policy::Policy;
|
||||||
|
use rpki::storage::RocksStore;
|
||||||
|
use rpki::sync::rrdp::Fetcher;
|
||||||
|
use rpki::validation::run_tree_from_tal::{
|
||||||
|
run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_serial_audit,
|
||||||
|
};
|
||||||
|
use rpki::validation::tree::TreeRunConfig;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Instant;
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
rir: Option<String>,
|
||||||
|
base_bundle_dir: Option<PathBuf>,
|
||||||
|
out_dir: Option<PathBuf>,
|
||||||
|
validation_time: Option<time::OffsetDateTime>,
|
||||||
|
http_timeout_secs: u64,
|
||||||
|
rsync_timeout_secs: u64,
|
||||||
|
rsync_mirror_root: Option<PathBuf>,
|
||||||
|
max_depth: Option<usize>,
|
||||||
|
max_instances: Option<usize>,
|
||||||
|
trust_anchor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: replay_bundle_capture_delta --rir <name> --base-bundle-dir <path> --out-dir <path> [--validation-time <rfc3339>] [--http-timeout-secs <n>] [--rsync-timeout-secs <n>] [--rsync-mirror-root <path>] [--max-depth <n>] [--max-instances <n>] [--trust-anchor <name>]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args {
|
||||||
|
http_timeout_secs: 20,
|
||||||
|
rsync_timeout_secs: 60,
|
||||||
|
..Args::default()
|
||||||
|
};
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--rir" => {
|
||||||
|
i += 1;
|
||||||
|
args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone());
|
||||||
|
}
|
||||||
|
"--base-bundle-dir" => {
|
||||||
|
i += 1;
|
||||||
|
args.base_bundle_dir = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--base-bundle-dir requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--out-dir" => {
|
||||||
|
i += 1;
|
||||||
|
args.out_dir = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--out-dir requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--validation-time" => {
|
||||||
|
i += 1;
|
||||||
|
let value = argv.get(i).ok_or("--validation-time requires a value")?;
|
||||||
|
args.validation_time = Some(
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||||
|
.map_err(|e| format!("invalid --validation-time: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--http-timeout-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.http_timeout_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--http-timeout-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --http-timeout-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--rsync-timeout-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.rsync_timeout_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--rsync-timeout-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --rsync-timeout-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--rsync-mirror-root" => {
|
||||||
|
i += 1;
|
||||||
|
args.rsync_mirror_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--rsync-mirror-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--max-depth" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_depth = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-depth requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-depth: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--max-instances" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_instances = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-instances requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-instances: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--trust-anchor" => {
|
||||||
|
i += 1;
|
||||||
|
args.trust_anchor = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--trust-anchor requires a value")?
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.rir.is_none() {
|
||||||
|
return Err(format!("--rir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.base_bundle_dir.is_none() {
|
||||||
|
return Err(format!("--base-bundle-dir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.out_dir.is_none() {
|
||||||
|
return Err(format!("--out-dir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_recorded_target_snapshots(
|
||||||
|
store: &RocksStore,
|
||||||
|
base_bundle_dir: &Path,
|
||||||
|
http: &RecordingHttpFetcher<BlockingHttpFetcher>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let base_locks: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(base_bundle_dir.join("base-locks.json"))
|
||||||
|
.map_err(|e| format!("read base locks failed: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse base locks failed: {e}"))?;
|
||||||
|
let base_rrdp = base_locks
|
||||||
|
.get("rrdp")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
for (notify_uri, base_lock) in base_rrdp {
|
||||||
|
let Some(base_transport) = base_lock.get("transport").and_then(|v| v.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if base_transport != "rrdp" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(base_session) = base_lock.get("session").and_then(|v| v.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(base_serial) = base_lock.get("serial").and_then(|v| v.as_u64()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(record) = store
|
||||||
|
.get_rrdp_source_record(¬ify_uri)
|
||||||
|
.map_err(|e| format!("read rrdp source record failed for {notify_uri}: {e}"))?
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(target_session) = record.last_session_id.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(target_serial) = record.last_serial else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if target_session != base_session || target_serial <= base_serial {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(snapshot_uri) = record.last_snapshot_uri.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if http.snapshot_responses().contains_key(snapshot_uri) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = http
|
||||||
|
.fetch(snapshot_uri)
|
||||||
|
.map_err(|e| format!("fetch target snapshot for {notify_uri} failed: {e}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: Args) -> Result<PathBuf, String> {
|
||||||
|
let rir = args.rir.as_ref().unwrap();
|
||||||
|
let rir_normalized = rir.to_ascii_lowercase();
|
||||||
|
let out_root = args.out_dir.as_ref().unwrap();
|
||||||
|
let base_root = args.base_bundle_dir.as_ref().unwrap();
|
||||||
|
let base_rir_dir = base_root.join(&rir_normalized);
|
||||||
|
if !base_rir_dir.is_dir() {
|
||||||
|
return Err(format!(
|
||||||
|
"base bundle rir dir not found: {}",
|
||||||
|
base_rir_dir.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if out_root.exists() {
|
||||||
|
fs::remove_dir_all(out_root)
|
||||||
|
.map_err(|e| format!("remove old out dir failed: {}: {e}", out_root.display()))?;
|
||||||
|
}
|
||||||
|
copy_dir_all(base_root, out_root)?;
|
||||||
|
let rir_dir = out_root.join(&rir_normalized);
|
||||||
|
|
||||||
|
let trust_anchor = args
|
||||||
|
.trust_anchor
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| rir_normalized.clone());
|
||||||
|
let tal_bytes = fs::read(rir_dir.join("tal.tal"))
|
||||||
|
.map_err(|e| format!("read tal from base bundle failed: {e}"))?;
|
||||||
|
let ta_bytes = fs::read(rir_dir.join("ta.cer"))
|
||||||
|
.map_err(|e| format!("read ta from base bundle failed: {e}"))?;
|
||||||
|
let base_validation_time = load_validation_time(&rir_dir.join("base-locks.json"))?;
|
||||||
|
let target_validation_time = args
|
||||||
|
.validation_time
|
||||||
|
.unwrap_or_else(time::OffsetDateTime::now_utc);
|
||||||
|
|
||||||
|
let target_store_dir = out_root.join(".tmp").join(format!("{rir}-live-target-db"));
|
||||||
|
let self_replay_dir = out_root.join(".tmp").join(format!("{rir}-self-delta-db"));
|
||||||
|
let _ = fs::remove_dir_all(&target_store_dir);
|
||||||
|
let _ = fs::remove_dir_all(&self_replay_dir);
|
||||||
|
if let Some(parent) = target_store_dir.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create tmp dir failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let target_store = RocksStore::open(&target_store_dir)
|
||||||
|
.map_err(|e| format!("open target rocksdb failed: {e}"))?;
|
||||||
|
|
||||||
|
let _base = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||||
|
&target_store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&rir_dir.join("base-payload-archive"),
|
||||||
|
&rir_dir.join("base-locks.json"),
|
||||||
|
base_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("base bootstrap replay failed: {e}"))?;
|
||||||
|
|
||||||
|
let http = RecordingHttpFetcher::new(
|
||||||
|
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||||
|
timeout: std::time::Duration::from_secs(args.http_timeout_secs),
|
||||||
|
..HttpFetcherConfig::default()
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("create http fetcher failed: {e}"))?,
|
||||||
|
);
|
||||||
|
let rsync = RecordingRsyncFetcher::new(SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||||
|
timeout: std::time::Duration::from_secs(args.rsync_timeout_secs),
|
||||||
|
mirror_root: args.rsync_mirror_root.clone(),
|
||||||
|
..SystemRsyncConfig::default()
|
||||||
|
}));
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
let target_out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||||
|
&target_store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&http,
|
||||||
|
&rsync,
|
||||||
|
target_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("live target run failed: {e}"))?;
|
||||||
|
let duration = started.elapsed();
|
||||||
|
ensure_recorded_target_snapshots(&target_store, &rir_dir, &http)?;
|
||||||
|
|
||||||
|
let delta_ccr = build_ccr_from_run(
|
||||||
|
&target_store,
|
||||||
|
&[target_out.discovery.trust_anchor.clone()],
|
||||||
|
&target_out.tree.vrps,
|
||||||
|
&target_out.tree.aspas,
|
||||||
|
&target_out.tree.router_keys,
|
||||||
|
target_validation_time,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("build delta ccr failed: {e}"))?;
|
||||||
|
let delta_ccr_path = rir_dir.join("delta.ccr");
|
||||||
|
write_ccr_file(&delta_ccr_path, &delta_ccr)
|
||||||
|
.map_err(|e| format!("write delta ccr failed: {e}"))?;
|
||||||
|
let delta_ccr_bytes = fs::read(&delta_ccr_path)
|
||||||
|
.map_err(|e| format!("read delta ccr failed: {}: {e}", delta_ccr_path.display()))?;
|
||||||
|
let delta_decoded = decode_content_info(&delta_ccr_bytes)
|
||||||
|
.map_err(|e| format!("decode delta ccr failed: {e}"))?;
|
||||||
|
let delta_verify =
|
||||||
|
verify_content_info(&delta_decoded).map_err(|e| format!("verify delta ccr failed: {e}"))?;
|
||||||
|
|
||||||
|
let delta_vrp_rows = build_vrp_compare_rows(&target_out.tree.vrps, &trust_anchor);
|
||||||
|
let delta_vap_rows = build_vap_compare_rows(&target_out.tree.aspas, &trust_anchor);
|
||||||
|
let (ccr_vrps, ccr_vaps) =
|
||||||
|
rpki::bundle::decode_ccr_compare_views(&delta_decoded, &trust_anchor)?;
|
||||||
|
if delta_vrp_rows != ccr_vrps {
|
||||||
|
return Err("record-delta.csv compare view does not match delta.ccr".to_string());
|
||||||
|
}
|
||||||
|
if delta_vap_rows != ccr_vaps {
|
||||||
|
return Err("record-delta-vaps.csv compare view does not match delta.ccr".to_string());
|
||||||
|
}
|
||||||
|
write_vrp_csv(&rir_dir.join("record-delta.csv"), &delta_vrp_rows)?;
|
||||||
|
write_vap_csv(&rir_dir.join("record-delta-vaps.csv"), &delta_vap_rows)?;
|
||||||
|
|
||||||
|
let capture = write_live_delta_replay_bundle_inputs(
|
||||||
|
&rir_dir,
|
||||||
|
&rir_normalized,
|
||||||
|
target_validation_time,
|
||||||
|
&target_out.publication_points,
|
||||||
|
&target_store,
|
||||||
|
&http.snapshot_responses(),
|
||||||
|
&rsync.snapshot_fetches(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let self_store = RocksStore::open(&self_replay_dir)
|
||||||
|
.map_err(|e| format!("open self replay db failed: {e}"))?;
|
||||||
|
let replay_out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
|
||||||
|
&self_store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&rir_dir.join("base-payload-archive"),
|
||||||
|
&rir_dir.join("base-locks.json"),
|
||||||
|
&rir_dir.join("payload-delta-archive"),
|
||||||
|
&rir_dir.join("locks-delta.json"),
|
||||||
|
base_validation_time,
|
||||||
|
target_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("self delta replay failed: {e}"))?;
|
||||||
|
let replay_vrps = build_vrp_compare_rows(&replay_out.tree.vrps, &trust_anchor);
|
||||||
|
let replay_vaps = build_vap_compare_rows(&replay_out.tree.aspas, &trust_anchor);
|
||||||
|
if replay_vrps != delta_vrp_rows {
|
||||||
|
return Err("self delta replay VRP compare view mismatch".to_string());
|
||||||
|
}
|
||||||
|
if replay_vaps != delta_vap_rows {
|
||||||
|
return Err("self delta replay VAP compare view mismatch".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(rir_dir.join("timings"))
|
||||||
|
.map_err(|e| format!("create timings dir failed: {e}"))?;
|
||||||
|
write_json(
|
||||||
|
&rir_dir.join("timings").join("delta-produce.json"),
|
||||||
|
&serde_json::json!({
|
||||||
|
"mode": "delta",
|
||||||
|
"validationTime": target_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
"durationSeconds": duration.as_secs_f64(),
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut bundle_json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(rir_dir.join("bundle.json"))
|
||||||
|
.map_err(|e| format!("read base bundle.json failed: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse base bundle.json failed: {e}"))?;
|
||||||
|
bundle_json["deltaValidationTime"] = serde_json::Value::String(
|
||||||
|
target_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format delta validation time failed: {e}"))?,
|
||||||
|
);
|
||||||
|
bundle_json["deltaCcrSha256"] = serde_json::Value::String(sha256_hex(&delta_ccr_bytes));
|
||||||
|
bundle_json["deltaVrpCount"] = serde_json::Value::from(delta_vrp_rows.len() as u64);
|
||||||
|
bundle_json["deltaVapCount"] = serde_json::Value::from(delta_vap_rows.len() as u64);
|
||||||
|
bundle_json["hasAspa"] = serde_json::Value::Bool(
|
||||||
|
bundle_json
|
||||||
|
.get("hasAspa")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false)
|
||||||
|
|| !delta_vap_rows.is_empty(),
|
||||||
|
);
|
||||||
|
bundle_json["hasRouterKey"] = serde_json::Value::Bool(
|
||||||
|
bundle_json
|
||||||
|
.get("hasRouterKey")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false)
|
||||||
|
|| delta_verify.router_key_count > 0,
|
||||||
|
);
|
||||||
|
write_json(&rir_dir.join("bundle.json"), &bundle_json)?;
|
||||||
|
|
||||||
|
let mut verification_json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(rir_dir.join("verification.json"))
|
||||||
|
.map_err(|e| format!("read base verification.json failed: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse base verification.json failed: {e}"))?;
|
||||||
|
verification_json["delta"] = serde_json::json!({
|
||||||
|
"validationTime": target_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format delta validation time failed: {e}"))?,
|
||||||
|
"ccr": {
|
||||||
|
"path": "delta.ccr",
|
||||||
|
"sha256": sha256_hex(&delta_ccr_bytes),
|
||||||
|
"stateHashesOk": delta_verify.state_hashes_ok,
|
||||||
|
"manifestInstances": delta_verify.manifest_instances,
|
||||||
|
"roaVrpCount": delta_verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": delta_verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": delta_verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"vrpsSelfMatch": true,
|
||||||
|
"vapsSelfMatch": true,
|
||||||
|
"deltaVrpCount": delta_vrp_rows.len(),
|
||||||
|
"deltaVapCount": delta_vap_rows.len(),
|
||||||
|
},
|
||||||
|
"capture": {
|
||||||
|
"captureId": capture.capture_id,
|
||||||
|
"rrdpRepoCount": capture.rrdp_repo_count,
|
||||||
|
"rsyncModuleCount": capture.rsync_module_count,
|
||||||
|
"selfReplayOk": true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
write_json(&rir_dir.join("verification.json"), &verification_json)?;
|
||||||
|
|
||||||
|
let bundle_manifest = build_single_rir_bundle_manifest(
|
||||||
|
"20260330-v1",
|
||||||
|
"ours",
|
||||||
|
&rir_normalized,
|
||||||
|
&base_validation_time,
|
||||||
|
Some(&target_validation_time),
|
||||||
|
bundle_json["hasAspa"].as_bool().unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
write_json(&out_root.join("bundle-manifest.json"), &bundle_manifest)?;
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&target_store_dir);
|
||||||
|
let _ = fs::remove_dir_all(&self_replay_dir);
|
||||||
|
|
||||||
|
Ok(out_root.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let out = run(args)?;
|
||||||
|
println!("{}", out.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_requires_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"replay_bundle_capture_delta".to_string(),
|
||||||
|
"--rir".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
"--base-bundle-dir".to_string(),
|
||||||
|
"base".to_string(),
|
||||||
|
"--out-dir".to_string(),
|
||||||
|
"out".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(args.rir.as_deref(), Some("apnic"));
|
||||||
|
assert_eq!(args.base_bundle_dir.as_deref(), Some(Path::new("base")));
|
||||||
|
assert_eq!(args.out_dir.as_deref(), Some(Path::new("out")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_requireds() {
|
||||||
|
let err = parse_args(&["replay_bundle_capture_delta".to_string()]).unwrap_err();
|
||||||
|
assert!(err.contains("--rir is required"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
990
src/bin/replay_bundle_capture_sequence.rs
Normal file
990
src/bin/replay_bundle_capture_sequence.rs
Normal file
@@ -0,0 +1,990 @@
|
|||||||
|
use rpki::bundle::{
|
||||||
|
BaseBundleStateMetadataV2, BundleManifestEntryV2, BundleManifestV2, DeltaSequenceMetadataV2,
|
||||||
|
DeltaStepMetadataV2, RecordingHttpFetcher, RecordingRsyncFetcher, RirBundleMetadataV2,
|
||||||
|
build_vap_compare_rows, build_vrp_compare_rows, sha256_hex, write_current_replay_state_locks,
|
||||||
|
write_json, write_live_base_replay_bundle_inputs, write_live_delta_replay_step_inputs,
|
||||||
|
write_vap_csv, write_vrp_csv,
|
||||||
|
};
|
||||||
|
use rpki::ccr::{
|
||||||
|
CcrVerifySummary, build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file,
|
||||||
|
};
|
||||||
|
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||||
|
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||||
|
use rpki::policy::Policy;
|
||||||
|
use rpki::storage::RocksStore;
|
||||||
|
use rpki::sync::rrdp::Fetcher;
|
||||||
|
use rpki::validation::run_tree_from_tal::{
|
||||||
|
run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_serial_audit,
|
||||||
|
};
|
||||||
|
use rpki::validation::tree::TreeRunConfig;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
rir: Option<String>,
|
||||||
|
out_dir: Option<PathBuf>,
|
||||||
|
tal_path: Option<PathBuf>,
|
||||||
|
ta_path: Option<PathBuf>,
|
||||||
|
base_validation_time: Option<time::OffsetDateTime>,
|
||||||
|
delta_count: usize,
|
||||||
|
delta_interval_secs: u64,
|
||||||
|
http_timeout_secs: u64,
|
||||||
|
rsync_timeout_secs: u64,
|
||||||
|
rsync_mirror_root: Option<PathBuf>,
|
||||||
|
max_depth: Option<usize>,
|
||||||
|
max_instances: Option<usize>,
|
||||||
|
trust_anchor: Option<String>,
|
||||||
|
keep_db: bool,
|
||||||
|
capture_inputs_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: replay_bundle_capture_sequence --rir <name> --out-dir <path> --tal-path <path> --ta-path <path> [--base-validation-time <rfc3339>] [--delta-count <n>] [--delta-interval-secs <n>] [--http-timeout-secs <n>] [--rsync-timeout-secs <n>] [--rsync-mirror-root <path>] [--max-depth <n>] [--max-instances <n>] [--trust-anchor <name>] [--keep-db] [--capture-inputs-only]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args {
|
||||||
|
delta_count: 5,
|
||||||
|
delta_interval_secs: 600,
|
||||||
|
http_timeout_secs: 20,
|
||||||
|
rsync_timeout_secs: 60,
|
||||||
|
..Args::default()
|
||||||
|
};
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--rir" => {
|
||||||
|
i += 1;
|
||||||
|
args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone());
|
||||||
|
}
|
||||||
|
"--out-dir" => {
|
||||||
|
i += 1;
|
||||||
|
args.out_dir = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--out-dir requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--tal-path" => {
|
||||||
|
i += 1;
|
||||||
|
args.tal_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--tal-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--ta-path" => {
|
||||||
|
i += 1;
|
||||||
|
args.ta_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--ta-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--base-validation-time" => {
|
||||||
|
i += 1;
|
||||||
|
let value = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--base-validation-time requires a value")?;
|
||||||
|
args.base_validation_time = Some(
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||||
|
.map_err(|e| format!("invalid --base-validation-time: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--delta-count" => {
|
||||||
|
i += 1;
|
||||||
|
args.delta_count = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--delta-count requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --delta-count: {e}"))?;
|
||||||
|
}
|
||||||
|
"--delta-interval-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.delta_interval_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--delta-interval-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --delta-interval-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--http-timeout-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.http_timeout_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--http-timeout-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --http-timeout-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--rsync-timeout-secs" => {
|
||||||
|
i += 1;
|
||||||
|
args.rsync_timeout_secs = argv
|
||||||
|
.get(i)
|
||||||
|
.ok_or("--rsync-timeout-secs requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --rsync-timeout-secs: {e}"))?;
|
||||||
|
}
|
||||||
|
"--rsync-mirror-root" => {
|
||||||
|
i += 1;
|
||||||
|
args.rsync_mirror_root = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--rsync-mirror-root requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--max-depth" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_depth = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-depth requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-depth: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--max-instances" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_instances = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-instances requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-instances: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--trust-anchor" => {
|
||||||
|
i += 1;
|
||||||
|
args.trust_anchor = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--trust-anchor requires a value")?
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--keep-db" => args.keep_db = true,
|
||||||
|
"--capture-inputs-only" => args.capture_inputs_only = true,
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.rir.is_none() {
|
||||||
|
return Err(format!("--rir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.out_dir.is_none() {
|
||||||
|
return Err(format!("--out-dir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.tal_path.is_none() {
|
||||||
|
return Err(format!("--tal-path is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.ta_path.is_none() {
|
||||||
|
return Err(format!("--ta-path is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_v2_top_readme(
|
||||||
|
path: &Path,
|
||||||
|
rir: &str,
|
||||||
|
delta_count: usize,
|
||||||
|
delta_interval_secs: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(
|
||||||
|
path,
|
||||||
|
format!(
|
||||||
|
"# Ours Multi-Delta Replay Bundle\n\n- RIR: `{rir}`\n- Schema: `20260401-v2`\n- Configured delta steps: `{delta_count}`\n- Configured interval seconds: `{delta_interval_secs}`\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write readme failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_v2_rir_readme(
|
||||||
|
path: &Path,
|
||||||
|
rir: &str,
|
||||||
|
base_validation_time: &str,
|
||||||
|
delta_count: usize,
|
||||||
|
delta_interval_secs: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(
|
||||||
|
path,
|
||||||
|
format!(
|
||||||
|
"# {rir} multi-delta live replay bundle\n\n- `base-locks.json.validationTime` = `{base_validation_time}`\n- `delta-steps/` contains `{delta_count}` ordered target steps\n- configured interval seconds = `{delta_interval_secs}`\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_recorded_target_snapshots_for_locks(
|
||||||
|
store: &RocksStore,
|
||||||
|
previous_locks_path: &Path,
|
||||||
|
http: &RecordingHttpFetcher<BlockingHttpFetcher>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let previous_locks: serde_json::Value =
|
||||||
|
serde_json::from_slice(&fs::read(previous_locks_path).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"read previous locks failed: {}: {e}",
|
||||||
|
previous_locks_path.display()
|
||||||
|
)
|
||||||
|
})?)
|
||||||
|
.map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"parse previous locks failed: {}: {e}",
|
||||||
|
previous_locks_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let previous_rrdp = previous_locks
|
||||||
|
.get("rrdp")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
for (notify_uri, base_lock) in previous_rrdp {
|
||||||
|
let Some(base_transport) = base_lock.get("transport").and_then(|v| v.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if base_transport != "rrdp" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(base_session) = base_lock.get("session").and_then(|v| v.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(base_serial) = base_lock.get("serial").and_then(|v| v.as_u64()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(record) = store
|
||||||
|
.get_rrdp_source_record(¬ify_uri)
|
||||||
|
.map_err(|e| format!("read rrdp source record failed for {notify_uri}: {e}"))?
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(target_session) = record.last_session_id.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(target_serial) = record.last_serial else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if target_session != base_session || target_serial <= base_serial {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(snapshot_uri) = record.last_snapshot_uri.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if step_http_has_snapshot(http, snapshot_uri) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Err(err) = http.fetch(snapshot_uri) {
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] warning: fetch target snapshot failed notify_uri={} snapshot_uri={} err={}",
|
||||||
|
notify_uri, snapshot_uri, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_http_has_snapshot(
|
||||||
|
http: &RecordingHttpFetcher<BlockingHttpFetcher>,
|
||||||
|
snapshot_uri: &str,
|
||||||
|
) -> bool {
|
||||||
|
http.snapshot_responses().contains_key(snapshot_uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: Args) -> Result<PathBuf, String> {
|
||||||
|
let rir = args.rir.as_ref().unwrap();
|
||||||
|
let rir_normalized = rir.to_ascii_lowercase();
|
||||||
|
let trust_anchor = args
|
||||||
|
.trust_anchor
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| rir_normalized.clone());
|
||||||
|
let out_root = args.out_dir.as_ref().unwrap();
|
||||||
|
if out_root.exists() {
|
||||||
|
fs::remove_dir_all(out_root)
|
||||||
|
.map_err(|e| format!("remove old out dir failed: {}: {e}", out_root.display()))?;
|
||||||
|
}
|
||||||
|
let rir_dir = out_root.join(&rir_normalized);
|
||||||
|
let delta_steps_root = rir_dir.join("delta-steps");
|
||||||
|
fs::create_dir_all(&delta_steps_root).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"create delta steps dir failed: {}: {e}",
|
||||||
|
delta_steps_root.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tal_bytes =
|
||||||
|
fs::read(args.tal_path.as_ref().unwrap()).map_err(|e| format!("read tal failed: {e}"))?;
|
||||||
|
let ta_bytes =
|
||||||
|
fs::read(args.ta_path.as_ref().unwrap()).map_err(|e| format!("read ta failed: {e}"))?;
|
||||||
|
fs::write(rir_dir.join("tal.tal"), &tal_bytes).map_err(|e| format!("write tal failed: {e}"))?;
|
||||||
|
fs::write(rir_dir.join("ta.cer"), &ta_bytes).map_err(|e| format!("write ta failed: {e}"))?;
|
||||||
|
|
||||||
|
let base_validation_time = args
|
||||||
|
.base_validation_time
|
||||||
|
.unwrap_or_else(time::OffsetDateTime::now_utc);
|
||||||
|
|
||||||
|
let work_db_dir = out_root
|
||||||
|
.join(".tmp")
|
||||||
|
.join(format!("{rir}-sequence-work-db"));
|
||||||
|
let base_self_replay_dir = out_root
|
||||||
|
.join(".tmp")
|
||||||
|
.join(format!("{rir}-sequence-base-self-replay-db"));
|
||||||
|
let _ = fs::remove_dir_all(&work_db_dir);
|
||||||
|
let _ = fs::remove_dir_all(&base_self_replay_dir);
|
||||||
|
if let Some(parent) = work_db_dir.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create tmp dir failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = RocksStore::open(&work_db_dir).map_err(|e| format!("open work db failed: {e}"))?;
|
||||||
|
let base_http = RecordingHttpFetcher::new(
|
||||||
|
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||||
|
timeout: Duration::from_secs(args.http_timeout_secs),
|
||||||
|
..HttpFetcherConfig::default()
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("create base http fetcher failed: {e}"))?,
|
||||||
|
);
|
||||||
|
let base_rsync_inner = SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||||
|
timeout: Duration::from_secs(args.rsync_timeout_secs),
|
||||||
|
mirror_root: args.rsync_mirror_root.clone(),
|
||||||
|
..SystemRsyncConfig::default()
|
||||||
|
});
|
||||||
|
let base_rsync = if args.capture_inputs_only {
|
||||||
|
RecordingRsyncFetcher::new_without_objects(base_rsync_inner)
|
||||||
|
} else {
|
||||||
|
RecordingRsyncFetcher::new(base_rsync_inner)
|
||||||
|
};
|
||||||
|
eprintln!("[sequence] base live run start rir={rir_normalized}");
|
||||||
|
let started = Instant::now();
|
||||||
|
let base_out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||||
|
&store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&base_http,
|
||||||
|
&base_rsync,
|
||||||
|
base_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("live base run failed: {e}"))?;
|
||||||
|
let base_duration = started.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] base live run done rir={} duration_s={:.3}",
|
||||||
|
rir_normalized,
|
||||||
|
base_duration.as_secs_f64()
|
||||||
|
);
|
||||||
|
|
||||||
|
eprintln!("[sequence] base input materialization start rir={rir_normalized}");
|
||||||
|
let base_capture = write_live_base_replay_bundle_inputs(
|
||||||
|
&rir_dir,
|
||||||
|
&rir_normalized,
|
||||||
|
base_validation_time,
|
||||||
|
&base_out.publication_points,
|
||||||
|
&store,
|
||||||
|
&base_http.snapshot_responses(),
|
||||||
|
&base_rsync.snapshot_fetches(),
|
||||||
|
)?;
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] base input materialization done rir={} rrdp_repos={} rsync_modules={}",
|
||||||
|
rir_normalized, base_capture.rrdp_repo_count, base_capture.rsync_module_count
|
||||||
|
);
|
||||||
|
let base_ccr_path = rir_dir.join("base.ccr");
|
||||||
|
let base_vrps_path = rir_dir.join("base-vrps.csv");
|
||||||
|
let base_vaps_path = rir_dir.join("base-vaps.csv");
|
||||||
|
let (base_ccr_sha256, base_vrp_rows, base_vap_rows, base_verify, base_self_replay_ok) =
|
||||||
|
if args.capture_inputs_only {
|
||||||
|
eprintln!("[sequence] base output generation skipped rir={rir_normalized}");
|
||||||
|
(
|
||||||
|
String::new(),
|
||||||
|
std::collections::BTreeSet::<rpki::bundle::VrpCompareRow>::new(),
|
||||||
|
std::collections::BTreeSet::<rpki::bundle::VapCompareRow>::new(),
|
||||||
|
CcrVerifySummary {
|
||||||
|
content_type_oid: String::new(),
|
||||||
|
version: 0,
|
||||||
|
produced_at_rfc3339_utc: String::new(),
|
||||||
|
state_hashes_ok: false,
|
||||||
|
manifest_instances: 0,
|
||||||
|
roa_payload_sets: 0,
|
||||||
|
roa_vrp_count: 0,
|
||||||
|
aspa_payload_sets: 0,
|
||||||
|
trust_anchor_ski_count: 0,
|
||||||
|
router_key_sets: 0,
|
||||||
|
router_key_count: 0,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
eprintln!("[sequence] base CCR/self-replay start rir={rir_normalized}");
|
||||||
|
let base_ccr = build_ccr_from_run(
|
||||||
|
&store,
|
||||||
|
&[base_out.discovery.trust_anchor.clone()],
|
||||||
|
&base_out.tree.vrps,
|
||||||
|
&base_out.tree.aspas,
|
||||||
|
&base_out.tree.router_keys,
|
||||||
|
base_validation_time,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("build base ccr failed: {e}"))?;
|
||||||
|
write_ccr_file(&base_ccr_path, &base_ccr)
|
||||||
|
.map_err(|e| format!("write base ccr failed: {e}"))?;
|
||||||
|
let base_ccr_bytes = fs::read(&base_ccr_path)
|
||||||
|
.map_err(|e| format!("read base ccr failed: {}: {e}", base_ccr_path.display()))?;
|
||||||
|
let base_decoded = decode_content_info(&base_ccr_bytes)
|
||||||
|
.map_err(|e| format!("decode base ccr failed: {e}"))?;
|
||||||
|
let base_verify = verify_content_info(&base_decoded)
|
||||||
|
.map_err(|e| format!("verify base ccr failed: {e}"))?;
|
||||||
|
let base_vrp_rows = build_vrp_compare_rows(&base_out.tree.vrps, &trust_anchor);
|
||||||
|
let base_vap_rows = build_vap_compare_rows(&base_out.tree.aspas, &trust_anchor);
|
||||||
|
let (base_ccr_vrps, base_ccr_vaps) =
|
||||||
|
rpki::bundle::decode_ccr_compare_views(&base_decoded, &trust_anchor)?;
|
||||||
|
if base_vrp_rows != base_ccr_vrps {
|
||||||
|
return Err("base-vrps compare view does not match base.ccr".to_string());
|
||||||
|
}
|
||||||
|
if base_vap_rows != base_ccr_vaps {
|
||||||
|
return Err("base-vaps compare view does not match base.ccr".to_string());
|
||||||
|
}
|
||||||
|
write_vrp_csv(&base_vrps_path, &base_vrp_rows)?;
|
||||||
|
write_vap_csv(&base_vaps_path, &base_vap_rows)?;
|
||||||
|
let base_replay_store = RocksStore::open(&base_self_replay_dir)
|
||||||
|
.map_err(|e| format!("open base self replay db failed: {e}"))?;
|
||||||
|
let base_replay_out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||||
|
&base_replay_store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&rir_dir.join("base-payload-archive"),
|
||||||
|
&rir_dir.join("base-locks.json"),
|
||||||
|
base_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("base self replay failed: {e}"))?;
|
||||||
|
if build_vrp_compare_rows(&base_replay_out.tree.vrps, &trust_anchor) != base_vrp_rows {
|
||||||
|
return Err("base self replay VRP compare view mismatch".to_string());
|
||||||
|
}
|
||||||
|
if build_vap_compare_rows(&base_replay_out.tree.aspas, &trust_anchor) != base_vap_rows {
|
||||||
|
return Err("base self replay VAP compare view mismatch".to_string());
|
||||||
|
}
|
||||||
|
let output = (
|
||||||
|
sha256_hex(&base_ccr_bytes),
|
||||||
|
base_vrp_rows,
|
||||||
|
base_vap_rows,
|
||||||
|
base_verify,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
eprintln!("[sequence] base CCR/self-replay done rir={rir_normalized}");
|
||||||
|
output
|
||||||
|
};
|
||||||
|
fs::create_dir_all(rir_dir.join("timings"))
|
||||||
|
.map_err(|e| format!("create timings dir failed: {e}"))?;
|
||||||
|
write_json(
|
||||||
|
&rir_dir.join("timings").join("base-produce.json"),
|
||||||
|
&serde_json::json!({
|
||||||
|
"mode": "base",
|
||||||
|
"validationTime": base_validation_time.format(&Rfc3339).map_err(|e| format!("format base validation time failed: {e}"))?,
|
||||||
|
"durationSeconds": base_duration.as_secs_f64(),
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut steps_json = Vec::new();
|
||||||
|
let mut delta_steps = Vec::new();
|
||||||
|
let mut previous_locks_path = rir_dir.join("base-locks.json");
|
||||||
|
let mut previous_ref = "base".to_string();
|
||||||
|
let sequence_self_replay_dir = out_root
|
||||||
|
.join(".tmp")
|
||||||
|
.join(format!("{rir}-sequence-self-replay-db"));
|
||||||
|
let _ = fs::remove_dir_all(&sequence_self_replay_dir);
|
||||||
|
let sequence_replay_store = if args.capture_inputs_only {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let store = RocksStore::open(&sequence_self_replay_dir)
|
||||||
|
.map_err(|e| format!("open sequence self replay db failed: {e}"))?;
|
||||||
|
let _base_replay = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||||
|
&store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&rir_dir.join("base-payload-archive"),
|
||||||
|
&rir_dir.join("base-locks.json"),
|
||||||
|
base_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("sequence base self replay failed: {e}"))?;
|
||||||
|
Some(store)
|
||||||
|
};
|
||||||
|
let mut all_steps_self_replay_ok = true;
|
||||||
|
|
||||||
|
for step_index in 1..=args.delta_count {
|
||||||
|
if step_index > 1 && args.delta_interval_secs > 0 {
|
||||||
|
std::thread::sleep(Duration::from_secs(args.delta_interval_secs));
|
||||||
|
}
|
||||||
|
let step_id = format!("step-{step_index:03}");
|
||||||
|
let step_dir = delta_steps_root.join(&step_id);
|
||||||
|
fs::create_dir_all(&step_dir)
|
||||||
|
.map_err(|e| format!("create step dir failed: {}: {e}", step_dir.display()))?;
|
||||||
|
let step_validation_time = time::OffsetDateTime::now_utc();
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step live run start rir={} step={}",
|
||||||
|
rir_normalized, step_id
|
||||||
|
);
|
||||||
|
let step_http = RecordingHttpFetcher::new(
|
||||||
|
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||||
|
timeout: Duration::from_secs(args.http_timeout_secs),
|
||||||
|
..HttpFetcherConfig::default()
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("create step http fetcher failed: {e}"))?,
|
||||||
|
);
|
||||||
|
let step_rsync_inner = SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||||
|
timeout: Duration::from_secs(args.rsync_timeout_secs),
|
||||||
|
mirror_root: args.rsync_mirror_root.clone(),
|
||||||
|
..SystemRsyncConfig::default()
|
||||||
|
});
|
||||||
|
let step_rsync = if args.capture_inputs_only {
|
||||||
|
RecordingRsyncFetcher::new_without_objects(step_rsync_inner)
|
||||||
|
} else {
|
||||||
|
RecordingRsyncFetcher::new(step_rsync_inner)
|
||||||
|
};
|
||||||
|
let started = Instant::now();
|
||||||
|
let step_out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||||
|
&store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&step_http,
|
||||||
|
&step_rsync,
|
||||||
|
step_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("live delta step {step_id} failed: {e}"))?;
|
||||||
|
let step_duration = started.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step live run done rir={} step={} duration_s={:.3}",
|
||||||
|
rir_normalized,
|
||||||
|
step_id,
|
||||||
|
step_duration.as_secs_f64()
|
||||||
|
);
|
||||||
|
ensure_recorded_target_snapshots_for_locks(&store, &previous_locks_path, &step_http)?;
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step output generation phase start rir={} step={}",
|
||||||
|
rir_normalized, step_id
|
||||||
|
);
|
||||||
|
let delta_ccr_path = step_dir.join("delta.ccr");
|
||||||
|
let delta_vrps_path = step_dir.join("record-delta.csv");
|
||||||
|
let delta_vaps_path = step_dir.join("record-delta-vaps.csv");
|
||||||
|
let (delta_ccr_sha256, delta_vrp_rows, delta_vap_rows, delta_verify, step_self_replay_ok) =
|
||||||
|
if args.capture_inputs_only {
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step CCR/self-replay skipped rir={} step={}",
|
||||||
|
rir_normalized, step_id
|
||||||
|
);
|
||||||
|
(
|
||||||
|
String::new(),
|
||||||
|
std::collections::BTreeSet::<rpki::bundle::VrpCompareRow>::new(),
|
||||||
|
std::collections::BTreeSet::<rpki::bundle::VapCompareRow>::new(),
|
||||||
|
CcrVerifySummary {
|
||||||
|
content_type_oid: String::new(),
|
||||||
|
version: 0,
|
||||||
|
produced_at_rfc3339_utc: String::new(),
|
||||||
|
state_hashes_ok: false,
|
||||||
|
manifest_instances: 0,
|
||||||
|
roa_payload_sets: 0,
|
||||||
|
roa_vrp_count: 0,
|
||||||
|
aspa_payload_sets: 0,
|
||||||
|
trust_anchor_ski_count: 0,
|
||||||
|
router_key_sets: 0,
|
||||||
|
router_key_count: 0,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step CCR/self-replay start rir={} step={}",
|
||||||
|
rir_normalized, step_id
|
||||||
|
);
|
||||||
|
let delta_ccr = build_ccr_from_run(
|
||||||
|
&store,
|
||||||
|
&[step_out.discovery.trust_anchor.clone()],
|
||||||
|
&step_out.tree.vrps,
|
||||||
|
&step_out.tree.aspas,
|
||||||
|
&step_out.tree.router_keys,
|
||||||
|
step_validation_time,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("build step ccr failed for {step_id}: {e}"))?;
|
||||||
|
write_ccr_file(&delta_ccr_path, &delta_ccr)
|
||||||
|
.map_err(|e| format!("write step ccr failed for {step_id}: {e}"))?;
|
||||||
|
let delta_ccr_bytes = fs::read(&delta_ccr_path).map_err(|e| {
|
||||||
|
format!("read step ccr failed: {}: {e}", delta_ccr_path.display())
|
||||||
|
})?;
|
||||||
|
let delta_decoded = decode_content_info(&delta_ccr_bytes)
|
||||||
|
.map_err(|e| format!("decode step ccr failed for {step_id}: {e}"))?;
|
||||||
|
let delta_verify = verify_content_info(&delta_decoded)
|
||||||
|
.map_err(|e| format!("verify step ccr failed for {step_id}: {e}"))?;
|
||||||
|
let delta_vrp_rows = build_vrp_compare_rows(&step_out.tree.vrps, &trust_anchor);
|
||||||
|
let delta_vap_rows = build_vap_compare_rows(&step_out.tree.aspas, &trust_anchor);
|
||||||
|
let (ccr_vrps, ccr_vaps) =
|
||||||
|
rpki::bundle::decode_ccr_compare_views(&delta_decoded, &trust_anchor)?;
|
||||||
|
if delta_vrp_rows != ccr_vrps {
|
||||||
|
return Err(format!(
|
||||||
|
"{step_id} VRP compare view does not match delta.ccr"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if delta_vap_rows != ccr_vaps {
|
||||||
|
return Err(format!(
|
||||||
|
"{step_id} VAP compare view does not match delta.ccr"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
write_vrp_csv(&delta_vrps_path, &delta_vrp_rows)?;
|
||||||
|
write_vap_csv(&delta_vaps_path, &delta_vap_rows)?;
|
||||||
|
let step_replay_out =
|
||||||
|
run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
|
||||||
|
sequence_replay_store
|
||||||
|
.as_ref()
|
||||||
|
.expect("sequence replay store"),
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
&step_dir.join("payload-delta-archive"),
|
||||||
|
&previous_locks_path,
|
||||||
|
&step_dir.join("locks-delta.json"),
|
||||||
|
step_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("sequence self replay failed for {step_id}: {e}"))?;
|
||||||
|
let step_self_replay_ok =
|
||||||
|
build_vrp_compare_rows(&step_replay_out.tree.vrps, &trust_anchor)
|
||||||
|
== delta_vrp_rows
|
||||||
|
&& build_vap_compare_rows(&step_replay_out.tree.aspas, &trust_anchor)
|
||||||
|
== delta_vap_rows;
|
||||||
|
let output = (
|
||||||
|
sha256_hex(&delta_ccr_bytes),
|
||||||
|
delta_vrp_rows,
|
||||||
|
delta_vap_rows,
|
||||||
|
delta_verify,
|
||||||
|
step_self_replay_ok,
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step CCR/self-replay done rir={} step={}",
|
||||||
|
rir_normalized, step_id
|
||||||
|
);
|
||||||
|
output
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step input materialization start rir={} step={}",
|
||||||
|
rir_normalized, step_id
|
||||||
|
);
|
||||||
|
let delta_capture = write_live_delta_replay_step_inputs(
|
||||||
|
&step_dir,
|
||||||
|
&rir_normalized,
|
||||||
|
&previous_locks_path,
|
||||||
|
step_validation_time,
|
||||||
|
&step_out.publication_points,
|
||||||
|
&store,
|
||||||
|
&step_http.snapshot_responses(),
|
||||||
|
&step_rsync.snapshot_fetches(),
|
||||||
|
)?;
|
||||||
|
let target_lock_capture_id = format!("{rir_normalized}-target-{step_id}");
|
||||||
|
write_current_replay_state_locks(
|
||||||
|
&step_dir.join("target-locks.json"),
|
||||||
|
&target_lock_capture_id,
|
||||||
|
step_validation_time,
|
||||||
|
&step_out.publication_points,
|
||||||
|
&store,
|
||||||
|
)?;
|
||||||
|
eprintln!(
|
||||||
|
"[sequence] step input materialization done rir={} step={} rrdp_repos={} rsync_modules={}",
|
||||||
|
rir_normalized,
|
||||||
|
step_id,
|
||||||
|
delta_capture.rrdp_repo_count,
|
||||||
|
delta_capture.rsync_module_count
|
||||||
|
);
|
||||||
|
write_json(
|
||||||
|
&step_dir.join("verification.json"),
|
||||||
|
&serde_json::json!({
|
||||||
|
"index": step_index,
|
||||||
|
"id": step_id,
|
||||||
|
"validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
"capture": {
|
||||||
|
"captureId": delta_capture.capture_id,
|
||||||
|
"rrdpRepoCount": delta_capture.rrdp_repo_count,
|
||||||
|
"rsyncModuleCount": delta_capture.rsync_module_count,
|
||||||
|
},
|
||||||
|
"ccr": {
|
||||||
|
"path": "delta.ccr",
|
||||||
|
"sha256": delta_ccr_sha256.clone(),
|
||||||
|
"stateHashesOk": delta_verify.state_hashes_ok,
|
||||||
|
"manifestInstances": delta_verify.manifest_instances,
|
||||||
|
"roaVrpCount": delta_verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": delta_verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": delta_verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"vrpCount": delta_vrp_rows.len(),
|
||||||
|
"vapCount": delta_vap_rows.len(),
|
||||||
|
},
|
||||||
|
"selfReplayOk": serde_json::Value::Null,
|
||||||
|
"timings": {
|
||||||
|
"durationSeconds": step_duration.as_secs_f64(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
fs::create_dir_all(step_dir.join("timings"))
|
||||||
|
.map_err(|e| format!("create step timings dir failed: {e}"))?;
|
||||||
|
write_json(
|
||||||
|
&step_dir.join("timings").join("delta-produce.json"),
|
||||||
|
&serde_json::json!({
|
||||||
|
"mode": "delta",
|
||||||
|
"stepIndex": step_index,
|
||||||
|
"validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
"durationSeconds": step_duration.as_secs_f64(),
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
delta_steps.push(DeltaStepMetadataV2 {
|
||||||
|
index: step_index,
|
||||||
|
id: step_id.clone(),
|
||||||
|
relative_path: format!("delta-steps/{step_id}"),
|
||||||
|
base_ref: previous_ref.clone(),
|
||||||
|
validation_time: step_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
delta_ccr_sha256: delta_ccr_sha256.clone(),
|
||||||
|
vrp_count: delta_vrp_rows.len(),
|
||||||
|
vap_count: delta_vap_rows.len(),
|
||||||
|
relative_archive_path: format!("delta-steps/{step_id}/payload-delta-archive"),
|
||||||
|
relative_transition_locks_path: format!("delta-steps/{step_id}/locks-delta.json"),
|
||||||
|
relative_target_locks_path: format!("delta-steps/{step_id}/target-locks.json"),
|
||||||
|
relative_ccr_path: format!("delta-steps/{step_id}/delta.ccr"),
|
||||||
|
relative_vrps_path: format!("delta-steps/{step_id}/record-delta.csv"),
|
||||||
|
relative_vaps_path: format!("delta-steps/{step_id}/record-delta-vaps.csv"),
|
||||||
|
has_aspa: !delta_vap_rows.is_empty(),
|
||||||
|
has_router_key: delta_verify.router_key_count > 0,
|
||||||
|
});
|
||||||
|
all_steps_self_replay_ok &= step_self_replay_ok;
|
||||||
|
|
||||||
|
steps_json.push(serde_json::json!({
|
||||||
|
"index": step_index,
|
||||||
|
"id": step_id,
|
||||||
|
"validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
"capture": {
|
||||||
|
"captureId": delta_capture.capture_id,
|
||||||
|
"rrdpRepoCount": delta_capture.rrdp_repo_count,
|
||||||
|
"rsyncModuleCount": delta_capture.rsync_module_count,
|
||||||
|
},
|
||||||
|
"ccr": {
|
||||||
|
"path": format!("delta-steps/{step_id}/delta.ccr"),
|
||||||
|
"sha256": delta_ccr_sha256,
|
||||||
|
"stateHashesOk": delta_verify.state_hashes_ok,
|
||||||
|
"manifestInstances": delta_verify.manifest_instances,
|
||||||
|
"roaVrpCount": delta_verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": delta_verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": delta_verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"vrpCount": delta_vrp_rows.len(),
|
||||||
|
"vapCount": delta_vap_rows.len(),
|
||||||
|
},
|
||||||
|
"selfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(step_self_replay_ok) }
|
||||||
|
}));
|
||||||
|
|
||||||
|
previous_locks_path = step_dir.join("target-locks.json");
|
||||||
|
previous_ref = step_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = RirBundleMetadataV2 {
|
||||||
|
schema_version: "20260401-v2".to_string(),
|
||||||
|
bundle_producer: "ours".to_string(),
|
||||||
|
rir: rir_normalized.clone(),
|
||||||
|
tal_sha256: sha256_hex(&tal_bytes),
|
||||||
|
ta_cert_sha256: sha256_hex(&ta_bytes),
|
||||||
|
has_any_aspa: !base_vap_rows.is_empty() || delta_steps.iter().any(|step| step.has_aspa),
|
||||||
|
has_any_router_key: base_verify.router_key_count > 0
|
||||||
|
|| delta_steps.iter().any(|step| step.has_router_key),
|
||||||
|
base: BaseBundleStateMetadataV2 {
|
||||||
|
validation_time: base_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format base validation time failed: {e}"))?,
|
||||||
|
ccr_sha256: base_ccr_sha256.clone(),
|
||||||
|
vrp_count: base_vrp_rows.len(),
|
||||||
|
vap_count: base_vap_rows.len(),
|
||||||
|
relative_archive_path: "base-payload-archive".to_string(),
|
||||||
|
relative_locks_path: "base-locks.json".to_string(),
|
||||||
|
relative_ccr_path: "base.ccr".to_string(),
|
||||||
|
relative_vrps_path: "base-vrps.csv".to_string(),
|
||||||
|
relative_vaps_path: "base-vaps.csv".to_string(),
|
||||||
|
},
|
||||||
|
delta_sequence: DeltaSequenceMetadataV2 {
|
||||||
|
configured_delta_count: args.delta_count,
|
||||||
|
configured_interval_seconds: args.delta_interval_secs,
|
||||||
|
steps: delta_steps.clone(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
write_json(&rir_dir.join("bundle.json"), &metadata)?;
|
||||||
|
write_json(
|
||||||
|
&rir_dir.join("verification.json"),
|
||||||
|
&serde_json::json!({
|
||||||
|
"base": {
|
||||||
|
"validationTime": metadata.base.validation_time,
|
||||||
|
"ccr": {
|
||||||
|
"path": "base.ccr",
|
||||||
|
"sha256": metadata.base.ccr_sha256,
|
||||||
|
"stateHashesOk": base_verify.state_hashes_ok,
|
||||||
|
"manifestInstances": base_verify.manifest_instances,
|
||||||
|
"roaVrpCount": base_verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": base_verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": base_verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"baseVrpCount": metadata.base.vrp_count,
|
||||||
|
"baseVapCount": metadata.base.vap_count,
|
||||||
|
},
|
||||||
|
"capture": {
|
||||||
|
"captureId": base_capture.capture_id,
|
||||||
|
"rrdpRepoCount": base_capture.rrdp_repo_count,
|
||||||
|
"rsyncModuleCount": base_capture.rsync_module_count,
|
||||||
|
"selfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(base_self_replay_ok) },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": steps_json,
|
||||||
|
"summary": {
|
||||||
|
"baseSelfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(base_self_replay_ok) },
|
||||||
|
"stepCount": args.delta_count,
|
||||||
|
"allStepsSelfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(all_steps_self_replay_ok) },
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
write_v2_top_readme(
|
||||||
|
&out_root.join("README.md"),
|
||||||
|
&rir_normalized,
|
||||||
|
args.delta_count,
|
||||||
|
args.delta_interval_secs,
|
||||||
|
)?;
|
||||||
|
write_v2_rir_readme(
|
||||||
|
&rir_dir.join("README.md"),
|
||||||
|
&rir_normalized,
|
||||||
|
&metadata.base.validation_time,
|
||||||
|
args.delta_count,
|
||||||
|
args.delta_interval_secs,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let bundle_manifest = BundleManifestV2 {
|
||||||
|
schema_version: "20260401-v2".to_string(),
|
||||||
|
bundle_producer: "ours".to_string(),
|
||||||
|
recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format recorded_at failed: {e}"))?,
|
||||||
|
rirs: vec![rir_normalized.clone()],
|
||||||
|
per_rir_bundles: vec![BundleManifestEntryV2 {
|
||||||
|
rir: rir_normalized.clone(),
|
||||||
|
relative_path: rir_normalized,
|
||||||
|
base_validation_time: metadata.base.validation_time.clone(),
|
||||||
|
step_count: metadata.delta_sequence.steps.len(),
|
||||||
|
first_delta_validation_time: metadata
|
||||||
|
.delta_sequence
|
||||||
|
.steps
|
||||||
|
.first()
|
||||||
|
.map(|step| step.validation_time.clone()),
|
||||||
|
last_delta_validation_time: metadata
|
||||||
|
.delta_sequence
|
||||||
|
.steps
|
||||||
|
.last()
|
||||||
|
.map(|step| step.validation_time.clone()),
|
||||||
|
has_aspa: metadata.has_any_aspa,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
write_json(&out_root.join("bundle-manifest.json"), &bundle_manifest)?;
|
||||||
|
|
||||||
|
if !args.keep_db {
|
||||||
|
drop(sequence_replay_store);
|
||||||
|
drop(store);
|
||||||
|
let _ = fs::remove_dir_all(&work_db_dir);
|
||||||
|
let _ = fs::remove_dir_all(&base_self_replay_dir);
|
||||||
|
let _ = fs::remove_dir_all(&sequence_self_replay_dir);
|
||||||
|
let tmp_dir = out_root.join(".tmp");
|
||||||
|
if tmp_dir.is_dir() {
|
||||||
|
let is_empty = fs::read_dir(&tmp_dir)
|
||||||
|
.map_err(|e| format!("read tmp dir failed: {}: {e}", tmp_dir.display()))?
|
||||||
|
.next()
|
||||||
|
.is_none();
|
||||||
|
if is_empty {
|
||||||
|
let _ = fs::remove_dir(&tmp_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out_root.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let out = run(args)?;
|
||||||
|
println!("{}", out.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_defaults_delta_sequence_parameters() {
|
||||||
|
let argv = vec![
|
||||||
|
"replay_bundle_capture_sequence".to_string(),
|
||||||
|
"--rir".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
"--out-dir".to_string(),
|
||||||
|
"out".to_string(),
|
||||||
|
"--tal-path".to_string(),
|
||||||
|
"tal".to_string(),
|
||||||
|
"--ta-path".to_string(),
|
||||||
|
"ta".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(args.delta_count, 5);
|
||||||
|
assert_eq!(args.delta_interval_secs, 600);
|
||||||
|
assert!(!args.keep_db);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_accepts_overrides_and_keep_db() {
|
||||||
|
let argv = vec![
|
||||||
|
"replay_bundle_capture_sequence".to_string(),
|
||||||
|
"--rir".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
"--out-dir".to_string(),
|
||||||
|
"out".to_string(),
|
||||||
|
"--tal-path".to_string(),
|
||||||
|
"tal".to_string(),
|
||||||
|
"--ta-path".to_string(),
|
||||||
|
"ta".to_string(),
|
||||||
|
"--delta-count".to_string(),
|
||||||
|
"2".to_string(),
|
||||||
|
"--delta-interval-secs".to_string(),
|
||||||
|
"0".to_string(),
|
||||||
|
"--keep-db".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(args.delta_count, 2);
|
||||||
|
assert_eq!(args.delta_interval_secs, 0);
|
||||||
|
assert!(args.keep_db);
|
||||||
|
}
|
||||||
|
}
|
||||||
798
src/bin/replay_bundle_record.rs
Normal file
798
src/bin/replay_bundle_record.rs
Normal file
@@ -0,0 +1,798 @@
|
|||||||
|
use rpki::bundle::{
|
||||||
|
BundleManifest, BundleManifestEntry, RirBundleMetadata, build_vap_compare_rows,
|
||||||
|
build_vrp_compare_rows, decode_ccr_compare_views, write_vap_csv, write_vrp_csv,
|
||||||
|
};
|
||||||
|
use rpki::ccr::{build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file};
|
||||||
|
use rpki::policy::Policy;
|
||||||
|
use rpki::storage::RocksStore;
|
||||||
|
use rpki::validation::run_tree_from_tal::{
|
||||||
|
run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit,
|
||||||
|
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||||
|
};
|
||||||
|
use rpki::validation::tree::TreeRunConfig;
|
||||||
|
use sha2::Digest;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Instant;
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Args {
|
||||||
|
rir: Option<String>,
|
||||||
|
out_dir: Option<PathBuf>,
|
||||||
|
tal_path: Option<PathBuf>,
|
||||||
|
ta_path: Option<PathBuf>,
|
||||||
|
payload_replay_archive: Option<PathBuf>,
|
||||||
|
payload_replay_locks: Option<PathBuf>,
|
||||||
|
payload_delta_archive: Option<PathBuf>,
|
||||||
|
payload_delta_locks: Option<PathBuf>,
|
||||||
|
validation_time: Option<time::OffsetDateTime>,
|
||||||
|
max_depth: Option<usize>,
|
||||||
|
max_instances: Option<usize>,
|
||||||
|
trust_anchor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"Usage: replay_bundle_record --rir <name> --out-dir <path> --tal-path <path> --ta-path <path> --payload-replay-archive <path> --payload-replay-locks <path> [--payload-delta-archive <path> --payload-delta-locks <path>] [--validation-time <rfc3339>] [--max-depth <n>] [--max-instances <n>] [--trust-anchor <name>]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args::default();
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--help" | "-h" => return Err(usage().to_string()),
|
||||||
|
"--rir" => {
|
||||||
|
i += 1;
|
||||||
|
args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone());
|
||||||
|
}
|
||||||
|
"--out-dir" => {
|
||||||
|
i += 1;
|
||||||
|
args.out_dir = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--out-dir requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--tal-path" => {
|
||||||
|
i += 1;
|
||||||
|
args.tal_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--tal-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--ta-path" => {
|
||||||
|
i += 1;
|
||||||
|
args.ta_path = Some(PathBuf::from(
|
||||||
|
argv.get(i).ok_or("--ta-path requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--payload-replay-archive" => {
|
||||||
|
i += 1;
|
||||||
|
args.payload_replay_archive = Some(PathBuf::from(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--payload-replay-archive requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--payload-replay-locks" => {
|
||||||
|
i += 1;
|
||||||
|
args.payload_replay_locks = Some(PathBuf::from(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--payload-replay-locks requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--payload-delta-archive" => {
|
||||||
|
i += 1;
|
||||||
|
args.payload_delta_archive = Some(PathBuf::from(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--payload-delta-archive requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--payload-delta-locks" => {
|
||||||
|
i += 1;
|
||||||
|
args.payload_delta_locks = Some(PathBuf::from(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--payload-delta-locks requires a value")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--validation-time" => {
|
||||||
|
i += 1;
|
||||||
|
let value = argv.get(i).ok_or("--validation-time requires a value")?;
|
||||||
|
args.validation_time = Some(
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||||
|
.map_err(|e| format!("invalid --validation-time: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--max-depth" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_depth = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-depth requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-depth: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--max-instances" => {
|
||||||
|
i += 1;
|
||||||
|
args.max_instances = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--max-instances requires a value")?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("invalid --max-instances: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--trust-anchor" => {
|
||||||
|
i += 1;
|
||||||
|
args.trust_anchor = Some(
|
||||||
|
argv.get(i)
|
||||||
|
.ok_or("--trust-anchor requires a value")?
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.rir.is_none() {
|
||||||
|
return Err(format!("--rir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.out_dir.is_none() {
|
||||||
|
return Err(format!("--out-dir is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.tal_path.is_none() {
|
||||||
|
return Err(format!("--tal-path is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.ta_path.is_none() {
|
||||||
|
return Err(format!("--ta-path is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.payload_replay_archive.is_none() {
|
||||||
|
return Err(format!("--payload-replay-archive is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
if args.payload_replay_locks.is_none() {
|
||||||
|
return Err(format!("--payload-replay-locks is required\n{}", usage()));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_validation_time(path: &Path) -> Result<time::OffsetDateTime, String> {
|
||||||
|
let json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(path).map_err(|e| format!("read locks failed: {}: {e}", path.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse locks failed: {}: {e}", path.display()))?;
|
||||||
|
let value = json
|
||||||
|
.get("validationTime")
|
||||||
|
.or_else(|| json.get("validation_time"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| format!("validationTime missing in {}", path.display()))?;
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||||
|
.map_err(|e| format!("invalid validationTime in {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256_hex(bytes: &[u8]) -> String {
|
||||||
|
hex::encode(sha2::Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
|
||||||
|
fs::create_dir_all(dst)
|
||||||
|
.map_err(|e| format!("create directory failed: {}: {e}", dst.display()))?;
|
||||||
|
for entry in
|
||||||
|
fs::read_dir(src).map_err(|e| format!("read_dir failed: {}: {e}", src.display()))?
|
||||||
|
{
|
||||||
|
let entry = entry.map_err(|e| format!("read_dir entry failed: {}: {e}", src.display()))?;
|
||||||
|
let ty = entry
|
||||||
|
.file_type()
|
||||||
|
.map_err(|e| format!("file_type failed: {}: {e}", entry.path().display()))?;
|
||||||
|
let to = dst.join(entry.file_name());
|
||||||
|
if ty.is_dir() {
|
||||||
|
copy_dir_all(&entry.path(), &to)?;
|
||||||
|
} else if ty.is_file() {
|
||||||
|
if let Some(parent) = to.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::copy(entry.path(), &to).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"copy failed: {} -> {}: {e}",
|
||||||
|
entry.path().display(),
|
||||||
|
to.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(path, bytes).map_err(|e| format!("write json failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_top_readme(path: &Path, rir: &str) -> Result<(), String> {
|
||||||
|
fs::write(
|
||||||
|
path,
|
||||||
|
format!(
|
||||||
|
"# Ours Replay Bundle\n\nThis run contains one per-RIR bundle generated by `ours`.\n\n- RIR: `{rir}`\n- Reference result format: `CCR`\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write readme failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_rir_readme(path: &Path, rir: &str, base_validation_time: &str) -> Result<(), String> {
|
||||||
|
fs::write(
|
||||||
|
path,
|
||||||
|
format!(
|
||||||
|
"# {rir} replay bundle\n\n- `tal.tal` and `ta.cer` are the direct replay inputs.\n- `base-locks.json.validationTime` = `{base_validation_time}`.\n- `base.ccr` is the authoritative reference result.\n- `base-vrps.csv` and `base-vaps.csv` are compare views derived from `base.ccr`.\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_timing_json(
|
||||||
|
path: &Path,
|
||||||
|
mode: &str,
|
||||||
|
validation_time: &time::OffsetDateTime,
|
||||||
|
duration: std::time::Duration,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
write_json(
|
||||||
|
path,
|
||||||
|
&serde_json::json!({
|
||||||
|
"mode": mode,
|
||||||
|
"validationTime": validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
"durationSeconds": duration.as_secs_f64(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_delta_base_locks_sha(
|
||||||
|
delta_root: &Path,
|
||||||
|
emitted_base_locks_sha256: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let delta_locks = delta_root.join("locks-delta.json");
|
||||||
|
if delta_locks.is_file() {
|
||||||
|
let mut json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(&delta_locks)
|
||||||
|
.map_err(|e| format!("read delta locks failed: {}: {e}", delta_locks.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse delta locks failed: {}: {e}", delta_locks.display()))?;
|
||||||
|
json.as_object_mut()
|
||||||
|
.ok_or_else(|| format!("delta locks must be object: {}", delta_locks.display()))?
|
||||||
|
.insert(
|
||||||
|
"baseLocksSha256".to_string(),
|
||||||
|
serde_json::Value::String(emitted_base_locks_sha256.to_string()),
|
||||||
|
);
|
||||||
|
write_json(&delta_locks, &json)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let archive_root = delta_root.join("payload-delta-archive");
|
||||||
|
if archive_root.is_dir() {
|
||||||
|
for path in walk_json_files_named(&archive_root, "base.json")? {
|
||||||
|
let mut json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(&path)
|
||||||
|
.map_err(|e| format!("read base.json failed: {}: {e}", path.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse base.json failed: {}: {e}", path.display()))?;
|
||||||
|
json.as_object_mut()
|
||||||
|
.ok_or_else(|| format!("base.json must be object: {}", path.display()))?
|
||||||
|
.insert(
|
||||||
|
"baseLocksSha256".to_string(),
|
||||||
|
serde_json::Value::String(emitted_base_locks_sha256.to_string()),
|
||||||
|
);
|
||||||
|
write_json(&path, &json)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_json_files_named(root: &Path, name: &str) -> Result<Vec<PathBuf>, String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if !root.is_dir() {
|
||||||
|
return Ok(out);
|
||||||
|
}
|
||||||
|
let mut stack = vec![root.to_path_buf()];
|
||||||
|
while let Some(dir) = stack.pop() {
|
||||||
|
for entry in
|
||||||
|
fs::read_dir(&dir).map_err(|e| format!("read_dir failed: {}: {e}", dir.display()))?
|
||||||
|
{
|
||||||
|
let entry =
|
||||||
|
entry.map_err(|e| format!("read_dir entry failed: {}: {e}", dir.display()))?;
|
||||||
|
let path = entry.path();
|
||||||
|
let ty = entry
|
||||||
|
.file_type()
|
||||||
|
.map_err(|e| format!("file_type failed: {}: {e}", path.display()))?;
|
||||||
|
if ty.is_dir() {
|
||||||
|
stack.push(path);
|
||||||
|
} else if ty.is_file() && entry.file_name() == name {
|
||||||
|
out.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: Args) -> Result<PathBuf, String> {
|
||||||
|
let rir = args.rir.as_ref().unwrap();
|
||||||
|
let rir_normalized = rir.to_ascii_lowercase();
|
||||||
|
let out_root = args.out_dir.as_ref().unwrap();
|
||||||
|
let tal_path = args.tal_path.as_ref().unwrap();
|
||||||
|
let ta_path = args.ta_path.as_ref().unwrap();
|
||||||
|
let replay_archive = args.payload_replay_archive.as_ref().unwrap();
|
||||||
|
let replay_locks = args.payload_replay_locks.as_ref().unwrap();
|
||||||
|
let trust_anchor = args
|
||||||
|
.trust_anchor
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| rir_normalized.clone());
|
||||||
|
|
||||||
|
let base_validation_time = match args.validation_time {
|
||||||
|
Some(value) => value,
|
||||||
|
None => load_validation_time(replay_locks)?,
|
||||||
|
};
|
||||||
|
let delta_validation_time = match args.payload_delta_locks.as_ref() {
|
||||||
|
Some(path) => Some(load_validation_time(path)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let run_root = out_root;
|
||||||
|
let rir_dir = run_root.join(&rir_normalized);
|
||||||
|
fs::create_dir_all(&rir_dir)
|
||||||
|
.map_err(|e| format!("create rir dir failed: {}: {e}", rir_dir.display()))?;
|
||||||
|
|
||||||
|
let tal_bytes =
|
||||||
|
fs::read(tal_path).map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
|
||||||
|
let ta_bytes =
|
||||||
|
fs::read(ta_path).map_err(|e| format!("read ta failed: {}: {e}", ta_path.display()))?;
|
||||||
|
|
||||||
|
let db_dir = run_root.join(".tmp").join(format!("{rir}-base-db"));
|
||||||
|
if db_dir.exists() {
|
||||||
|
fs::remove_dir_all(&db_dir)
|
||||||
|
.map_err(|e| format!("remove old db failed: {}: {e}", db_dir.display()))?;
|
||||||
|
}
|
||||||
|
if let Some(parent) = db_dir.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create db parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let store = RocksStore::open(&db_dir).map_err(|e| format!("open rocksdb failed: {e}"))?;
|
||||||
|
|
||||||
|
let base_started = Instant::now();
|
||||||
|
let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||||
|
&store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
replay_archive,
|
||||||
|
replay_locks,
|
||||||
|
base_validation_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("base replay failed: {e}"))?;
|
||||||
|
let base_duration = base_started.elapsed();
|
||||||
|
|
||||||
|
let ccr = build_ccr_from_run(
|
||||||
|
&store,
|
||||||
|
&[out.discovery.trust_anchor.clone()],
|
||||||
|
&out.tree.vrps,
|
||||||
|
&out.tree.aspas,
|
||||||
|
&out.tree.router_keys,
|
||||||
|
base_validation_time,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("build ccr failed: {e}"))?;
|
||||||
|
|
||||||
|
let base_ccr_path = rir_dir.join("base.ccr");
|
||||||
|
write_ccr_file(&base_ccr_path, &ccr).map_err(|e| format!("write ccr failed: {e}"))?;
|
||||||
|
let ccr_bytes = fs::read(&base_ccr_path)
|
||||||
|
.map_err(|e| format!("read written ccr failed: {}: {e}", base_ccr_path.display()))?;
|
||||||
|
let decoded =
|
||||||
|
decode_content_info(&ccr_bytes).map_err(|e| format!("decode written ccr failed: {e}"))?;
|
||||||
|
let verify = verify_content_info(&decoded).map_err(|e| format!("verify ccr failed: {e}"))?;
|
||||||
|
|
||||||
|
let vrp_rows = build_vrp_compare_rows(&out.tree.vrps, &trust_anchor);
|
||||||
|
let vap_rows = build_vap_compare_rows(&out.tree.aspas, &trust_anchor);
|
||||||
|
let (ccr_vrps, ccr_vaps) = decode_ccr_compare_views(&decoded, &trust_anchor)?;
|
||||||
|
if vrp_rows != ccr_vrps {
|
||||||
|
return Err("base-vrps compare view does not match base.ccr".to_string());
|
||||||
|
}
|
||||||
|
if vap_rows != ccr_vaps {
|
||||||
|
return Err("base-vaps compare view does not match base.ccr".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_vrps_csv = rir_dir.join("base-vrps.csv");
|
||||||
|
let base_vaps_csv = rir_dir.join("base-vaps.csv");
|
||||||
|
write_vrp_csv(&base_vrps_csv, &vrp_rows)?;
|
||||||
|
write_vap_csv(&base_vaps_csv, &vap_rows)?;
|
||||||
|
|
||||||
|
copy_dir_all(replay_archive, &rir_dir.join("base-payload-archive"))?;
|
||||||
|
let mut base_locks_json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(replay_locks)
|
||||||
|
.map_err(|e| format!("read base locks failed: {}: {e}", replay_locks.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse base locks failed: {}: {e}", replay_locks.display()))?;
|
||||||
|
base_locks_json["validationTime"] = serde_json::Value::String(
|
||||||
|
base_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
);
|
||||||
|
let emitted_base_locks_path = rir_dir.join("base-locks.json");
|
||||||
|
write_json(&emitted_base_locks_path, &base_locks_json)?;
|
||||||
|
let emitted_base_locks_sha256 =
|
||||||
|
sha256_hex(&fs::read(&emitted_base_locks_path).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"read emitted base locks failed: {}: {e}",
|
||||||
|
emitted_base_locks_path.display()
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
|
||||||
|
if let Some(delta_archive) = args.payload_delta_archive.as_ref() {
|
||||||
|
copy_dir_all(delta_archive, &rir_dir.join("payload-delta-archive"))?;
|
||||||
|
}
|
||||||
|
if let Some(delta_locks) = args.payload_delta_locks.as_ref() {
|
||||||
|
let mut delta_json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(delta_locks)
|
||||||
|
.map_err(|e| format!("read delta locks failed: {}: {e}", delta_locks.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse delta locks failed: {}: {e}", delta_locks.display()))?;
|
||||||
|
if let Some(delta_time) = delta_validation_time.as_ref() {
|
||||||
|
delta_json
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or_else(|| "delta locks json must be an object".to_string())?
|
||||||
|
.insert(
|
||||||
|
"validationTime".to_string(),
|
||||||
|
serde_json::Value::String(
|
||||||
|
delta_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format delta validation time failed: {e}"))?,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
write_json(&rir_dir.join("locks-delta.json"), &delta_json)?;
|
||||||
|
}
|
||||||
|
if args.payload_delta_archive.is_some() && args.payload_delta_locks.is_some() {
|
||||||
|
rewrite_delta_base_locks_sha(&rir_dir, &emitted_base_locks_sha256)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(rir_dir.join("tal.tal"), &tal_bytes).map_err(|e| format!("write tal failed: {e}"))?;
|
||||||
|
fs::write(rir_dir.join("ta.cer"), &ta_bytes).map_err(|e| format!("write ta failed: {e}"))?;
|
||||||
|
|
||||||
|
let mut metadata = RirBundleMetadata {
|
||||||
|
schema_version: "20260330-v1".to_string(),
|
||||||
|
bundle_producer: "ours".to_string(),
|
||||||
|
rir: rir_normalized.clone(),
|
||||||
|
base_validation_time: base_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
delta_validation_time: delta_validation_time.as_ref().map(|value| {
|
||||||
|
value
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.expect("delta validation time must format")
|
||||||
|
}),
|
||||||
|
tal_sha256: sha256_hex(&tal_bytes),
|
||||||
|
ta_cert_sha256: sha256_hex(&ta_bytes),
|
||||||
|
base_ccr_sha256: sha256_hex(&ccr_bytes),
|
||||||
|
delta_ccr_sha256: None,
|
||||||
|
has_aspa: !vap_rows.is_empty(),
|
||||||
|
has_router_key: verify.router_key_count > 0,
|
||||||
|
base_vrp_count: vrp_rows.len(),
|
||||||
|
base_vap_count: vap_rows.len(),
|
||||||
|
delta_vrp_count: None,
|
||||||
|
delta_vap_count: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::create_dir_all(rir_dir.join("timings"))
|
||||||
|
.map_err(|e| format!("create timings dir failed: {e}"))?;
|
||||||
|
write_timing_json(
|
||||||
|
&rir_dir.join("timings").join("base-produce.json"),
|
||||||
|
"base",
|
||||||
|
&base_validation_time,
|
||||||
|
base_duration,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut verification = serde_json::json!({
|
||||||
|
"base": {
|
||||||
|
"validationTime": metadata.base_validation_time,
|
||||||
|
"ccr": {
|
||||||
|
"path": "base.ccr",
|
||||||
|
"sha256": metadata.base_ccr_sha256,
|
||||||
|
"stateHashesOk": verify.state_hashes_ok,
|
||||||
|
"manifestInstances": verify.manifest_instances,
|
||||||
|
"roaVrpCount": verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"vrpsSelfMatch": true,
|
||||||
|
"vapsSelfMatch": true,
|
||||||
|
"baseVrpCount": metadata.base_vrp_count,
|
||||||
|
"baseVapCount": metadata.base_vap_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let (Some(delta_archive), Some(delta_locks), Some(delta_time)) = (
|
||||||
|
args.payload_delta_archive.as_ref(),
|
||||||
|
args.payload_delta_locks.as_ref(),
|
||||||
|
delta_validation_time.as_ref(),
|
||||||
|
) {
|
||||||
|
let delta_db_dir = run_root.join(".tmp").join(format!("{rir}-delta-db"));
|
||||||
|
if delta_db_dir.exists() {
|
||||||
|
fs::remove_dir_all(&delta_db_dir).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"remove old delta db failed: {}: {e}",
|
||||||
|
delta_db_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
if let Some(parent) = delta_db_dir.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create delta db parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let delta_store = RocksStore::open(&delta_db_dir)
|
||||||
|
.map_err(|e| format!("open delta rocksdb failed: {e}"))?;
|
||||||
|
let delta_started = Instant::now();
|
||||||
|
let delta_out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
|
||||||
|
&delta_store,
|
||||||
|
&Policy::default(),
|
||||||
|
&tal_bytes,
|
||||||
|
&ta_bytes,
|
||||||
|
None,
|
||||||
|
replay_archive,
|
||||||
|
replay_locks,
|
||||||
|
delta_archive,
|
||||||
|
delta_locks,
|
||||||
|
base_validation_time,
|
||||||
|
*delta_time,
|
||||||
|
&TreeRunConfig {
|
||||||
|
max_depth: args.max_depth,
|
||||||
|
max_instances: args.max_instances,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("delta replay failed: {e}"))?;
|
||||||
|
let delta_duration = delta_started.elapsed();
|
||||||
|
|
||||||
|
let delta_ccr = build_ccr_from_run(
|
||||||
|
&delta_store,
|
||||||
|
&[delta_out.discovery.trust_anchor.clone()],
|
||||||
|
&delta_out.tree.vrps,
|
||||||
|
&delta_out.tree.aspas,
|
||||||
|
&delta_out.tree.router_keys,
|
||||||
|
*delta_time,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("build delta ccr failed: {e}"))?;
|
||||||
|
|
||||||
|
let delta_ccr_path = rir_dir.join("delta.ccr");
|
||||||
|
write_ccr_file(&delta_ccr_path, &delta_ccr)
|
||||||
|
.map_err(|e| format!("write delta ccr failed: {e}"))?;
|
||||||
|
let delta_ccr_bytes = fs::read(&delta_ccr_path).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"read written delta ccr failed: {}: {e}",
|
||||||
|
delta_ccr_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let delta_decoded = decode_content_info(&delta_ccr_bytes)
|
||||||
|
.map_err(|e| format!("decode written delta ccr failed: {e}"))?;
|
||||||
|
let delta_verify = verify_content_info(&delta_decoded)
|
||||||
|
.map_err(|e| format!("verify delta ccr failed: {e}"))?;
|
||||||
|
|
||||||
|
let delta_vrp_rows = build_vrp_compare_rows(&delta_out.tree.vrps, &trust_anchor);
|
||||||
|
let delta_vap_rows = build_vap_compare_rows(&delta_out.tree.aspas, &trust_anchor);
|
||||||
|
let (delta_ccr_vrps, delta_ccr_vaps) =
|
||||||
|
decode_ccr_compare_views(&delta_decoded, &trust_anchor)?;
|
||||||
|
if delta_vrp_rows != delta_ccr_vrps {
|
||||||
|
return Err("record-delta.csv compare view does not match delta.ccr".to_string());
|
||||||
|
}
|
||||||
|
if delta_vap_rows != delta_ccr_vaps {
|
||||||
|
return Err("record-delta-vaps.csv compare view does not match delta.ccr".to_string());
|
||||||
|
}
|
||||||
|
write_vrp_csv(&rir_dir.join("record-delta.csv"), &delta_vrp_rows)?;
|
||||||
|
write_vap_csv(&rir_dir.join("record-delta-vaps.csv"), &delta_vap_rows)?;
|
||||||
|
write_timing_json(
|
||||||
|
&rir_dir.join("timings").join("delta-produce.json"),
|
||||||
|
"delta",
|
||||||
|
delta_time,
|
||||||
|
delta_duration,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
metadata.delta_ccr_sha256 = Some(sha256_hex(&delta_ccr_bytes));
|
||||||
|
metadata.delta_vrp_count = Some(delta_vrp_rows.len());
|
||||||
|
metadata.delta_vap_count = Some(delta_vap_rows.len());
|
||||||
|
metadata.has_aspa = metadata.has_aspa || !delta_vap_rows.is_empty();
|
||||||
|
metadata.has_router_key = metadata.has_router_key || delta_verify.router_key_count > 0;
|
||||||
|
|
||||||
|
verification["delta"] = serde_json::json!({
|
||||||
|
"validationTime": delta_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format delta validation time failed: {e}"))?,
|
||||||
|
"ccr": {
|
||||||
|
"path": "delta.ccr",
|
||||||
|
"sha256": metadata.delta_ccr_sha256.clone().expect("delta sha must exist"),
|
||||||
|
"stateHashesOk": delta_verify.state_hashes_ok,
|
||||||
|
"manifestInstances": delta_verify.manifest_instances,
|
||||||
|
"roaVrpCount": delta_verify.roa_vrp_count,
|
||||||
|
"aspaPayloadSets": delta_verify.aspa_payload_sets,
|
||||||
|
"routerKeyCount": delta_verify.router_key_count,
|
||||||
|
},
|
||||||
|
"compareViews": {
|
||||||
|
"vrpsSelfMatch": true,
|
||||||
|
"vapsSelfMatch": true,
|
||||||
|
"deltaVrpCount": metadata.delta_vrp_count,
|
||||||
|
"deltaVapCount": metadata.delta_vap_count,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&delta_db_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
write_json(&rir_dir.join("bundle.json"), &metadata)?;
|
||||||
|
write_json(&rir_dir.join("verification.json"), &verification)?;
|
||||||
|
write_top_readme(&run_root.join("README.md"), rir)?;
|
||||||
|
write_rir_readme(
|
||||||
|
&rir_dir.join("README.md"),
|
||||||
|
rir,
|
||||||
|
&metadata.base_validation_time,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let bundle_manifest = BundleManifest {
|
||||||
|
schema_version: "20260330-v1".to_string(),
|
||||||
|
bundle_producer: "ours".to_string(),
|
||||||
|
recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format recorded_at failed: {e}"))?,
|
||||||
|
rirs: vec![rir_normalized.clone()],
|
||||||
|
per_rir_bundles: vec![BundleManifestEntry {
|
||||||
|
rir: rir_normalized.clone(),
|
||||||
|
relative_path: rir_normalized,
|
||||||
|
base_validation_time: metadata.base_validation_time.clone(),
|
||||||
|
delta_validation_time: metadata.delta_validation_time.clone(),
|
||||||
|
has_aspa: metadata.has_aspa,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
write_json(&run_root.join("bundle-manifest.json"), &bundle_manifest)?;
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&db_dir);
|
||||||
|
|
||||||
|
Ok(run_root.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||||
|
let out = run(args)?;
|
||||||
|
println!("{}", out.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_requires_required_flags() {
|
||||||
|
let argv = vec![
|
||||||
|
"replay_bundle_record".to_string(),
|
||||||
|
"--rir".to_string(),
|
||||||
|
"apnic".to_string(),
|
||||||
|
"--out-dir".to_string(),
|
||||||
|
"out".to_string(),
|
||||||
|
"--tal-path".to_string(),
|
||||||
|
"tal".to_string(),
|
||||||
|
"--ta-path".to_string(),
|
||||||
|
"ta".to_string(),
|
||||||
|
"--payload-replay-archive".to_string(),
|
||||||
|
"archive".to_string(),
|
||||||
|
"--payload-replay-locks".to_string(),
|
||||||
|
"locks.json".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse");
|
||||||
|
assert_eq!(args.rir.as_deref(), Some("apnic"));
|
||||||
|
assert_eq!(args.out_dir.as_deref(), Some(Path::new("out")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_missing_requireds() {
|
||||||
|
let argv = vec!["replay_bundle_record".to_string()];
|
||||||
|
let err = parse_args(&argv).unwrap_err();
|
||||||
|
assert!(err.contains("--rir is required"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_validation_time_reads_top_level_validation_time() {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("locks.json");
|
||||||
|
std::fs::write(&path, r#"{"validationTime":"2026-03-16T11:49:15+08:00"}"#)
|
||||||
|
.expect("write locks");
|
||||||
|
let got = load_validation_time(&path).expect("load validation time");
|
||||||
|
assert_eq!(
|
||||||
|
got.format(&Rfc3339).expect("format"),
|
||||||
|
"2026-03-16T11:49:15+08:00"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_dir_all_copies_nested_tree() {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let src = dir.path().join("src");
|
||||||
|
let dst = dir.path().join("dst");
|
||||||
|
std::fs::create_dir_all(src.join("sub")).expect("mkdir");
|
||||||
|
std::fs::write(src.join("a.txt"), b"a").expect("write a");
|
||||||
|
std::fs::write(src.join("sub").join("b.txt"), b"b").expect("write b");
|
||||||
|
copy_dir_all(&src, &dst).expect("copy dir");
|
||||||
|
assert_eq!(std::fs::read(dst.join("a.txt")).expect("read a"), b"a");
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(dst.join("sub").join("b.txt")).expect("read b"),
|
||||||
|
b"b"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_base_bundle_record_smoke_root_only_apnic() {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let out_dir = dir.path().join("bundle");
|
||||||
|
let out = run(Args {
|
||||||
|
rir: Some("apnic".to_string()),
|
||||||
|
out_dir: Some(out_dir.clone()),
|
||||||
|
tal_path: Some(PathBuf::from("tests/fixtures/tal/apnic-rfc7730-https.tal")),
|
||||||
|
ta_path: Some(PathBuf::from("tests/fixtures/ta/apnic-ta.cer")),
|
||||||
|
payload_replay_archive: Some(PathBuf::from(
|
||||||
|
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/base-payload-archive",
|
||||||
|
)),
|
||||||
|
payload_replay_locks: Some(PathBuf::from(
|
||||||
|
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/base-locks.json",
|
||||||
|
)),
|
||||||
|
payload_delta_archive: Some(PathBuf::from(
|
||||||
|
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/payload-delta-archive",
|
||||||
|
)),
|
||||||
|
payload_delta_locks: Some(PathBuf::from(
|
||||||
|
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/locks-delta.json",
|
||||||
|
)),
|
||||||
|
validation_time: None,
|
||||||
|
max_depth: Some(0),
|
||||||
|
max_instances: Some(1),
|
||||||
|
trust_anchor: Some("apnic".to_string()),
|
||||||
|
})
|
||||||
|
.expect("run bundle record");
|
||||||
|
assert_eq!(out, out_dir);
|
||||||
|
assert!(out_dir.join("bundle-manifest.json").is_file());
|
||||||
|
assert!(out_dir.join("README.md").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("bundle.json").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("tal.tal").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("ta.cer").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("base-payload-archive").is_dir());
|
||||||
|
assert!(out_dir.join("apnic").join("base-locks.json").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("base.ccr").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("base-vrps.csv").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("base-vaps.csv").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("delta.ccr").is_file());
|
||||||
|
assert!(out_dir.join("apnic").join("record-delta.csv").is_file());
|
||||||
|
assert!(
|
||||||
|
out_dir
|
||||||
|
.join("apnic")
|
||||||
|
.join("record-delta-vaps.csv")
|
||||||
|
.is_file()
|
||||||
|
);
|
||||||
|
assert!(out_dir.join("apnic").join("verification.json").is_file());
|
||||||
|
let bundle_json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&std::fs::read(out_dir.join("apnic").join("bundle.json")).expect("read bundle.json"),
|
||||||
|
)
|
||||||
|
.expect("parse bundle.json");
|
||||||
|
assert_eq!(bundle_json["bundleProducer"], "ours");
|
||||||
|
assert_eq!(bundle_json["rir"], "apnic");
|
||||||
|
assert!(bundle_json.get("baseVrpCount").is_some());
|
||||||
|
assert!(bundle_json.get("baseCcrSha256").is_some());
|
||||||
|
assert!(bundle_json.get("deltaVrpCount").is_some());
|
||||||
|
assert!(bundle_json.get("deltaCcrSha256").is_some());
|
||||||
|
let base_locks_bytes = std::fs::read(out_dir.join("apnic").join("base-locks.json"))
|
||||||
|
.expect("read emitted base locks");
|
||||||
|
let expected_base_locks_sha = sha256_hex(&base_locks_bytes);
|
||||||
|
let delta_locks_json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&std::fs::read(out_dir.join("apnic").join("locks-delta.json"))
|
||||||
|
.expect("read delta locks"),
|
||||||
|
)
|
||||||
|
.expect("parse delta locks");
|
||||||
|
assert_eq!(delta_locks_json["baseLocksSha256"], expected_base_locks_sha);
|
||||||
|
}
|
||||||
|
}
|
||||||
1054
src/bin/replay_bundle_refresh_sequence_outputs.rs
Normal file
1054
src/bin/replay_bundle_refresh_sequence_outputs.rs
Normal file
File diff suppressed because it is too large
Load Diff
108
src/bin/repository_view_stats.rs
Normal file
108
src/bin/repository_view_stats.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use rocksdb::{DB, IteratorMode, Options};
|
||||||
|
use rpki::storage::{CF_REPOSITORY_VIEW, column_family_descriptors};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn usage() -> String {
|
||||||
|
let bin = "repository_view_stats";
|
||||||
|
format!(
|
||||||
|
"\
|
||||||
|
Usage:
|
||||||
|
{bin} --db <path>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--db <path> RocksDB directory
|
||||||
|
--help Show this help
|
||||||
|
"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args(argv: &[String]) -> Result<PathBuf, String> {
|
||||||
|
if argv.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||||
|
return Err(usage());
|
||||||
|
}
|
||||||
|
let mut db_path: Option<PathBuf> = None;
|
||||||
|
let mut i = 1usize;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--db" => {
|
||||||
|
i += 1;
|
||||||
|
let value = argv.get(i).ok_or("--db requires a value")?;
|
||||||
|
db_path = Some(PathBuf::from(value));
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_size(path: &Path) -> Result<u64, Box<dyn std::error::Error>> {
|
||||||
|
let mut total = 0u64;
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let file_type = entry.file_type()?;
|
||||||
|
if file_type.is_file() {
|
||||||
|
total = total.saturating_add(entry.metadata()?.len());
|
||||||
|
} else if file_type.is_dir() {
|
||||||
|
total = total.saturating_add(dir_size(&entry.path())?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
let db_path = parse_args(&argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
|
||||||
|
|
||||||
|
let mut opts = Options::default();
|
||||||
|
opts.create_if_missing(false);
|
||||||
|
opts.create_missing_column_families(false);
|
||||||
|
let db = DB::open_cf_descriptors(&opts, &db_path, column_family_descriptors())?;
|
||||||
|
let cf = db
|
||||||
|
.cf_handle(CF_REPOSITORY_VIEW)
|
||||||
|
.ok_or("missing repository_view column family")?;
|
||||||
|
|
||||||
|
let mut kv_count = 0u64;
|
||||||
|
let mut key_bytes_total = 0u64;
|
||||||
|
let mut value_bytes_total = 0u64;
|
||||||
|
let mut max_key_bytes = 0usize;
|
||||||
|
let mut max_value_bytes = 0usize;
|
||||||
|
|
||||||
|
for entry in db.iterator_cf(cf, IteratorMode::Start) {
|
||||||
|
let (key, value) = entry?;
|
||||||
|
kv_count += 1;
|
||||||
|
key_bytes_total += key.len() as u64;
|
||||||
|
value_bytes_total += value.len() as u64;
|
||||||
|
max_key_bytes = max_key_bytes.max(key.len());
|
||||||
|
max_value_bytes = max_value_bytes.max(value.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
let logical_total_bytes = key_bytes_total + value_bytes_total;
|
||||||
|
let avg_key_bytes = if kv_count > 0 {
|
||||||
|
key_bytes_total as f64 / kv_count as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let avg_value_bytes = if kv_count > 0 {
|
||||||
|
value_bytes_total as f64 / kv_count as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let out = serde_json::json!({
|
||||||
|
"db_path": db_path.display().to_string(),
|
||||||
|
"column_family": CF_REPOSITORY_VIEW,
|
||||||
|
"kv_count": kv_count,
|
||||||
|
"key_bytes_total": key_bytes_total,
|
||||||
|
"value_bytes_total": value_bytes_total,
|
||||||
|
"logical_total_bytes": logical_total_bytes,
|
||||||
|
"db_dir_on_disk_bytes": dir_size(&db_path)?,
|
||||||
|
"avg_key_bytes": avg_key_bytes,
|
||||||
|
"avg_value_bytes": avg_value_bytes,
|
||||||
|
"max_key_bytes": max_key_bytes,
|
||||||
|
"max_value_bytes": max_value_bytes,
|
||||||
|
});
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -2,14 +2,12 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use rocksdb::{DB, IteratorMode, Options};
|
use rocksdb::{DB, IteratorMode, Options};
|
||||||
use rpki::storage::{
|
use rpki::storage::{
|
||||||
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_STATE, CF_RRDP_URI_OWNER,
|
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, RrdpSourceMemberRecord,
|
||||||
RrdpSourceMemberRecord, RrdpSourceRecord, RrdpUriOwnerRecord, column_family_descriptors,
|
RrdpSourceRecord, RrdpUriOwnerRecord, column_family_descriptors,
|
||||||
};
|
};
|
||||||
use rpki::sync::rrdp::RrdpState;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum DumpView {
|
enum DumpView {
|
||||||
LegacyState,
|
|
||||||
Source,
|
Source,
|
||||||
Members,
|
Members,
|
||||||
Owners,
|
Owners,
|
||||||
@@ -19,13 +17,12 @@ enum DumpView {
|
|||||||
impl DumpView {
|
impl DumpView {
|
||||||
fn parse(value: &str) -> Result<Self, String> {
|
fn parse(value: &str) -> Result<Self, String> {
|
||||||
match value {
|
match value {
|
||||||
"legacy-state" => Ok(Self::LegacyState),
|
|
||||||
"source" => Ok(Self::Source),
|
"source" => Ok(Self::Source),
|
||||||
"members" => Ok(Self::Members),
|
"members" => Ok(Self::Members),
|
||||||
"owners" => Ok(Self::Owners),
|
"owners" => Ok(Self::Owners),
|
||||||
"all" => Ok(Self::All),
|
"all" => Ok(Self::All),
|
||||||
other => Err(format!(
|
other => Err(format!(
|
||||||
"invalid --view: {other} (expected one of: legacy-state, source, members, owners, all)"
|
"invalid --view: {other} (expected one of: source, members, owners, all)"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +39,7 @@ fn usage() -> String {
|
|||||||
format!(
|
format!(
|
||||||
"\
|
"\
|
||||||
Usage:
|
Usage:
|
||||||
{bin} --db <path> [--view <legacy-state|source|members|owners|all>]
|
{bin} --db <path> [--view <source|members|owners|all>]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--db <path> RocksDB directory
|
--db <path> RocksDB directory
|
||||||
@@ -94,22 +91,6 @@ fn open_db(path: &std::path::Path) -> Result<DB, Box<dyn std::error::Error>> {
|
|||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_legacy_state(db: &DB) -> Result<Vec<(String, RrdpState)>, Box<dyn std::error::Error>> {
|
|
||||||
let cf = db
|
|
||||||
.cf_handle(CF_RRDP_STATE)
|
|
||||||
.ok_or("missing column family: rrdp_state")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for res in db.iterator_cf(cf, IteratorMode::Start) {
|
|
||||||
let (k, v) = res?;
|
|
||||||
let notify_uri = String::from_utf8_lossy(&k).to_string();
|
|
||||||
let state = RrdpState::decode(&v)
|
|
||||||
.map_err(|e| format!("decode rrdp_state failed for {notify_uri}: {e}"))?;
|
|
||||||
out.push((notify_uri, state));
|
|
||||||
}
|
|
||||||
out.sort_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_source_records(db: &DB) -> Result<Vec<RrdpSourceRecord>, Box<dyn std::error::Error>> {
|
fn collect_source_records(db: &DB) -> Result<Vec<RrdpSourceRecord>, Box<dyn std::error::Error>> {
|
||||||
let cf = db
|
let cf = db
|
||||||
.cf_handle(CF_RRDP_SOURCE)
|
.cf_handle(CF_RRDP_SOURCE)
|
||||||
@@ -163,14 +144,6 @@ fn collect_uri_owner_records(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_legacy_state(entries: &[(String, RrdpState)]) {
|
|
||||||
println!("[legacy-state]");
|
|
||||||
println!("notify_uri\tserial\tsession_id");
|
|
||||||
for (notify_uri, state) in entries {
|
|
||||||
println!("{notify_uri}\t{}\t{}", state.serial, state.session_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_source_records(entries: &[RrdpSourceRecord]) {
|
fn print_source_records(entries: &[RrdpSourceRecord]) {
|
||||||
println!("[source]");
|
println!("[source]");
|
||||||
println!("notify_uri\tlast_serial\tlast_session_id\tsync_state\tlast_snapshot_uri\tlast_error");
|
println!("notify_uri\tlast_serial\tlast_session_id\tsync_state\tlast_snapshot_uri\tlast_error");
|
||||||
@@ -228,12 +201,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let db = open_db(&args.db_path)?;
|
let db = open_db(&args.db_path)?;
|
||||||
|
|
||||||
match args.view {
|
match args.view {
|
||||||
DumpView::LegacyState => print_legacy_state(&collect_legacy_state(&db)?),
|
|
||||||
DumpView::Source => print_source_records(&collect_source_records(&db)?),
|
DumpView::Source => print_source_records(&collect_source_records(&db)?),
|
||||||
DumpView::Members => print_source_member_records(&collect_source_member_records(&db)?),
|
DumpView::Members => print_source_member_records(&collect_source_member_records(&db)?),
|
||||||
DumpView::Owners => print_uri_owner_records(&collect_uri_owner_records(&db)?),
|
DumpView::Owners => print_uri_owner_records(&collect_uri_owner_records(&db)?),
|
||||||
DumpView::All => {
|
DumpView::All => {
|
||||||
print_legacy_state(&collect_legacy_state(&db)?);
|
|
||||||
print_source_records(&collect_source_records(&db)?);
|
print_source_records(&collect_source_records(&db)?);
|
||||||
print_source_member_records(&collect_source_member_records(&db)?);
|
print_source_member_records(&collect_source_member_records(&db)?);
|
||||||
print_uri_owner_records(&collect_uri_owner_records(&db)?);
|
print_uri_owner_records(&collect_uri_owner_records(&db)?);
|
||||||
@@ -275,19 +246,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collect_rrdp_views_reads_legacy_and_current_records() {
|
fn collect_rrdp_views_reads_current_records() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
let store = RocksStore::open(dir.path()).expect("open store");
|
let store = RocksStore::open(dir.path()).expect("open store");
|
||||||
let legacy_state = RrdpState {
|
|
||||||
session_id: "session-1".to_string(),
|
|
||||||
serial: 42,
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_rrdp_state(
|
|
||||||
"https://example.test/notify.xml",
|
|
||||||
&legacy_state.encode().expect("encode legacy state"),
|
|
||||||
)
|
|
||||||
.expect("put legacy state");
|
|
||||||
store
|
store
|
||||||
.put_rrdp_source_record(&RrdpSourceRecord {
|
.put_rrdp_source_record(&RrdpSourceRecord {
|
||||||
notify_uri: "https://example.test/notify.xml".to_string(),
|
notify_uri: "https://example.test/notify.xml".to_string(),
|
||||||
@@ -328,14 +289,10 @@ mod tests {
|
|||||||
drop(store);
|
drop(store);
|
||||||
|
|
||||||
let db = open_db(dir.path()).expect("open db");
|
let db = open_db(dir.path()).expect("open db");
|
||||||
let legacy = collect_legacy_state(&db).expect("legacy dump");
|
|
||||||
let sources = collect_source_records(&db).expect("source dump");
|
let sources = collect_source_records(&db).expect("source dump");
|
||||||
let members = collect_source_member_records(&db).expect("members dump");
|
let members = collect_source_member_records(&db).expect("members dump");
|
||||||
let owners = collect_uri_owner_records(&db).expect("owners dump");
|
let owners = collect_uri_owner_records(&db).expect("owners dump");
|
||||||
|
|
||||||
assert_eq!(legacy.len(), 1);
|
|
||||||
assert_eq!(legacy[0].0, "https://example.test/notify.xml");
|
|
||||||
assert_eq!(legacy[0].1.serial, 42);
|
|
||||||
assert_eq!(sources.len(), 1);
|
assert_eq!(sources.len(), 1);
|
||||||
assert_eq!(sources[0].sync_state, RrdpSourceSyncState::DeltaReady);
|
assert_eq!(sources[0].sync_state, RrdpSourceSyncState::DeltaReady);
|
||||||
assert_eq!(members.len(), 1);
|
assert_eq!(members.len(), 1);
|
||||||
|
|||||||
103
src/bin/trace_arin_missing_vrps.rs
Normal file
103
src/bin/trace_arin_missing_vrps.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use rpki::audit_trace::trace_rule_to_root;
|
||||||
|
use rpki::storage::{AuditRuleKind, RocksStore, VcirOutputType};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 3 {
|
||||||
|
eprintln!("usage: trace_arin_missing_vrps <db> <row> [<row> ...]");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = RocksStore::open(Path::new(&args[1])).expect("open db");
|
||||||
|
let vcirs = store.list_vcirs().expect("list vcirs");
|
||||||
|
|
||||||
|
for row in &args[2..] {
|
||||||
|
let parts: Vec<&str> = row.split(',').collect();
|
||||||
|
if parts.len() != 4 {
|
||||||
|
println!("ROW {row}");
|
||||||
|
println!("ERROR invalid compare row");
|
||||||
|
println!();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let asn: u32 = parts[0]
|
||||||
|
.trim_start_matches("AS")
|
||||||
|
.parse()
|
||||||
|
.expect("parse asn");
|
||||||
|
let prefix = parts[1].to_string();
|
||||||
|
let max_length: u8 = parts[2].parse().expect("parse max length");
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
println!("ROW {row}");
|
||||||
|
for vcir in &vcirs {
|
||||||
|
for output in &vcir.local_outputs {
|
||||||
|
if output.output_type != VcirOutputType::Vrp {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let payload: Value = match serde_json::from_str(&output.payload_json) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let payload_asn = payload
|
||||||
|
.get("asn")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|v| v as u32);
|
||||||
|
let payload_prefix = payload
|
||||||
|
.get("prefix")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|v| v.to_string());
|
||||||
|
let payload_max = payload
|
||||||
|
.get("max_length")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|v| v as u8);
|
||||||
|
if payload_asn == Some(asn)
|
||||||
|
&& payload_prefix.as_ref() == Some(&prefix)
|
||||||
|
&& payload_max == Some(max_length)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
println!("manifest_rsync_uri={}", vcir.manifest_rsync_uri);
|
||||||
|
println!("source_object_uri={}", output.source_object_uri);
|
||||||
|
println!("source_object_hash={}", output.source_object_hash);
|
||||||
|
println!("source_ee_cert_hash={}", output.source_ee_cert_hash);
|
||||||
|
println!("rule_hash={}", output.rule_hash);
|
||||||
|
println!("validation_path_hint={:?}", output.validation_path_hint);
|
||||||
|
|
||||||
|
if let Some(trace) =
|
||||||
|
trace_rule_to_root(&store, AuditRuleKind::Roa, &output.rule_hash)
|
||||||
|
.expect("trace rule")
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"trace_leaf_manifest={}",
|
||||||
|
trace
|
||||||
|
.chain_leaf_to_root
|
||||||
|
.first()
|
||||||
|
.map(|node| node.manifest_rsync_uri.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"trace_source_object_uri={}",
|
||||||
|
trace.resolved_output.source_object_uri
|
||||||
|
);
|
||||||
|
println!("trace_chain_len={}", trace.chain_leaf_to_root.len());
|
||||||
|
for (idx, node) in trace.chain_leaf_to_root.iter().enumerate() {
|
||||||
|
println!("chain[{idx}].manifest={}", node.manifest_rsync_uri);
|
||||||
|
println!(
|
||||||
|
"chain[{idx}].current_manifest={}",
|
||||||
|
node.current_manifest_rsync_uri
|
||||||
|
);
|
||||||
|
println!("chain[{idx}].current_crl={}", node.current_crl_rsync_uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
println!("NOT_FOUND");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
323
src/blob_store.rs
Normal file
323
src/blob_store.rs
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rocksdb::{DB, Options, WriteBatch};
|
||||||
|
|
||||||
|
use crate::storage::{RawByHashEntry, RocksStore, StorageError, StorageResult};
|
||||||
|
|
||||||
|
const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
|
||||||
|
|
||||||
|
fn raw_by_hash_key(sha256_hex: &str) -> String {
|
||||||
|
format!("{RAW_BY_HASH_KEY_PREFIX}{sha256_hex}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RawObjectStore {
|
||||||
|
fn get_raw_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>>;
|
||||||
|
|
||||||
|
fn get_raw_entries_batch(
|
||||||
|
&self,
|
||||||
|
sha256_hexes: &[String],
|
||||||
|
) -> StorageResult<Vec<Option<RawByHashEntry>>>;
|
||||||
|
|
||||||
|
fn get_blob_bytes(&self, sha256_hex: &str) -> StorageResult<Option<Vec<u8>>> {
|
||||||
|
self.get_raw_entry(sha256_hex)
|
||||||
|
.map(|entry| entry.map(|entry| entry.bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_blob_bytes_batch(
|
||||||
|
&self,
|
||||||
|
sha256_hexes: &[String],
|
||||||
|
) -> StorageResult<Vec<Option<Vec<u8>>>> {
|
||||||
|
self.get_raw_entries_batch(sha256_hexes).map(|entries| {
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| entry.map(|entry| entry.bytes))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ExternalRawStoreDb {
|
||||||
|
path: PathBuf,
|
||||||
|
db: Arc<DB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExternalRawStoreDb {
|
||||||
|
pub fn open(path: impl Into<PathBuf>) -> StorageResult<Self> {
|
||||||
|
let path = path.into();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
|
}
|
||||||
|
let mut opts = Options::default();
|
||||||
|
opts.create_if_missing(true);
|
||||||
|
opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
|
||||||
|
let db = DB::open(&opts, &path).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
|
Ok(Self {
|
||||||
|
path,
|
||||||
|
db: Arc::new(db),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put_raw_entry(&self, entry: &RawByHashEntry) -> StorageResult<()> {
|
||||||
|
entry.validate_internal()?;
|
||||||
|
let key = raw_by_hash_key(&entry.sha256_hex);
|
||||||
|
let value =
|
||||||
|
serde_cbor::to_vec(entry).map_err(|e| StorageError::Codec { entity: "raw_by_hash", detail: e.to_string() })?;
|
||||||
|
self.db
|
||||||
|
.put(key.as_bytes(), value)
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put_raw_entries_batch(&self, entries: &[RawByHashEntry]) -> StorageResult<()> {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut batch = WriteBatch::default();
|
||||||
|
for entry in entries {
|
||||||
|
entry.validate_internal()?;
|
||||||
|
let key = raw_by_hash_key(&entry.sha256_hex);
|
||||||
|
let value = serde_cbor::to_vec(entry).map_err(|e| StorageError::Codec {
|
||||||
|
entity: "raw_by_hash",
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
batch.put(key.as_bytes(), value);
|
||||||
|
}
|
||||||
|
self.db
|
||||||
|
.write(batch)
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_raw_entry(&self, sha256_hex: &str) -> StorageResult<()> {
|
||||||
|
let key = raw_by_hash_key(sha256_hex);
|
||||||
|
self.db
|
||||||
|
.delete(key.as_bytes())
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &PathBuf {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawObjectStore for RocksStore {
|
||||||
|
fn get_raw_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>> {
|
||||||
|
self.get_raw_by_hash_entry(sha256_hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw_entries_batch(
|
||||||
|
&self,
|
||||||
|
sha256_hexes: &[String],
|
||||||
|
) -> StorageResult<Vec<Option<RawByHashEntry>>> {
|
||||||
|
self.get_raw_by_hash_entries_batch(sha256_hexes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawObjectStore for ExternalRawStoreDb {
|
||||||
|
fn get_raw_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>> {
|
||||||
|
let key = raw_by_hash_key(sha256_hex);
|
||||||
|
let Some(bytes) = self
|
||||||
|
.db
|
||||||
|
.get(key.as_bytes())
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let entry = serde_cbor::from_slice::<RawByHashEntry>(&bytes).map_err(|e| StorageError::Codec {
|
||||||
|
entity: "raw_by_hash",
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
entry.validate_internal()?;
|
||||||
|
Ok(Some(entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw_entries_batch(
|
||||||
|
&self,
|
||||||
|
sha256_hexes: &[String],
|
||||||
|
) -> StorageResult<Vec<Option<RawByHashEntry>>> {
|
||||||
|
if sha256_hexes.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let keys: Vec<String> = sha256_hexes.iter().map(|hash| raw_by_hash_key(hash)).collect();
|
||||||
|
self.db
|
||||||
|
.multi_get(keys.iter().map(|key| key.as_bytes()))
|
||||||
|
.into_iter()
|
||||||
|
.map(|res| {
|
||||||
|
let maybe = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
|
match maybe {
|
||||||
|
Some(bytes) => {
|
||||||
|
let entry = serde_cbor::from_slice::<RawByHashEntry>(&bytes).map_err(|e| {
|
||||||
|
StorageError::Codec {
|
||||||
|
entity: "raw_by_hash",
|
||||||
|
detail: e.to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
entry.validate_internal()?;
|
||||||
|
Ok(Some(entry))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{ExternalRawStoreDb, RawObjectStore};
|
||||||
|
use crate::storage::{RawByHashEntry, RocksStore, StorageError};
|
||||||
|
|
||||||
|
fn sha256_hex(bytes: &[u8]) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
hex::encode(Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rocks_store_raw_object_store_reads_single_and_batch_entries() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||||
|
|
||||||
|
let a = b"object-a".to_vec();
|
||||||
|
let b = b"object-b".to_vec();
|
||||||
|
let a_hash = sha256_hex(&a);
|
||||||
|
let b_hash = sha256_hex(&b);
|
||||||
|
|
||||||
|
store
|
||||||
|
.put_raw_by_hash_entry(&RawByHashEntry::from_bytes(a_hash.clone(), a.clone()))
|
||||||
|
.expect("put a");
|
||||||
|
store
|
||||||
|
.put_raw_by_hash_entry(&RawByHashEntry::from_bytes(b_hash.clone(), b.clone()))
|
||||||
|
.expect("put b");
|
||||||
|
|
||||||
|
let single = store
|
||||||
|
.get_raw_entry(&a_hash)
|
||||||
|
.expect("get single")
|
||||||
|
.expect("present");
|
||||||
|
assert_eq!(single.bytes, a);
|
||||||
|
|
||||||
|
let batch = store
|
||||||
|
.get_raw_entries_batch(&[a_hash.clone(), "00".repeat(32), b_hash.clone()])
|
||||||
|
.expect("get batch");
|
||||||
|
assert_eq!(batch.len(), 3);
|
||||||
|
assert_eq!(batch[0].as_ref().map(|entry| entry.bytes.as_slice()), Some(a.as_slice()));
|
||||||
|
assert!(batch[1].is_none());
|
||||||
|
assert_eq!(batch[2].as_ref().map(|entry| entry.bytes.as_slice()), Some(b.as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_raw_store_db_roundtrips_entries() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store");
|
||||||
|
|
||||||
|
let mut entry = RawByHashEntry::from_bytes(sha256_hex(b"blob"), b"blob".to_vec());
|
||||||
|
entry.origin_uris.push("rsync://example.test/repo/a.cer".to_string());
|
||||||
|
entry.object_type = Some("cer".to_string());
|
||||||
|
raw_store.put_raw_entry(&entry).expect("put raw entry");
|
||||||
|
|
||||||
|
let got = raw_store
|
||||||
|
.get_raw_entry(&entry.sha256_hex)
|
||||||
|
.expect("read raw entry")
|
||||||
|
.expect("entry exists");
|
||||||
|
assert_eq!(got, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_raw_store_db_batch_writes_and_reads() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store");
|
||||||
|
|
||||||
|
let a = RawByHashEntry::from_bytes(sha256_hex(b"a"), b"a".to_vec());
|
||||||
|
let b = RawByHashEntry::from_bytes(sha256_hex(b"b"), b"b".to_vec());
|
||||||
|
raw_store
|
||||||
|
.put_raw_entries_batch(&[a.clone(), b.clone()])
|
||||||
|
.expect("batch put");
|
||||||
|
|
||||||
|
let batch = raw_store
|
||||||
|
.get_raw_entries_batch(&[a.sha256_hex.clone(), b.sha256_hex.clone()])
|
||||||
|
.expect("batch get");
|
||||||
|
assert_eq!(batch.len(), 2);
|
||||||
|
assert_eq!(batch[0], Some(a));
|
||||||
|
assert_eq!(batch[1], Some(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raw_object_store_default_blob_helpers_return_bytes_only() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("nested/raw-store.db"))
|
||||||
|
.expect("open raw store");
|
||||||
|
|
||||||
|
let mut entry = RawByHashEntry::from_bytes(sha256_hex(b"blob"), b"blob".to_vec());
|
||||||
|
entry.origin_uris.push("rsync://example.test/repo/blob.roa".to_string());
|
||||||
|
raw_store.put_raw_entry(&entry).expect("put raw entry");
|
||||||
|
|
||||||
|
let single = raw_store
|
||||||
|
.get_blob_bytes(&entry.sha256_hex)
|
||||||
|
.expect("get blob bytes")
|
||||||
|
.expect("entry exists");
|
||||||
|
assert_eq!(single, b"blob".to_vec());
|
||||||
|
|
||||||
|
let batch = raw_store
|
||||||
|
.get_blob_bytes_batch(&[entry.sha256_hex.clone(), "00".repeat(32)])
|
||||||
|
.expect("get blob bytes batch");
|
||||||
|
assert_eq!(batch, vec![Some(b"blob".to_vec()), None]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_raw_store_db_delete_removes_entry() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store");
|
||||||
|
|
||||||
|
let entry = RawByHashEntry::from_bytes(sha256_hex(b"gone"), b"gone".to_vec());
|
||||||
|
raw_store.put_raw_entry(&entry).expect("put");
|
||||||
|
assert!(raw_store.get_raw_entry(&entry.sha256_hex).unwrap().is_some());
|
||||||
|
|
||||||
|
raw_store
|
||||||
|
.delete_raw_entry(&entry.sha256_hex)
|
||||||
|
.expect("delete entry");
|
||||||
|
assert!(raw_store.get_raw_entry(&entry.sha256_hex).unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_raw_store_db_rejects_invalid_entry_on_put() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store");
|
||||||
|
|
||||||
|
let bad = RawByHashEntry {
|
||||||
|
sha256_hex: "11".repeat(32),
|
||||||
|
bytes: b"blob".to_vec(),
|
||||||
|
origin_uris: Vec::new(),
|
||||||
|
object_type: None,
|
||||||
|
encoding: None,
|
||||||
|
};
|
||||||
|
let err = raw_store.put_raw_entry(&bad).expect_err("invalid hash should fail");
|
||||||
|
assert!(matches!(err, StorageError::InvalidData { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_raw_store_db_reports_codec_error_for_corrupt_value() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store");
|
||||||
|
raw_store
|
||||||
|
.db
|
||||||
|
.put(b"rawbyhash:deadbeef", b"not-cbor")
|
||||||
|
.expect("inject corrupt bytes");
|
||||||
|
|
||||||
|
let err = raw_store
|
||||||
|
.get_raw_entry("deadbeef")
|
||||||
|
.expect_err("corrupt value should fail");
|
||||||
|
assert!(matches!(err, StorageError::Codec { entity: "raw_by_hash", .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_raw_store_db_batch_returns_empty_for_empty_request() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let raw_store = ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store");
|
||||||
|
let entries = raw_store
|
||||||
|
.get_raw_entries_batch(&[])
|
||||||
|
.expect("empty batch succeeds");
|
||||||
|
assert!(entries.is_empty());
|
||||||
|
raw_store.put_raw_entries_batch(&[]).expect("empty put succeeds");
|
||||||
|
}
|
||||||
|
}
|
||||||
245
src/bundle/compare_view.rs
Normal file
245
src/bundle/compare_view.rs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::ccr::{CcrContentInfo, extract_vrp_rows};
|
||||||
|
use crate::validation::objects::{AspaAttestation, Vrp};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct VrpCompareRow {
|
||||||
|
pub asn: String,
|
||||||
|
pub ip_prefix: String,
|
||||||
|
pub max_length: String,
|
||||||
|
pub trust_anchor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct VapCompareRow {
|
||||||
|
pub customer_asn: String,
|
||||||
|
pub providers: String,
|
||||||
|
pub trust_anchor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_asn(asn: u32) -> String {
|
||||||
|
format!("AS{asn}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonical_prefix(prefix: &crate::data_model::roa::IpPrefix) -> String {
|
||||||
|
let mut addr = prefix.addr_bytes().to_vec();
|
||||||
|
let total_bits = match prefix.afi {
|
||||||
|
crate::data_model::roa::RoaAfi::Ipv4 => 32usize,
|
||||||
|
crate::data_model::roa::RoaAfi::Ipv6 => 128usize,
|
||||||
|
};
|
||||||
|
let keep = usize::from(prefix.prefix_len);
|
||||||
|
for bit in keep..total_bits {
|
||||||
|
let byte = bit / 8;
|
||||||
|
let offset = 7 - (bit % 8);
|
||||||
|
addr[byte] &= !(1u8 << offset);
|
||||||
|
}
|
||||||
|
match prefix.afi {
|
||||||
|
crate::data_model::roa::RoaAfi::Ipv4 => {
|
||||||
|
let ipv4 = std::net::Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]);
|
||||||
|
format!("{ipv4}/{}", prefix.prefix_len)
|
||||||
|
}
|
||||||
|
crate::data_model::roa::RoaAfi::Ipv6 => {
|
||||||
|
let mut bytes = [0u8; 16];
|
||||||
|
bytes.copy_from_slice(&addr[..16]);
|
||||||
|
let ipv6 = std::net::Ipv6Addr::from(bytes);
|
||||||
|
format!("{ipv6}/{}", prefix.prefix_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_vrp_compare_rows(vrps: &[Vrp], trust_anchor: &str) -> BTreeSet<VrpCompareRow> {
|
||||||
|
vrps.iter()
|
||||||
|
.map(|vrp| VrpCompareRow {
|
||||||
|
asn: normalize_asn(vrp.asn),
|
||||||
|
ip_prefix: canonical_prefix(&vrp.prefix),
|
||||||
|
max_length: vrp.max_length.to_string(),
|
||||||
|
trust_anchor: trust_anchor.to_ascii_lowercase(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_vap_compare_rows(
|
||||||
|
aspas: &[AspaAttestation],
|
||||||
|
trust_anchor: &str,
|
||||||
|
) -> BTreeSet<VapCompareRow> {
|
||||||
|
aspas
|
||||||
|
.iter()
|
||||||
|
.map(|aspa| {
|
||||||
|
let mut providers = aspa.provider_as_ids.iter().copied().collect::<Vec<_>>();
|
||||||
|
providers.sort_unstable();
|
||||||
|
providers.dedup();
|
||||||
|
VapCompareRow {
|
||||||
|
customer_asn: normalize_asn(aspa.customer_as_id),
|
||||||
|
providers: providers
|
||||||
|
.into_iter()
|
||||||
|
.map(normalize_asn)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";"),
|
||||||
|
trust_anchor: trust_anchor.to_ascii_lowercase(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_ccr_compare_views(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
trust_anchor: &str,
|
||||||
|
) -> Result<(BTreeSet<VrpCompareRow>, BTreeSet<VapCompareRow>), String> {
|
||||||
|
let vrps = extract_vrp_rows(content_info)
|
||||||
|
.map_err(|e| format!("extract vrp rows from ccr failed: {e}"))?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(asn, prefix, max_length)| VrpCompareRow {
|
||||||
|
asn: normalize_asn(asn),
|
||||||
|
ip_prefix: prefix,
|
||||||
|
max_length: max_length.to_string(),
|
||||||
|
trust_anchor: trust_anchor.to_ascii_lowercase(),
|
||||||
|
})
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
let vaps = content_info
|
||||||
|
.content
|
||||||
|
.vaps
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| {
|
||||||
|
state
|
||||||
|
.aps
|
||||||
|
.iter()
|
||||||
|
.map(|vap| VapCompareRow {
|
||||||
|
customer_asn: normalize_asn(vap.customer_as_id),
|
||||||
|
providers: vap
|
||||||
|
.providers
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(normalize_asn)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";"),
|
||||||
|
trust_anchor: trust_anchor.to_ascii_lowercase(),
|
||||||
|
})
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok((vrps, vaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_vrp_csv(path: &Path, rows: &BTreeSet<VrpCompareRow>) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let mut file = std::io::BufWriter::new(
|
||||||
|
std::fs::File::create(path)
|
||||||
|
.map_err(|e| format!("create file failed: {}: {e}", path.display()))?,
|
||||||
|
);
|
||||||
|
writeln!(file, "ASN,IP Prefix,Max Length,Trust Anchor").map_err(|e| e.to_string())?;
|
||||||
|
for row in rows {
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"{},{},{},{}",
|
||||||
|
row.asn, row.ip_prefix, row.max_length, row.trust_anchor
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_vap_csv(path: &Path, rows: &BTreeSet<VapCompareRow>) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let mut file = std::io::BufWriter::new(
|
||||||
|
std::fs::File::create(path)
|
||||||
|
.map_err(|e| format!("create file failed: {}: {e}", path.display()))?,
|
||||||
|
);
|
||||||
|
writeln!(file, "Customer ASN,Providers,Trust Anchor").map_err(|e| e.to_string())?;
|
||||||
|
for row in rows {
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"{},{},{}",
|
||||||
|
row.customer_asn, row.providers, row.trust_anchor
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ccr::{
|
||||||
|
CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation,
|
||||||
|
build_aspa_payload_state, build_roa_payload_state,
|
||||||
|
};
|
||||||
|
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_vap_compare_rows_sorts_and_dedups_providers() {
|
||||||
|
let rows = build_vap_compare_rows(
|
||||||
|
&[AspaAttestation {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![64498, 64497, 64498],
|
||||||
|
}],
|
||||||
|
"APNIC",
|
||||||
|
);
|
||||||
|
let row = rows.iter().next().expect("one row");
|
||||||
|
assert_eq!(row.customer_asn, "AS64496");
|
||||||
|
assert_eq!(row.providers, "AS64497;AS64498");
|
||||||
|
assert_eq!(row.trust_anchor, "apnic");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_ccr_compare_views_extracts_vrps_and_vaps() {
|
||||||
|
let vrps = build_roa_payload_state(&[Vrp {
|
||||||
|
asn: 64496,
|
||||||
|
prefix: IpPrefix {
|
||||||
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len: 24,
|
||||||
|
addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
},
|
||||||
|
max_length: 24,
|
||||||
|
}])
|
||||||
|
.expect("build vrps");
|
||||||
|
let vaps = build_aspa_payload_state(&[AspaAttestation {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![64497],
|
||||||
|
}])
|
||||||
|
.expect("build vaps");
|
||||||
|
let content = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||||
|
version: 0,
|
||||||
|
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||||
|
produced_at: time::OffsetDateTime::now_utc(),
|
||||||
|
mfts: None,
|
||||||
|
vrps: Some(vrps),
|
||||||
|
vaps: Some(vaps),
|
||||||
|
tas: None,
|
||||||
|
rks: None,
|
||||||
|
});
|
||||||
|
let (vrp_rows, vap_rows) =
|
||||||
|
decode_ccr_compare_views(&content, "apnic").expect("decode compare views");
|
||||||
|
assert_eq!(vrp_rows.len(), 1);
|
||||||
|
assert_eq!(vap_rows.len(), 1);
|
||||||
|
assert_eq!(vap_rows.iter().next().unwrap().providers, "AS64497");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_vrp_compare_rows_canonicalizes_ipv6_prefix_text() {
|
||||||
|
let rows = build_vrp_compare_rows(
|
||||||
|
&[Vrp {
|
||||||
|
asn: 64496,
|
||||||
|
prefix: IpPrefix {
|
||||||
|
afi: RoaAfi::Ipv6,
|
||||||
|
prefix_len: 32,
|
||||||
|
addr: [0x20, 0x01, 0x0d, 0xb8, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||||
|
},
|
||||||
|
max_length: 48,
|
||||||
|
}],
|
||||||
|
"APNIC",
|
||||||
|
);
|
||||||
|
let row = rows.iter().next().expect("row");
|
||||||
|
assert_eq!(row.ip_prefix, "2001:db8::/32");
|
||||||
|
}
|
||||||
|
}
|
||||||
1722
src/bundle/live_capture.rs
Normal file
1722
src/bundle/live_capture.rs
Normal file
File diff suppressed because it is too large
Load Diff
24
src/bundle/mod.rs
Normal file
24
src/bundle/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
pub mod compare_view;
|
||||||
|
pub mod live_capture;
|
||||||
|
pub mod record_io;
|
||||||
|
pub mod spec;
|
||||||
|
|
||||||
|
pub use compare_view::{
|
||||||
|
VapCompareRow, VrpCompareRow, build_vap_compare_rows, build_vrp_compare_rows,
|
||||||
|
decode_ccr_compare_views, write_vap_csv, write_vrp_csv,
|
||||||
|
};
|
||||||
|
pub use live_capture::{
|
||||||
|
LiveBaseCaptureSummary, LiveDeltaCaptureSummary, RecordedHttpResponse, RecordedRsyncFetch,
|
||||||
|
RecordingHttpFetcher, RecordingRsyncFetcher, write_current_replay_state_locks,
|
||||||
|
write_live_base_replay_bundle_inputs, write_live_delta_replay_bundle_inputs,
|
||||||
|
write_live_delta_replay_step_inputs,
|
||||||
|
};
|
||||||
|
pub use record_io::{
|
||||||
|
build_single_rir_bundle_manifest, copy_dir_all, load_validation_time, sha256_hex, write_bytes,
|
||||||
|
write_json, write_live_bundle_rir_readme, write_live_bundle_top_readme, write_timing_json,
|
||||||
|
};
|
||||||
|
pub use spec::{
|
||||||
|
BaseBundleStateMetadataV2, BundleManifestEntryV2, BundleManifestV2, DeltaSequenceMetadataV2,
|
||||||
|
DeltaStepMetadataV2, RirBundleMetadataV2,
|
||||||
|
};
|
||||||
|
pub use spec::{BundleManifest, BundleManifestEntry, RirBundleMetadata};
|
||||||
274
src/bundle/record_io.rs
Normal file
274
src/bundle/record_io.rs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use sha2::Digest;
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
use super::{BundleManifest, BundleManifestEntry};
|
||||||
|
|
||||||
|
pub fn sha256_hex(bytes: &[u8]) -> String {
|
||||||
|
hex::encode(sha2::Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_json(path: &Path, value: &impl Serialize) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(path, bytes).map_err(|e| format!("write json failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_timing_json(
|
||||||
|
path: &Path,
|
||||||
|
mode: &str,
|
||||||
|
validation_time: &time::OffsetDateTime,
|
||||||
|
duration: std::time::Duration,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
write_json(
|
||||||
|
path,
|
||||||
|
&serde_json::json!({
|
||||||
|
"mode": mode,
|
||||||
|
"validationTime": validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||||
|
"durationSeconds": duration.as_secs_f64(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_live_bundle_top_readme(path: &Path, rir: &str) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(
|
||||||
|
path,
|
||||||
|
format!(
|
||||||
|
"# Ours Live Replay Bundle\n\nThis run contains one per-RIR bundle recorded online by `ours`.\n\n- RIR: `{rir}`\n- Reference result format: `CCR`\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write readme failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_live_bundle_rir_readme(
|
||||||
|
path: &Path,
|
||||||
|
rir: &str,
|
||||||
|
base_validation_time: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(
|
||||||
|
path,
|
||||||
|
format!(
|
||||||
|
"# {rir} live replay bundle\n\n- `tal.tal` and `ta.cer` are the actual live run inputs.\n- `base-locks.json.validationTime` = `{base_validation_time}`.\n- `base.ccr` is the authoritative reference result.\n- `base-vrps.csv` and `base-vaps.csv` are compare views derived from `base.ccr`.\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_bytes(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(path, bytes).map_err(|e| format!("write file failed: {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
|
||||||
|
fs::create_dir_all(dst)
|
||||||
|
.map_err(|e| format!("create directory failed: {}: {e}", dst.display()))?;
|
||||||
|
for entry in
|
||||||
|
fs::read_dir(src).map_err(|e| format!("read_dir failed: {}: {e}", src.display()))?
|
||||||
|
{
|
||||||
|
let entry = entry.map_err(|e| format!("read_dir entry failed: {}: {e}", src.display()))?;
|
||||||
|
let ty = entry
|
||||||
|
.file_type()
|
||||||
|
.map_err(|e| format!("file_type failed: {}: {e}", entry.path().display()))?;
|
||||||
|
let to = dst.join(entry.file_name());
|
||||||
|
if ty.is_dir() {
|
||||||
|
copy_dir_all(&entry.path(), &to)?;
|
||||||
|
} else if ty.is_file() {
|
||||||
|
if let Some(parent) = to.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::copy(entry.path(), &to).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"copy failed: {} -> {}: {e}",
|
||||||
|
entry.path().display(),
|
||||||
|
to.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_validation_time(path: &Path) -> Result<time::OffsetDateTime, String> {
|
||||||
|
let json: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(path).map_err(|e| format!("read json failed: {}: {e}", path.display()))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("parse json failed: {}: {e}", path.display()))?;
|
||||||
|
let value = json
|
||||||
|
.get("validationTime")
|
||||||
|
.or_else(|| json.get("validation_time"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| format!("validationTime missing in {}", path.display()))?;
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||||
|
.map_err(|e| format!("invalid validationTime in {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_single_rir_bundle_manifest(
|
||||||
|
schema_version: &str,
|
||||||
|
bundle_producer: &str,
|
||||||
|
rir: &str,
|
||||||
|
base_validation_time: &time::OffsetDateTime,
|
||||||
|
delta_validation_time: Option<&time::OffsetDateTime>,
|
||||||
|
has_aspa: bool,
|
||||||
|
) -> Result<BundleManifest, String> {
|
||||||
|
Ok(BundleManifest {
|
||||||
|
schema_version: schema_version.to_string(),
|
||||||
|
bundle_producer: bundle_producer.to_string(),
|
||||||
|
recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format recorded_at failed: {e}"))?,
|
||||||
|
rirs: vec![rir.to_string()],
|
||||||
|
per_rir_bundles: vec![BundleManifestEntry {
|
||||||
|
rir: rir.to_string(),
|
||||||
|
relative_path: rir.to_string(),
|
||||||
|
base_validation_time: base_validation_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format base validation time failed: {e}"))?,
|
||||||
|
delta_validation_time: match delta_validation_time {
|
||||||
|
Some(value) => Some(
|
||||||
|
value
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| format!("format delta validation time failed: {e}"))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
has_aspa,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_validation_time_reads_validation_time_field() {
|
||||||
|
let td = tempdir().expect("tempdir");
|
||||||
|
let path = td.path().join("locks.json");
|
||||||
|
fs::write(&path, r#"{"validationTime":"2026-04-01T00:00:00Z"}"#).expect("write");
|
||||||
|
let parsed = load_validation_time(&path).expect("load");
|
||||||
|
assert_eq!(
|
||||||
|
parsed.format(&Rfc3339).expect("format"),
|
||||||
|
"2026-04-01T00:00:00Z"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_dir_all_copies_nested_files() {
|
||||||
|
let td = tempdir().expect("tempdir");
|
||||||
|
let src = td.path().join("src");
|
||||||
|
let dst = td.path().join("dst");
|
||||||
|
fs::create_dir_all(src.join("nested")).expect("mkdir");
|
||||||
|
fs::write(src.join("root.txt"), b"root").expect("write root");
|
||||||
|
fs::write(src.join("nested/child.txt"), b"child").expect("write child");
|
||||||
|
copy_dir_all(&src, &dst).expect("copy");
|
||||||
|
assert_eq!(fs::read(dst.join("root.txt")).expect("read root"), b"root");
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(dst.join("nested/child.txt")).expect("read child"),
|
||||||
|
b"child"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_single_rir_bundle_manifest_formats_times() {
|
||||||
|
let base = time::OffsetDateTime::parse("2026-04-01T00:00:00Z", &Rfc3339).expect("base");
|
||||||
|
let delta = time::OffsetDateTime::parse("2026-04-01T00:10:00Z", &Rfc3339).expect("delta");
|
||||||
|
let manifest = build_single_rir_bundle_manifest(
|
||||||
|
"20260330-v1",
|
||||||
|
"ours",
|
||||||
|
"apnic",
|
||||||
|
&base,
|
||||||
|
Some(&delta),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("manifest");
|
||||||
|
assert_eq!(manifest.schema_version, "20260330-v1");
|
||||||
|
assert_eq!(manifest.rirs, vec!["apnic".to_string()]);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.per_rir_bundles[0].base_validation_time,
|
||||||
|
"2026-04-01T00:00:00Z"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.per_rir_bundles[0].delta_validation_time.as_deref(),
|
||||||
|
Some("2026-04-01T00:10:00Z")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_json_and_write_bytes_create_parent_directories() {
|
||||||
|
let td = tempdir().expect("tempdir");
|
||||||
|
let json_path = td.path().join("nested/meta/data.json");
|
||||||
|
write_json(&json_path, &serde_json::json!({"ok": true})).expect("write json");
|
||||||
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_slice(&fs::read(&json_path).expect("read json")).expect("parse");
|
||||||
|
assert_eq!(json["ok"], true);
|
||||||
|
|
||||||
|
let bytes_path = td.path().join("nested/raw/file.bin");
|
||||||
|
write_bytes(&bytes_path, b"payload").expect("write bytes");
|
||||||
|
assert_eq!(fs::read(&bytes_path).expect("read bytes"), b"payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_timing_and_readmes_emit_expected_text() {
|
||||||
|
let td = tempdir().expect("tempdir");
|
||||||
|
let timing_path = td.path().join("timings/base-produce.json");
|
||||||
|
let validation_time =
|
||||||
|
time::OffsetDateTime::parse("2026-04-01T00:00:00Z", &Rfc3339).expect("time");
|
||||||
|
write_timing_json(
|
||||||
|
&timing_path,
|
||||||
|
"base",
|
||||||
|
&validation_time,
|
||||||
|
std::time::Duration::from_secs_f64(1.25),
|
||||||
|
)
|
||||||
|
.expect("write timing");
|
||||||
|
let timing: serde_json::Value =
|
||||||
|
serde_json::from_slice(&fs::read(&timing_path).expect("read timing")).expect("parse");
|
||||||
|
assert_eq!(timing["mode"], "base");
|
||||||
|
assert_eq!(timing["validationTime"], "2026-04-01T00:00:00Z");
|
||||||
|
assert_eq!(timing["durationSeconds"], 1.25);
|
||||||
|
|
||||||
|
let top_readme = td.path().join("README.md");
|
||||||
|
write_live_bundle_top_readme(&top_readme, "apnic").expect("write top readme");
|
||||||
|
let top_text = fs::read_to_string(&top_readme).expect("read top readme");
|
||||||
|
assert!(top_text.contains("RIR: `apnic`"));
|
||||||
|
assert!(top_text.contains("Reference result format: `CCR`"));
|
||||||
|
|
||||||
|
let rir_readme = td.path().join("apnic/README.md");
|
||||||
|
write_live_bundle_rir_readme(&rir_readme, "apnic", "2026-04-01T00:00:00Z")
|
||||||
|
.expect("write rir readme");
|
||||||
|
let rir_text = fs::read_to_string(&rir_readme).expect("read rir readme");
|
||||||
|
assert!(rir_text.contains("base-locks.json.validationTime"));
|
||||||
|
assert!(rir_text.contains("base-vrps.csv"));
|
||||||
|
assert!(rir_text.contains("base-vaps.csv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_single_rir_bundle_manifest_supports_none_delta_time() {
|
||||||
|
let base = time::OffsetDateTime::parse("2026-04-01T00:00:00Z", &Rfc3339).expect("base");
|
||||||
|
let manifest =
|
||||||
|
build_single_rir_bundle_manifest("20260330-v1", "ours", "afrinic", &base, None, false)
|
||||||
|
.expect("manifest");
|
||||||
|
assert_eq!(manifest.per_rir_bundles[0].delta_validation_time, None);
|
||||||
|
assert!(!manifest.per_rir_bundles[0].has_aspa);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/bundle/spec.rs
Normal file
185
src/bundle/spec.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BundleManifest {
|
||||||
|
#[serde(rename = "schemaVersion")]
|
||||||
|
pub schema_version: String,
|
||||||
|
#[serde(rename = "bundleProducer")]
|
||||||
|
pub bundle_producer: String,
|
||||||
|
#[serde(rename = "recordedAt")]
|
||||||
|
pub recorded_at_rfc3339_utc: String,
|
||||||
|
pub rirs: Vec<String>,
|
||||||
|
#[serde(rename = "perRirBundles")]
|
||||||
|
pub per_rir_bundles: Vec<BundleManifestEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BundleManifestEntry {
|
||||||
|
pub rir: String,
|
||||||
|
pub relative_path: String,
|
||||||
|
#[serde(rename = "baseValidationTime")]
|
||||||
|
pub base_validation_time: String,
|
||||||
|
#[serde(
|
||||||
|
rename = "deltaValidationTime",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub delta_validation_time: Option<String>,
|
||||||
|
#[serde(rename = "hasAspa")]
|
||||||
|
pub has_aspa: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct RirBundleMetadata {
|
||||||
|
#[serde(rename = "schemaVersion")]
|
||||||
|
pub schema_version: String,
|
||||||
|
#[serde(rename = "bundleProducer")]
|
||||||
|
pub bundle_producer: String,
|
||||||
|
pub rir: String,
|
||||||
|
#[serde(rename = "baseValidationTime")]
|
||||||
|
pub base_validation_time: String,
|
||||||
|
#[serde(
|
||||||
|
rename = "deltaValidationTime",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub delta_validation_time: Option<String>,
|
||||||
|
#[serde(rename = "talSha256")]
|
||||||
|
pub tal_sha256: String,
|
||||||
|
#[serde(rename = "taCertSha256")]
|
||||||
|
pub ta_cert_sha256: String,
|
||||||
|
#[serde(rename = "baseCcrSha256")]
|
||||||
|
pub base_ccr_sha256: String,
|
||||||
|
#[serde(rename = "deltaCcrSha256", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub delta_ccr_sha256: Option<String>,
|
||||||
|
#[serde(rename = "hasAspa")]
|
||||||
|
pub has_aspa: bool,
|
||||||
|
#[serde(rename = "hasRouterKey")]
|
||||||
|
pub has_router_key: bool,
|
||||||
|
#[serde(rename = "baseVrpCount")]
|
||||||
|
pub base_vrp_count: usize,
|
||||||
|
#[serde(rename = "baseVapCount")]
|
||||||
|
pub base_vap_count: usize,
|
||||||
|
#[serde(rename = "deltaVrpCount", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub delta_vrp_count: Option<usize>,
|
||||||
|
#[serde(rename = "deltaVapCount", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub delta_vap_count: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BundleManifestV2 {
|
||||||
|
#[serde(rename = "schemaVersion")]
|
||||||
|
pub schema_version: String,
|
||||||
|
#[serde(rename = "bundleProducer")]
|
||||||
|
pub bundle_producer: String,
|
||||||
|
#[serde(rename = "recordedAt")]
|
||||||
|
pub recorded_at_rfc3339_utc: String,
|
||||||
|
pub rirs: Vec<String>,
|
||||||
|
#[serde(rename = "perRirBundles")]
|
||||||
|
pub per_rir_bundles: Vec<BundleManifestEntryV2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BundleManifestEntryV2 {
|
||||||
|
pub rir: String,
|
||||||
|
pub relative_path: String,
|
||||||
|
#[serde(rename = "baseValidationTime")]
|
||||||
|
pub base_validation_time: String,
|
||||||
|
#[serde(rename = "stepCount")]
|
||||||
|
pub step_count: usize,
|
||||||
|
#[serde(
|
||||||
|
rename = "firstDeltaValidationTime",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub first_delta_validation_time: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
rename = "lastDeltaValidationTime",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub last_delta_validation_time: Option<String>,
|
||||||
|
#[serde(rename = "hasAspa")]
|
||||||
|
pub has_aspa: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BaseBundleStateMetadataV2 {
|
||||||
|
#[serde(rename = "validationTime")]
|
||||||
|
pub validation_time: String,
|
||||||
|
#[serde(rename = "ccrSha256")]
|
||||||
|
pub ccr_sha256: String,
|
||||||
|
#[serde(rename = "vrpCount")]
|
||||||
|
pub vrp_count: usize,
|
||||||
|
#[serde(rename = "vapCount")]
|
||||||
|
pub vap_count: usize,
|
||||||
|
#[serde(rename = "relativeArchivePath")]
|
||||||
|
pub relative_archive_path: String,
|
||||||
|
#[serde(rename = "relativeLocksPath")]
|
||||||
|
pub relative_locks_path: String,
|
||||||
|
#[serde(rename = "relativeCcrPath")]
|
||||||
|
pub relative_ccr_path: String,
|
||||||
|
#[serde(rename = "relativeVrpsPath")]
|
||||||
|
pub relative_vrps_path: String,
|
||||||
|
#[serde(rename = "relativeVapsPath")]
|
||||||
|
pub relative_vaps_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct DeltaStepMetadataV2 {
|
||||||
|
pub index: usize,
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "relativePath")]
|
||||||
|
pub relative_path: String,
|
||||||
|
#[serde(rename = "baseRef")]
|
||||||
|
pub base_ref: String,
|
||||||
|
#[serde(rename = "validationTime")]
|
||||||
|
pub validation_time: String,
|
||||||
|
#[serde(rename = "deltaCcrSha256")]
|
||||||
|
pub delta_ccr_sha256: String,
|
||||||
|
#[serde(rename = "vrpCount")]
|
||||||
|
pub vrp_count: usize,
|
||||||
|
#[serde(rename = "vapCount")]
|
||||||
|
pub vap_count: usize,
|
||||||
|
#[serde(rename = "relativeArchivePath")]
|
||||||
|
pub relative_archive_path: String,
|
||||||
|
#[serde(rename = "relativeTransitionLocksPath")]
|
||||||
|
pub relative_transition_locks_path: String,
|
||||||
|
#[serde(rename = "relativeTargetLocksPath")]
|
||||||
|
pub relative_target_locks_path: String,
|
||||||
|
#[serde(rename = "relativeCcrPath")]
|
||||||
|
pub relative_ccr_path: String,
|
||||||
|
#[serde(rename = "relativeVrpsPath")]
|
||||||
|
pub relative_vrps_path: String,
|
||||||
|
#[serde(rename = "relativeVapsPath")]
|
||||||
|
pub relative_vaps_path: String,
|
||||||
|
#[serde(rename = "hasAspa")]
|
||||||
|
pub has_aspa: bool,
|
||||||
|
#[serde(rename = "hasRouterKey")]
|
||||||
|
pub has_router_key: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct DeltaSequenceMetadataV2 {
|
||||||
|
#[serde(rename = "configuredDeltaCount")]
|
||||||
|
pub configured_delta_count: usize,
|
||||||
|
#[serde(rename = "configuredIntervalSeconds")]
|
||||||
|
pub configured_interval_seconds: u64,
|
||||||
|
pub steps: Vec<DeltaStepMetadataV2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct RirBundleMetadataV2 {
|
||||||
|
#[serde(rename = "schemaVersion")]
|
||||||
|
pub schema_version: String,
|
||||||
|
#[serde(rename = "bundleProducer")]
|
||||||
|
pub bundle_producer: String,
|
||||||
|
pub rir: String,
|
||||||
|
#[serde(rename = "talSha256")]
|
||||||
|
pub tal_sha256: String,
|
||||||
|
#[serde(rename = "taCertSha256")]
|
||||||
|
pub ta_cert_sha256: String,
|
||||||
|
#[serde(rename = "hasAnyAspa")]
|
||||||
|
pub has_any_aspa: bool,
|
||||||
|
#[serde(rename = "hasAnyRouterKey")]
|
||||||
|
pub has_any_router_key: bool,
|
||||||
|
pub base: BaseBundleStateMetadataV2,
|
||||||
|
#[serde(rename = "deltaSequence")]
|
||||||
|
pub delta_sequence: DeltaSequenceMetadataV2,
|
||||||
|
}
|
||||||
876
src/ccr/build.rs
Normal file
876
src/ccr/build.rs
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
use crate::ccr::encode::{
|
||||||
|
encode_aspa_payload_state_payload_der, encode_manifest_state_payload_der,
|
||||||
|
encode_roa_payload_state_payload_der, encode_router_key_state_payload_der,
|
||||||
|
encode_trust_anchor_state_payload_der,
|
||||||
|
};
|
||||||
|
use crate::ccr::hash::compute_state_hash;
|
||||||
|
use crate::ccr::model::{
|
||||||
|
AspaPayloadSet, AspaPayloadState, ManifestInstance, ManifestState, RoaPayloadSet,
|
||||||
|
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
|
||||||
|
};
|
||||||
|
use crate::blob_store::RawObjectStore;
|
||||||
|
use crate::data_model::manifest::ManifestObject;
|
||||||
|
use crate::data_model::rc::{AccessDescription, SubjectInfoAccess};
|
||||||
|
use crate::data_model::roa::RoaAfi;
|
||||||
|
use crate::data_model::router_cert::BgpsecRouterCertificate;
|
||||||
|
use crate::data_model::ta::TrustAnchor;
|
||||||
|
use crate::storage::{RocksStore, ValidatedCaInstanceResult, VcirArtifactRole};
|
||||||
|
use crate::validation::objects::{AspaAttestation, Vrp};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcrBuildError {
|
||||||
|
#[error("trust anchor set must not be empty")]
|
||||||
|
EmptyTrustAnchors,
|
||||||
|
|
||||||
|
#[error("trust anchor certificate missing SubjectKeyIdentifier")]
|
||||||
|
MissingTrustAnchorSki,
|
||||||
|
|
||||||
|
#[error("ROA payload state encoding failed: {0}")]
|
||||||
|
RoaEncode(String),
|
||||||
|
|
||||||
|
#[error("ASPA payload state encoding failed: {0}")]
|
||||||
|
AspaEncode(String),
|
||||||
|
|
||||||
|
#[error("TrustAnchor state encoding failed: {0}")]
|
||||||
|
TrustAnchorEncode(String),
|
||||||
|
|
||||||
|
#[error("manifest artifact missing in VCIR: {0}")]
|
||||||
|
MissingManifestArtifact(String),
|
||||||
|
|
||||||
|
#[error("manifest raw bytes missing in store for {manifest_rsync_uri}: {sha256_hex}")]
|
||||||
|
MissingManifestRawBytes {
|
||||||
|
manifest_rsync_uri: String,
|
||||||
|
sha256_hex: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("manifest raw bytes load failed for {manifest_rsync_uri}: {detail}")]
|
||||||
|
LoadManifestRawBytes {
|
||||||
|
manifest_rsync_uri: String,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("manifest decode failed for {manifest_rsync_uri}: {detail}")]
|
||||||
|
ManifestDecode {
|
||||||
|
manifest_rsync_uri: String,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("manifest EE certificate missing AuthorityKeyIdentifier: {0}")]
|
||||||
|
ManifestEeMissingAki(String),
|
||||||
|
|
||||||
|
#[error("manifest EE certificate missing Subject Information Access extension: {0}")]
|
||||||
|
ManifestEeMissingSia(String),
|
||||||
|
|
||||||
|
#[error("manifest EE certificate SIA is not the EE form: {0}")]
|
||||||
|
ManifestEeSiaWrongVariant(String),
|
||||||
|
|
||||||
|
#[error("manifest child SKI is not valid lowercase hex or not 20 bytes: {0}")]
|
||||||
|
InvalidChildSki(String),
|
||||||
|
|
||||||
|
#[error("manifest AccessDescription contains unsupported accessMethod OID: {0}")]
|
||||||
|
UnsupportedAccessMethodOid(String),
|
||||||
|
|
||||||
|
#[error("manifest state encoding failed: {0}")]
|
||||||
|
ManifestEncode(String),
|
||||||
|
|
||||||
|
#[error("duplicate manifest hash with conflicting content for URI: {0}")]
|
||||||
|
DuplicateManifestHashConflict(String),
|
||||||
|
|
||||||
|
#[error("router key state encoding failed: {0}")]
|
||||||
|
RouterKeyEncode(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_roa_payload_state(vrps: &[Vrp]) -> Result<RoaPayloadState, CcrBuildError> {
|
||||||
|
let mut grouped: BTreeMap<u32, BTreeSet<RoaPayloadKey>> = BTreeMap::new();
|
||||||
|
for vrp in vrps {
|
||||||
|
grouped
|
||||||
|
.entry(vrp.asn)
|
||||||
|
.or_default()
|
||||||
|
.insert(RoaPayloadKey::from_vrp(vrp));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rps = Vec::with_capacity(grouped.len());
|
||||||
|
for (asn, entries) in grouped {
|
||||||
|
let mut families: BTreeMap<u16, Vec<RoaPayloadKey>> = BTreeMap::new();
|
||||||
|
for entry in entries {
|
||||||
|
families.entry(entry.afi).or_default().push(entry);
|
||||||
|
}
|
||||||
|
let mut ip_addr_blocks = Vec::with_capacity(families.len());
|
||||||
|
for (afi, entries) in families {
|
||||||
|
ip_addr_blocks.push(encode_roa_ip_address_family(afi, &entries));
|
||||||
|
}
|
||||||
|
rps.push(RoaPayloadSet {
|
||||||
|
as_id: asn,
|
||||||
|
ip_addr_blocks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_der = encode_roa_payload_state_payload_der(&rps)
|
||||||
|
.map_err(|e| CcrBuildError::RoaEncode(e.to_string()))?;
|
||||||
|
Ok(RoaPayloadState {
|
||||||
|
rps,
|
||||||
|
hash: compute_state_hash(&payload_der),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_aspa_payload_state(
|
||||||
|
attestations: &[AspaAttestation],
|
||||||
|
) -> Result<AspaPayloadState, CcrBuildError> {
|
||||||
|
let mut grouped: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
|
||||||
|
for attestation in attestations {
|
||||||
|
grouped
|
||||||
|
.entry(attestation.customer_as_id)
|
||||||
|
.or_default()
|
||||||
|
.extend(attestation.provider_as_ids.iter().copied());
|
||||||
|
}
|
||||||
|
|
||||||
|
let aps: Vec<AspaPayloadSet> = grouped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(customer_as_id, providers)| AspaPayloadSet {
|
||||||
|
customer_as_id,
|
||||||
|
providers: providers.into_iter().collect(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let payload_der = encode_aspa_payload_state_payload_der(&aps)
|
||||||
|
.map_err(|e| CcrBuildError::AspaEncode(e.to_string()))?;
|
||||||
|
Ok(AspaPayloadState {
|
||||||
|
aps,
|
||||||
|
hash: compute_state_hash(&payload_der),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_trust_anchor_state(
|
||||||
|
trust_anchors: &[TrustAnchor],
|
||||||
|
) -> Result<TrustAnchorState, CcrBuildError> {
|
||||||
|
if trust_anchors.is_empty() {
|
||||||
|
return Err(CcrBuildError::EmptyTrustAnchors);
|
||||||
|
}
|
||||||
|
let mut skis: BTreeSet<Vec<u8>> = BTreeSet::new();
|
||||||
|
for ta in trust_anchors {
|
||||||
|
let ski = ta
|
||||||
|
.ta_certificate
|
||||||
|
.rc_ca
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_key_identifier
|
||||||
|
.clone()
|
||||||
|
.ok_or(CcrBuildError::MissingTrustAnchorSki)?;
|
||||||
|
skis.insert(ski);
|
||||||
|
}
|
||||||
|
let skis: Vec<Vec<u8>> = skis.into_iter().collect();
|
||||||
|
let payload_der = encode_trust_anchor_state_payload_der(&skis)
|
||||||
|
.map_err(|e| CcrBuildError::TrustAnchorEncode(e.to_string()))?;
|
||||||
|
Ok(TrustAnchorState {
|
||||||
|
skis,
|
||||||
|
hash: compute_state_hash(&payload_der),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_manifest_state_from_vcirs(
|
||||||
|
store: &RocksStore,
|
||||||
|
vcirs: &[ValidatedCaInstanceResult],
|
||||||
|
) -> Result<ManifestState, CcrBuildError> {
|
||||||
|
let mut mis_by_hash: BTreeMap<Vec<u8>, ManifestInstance> = BTreeMap::new();
|
||||||
|
let mut most_recent_update = time::OffsetDateTime::UNIX_EPOCH;
|
||||||
|
|
||||||
|
for vcir in vcirs {
|
||||||
|
let manifest_artifact = vcir
|
||||||
|
.related_artifacts
|
||||||
|
.iter()
|
||||||
|
.find(|artifact| {
|
||||||
|
artifact.artifact_role == VcirArtifactRole::Manifest
|
||||||
|
&& artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CcrBuildError::MissingManifestArtifact(vcir.current_manifest_rsync_uri.clone())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let raw_bytes = store
|
||||||
|
.get_blob_bytes(&manifest_artifact.sha256)
|
||||||
|
.map_err(|e| CcrBuildError::LoadManifestRawBytes {
|
||||||
|
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| CcrBuildError::MissingManifestRawBytes {
|
||||||
|
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
sha256_hex: manifest_artifact.sha256.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let manifest = ManifestObject::decode_der(&raw_bytes).map_err(|e| {
|
||||||
|
CcrBuildError::ManifestDecode {
|
||||||
|
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
|
||||||
|
let aki = ee
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.authority_key_identifier
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CcrBuildError::ManifestEeMissingAki(vcir.current_manifest_rsync_uri.clone())
|
||||||
|
})?;
|
||||||
|
let sia = ee
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_info_access
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CcrBuildError::ManifestEeMissingSia(vcir.current_manifest_rsync_uri.clone())
|
||||||
|
})?;
|
||||||
|
let locations = match sia {
|
||||||
|
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
||||||
|
.access_descriptions
|
||||||
|
.iter()
|
||||||
|
.map(encode_access_description_der)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
SubjectInfoAccess::Ca(_) => {
|
||||||
|
return Err(CcrBuildError::ManifestEeSiaWrongVariant(
|
||||||
|
vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let subordinates = collect_subordinate_ski_bytes(vcir)?;
|
||||||
|
let this_update = vcir
|
||||||
|
.validated_manifest_meta
|
||||||
|
.validated_manifest_this_update
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| CcrBuildError::ManifestDecode {
|
||||||
|
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
detail: format!("invalid validated_manifest_this_update: {e}"),
|
||||||
|
})?;
|
||||||
|
if this_update > most_recent_update {
|
||||||
|
most_recent_update = this_update;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance = ManifestInstance {
|
||||||
|
hash: sha2::Sha256::digest(&raw_bytes).to_vec(),
|
||||||
|
size: raw_bytes.len() as u64,
|
||||||
|
aki,
|
||||||
|
manifest_number: crate::data_model::common::BigUnsigned {
|
||||||
|
bytes_be: vcir
|
||||||
|
.validated_manifest_meta
|
||||||
|
.validated_manifest_number
|
||||||
|
.clone(),
|
||||||
|
},
|
||||||
|
this_update,
|
||||||
|
locations,
|
||||||
|
subordinates,
|
||||||
|
};
|
||||||
|
|
||||||
|
match mis_by_hash.get(instance.hash.as_slice()) {
|
||||||
|
Some(existing) if existing != &instance => {
|
||||||
|
return Err(CcrBuildError::DuplicateManifestHashConflict(
|
||||||
|
vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
mis_by_hash.insert(instance.hash.clone(), instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mis: Vec<ManifestInstance> = mis_by_hash.into_values().collect();
|
||||||
|
let payload_der = encode_manifest_state_payload_der(&mis)
|
||||||
|
.map_err(|e| CcrBuildError::ManifestEncode(e.to_string()))?;
|
||||||
|
Ok(ManifestState {
|
||||||
|
mis,
|
||||||
|
most_recent_update,
|
||||||
|
hash: compute_state_hash(&payload_der),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_subordinate_ski_bytes(
|
||||||
|
vcir: &ValidatedCaInstanceResult,
|
||||||
|
) -> Result<Vec<Vec<u8>>, CcrBuildError> {
|
||||||
|
let mut skis = BTreeSet::new();
|
||||||
|
for child in &vcir.child_entries {
|
||||||
|
let bytes = hex::decode(&child.child_ski)
|
||||||
|
.map_err(|_| CcrBuildError::InvalidChildSki(child.child_ski.clone()))?;
|
||||||
|
if bytes.len() != 20 {
|
||||||
|
return Err(CcrBuildError::InvalidChildSki(child.child_ski.clone()));
|
||||||
|
}
|
||||||
|
skis.insert(bytes);
|
||||||
|
}
|
||||||
|
Ok(skis.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_access_description_der(ad: &AccessDescription) -> Result<Vec<u8>, CcrBuildError> {
|
||||||
|
let oid = encode_oid_from_string(&ad.access_method_oid)?;
|
||||||
|
let uri = encode_tlv(0x86, ad.access_location.as_bytes().to_vec());
|
||||||
|
Ok(encode_sequence(&[oid, uri]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_oid_from_string(oid: &str) -> Result<Vec<u8>, CcrBuildError> {
|
||||||
|
let arcs = oid
|
||||||
|
.split('.')
|
||||||
|
.map(|part| {
|
||||||
|
part.parse::<u64>()
|
||||||
|
.map_err(|_| CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
if arcs.len() < 2 {
|
||||||
|
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
|
||||||
|
}
|
||||||
|
if arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
|
||||||
|
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
|
||||||
|
}
|
||||||
|
let mut body = Vec::new();
|
||||||
|
body.push((arcs[0] * 40 + arcs[1]) as u8);
|
||||||
|
for arc in &arcs[2..] {
|
||||||
|
encode_base128(*arc, &mut body);
|
||||||
|
}
|
||||||
|
Ok(encode_tlv(0x06, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_base128(mut value: u64, out: &mut Vec<u8>) {
|
||||||
|
let mut tmp = vec![(value & 0x7F) as u8];
|
||||||
|
value >>= 7;
|
||||||
|
while value > 0 {
|
||||||
|
tmp.push(((value & 0x7F) as u8) | 0x80);
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
tmp.reverse();
|
||||||
|
out.extend_from_slice(&tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_router_key_state(
|
||||||
|
router_certs: &[BgpsecRouterCertificate],
|
||||||
|
) -> Result<RouterKeyState, CcrBuildError> {
|
||||||
|
let mut grouped: BTreeMap<u32, BTreeSet<RouterKey>> = BTreeMap::new();
|
||||||
|
for cert in router_certs {
|
||||||
|
let key = RouterKey {
|
||||||
|
ski: cert.subject_key_identifier.clone(),
|
||||||
|
spki_der: cert.spki_der.clone(),
|
||||||
|
};
|
||||||
|
for asn in &cert.asns {
|
||||||
|
grouped.entry(*asn).or_default().insert(key.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rksets: Vec<RouterKeySet> = grouped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(as_id, router_keys)| RouterKeySet {
|
||||||
|
as_id,
|
||||||
|
router_keys: router_keys.into_iter().collect(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let payload_der = encode_router_key_state_payload_der(&rksets)
|
||||||
|
.map_err(|e| CcrBuildError::RouterKeyEncode(e.to_string()))?;
|
||||||
|
Ok(RouterKeyState {
|
||||||
|
rksets,
|
||||||
|
hash: compute_state_hash(&payload_der),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct RoaPayloadKey {
|
||||||
|
afi: u16,
|
||||||
|
addr: Vec<u8>,
|
||||||
|
prefix_len: u8,
|
||||||
|
max_length: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoaPayloadKey {
|
||||||
|
fn from_vrp(vrp: &Vrp) -> Self {
|
||||||
|
let afi = match vrp.prefix.afi {
|
||||||
|
RoaAfi::Ipv4 => 1,
|
||||||
|
RoaAfi::Ipv6 => 2,
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
afi,
|
||||||
|
addr: vrp.prefix.addr.to_vec(),
|
||||||
|
prefix_len: vrp.prefix.prefix_len as u8,
|
||||||
|
max_length: vrp.max_length as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_roa_ip_address_family(afi: u16, entries: &[RoaPayloadKey]) -> Vec<u8> {
|
||||||
|
let address_family = afi.to_be_bytes();
|
||||||
|
let addresses = entries
|
||||||
|
.iter()
|
||||||
|
.map(encode_roa_ip_address)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
encode_sequence(&[
|
||||||
|
encode_octet_string(&address_family),
|
||||||
|
encode_sequence(&addresses),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_roa_ip_address(entry: &RoaPayloadKey) -> Vec<u8> {
|
||||||
|
let (unused_bits, content) = encode_prefix_bit_string(&entry.addr, entry.prefix_len);
|
||||||
|
let mut fields = vec![encode_bit_string(unused_bits, &content)];
|
||||||
|
if entry.max_length != entry.prefix_len {
|
||||||
|
fields.push(encode_integer_u8(entry.max_length));
|
||||||
|
}
|
||||||
|
encode_sequence(&fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_prefix_bit_string(addr: &[u8], prefix_len: u8) -> (u8, Vec<u8>) {
|
||||||
|
if prefix_len == 0 {
|
||||||
|
return (0, Vec::new());
|
||||||
|
}
|
||||||
|
let octets = ((prefix_len as usize) + 7) / 8;
|
||||||
|
let mut content = addr[..octets].to_vec();
|
||||||
|
let rem = prefix_len % 8;
|
||||||
|
let unused = if rem == 0 { 0 } else { 8 - rem };
|
||||||
|
if unused > 0 {
|
||||||
|
let mask = 0xFFu8 << unused;
|
||||||
|
let last = content.last_mut().expect("octets > 0");
|
||||||
|
*last &= mask;
|
||||||
|
}
|
||||||
|
(unused, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_u8(v: u8) -> Vec<u8> {
|
||||||
|
encode_integer_bytes(vec![v])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_bytes(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
bytes.push(0);
|
||||||
|
}
|
||||||
|
if bytes[0] & 0x80 != 0 {
|
||||||
|
bytes.insert(0, 0);
|
||||||
|
}
|
||||||
|
encode_tlv(0x02, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_bit_string(unused_bits: u8, content: &[u8]) -> Vec<u8> {
|
||||||
|
let mut value = Vec::with_capacity(content.len() + 1);
|
||||||
|
value.push(unused_bits);
|
||||||
|
value.extend_from_slice(content);
|
||||||
|
encode_tlv(0x03, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0x04, bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let total_len: usize = elements.iter().map(Vec::len).sum();
|
||||||
|
let mut buf = Vec::with_capacity(total_len);
|
||||||
|
for element in elements {
|
||||||
|
buf.extend_from_slice(element);
|
||||||
|
}
|
||||||
|
encode_tlv(0x30, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(1 + 9 + value.len());
|
||||||
|
out.push(tag);
|
||||||
|
encode_length(value.len(), &mut out);
|
||||||
|
out.extend_from_slice(&value);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_length(len: usize, out: &mut Vec<u8>) {
|
||||||
|
if len < 0x80 {
|
||||||
|
out.push(len as u8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
let mut value = len;
|
||||||
|
while value > 0 {
|
||||||
|
bytes.push((value & 0xFF) as u8);
|
||||||
|
value >>= 8;
|
||||||
|
}
|
||||||
|
bytes.reverse();
|
||||||
|
out.push(0x80 | (bytes.len() as u8));
|
||||||
|
out.extend_from_slice(&bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ccr::decode::decode_content_info;
|
||||||
|
use crate::ccr::encode::{
|
||||||
|
encode_aspa_payload_state, encode_content_info, encode_trust_anchor_state,
|
||||||
|
};
|
||||||
|
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
|
||||||
|
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||||
|
use crate::data_model::ta::TrustAnchor;
|
||||||
|
use crate::data_model::tal::Tal;
|
||||||
|
|
||||||
|
fn sample_vrp_v4(asn: u32, a: [u8; 4], prefix_len: u16, max_length: u16) -> Vrp {
|
||||||
|
Vrp {
|
||||||
|
asn,
|
||||||
|
prefix: IpPrefix {
|
||||||
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len,
|
||||||
|
addr: [a[0], a[1], a[2], a[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
},
|
||||||
|
max_length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_vrp_v6(asn: u32, a: [u8; 16], prefix_len: u16, max_length: u16) -> Vrp {
|
||||||
|
Vrp {
|
||||||
|
asn,
|
||||||
|
prefix: IpPrefix {
|
||||||
|
afi: RoaAfi::Ipv6,
|
||||||
|
prefix_len,
|
||||||
|
addr: a,
|
||||||
|
},
|
||||||
|
max_length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_family_block(block: &[u8]) -> (u16, Vec<(u8, Vec<u8>, Option<u8>)>) {
|
||||||
|
let mut top = crate::data_model::common::DerReader::new(block);
|
||||||
|
let mut seq = top.take_sequence().expect("family seq");
|
||||||
|
assert!(top.is_empty());
|
||||||
|
let afi_bytes = seq.take_octet_string().expect("afi octet string");
|
||||||
|
let afi = u16::from_be_bytes([afi_bytes[0], afi_bytes[1]]);
|
||||||
|
let mut addrs = seq.take_sequence().expect("addresses seq");
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
while !addrs.is_empty() {
|
||||||
|
let mut addr_seq = addrs.take_sequence().expect("roa ip addr seq");
|
||||||
|
let (unused_bits, content) = addr_seq.take_bit_string().expect("bit string");
|
||||||
|
let prefix_len = (content.len() * 8) as u8 - unused_bits;
|
||||||
|
let max_len = if addr_seq.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(addr_seq.take_uint_u64().expect("maxlen") as u8)
|
||||||
|
};
|
||||||
|
entries.push((prefix_len, content.to_vec(), max_len));
|
||||||
|
}
|
||||||
|
(afi, entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_trust_anchor(path_tal: &str, path_ta: &str) -> TrustAnchor {
|
||||||
|
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let tal_bytes = std::fs::read(base.join(path_tal)).expect("read tal");
|
||||||
|
let ta_der = std::fs::read(base.join(path_ta)).expect("read ta");
|
||||||
|
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal");
|
||||||
|
TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_router_cert(asns: Vec<u32>, ski_fill: u8, spki_fill: u8) -> BgpsecRouterCertificate {
|
||||||
|
let ta = sample_trust_anchor(
|
||||||
|
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||||
|
"tests/fixtures/ta/apnic-ta.cer",
|
||||||
|
);
|
||||||
|
BgpsecRouterCertificate {
|
||||||
|
raw_der: vec![0x01, 0x02],
|
||||||
|
resource_cert: ta.ta_certificate.rc_ca,
|
||||||
|
subject_key_identifier: vec![ski_fill; 20],
|
||||||
|
spki_der: vec![0x30, 0x03, 0x03, 0x01, spki_fill],
|
||||||
|
asns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_manifest_vcir(
|
||||||
|
manifest_uri: &str,
|
||||||
|
manifest_der: &[u8],
|
||||||
|
child_ski_hex: &str,
|
||||||
|
) -> ValidatedCaInstanceResult {
|
||||||
|
let manifest = ManifestObject::decode_der(manifest_der).expect("decode manifest fixture");
|
||||||
|
let hash = hex::encode(sha2::Sha256::digest(manifest_der));
|
||||||
|
let now = manifest.manifest.this_update;
|
||||||
|
let next = manifest.manifest.next_update;
|
||||||
|
crate::storage::ValidatedCaInstanceResult {
|
||||||
|
manifest_rsync_uri: manifest_uri.to_string(),
|
||||||
|
parent_manifest_rsync_uri: None,
|
||||||
|
tal_id: "test-tal".to_string(),
|
||||||
|
ca_subject_name: "CN=test".to_string(),
|
||||||
|
ca_ski: "11".repeat(20),
|
||||||
|
issuer_ski: "22".repeat(20),
|
||||||
|
last_successful_validation_time: crate::storage::PackTime::from_utc_offset_datetime(
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
current_manifest_rsync_uri: manifest_uri.to_string(),
|
||||||
|
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
|
||||||
|
validated_manifest_meta: crate::storage::ValidatedManifestMeta {
|
||||||
|
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||||
|
validated_manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
validated_manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(
|
||||||
|
next,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
instance_gate: crate::storage::VcirInstanceGate {
|
||||||
|
manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
|
||||||
|
current_crl_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
|
||||||
|
self_ca_not_after: crate::storage::PackTime::from_utc_offset_datetime(next),
|
||||||
|
instance_effective_until: crate::storage::PackTime::from_utc_offset_datetime(next),
|
||||||
|
},
|
||||||
|
child_entries: vec![crate::storage::VcirChildEntry {
|
||||||
|
child_manifest_rsync_uri: format!("{manifest_uri}/child.mft"),
|
||||||
|
child_cert_rsync_uri: format!("{manifest_uri}/child.cer"),
|
||||||
|
child_cert_hash: "aa".repeat(32),
|
||||||
|
child_ski: child_ski_hex.to_string(),
|
||||||
|
child_rsync_base_uri: format!("{manifest_uri}/"),
|
||||||
|
child_publication_point_rsync_uri: format!("{manifest_uri}/"),
|
||||||
|
child_rrdp_notification_uri: None,
|
||||||
|
child_effective_ip_resources: None,
|
||||||
|
child_effective_as_resources: None,
|
||||||
|
accepted_at_validation_time: crate::storage::PackTime::from_utc_offset_datetime(
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
local_outputs: Vec::new(),
|
||||||
|
related_artifacts: vec![crate::storage::VcirRelatedArtifact {
|
||||||
|
artifact_role: crate::storage::VcirArtifactRole::Manifest,
|
||||||
|
artifact_kind: crate::storage::VcirArtifactKind::Mft,
|
||||||
|
uri: Some(manifest_uri.to_string()),
|
||||||
|
sha256: hash,
|
||||||
|
object_type: Some("mft".to_string()),
|
||||||
|
validation_status: crate::storage::VcirArtifactValidationStatus::Accepted,
|
||||||
|
}],
|
||||||
|
summary: crate::storage::VcirSummary {
|
||||||
|
local_vrp_count: 0,
|
||||||
|
local_aspa_count: 0,
|
||||||
|
local_router_key_count: 0,
|
||||||
|
child_count: 1,
|
||||||
|
accepted_object_count: 1,
|
||||||
|
rejected_object_count: 0,
|
||||||
|
},
|
||||||
|
audit_summary: crate::storage::VcirAuditSummary {
|
||||||
|
failed_fetch_eligible: true,
|
||||||
|
last_failed_fetch_reason: None,
|
||||||
|
warning_count: 0,
|
||||||
|
audit_flags: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_manifest_state_from_vcirs_collects_current_manifests_and_hashes_payload() {
|
||||||
|
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let manifest_a = std::fs::read(base.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft")).expect("read manifest a");
|
||||||
|
let manifest_b = std::fs::read(base.join(
|
||||||
|
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
|
||||||
|
))
|
||||||
|
.expect("read manifest b");
|
||||||
|
let vcir_a =
|
||||||
|
sample_manifest_vcir("rsync://example.test/a.mft", &manifest_a, &"33".repeat(20));
|
||||||
|
let vcir_b =
|
||||||
|
sample_manifest_vcir("rsync://example.test/b.mft", &manifest_b, &"44".repeat(20));
|
||||||
|
let store_dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||||
|
for (vcir, bytes) in [(&vcir_a, &manifest_a), (&vcir_b, &manifest_b)] {
|
||||||
|
let artifact = &vcir.related_artifacts[0];
|
||||||
|
let mut raw =
|
||||||
|
crate::storage::RawByHashEntry::from_bytes(artifact.sha256.clone(), bytes.to_vec());
|
||||||
|
raw.origin_uris
|
||||||
|
.push(vcir.current_manifest_rsync_uri.clone());
|
||||||
|
raw.object_type = Some("mft".to_string());
|
||||||
|
raw.encoding = Some("der".to_string());
|
||||||
|
store.put_raw_by_hash_entry(&raw).expect("put raw manifest");
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = build_manifest_state_from_vcirs(&store, &[vcir_a.clone(), vcir_b.clone()])
|
||||||
|
.expect("build manifest state");
|
||||||
|
assert_eq!(state.mis.len(), 2);
|
||||||
|
assert!(state.mis[0].hash < state.mis[1].hash);
|
||||||
|
let expected_subordinates = [
|
||||||
|
hex::decode(vcir_a.child_entries[0].child_ski.clone()).unwrap(),
|
||||||
|
hex::decode(vcir_b.child_entries[0].child_ski.clone()).unwrap(),
|
||||||
|
];
|
||||||
|
let actual_subordinates = state
|
||||||
|
.mis
|
||||||
|
.iter()
|
||||||
|
.map(|mi| {
|
||||||
|
assert_eq!(mi.subordinates.len(), 1);
|
||||||
|
assert!(!mi.locations.is_empty());
|
||||||
|
mi.subordinates[0].clone()
|
||||||
|
})
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
let expected_subordinates = expected_subordinates
|
||||||
|
.into_iter()
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(actual_subordinates, expected_subordinates);
|
||||||
|
let payload_der =
|
||||||
|
encode_manifest_state_payload_der(&state.mis).expect("encode mis payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
let max_time = [
|
||||||
|
vcir_a
|
||||||
|
.validated_manifest_meta
|
||||||
|
.validated_manifest_this_update
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
vcir_b
|
||||||
|
.validated_manifest_meta
|
||||||
|
.validated_manifest_this_update
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.max()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(state.most_recent_update, max_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_manifest_state_from_vcirs_empty_uses_epoch() {
|
||||||
|
let store_dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||||
|
let state = build_manifest_state_from_vcirs(&store, &[]).expect("empty manifest state");
|
||||||
|
assert!(state.mis.is_empty());
|
||||||
|
assert_eq!(state.most_recent_update, time::OffsetDateTime::UNIX_EPOCH);
|
||||||
|
let payload_der =
|
||||||
|
encode_manifest_state_payload_der(&state.mis).expect("encode mis payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_router_key_state_groups_dedupes_and_sorts() {
|
||||||
|
let certs = vec![
|
||||||
|
sample_router_cert(vec![64497], 0x21, 0xA1),
|
||||||
|
sample_router_cert(vec![64496, 64497], 0x11, 0xA0),
|
||||||
|
sample_router_cert(vec![64496], 0x11, 0xA0),
|
||||||
|
sample_router_cert(vec![64496], 0x11, 0xA2),
|
||||||
|
];
|
||||||
|
let state = build_router_key_state(&certs).expect("build router key state");
|
||||||
|
assert_eq!(state.rksets.len(), 2);
|
||||||
|
assert_eq!(state.rksets[0].as_id, 64496);
|
||||||
|
assert_eq!(state.rksets[1].as_id, 64497);
|
||||||
|
assert_eq!(state.rksets[0].router_keys.len(), 2);
|
||||||
|
assert_eq!(state.rksets[1].router_keys.len(), 2);
|
||||||
|
assert!(state.rksets[0].router_keys[0].ski <= state.rksets[0].router_keys[1].ski);
|
||||||
|
let payload_der =
|
||||||
|
encode_router_key_state_payload_der(&state.rksets).expect("encode rk payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_router_key_state_empty_is_valid_and_hashes_empty_sequence() {
|
||||||
|
let state = build_router_key_state(&[]).expect("empty router key state");
|
||||||
|
assert!(state.rksets.is_empty());
|
||||||
|
let payload_der =
|
||||||
|
encode_router_key_state_payload_der(&state.rksets).expect("encode rk payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_roa_payload_state_groups_dedupes_and_sorts() {
|
||||||
|
let vrps = vec![
|
||||||
|
sample_vrp_v4(64497, [10, 1, 0, 0], 16, 16),
|
||||||
|
sample_vrp_v4(64496, [10, 0, 0, 0], 8, 8),
|
||||||
|
sample_vrp_v4(64496, [10, 0, 0, 0], 8, 8),
|
||||||
|
sample_vrp_v4(64496, [10, 1, 0, 0], 16, 24),
|
||||||
|
sample_vrp_v6(
|
||||||
|
64496,
|
||||||
|
[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
32,
|
||||||
|
48,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let state = build_roa_payload_state(&vrps).expect("build roa state");
|
||||||
|
assert_eq!(state.rps.len(), 2);
|
||||||
|
assert_eq!(state.rps[0].as_id, 64496);
|
||||||
|
assert_eq!(state.rps[1].as_id, 64497);
|
||||||
|
assert_eq!(state.rps[0].ip_addr_blocks.len(), 2);
|
||||||
|
let (afi4, entries4) = decode_family_block(&state.rps[0].ip_addr_blocks[0]);
|
||||||
|
let (afi6, entries6) = decode_family_block(&state.rps[0].ip_addr_blocks[1]);
|
||||||
|
assert_eq!(afi4, 1);
|
||||||
|
assert_eq!(afi6, 2);
|
||||||
|
assert_eq!(entries4.len(), 2);
|
||||||
|
assert_eq!(entries4[0], (8, vec![10], None));
|
||||||
|
assert_eq!(entries4[1], (16, vec![10, 1], Some(24)));
|
||||||
|
assert_eq!(entries6.len(), 1);
|
||||||
|
assert_eq!(entries6[0], (32, vec![0x20, 0x01, 0x0d, 0xb8], Some(48)));
|
||||||
|
let payload_der = encode_roa_payload_state_payload_der(&state.rps).expect("encode payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_roa_payload_state_empty_is_valid_and_hashes_empty_sequence() {
|
||||||
|
let state = build_roa_payload_state(&[]).expect("empty roa state");
|
||||||
|
assert!(state.rps.is_empty());
|
||||||
|
let payload_der = encode_roa_payload_state_payload_der(&state.rps).expect("encode payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_aspa_payload_state_merges_and_sorts() {
|
||||||
|
let aspas = vec![
|
||||||
|
AspaAttestation {
|
||||||
|
customer_as_id: 64497,
|
||||||
|
provider_as_ids: vec![65002],
|
||||||
|
},
|
||||||
|
AspaAttestation {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![65003, 65001],
|
||||||
|
},
|
||||||
|
AspaAttestation {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![65002, 65001],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let state = build_aspa_payload_state(&aspas).expect("build aspa state");
|
||||||
|
assert_eq!(state.aps.len(), 2);
|
||||||
|
assert_eq!(state.aps[0].customer_as_id, 64496);
|
||||||
|
assert_eq!(state.aps[0].providers, vec![65001, 65002, 65003]);
|
||||||
|
assert_eq!(state.aps[1].customer_as_id, 64497);
|
||||||
|
let encoded = encode_aspa_payload_state(&state).expect("encode aspa state");
|
||||||
|
let decoded = decode_content_info(
|
||||||
|
&encode_content_info(&CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||||
|
version: 0,
|
||||||
|
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||||
|
produced_at: time::OffsetDateTime::now_utc(),
|
||||||
|
mfts: None,
|
||||||
|
vrps: None,
|
||||||
|
vaps: Some(state.clone()),
|
||||||
|
tas: None,
|
||||||
|
rks: None,
|
||||||
|
}))
|
||||||
|
.expect("encode ccr"),
|
||||||
|
)
|
||||||
|
.expect("decode ccr");
|
||||||
|
assert_eq!(decoded.content.vaps, Some(state));
|
||||||
|
assert!(!encoded.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_trust_anchor_state_collects_sorted_unique_skis() {
|
||||||
|
let apnic = sample_trust_anchor(
|
||||||
|
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||||
|
"tests/fixtures/ta/apnic-ta.cer",
|
||||||
|
);
|
||||||
|
let arin = sample_trust_anchor(
|
||||||
|
"tests/fixtures/tal/arin.tal",
|
||||||
|
"tests/fixtures/ta/arin-ta.cer",
|
||||||
|
);
|
||||||
|
let state = build_trust_anchor_state(&[apnic.clone(), arin.clone(), apnic])
|
||||||
|
.expect("build ta state");
|
||||||
|
assert_eq!(state.skis.len(), 2);
|
||||||
|
assert!(state.skis[0] < state.skis[1]);
|
||||||
|
let payload_der =
|
||||||
|
encode_trust_anchor_state_payload_der(&state.skis).expect("encode ta payload");
|
||||||
|
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
|
||||||
|
let encoded = encode_trust_anchor_state(&state).expect("encode ta state");
|
||||||
|
assert!(!encoded.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_trust_anchor_state_rejects_empty_and_missing_ski() {
|
||||||
|
let err = build_trust_anchor_state(&[]).expect_err("empty TAs must fail");
|
||||||
|
assert!(err.to_string().contains("trust anchor set"), "{err}");
|
||||||
|
|
||||||
|
let mut ta = sample_trust_anchor(
|
||||||
|
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||||
|
"tests/fixtures/ta/apnic-ta.cer",
|
||||||
|
);
|
||||||
|
ta.ta_certificate
|
||||||
|
.rc_ca
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_key_identifier = None;
|
||||||
|
let err = build_trust_anchor_state(&[ta]).expect_err("missing ski must fail");
|
||||||
|
assert!(err.to_string().contains("SubjectKeyIdentifier"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
473
src/ccr/decode.rs
Normal file
473
src/ccr/decode.rs
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
use crate::ccr::model::{
|
||||||
|
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
|
||||||
|
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
|
||||||
|
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
|
||||||
|
};
|
||||||
|
use crate::data_model::common::{BigUnsigned, DerReader};
|
||||||
|
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_CT_RPKI_CCR_RAW, OID_SHA256, OID_SHA256_RAW};
|
||||||
|
use der_parser::der::parse_der_oid;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcrDecodeError {
|
||||||
|
#[error("DER parse error: {0}")]
|
||||||
|
Parse(String),
|
||||||
|
|
||||||
|
#[error("unexpected contentType OID: expected {expected}, got {actual}")]
|
||||||
|
UnexpectedContentType {
|
||||||
|
expected: &'static str,
|
||||||
|
actual: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("unexpected digest algorithm OID: expected {expected}, got {actual}")]
|
||||||
|
UnexpectedDigestAlgorithm {
|
||||||
|
expected: &'static str,
|
||||||
|
actual: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("CCR model validation failed after decode: {0}")]
|
||||||
|
Validate(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_content_info(der: &[u8]) -> Result<CcrContentInfo, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ContentInfo".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let content_type_raw = seq.take_tag(0x06).map_err(CcrDecodeError::Parse)?;
|
||||||
|
if content_type_raw != OID_CT_RPKI_CCR_RAW {
|
||||||
|
return Err(CcrDecodeError::UnexpectedContentType {
|
||||||
|
expected: OID_CT_RPKI_CCR,
|
||||||
|
actual: oid_string(content_type_raw)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let inner = seq.take_tag(0xA0).map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing fields in ContentInfo".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let content = decode_ccr(inner)?;
|
||||||
|
let ci = CcrContentInfo::new(content);
|
||||||
|
ci.validate().map_err(CcrDecodeError::Validate)?;
|
||||||
|
Ok(ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_ccr(der: &[u8]) -> Result<RpkiCanonicalCacheRepresentation, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse("trailing bytes after CCR".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = if !seq.is_empty() && seq.peek_tag().map_err(CcrDecodeError::Parse)? == 0xA0 {
|
||||||
|
let explicit = seq.take_tag(0xA0).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut inner = DerReader::new(explicit);
|
||||||
|
let version = inner.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
|
||||||
|
if !inner.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes inside CCR version EXPLICIT".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
version
|
||||||
|
} else {
|
||||||
|
CCR_VERSION_V0
|
||||||
|
};
|
||||||
|
|
||||||
|
let hash_alg = decode_digest_algorithm(seq.take_sequence().map_err(CcrDecodeError::Parse)?)?;
|
||||||
|
let produced_at = parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
|
||||||
|
|
||||||
|
let mut mfts = None;
|
||||||
|
let mut vrps = None;
|
||||||
|
let mut vaps = None;
|
||||||
|
let mut tas = None;
|
||||||
|
let mut rks = None;
|
||||||
|
while !seq.is_empty() {
|
||||||
|
let tag = seq.peek_tag().map_err(CcrDecodeError::Parse)?;
|
||||||
|
let (tag_read, value) = seq.take_any().map_err(CcrDecodeError::Parse)?;
|
||||||
|
debug_assert_eq!(tag, tag_read);
|
||||||
|
match tag {
|
||||||
|
0xA1 => mfts = Some(decode_manifest_state(value)?),
|
||||||
|
0xA2 => vrps = Some(decode_roa_payload_state(value)?),
|
||||||
|
0xA3 => vaps = Some(decode_aspa_payload_state(value)?),
|
||||||
|
0xA4 => tas = Some(decode_trust_anchor_state(value)?),
|
||||||
|
0xA5 => rks = Some(decode_router_key_state(value)?),
|
||||||
|
_ => {
|
||||||
|
return Err(CcrDecodeError::Parse(format!(
|
||||||
|
"unexpected CCR field tag 0x{tag:02X}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ccr = RpkiCanonicalCacheRepresentation {
|
||||||
|
version,
|
||||||
|
hash_alg,
|
||||||
|
produced_at,
|
||||||
|
mfts,
|
||||||
|
vrps,
|
||||||
|
vaps,
|
||||||
|
tas,
|
||||||
|
rks,
|
||||||
|
};
|
||||||
|
ccr.validate().map_err(CcrDecodeError::Validate)?;
|
||||||
|
Ok(ccr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_manifest_state(explicit_der: &[u8]) -> Result<ManifestState, CcrDecodeError> {
|
||||||
|
let mut outer = DerReader::new(explicit_der);
|
||||||
|
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !outer.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ManifestState".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mis_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut mis_reader = DerReader::new(mis_der);
|
||||||
|
let mut mis = Vec::new();
|
||||||
|
while !mis_reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = mis_reader.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
mis.push(decode_manifest_instance(full)?);
|
||||||
|
}
|
||||||
|
let most_recent_update =
|
||||||
|
parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
|
||||||
|
let hash = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing fields in ManifestState".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(ManifestState {
|
||||||
|
mis,
|
||||||
|
most_recent_update,
|
||||||
|
hash,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_manifest_instance(der: &[u8]) -> Result<ManifestInstance, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ManifestInstance".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let hash = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
let size = seq.take_uint_u64().map_err(CcrDecodeError::Parse)?;
|
||||||
|
let aki = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
let manifest_number = decode_big_unsigned(seq.take_tag(0x02).map_err(CcrDecodeError::Parse)?)?;
|
||||||
|
let this_update = parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
|
||||||
|
let locations_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut locations_reader = DerReader::new(locations_der);
|
||||||
|
let mut locations = Vec::new();
|
||||||
|
while !locations_reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = locations_reader
|
||||||
|
.take_any_full()
|
||||||
|
.map_err(CcrDecodeError::Parse)?;
|
||||||
|
locations.push(full.to_vec());
|
||||||
|
}
|
||||||
|
let subordinates = if !seq.is_empty() {
|
||||||
|
let subordinate_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(subordinate_der);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
out.push(
|
||||||
|
reader
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
Ok(ManifestInstance {
|
||||||
|
hash,
|
||||||
|
size,
|
||||||
|
aki,
|
||||||
|
manifest_number,
|
||||||
|
this_update,
|
||||||
|
locations,
|
||||||
|
subordinates,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_roa_payload_state(explicit_der: &[u8]) -> Result<RoaPayloadState, CcrDecodeError> {
|
||||||
|
let mut outer = DerReader::new(explicit_der);
|
||||||
|
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !outer.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ROAPayloadState".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let payload_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(payload_der);
|
||||||
|
let mut rps = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
rps.push(decode_roa_payload_set(full)?);
|
||||||
|
}
|
||||||
|
let hash = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
Ok(RoaPayloadState { rps, hash })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_roa_payload_set(der: &[u8]) -> Result<RoaPayloadSet, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ROAPayloadSet".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
|
||||||
|
let blocks_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(blocks_der);
|
||||||
|
let mut ip_addr_blocks = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
ip_addr_blocks.push(full.to_vec());
|
||||||
|
}
|
||||||
|
Ok(RoaPayloadSet {
|
||||||
|
as_id,
|
||||||
|
ip_addr_blocks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_aspa_payload_state(explicit_der: &[u8]) -> Result<AspaPayloadState, CcrDecodeError> {
|
||||||
|
let mut outer = DerReader::new(explicit_der);
|
||||||
|
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !outer.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ASPAPayloadState".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let payload_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(payload_der);
|
||||||
|
let mut aps = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
aps.push(decode_aspa_payload_set(full)?);
|
||||||
|
}
|
||||||
|
let hash = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
Ok(AspaPayloadState { aps, hash })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_aspa_payload_set(der: &[u8]) -> Result<AspaPayloadSet, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ASPAPayloadSet".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let customer_as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
|
||||||
|
let providers_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(providers_der);
|
||||||
|
let mut providers = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
providers.push(reader.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32);
|
||||||
|
}
|
||||||
|
Ok(AspaPayloadSet {
|
||||||
|
customer_as_id,
|
||||||
|
providers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_trust_anchor_state(explicit_der: &[u8]) -> Result<TrustAnchorState, CcrDecodeError> {
|
||||||
|
let mut outer = DerReader::new(explicit_der);
|
||||||
|
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !outer.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after TrustAnchorState".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let skis_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(skis_der);
|
||||||
|
let mut skis = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
skis.push(
|
||||||
|
reader
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let hash = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
Ok(TrustAnchorState { skis, hash })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_router_key_state(explicit_der: &[u8]) -> Result<RouterKeyState, CcrDecodeError> {
|
||||||
|
let mut outer = DerReader::new(explicit_der);
|
||||||
|
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !outer.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after RouterKeyState".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let sets_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(sets_der);
|
||||||
|
let mut rksets = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
rksets.push(decode_router_key_set(full)?);
|
||||||
|
}
|
||||||
|
let hash = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
Ok(RouterKeyState { rksets, hash })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_router_key_set(der: &[u8]) -> Result<RouterKeySet, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after RouterKeySet".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
|
||||||
|
let keys_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
|
||||||
|
let mut reader = DerReader::new(keys_der);
|
||||||
|
let mut router_keys = Vec::new();
|
||||||
|
while !reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
router_keys.push(decode_router_key(full)?);
|
||||||
|
}
|
||||||
|
Ok(RouterKeySet { as_id, router_keys })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_router_key(der: &[u8]) -> Result<RouterKey, CcrDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after RouterKey".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let ski = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CcrDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
let (_tag, full, _value) = seq.take_any_full().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse("trailing fields in RouterKey".into()));
|
||||||
|
}
|
||||||
|
Ok(RouterKey {
|
||||||
|
ski,
|
||||||
|
spki_der: full.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_digest_algorithm(mut seq: DerReader<'_>) -> Result<CcrDigestAlgorithm, CcrDecodeError> {
|
||||||
|
let oid_raw = seq.take_tag(0x06).map_err(CcrDecodeError::Parse)?;
|
||||||
|
if oid_raw != OID_SHA256_RAW {
|
||||||
|
return Err(CcrDecodeError::UnexpectedDigestAlgorithm {
|
||||||
|
expected: OID_SHA256,
|
||||||
|
actual: oid_string(oid_raw)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !seq.is_empty() {
|
||||||
|
let tag = seq.peek_tag().map_err(CcrDecodeError::Parse)?;
|
||||||
|
if tag == 0x05 {
|
||||||
|
let null = seq.take_tag(0x05).map_err(CcrDecodeError::Parse)?;
|
||||||
|
if !null.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"AlgorithmIdentifier NULL parameters must be empty".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"trailing fields in DigestAlgorithmIdentifier".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(CcrDigestAlgorithm::Sha256)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oid_string(raw_body: &[u8]) -> Result<String, CcrDecodeError> {
|
||||||
|
let der = {
|
||||||
|
let mut out = Vec::with_capacity(raw_body.len() + 2);
|
||||||
|
out.push(0x06);
|
||||||
|
if raw_body.len() < 0x80 {
|
||||||
|
out.push(raw_body.len() as u8);
|
||||||
|
} else {
|
||||||
|
return Err(CcrDecodeError::Parse("OID too long".into()));
|
||||||
|
}
|
||||||
|
out.extend_from_slice(raw_body);
|
||||||
|
out
|
||||||
|
};
|
||||||
|
let (_rem, oid) = parse_der_oid(&der).map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
|
||||||
|
let oid = oid
|
||||||
|
.as_oid_val()
|
||||||
|
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
|
||||||
|
Ok(oid.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_generalized_time(bytes: &[u8]) -> Result<time::OffsetDateTime, CcrDecodeError> {
|
||||||
|
let s = std::str::from_utf8(bytes).map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
|
||||||
|
if s.len() != 15 || !s.ends_with('Z') {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"GeneralizedTime must be YYYYMMDDHHMMSSZ".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let parse = |range: std::ops::Range<usize>| -> Result<u32, CcrDecodeError> {
|
||||||
|
s[range]
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|e| CcrDecodeError::Parse(e.to_string()))
|
||||||
|
};
|
||||||
|
let year = parse(0..4)? as i32;
|
||||||
|
let month = parse(4..6)? as u8;
|
||||||
|
let day = parse(6..8)? as u8;
|
||||||
|
let hour = parse(8..10)? as u8;
|
||||||
|
let minute = parse(10..12)? as u8;
|
||||||
|
let second = parse(12..14)? as u8;
|
||||||
|
let month = time::Month::try_from(month).map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
|
||||||
|
let date = time::Date::from_calendar_date(year, month, day)
|
||||||
|
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
|
||||||
|
let timev = time::Time::from_hms(hour, minute, second)
|
||||||
|
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
|
||||||
|
Ok(time::PrimitiveDateTime::new(date, timev).assume_utc())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_big_unsigned(bytes: &[u8]) -> Result<BigUnsigned, CcrDecodeError> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Err(CcrDecodeError::Parse("INTEGER has empty content".into()));
|
||||||
|
}
|
||||||
|
if bytes[0] & 0x80 != 0 {
|
||||||
|
return Err(CcrDecodeError::Parse("INTEGER must be non-negative".into()));
|
||||||
|
}
|
||||||
|
if bytes.len() > 1 && bytes[0] == 0x00 && (bytes[1] & 0x80) == 0 {
|
||||||
|
return Err(CcrDecodeError::Parse(
|
||||||
|
"INTEGER not minimally encoded".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let bytes_be = if bytes.len() > 1 && bytes[0] == 0x00 {
|
||||||
|
bytes[1..].to_vec()
|
||||||
|
} else {
|
||||||
|
bytes.to_vec()
|
||||||
|
};
|
||||||
|
Ok(BigUnsigned { bytes_be })
|
||||||
|
}
|
||||||
112
src/ccr/dump.rs
Normal file
112
src/ccr/dump.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcrDumpError {
|
||||||
|
#[error("CCR decode failed: {0}")]
|
||||||
|
Decode(#[from] CcrDecodeError),
|
||||||
|
|
||||||
|
#[error("format time failed: {0}")]
|
||||||
|
FormatTime(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump_content_info_json_value(der: &[u8]) -> Result<serde_json::Value, CcrDumpError> {
|
||||||
|
let content_info = decode_content_info(der)?;
|
||||||
|
dump_content_info_json(&content_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump_content_info_json(
|
||||||
|
content_info: &crate::ccr::model::CcrContentInfo,
|
||||||
|
) -> Result<serde_json::Value, CcrDumpError> {
|
||||||
|
let produced_at = content_info
|
||||||
|
.content
|
||||||
|
.produced_at
|
||||||
|
.to_offset(time::UtcOffset::UTC)
|
||||||
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
|
.map_err(|e| CcrDumpError::FormatTime(e.to_string()))?;
|
||||||
|
|
||||||
|
let mfts = content_info.content.mfts.as_ref().map(|state| {
|
||||||
|
json!({
|
||||||
|
"present": true,
|
||||||
|
"manifest_instances": state.mis.len(),
|
||||||
|
"most_recent_update_rfc3339_utc": state.most_recent_update.to_offset(time::UtcOffset::UTC).format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||||
|
"hash_hex": hex::encode(&state.hash),
|
||||||
|
})
|
||||||
|
}).unwrap_or_else(|| json!({"present": false}));
|
||||||
|
|
||||||
|
let vrps_total = content_info
|
||||||
|
.content
|
||||||
|
.vrps
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| {
|
||||||
|
state
|
||||||
|
.rps
|
||||||
|
.iter()
|
||||||
|
.map(|set| set.ip_addr_blocks.len())
|
||||||
|
.sum::<usize>()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let vrps = content_info
|
||||||
|
.content
|
||||||
|
.vrps
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| {
|
||||||
|
json!({
|
||||||
|
"present": true,
|
||||||
|
"payload_sets": state.rps.len(),
|
||||||
|
"hash_hex": hex::encode(&state.hash),
|
||||||
|
"ip_addr_block_count": vrps_total,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| json!({"present": false}));
|
||||||
|
|
||||||
|
let vaps = content_info
|
||||||
|
.content
|
||||||
|
.vaps
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| {
|
||||||
|
json!({
|
||||||
|
"present": true,
|
||||||
|
"payload_sets": state.aps.len(),
|
||||||
|
"hash_hex": hex::encode(&state.hash),
|
||||||
|
"provider_count": state.aps.iter().map(|set| set.providers.len()).sum::<usize>(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| json!({"present": false}));
|
||||||
|
|
||||||
|
let tas = content_info
|
||||||
|
.content
|
||||||
|
.tas
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| {
|
||||||
|
json!({
|
||||||
|
"present": true,
|
||||||
|
"ski_count": state.skis.len(),
|
||||||
|
"hash_hex": hex::encode(&state.hash),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| json!({"present": false}));
|
||||||
|
|
||||||
|
let rks = content_info.content.rks.as_ref().map(|state| {
|
||||||
|
json!({
|
||||||
|
"present": true,
|
||||||
|
"router_key_sets": state.rksets.len(),
|
||||||
|
"router_key_count": state.rksets.iter().map(|set| set.router_keys.len()).sum::<usize>(),
|
||||||
|
"hash_hex": hex::encode(&state.hash),
|
||||||
|
})
|
||||||
|
}).unwrap_or_else(|| json!({"present": false}));
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"content_type_oid": content_info.content_type_oid,
|
||||||
|
"version": content_info.content.version,
|
||||||
|
"hash_alg": content_info.content.hash_alg.oid(),
|
||||||
|
"produced_at_rfc3339_utc": produced_at,
|
||||||
|
"state_aspects": {
|
||||||
|
"mfts": mfts,
|
||||||
|
"vrps": vrps,
|
||||||
|
"vaps": vaps,
|
||||||
|
"tas": tas,
|
||||||
|
"rks": rks,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
303
src/ccr/encode.rs
Normal file
303
src/ccr/encode.rs
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
use crate::ccr::model::{
|
||||||
|
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
|
||||||
|
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
|
||||||
|
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
|
||||||
|
};
|
||||||
|
use crate::data_model::common::BigUnsigned;
|
||||||
|
use crate::data_model::oid::{OID_CT_RPKI_CCR_RAW, OID_SHA256_RAW};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcrEncodeError {
|
||||||
|
#[error("CCR model validation failed: {0}")]
|
||||||
|
Validate(String),
|
||||||
|
|
||||||
|
#[error("GeneralizedTime formatting failed: {0}")]
|
||||||
|
ProducedAtFormat(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_content_info(content_info: &CcrContentInfo) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
content_info.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let content_der = encode_ccr(&content_info.content)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_oid(OID_CT_RPKI_CCR_RAW),
|
||||||
|
encode_explicit(0, &content_der),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_ccr(ccr: &RpkiCanonicalCacheRepresentation) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
ccr.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
if ccr.version != CCR_VERSION_V0 {
|
||||||
|
fields.push(encode_explicit(0, &encode_integer_u32(ccr.version)));
|
||||||
|
}
|
||||||
|
fields.push(encode_digest_algorithm(&ccr.hash_alg));
|
||||||
|
fields.push(encode_generalized_time(ccr.produced_at)?);
|
||||||
|
if let Some(mfts) = &ccr.mfts {
|
||||||
|
fields.push(encode_explicit(1, &encode_manifest_state(mfts)?));
|
||||||
|
}
|
||||||
|
if let Some(vrps) = &ccr.vrps {
|
||||||
|
fields.push(encode_explicit(2, &encode_roa_payload_state(vrps)?));
|
||||||
|
}
|
||||||
|
if let Some(vaps) = &ccr.vaps {
|
||||||
|
fields.push(encode_explicit(3, &encode_aspa_payload_state(vaps)?));
|
||||||
|
}
|
||||||
|
if let Some(tas) = &ccr.tas {
|
||||||
|
fields.push(encode_explicit(4, &encode_trust_anchor_state(tas)?));
|
||||||
|
}
|
||||||
|
if let Some(rks) = &ccr.rks {
|
||||||
|
fields.push(encode_explicit(5, &encode_router_key_state(rks)?));
|
||||||
|
}
|
||||||
|
Ok(encode_sequence(&fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_manifest_state(state: &ManifestState) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
state.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let mis = encode_manifest_state_payload_der(&state.mis)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
mis,
|
||||||
|
encode_generalized_time(state.most_recent_update)?,
|
||||||
|
encode_octet_string(&state.hash),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_manifest_state_payload_der(
|
||||||
|
instances: &[ManifestInstance],
|
||||||
|
) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
Ok(encode_sequence(
|
||||||
|
&instances
|
||||||
|
.iter()
|
||||||
|
.map(encode_manifest_instance)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_manifest_instance(instance: &ManifestInstance) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
instance.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let mut fields = vec![
|
||||||
|
encode_octet_string(&instance.hash),
|
||||||
|
encode_integer_u64(instance.size),
|
||||||
|
encode_octet_string(&instance.aki),
|
||||||
|
encode_integer_bigunsigned(&instance.manifest_number),
|
||||||
|
encode_generalized_time(instance.this_update)?,
|
||||||
|
encode_sequence(&instance.locations),
|
||||||
|
];
|
||||||
|
if !instance.subordinates.is_empty() {
|
||||||
|
fields.push(encode_sequence(
|
||||||
|
&instance
|
||||||
|
.subordinates
|
||||||
|
.iter()
|
||||||
|
.map(|ski| encode_octet_string(ski))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(encode_sequence(&fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_roa_payload_state(state: &RoaPayloadState) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
state.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let rps = encode_roa_payload_state_payload_der(&state.rps)?;
|
||||||
|
Ok(encode_sequence(&[rps, encode_octet_string(&state.hash)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_roa_payload_state_payload_der(
|
||||||
|
sets: &[RoaPayloadSet],
|
||||||
|
) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
Ok(encode_sequence(
|
||||||
|
&sets
|
||||||
|
.iter()
|
||||||
|
.map(encode_roa_payload_set)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_roa_payload_set(set: &RoaPayloadSet) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
set.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_integer_u32(set.as_id),
|
||||||
|
encode_sequence(&set.ip_addr_blocks),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_aspa_payload_state(state: &AspaPayloadState) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
state.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let aps = encode_aspa_payload_state_payload_der(&state.aps)?;
|
||||||
|
Ok(encode_sequence(&[aps, encode_octet_string(&state.hash)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_aspa_payload_state_payload_der(
|
||||||
|
sets: &[AspaPayloadSet],
|
||||||
|
) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
Ok(encode_sequence(
|
||||||
|
&sets
|
||||||
|
.iter()
|
||||||
|
.map(encode_aspa_payload_set)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_aspa_payload_set(set: &AspaPayloadSet) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
set.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_integer_u32(set.customer_as_id),
|
||||||
|
encode_sequence(
|
||||||
|
&set.providers
|
||||||
|
.iter()
|
||||||
|
.map(|provider| encode_integer_u32(*provider))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_trust_anchor_state(state: &TrustAnchorState) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
state.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let skis = encode_trust_anchor_state_payload_der(&state.skis)?;
|
||||||
|
Ok(encode_sequence(&[skis, encode_octet_string(&state.hash)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_trust_anchor_state_payload_der(skis: &[Vec<u8>]) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
Ok(encode_sequence(
|
||||||
|
&skis
|
||||||
|
.iter()
|
||||||
|
.map(|ski| encode_octet_string(ski))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_router_key_state(state: &RouterKeyState) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
state.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
let rksets = encode_router_key_state_payload_der(&state.rksets)?;
|
||||||
|
Ok(encode_sequence(&[rksets, encode_octet_string(&state.hash)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_router_key_state_payload_der(
|
||||||
|
sets: &[RouterKeySet],
|
||||||
|
) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
Ok(encode_sequence(
|
||||||
|
&sets
|
||||||
|
.iter()
|
||||||
|
.map(encode_router_key_set)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_router_key_set(set: &RouterKeySet) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
set.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_integer_u32(set.as_id),
|
||||||
|
encode_sequence(
|
||||||
|
&set.router_keys
|
||||||
|
.iter()
|
||||||
|
.map(encode_router_key)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_router_key(key: &RouterKey) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
key.validate().map_err(CcrEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_octet_string(&key.ski),
|
||||||
|
key.spki_der.clone(),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_digest_algorithm(alg: &CcrDigestAlgorithm) -> Vec<u8> {
|
||||||
|
match alg {
|
||||||
|
CcrDigestAlgorithm::Sha256 => encode_sequence(&[encode_oid(OID_SHA256_RAW)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_generalized_time(t: time::OffsetDateTime) -> Result<Vec<u8>, CcrEncodeError> {
|
||||||
|
let t = t.to_offset(time::UtcOffset::UTC);
|
||||||
|
let s = format!(
|
||||||
|
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
|
||||||
|
t.year(),
|
||||||
|
u8::from(t.month()),
|
||||||
|
t.day(),
|
||||||
|
t.hour(),
|
||||||
|
t.minute(),
|
||||||
|
t.second()
|
||||||
|
);
|
||||||
|
Ok(encode_tlv(0x18, s.into_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_u32(v: u32) -> Vec<u8> {
|
||||||
|
encode_integer_bytes(unsigned_integer_bytes(v as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_u64(v: u64) -> Vec<u8> {
|
||||||
|
encode_integer_bytes(unsigned_integer_bytes(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_bigunsigned(v: &BigUnsigned) -> Vec<u8> {
|
||||||
|
encode_integer_bytes(v.bytes_be.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_bytes(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
bytes.push(0);
|
||||||
|
}
|
||||||
|
if bytes[0] & 0x80 != 0 {
|
||||||
|
bytes.insert(0, 0);
|
||||||
|
}
|
||||||
|
encode_tlv(0x02, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unsigned_integer_bytes(v: u64) -> Vec<u8> {
|
||||||
|
if v == 0 {
|
||||||
|
return vec![0];
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut n = v;
|
||||||
|
while n > 0 {
|
||||||
|
out.push((n & 0xFF) as u8);
|
||||||
|
n >>= 8;
|
||||||
|
}
|
||||||
|
out.reverse();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_oid(raw_body: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0x06, raw_body.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0x04, bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_explicit(tag_number: u8, inner_der: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0xA0 + tag_number, inner_der.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let total_len: usize = elements.iter().map(Vec::len).sum();
|
||||||
|
let mut buf = Vec::with_capacity(total_len);
|
||||||
|
for element in elements {
|
||||||
|
buf.extend_from_slice(element);
|
||||||
|
}
|
||||||
|
encode_tlv(0x30, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(1 + 9 + value.len());
|
||||||
|
out.push(tag);
|
||||||
|
encode_length(value.len(), &mut out);
|
||||||
|
out.extend_from_slice(&value);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_length(len: usize, out: &mut Vec<u8>) {
|
||||||
|
if len < 0x80 {
|
||||||
|
out.push(len as u8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
let mut value = len;
|
||||||
|
while value > 0 {
|
||||||
|
bytes.push((value & 0xFF) as u8);
|
||||||
|
value >>= 8;
|
||||||
|
}
|
||||||
|
bytes.reverse();
|
||||||
|
out.push(0x80 | (bytes.len() as u8));
|
||||||
|
out.extend_from_slice(&bytes);
|
||||||
|
}
|
||||||
272
src/ccr/export.rs
Normal file
272
src/ccr/export.rs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
use crate::ccr::build::{
|
||||||
|
CcrBuildError, build_aspa_payload_state, build_manifest_state_from_vcirs,
|
||||||
|
build_roa_payload_state, build_trust_anchor_state,
|
||||||
|
};
|
||||||
|
use crate::ccr::encode::{CcrEncodeError, encode_content_info};
|
||||||
|
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
|
||||||
|
use crate::data_model::ta::TrustAnchor;
|
||||||
|
use crate::storage::RocksStore;
|
||||||
|
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcrExportError {
|
||||||
|
#[error("list VCIRs failed: {0}")]
|
||||||
|
ListVcirs(String),
|
||||||
|
|
||||||
|
#[error("build CCR state failed: {0}")]
|
||||||
|
Build(#[from] CcrBuildError),
|
||||||
|
|
||||||
|
#[error("encode CCR failed: {0}")]
|
||||||
|
Encode(#[from] CcrEncodeError),
|
||||||
|
|
||||||
|
#[error("write CCR file failed: {0}: {1}")]
|
||||||
|
Write(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_ccr_from_run(
|
||||||
|
store: &RocksStore,
|
||||||
|
trust_anchors: &[TrustAnchor],
|
||||||
|
vrps: &[Vrp],
|
||||||
|
aspas: &[AspaAttestation],
|
||||||
|
router_keys: &[RouterKeyPayload],
|
||||||
|
produced_at: time::OffsetDateTime,
|
||||||
|
) -> Result<RpkiCanonicalCacheRepresentation, CcrExportError> {
|
||||||
|
let vcirs = store
|
||||||
|
.list_vcirs()
|
||||||
|
.map_err(|e| CcrExportError::ListVcirs(e.to_string()))?;
|
||||||
|
|
||||||
|
let mfts = Some(build_manifest_state_from_vcirs(store, &vcirs)?);
|
||||||
|
let vrps = Some(build_roa_payload_state(vrps)?);
|
||||||
|
let vaps = Some(build_aspa_payload_state(aspas)?);
|
||||||
|
let tas = Some(build_trust_anchor_state(trust_anchors)?);
|
||||||
|
let rks = Some(build_router_key_state_from_runtime(router_keys)?);
|
||||||
|
|
||||||
|
Ok(RpkiCanonicalCacheRepresentation {
|
||||||
|
version: 0,
|
||||||
|
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||||
|
produced_at,
|
||||||
|
mfts,
|
||||||
|
vrps,
|
||||||
|
vaps,
|
||||||
|
tas,
|
||||||
|
rks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_router_key_state_from_runtime(
|
||||||
|
router_keys: &[RouterKeyPayload],
|
||||||
|
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
|
||||||
|
use crate::ccr::model::{RouterKey, RouterKeySet};
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
let mut grouped: BTreeMap<u32, BTreeSet<RouterKey>> = BTreeMap::new();
|
||||||
|
for router_key in router_keys {
|
||||||
|
grouped
|
||||||
|
.entry(router_key.as_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(RouterKey {
|
||||||
|
ski: router_key.ski.clone(),
|
||||||
|
spki_der: router_key.spki_der.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let rksets = grouped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(as_id, router_keys)| RouterKeySet {
|
||||||
|
as_id,
|
||||||
|
router_keys: router_keys.into_iter().collect(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
build_router_key_state_from_sets(&rksets)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_router_key_state_from_sets(
|
||||||
|
rksets: &[crate::ccr::model::RouterKeySet],
|
||||||
|
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
|
||||||
|
let der = crate::ccr::encode::encode_router_key_state_payload_der(rksets)
|
||||||
|
.map_err(|e| CcrBuildError::RouterKeyEncode(e.to_string()))?;
|
||||||
|
Ok(crate::ccr::model::RouterKeyState {
|
||||||
|
rksets: rksets.to_vec(),
|
||||||
|
hash: crate::ccr::compute_state_hash(&der),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_ccr_file(
|
||||||
|
path: &Path,
|
||||||
|
ccr: &RpkiCanonicalCacheRepresentation,
|
||||||
|
) -> Result<(), CcrExportError> {
|
||||||
|
let der = encode_content_info(&CcrContentInfo::new(ccr.clone()))?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))?;
|
||||||
|
}
|
||||||
|
std::fs::write(path, der)
|
||||||
|
.map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ccr::decode::decode_content_info;
|
||||||
|
use crate::data_model::manifest::ManifestObject;
|
||||||
|
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||||
|
use crate::data_model::ta::TrustAnchor;
|
||||||
|
use crate::data_model::tal::Tal;
|
||||||
|
use crate::storage::{
|
||||||
|
PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
|
||||||
|
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
|
||||||
|
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||||
|
};
|
||||||
|
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
fn sample_trust_anchor() -> TrustAnchor {
|
||||||
|
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let tal_bytes = std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal"))
|
||||||
|
.expect("read tal");
|
||||||
|
let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).expect("read ta");
|
||||||
|
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal");
|
||||||
|
TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_vcir_and_raw(store: &RocksStore) -> ValidatedCaInstanceResult {
|
||||||
|
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let manifest_der = std::fs::read(base.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft")).expect("read manifest");
|
||||||
|
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
|
||||||
|
let hash = hex::encode(sha2::Sha256::digest(&manifest_der));
|
||||||
|
let mut raw = RawByHashEntry::from_bytes(hash.clone(), manifest_der.clone());
|
||||||
|
raw.origin_uris
|
||||||
|
.push("rsync://example.test/repo/current.mft".to_string());
|
||||||
|
raw.object_type = Some("mft".to_string());
|
||||||
|
raw.encoding = Some("der".to_string());
|
||||||
|
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
||||||
|
ValidatedCaInstanceResult {
|
||||||
|
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||||
|
parent_manifest_rsync_uri: None,
|
||||||
|
tal_id: "apnic".to_string(),
|
||||||
|
ca_subject_name: "CN=test".to_string(),
|
||||||
|
ca_ski: "11".repeat(20),
|
||||||
|
issuer_ski: "22".repeat(20),
|
||||||
|
last_successful_validation_time: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.this_update,
|
||||||
|
),
|
||||||
|
current_manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||||
|
current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(),
|
||||||
|
validated_manifest_meta: ValidatedManifestMeta {
|
||||||
|
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||||
|
validated_manifest_this_update: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.this_update,
|
||||||
|
),
|
||||||
|
validated_manifest_next_update: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.next_update,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
instance_gate: VcirInstanceGate {
|
||||||
|
manifest_next_update: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.next_update,
|
||||||
|
),
|
||||||
|
current_crl_next_update: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.next_update,
|
||||||
|
),
|
||||||
|
self_ca_not_after: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.next_update,
|
||||||
|
),
|
||||||
|
instance_effective_until: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.next_update,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child_entries: vec![VcirChildEntry {
|
||||||
|
child_manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(),
|
||||||
|
child_cert_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
|
||||||
|
child_cert_hash: "aa".repeat(32),
|
||||||
|
child_ski: "33".repeat(20),
|
||||||
|
child_rsync_base_uri: "rsync://example.test/repo/".to_string(),
|
||||||
|
child_publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
|
||||||
|
child_rrdp_notification_uri: None,
|
||||||
|
child_effective_ip_resources: None,
|
||||||
|
child_effective_as_resources: None,
|
||||||
|
accepted_at_validation_time: PackTime::from_utc_offset_datetime(
|
||||||
|
manifest.manifest.this_update,
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
local_outputs: Vec::new(),
|
||||||
|
related_artifacts: vec![VcirRelatedArtifact {
|
||||||
|
artifact_role: VcirArtifactRole::Manifest,
|
||||||
|
artifact_kind: VcirArtifactKind::Mft,
|
||||||
|
uri: Some("rsync://example.test/repo/current.mft".to_string()),
|
||||||
|
sha256: hash,
|
||||||
|
object_type: Some("mft".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
|
}],
|
||||||
|
summary: VcirSummary {
|
||||||
|
local_vrp_count: 0,
|
||||||
|
local_aspa_count: 0,
|
||||||
|
local_router_key_count: 0,
|
||||||
|
child_count: 1,
|
||||||
|
accepted_object_count: 1,
|
||||||
|
rejected_object_count: 0,
|
||||||
|
},
|
||||||
|
audit_summary: VcirAuditSummary {
|
||||||
|
failed_fetch_eligible: true,
|
||||||
|
last_failed_fetch_reason: None,
|
||||||
|
warning_count: 0,
|
||||||
|
audit_flags: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_and_write_ccr_from_run_exports_der_content_info() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let db_path = td.path().join("db");
|
||||||
|
let store = RocksStore::open(&db_path).expect("open rocksdb");
|
||||||
|
let vcir = sample_vcir_and_raw(&store);
|
||||||
|
store.put_vcir(&vcir).expect("put vcir");
|
||||||
|
let trust_anchor = sample_trust_anchor();
|
||||||
|
let vrps = vec![Vrp {
|
||||||
|
asn: 64496,
|
||||||
|
prefix: IpPrefix {
|
||||||
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len: 8,
|
||||||
|
addr: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
},
|
||||||
|
max_length: 8,
|
||||||
|
}];
|
||||||
|
let aspas = vec![AspaAttestation {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![64497],
|
||||||
|
}];
|
||||||
|
let router_keys = vec![RouterKeyPayload {
|
||||||
|
as_id: 64496,
|
||||||
|
ski: vec![0x11; 20],
|
||||||
|
spki_der: vec![0x30, 0x00],
|
||||||
|
source_object_uri: "rsync://example.test/repo/router.cer".to_string(),
|
||||||
|
source_object_hash: hex::encode([0x11; 32]),
|
||||||
|
source_ee_cert_hash: hex::encode([0x11; 32]),
|
||||||
|
item_effective_until: PackTime::from_utc_offset_datetime(
|
||||||
|
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
|
||||||
|
),
|
||||||
|
}];
|
||||||
|
let ccr = build_ccr_from_run(
|
||||||
|
&store,
|
||||||
|
&[trust_anchor],
|
||||||
|
&vrps,
|
||||||
|
&aspas,
|
||||||
|
&router_keys,
|
||||||
|
time::OffsetDateTime::now_utc(),
|
||||||
|
)
|
||||||
|
.expect("build ccr");
|
||||||
|
assert!(ccr.mfts.is_some());
|
||||||
|
assert!(ccr.vrps.is_some());
|
||||||
|
assert!(ccr.vaps.is_some());
|
||||||
|
assert!(ccr.tas.is_some());
|
||||||
|
assert!(ccr.rks.is_some());
|
||||||
|
assert_eq!(ccr.rks.as_ref().unwrap().rksets.len(), 1);
|
||||||
|
let out = td.path().join("out/example.ccr");
|
||||||
|
write_ccr_file(&out, &ccr).expect("write ccr");
|
||||||
|
let der = std::fs::read(&out).expect("read ccr file");
|
||||||
|
let decoded = decode_content_info(&der).expect("decode ccr");
|
||||||
|
assert_eq!(decoded.content.version, 0);
|
||||||
|
assert!(decoded.content.mfts.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/ccr/hash.rs
Normal file
9
src/ccr/hash.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
pub fn compute_state_hash(payload_der: &[u8]) -> Vec<u8> {
|
||||||
|
sha2::Sha256::digest(payload_der).to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_state_hash(expected: &[u8], payload_der: &[u8]) -> bool {
|
||||||
|
compute_state_hash(payload_der).as_slice() == expected
|
||||||
|
}
|
||||||
34
src/ccr/mod.rs
Normal file
34
src/ccr/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod build;
|
||||||
|
pub mod decode;
|
||||||
|
pub mod dump;
|
||||||
|
pub mod encode;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod export;
|
||||||
|
pub mod hash;
|
||||||
|
pub mod model;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod verify;
|
||||||
|
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub use build::{
|
||||||
|
CcrBuildError, build_aspa_payload_state, build_manifest_state_from_vcirs,
|
||||||
|
build_roa_payload_state, build_trust_anchor_state,
|
||||||
|
};
|
||||||
|
pub use decode::{CcrDecodeError, decode_content_info};
|
||||||
|
pub use dump::{CcrDumpError, dump_content_info_json, dump_content_info_json_value};
|
||||||
|
pub use encode::{CcrEncodeError, encode_content_info};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub use export::{CcrExportError, build_ccr_from_run, write_ccr_file};
|
||||||
|
pub use hash::{compute_state_hash, verify_state_hash};
|
||||||
|
pub use model::{
|
||||||
|
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
|
||||||
|
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
|
||||||
|
RpkiCanonicalCacheRepresentation, TrustAnchorState,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub use verify::{
|
||||||
|
CcrVerifyError, CcrVerifySummary, extract_vrp_rows, verify_against_report_json_path,
|
||||||
|
verify_against_vcir_store, verify_against_vcir_store_path, verify_content_info,
|
||||||
|
verify_content_info_bytes,
|
||||||
|
};
|
||||||
398
src/ccr/model.rs
Normal file
398
src/ccr/model.rs
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
use crate::data_model::common::{BigUnsigned, der_take_tlv};
|
||||||
|
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_SHA256};
|
||||||
|
|
||||||
|
pub const CCR_VERSION_V0: u32 = 0;
|
||||||
|
pub const DIGEST_LEN_SHA256: usize = 32;
|
||||||
|
pub const KEY_IDENTIFIER_LEN_SHA1: usize = 20;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CcrDigestAlgorithm {
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcrDigestAlgorithm {
|
||||||
|
pub fn oid(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Sha256 => OID_SHA256,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CcrContentInfo {
|
||||||
|
pub content_type_oid: String,
|
||||||
|
pub content: RpkiCanonicalCacheRepresentation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcrContentInfo {
|
||||||
|
pub fn new(content: RpkiCanonicalCacheRepresentation) -> Self {
|
||||||
|
Self {
|
||||||
|
content_type_oid: OID_CT_RPKI_CCR.to_string(),
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.content_type_oid != OID_CT_RPKI_CCR {
|
||||||
|
return Err(format!(
|
||||||
|
"contentType must be {OID_CT_RPKI_CCR}, got {}",
|
||||||
|
self.content_type_oid
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.content.validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RpkiCanonicalCacheRepresentation {
|
||||||
|
pub version: u32,
|
||||||
|
pub hash_alg: CcrDigestAlgorithm,
|
||||||
|
pub produced_at: time::OffsetDateTime,
|
||||||
|
pub mfts: Option<ManifestState>,
|
||||||
|
pub vrps: Option<RoaPayloadState>,
|
||||||
|
pub vaps: Option<AspaPayloadState>,
|
||||||
|
pub tas: Option<TrustAnchorState>,
|
||||||
|
pub rks: Option<RouterKeyState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpkiCanonicalCacheRepresentation {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.version != CCR_VERSION_V0 {
|
||||||
|
return Err(format!("CCR version must be 0, got {}", self.version));
|
||||||
|
}
|
||||||
|
if !matches!(self.hash_alg, CcrDigestAlgorithm::Sha256) {
|
||||||
|
return Err("CCR hashAlg must be SHA-256".into());
|
||||||
|
}
|
||||||
|
if self.mfts.is_none()
|
||||||
|
&& self.vrps.is_none()
|
||||||
|
&& self.vaps.is_none()
|
||||||
|
&& self.tas.is_none()
|
||||||
|
&& self.rks.is_none()
|
||||||
|
{
|
||||||
|
return Err("at least one of mfts/vrps/vaps/tas/rks must be present".into());
|
||||||
|
}
|
||||||
|
if let Some(mfts) = &self.mfts {
|
||||||
|
mfts.validate()?;
|
||||||
|
}
|
||||||
|
if let Some(vrps) = &self.vrps {
|
||||||
|
vrps.validate()?;
|
||||||
|
}
|
||||||
|
if let Some(vaps) = &self.vaps {
|
||||||
|
vaps.validate()?;
|
||||||
|
}
|
||||||
|
if let Some(tas) = &self.tas {
|
||||||
|
tas.validate()?;
|
||||||
|
}
|
||||||
|
if let Some(rks) = &self.rks {
|
||||||
|
rks.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ManifestState {
|
||||||
|
pub mis: Vec<ManifestInstance>,
|
||||||
|
pub most_recent_update: time::OffsetDateTime,
|
||||||
|
pub hash: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManifestState {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_sha256_digest("ManifestState.hash", &self.hash)?;
|
||||||
|
validate_sorted_unique_by(
|
||||||
|
&self.mis,
|
||||||
|
|item| item.hash.as_slice(),
|
||||||
|
"ManifestState.mis must be sorted by hash and unique",
|
||||||
|
)?;
|
||||||
|
for instance in &self.mis {
|
||||||
|
instance.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ManifestInstance {
|
||||||
|
pub hash: Vec<u8>,
|
||||||
|
pub size: u64,
|
||||||
|
pub aki: Vec<u8>,
|
||||||
|
pub manifest_number: BigUnsigned,
|
||||||
|
pub this_update: time::OffsetDateTime,
|
||||||
|
pub locations: Vec<Vec<u8>>,
|
||||||
|
pub subordinates: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManifestInstance {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_sha256_digest("ManifestInstance.hash", &self.hash)?;
|
||||||
|
if self.size < 1000 {
|
||||||
|
return Err(format!(
|
||||||
|
"ManifestInstance.size must be >= 1000, got {}",
|
||||||
|
self.size
|
||||||
|
));
|
||||||
|
}
|
||||||
|
validate_key_identifier("ManifestInstance.aki", &self.aki)?;
|
||||||
|
validate_big_unsigned_bytes(
|
||||||
|
"ManifestInstance.manifest_number",
|
||||||
|
&self.manifest_number.bytes_be,
|
||||||
|
)?;
|
||||||
|
if self.locations.is_empty() {
|
||||||
|
return Err(
|
||||||
|
"ManifestInstance.locations must contain at least one AccessDescription".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for location in &self.locations {
|
||||||
|
validate_full_der_with_tag("ManifestInstance.locations[]", location, Some(0x30))?;
|
||||||
|
}
|
||||||
|
if !self.subordinates.is_empty() {
|
||||||
|
validate_sorted_unique_bytes(
|
||||||
|
&self.subordinates,
|
||||||
|
KEY_IDENTIFIER_LEN_SHA1,
|
||||||
|
"ManifestInstance.subordinates must be sorted/unique 20-byte SKIs",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RoaPayloadState {
|
||||||
|
pub rps: Vec<RoaPayloadSet>,
|
||||||
|
pub hash: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoaPayloadState {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_sha256_digest("ROAPayloadState.hash", &self.hash)?;
|
||||||
|
validate_sorted_unique_by(
|
||||||
|
&self.rps,
|
||||||
|
|item| &item.as_id,
|
||||||
|
"ROAPayloadState.rps must be sorted by asID and unique",
|
||||||
|
)?;
|
||||||
|
for set in &self.rps {
|
||||||
|
set.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RoaPayloadSet {
|
||||||
|
pub as_id: u32,
|
||||||
|
pub ip_addr_blocks: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoaPayloadSet {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.ip_addr_blocks.is_empty() || self.ip_addr_blocks.len() > 2 {
|
||||||
|
return Err(format!(
|
||||||
|
"ROAPayloadSet.ip_addr_blocks must contain 1..=2 entries, got {}",
|
||||||
|
self.ip_addr_blocks.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for block in &self.ip_addr_blocks {
|
||||||
|
validate_full_der_with_tag("ROAPayloadSet.ip_addr_blocks[]", block, Some(0x30))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct AspaPayloadState {
|
||||||
|
pub aps: Vec<AspaPayloadSet>,
|
||||||
|
pub hash: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AspaPayloadState {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_sha256_digest("ASPAPayloadState.hash", &self.hash)?;
|
||||||
|
validate_sorted_unique_by(
|
||||||
|
&self.aps,
|
||||||
|
|item| &item.customer_as_id,
|
||||||
|
"ASPAPayloadState.aps must be sorted by customerASID and unique",
|
||||||
|
)?;
|
||||||
|
for set in &self.aps {
|
||||||
|
set.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct AspaPayloadSet {
|
||||||
|
pub customer_as_id: u32,
|
||||||
|
pub providers: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AspaPayloadSet {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.providers.is_empty() {
|
||||||
|
return Err("ASPAPayloadSet.providers must be non-empty".into());
|
||||||
|
}
|
||||||
|
validate_sorted_unique_by(
|
||||||
|
&self.providers,
|
||||||
|
|provider| provider,
|
||||||
|
"ASPAPayloadSet.providers must be sorted ascending and unique",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TrustAnchorState {
|
||||||
|
pub skis: Vec<Vec<u8>>,
|
||||||
|
pub hash: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustAnchorState {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.skis.is_empty() {
|
||||||
|
return Err("TrustAnchorState.skis must be non-empty".into());
|
||||||
|
}
|
||||||
|
validate_sha256_digest("TrustAnchorState.hash", &self.hash)?;
|
||||||
|
validate_sorted_unique_bytes(
|
||||||
|
&self.skis,
|
||||||
|
KEY_IDENTIFIER_LEN_SHA1,
|
||||||
|
"TrustAnchorState.skis must be sorted/unique 20-byte SKIs",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RouterKeyState {
|
||||||
|
pub rksets: Vec<RouterKeySet>,
|
||||||
|
pub hash: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouterKeyState {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_sha256_digest("RouterKeyState.hash", &self.hash)?;
|
||||||
|
validate_sorted_unique_by(
|
||||||
|
&self.rksets,
|
||||||
|
|item| &item.as_id,
|
||||||
|
"RouterKeyState.rksets must be sorted by asID and unique",
|
||||||
|
)?;
|
||||||
|
for rkset in &self.rksets {
|
||||||
|
rkset.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RouterKeySet {
|
||||||
|
pub as_id: u32,
|
||||||
|
pub router_keys: Vec<RouterKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouterKeySet {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.router_keys.is_empty() {
|
||||||
|
return Err("RouterKeySet.router_keys must be non-empty".into());
|
||||||
|
}
|
||||||
|
validate_sorted_unique_by(
|
||||||
|
&self.router_keys,
|
||||||
|
|key| key,
|
||||||
|
"RouterKeySet.router_keys must be sorted by SKI and unique by (SKI, SPKI DER)",
|
||||||
|
)?;
|
||||||
|
for key in &self.router_keys {
|
||||||
|
key.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct RouterKey {
|
||||||
|
pub ski: Vec<u8>,
|
||||||
|
pub spki_der: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouterKey {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_key_identifier("RouterKey.ski", &self.ski)?;
|
||||||
|
validate_full_der_with_tag("RouterKey.spki_der", &self.spki_der, Some(0x30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sha256_digest(field: &str, bytes: &[u8]) -> Result<(), String> {
|
||||||
|
if bytes.len() != DIGEST_LEN_SHA256 {
|
||||||
|
return Err(format!(
|
||||||
|
"{field} must be {DIGEST_LEN_SHA256} bytes, got {}",
|
||||||
|
bytes.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_key_identifier(field: &str, bytes: &[u8]) -> Result<(), String> {
|
||||||
|
if bytes.len() != KEY_IDENTIFIER_LEN_SHA1 {
|
||||||
|
return Err(format!(
|
||||||
|
"{field} must be {KEY_IDENTIFIER_LEN_SHA1} bytes, got {}",
|
||||||
|
bytes.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_big_unsigned_bytes(field: &str, bytes: &[u8]) -> Result<(), String> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Err(format!("{field} must not be empty"));
|
||||||
|
}
|
||||||
|
if bytes.len() > 1 && bytes[0] == 0x00 {
|
||||||
|
return Err(format!(
|
||||||
|
"{field} must be minimally encoded as an unsigned integer"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sorted_unique_by<T, K: Ord + ?Sized>(
|
||||||
|
values: &[T],
|
||||||
|
key_fn: impl Fn(&T) -> &K,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for window in values.windows(2) {
|
||||||
|
if key_fn(&window[0]) >= key_fn(&window[1]) {
|
||||||
|
return Err(message.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sorted_unique_bytes(
|
||||||
|
values: &[Vec<u8>],
|
||||||
|
expected_len: usize,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for value in values {
|
||||||
|
if value.len() != expected_len {
|
||||||
|
return Err(message.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for window in values.windows(2) {
|
||||||
|
if window[0] >= window[1] {
|
||||||
|
return Err(message.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_full_der_with_tag(
|
||||||
|
field: &str,
|
||||||
|
der: &[u8],
|
||||||
|
expected_tag: Option<u8>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (tag, _value, rem) = der_take_tlv(der).map_err(|e| format!("{field}: {e}"))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(format!("{field}: trailing bytes after DER object"));
|
||||||
|
}
|
||||||
|
if let Some(expected_tag) = expected_tag {
|
||||||
|
if tag != expected_tag {
|
||||||
|
return Err(format!(
|
||||||
|
"{field}: unexpected tag 0x{tag:02X}, expected 0x{expected_tag:02X}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
683
src/ccr/verify.rs
Normal file
683
src/ccr/verify.rs
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
|
||||||
|
use crate::ccr::encode::{
|
||||||
|
encode_aspa_payload_state_payload_der, encode_manifest_state_payload_der,
|
||||||
|
encode_roa_payload_state_payload_der, encode_router_key_state_payload_der,
|
||||||
|
encode_trust_anchor_state_payload_der,
|
||||||
|
};
|
||||||
|
use crate::ccr::hash::verify_state_hash;
|
||||||
|
use crate::ccr::model::{CcrContentInfo, RouterKeyState, TrustAnchorState};
|
||||||
|
use crate::storage::{RocksStore, VcirArtifactRole};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct CcrVerifySummary {
|
||||||
|
pub content_type_oid: String,
|
||||||
|
pub version: u32,
|
||||||
|
pub produced_at_rfc3339_utc: String,
|
||||||
|
pub state_hashes_ok: bool,
|
||||||
|
pub manifest_instances: usize,
|
||||||
|
pub roa_payload_sets: usize,
|
||||||
|
pub roa_vrp_count: usize,
|
||||||
|
pub aspa_payload_sets: usize,
|
||||||
|
pub trust_anchor_ski_count: usize,
|
||||||
|
pub router_key_sets: usize,
|
||||||
|
pub router_key_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcrVerifyError {
|
||||||
|
#[error("CCR decode failed: {0}")]
|
||||||
|
Decode(#[from] CcrDecodeError),
|
||||||
|
|
||||||
|
#[error("ManifestState hash mismatch")]
|
||||||
|
ManifestHashMismatch,
|
||||||
|
|
||||||
|
#[error("ROAPayloadState hash mismatch")]
|
||||||
|
RoaHashMismatch,
|
||||||
|
|
||||||
|
#[error("ASPAPayloadState hash mismatch")]
|
||||||
|
AspaHashMismatch,
|
||||||
|
|
||||||
|
#[error("TrustAnchorState hash mismatch")]
|
||||||
|
TrustAnchorHashMismatch,
|
||||||
|
|
||||||
|
#[error("RouterKeyState hash mismatch")]
|
||||||
|
RouterKeyHashMismatch,
|
||||||
|
|
||||||
|
#[error("read report json failed: {0}: {1}")]
|
||||||
|
ReportRead(String, String),
|
||||||
|
|
||||||
|
#[error("parse report json failed: {0}")]
|
||||||
|
ReportParse(String),
|
||||||
|
|
||||||
|
#[error("VRP set mismatch: only_in_ccr={only_in_ccr} only_in_report={only_in_report}")]
|
||||||
|
ReportVrpMismatch {
|
||||||
|
only_in_ccr: usize,
|
||||||
|
only_in_report: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("ASPA set mismatch: only_in_ccr={only_in_ccr} only_in_report={only_in_report}")]
|
||||||
|
ReportAspaMismatch {
|
||||||
|
only_in_ccr: usize,
|
||||||
|
only_in_report: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("open RocksDB failed: {0}")]
|
||||||
|
OpenStore(String),
|
||||||
|
|
||||||
|
#[error("list VCIRs failed: {0}")]
|
||||||
|
ListVcirs(String),
|
||||||
|
|
||||||
|
#[error("VCIR manifest set mismatch: only_in_ccr={only_in_ccr} only_in_vcir={only_in_vcir}")]
|
||||||
|
VcirManifestMismatch {
|
||||||
|
only_in_ccr: usize,
|
||||||
|
only_in_vcir: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_content_info_bytes(der: &[u8]) -> Result<CcrVerifySummary, CcrVerifyError> {
|
||||||
|
let content_info = decode_content_info(der)?;
|
||||||
|
verify_content_info(&content_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_content_info(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
) -> Result<CcrVerifySummary, CcrVerifyError> {
|
||||||
|
content_info.validate().map_err(CcrDecodeError::Validate)?;
|
||||||
|
let state_hashes_ok = true;
|
||||||
|
let mut manifest_instances = 0usize;
|
||||||
|
let mut roa_payload_sets = 0usize;
|
||||||
|
let mut roa_vrp_count = 0usize;
|
||||||
|
let mut aspa_payload_sets = 0usize;
|
||||||
|
let mut trust_anchor_ski_count = 0usize;
|
||||||
|
let mut router_key_sets = 0usize;
|
||||||
|
let mut router_key_count = 0usize;
|
||||||
|
|
||||||
|
if let Some(mfts) = &content_info.content.mfts {
|
||||||
|
let payload_der = encode_manifest_state_payload_der(&mfts.mis)
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||||
|
if !verify_state_hash(&mfts.hash, &payload_der) {
|
||||||
|
return Err(CcrVerifyError::ManifestHashMismatch);
|
||||||
|
}
|
||||||
|
manifest_instances = mfts.mis.len();
|
||||||
|
}
|
||||||
|
if let Some(vrps) = &content_info.content.vrps {
|
||||||
|
let payload_der = encode_roa_payload_state_payload_der(&vrps.rps)
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||||
|
if !verify_state_hash(&vrps.hash, &payload_der) {
|
||||||
|
return Err(CcrVerifyError::RoaHashMismatch);
|
||||||
|
}
|
||||||
|
roa_payload_sets = vrps.rps.len();
|
||||||
|
roa_vrp_count = vrps
|
||||||
|
.rps
|
||||||
|
.iter()
|
||||||
|
.map(|set| count_roa_block_entries(&set.ip_addr_blocks))
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
if let Some(vaps) = &content_info.content.vaps {
|
||||||
|
let payload_der = encode_aspa_payload_state_payload_der(&vaps.aps)
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||||
|
if !verify_state_hash(&vaps.hash, &payload_der) {
|
||||||
|
return Err(CcrVerifyError::AspaHashMismatch);
|
||||||
|
}
|
||||||
|
aspa_payload_sets = vaps.aps.len();
|
||||||
|
}
|
||||||
|
if let Some(tas) = &content_info.content.tas {
|
||||||
|
verify_trust_anchor_state_hash(tas)?;
|
||||||
|
trust_anchor_ski_count = tas.skis.len();
|
||||||
|
}
|
||||||
|
if let Some(rks) = &content_info.content.rks {
|
||||||
|
verify_router_key_state_hash(rks)?;
|
||||||
|
router_key_sets = rks.rksets.len();
|
||||||
|
router_key_count = rks.rksets.iter().map(|set| set.router_keys.len()).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
let produced_at_rfc3339_utc = content_info
|
||||||
|
.content
|
||||||
|
.produced_at
|
||||||
|
.to_offset(time::UtcOffset::UTC)
|
||||||
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||||
|
|
||||||
|
Ok(CcrVerifySummary {
|
||||||
|
content_type_oid: content_info.content_type_oid.clone(),
|
||||||
|
version: content_info.content.version,
|
||||||
|
produced_at_rfc3339_utc,
|
||||||
|
state_hashes_ok,
|
||||||
|
manifest_instances,
|
||||||
|
roa_payload_sets,
|
||||||
|
roa_vrp_count,
|
||||||
|
aspa_payload_sets,
|
||||||
|
trust_anchor_ski_count,
|
||||||
|
router_key_sets,
|
||||||
|
router_key_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_against_report_json_path(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
report_json_path: &Path,
|
||||||
|
) -> Result<(), CcrVerifyError> {
|
||||||
|
let bytes = std::fs::read(report_json_path).map_err(|e| {
|
||||||
|
CcrVerifyError::ReportRead(report_json_path.display().to_string(), e.to_string())
|
||||||
|
})?;
|
||||||
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_slice(&bytes).map_err(|e| CcrVerifyError::ReportParse(e.to_string()))?;
|
||||||
|
|
||||||
|
let report_vrps = report_vrp_keys(&json)?;
|
||||||
|
let ccr_vrps = extract_vrp_rows(content_info)?;
|
||||||
|
let only_in_ccr = ccr_vrps.difference(&report_vrps).count();
|
||||||
|
let only_in_report = report_vrps.difference(&ccr_vrps).count();
|
||||||
|
if only_in_ccr != 0 || only_in_report != 0 {
|
||||||
|
return Err(CcrVerifyError::ReportVrpMismatch {
|
||||||
|
only_in_ccr,
|
||||||
|
only_in_report,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let report_aspas = report_aspa_keys(&json)?;
|
||||||
|
let ccr_aspas = ccr_aspa_keys(content_info)?;
|
||||||
|
let only_in_ccr = ccr_aspas.difference(&report_aspas).count();
|
||||||
|
let only_in_report = report_aspas.difference(&ccr_aspas).count();
|
||||||
|
if only_in_ccr != 0 || only_in_report != 0 {
|
||||||
|
return Err(CcrVerifyError::ReportAspaMismatch {
|
||||||
|
only_in_ccr,
|
||||||
|
only_in_report,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_against_vcir_store_path(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
db_path: &Path,
|
||||||
|
) -> Result<(), CcrVerifyError> {
|
||||||
|
let store = RocksStore::open(db_path).map_err(|e| CcrVerifyError::OpenStore(e.to_string()))?;
|
||||||
|
verify_against_vcir_store(content_info, &store)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_against_vcir_store(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
store: &RocksStore,
|
||||||
|
) -> Result<(), CcrVerifyError> {
|
||||||
|
let Some(mfts) = &content_info.content.mfts else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let vcirs = store
|
||||||
|
.list_vcirs()
|
||||||
|
.map_err(|e| CcrVerifyError::ListVcirs(e.to_string()))?;
|
||||||
|
let mut vcir_hashes = BTreeSet::new();
|
||||||
|
for vcir in vcirs {
|
||||||
|
if let Some(artifact) = vcir.related_artifacts.iter().find(|artifact| {
|
||||||
|
artifact.artifact_role == VcirArtifactRole::Manifest
|
||||||
|
&& artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
|
||||||
|
}) {
|
||||||
|
if let Ok(bytes) = hex::decode(&artifact.sha256) {
|
||||||
|
vcir_hashes.insert(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ccr_hashes = mfts
|
||||||
|
.mis
|
||||||
|
.iter()
|
||||||
|
.map(|mi| mi.hash.clone())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let only_in_ccr = ccr_hashes.difference(&vcir_hashes).count();
|
||||||
|
let only_in_vcir = vcir_hashes.difference(&ccr_hashes).count();
|
||||||
|
if only_in_ccr != 0 || only_in_vcir != 0 {
|
||||||
|
return Err(CcrVerifyError::VcirManifestMismatch {
|
||||||
|
only_in_ccr,
|
||||||
|
only_in_vcir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_trust_anchor_state_hash(state: &TrustAnchorState) -> Result<(), CcrVerifyError> {
|
||||||
|
let payload_der = encode_trust_anchor_state_payload_der(&state.skis)
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||||
|
if !verify_state_hash(&state.hash, &payload_der) {
|
||||||
|
return Err(CcrVerifyError::TrustAnchorHashMismatch);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_router_key_state_hash(state: &RouterKeyState) -> Result<(), CcrVerifyError> {
|
||||||
|
let payload_der = encode_router_key_state_payload_der(&state.rksets)
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||||
|
if !verify_state_hash(&state.hash, &payload_der) {
|
||||||
|
return Err(CcrVerifyError::RouterKeyHashMismatch);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_vrp_keys(
|
||||||
|
json: &serde_json::Value,
|
||||||
|
) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
let Some(items) = json.get("vrps").and_then(|v| v.as_array()) else {
|
||||||
|
return Ok(out);
|
||||||
|
};
|
||||||
|
for item in items {
|
||||||
|
let asn = item
|
||||||
|
.get("asn")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].asn missing".into()))?
|
||||||
|
as u32;
|
||||||
|
let prefix = item
|
||||||
|
.get("prefix")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].prefix missing".into()))?
|
||||||
|
.to_string();
|
||||||
|
let max_length = item
|
||||||
|
.get("max_length")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].max_length missing".into()))?
|
||||||
|
as u16;
|
||||||
|
out.insert((asn, prefix, max_length));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_aspa_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
let Some(items) = json.get("aspas").and_then(|v| v.as_array()) else {
|
||||||
|
return Ok(out);
|
||||||
|
};
|
||||||
|
for item in items {
|
||||||
|
let customer = item
|
||||||
|
.get("customer_as_id")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.ok_or_else(|| CcrVerifyError::ReportParse("aspas[].customer_as_id missing".into()))?
|
||||||
|
as u32;
|
||||||
|
let mut providers = item
|
||||||
|
.get("provider_as_ids")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.ok_or_else(|| CcrVerifyError::ReportParse("aspas[].provider_as_ids missing".into()))?
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
v.as_u64()
|
||||||
|
.ok_or_else(|| CcrVerifyError::ReportParse("provider_as_ids[] invalid".into()))
|
||||||
|
.map(|v| v as u32)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
providers.sort_unstable();
|
||||||
|
providers.dedup();
|
||||||
|
out.insert((customer, providers));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_vrp_rows(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
let Some(vrps) = &content_info.content.vrps else {
|
||||||
|
return Ok(out);
|
||||||
|
};
|
||||||
|
for set in &vrps.rps {
|
||||||
|
for block in &set.ip_addr_blocks {
|
||||||
|
let (afi, entries) = decode_roa_family_block(block)?;
|
||||||
|
for (prefix_len, addr_bytes, max_len) in entries {
|
||||||
|
let prefix = format_prefix(afi, &addr_bytes, prefix_len)?;
|
||||||
|
out.insert((set.as_id, prefix, max_len.unwrap_or(prefix_len as u16)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ccr_aspa_keys(
|
||||||
|
content_info: &CcrContentInfo,
|
||||||
|
) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
let Some(vaps) = &content_info.content.vaps else {
|
||||||
|
return Ok(out);
|
||||||
|
};
|
||||||
|
for set in &vaps.aps {
|
||||||
|
out.insert((set.customer_as_id, set.providers.clone()));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_roa_family_block(
|
||||||
|
block: &[u8],
|
||||||
|
) -> Result<(u16, Vec<(u8, Vec<u8>, Option<u16>)>), CcrVerifyError> {
|
||||||
|
let mut top = crate::data_model::common::DerReader::new(block);
|
||||||
|
let mut seq = top
|
||||||
|
.take_sequence()
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CcrVerifyError::Decode(CcrDecodeError::Parse(
|
||||||
|
"trailing bytes after ROAIPAddressFamily".into(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let afi_bytes = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||||
|
let afi = u16::from_be_bytes([afi_bytes[0], afi_bytes[1]]);
|
||||||
|
let mut addrs = seq
|
||||||
|
.take_sequence()
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
while !addrs.is_empty() {
|
||||||
|
let mut addr_seq = addrs
|
||||||
|
.take_sequence()
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||||
|
let (unused_bits, content) = addr_seq
|
||||||
|
.take_bit_string()
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||||
|
let prefix_len = (content.len() * 8) as u8 - unused_bits;
|
||||||
|
let max_len = if addr_seq.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
addr_seq
|
||||||
|
.take_uint_u64()
|
||||||
|
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?
|
||||||
|
as u16,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
entries.push((prefix_len, content.to_vec(), max_len));
|
||||||
|
}
|
||||||
|
Ok((afi, entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_prefix(afi: u16, addr_bytes: &[u8], prefix_len: u8) -> Result<String, CcrVerifyError> {
|
||||||
|
match afi {
|
||||||
|
1 => {
|
||||||
|
let mut full = [0u8; 4];
|
||||||
|
full[..addr_bytes.len()].copy_from_slice(addr_bytes);
|
||||||
|
Ok(format!("{}/{prefix_len}", std::net::Ipv4Addr::from(full)))
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let mut full = [0u8; 16];
|
||||||
|
full[..addr_bytes.len()].copy_from_slice(addr_bytes);
|
||||||
|
Ok(format!("{}/{prefix_len}", std::net::Ipv6Addr::from(full)))
|
||||||
|
}
|
||||||
|
other => Err(CcrVerifyError::Decode(CcrDecodeError::Parse(format!(
|
||||||
|
"unsupported AFI {other}"
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_roa_block_entries(blocks: &[Vec<u8>]) -> usize {
|
||||||
|
blocks
|
||||||
|
.iter()
|
||||||
|
.map(|block| {
|
||||||
|
decode_roa_family_block(block)
|
||||||
|
.map(|(_, entries)| entries.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ccr::build::{build_aspa_payload_state, build_roa_payload_state};
|
||||||
|
use crate::ccr::encode::{
|
||||||
|
encode_manifest_state_payload_der, encode_roa_payload_state_payload_der,
|
||||||
|
encode_router_key_state_payload_der, encode_trust_anchor_state_payload_der,
|
||||||
|
};
|
||||||
|
use crate::ccr::model::{
|
||||||
|
CcrDigestAlgorithm, ManifestInstance, ManifestState, RouterKey, RouterKeySet,
|
||||||
|
RpkiCanonicalCacheRepresentation,
|
||||||
|
};
|
||||||
|
use crate::data_model::common::BigUnsigned;
|
||||||
|
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||||
|
use crate::storage::{
|
||||||
|
PackTime, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
|
||||||
|
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry,
|
||||||
|
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||||
|
};
|
||||||
|
use crate::validation::objects::{AspaAttestation, Vrp};
|
||||||
|
|
||||||
|
fn sample_time() -> time::OffsetDateTime {
|
||||||
|
time::OffsetDateTime::parse(
|
||||||
|
"2026-03-24T00:00:00Z",
|
||||||
|
&time::format_description::well_known::Rfc3339,
|
||||||
|
)
|
||||||
|
.expect("time")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_content_info() -> CcrContentInfo {
|
||||||
|
let mis = vec![ManifestInstance {
|
||||||
|
hash: vec![0x10; 32],
|
||||||
|
size: 2048,
|
||||||
|
aki: vec![0x20; 20],
|
||||||
|
manifest_number: BigUnsigned { bytes_be: vec![1] },
|
||||||
|
this_update: sample_time(),
|
||||||
|
locations: vec![vec![0x30, 0x00]],
|
||||||
|
subordinates: vec![vec![0x30; 20]],
|
||||||
|
}];
|
||||||
|
let mfts = ManifestState {
|
||||||
|
most_recent_update: sample_time(),
|
||||||
|
hash: crate::ccr::compute_state_hash(&encode_manifest_state_payload_der(&mis).unwrap()),
|
||||||
|
mis,
|
||||||
|
};
|
||||||
|
let vrps = build_roa_payload_state(&[Vrp {
|
||||||
|
asn: 64496,
|
||||||
|
prefix: IpPrefix {
|
||||||
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len: 0,
|
||||||
|
addr: [0; 16],
|
||||||
|
},
|
||||||
|
max_length: 0,
|
||||||
|
}])
|
||||||
|
.expect("build roa state");
|
||||||
|
let vaps = build_aspa_payload_state(&[AspaAttestation {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![64497],
|
||||||
|
}])
|
||||||
|
.expect("build aspa state");
|
||||||
|
let skis = vec![vec![0x11; 20]];
|
||||||
|
let tas = TrustAnchorState {
|
||||||
|
hash: crate::ccr::compute_state_hash(
|
||||||
|
&encode_trust_anchor_state_payload_der(&skis).unwrap(),
|
||||||
|
),
|
||||||
|
skis,
|
||||||
|
};
|
||||||
|
let rksets = vec![RouterKeySet {
|
||||||
|
as_id: 64496,
|
||||||
|
router_keys: vec![RouterKey {
|
||||||
|
ski: vec![0x22; 20],
|
||||||
|
spki_der: vec![0x30, 0x00],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
let rks = RouterKeyState {
|
||||||
|
hash: crate::ccr::compute_state_hash(
|
||||||
|
&encode_router_key_state_payload_der(&rksets).unwrap(),
|
||||||
|
),
|
||||||
|
rksets,
|
||||||
|
};
|
||||||
|
CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||||
|
version: 0,
|
||||||
|
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||||
|
produced_at: sample_time(),
|
||||||
|
mfts: Some(mfts),
|
||||||
|
vrps: Some(vrps),
|
||||||
|
vaps: Some(vaps),
|
||||||
|
tas: Some(tas),
|
||||||
|
rks: Some(rks),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_detects_each_state_hash_mismatch() {
|
||||||
|
let mut ci = sample_content_info();
|
||||||
|
ci.content.vrps.as_mut().unwrap().hash[0] ^= 0x01;
|
||||||
|
assert!(matches!(
|
||||||
|
verify_content_info(&ci),
|
||||||
|
Err(CcrVerifyError::RoaHashMismatch)
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut ci = sample_content_info();
|
||||||
|
ci.content.vaps.as_mut().unwrap().hash[0] ^= 0x01;
|
||||||
|
assert!(matches!(
|
||||||
|
verify_content_info(&ci),
|
||||||
|
Err(CcrVerifyError::AspaHashMismatch)
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut ci = sample_content_info();
|
||||||
|
ci.content.tas.as_mut().unwrap().hash[0] ^= 0x01;
|
||||||
|
assert!(matches!(
|
||||||
|
verify_content_info(&ci),
|
||||||
|
Err(CcrVerifyError::TrustAnchorHashMismatch)
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut ci = sample_content_info();
|
||||||
|
ci.content.rks.as_mut().unwrap().hash[0] ^= 0x01;
|
||||||
|
assert!(matches!(
|
||||||
|
verify_content_info(&ci),
|
||||||
|
Err(CcrVerifyError::RouterKeyHashMismatch)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_against_report_json_accepts_matching_report_and_rejects_parse_errors() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let report = serde_json::json!({
|
||||||
|
"vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
|
||||||
|
"aspas": [{"customer_as_id": 64496, "provider_as_ids": [64497]}]
|
||||||
|
});
|
||||||
|
let report_path = td.path().join("report.json");
|
||||||
|
std::fs::write(&report_path, serde_json::to_vec(&report).unwrap()).unwrap();
|
||||||
|
verify_against_report_json_path(&sample_content_info(), &report_path)
|
||||||
|
.expect("matching report");
|
||||||
|
|
||||||
|
let bad_path = td.path().join("bad.json");
|
||||||
|
std::fs::write(&bad_path, b"not-json").unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
verify_against_report_json_path(&sample_content_info(), &bad_path),
|
||||||
|
Err(CcrVerifyError::ReportParse(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_against_report_json_rejects_missing_fields_and_aspa_mismatch() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let missing =
|
||||||
|
serde_json::json!({"vrps":[{"prefix":"0.0.0.0/0","max_length":0}],"aspas":[]});
|
||||||
|
let missing_path = td.path().join("missing.json");
|
||||||
|
std::fs::write(&missing_path, serde_json::to_vec(&missing).unwrap()).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
verify_against_report_json_path(&sample_content_info(), &missing_path),
|
||||||
|
Err(CcrVerifyError::ReportParse(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let mismatch = serde_json::json!({
|
||||||
|
"vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
|
||||||
|
"aspas": [{"customer_as_id": 64496, "provider_as_ids": [65000]}]
|
||||||
|
});
|
||||||
|
let mismatch_path = td.path().join("mismatch.json");
|
||||||
|
std::fs::write(&mismatch_path, serde_json::to_vec(&mismatch).unwrap()).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
verify_against_report_json_path(&sample_content_info(), &mismatch_path),
|
||||||
|
Err(CcrVerifyError::ReportAspaMismatch { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_against_vcir_store_rejects_mismatched_manifest_hashes() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let store = RocksStore::open(td.path()).expect("open db");
|
||||||
|
let vcir = ValidatedCaInstanceResult {
|
||||||
|
manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
|
||||||
|
parent_manifest_rsync_uri: None,
|
||||||
|
tal_id: "apnic".to_string(),
|
||||||
|
ca_subject_name: "CN=test".to_string(),
|
||||||
|
ca_ski: "11".repeat(20),
|
||||||
|
issuer_ski: "22".repeat(20),
|
||||||
|
last_successful_validation_time: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
current_manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
|
||||||
|
current_crl_rsync_uri: "rsync://example.test/current.crl".to_string(),
|
||||||
|
validated_manifest_meta: ValidatedManifestMeta {
|
||||||
|
validated_manifest_number: vec![1],
|
||||||
|
validated_manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
validated_manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
},
|
||||||
|
instance_gate: VcirInstanceGate {
|
||||||
|
manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
current_crl_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
self_ca_not_after: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
instance_effective_until: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
},
|
||||||
|
child_entries: vec![VcirChildEntry {
|
||||||
|
child_manifest_rsync_uri: "rsync://example.test/child.mft".to_string(),
|
||||||
|
child_cert_rsync_uri: "rsync://example.test/child.cer".to_string(),
|
||||||
|
child_cert_hash: "aa".repeat(32),
|
||||||
|
child_ski: "33".repeat(20),
|
||||||
|
child_rsync_base_uri: "rsync://example.test/".to_string(),
|
||||||
|
child_publication_point_rsync_uri: "rsync://example.test/".to_string(),
|
||||||
|
child_rrdp_notification_uri: None,
|
||||||
|
child_effective_ip_resources: None,
|
||||||
|
child_effective_as_resources: None,
|
||||||
|
accepted_at_validation_time: PackTime::from_utc_offset_datetime(sample_time()),
|
||||||
|
}],
|
||||||
|
local_outputs: Vec::new(),
|
||||||
|
related_artifacts: vec![VcirRelatedArtifact {
|
||||||
|
artifact_role: VcirArtifactRole::Manifest,
|
||||||
|
artifact_kind: VcirArtifactKind::Mft,
|
||||||
|
uri: Some("rsync://example.test/current.mft".to_string()),
|
||||||
|
sha256: "ff".repeat(32),
|
||||||
|
object_type: Some("mft".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
|
}],
|
||||||
|
summary: VcirSummary {
|
||||||
|
local_vrp_count: 0,
|
||||||
|
local_aspa_count: 0,
|
||||||
|
local_router_key_count: 0,
|
||||||
|
child_count: 1,
|
||||||
|
accepted_object_count: 1,
|
||||||
|
rejected_object_count: 0,
|
||||||
|
},
|
||||||
|
audit_summary: VcirAuditSummary {
|
||||||
|
failed_fetch_eligible: true,
|
||||||
|
last_failed_fetch_reason: None,
|
||||||
|
warning_count: 0,
|
||||||
|
audit_flags: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store.put_vcir(&vcir).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
verify_against_vcir_store(&sample_content_info(), &store),
|
||||||
|
Err(CcrVerifyError::VcirManifestMismatch { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_vrp_helpers_reject_bad_afi_and_count_invalid_block_as_zero() {
|
||||||
|
let block = vec![0x30, 0x08, 0x04, 0x02, 0x00, 0x63, 0x30, 0x02, 0x03, 0x00];
|
||||||
|
let ci = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||||
|
version: 0,
|
||||||
|
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||||
|
produced_at: sample_time(),
|
||||||
|
mfts: None,
|
||||||
|
vrps: Some(crate::ccr::model::RoaPayloadState {
|
||||||
|
rps: vec![crate::ccr::model::RoaPayloadSet {
|
||||||
|
as_id: 64496,
|
||||||
|
ip_addr_blocks: vec![block.clone()],
|
||||||
|
}],
|
||||||
|
hash: crate::ccr::compute_state_hash(
|
||||||
|
&encode_roa_payload_state_payload_der(&[crate::ccr::model::RoaPayloadSet {
|
||||||
|
as_id: 64496,
|
||||||
|
ip_addr_blocks: vec![block],
|
||||||
|
}])
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
vaps: None,
|
||||||
|
tas: None,
|
||||||
|
rks: None,
|
||||||
|
});
|
||||||
|
assert!(matches!(
|
||||||
|
extract_vrp_rows(&ci),
|
||||||
|
Err(CcrVerifyError::Decode(_))
|
||||||
|
));
|
||||||
|
let bad_count = count_roa_block_entries(&[vec![0x04, 0x00]]);
|
||||||
|
assert_eq!(bad_count, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/cir/decode.rs
Normal file
170
src/cir/decode.rs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
use crate::cir::model::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
|
||||||
|
};
|
||||||
|
use crate::data_model::common::DerReader;
|
||||||
|
use crate::data_model::oid::{OID_SHA256, OID_SHA256_RAW};
|
||||||
|
use der_parser::der::parse_der_oid;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CirDecodeError {
|
||||||
|
#[error("DER parse error: {0}")]
|
||||||
|
Parse(String),
|
||||||
|
|
||||||
|
#[error("unexpected CIR version: expected {expected}, got {actual}")]
|
||||||
|
UnexpectedVersion { expected: u32, actual: u32 },
|
||||||
|
|
||||||
|
#[error("unexpected digest algorithm OID: expected {expected}, got {actual}")]
|
||||||
|
UnexpectedDigestAlgorithm {
|
||||||
|
expected: &'static str,
|
||||||
|
actual: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("CIR model validation failed after decode: {0}")]
|
||||||
|
Validate(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CirDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CirDecodeError::Parse("trailing bytes after CIR".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = seq.take_uint_u64().map_err(CirDecodeError::Parse)? as u32;
|
||||||
|
if version != CIR_VERSION_V1 {
|
||||||
|
return Err(CirDecodeError::UnexpectedVersion {
|
||||||
|
expected: CIR_VERSION_V1,
|
||||||
|
actual: version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let hash_alg = decode_hash_alg(seq.take_tag(0x06).map_err(CirDecodeError::Parse)?)?;
|
||||||
|
let validation_time =
|
||||||
|
parse_generalized_time(seq.take_tag(0x18).map_err(CirDecodeError::Parse)?)?;
|
||||||
|
|
||||||
|
let objects_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
|
||||||
|
let mut objects_reader = DerReader::new(objects_der);
|
||||||
|
let mut objects = Vec::new();
|
||||||
|
while !objects_reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = objects_reader
|
||||||
|
.take_any_full()
|
||||||
|
.map_err(CirDecodeError::Parse)?;
|
||||||
|
objects.push(decode_object(full)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tals_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
|
||||||
|
let mut tals_reader = DerReader::new(tals_der);
|
||||||
|
let mut tals = Vec::new();
|
||||||
|
while !tals_reader.is_empty() {
|
||||||
|
let (_tag, full, _value) = tals_reader.take_any_full().map_err(CirDecodeError::Parse)?;
|
||||||
|
tals.push(decode_tal(full)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CirDecodeError::Parse("trailing fields in CIR".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version,
|
||||||
|
hash_alg,
|
||||||
|
validation_time,
|
||||||
|
objects,
|
||||||
|
tals,
|
||||||
|
};
|
||||||
|
cir.validate().map_err(CirDecodeError::Validate)?;
|
||||||
|
Ok(cir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_hash_alg(raw_body: &[u8]) -> Result<CirHashAlgorithm, CirDecodeError> {
|
||||||
|
if raw_body != OID_SHA256_RAW {
|
||||||
|
return Err(CirDecodeError::UnexpectedDigestAlgorithm {
|
||||||
|
expected: OID_SHA256,
|
||||||
|
actual: oid_string(raw_body)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(CirHashAlgorithm::Sha256)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_object(der: &[u8]) -> Result<CirObject, CirDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CirDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CirDecodeError::Parse(
|
||||||
|
"trailing bytes after CirObject".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let rsync_uri = std::str::from_utf8(seq.take_tag(0x16).map_err(CirDecodeError::Parse)?)
|
||||||
|
.map_err(|e| CirDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_string();
|
||||||
|
let sha256 = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CirDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CirDecodeError::Parse("trailing fields in CirObject".into()));
|
||||||
|
}
|
||||||
|
Ok(CirObject { rsync_uri, sha256 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_tal(der: &[u8]) -> Result<CirTal, CirDecodeError> {
|
||||||
|
let mut top = DerReader::new(der);
|
||||||
|
let mut seq = top.take_sequence().map_err(CirDecodeError::Parse)?;
|
||||||
|
if !top.is_empty() {
|
||||||
|
return Err(CirDecodeError::Parse("trailing bytes after CirTal".into()));
|
||||||
|
}
|
||||||
|
let tal_uri = std::str::from_utf8(seq.take_tag(0x16).map_err(CirDecodeError::Parse)?)
|
||||||
|
.map_err(|e| CirDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_string();
|
||||||
|
let tal_bytes = seq
|
||||||
|
.take_octet_string()
|
||||||
|
.map_err(CirDecodeError::Parse)?
|
||||||
|
.to_vec();
|
||||||
|
if !seq.is_empty() {
|
||||||
|
return Err(CirDecodeError::Parse("trailing fields in CirTal".into()));
|
||||||
|
}
|
||||||
|
Ok(CirTal { tal_uri, tal_bytes })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oid_string(raw_body: &[u8]) -> Result<String, CirDecodeError> {
|
||||||
|
let der = {
|
||||||
|
let mut out = Vec::with_capacity(raw_body.len() + 2);
|
||||||
|
out.push(0x06);
|
||||||
|
if raw_body.len() < 0x80 {
|
||||||
|
out.push(raw_body.len() as u8);
|
||||||
|
} else {
|
||||||
|
return Err(CirDecodeError::Parse("OID too long".into()));
|
||||||
|
}
|
||||||
|
out.extend_from_slice(raw_body);
|
||||||
|
out
|
||||||
|
};
|
||||||
|
let (_rem, oid) = parse_der_oid(&der).map_err(|e| CirDecodeError::Parse(e.to_string()))?;
|
||||||
|
let oid = oid
|
||||||
|
.as_oid_val()
|
||||||
|
.map_err(|e| CirDecodeError::Parse(e.to_string()))?;
|
||||||
|
Ok(oid.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_generalized_time(bytes: &[u8]) -> Result<time::OffsetDateTime, CirDecodeError> {
|
||||||
|
let s = std::str::from_utf8(bytes).map_err(|e| CirDecodeError::Parse(e.to_string()))?;
|
||||||
|
if s.len() != 15 || !s.ends_with('Z') {
|
||||||
|
return Err(CirDecodeError::Parse(
|
||||||
|
"GeneralizedTime must be YYYYMMDDHHMMSSZ".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let parse = |range: std::ops::Range<usize>| -> Result<u32, CirDecodeError> {
|
||||||
|
s[range]
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|e| CirDecodeError::Parse(e.to_string()))
|
||||||
|
};
|
||||||
|
let year = parse(0..4)? as i32;
|
||||||
|
let month = parse(4..6)? as u8;
|
||||||
|
let day = parse(6..8)? as u8;
|
||||||
|
let hour = parse(8..10)? as u8;
|
||||||
|
let minute = parse(10..12)? as u8;
|
||||||
|
let second = parse(12..14)? as u8;
|
||||||
|
let month = time::Month::try_from(month).map_err(|e| CirDecodeError::Parse(e.to_string()))?;
|
||||||
|
let date = time::Date::from_calendar_date(year, month, day)
|
||||||
|
.map_err(|e| CirDecodeError::Parse(e.to_string()))?;
|
||||||
|
let timev = time::Time::from_hms(hour, minute, second)
|
||||||
|
.map_err(|e| CirDecodeError::Parse(e.to_string()))?;
|
||||||
|
Ok(time::PrimitiveDateTime::new(date, timev).assume_utc())
|
||||||
|
}
|
||||||
147
src/cir/encode.rs
Normal file
147
src/cir/encode.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use crate::cir::model::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
|
||||||
|
};
|
||||||
|
use crate::data_model::oid::OID_SHA256_RAW;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CirEncodeError {
|
||||||
|
#[error("CIR model validation failed: {0}")]
|
||||||
|
Validate(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result<Vec<u8>, CirEncodeError> {
|
||||||
|
cir.validate().map_err(CirEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_integer_u32(cir.version),
|
||||||
|
encode_oid(match cir.hash_alg {
|
||||||
|
CirHashAlgorithm::Sha256 => OID_SHA256_RAW,
|
||||||
|
}),
|
||||||
|
encode_generalized_time(cir.validation_time),
|
||||||
|
encode_sequence(
|
||||||
|
&cir.objects
|
||||||
|
.iter()
|
||||||
|
.map(encode_object)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
),
|
||||||
|
encode_sequence(
|
||||||
|
&cir.tals
|
||||||
|
.iter()
|
||||||
|
.map(encode_tal)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_object(object: &CirObject) -> Result<Vec<u8>, CirEncodeError> {
|
||||||
|
object.validate().map_err(CirEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_ia5_string(object.rsync_uri.as_bytes()),
|
||||||
|
encode_octet_string(&object.sha256),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_tal(tal: &CirTal) -> Result<Vec<u8>, CirEncodeError> {
|
||||||
|
tal.validate().map_err(CirEncodeError::Validate)?;
|
||||||
|
Ok(encode_sequence(&[
|
||||||
|
encode_ia5_string(tal.tal_uri.as_bytes()),
|
||||||
|
encode_octet_string(&tal.tal_bytes),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_generalized_time(t: time::OffsetDateTime) -> Vec<u8> {
|
||||||
|
let t = t.to_offset(time::UtcOffset::UTC);
|
||||||
|
let s = format!(
|
||||||
|
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
|
||||||
|
t.year(),
|
||||||
|
u8::from(t.month()),
|
||||||
|
t.day(),
|
||||||
|
t.hour(),
|
||||||
|
t.minute(),
|
||||||
|
t.second()
|
||||||
|
);
|
||||||
|
encode_tlv(0x18, s.into_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_u32(v: u32) -> Vec<u8> {
|
||||||
|
encode_integer_bytes(unsigned_integer_bytes(v as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_integer_bytes(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
bytes.push(0);
|
||||||
|
}
|
||||||
|
if bytes[0] & 0x80 != 0 {
|
||||||
|
bytes.insert(0, 0);
|
||||||
|
}
|
||||||
|
encode_tlv(0x02, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unsigned_integer_bytes(v: u64) -> Vec<u8> {
|
||||||
|
if v == 0 {
|
||||||
|
return vec![0];
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut n = v;
|
||||||
|
while n > 0 {
|
||||||
|
out.push((n & 0xFF) as u8);
|
||||||
|
n >>= 8;
|
||||||
|
}
|
||||||
|
out.reverse();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_oid(raw_body: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0x06, raw_body.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_ia5_string(bytes: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0x16, bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||||
|
encode_tlv(0x04, bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
for element in elements {
|
||||||
|
body.extend_from_slice(element);
|
||||||
|
}
|
||||||
|
encode_tlv(0x30, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(1 + encoded_len_len(value.len()) + value.len());
|
||||||
|
out.push(tag);
|
||||||
|
encode_len_into(value.len(), &mut out);
|
||||||
|
out.extend_from_slice(&value);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encoded_len_len(len: usize) -> usize {
|
||||||
|
if len < 0x80 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
1 + len.to_be_bytes().iter().skip_while(|&&b| b == 0).count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_len_into(len: usize, out: &mut Vec<u8>) {
|
||||||
|
if len < 0x80 {
|
||||||
|
out.push(len as u8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bytes = len.to_be_bytes();
|
||||||
|
let first_non_zero = bytes
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b != 0)
|
||||||
|
.unwrap_or(bytes.len() - 1);
|
||||||
|
let len_bytes = &bytes[first_non_zero..];
|
||||||
|
out.push(0x80 | (len_bytes.len() as u8));
|
||||||
|
out.extend_from_slice(len_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const _: () = {
|
||||||
|
let _ = CIR_VERSION_V1;
|
||||||
|
};
|
||||||
544
src/cir/export.rs
Normal file
544
src/cir/export.rs
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::audit::{AuditObjectResult, PublicationPointAudit};
|
||||||
|
use crate::blob_store::RawObjectStore;
|
||||||
|
use crate::cir::encode::{CirEncodeError, encode_cir};
|
||||||
|
use crate::cir::model::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
|
||||||
|
};
|
||||||
|
use crate::cir::static_pool::{
|
||||||
|
CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store,
|
||||||
|
write_bytes_to_static_pool,
|
||||||
|
};
|
||||||
|
use crate::data_model::ta::TrustAnchor;
|
||||||
|
use crate::storage::{RepositoryViewState, RocksStore};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirExportTiming {
|
||||||
|
pub build_cir_ms: u64,
|
||||||
|
pub static_pool_ms: u64,
|
||||||
|
pub write_cir_ms: u64,
|
||||||
|
pub total_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CirExportError {
|
||||||
|
#[error("list repository_view entries failed: {0}")]
|
||||||
|
ListRepositoryView(String),
|
||||||
|
|
||||||
|
#[error("CIR TAL URI must be http(s), got: {0}")]
|
||||||
|
InvalidTalUri(String),
|
||||||
|
|
||||||
|
#[error("TAL does not contain any rsync TA URI; CIR replay scheme A requires one")]
|
||||||
|
MissingTaRsyncUri,
|
||||||
|
|
||||||
|
#[error("CIR model validation failed: {0}")]
|
||||||
|
Validate(String),
|
||||||
|
|
||||||
|
#[error("encode CIR failed: {0}")]
|
||||||
|
Encode(#[from] CirEncodeError),
|
||||||
|
|
||||||
|
#[error("static pool export failed: {0}")]
|
||||||
|
StaticPool(#[from] CirStaticPoolError),
|
||||||
|
|
||||||
|
#[error("write CIR file failed: {0}: {1}")]
|
||||||
|
Write(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CirObjectPoolExportSummary {
|
||||||
|
Static(CirStaticPoolExportSummary),
|
||||||
|
RawStore(CirRawStoreExportSummary),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirRawStoreExportSummary {
|
||||||
|
pub unique_hashes: usize,
|
||||||
|
pub written_entries: usize,
|
||||||
|
pub reused_entries: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirExportSummary {
|
||||||
|
pub object_count: usize,
|
||||||
|
pub tal_count: usize,
|
||||||
|
pub object_pool: CirObjectPoolExportSummary,
|
||||||
|
pub timing: CirExportTiming,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_cir_from_run(
|
||||||
|
store: &RocksStore,
|
||||||
|
trust_anchor: &TrustAnchor,
|
||||||
|
tal_uri: &str,
|
||||||
|
validation_time: time::OffsetDateTime,
|
||||||
|
publication_points: &[PublicationPointAudit],
|
||||||
|
) -> Result<CanonicalInputRepresentation, CirExportError> {
|
||||||
|
if !(tal_uri.starts_with("https://") || tal_uri.starts_with("http://")) {
|
||||||
|
return Err(CirExportError::InvalidTalUri(tal_uri.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = store
|
||||||
|
.list_repository_view_entries_with_prefix("rsync://")
|
||||||
|
.map_err(|e| CirExportError::ListRepositoryView(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut objects: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
for entry in entries {
|
||||||
|
if matches!(
|
||||||
|
entry.state,
|
||||||
|
RepositoryViewState::Present | RepositoryViewState::Replaced
|
||||||
|
) && let Some(hash) = entry.current_hash
|
||||||
|
{
|
||||||
|
objects.insert(entry.rsync_uri, hash.to_ascii_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CIR must describe the actual input world used by validation. When a
|
||||||
|
// publication point falls back to the latest validated current instance,
|
||||||
|
// repository_view may not contain the reused manifest/object set. Pull
|
||||||
|
// those object hashes from the audit so replay can reconstruct the same
|
||||||
|
// world state.
|
||||||
|
for pp in publication_points {
|
||||||
|
if pp.source != "vcir_current_instance" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for obj in &pp.objects {
|
||||||
|
if obj.result != AuditObjectResult::Ok {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !obj.rsync_uri.starts_with("rsync://") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
objects.insert(obj.rsync_uri.clone(), obj.sha256_hex.to_ascii_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ta_hash = ta_sha256_hex(&trust_anchor.ta_certificate.raw_der);
|
||||||
|
let mut saw_rsync_uri = false;
|
||||||
|
for uri in &trust_anchor.tal.ta_uris {
|
||||||
|
if uri.scheme() == "rsync" {
|
||||||
|
saw_rsync_uri = true;
|
||||||
|
objects.insert(uri.as_str().to_string(), ta_hash.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !saw_rsync_uri {
|
||||||
|
return Err(CirExportError::MissingTaRsyncUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: validation_time.to_offset(time::UtcOffset::UTC),
|
||||||
|
objects: objects
|
||||||
|
.into_iter()
|
||||||
|
.map(|(rsync_uri, sha256_hex)| CirObject {
|
||||||
|
rsync_uri,
|
||||||
|
sha256: hex::decode(sha256_hex).expect("validated hex"),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: tal_uri.to_string(),
|
||||||
|
tal_bytes: trust_anchor.tal.raw.clone(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
cir.validate().map_err(CirExportError::Validate)?;
|
||||||
|
Ok(cir)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_cir_file(
|
||||||
|
path: &Path,
|
||||||
|
cir: &CanonicalInputRepresentation,
|
||||||
|
) -> Result<(), CirExportError> {
|
||||||
|
let der = encode_cir(cir)?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| CirExportError::Write(path.display().to_string(), e.to_string()))?;
|
||||||
|
}
|
||||||
|
std::fs::write(path, der)
|
||||||
|
.map_err(|e| CirExportError::Write(path.display().to_string(), e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_cir_static_pool(
|
||||||
|
store: &RocksStore,
|
||||||
|
static_root: &Path,
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
cir: &CanonicalInputRepresentation,
|
||||||
|
trust_anchor: &TrustAnchor,
|
||||||
|
) -> Result<CirStaticPoolExportSummary, CirExportError> {
|
||||||
|
let ta_hash = ta_sha256_hex(&trust_anchor.ta_certificate.raw_der);
|
||||||
|
let hashes = cir
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.map(|item| hex::encode(&item.sha256))
|
||||||
|
.filter(|hash| hash != &ta_hash)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut summary = export_hashes_from_store(store, static_root, capture_date_utc, &hashes)?;
|
||||||
|
|
||||||
|
let ta_result = write_bytes_to_static_pool(
|
||||||
|
static_root,
|
||||||
|
capture_date_utc,
|
||||||
|
&ta_hash,
|
||||||
|
&trust_anchor.ta_certificate.raw_der,
|
||||||
|
)?;
|
||||||
|
let mut unique = hashes.iter().cloned().collect::<BTreeSet<_>>();
|
||||||
|
unique.insert(ta_hash.clone());
|
||||||
|
summary.unique_hashes = unique.len();
|
||||||
|
if ta_result.written {
|
||||||
|
summary.written_files += 1;
|
||||||
|
} else {
|
||||||
|
summary.reused_files += 1;
|
||||||
|
}
|
||||||
|
Ok(summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_cir_raw_store(
|
||||||
|
store: &RocksStore,
|
||||||
|
raw_store_path: &Path,
|
||||||
|
cir: &CanonicalInputRepresentation,
|
||||||
|
trust_anchor: &TrustAnchor,
|
||||||
|
) -> Result<CirRawStoreExportSummary, CirExportError> {
|
||||||
|
let ta_hash = ta_sha256_hex(&trust_anchor.ta_certificate.raw_der);
|
||||||
|
let unique: BTreeSet<String> = cir
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.map(|item| hex::encode(&item.sha256))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut written_entries = 0usize;
|
||||||
|
let mut reused_entries = 0usize;
|
||||||
|
for sha256_hex in &unique {
|
||||||
|
if store
|
||||||
|
.get_raw_entry(sha256_hex)
|
||||||
|
.map_err(|e| CirExportError::Write(raw_store_path.display().to_string(), e.to_string()))?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
reused_entries += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if sha256_hex == &ta_hash {
|
||||||
|
let mut entry =
|
||||||
|
crate::storage::RawByHashEntry::from_bytes(ta_hash.clone(), trust_anchor.ta_certificate.raw_der.clone());
|
||||||
|
entry.object_type = Some("cer".to_string());
|
||||||
|
for object in &cir.objects {
|
||||||
|
if hex::encode(&object.sha256) == ta_hash {
|
||||||
|
entry.origin_uris.push(object.rsync_uri.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store
|
||||||
|
.put_raw_by_hash_entry(&entry)
|
||||||
|
.map_err(|e| CirExportError::Write(raw_store_path.display().to_string(), e.to_string()))?;
|
||||||
|
written_entries += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Err(CirExportError::Write(
|
||||||
|
raw_store_path.display().to_string(),
|
||||||
|
format!("raw store missing object for sha256={sha256_hex}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CirRawStoreExportSummary {
|
||||||
|
unique_hashes: unique.len(),
|
||||||
|
written_entries,
|
||||||
|
reused_entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_cir_from_run(
|
||||||
|
store: &RocksStore,
|
||||||
|
trust_anchor: &TrustAnchor,
|
||||||
|
tal_uri: &str,
|
||||||
|
validation_time: time::OffsetDateTime,
|
||||||
|
publication_points: &[PublicationPointAudit],
|
||||||
|
cir_out: &Path,
|
||||||
|
static_root: Option<&Path>,
|
||||||
|
raw_store_path: Option<&Path>,
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
) -> Result<CirExportSummary, CirExportError> {
|
||||||
|
let backend_count = static_root.is_some() as u8 + raw_store_path.is_some() as u8;
|
||||||
|
match backend_count {
|
||||||
|
1 => {}
|
||||||
|
_ => {
|
||||||
|
return Err(CirExportError::Validate(
|
||||||
|
"must specify exactly one CIR object pool backend".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let total_started = std::time::Instant::now();
|
||||||
|
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let cir = build_cir_from_run(
|
||||||
|
store,
|
||||||
|
trust_anchor,
|
||||||
|
tal_uri,
|
||||||
|
validation_time,
|
||||||
|
publication_points,
|
||||||
|
)?;
|
||||||
|
let build_cir_ms = started.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let object_pool = if let Some(static_root) = static_root {
|
||||||
|
CirObjectPoolExportSummary::Static(export_cir_static_pool(
|
||||||
|
store,
|
||||||
|
static_root,
|
||||||
|
capture_date_utc,
|
||||||
|
&cir,
|
||||||
|
trust_anchor,
|
||||||
|
)?)
|
||||||
|
} else {
|
||||||
|
CirObjectPoolExportSummary::RawStore(export_cir_raw_store(
|
||||||
|
store,
|
||||||
|
raw_store_path.expect("validated"),
|
||||||
|
&cir,
|
||||||
|
trust_anchor,
|
||||||
|
)?)
|
||||||
|
};
|
||||||
|
let static_pool_ms = started.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
write_cir_file(cir_out, &cir)?;
|
||||||
|
let write_cir_ms = started.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
Ok(CirExportSummary {
|
||||||
|
object_count: cir.objects.len(),
|
||||||
|
tal_count: cir.tals.len(),
|
||||||
|
object_pool,
|
||||||
|
timing: CirExportTiming {
|
||||||
|
build_cir_ms,
|
||||||
|
static_pool_ms,
|
||||||
|
write_cir_ms,
|
||||||
|
total_ms: total_started.elapsed().as_millis() as u64,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ta_sha256_hex(bytes: &[u8]) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
hex::encode(Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cir::decode::decode_cir;
|
||||||
|
use crate::cir::static_pool_path;
|
||||||
|
use crate::data_model::ta::TrustAnchor;
|
||||||
|
use crate::data_model::tal::Tal;
|
||||||
|
use crate::storage::{RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore};
|
||||||
|
|
||||||
|
fn sample_time() -> time::OffsetDateTime {
|
||||||
|
time::OffsetDateTime::parse(
|
||||||
|
"2026-04-07T12:34:56Z",
|
||||||
|
&time::format_description::well_known::Rfc3339,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_date() -> time::Date {
|
||||||
|
time::Date::from_calendar_date(2026, time::Month::April, 7).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_trust_anchor() -> TrustAnchor {
|
||||||
|
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let tal_bytes =
|
||||||
|
std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal")).unwrap();
|
||||||
|
let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).unwrap();
|
||||||
|
let tal = Tal::decode_bytes(&tal_bytes).unwrap();
|
||||||
|
TrustAnchor::bind_der(tal, &ta_der, None).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256_hex(bytes: &[u8]) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
hex::encode(Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cir_from_run_collects_repository_view_and_tal() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let store = RocksStore::open(td.path()).unwrap();
|
||||||
|
let bytes = b"object-a".to_vec();
|
||||||
|
let hash = sha256_hex(&bytes);
|
||||||
|
let mut raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone());
|
||||||
|
raw.origin_uris
|
||||||
|
.push("rsync://example.test/repo/a.cer".into());
|
||||||
|
store.put_raw_by_hash_entry(&raw).unwrap();
|
||||||
|
store
|
||||||
|
.put_repository_view_entry(&RepositoryViewEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/a.cer".to_string(),
|
||||||
|
current_hash: Some(hash),
|
||||||
|
repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
|
||||||
|
object_type: Some("cer".to_string()),
|
||||||
|
state: RepositoryViewState::Present,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ta = sample_trust_anchor();
|
||||||
|
let cir = build_cir_from_run(
|
||||||
|
&store,
|
||||||
|
&ta,
|
||||||
|
"https://example.test/root.tal",
|
||||||
|
sample_time(),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.expect("build cir");
|
||||||
|
assert_eq!(cir.version, CIR_VERSION_V1);
|
||||||
|
assert_eq!(cir.tals.len(), 1);
|
||||||
|
assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal");
|
||||||
|
assert!(
|
||||||
|
cir.objects
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cir.objects
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.rsync_uri.contains("apnic-rpki-root-iana-origin.cer"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_cir_from_run_writes_der_and_static_pool() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let store_dir = td.path().join("db");
|
||||||
|
let out_dir = td.path().join("out");
|
||||||
|
let static_root = td.path().join("static");
|
||||||
|
let store = RocksStore::open(&store_dir).unwrap();
|
||||||
|
|
||||||
|
let bytes = b"object-b".to_vec();
|
||||||
|
let hash = sha256_hex(&bytes);
|
||||||
|
let mut raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone());
|
||||||
|
raw.origin_uris
|
||||||
|
.push("rsync://example.test/repo/b.roa".into());
|
||||||
|
store.put_raw_by_hash_entry(&raw).unwrap();
|
||||||
|
store
|
||||||
|
.put_repository_view_entry(&RepositoryViewEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/b.roa".to_string(),
|
||||||
|
current_hash: Some(hash.clone()),
|
||||||
|
repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
|
||||||
|
object_type: Some("roa".to_string()),
|
||||||
|
state: RepositoryViewState::Present,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ta = sample_trust_anchor();
|
||||||
|
let cir_path = out_dir.join("example.cir");
|
||||||
|
let summary = export_cir_from_run(
|
||||||
|
&store,
|
||||||
|
&ta,
|
||||||
|
"https://example.test/root.tal",
|
||||||
|
sample_time(),
|
||||||
|
&[],
|
||||||
|
&cir_path,
|
||||||
|
Some(&static_root),
|
||||||
|
None,
|
||||||
|
sample_date(),
|
||||||
|
)
|
||||||
|
.expect("export cir");
|
||||||
|
assert_eq!(summary.tal_count, 1);
|
||||||
|
assert!(summary.object_count >= 2);
|
||||||
|
match summary.object_pool {
|
||||||
|
CirObjectPoolExportSummary::Static(_) => {}
|
||||||
|
other => panic!("unexpected backend: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let der = std::fs::read(&cir_path).unwrap();
|
||||||
|
let cir = decode_cir(&der).unwrap();
|
||||||
|
assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal");
|
||||||
|
|
||||||
|
let object_path = static_pool_path(&static_root, sample_date(), &hash).unwrap();
|
||||||
|
assert_eq!(std::fs::read(object_path).unwrap(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_cir_from_run_uses_raw_store_backend_without_pool_export() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let store_dir = td.path().join("db");
|
||||||
|
let raw_store = td.path().join("raw-store.db");
|
||||||
|
let out_dir = td.path().join("out");
|
||||||
|
let store = RocksStore::open_with_external_raw_store(&store_dir, &raw_store).unwrap();
|
||||||
|
|
||||||
|
let bytes = b"object-d".to_vec();
|
||||||
|
let hash = sha256_hex(&bytes);
|
||||||
|
let mut raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone());
|
||||||
|
raw.origin_uris
|
||||||
|
.push("rsync://example.test/repo/d.roa".into());
|
||||||
|
store.put_raw_by_hash_entry(&raw).unwrap();
|
||||||
|
store
|
||||||
|
.put_repository_view_entry(&RepositoryViewEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/d.roa".to_string(),
|
||||||
|
current_hash: Some(hash.clone()),
|
||||||
|
repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
|
||||||
|
object_type: Some("roa".to_string()),
|
||||||
|
state: RepositoryViewState::Present,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ta = sample_trust_anchor();
|
||||||
|
let cir_path = out_dir.join("example.cir");
|
||||||
|
let summary = export_cir_from_run(
|
||||||
|
&store,
|
||||||
|
&ta,
|
||||||
|
"https://example.test/root.tal",
|
||||||
|
sample_time(),
|
||||||
|
&[],
|
||||||
|
&cir_path,
|
||||||
|
None,
|
||||||
|
Some(&raw_store),
|
||||||
|
sample_date(),
|
||||||
|
)
|
||||||
|
.expect("export cir to raw store");
|
||||||
|
match summary.object_pool {
|
||||||
|
CirObjectPoolExportSummary::RawStore(ref s) => assert!(s.unique_hashes >= 2),
|
||||||
|
other => panic!("unexpected backend: {other:?}"),
|
||||||
|
}
|
||||||
|
assert!(raw_store.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cir_from_run_includes_vcir_current_instance_objects_from_audit() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let store = RocksStore::open(td.path()).unwrap();
|
||||||
|
let ta = sample_trust_anchor();
|
||||||
|
|
||||||
|
let mut pp = PublicationPointAudit {
|
||||||
|
source: "vcir_current_instance".to_string(),
|
||||||
|
..PublicationPointAudit::default()
|
||||||
|
};
|
||||||
|
pp.objects.push(crate::audit::ObjectAuditEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/fallback.mft".to_string(),
|
||||||
|
sha256_hex: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
.to_string(),
|
||||||
|
kind: crate::audit::AuditObjectKind::Manifest,
|
||||||
|
result: crate::audit::AuditObjectResult::Ok,
|
||||||
|
detail: None,
|
||||||
|
});
|
||||||
|
pp.objects.push(crate::audit::ObjectAuditEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/fallback.roa".to_string(),
|
||||||
|
sha256_hex: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
.to_string(),
|
||||||
|
kind: crate::audit::AuditObjectKind::Roa,
|
||||||
|
result: crate::audit::AuditObjectResult::Ok,
|
||||||
|
detail: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let cir = build_cir_from_run(
|
||||||
|
&store,
|
||||||
|
&ta,
|
||||||
|
"https://example.test/root.tal",
|
||||||
|
sample_time(),
|
||||||
|
&[pp],
|
||||||
|
)
|
||||||
|
.expect("build cir");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cir.objects
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.mft")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cir.objects
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.roa")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
710
src/cir/materialize.rs
Normal file
710
src/cir/materialize.rs
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::blob_store::{ExternalRawStoreDb, RawObjectStore};
|
||||||
|
use crate::cir::model::CanonicalInputRepresentation;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CirMaterializeError {
|
||||||
|
#[error("invalid rsync URI: {0}")]
|
||||||
|
InvalidRsyncUri(String),
|
||||||
|
|
||||||
|
#[error("rsync URI must reference a file object, got directory-like URI: {0}")]
|
||||||
|
DirectoryLikeRsyncUri(String),
|
||||||
|
|
||||||
|
#[error("create mirror root failed: {path}: {detail}")]
|
||||||
|
CreateMirrorRoot { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("remove mirror root failed: {path}: {detail}")]
|
||||||
|
RemoveMirrorRoot { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("create parent directory failed: {path}: {detail}")]
|
||||||
|
CreateParent { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("remove existing target failed: {path}: {detail}")]
|
||||||
|
RemoveExistingTarget { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("static object not found for sha256={sha256_hex}")]
|
||||||
|
MissingStaticObject { sha256_hex: String },
|
||||||
|
|
||||||
|
#[error("link target failed: {src} -> {dst}: {detail}")]
|
||||||
|
Link {
|
||||||
|
src: String,
|
||||||
|
dst: String,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("copy target failed: {src} -> {dst}: {detail}")]
|
||||||
|
Copy {
|
||||||
|
src: String,
|
||||||
|
dst: String,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("mirror tree mismatch after materialize: {0}")]
|
||||||
|
TreeMismatch(String),
|
||||||
|
|
||||||
|
#[error("open raw store failed: {path}: {detail}")]
|
||||||
|
OpenRawStore { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("raw object not found for sha256={sha256_hex}")]
|
||||||
|
MissingRawStoreObject { sha256_hex: String },
|
||||||
|
|
||||||
|
#[error("read raw store failed for sha256={sha256_hex}: {detail}")]
|
||||||
|
ReadRawStore { sha256_hex: String, detail: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirMaterializeSummary {
|
||||||
|
pub object_count: usize,
|
||||||
|
pub linked_files: usize,
|
||||||
|
pub copied_files: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn materialize_cir(
|
||||||
|
cir: &CanonicalInputRepresentation,
|
||||||
|
static_root: &Path,
|
||||||
|
mirror_root: &Path,
|
||||||
|
clean_rebuild: bool,
|
||||||
|
) -> Result<CirMaterializeSummary, CirMaterializeError> {
|
||||||
|
cir.validate().map_err(CirMaterializeError::TreeMismatch)?;
|
||||||
|
|
||||||
|
if clean_rebuild && mirror_root.exists() {
|
||||||
|
fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
|
||||||
|
path: mirror_root.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
|
||||||
|
path: mirror_root.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut linked_files = 0usize;
|
||||||
|
let mut copied_files = 0usize;
|
||||||
|
|
||||||
|
for object in &cir.objects {
|
||||||
|
let sha256_hex = hex::encode(&object.sha256);
|
||||||
|
let source = resolve_static_pool_file(static_root, &sha256_hex)?;
|
||||||
|
let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?;
|
||||||
|
let target = mirror_root.join(&relative);
|
||||||
|
|
||||||
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent {
|
||||||
|
path: parent.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.exists() {
|
||||||
|
fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget {
|
||||||
|
path: target.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::hard_link(&source, &target) {
|
||||||
|
Ok(()) => linked_files += 1,
|
||||||
|
Err(link_err) => {
|
||||||
|
fs::copy(&source, &target).map_err(|copy_err| CirMaterializeError::Copy {
|
||||||
|
src: source.display().to_string(),
|
||||||
|
dst: target.display().to_string(),
|
||||||
|
detail: format!("{copy_err}; original link error: {link_err}"),
|
||||||
|
})?;
|
||||||
|
copied_files += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual = collect_materialized_uris(mirror_root)?;
|
||||||
|
let expected = cir
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.rsync_uri.clone())
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
if actual != expected {
|
||||||
|
return Err(CirMaterializeError::TreeMismatch(format!(
|
||||||
|
"expected {} files, got {} files",
|
||||||
|
expected.len(),
|
||||||
|
actual.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CirMaterializeSummary {
|
||||||
|
object_count: cir.objects.len(),
|
||||||
|
linked_files,
|
||||||
|
copied_files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn materialize_cir_from_raw_store(
|
||||||
|
cir: &CanonicalInputRepresentation,
|
||||||
|
raw_store_db: &Path,
|
||||||
|
mirror_root: &Path,
|
||||||
|
clean_rebuild: bool,
|
||||||
|
) -> Result<CirMaterializeSummary, CirMaterializeError> {
|
||||||
|
cir.validate().map_err(CirMaterializeError::TreeMismatch)?;
|
||||||
|
|
||||||
|
if clean_rebuild && mirror_root.exists() {
|
||||||
|
fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
|
||||||
|
path: mirror_root.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
|
||||||
|
path: mirror_root.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let raw_store =
|
||||||
|
ExternalRawStoreDb::open(raw_store_db).map_err(|e| CirMaterializeError::OpenRawStore {
|
||||||
|
path: raw_store_db.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut copied_files = 0usize;
|
||||||
|
for object in &cir.objects {
|
||||||
|
let sha256_hex = hex::encode(&object.sha256);
|
||||||
|
let entry = raw_store
|
||||||
|
.get_raw_entry(&sha256_hex)
|
||||||
|
.map_err(|e| CirMaterializeError::ReadRawStore {
|
||||||
|
sha256_hex: sha256_hex.clone(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| CirMaterializeError::MissingRawStoreObject {
|
||||||
|
sha256_hex: sha256_hex.clone(),
|
||||||
|
})?;
|
||||||
|
let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?;
|
||||||
|
let target = mirror_root.join(&relative);
|
||||||
|
|
||||||
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent {
|
||||||
|
path: parent.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.exists() {
|
||||||
|
fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget {
|
||||||
|
path: target.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(&target, &entry.bytes).map_err(|e| CirMaterializeError::Copy {
|
||||||
|
src: raw_store_db.display().to_string(),
|
||||||
|
dst: target.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
copied_files += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual = collect_materialized_uris(mirror_root)?;
|
||||||
|
let expected = cir
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.rsync_uri.clone())
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
if actual != expected {
|
||||||
|
return Err(CirMaterializeError::TreeMismatch(format!(
|
||||||
|
"expected {} files, got {} files",
|
||||||
|
expected.len(),
|
||||||
|
actual.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CirMaterializeSummary {
|
||||||
|
object_count: cir.objects.len(),
|
||||||
|
linked_files: 0,
|
||||||
|
copied_files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mirror_relative_path_for_rsync_uri(rsync_uri: &str) -> Result<PathBuf, CirMaterializeError> {
|
||||||
|
let url = url::Url::parse(rsync_uri)
|
||||||
|
.map_err(|_| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?;
|
||||||
|
if url.scheme() != "rsync" {
|
||||||
|
return Err(CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()));
|
||||||
|
}
|
||||||
|
let host = url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?;
|
||||||
|
let segments = url
|
||||||
|
.path_segments()
|
||||||
|
.ok_or_else(|| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if segments.is_empty() || segments.last().copied().unwrap_or_default().is_empty() {
|
||||||
|
return Err(CirMaterializeError::DirectoryLikeRsyncUri(
|
||||||
|
rsync_uri.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = PathBuf::from(host);
|
||||||
|
for segment in segments {
|
||||||
|
if !segment.is_empty() {
|
||||||
|
path.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_static_pool_file(
|
||||||
|
static_root: &Path,
|
||||||
|
sha256_hex: &str,
|
||||||
|
) -> Result<PathBuf, CirMaterializeError> {
|
||||||
|
if sha256_hex.len() != 64 || !sha256_hex.as_bytes().iter().all(u8::is_ascii_hexdigit) {
|
||||||
|
return Err(CirMaterializeError::MissingStaticObject {
|
||||||
|
sha256_hex: sha256_hex.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let prefix1 = &sha256_hex[0..2];
|
||||||
|
let prefix2 = &sha256_hex[2..4];
|
||||||
|
|
||||||
|
let entries =
|
||||||
|
fs::read_dir(static_root).map_err(|_| CirMaterializeError::MissingStaticObject {
|
||||||
|
sha256_hex: sha256_hex.to_string(),
|
||||||
|
})?;
|
||||||
|
let mut dates = entries
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|entry| entry.path().is_dir())
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
dates.sort();
|
||||||
|
|
||||||
|
for date_dir in dates {
|
||||||
|
let candidate = date_dir.join(prefix1).join(prefix2).join(sha256_hex);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(CirMaterializeError::MissingStaticObject {
|
||||||
|
sha256_hex: sha256_hex.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_materialized_uris(
|
||||||
|
mirror_root: &Path,
|
||||||
|
) -> Result<std::collections::BTreeSet<String>, CirMaterializeError> {
|
||||||
|
let mut out = std::collections::BTreeSet::new();
|
||||||
|
let mut stack = vec![mirror_root.to_path_buf()];
|
||||||
|
while let Some(path) = stack.pop() {
|
||||||
|
for entry in fs::read_dir(&path).map_err(|e| CirMaterializeError::CreateMirrorRoot {
|
||||||
|
path: path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})? {
|
||||||
|
let entry = entry.map_err(|e| CirMaterializeError::CreateMirrorRoot {
|
||||||
|
path: path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
stack.push(path);
|
||||||
|
} else {
|
||||||
|
let rel = path
|
||||||
|
.strip_prefix(mirror_root)
|
||||||
|
.expect("materialized path under mirror root")
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
let uri = format!("rsync://{rel}");
|
||||||
|
out.insert(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
CirMaterializeError, materialize_cir, materialize_cir_from_raw_store,
|
||||||
|
mirror_relative_path_for_rsync_uri, resolve_static_pool_file,
|
||||||
|
};
|
||||||
|
use crate::blob_store::ExternalRawStoreDb;
|
||||||
|
use crate::cir::model::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
|
||||||
|
};
|
||||||
|
use sha2::Digest;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn sample_time() -> time::OffsetDateTime {
|
||||||
|
time::OffsetDateTime::parse(
|
||||||
|
"2026-04-07T12:34:56Z",
|
||||||
|
&time::format_description::well_known::Rfc3339,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_cir() -> CanonicalInputRepresentation {
|
||||||
|
CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
|
||||||
|
sha256: hex::decode(
|
||||||
|
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(),
|
||||||
|
sha256: hex::decode(
|
||||||
|
"2222222222222222222222222222222222222222222222222222222222222222",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cir_with_real_hashes(a: &[u8], b: &[u8]) -> CanonicalInputRepresentation {
|
||||||
|
CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
|
||||||
|
sha256: sha2::Sha256::digest(a).to_vec(),
|
||||||
|
},
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(),
|
||||||
|
sha256: sha2::Sha256::digest(b).to_vec(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mirror_relative_path_for_rsync_uri_maps_host_and_path() {
|
||||||
|
let path =
|
||||||
|
mirror_relative_path_for_rsync_uri("rsync://example.net/repo/nested/b.roa").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
path,
|
||||||
|
PathBuf::from("example.net")
|
||||||
|
.join("repo")
|
||||||
|
.join("nested")
|
||||||
|
.join("b.roa")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_static_pool_file_finds_hash_across_dates() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let path = td.path().join("20260407").join("11").join("11");
|
||||||
|
std::fs::create_dir_all(&path).unwrap();
|
||||||
|
let file = path.join("1111111111111111111111111111111111111111111111111111111111111111");
|
||||||
|
std::fs::write(&file, b"x").unwrap();
|
||||||
|
|
||||||
|
let resolved = resolve_static_pool_file(
|
||||||
|
td.path(),
|
||||||
|
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolved, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_static_pool_file_rejects_invalid_hash_and_missing_hash() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let err = resolve_static_pool_file(td.path(), "not-a-hash")
|
||||||
|
.expect_err("invalid hash should fail");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
CirMaterializeError::MissingStaticObject { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
let err = resolve_static_pool_file(
|
||||||
|
td.path(),
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
)
|
||||||
|
.expect_err("missing hash should fail");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
CirMaterializeError::MissingStaticObject { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mirror_relative_path_rejects_non_rsync_and_directory_like_uris() {
|
||||||
|
let err = mirror_relative_path_for_rsync_uri("https://example.net/repo/a.roa")
|
||||||
|
.expect_err("non-rsync uri must fail");
|
||||||
|
assert!(matches!(err, CirMaterializeError::InvalidRsyncUri(_)));
|
||||||
|
|
||||||
|
let err = mirror_relative_path_for_rsync_uri("rsync://example.net/repo/")
|
||||||
|
.expect_err("directory-like uri must fail");
|
||||||
|
assert!(matches!(err, CirMaterializeError::DirectoryLikeRsyncUri(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_clean_rebuild_creates_exact_tree_and_removes_stale_files() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let static_root = td.path().join("static");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
|
||||||
|
write_static(
|
||||||
|
&static_root,
|
||||||
|
"20260407",
|
||||||
|
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
b"a",
|
||||||
|
);
|
||||||
|
write_static(
|
||||||
|
&static_root,
|
||||||
|
"20260407",
|
||||||
|
"2222222222222222222222222222222222222222222222222222222222222222",
|
||||||
|
b"b",
|
||||||
|
);
|
||||||
|
std::fs::create_dir_all(mirror_root.join("stale")).unwrap();
|
||||||
|
std::fs::write(mirror_root.join("stale/old.txt"), b"old").unwrap();
|
||||||
|
|
||||||
|
let summary = materialize_cir(&sample_cir(), &static_root, &mirror_root, true).unwrap();
|
||||||
|
assert_eq!(summary.object_count, 2);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(),
|
||||||
|
b"a"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(),
|
||||||
|
b"b"
|
||||||
|
);
|
||||||
|
assert!(!mirror_root.join("stale/old.txt").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_fails_when_static_object_missing() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let err = materialize_cir(&sample_cir(), td.path(), &td.path().join("mirror"), true)
|
||||||
|
.expect_err("missing static object must fail");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
CirMaterializeError::MissingStaticObject { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_without_clean_rebuild_detects_stale_extra_files() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let static_root = td.path().join("static");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
|
||||||
|
write_static(
|
||||||
|
&static_root,
|
||||||
|
"20260407",
|
||||||
|
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
b"a",
|
||||||
|
);
|
||||||
|
write_static(
|
||||||
|
&static_root,
|
||||||
|
"20260407",
|
||||||
|
"2222222222222222222222222222222222222222222222222222222222222222",
|
||||||
|
b"b",
|
||||||
|
);
|
||||||
|
std::fs::create_dir_all(mirror_root.join("extra")).unwrap();
|
||||||
|
std::fs::write(mirror_root.join("extra/stale.txt"), b"stale").unwrap();
|
||||||
|
|
||||||
|
let err = materialize_cir(&sample_cir(), &static_root, &mirror_root, false)
|
||||||
|
.expect_err("stale extra files should fail exact tree check");
|
||||||
|
assert!(matches!(err, CirMaterializeError::TreeMismatch(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_from_raw_store_creates_expected_tree() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let raw_store_path = td.path().join("raw-store.db");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
let a = b"a".to_vec();
|
||||||
|
let b = b"b".to_vec();
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
|
||||||
|
sha256: sha2::Sha256::digest(&a).to_vec(),
|
||||||
|
},
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(),
|
||||||
|
sha256: sha2::Sha256::digest(&b).to_vec(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
|
||||||
|
let mut entry_a =
|
||||||
|
crate::storage::RawByHashEntry::from_bytes(hex::encode(&cir.objects[0].sha256), a);
|
||||||
|
entry_a.origin_uris.push(cir.objects[0].rsync_uri.clone());
|
||||||
|
raw_store.put_raw_entry(&entry_a).unwrap();
|
||||||
|
|
||||||
|
let mut entry_b =
|
||||||
|
crate::storage::RawByHashEntry::from_bytes(hex::encode(&cir.objects[1].sha256), b);
|
||||||
|
entry_b.origin_uris.push(cir.objects[1].rsync_uri.clone());
|
||||||
|
raw_store.put_raw_entry(&entry_b).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary =
|
||||||
|
materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap();
|
||||||
|
assert_eq!(summary.object_count, 2);
|
||||||
|
assert_eq!(summary.linked_files, 0);
|
||||||
|
assert_eq!(summary.copied_files, 2);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(),
|
||||||
|
b"a"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(),
|
||||||
|
b"b"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_from_raw_store_fails_when_object_missing() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let raw_store_path = td.path().join("raw-store.db");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
let cir = cir_with_real_hashes(b"a", b"b");
|
||||||
|
{
|
||||||
|
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
|
||||||
|
let only = crate::storage::RawByHashEntry::from_bytes(
|
||||||
|
hex::encode(&cir.objects[0].sha256),
|
||||||
|
b"a".to_vec(),
|
||||||
|
);
|
||||||
|
raw_store.put_raw_entry(&only).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let err = materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true)
|
||||||
|
.expect_err("missing second object should fail");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
CirMaterializeError::MissingRawStoreObject { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_from_raw_store_detects_stale_tree_when_not_clean_rebuild() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let raw_store_path = td.path().join("raw-store.db");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
let cir = cir_with_real_hashes(b"a", b"b");
|
||||||
|
{
|
||||||
|
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
|
||||||
|
let mut entry_a = crate::storage::RawByHashEntry::from_bytes(
|
||||||
|
hex::encode(&cir.objects[0].sha256),
|
||||||
|
b"a".to_vec(),
|
||||||
|
);
|
||||||
|
entry_a.origin_uris.push(cir.objects[0].rsync_uri.clone());
|
||||||
|
raw_store.put_raw_entry(&entry_a).unwrap();
|
||||||
|
let mut entry_b = crate::storage::RawByHashEntry::from_bytes(
|
||||||
|
hex::encode(&cir.objects[1].sha256),
|
||||||
|
b"b".to_vec(),
|
||||||
|
);
|
||||||
|
entry_b.origin_uris.push(cir.objects[1].rsync_uri.clone());
|
||||||
|
raw_store.put_raw_entry(&entry_b).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(mirror_root.join("extra")).unwrap();
|
||||||
|
std::fs::write(mirror_root.join("extra/stale.txt"), b"stale").unwrap();
|
||||||
|
|
||||||
|
let err =
|
||||||
|
materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, false)
|
||||||
|
.expect_err("stale file should fail exact tree check");
|
||||||
|
assert!(matches!(err, CirMaterializeError::TreeMismatch(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_from_raw_store_overwrites_existing_targets() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let raw_store_path = td.path().join("raw-store.db");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
let a = b"new-a".to_vec();
|
||||||
|
let b = b"new-b".to_vec();
|
||||||
|
let cir = cir_with_real_hashes(&a, &b);
|
||||||
|
{
|
||||||
|
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
|
||||||
|
let mut entry_a = crate::storage::RawByHashEntry::from_bytes(
|
||||||
|
hex::encode(&cir.objects[0].sha256),
|
||||||
|
a.clone(),
|
||||||
|
);
|
||||||
|
entry_a.origin_uris.push(cir.objects[0].rsync_uri.clone());
|
||||||
|
raw_store.put_raw_entry(&entry_a).unwrap();
|
||||||
|
let mut entry_b = crate::storage::RawByHashEntry::from_bytes(
|
||||||
|
hex::encode(&cir.objects[1].sha256),
|
||||||
|
b.clone(),
|
||||||
|
);
|
||||||
|
entry_b.origin_uris.push(cir.objects[1].rsync_uri.clone());
|
||||||
|
raw_store.put_raw_entry(&entry_b).unwrap();
|
||||||
|
}
|
||||||
|
let target = mirror_root.join("example.net/repo/a.cer");
|
||||||
|
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(&target, b"old").unwrap();
|
||||||
|
|
||||||
|
let summary =
|
||||||
|
materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, false).unwrap();
|
||||||
|
assert_eq!(summary.copied_files, 2);
|
||||||
|
assert_eq!(std::fs::read(&target).unwrap(), a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn materialize_from_raw_store_reports_codec_errors() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let raw_store_path = td.path().join("raw-store.db");
|
||||||
|
let mirror_root = td.path().join("mirror");
|
||||||
|
{
|
||||||
|
let _raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let db = rocksdb::DB::open_default(&raw_store_path).unwrap();
|
||||||
|
db.put(
|
||||||
|
b"rawbyhash:1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
b"bad-cbor",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let err = materialize_cir_from_raw_store(
|
||||||
|
&CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
|
||||||
|
sha256: hex::decode(
|
||||||
|
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
}],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
&raw_store_path,
|
||||||
|
&mirror_root,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect_err("corrupt raw-store object should fail");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
CirMaterializeError::ReadRawStore { .. } | CirMaterializeError::MissingRawStoreObject { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_static(root: &Path, date: &str, hash: &str, bytes: &[u8]) {
|
||||||
|
let path = root.join(date).join(&hash[0..2]).join(&hash[2..4]);
|
||||||
|
std::fs::create_dir_all(&path).unwrap();
|
||||||
|
std::fs::write(path.join(hash), bytes).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
350
src/cir/mod.rs
Normal file
350
src/cir/mod.rs
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
pub mod decode;
|
||||||
|
pub mod encode;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod export;
|
||||||
|
pub mod materialize;
|
||||||
|
pub mod model;
|
||||||
|
pub mod sequence;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod static_pool;
|
||||||
|
|
||||||
|
pub use decode::{CirDecodeError, decode_cir};
|
||||||
|
pub use encode::{CirEncodeError, encode_cir};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub use export::{
|
||||||
|
CirExportError, CirExportSummary, build_cir_from_run, export_cir_from_run, write_cir_file,
|
||||||
|
};
|
||||||
|
pub use materialize::{
|
||||||
|
CirMaterializeError, CirMaterializeSummary, materialize_cir,
|
||||||
|
materialize_cir_from_raw_store,
|
||||||
|
mirror_relative_path_for_rsync_uri, resolve_static_pool_file,
|
||||||
|
};
|
||||||
|
pub use model::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
|
||||||
|
};
|
||||||
|
pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub use static_pool::{
|
||||||
|
CirStaticPoolError, CirStaticPoolExportSummary, CirStaticPoolWriteResult,
|
||||||
|
export_hashes_from_store, static_pool_path, static_pool_relative_path,
|
||||||
|
write_bytes_to_static_pool, write_raw_entry_to_static_pool,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
|
||||||
|
decode_cir, encode_cir,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sample_time() -> time::OffsetDateTime {
|
||||||
|
time::OffsetDateTime::parse(
|
||||||
|
"2026-04-07T12:34:56Z",
|
||||||
|
&time::format_description::well_known::Rfc3339,
|
||||||
|
)
|
||||||
|
.expect("valid rfc3339")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_cir() -> CanonicalInputRepresentation {
|
||||||
|
CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
|
||||||
|
sha256: vec![0x11; 32],
|
||||||
|
},
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/b.roa".to_string(),
|
||||||
|
sha256: vec![0x22; 32],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"https://tal.example.net/ta.cer\nrsync://example.net/repo/ta.cer\nMIIB"
|
||||||
|
.to_vec(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_encode_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(8 + value.len());
|
||||||
|
out.push(tag);
|
||||||
|
if value.len() < 0x80 {
|
||||||
|
out.push(value.len() as u8);
|
||||||
|
} else {
|
||||||
|
let len = value.len();
|
||||||
|
let bytes = len.to_be_bytes();
|
||||||
|
let first_non_zero = bytes
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b != 0)
|
||||||
|
.unwrap_or(bytes.len() - 1);
|
||||||
|
let len_bytes = &bytes[first_non_zero..];
|
||||||
|
out.push(0x80 | len_bytes.len() as u8);
|
||||||
|
out.extend_from_slice(len_bytes);
|
||||||
|
}
|
||||||
|
out.extend_from_slice(value);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_roundtrip_full_succeeds() {
|
||||||
|
let cir = sample_cir();
|
||||||
|
let der = encode_cir(&cir).expect("encode cir");
|
||||||
|
let decoded = decode_cir(&der).expect("decode cir");
|
||||||
|
assert_eq!(decoded, cir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_roundtrip_minimal_succeeds() {
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/minimal.tal".to_string(),
|
||||||
|
tal_bytes: b"rsync://example.net/repo/ta.cer\nMIIB".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let der = encode_cir(&cir).expect("encode minimal cir");
|
||||||
|
let decoded = decode_cir(&der).expect("decode minimal cir");
|
||||||
|
assert_eq!(decoded, cir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_model_rejects_unsorted_duplicate_objects() {
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/z.roa".to_string(),
|
||||||
|
sha256: vec![0x11; 32],
|
||||||
|
},
|
||||||
|
CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
|
||||||
|
sha256: vec![0x22; 32],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let err = encode_cir(&cir).expect_err("unsorted objects must fail");
|
||||||
|
assert!(err.to_string().contains("CIR.objects"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_model_rejects_duplicate_tals() {
|
||||||
|
let cir = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
tals: vec![
|
||||||
|
CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"a".to_vec(),
|
||||||
|
},
|
||||||
|
CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"b".to_vec(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let err = encode_cir(&cir).expect_err("duplicate tals must fail");
|
||||||
|
assert!(err.to_string().contains("CIR.tals"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_decode_rejects_wrong_version() {
|
||||||
|
let mut der = encode_cir(&sample_cir()).expect("encode cir");
|
||||||
|
let pos = der
|
||||||
|
.windows(3)
|
||||||
|
.position(|window| window == [0x02, 0x01, CIR_VERSION_V1 as u8])
|
||||||
|
.expect("find version integer");
|
||||||
|
der[pos + 2] = 2;
|
||||||
|
let err = decode_cir(&der).expect_err("wrong version must fail");
|
||||||
|
assert!(err.to_string().contains("unexpected CIR version"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_decode_rejects_wrong_hash_oid() {
|
||||||
|
let mut der = encode_cir(&sample_cir()).expect("encode cir");
|
||||||
|
let sha256_bytes = crate::data_model::oid::OID_SHA256_RAW;
|
||||||
|
let idx = der
|
||||||
|
.windows(sha256_bytes.len())
|
||||||
|
.position(|window| window == sha256_bytes)
|
||||||
|
.expect("find sha256 oid");
|
||||||
|
der[idx + sha256_bytes.len() - 1] ^= 0x01;
|
||||||
|
let err = decode_cir(&der).expect_err("wrong oid must fail");
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains(crate::data_model::oid::OID_SHA256),
|
||||||
|
"{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_decode_rejects_bad_generalized_time() {
|
||||||
|
let mut der = encode_cir(&sample_cir()).expect("encode cir");
|
||||||
|
let pos = der
|
||||||
|
.windows(15)
|
||||||
|
.position(|window| window == b"20260407123456Z")
|
||||||
|
.expect("find generalized time");
|
||||||
|
der[pos + 14] = b'X';
|
||||||
|
let err = decode_cir(&der).expect_err("bad time must fail");
|
||||||
|
assert!(err.to_string().contains("GeneralizedTime"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() {
|
||||||
|
let bad_object = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![CirObject {
|
||||||
|
rsync_uri: "https://example.net/repo/a.roa".to_string(),
|
||||||
|
sha256: vec![0x11; 32],
|
||||||
|
}],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let err = encode_cir(&bad_object).expect_err("non-rsync object uri must fail");
|
||||||
|
assert!(err.to_string().contains("rsync://"), "{err}");
|
||||||
|
|
||||||
|
let no_tals = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
tals: Vec::new(),
|
||||||
|
};
|
||||||
|
let err = encode_cir(&no_tals).expect_err("empty tals must fail");
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("CIR.tals must be non-empty"),
|
||||||
|
"{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() {
|
||||||
|
let bad_time = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
|
||||||
|
objects: Vec::new(),
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let err = encode_cir(&bad_time).expect_err("non-utc validation time must fail");
|
||||||
|
assert!(err.to_string().contains("UTC"), "{err}");
|
||||||
|
|
||||||
|
let bad_hash = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: vec![CirObject {
|
||||||
|
rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
|
||||||
|
sha256: vec![0x11; 31],
|
||||||
|
}],
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "https://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let err = encode_cir(&bad_hash).expect_err("bad digest len must fail");
|
||||||
|
assert!(err.to_string().contains("32 bytes"), "{err}");
|
||||||
|
|
||||||
|
let bad_tal_uri = CanonicalInputRepresentation {
|
||||||
|
version: CIR_VERSION_V1,
|
||||||
|
hash_alg: CirHashAlgorithm::Sha256,
|
||||||
|
validation_time: sample_time(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
tals: vec![CirTal {
|
||||||
|
tal_uri: "ftp://tal.example.net/root.tal".to_string(),
|
||||||
|
tal_bytes: b"x".to_vec(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let err = encode_cir(&bad_tal_uri).expect_err("bad tal uri must fail");
|
||||||
|
assert!(err.to_string().contains("http:// or https://"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_decode_rejects_trailing_bytes_and_trailing_fields() {
|
||||||
|
let cir = sample_cir();
|
||||||
|
let mut der = encode_cir(&cir).expect("encode cir");
|
||||||
|
der.push(0);
|
||||||
|
let err = decode_cir(&der).expect_err("trailing bytes after cir must fail");
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("trailing bytes after CIR"),
|
||||||
|
"{err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let object = test_encode_tlv(
|
||||||
|
0x30,
|
||||||
|
&[
|
||||||
|
test_encode_tlv(0x16, b"rsync://example.net/repo/a.roa"),
|
||||||
|
test_encode_tlv(0x04, &[0x11; 32]),
|
||||||
|
test_encode_tlv(0x02, &[0x01]),
|
||||||
|
]
|
||||||
|
.concat(),
|
||||||
|
);
|
||||||
|
let tal = test_encode_tlv(
|
||||||
|
0x30,
|
||||||
|
&[
|
||||||
|
test_encode_tlv(0x16, b"https://tal.example.net/root.tal"),
|
||||||
|
test_encode_tlv(0x04, b"x"),
|
||||||
|
]
|
||||||
|
.concat(),
|
||||||
|
);
|
||||||
|
let bad = test_encode_tlv(
|
||||||
|
0x30,
|
||||||
|
&[
|
||||||
|
test_encode_tlv(0x02, &[CIR_VERSION_V1 as u8]),
|
||||||
|
test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW),
|
||||||
|
test_encode_tlv(0x18, b"20260407123456Z"),
|
||||||
|
test_encode_tlv(0x30, &object),
|
||||||
|
test_encode_tlv(0x30, &tal),
|
||||||
|
]
|
||||||
|
.concat(),
|
||||||
|
);
|
||||||
|
let err = decode_cir(&bad).expect_err("trailing field in object must fail");
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("trailing fields in CirObject"),
|
||||||
|
"{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cir_decode_rejects_invalid_object_and_tal_shapes() {
|
||||||
|
let cir = sample_cir();
|
||||||
|
let mut der = encode_cir(&cir).expect("encode cir");
|
||||||
|
|
||||||
|
let rsync_text = b"rsync://example.net/repo/a.cer";
|
||||||
|
let idx = der
|
||||||
|
.windows(rsync_text.len())
|
||||||
|
.position(|window| window == rsync_text)
|
||||||
|
.expect("find object uri");
|
||||||
|
der[idx] = 0xFF;
|
||||||
|
let err = decode_cir(&der).expect_err("invalid utf8 object uri must fail");
|
||||||
|
assert!(err.to_string().contains("utf-8"), "{err}");
|
||||||
|
|
||||||
|
let mut der = encode_cir(&cir).expect("encode cir");
|
||||||
|
let tal_text = b"https://tal.example.net/root.tal";
|
||||||
|
let idx = der
|
||||||
|
.windows(tal_text.len())
|
||||||
|
.position(|window| window == tal_text)
|
||||||
|
.expect("find tal uri");
|
||||||
|
der[idx] = 0xFF;
|
||||||
|
let err = decode_cir(&der).expect_err("invalid utf8 tal uri must fail");
|
||||||
|
assert!(err.to_string().contains("utf-8"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/cir/model.rs
Normal file
122
src/cir/model.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use crate::data_model::oid::OID_SHA256;
|
||||||
|
|
||||||
|
pub const CIR_VERSION_V1: u32 = 1;
|
||||||
|
pub const DIGEST_LEN_SHA256: usize = 32;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CirHashAlgorithm {
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CirHashAlgorithm {
|
||||||
|
pub fn oid(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Sha256 => OID_SHA256,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CanonicalInputRepresentation {
|
||||||
|
pub version: u32,
|
||||||
|
pub hash_alg: CirHashAlgorithm,
|
||||||
|
pub validation_time: time::OffsetDateTime,
|
||||||
|
pub objects: Vec<CirObject>,
|
||||||
|
pub tals: Vec<CirTal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanonicalInputRepresentation {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.version != CIR_VERSION_V1 {
|
||||||
|
return Err(format!(
|
||||||
|
"CIR version must be {CIR_VERSION_V1}, got {}",
|
||||||
|
self.version
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !matches!(self.hash_alg, CirHashAlgorithm::Sha256) {
|
||||||
|
return Err("CIR hashAlg must be SHA-256".into());
|
||||||
|
}
|
||||||
|
if self.validation_time.offset() != time::UtcOffset::UTC {
|
||||||
|
return Err("CIR validationTime must be UTC".into());
|
||||||
|
}
|
||||||
|
validate_sorted_unique_strings(
|
||||||
|
self.objects.iter().map(|item| item.rsync_uri.as_str()),
|
||||||
|
"CIR.objects must be sorted by rsyncUri and unique",
|
||||||
|
)?;
|
||||||
|
validate_sorted_unique_strings(
|
||||||
|
self.tals.iter().map(|item| item.tal_uri.as_str()),
|
||||||
|
"CIR.tals must be sorted by talUri and unique",
|
||||||
|
)?;
|
||||||
|
if self.tals.is_empty() {
|
||||||
|
return Err("CIR.tals must be non-empty".into());
|
||||||
|
}
|
||||||
|
for object in &self.objects {
|
||||||
|
object.validate()?;
|
||||||
|
}
|
||||||
|
for tal in &self.tals {
|
||||||
|
tal.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirObject {
|
||||||
|
pub rsync_uri: String,
|
||||||
|
pub sha256: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CirObject {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if !self.rsync_uri.starts_with("rsync://") {
|
||||||
|
return Err(format!(
|
||||||
|
"CirObject.rsync_uri must start with rsync://, got {}",
|
||||||
|
self.rsync_uri
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.sha256.len() != DIGEST_LEN_SHA256 {
|
||||||
|
return Err(format!(
|
||||||
|
"CirObject.sha256 must be {DIGEST_LEN_SHA256} bytes, got {}",
|
||||||
|
self.sha256.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirTal {
|
||||||
|
pub tal_uri: String,
|
||||||
|
pub tal_bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CirTal {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if !(self.tal_uri.starts_with("https://") || self.tal_uri.starts_with("http://")) {
|
||||||
|
return Err(format!(
|
||||||
|
"CirTal.tal_uri must start with http:// or https://, got {}",
|
||||||
|
self.tal_uri
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.tal_bytes.is_empty() {
|
||||||
|
return Err("CirTal.tal_bytes must be non-empty".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sorted_unique_strings<'a>(
|
||||||
|
items: impl IntoIterator<Item = &'a str>,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut prev: Option<&'a str> = None;
|
||||||
|
for key in items {
|
||||||
|
if let Some(prev_key) = prev
|
||||||
|
&& key <= prev_key
|
||||||
|
{
|
||||||
|
return Err(message.into());
|
||||||
|
}
|
||||||
|
prev = Some(key);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
211
src/cir/sequence.rs
Normal file
211
src/cir/sequence.rs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CirSequenceStepKind {
|
||||||
|
Full,
|
||||||
|
Delta,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CirSequenceStep {
|
||||||
|
pub step_id: String,
|
||||||
|
pub kind: CirSequenceStepKind,
|
||||||
|
pub validation_time: String,
|
||||||
|
pub cir_path: String,
|
||||||
|
pub ccr_path: String,
|
||||||
|
pub report_path: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub timing_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stdout_log_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stderr_log_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub artifact_prefix: Option<String>,
|
||||||
|
pub previous_step_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CirSequenceManifest {
|
||||||
|
pub version: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub static_root: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub raw_store_db_path: Option<String>,
|
||||||
|
pub steps: Vec<CirSequenceStep>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CirSequenceManifest {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.version == 0 {
|
||||||
|
return Err("sequence.version must be positive".to_string());
|
||||||
|
}
|
||||||
|
let backend_count = self.static_root.is_some() as u8 + self.raw_store_db_path.is_some() as u8;
|
||||||
|
if backend_count != 1 {
|
||||||
|
return Err("sequence must set exactly one of static_root or raw_store_db_path".to_string());
|
||||||
|
}
|
||||||
|
match (self.static_root.as_ref(), self.raw_store_db_path.as_ref()) {
|
||||||
|
(Some(static_root), None) if !static_root.trim().is_empty() => {}
|
||||||
|
(None, Some(raw_store_db_path)) if !raw_store_db_path.trim().is_empty() => {}
|
||||||
|
_ => {
|
||||||
|
return Err("sequence backend path must not be empty".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.steps.is_empty() {
|
||||||
|
return Err("sequence.steps must not be empty".to_string());
|
||||||
|
}
|
||||||
|
let mut previous_ids = std::collections::BTreeSet::new();
|
||||||
|
for (idx, step) in self.steps.iter().enumerate() {
|
||||||
|
if step.step_id.trim().is_empty() {
|
||||||
|
return Err(format!("sequence.steps[{idx}].step_id must not be empty"));
|
||||||
|
}
|
||||||
|
if !previous_ids.insert(step.step_id.clone()) {
|
||||||
|
return Err(format!("sequence.steps[{idx}].step_id must be unique"));
|
||||||
|
}
|
||||||
|
if step.validation_time.trim().is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}].validation_time must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(timing_path) = &step.timing_path
|
||||||
|
&& timing_path.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}].timing_path must not be empty when set"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(stdout_log_path) = &step.stdout_log_path
|
||||||
|
&& stdout_log_path.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}].stdout_log_path must not be empty when set"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(stderr_log_path) = &step.stderr_log_path
|
||||||
|
&& stderr_log_path.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}].stderr_log_path must not be empty when set"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(artifact_prefix) = &step.artifact_prefix
|
||||||
|
&& artifact_prefix.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}].artifact_prefix must not be empty when set"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if step.cir_path.trim().is_empty()
|
||||||
|
|| step.ccr_path.trim().is_empty()
|
||||||
|
|| step.report_path.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}] output paths must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match step.kind {
|
||||||
|
CirSequenceStepKind::Full => {
|
||||||
|
if idx != 0 {
|
||||||
|
return Err("full step must be the first step".to_string());
|
||||||
|
}
|
||||||
|
if step.previous_step_id.is_some() {
|
||||||
|
return Err("full step must not reference previous_step_id".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CirSequenceStepKind::Delta => {
|
||||||
|
if idx == 0 {
|
||||||
|
return Err("delta step cannot be the first step".to_string());
|
||||||
|
}
|
||||||
|
let previous = step.previous_step_id.as_ref().ok_or_else(|| {
|
||||||
|
format!("sequence.steps[{idx}] delta step must set previous_step_id")
|
||||||
|
})?;
|
||||||
|
if !previous_ids.contains(previous) {
|
||||||
|
return Err(format!(
|
||||||
|
"sequence.steps[{idx}] previous_step_id must reference an earlier step"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind};
|
||||||
|
|
||||||
|
fn sample_manifest() -> CirSequenceManifest {
|
||||||
|
CirSequenceManifest {
|
||||||
|
version: 1,
|
||||||
|
static_root: Some("static".to_string()),
|
||||||
|
raw_store_db_path: None,
|
||||||
|
steps: vec![
|
||||||
|
CirSequenceStep {
|
||||||
|
step_id: "full".to_string(),
|
||||||
|
kind: CirSequenceStepKind::Full,
|
||||||
|
validation_time: "2026-04-09T00:00:00Z".to_string(),
|
||||||
|
cir_path: "full/input.cir".to_string(),
|
||||||
|
ccr_path: "full/result.ccr".to_string(),
|
||||||
|
report_path: "full/report.json".to_string(),
|
||||||
|
timing_path: Some("full/timing.json".to_string()),
|
||||||
|
stdout_log_path: Some("full/stdout.log".to_string()),
|
||||||
|
stderr_log_path: Some("full/stderr.log".to_string()),
|
||||||
|
artifact_prefix: Some("2026-04-09T00:00:00Z-test".to_string()),
|
||||||
|
previous_step_id: None,
|
||||||
|
},
|
||||||
|
CirSequenceStep {
|
||||||
|
step_id: "delta-001".to_string(),
|
||||||
|
kind: CirSequenceStepKind::Delta,
|
||||||
|
validation_time: "2026-04-09T00:10:00Z".to_string(),
|
||||||
|
cir_path: "delta-001/input.cir".to_string(),
|
||||||
|
ccr_path: "delta-001/result.ccr".to_string(),
|
||||||
|
report_path: "delta-001/report.json".to_string(),
|
||||||
|
timing_path: Some("delta-001/timing.json".to_string()),
|
||||||
|
stdout_log_path: Some("delta-001/stdout.log".to_string()),
|
||||||
|
stderr_log_path: Some("delta-001/stderr.log".to_string()),
|
||||||
|
artifact_prefix: Some("2026-04-09T00:10:00Z-test".to_string()),
|
||||||
|
previous_step_id: Some("full".to_string()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequence_manifest_validate_accepts_minimal_chain() {
|
||||||
|
sample_manifest().validate().expect("valid sequence");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequence_manifest_validate_rejects_bad_order_and_duplicates() {
|
||||||
|
let mut bad = sample_manifest();
|
||||||
|
bad.steps.swap(0, 1);
|
||||||
|
let err = bad.validate().expect_err("full must be first");
|
||||||
|
assert!(
|
||||||
|
err.contains("delta step cannot be the first step")
|
||||||
|
|| err.contains("full step must be the first step")
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut dup = sample_manifest();
|
||||||
|
dup.steps[1].step_id = "full".to_string();
|
||||||
|
let err = dup.validate().expect_err("duplicate id must fail");
|
||||||
|
assert!(err.contains("must be unique"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequence_manifest_validate_rejects_missing_previous_reference() {
|
||||||
|
let mut bad = sample_manifest();
|
||||||
|
bad.steps[1].previous_step_id = Some("missing".to_string());
|
||||||
|
let err = bad.validate().expect_err("missing previous must fail");
|
||||||
|
assert!(err.contains("previous_step_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequence_manifest_validate_accepts_raw_store_backend() {
|
||||||
|
let mut manifest = sample_manifest();
|
||||||
|
manifest.static_root = None;
|
||||||
|
manifest.raw_store_db_path = Some("raw-store.db".to_string());
|
||||||
|
manifest.validate().expect("raw store sequence");
|
||||||
|
}
|
||||||
|
}
|
||||||
377
src/cir/static_pool.rs
Normal file
377
src/cir/static_pool.rs
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::blob_store::RawObjectStore;
|
||||||
|
use crate::storage::{RawByHashEntry, RocksStore};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CirStaticPoolError {
|
||||||
|
#[error("invalid sha256 hex: {0}")]
|
||||||
|
InvalidSha256Hex(String),
|
||||||
|
|
||||||
|
#[error("raw bytes are empty for sha256={sha256_hex}")]
|
||||||
|
EmptyBytes { sha256_hex: String },
|
||||||
|
|
||||||
|
#[error("raw bytes do not match sha256 hex: {sha256_hex}")]
|
||||||
|
HashMismatch { sha256_hex: String },
|
||||||
|
|
||||||
|
#[error("create directory failed: {path}: {detail}")]
|
||||||
|
CreateDir { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("create temp file failed: {path}: {detail}")]
|
||||||
|
CreateTemp { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("write temp file failed: {path}: {detail}")]
|
||||||
|
WriteTemp { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("sync temp file failed: {path}: {detail}")]
|
||||||
|
SyncTemp { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("publish temp file failed: {temp_path} -> {final_path}: {detail}")]
|
||||||
|
Publish {
|
||||||
|
temp_path: String,
|
||||||
|
final_path: String,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("remove temp file failed: {path}: {detail}")]
|
||||||
|
RemoveTemp { path: String, detail: String },
|
||||||
|
|
||||||
|
#[error("raw_by_hash entry missing for sha256={sha256_hex}")]
|
||||||
|
MissingRawByHash { sha256_hex: String },
|
||||||
|
|
||||||
|
#[error("storage error: {0}")]
|
||||||
|
Storage(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirStaticPoolWriteResult {
|
||||||
|
pub final_path: PathBuf,
|
||||||
|
pub written: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CirStaticPoolExportSummary {
|
||||||
|
pub unique_hashes: usize,
|
||||||
|
pub written_files: usize,
|
||||||
|
pub reused_files: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn static_pool_relative_path(
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
sha256_hex: &str,
|
||||||
|
) -> Result<PathBuf, CirStaticPoolError> {
|
||||||
|
validate_sha256_hex(sha256_hex)?;
|
||||||
|
let date = format_utc_date(capture_date_utc);
|
||||||
|
Ok(PathBuf::from(date)
|
||||||
|
.join(&sha256_hex[0..2])
|
||||||
|
.join(&sha256_hex[2..4])
|
||||||
|
.join(sha256_hex))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn static_pool_path(
|
||||||
|
static_root: &Path,
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
sha256_hex: &str,
|
||||||
|
) -> Result<PathBuf, CirStaticPoolError> {
|
||||||
|
Ok(static_root.join(static_pool_relative_path(capture_date_utc, sha256_hex)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_bytes_to_static_pool(
|
||||||
|
static_root: &Path,
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
sha256_hex_value: &str,
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Result<CirStaticPoolWriteResult, CirStaticPoolError> {
|
||||||
|
validate_sha256_hex(sha256_hex_value)?;
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Err(CirStaticPoolError::EmptyBytes {
|
||||||
|
sha256_hex: sha256_hex_value.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let computed = compute_sha256_hex(bytes);
|
||||||
|
if computed != sha256_hex_value.to_ascii_lowercase() {
|
||||||
|
return Err(CirStaticPoolError::HashMismatch {
|
||||||
|
sha256_hex: sha256_hex_value.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_path = static_pool_path(static_root, capture_date_utc, sha256_hex_value)?;
|
||||||
|
if final_path.exists() {
|
||||||
|
return Ok(CirStaticPoolWriteResult {
|
||||||
|
final_path,
|
||||||
|
written: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = final_path.parent().expect("static pool file has parent");
|
||||||
|
fs::create_dir_all(parent).map_err(|e| CirStaticPoolError::CreateDir {
|
||||||
|
path: parent.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let temp_path = parent.join(format!("{sha256_hex_value}.tmp.{}", uuid::Uuid::new_v4()));
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create_new(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&temp_path)
|
||||||
|
.map_err(|e| CirStaticPoolError::CreateTemp {
|
||||||
|
path: temp_path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
file.write_all(bytes)
|
||||||
|
.map_err(|e| CirStaticPoolError::WriteTemp {
|
||||||
|
path: temp_path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
file.sync_all().map_err(|e| CirStaticPoolError::SyncTemp {
|
||||||
|
path: temp_path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
match fs::hard_link(&temp_path, &final_path) {
|
||||||
|
Ok(()) => {
|
||||||
|
fs::remove_file(&temp_path).map_err(|e| CirStaticPoolError::RemoveTemp {
|
||||||
|
path: temp_path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
Ok(CirStaticPoolWriteResult {
|
||||||
|
final_path,
|
||||||
|
written: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) if final_path.exists() => {
|
||||||
|
fs::remove_file(&temp_path).map_err(|remove_err| CirStaticPoolError::RemoveTemp {
|
||||||
|
path: temp_path.display().to_string(),
|
||||||
|
detail: remove_err.to_string(),
|
||||||
|
})?;
|
||||||
|
let _ = e;
|
||||||
|
Ok(CirStaticPoolWriteResult {
|
||||||
|
final_path,
|
||||||
|
written: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Err(CirStaticPoolError::Publish {
|
||||||
|
temp_path: temp_path.display().to_string(),
|
||||||
|
final_path: final_path.display().to_string(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_raw_entry_to_static_pool(
|
||||||
|
static_root: &Path,
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
entry: &RawByHashEntry,
|
||||||
|
) -> Result<CirStaticPoolWriteResult, CirStaticPoolError> {
|
||||||
|
write_bytes_to_static_pool(
|
||||||
|
static_root,
|
||||||
|
capture_date_utc,
|
||||||
|
&entry.sha256_hex,
|
||||||
|
&entry.bytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_hashes_from_store(
|
||||||
|
store: &RocksStore,
|
||||||
|
static_root: &Path,
|
||||||
|
capture_date_utc: time::Date,
|
||||||
|
sha256_hexes: &[String],
|
||||||
|
) -> Result<CirStaticPoolExportSummary, CirStaticPoolError> {
|
||||||
|
let unique: BTreeSet<String> = sha256_hexes
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.to_ascii_lowercase())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut written_files = 0usize;
|
||||||
|
let mut reused_files = 0usize;
|
||||||
|
for sha256_hex in &unique {
|
||||||
|
let bytes = store
|
||||||
|
.get_blob_bytes(sha256_hex)
|
||||||
|
.map_err(|e| CirStaticPoolError::Storage(e.to_string()))?
|
||||||
|
.ok_or_else(|| CirStaticPoolError::MissingRawByHash {
|
||||||
|
sha256_hex: sha256_hex.clone(),
|
||||||
|
})?;
|
||||||
|
let entry = RawByHashEntry::from_bytes(sha256_hex.clone(), bytes);
|
||||||
|
let result = write_raw_entry_to_static_pool(static_root, capture_date_utc, &entry)?;
|
||||||
|
if result.written {
|
||||||
|
written_files += 1;
|
||||||
|
} else {
|
||||||
|
reused_files += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CirStaticPoolExportSummary {
|
||||||
|
unique_hashes: unique.len(),
|
||||||
|
written_files,
|
||||||
|
reused_files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_utc_date(date: time::Date) -> String {
|
||||||
|
format!(
|
||||||
|
"{:04}{:02}{:02}",
|
||||||
|
date.year(),
|
||||||
|
u8::from(date.month()),
|
||||||
|
date.day()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sha256_hex(sha256_hex: &str) -> Result<(), CirStaticPoolError> {
|
||||||
|
if sha256_hex.len() != 64 || !sha256_hex.as_bytes().iter().all(u8::is_ascii_hexdigit) {
|
||||||
|
return Err(CirStaticPoolError::InvalidSha256Hex(sha256_hex.to_string()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_sha256_hex(bytes: &[u8]) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
hex::encode(Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
CirStaticPoolError, compute_sha256_hex, export_hashes_from_store,
|
||||||
|
static_pool_relative_path, write_bytes_to_static_pool,
|
||||||
|
};
|
||||||
|
use crate::storage::{RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn sample_date() -> time::Date {
|
||||||
|
time::Date::from_calendar_date(2026, time::Month::April, 7).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_pool_relative_path_uses_date_and_hash_prefixes() {
|
||||||
|
let path = static_pool_relative_path(
|
||||||
|
sample_date(),
|
||||||
|
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
||||||
|
)
|
||||||
|
.expect("relative path");
|
||||||
|
assert_eq!(
|
||||||
|
path,
|
||||||
|
std::path::PathBuf::from("20260407")
|
||||||
|
.join("ab")
|
||||||
|
.join("cd")
|
||||||
|
.join("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_bytes_to_static_pool_is_idempotent_and_leaves_no_temp_files() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let bytes = b"static-pool-object";
|
||||||
|
let sha = compute_sha256_hex(bytes);
|
||||||
|
|
||||||
|
let first =
|
||||||
|
write_bytes_to_static_pool(td.path(), sample_date(), &sha, bytes).expect("first write");
|
||||||
|
let second = write_bytes_to_static_pool(td.path(), sample_date(), &sha, bytes)
|
||||||
|
.expect("second write");
|
||||||
|
|
||||||
|
assert!(first.written);
|
||||||
|
assert!(!second.written);
|
||||||
|
assert_eq!(fs::read(&first.final_path).expect("read final"), bytes);
|
||||||
|
|
||||||
|
let all_files: Vec<_> = walk_files(td.path());
|
||||||
|
assert_eq!(all_files.len(), 1);
|
||||||
|
assert!(
|
||||||
|
!all_files[0]
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains(".tmp.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_bytes_to_static_pool_rejects_bad_hash_and_empty_bytes() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let err = write_bytes_to_static_pool(td.path(), sample_date(), "not-a-hash", b"x")
|
||||||
|
.expect_err("bad hash must fail");
|
||||||
|
assert!(matches!(err, CirStaticPoolError::InvalidSha256Hex(_)));
|
||||||
|
|
||||||
|
let err = write_bytes_to_static_pool(
|
||||||
|
td.path(),
|
||||||
|
sample_date(),
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
b"",
|
||||||
|
)
|
||||||
|
.expect_err("empty bytes must fail");
|
||||||
|
assert!(matches!(err, CirStaticPoolError::EmptyBytes { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_hashes_from_store_writes_unique_entries_and_fails_when_missing() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let store_dir = td.path().join("db");
|
||||||
|
let static_root = td.path().join("static");
|
||||||
|
let store = RocksStore::open(&store_dir).expect("open rocksdb");
|
||||||
|
|
||||||
|
let bytes = b"store-object".to_vec();
|
||||||
|
let sha = compute_sha256_hex(&bytes);
|
||||||
|
let mut entry = RawByHashEntry::from_bytes(sha.clone(), bytes.clone());
|
||||||
|
entry
|
||||||
|
.origin_uris
|
||||||
|
.push("rsync://example.test/repo/object.cer".to_string());
|
||||||
|
store.put_raw_by_hash_entry(&entry).expect("put raw entry");
|
||||||
|
store
|
||||||
|
.put_repository_view_entry(&RepositoryViewEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/object.cer".to_string(),
|
||||||
|
current_hash: Some(sha.clone()),
|
||||||
|
repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
|
||||||
|
object_type: Some("cer".to_string()),
|
||||||
|
state: RepositoryViewState::Present,
|
||||||
|
})
|
||||||
|
.expect("put repository view");
|
||||||
|
|
||||||
|
let summary = export_hashes_from_store(
|
||||||
|
&store,
|
||||||
|
&static_root,
|
||||||
|
sample_date(),
|
||||||
|
&[sha.clone(), sha.clone()],
|
||||||
|
)
|
||||||
|
.expect("export hashes");
|
||||||
|
assert_eq!(summary.unique_hashes, 1);
|
||||||
|
assert_eq!(summary.written_files, 1);
|
||||||
|
assert_eq!(summary.reused_files, 0);
|
||||||
|
|
||||||
|
let summary = export_hashes_from_store(&store, &static_root, sample_date(), &[sha.clone()])
|
||||||
|
.expect("re-export hashes");
|
||||||
|
assert_eq!(summary.unique_hashes, 1);
|
||||||
|
assert_eq!(summary.written_files, 0);
|
||||||
|
assert_eq!(summary.reused_files, 1);
|
||||||
|
|
||||||
|
let err = export_hashes_from_store(
|
||||||
|
&store,
|
||||||
|
&static_root,
|
||||||
|
sample_date(),
|
||||||
|
&[String::from(
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.expect_err("missing raw_by_hash must fail");
|
||||||
|
assert!(matches!(err, CirStaticPoolError::MissingRawByHash { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_files(root: &std::path::Path) -> Vec<std::path::PathBuf> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut stack = vec![root.to_path_buf()];
|
||||||
|
while let Some(path) = stack.pop() {
|
||||||
|
for entry in fs::read_dir(path).expect("read_dir") {
|
||||||
|
let entry = entry.expect("dir entry");
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
stack.push(path);
|
||||||
|
} else {
|
||||||
|
out.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
1117
src/cli.rs
1117
src/cli.rs
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,6 @@ pub mod roa;
|
|||||||
pub mod signed_object;
|
pub mod signed_object;
|
||||||
pub mod ta;
|
pub mod ta;
|
||||||
pub mod tal;
|
pub mod tal;
|
||||||
|
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod router_cert;
|
||||||
|
|||||||
@@ -68,3 +68,10 @@ pub const OID_AUTONOMOUS_SYS_IDS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1
|
|||||||
// RPKI CP (RFC 6484 / RFC 6487)
|
// RPKI CP (RFC 6484 / RFC 6487)
|
||||||
pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2";
|
pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2";
|
||||||
pub const OID_CP_IPADDR_ASNUMBER_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.14.2);
|
pub const OID_CP_IPADDR_ASNUMBER_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.14.2);
|
||||||
|
|
||||||
|
pub const OID_CT_RPKI_CCR: &str = "1.2.840.113549.1.9.16.1.54";
|
||||||
|
pub const OID_CT_RPKI_CCR_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.54);
|
||||||
|
|
||||||
|
pub const OID_KP_BGPSEC_ROUTER: &str = "1.3.6.1.5.5.7.3.30";
|
||||||
|
pub const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
|
||||||
|
pub const OID_SECP256R1: &str = "1.2.840.10045.3.1.7";
|
||||||
|
|||||||
361
src/data_model/router_cert.rs
Normal file
361
src/data_model/router_cert.rs
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
use crate::data_model::oid::{
|
||||||
|
OID_EC_PUBLIC_KEY, OID_EXTENDED_KEY_USAGE_RAW, OID_KP_BGPSEC_ROUTER, OID_SECP256R1,
|
||||||
|
};
|
||||||
|
use crate::data_model::rc::{
|
||||||
|
AsIdOrRange, AsIdentifierChoice, ResourceCertKind, ResourceCertificate,
|
||||||
|
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
|
||||||
|
};
|
||||||
|
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_predecoded_ee};
|
||||||
|
use x509_parser::extensions::ParsedExtension;
|
||||||
|
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||||
|
use x509_parser::public_key::PublicKey;
|
||||||
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct BgpsecRouterCertificateParsed {
|
||||||
|
pub rc_parsed: ResourceCertificateParsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct BgpsecRouterCertificate {
|
||||||
|
pub raw_der: Vec<u8>,
|
||||||
|
pub resource_cert: ResourceCertificate,
|
||||||
|
pub subject_key_identifier: Vec<u8>,
|
||||||
|
pub spki_der: Vec<u8>,
|
||||||
|
pub asns: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum BgpsecRouterCertificateParseError {
|
||||||
|
#[error("resource certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8209 §3.1)")]
|
||||||
|
ResourceCertificate(#[from] ResourceCertificateParseError),
|
||||||
|
|
||||||
|
#[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 8209 §3.1)")]
|
||||||
|
X509(String),
|
||||||
|
|
||||||
|
#[error("trailing bytes after router certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
||||||
|
TrailingBytes(usize),
|
||||||
|
|
||||||
|
#[error("router SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7; RFC 8208 §3.1)")]
|
||||||
|
SpkiParse(String),
|
||||||
|
|
||||||
|
#[error("trailing bytes after router SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 8208 §3.1)")]
|
||||||
|
SpkiTrailingBytes(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum BgpsecRouterCertificateProfileError {
|
||||||
|
#[error("resource certificate profile error: {0} (RFC 6487 §4; RFC 8209 §3.1)")]
|
||||||
|
ResourceCertificate(#[from] ResourceCertificateProfileError),
|
||||||
|
|
||||||
|
#[error("BGPsec router certificate must be an EE certificate (RFC 8209 §3.1)")]
|
||||||
|
NotEe,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8209 §3.3)"
|
||||||
|
)]
|
||||||
|
MissingSki,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate must include ExtendedKeyUsage (RFC 8209 §3.1.3.2; RFC 8209 §3.3)"
|
||||||
|
)]
|
||||||
|
MissingExtendedKeyUsage,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate ExtendedKeyUsage must be non-critical (RFC 6487 §4.8.4; RFC 8209 §3.1.3.2)"
|
||||||
|
)]
|
||||||
|
ExtendedKeyUsageCriticality,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate ExtendedKeyUsage must contain id-kp-bgpsec-router ({OID_KP_BGPSEC_ROUTER}) (RFC 8209 §3.1.3.2; RFC 8209 §3.3)"
|
||||||
|
)]
|
||||||
|
MissingBgpsecRouterEku,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate MUST NOT include Subject Information Access (RFC 8209 §3.1.3.3; RFC 8209 §3.3)"
|
||||||
|
)]
|
||||||
|
SubjectInfoAccessPresent,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate MUST NOT include IP resources extension (RFC 8209 §3.1.3.4; RFC 8209 §3.3)"
|
||||||
|
)]
|
||||||
|
IpResourcesPresent,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate MUST include AS resources extension (RFC 8209 §3.1.3.5; RFC 8209 §3.3)"
|
||||||
|
)]
|
||||||
|
AsResourcesMissing,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate AS resources MUST include one or more ASNs (RFC 8209 §3.1.3.5)"
|
||||||
|
)]
|
||||||
|
AsResourcesAsnumMissing,
|
||||||
|
|
||||||
|
#[error("BGPsec router certificate AS resources MUST NOT use inherit (RFC 8209 §3.1.3.5)")]
|
||||||
|
AsResourcesInherit,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate AS resources MUST contain explicit ASNs, not ranges (RFC 8209 §3.1.3.5)"
|
||||||
|
)]
|
||||||
|
AsResourcesRangeNotAllowed,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate subjectPublicKeyInfo.algorithm must be id-ecPublicKey ({OID_EC_PUBLIC_KEY}) (RFC 8208 §3.1)"
|
||||||
|
)]
|
||||||
|
SpkiAlgorithmNotEcPublicKey,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate subjectPublicKeyInfo.parameters must be secp256r1 ({OID_SECP256R1}) (RFC 8208 §3.1)"
|
||||||
|
)]
|
||||||
|
SpkiWrongCurve,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate subjectPublicKeyInfo.parameters missing or invalid (RFC 8208 §3.1)"
|
||||||
|
)]
|
||||||
|
SpkiParametersMissingOrInvalid,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"BGPsec router certificate subjectPublicKey MUST be uncompressed P-256 ECPoint (RFC 8208 §3.1)"
|
||||||
|
)]
|
||||||
|
SpkiEcPointNotUncompressedP256,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum BgpsecRouterCertificateDecodeError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Parse(#[from] BgpsecRouterCertificateParseError),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Validate(#[from] BgpsecRouterCertificateProfileError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum BgpsecRouterCertificatePathError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Decode(#[from] BgpsecRouterCertificateDecodeError),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
CertPath(#[from] CertPathError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BgpsecRouterCertificate {
|
||||||
|
pub fn parse_der(
|
||||||
|
der: &[u8],
|
||||||
|
) -> Result<BgpsecRouterCertificateParsed, BgpsecRouterCertificateParseError> {
|
||||||
|
let (rem, cert) = X509Certificate::from_der(der)
|
||||||
|
.map_err(|e| BgpsecRouterCertificateParseError::X509(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(BgpsecRouterCertificateParseError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
let (spki_rem, _spki) =
|
||||||
|
SubjectPublicKeyInfo::from_der(cert.tbs_certificate.subject_pki.raw)
|
||||||
|
.map_err(|e| BgpsecRouterCertificateParseError::SpkiParse(e.to_string()))?;
|
||||||
|
if !spki_rem.is_empty() {
|
||||||
|
return Err(BgpsecRouterCertificateParseError::SpkiTrailingBytes(
|
||||||
|
spki_rem.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let rc_parsed = ResourceCertificate::parse_der(der)?;
|
||||||
|
Ok(BgpsecRouterCertificateParsed { rc_parsed })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_profile(&self) -> Result<(), BgpsecRouterCertificateProfileError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, BgpsecRouterCertificateDecodeError> {
|
||||||
|
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_der(der: &[u8]) -> Result<Self, BgpsecRouterCertificateDecodeError> {
|
||||||
|
Self::decode_der(der)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_path_with_prevalidated_issuer(
|
||||||
|
der: &[u8],
|
||||||
|
issuer_ca: &ResourceCertificate,
|
||||||
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
|
issuer_crl: &crate::data_model::crl::RpkixCrl,
|
||||||
|
issuer_crl_revoked_serials: &std::collections::HashSet<Vec<u8>>,
|
||||||
|
issuer_ca_rsync_uri: Option<&str>,
|
||||||
|
issuer_crl_rsync_uri: Option<&str>,
|
||||||
|
validation_time: time::OffsetDateTime,
|
||||||
|
) -> Result<Self, BgpsecRouterCertificatePathError> {
|
||||||
|
let cert = Self::decode_der(der)?;
|
||||||
|
validate_ee_cert_path_with_predecoded_ee(
|
||||||
|
&cert.resource_cert,
|
||||||
|
der,
|
||||||
|
issuer_ca,
|
||||||
|
issuer_spki,
|
||||||
|
issuer_crl,
|
||||||
|
issuer_crl_revoked_serials,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
issuer_crl_rsync_uri,
|
||||||
|
validation_time,
|
||||||
|
)?;
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BgpsecRouterCertificateParsed {
|
||||||
|
pub fn validate_profile(
|
||||||
|
self,
|
||||||
|
) -> Result<BgpsecRouterCertificate, BgpsecRouterCertificateProfileError> {
|
||||||
|
let rc = self.rc_parsed.validate_profile()?;
|
||||||
|
if rc.kind != ResourceCertKind::Ee {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::NotEe);
|
||||||
|
}
|
||||||
|
let ski = rc
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_key_identifier
|
||||||
|
.clone()
|
||||||
|
.ok_or(BgpsecRouterCertificateProfileError::MissingSki)?;
|
||||||
|
|
||||||
|
if rc.tbs.extensions.subject_info_access.is_some() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SubjectInfoAccessPresent);
|
||||||
|
}
|
||||||
|
if rc.tbs.extensions.ip_resources.is_some() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::IpResourcesPresent);
|
||||||
|
}
|
||||||
|
let as_resources = rc
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.as_resources
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(BgpsecRouterCertificateProfileError::AsResourcesMissing)?;
|
||||||
|
let asns = extract_router_asns(as_resources)?;
|
||||||
|
|
||||||
|
let (rem, cert) = X509Certificate::from_der(&rc.raw_der).map_err(|e| {
|
||||||
|
BgpsecRouterCertificateProfileError::ResourceCertificate(
|
||||||
|
ResourceCertificateProfileError::InvalidCertificatePolicy(e.to_string()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::ResourceCertificate(
|
||||||
|
ResourceCertificateProfileError::InvalidCertificatePolicy(format!(
|
||||||
|
"trailing bytes after router certificate DER: {}",
|
||||||
|
rem.len()
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
validate_router_eku(&cert)?;
|
||||||
|
validate_router_spki(&rc.tbs.subject_public_key_info)?;
|
||||||
|
|
||||||
|
Ok(BgpsecRouterCertificate {
|
||||||
|
raw_der: rc.raw_der.clone(),
|
||||||
|
resource_cert: rc.clone(),
|
||||||
|
subject_key_identifier: ski,
|
||||||
|
spki_der: rc.tbs.subject_public_key_info.clone(),
|
||||||
|
asns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_router_asns(
|
||||||
|
as_resources: &crate::data_model::rc::AsResourceSet,
|
||||||
|
) -> Result<Vec<u32>, BgpsecRouterCertificateProfileError> {
|
||||||
|
let asnum = as_resources
|
||||||
|
.asnum
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(BgpsecRouterCertificateProfileError::AsResourcesAsnumMissing)?;
|
||||||
|
if matches!(asnum, AsIdentifierChoice::Inherit)
|
||||||
|
|| matches!(as_resources.rdi.as_ref(), Some(AsIdentifierChoice::Inherit))
|
||||||
|
{
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::AsResourcesInherit);
|
||||||
|
}
|
||||||
|
let AsIdentifierChoice::AsIdsOrRanges(items) = asnum else {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::AsResourcesInherit);
|
||||||
|
};
|
||||||
|
if items.is_empty() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::AsResourcesAsnumMissing);
|
||||||
|
}
|
||||||
|
let mut asns = Vec::with_capacity(items.len());
|
||||||
|
for item in items {
|
||||||
|
match item {
|
||||||
|
AsIdOrRange::Id(v) => asns.push(*v),
|
||||||
|
AsIdOrRange::Range { .. } => {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::AsResourcesRangeNotAllowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asns.sort_unstable();
|
||||||
|
asns.dedup();
|
||||||
|
Ok(asns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_router_eku(
|
||||||
|
cert: &X509Certificate<'_>,
|
||||||
|
) -> Result<(), BgpsecRouterCertificateProfileError> {
|
||||||
|
let mut matches = cert
|
||||||
|
.tbs_certificate
|
||||||
|
.extensions()
|
||||||
|
.iter()
|
||||||
|
.filter(|ext| ext.oid.as_bytes() == OID_EXTENDED_KEY_USAGE_RAW);
|
||||||
|
let Some(ext) = matches.next() else {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage);
|
||||||
|
};
|
||||||
|
if matches.next().is_some() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage);
|
||||||
|
}
|
||||||
|
if ext.critical {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::ExtendedKeyUsageCriticality);
|
||||||
|
}
|
||||||
|
let ParsedExtension::ExtendedKeyUsage(eku) = ext.parsed_extension() else {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage);
|
||||||
|
};
|
||||||
|
let found = eku
|
||||||
|
.other
|
||||||
|
.iter()
|
||||||
|
.any(|oid| oid.to_id_string() == OID_KP_BGPSEC_ROUTER);
|
||||||
|
if !found {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_router_spki(spki_der: &[u8]) -> Result<(), BgpsecRouterCertificateProfileError> {
|
||||||
|
let (rem, spki) = SubjectPublicKeyInfo::from_der(spki_der)
|
||||||
|
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid)?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
|
||||||
|
}
|
||||||
|
if spki.algorithm.algorithm.to_id_string() != OID_EC_PUBLIC_KEY {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey);
|
||||||
|
}
|
||||||
|
let Some(params) = spki.algorithm.parameters.as_ref() else {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
|
||||||
|
};
|
||||||
|
if params.header.tag().0 != 0x06 {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
|
||||||
|
}
|
||||||
|
let mut der = Vec::with_capacity(params.data.len() + 2);
|
||||||
|
der.push(0x06);
|
||||||
|
if params.data.len() >= 0x80 {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
|
||||||
|
}
|
||||||
|
der.push(params.data.len() as u8);
|
||||||
|
der.extend_from_slice(params.data);
|
||||||
|
let (prem, oid) = der_parser::der::parse_der_oid(&der)
|
||||||
|
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid)?;
|
||||||
|
if !prem.is_empty() {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
|
||||||
|
}
|
||||||
|
let curve = oid
|
||||||
|
.as_oid_val()
|
||||||
|
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid)?
|
||||||
|
.to_string();
|
||||||
|
if curve != OID_SECP256R1 {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiWrongCurve);
|
||||||
|
}
|
||||||
|
let parsed = spki
|
||||||
|
.parsed()
|
||||||
|
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256)?;
|
||||||
|
let PublicKey::EC(ec) = parsed else {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey);
|
||||||
|
};
|
||||||
|
if ec.data().len() != 65 || ec.data().first() != Some(&0x04) {
|
||||||
|
return Err(BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user