blockers 2
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-23 14:54:17 +02:00
parent f47d2d1377
commit cce96f3596
100 changed files with 2758 additions and 1912 deletions

9
scripts/mirror/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Mirror signing helpers
- `make-thin-v1.sh`: builds thin bundle v1, computes checksums, optional DSSE+TUF signing when `SIGN_KEY` is set, and runs verifier.
- `sign_thin_bundle.py`: signs manifest (DSSE) and root/targets/snapshot/timestamp JSON using an Ed25519 PEM key.
- `verify_thin_bundle.py`: checks SHA256 sidecars, manifest schema, tar determinism, and manifest/index digests.
- `ci-sign.sh`: CI wrapper. Set `MIRROR_SIGN_KEY_B64` (base64-encoded Ed25519 PEM) and run; it builds, signs, and verifies in one step.
- `verify_oci_layout.py`: validates OCI layout/index/manifest and blob digests when `OCI=1` is used.
Artifacts live under `out/mirror/thin/`.

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Verifies signing prerequisites without requiring the actual key contents.
set -euo pipefail
if [[ -z "${MIRROR_SIGN_KEY_B64:-}" ]]; then
echo "MIRROR_SIGN_KEY_B64 is not set" >&2
exit 2
fi
# basic base64 sanity check
if ! printf "%s" "$MIRROR_SIGN_KEY_B64" | base64 -d >/dev/null 2>&1; then
echo "MIRROR_SIGN_KEY_B64 is not valid base64" >&2
exit 3
fi
# ensure scripts exist
for f in scripts/mirror/ci-sign.sh scripts/mirror/sign_thin_bundle.py scripts/mirror/verify_thin_bundle.py; do
[[ -x "$f" || -f "$f" ]] || { echo "$f missing" >&2; exit 4; }
done
echo "Signing prerequisites present (key env set, scripts available)."

12
scripts/mirror/ci-sign.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
: "${MIRROR_SIGN_KEY_B64:?set MIRROR_SIGN_KEY_B64 to base64-encoded Ed25519 PEM private key}"
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
KEYDIR="$ROOT/out/mirror/thin/tuf/keys"
mkdir -p "$KEYDIR"
KEYFILE="$KEYDIR/ci-ed25519.pem"
printf "%s" "$MIRROR_SIGN_KEY_B64" | base64 -d > "$KEYFILE"
chmod 600 "$KEYFILE"
STAGE=${STAGE:-$ROOT/out/mirror/thin/stage-v1}
CREATED=${CREATED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
SIGN_KEY="$KEYFILE" STAGE="$STAGE" CREATED="$CREATED" "$ROOT/src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh"

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Sign mirror-thin-v1 artefacts using an Ed25519 key and emit DSSE + TUF signatures.
Usage:
python scripts/mirror/sign_thin_bundle.py \
--key out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pem \
--manifest out/mirror/thin/mirror-thin-v1.manifest.json \
--tar out/mirror/thin/mirror-thin-v1.tar.gz \
--tuf-dir out/mirror/thin/tuf
Writes:
- mirror-thin-v1.manifest.dsse.json
- updates signatures in root.json, targets.json, snapshot.json, timestamp.json
"""
import argparse, base64, json, pathlib, hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def load_key(path: pathlib.Path) -> Ed25519PrivateKey:
return serialization.load_pem_private_key(path.read_bytes(), password=None)
def keyid_from_pub(pub_path: pathlib.Path) -> str:
raw = pub_path.read_bytes()
return hashlib.sha256(raw).hexdigest()
def sign_bytes(key: Ed25519PrivateKey, data: bytes) -> bytes:
return key.sign(data)
def write_json(path: pathlib.Path, obj):
path.write_text(json.dumps(obj, indent=2, sort_keys=True) + "\n")
def sign_tuf(path: pathlib.Path, keyid: str, key: Ed25519PrivateKey):
data = path.read_bytes()
sig = sign_bytes(key, data)
obj = json.loads(data)
obj["signatures"] = [{"keyid": keyid, "sig": b64url(sig)}]
write_json(path, obj)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--key", required=True, type=pathlib.Path)
ap.add_argument("--manifest", required=True, type=pathlib.Path)
ap.add_argument("--tar", required=True, type=pathlib.Path)
ap.add_argument("--tuf-dir", required=True, type=pathlib.Path)
args = ap.parse_args()
key = load_key(args.key)
pub_path = args.key.with_suffix(".pub")
keyid = keyid_from_pub(pub_path)
manifest_bytes = args.manifest.read_bytes()
sig = sign_bytes(key, manifest_bytes)
dsse = {
"payloadType": "application/vnd.stellaops.mirror.manifest+json",
"payload": b64url(manifest_bytes),
"signatures": [{"keyid": keyid, "sig": b64url(sig)}],
}
dsse_path = args.manifest.with_suffix(".dsse.json")
write_json(dsse_path, dsse)
# update TUF metadata
for name in ["root.json", "targets.json", "snapshot.json", "timestamp.json"]:
sign_tuf(args.tuf_dir / name, keyid, key)
print(f"Signed DSSE + TUF using keyid {keyid}; DSSE -> {dsse_path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Verify OCI layout emitted by make-thin-v1.sh when OCI=1.
Checks:
1) oci-layout exists and version is 1.0.0
2) index.json manifest digest/size match manifest.json hash/size
3) manifest.json references config/layers present in blobs with matching sha256 and size
Usage:
python scripts/mirror/verify_oci_layout.py out/mirror/thin/oci
Exit 0 on success, non-zero on failure with message.
"""
import hashlib, json, pathlib, sys
def sha256(path: pathlib.Path) -> str:
h = hashlib.sha256()
with path.open('rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
h.update(chunk)
return h.hexdigest()
def main():
if len(sys.argv) != 2:
print(__doc__)
sys.exit(2)
root = pathlib.Path(sys.argv[1])
layout = root / "oci-layout"
index = root / "index.json"
manifest = root / "manifest.json"
if not layout.exists() or not index.exists() or not manifest.exists():
raise SystemExit("missing oci-layout/index.json/manifest.json")
layout_obj = json.loads(layout.read_text())
if layout_obj.get("imageLayoutVersion") != "1.0.0":
raise SystemExit("oci-layout version not 1.0.0")
idx_obj = json.loads(index.read_text())
if not idx_obj.get("manifests"):
raise SystemExit("index.json manifests empty")
man_digest = idx_obj["manifests"][0]["digest"]
man_size = idx_obj["manifests"][0]["size"]
actual_man_sha = sha256(manifest)
if man_digest != f"sha256:{actual_man_sha}":
raise SystemExit(f"manifest digest mismatch: {man_digest} vs sha256:{actual_man_sha}")
if man_size != manifest.stat().st_size:
raise SystemExit("manifest size mismatch")
man_obj = json.loads(manifest.read_text())
blobs = root / "blobs" / "sha256"
# config
cfg_digest = man_obj["config"]["digest"].split(":",1)[1]
cfg_size = man_obj["config"]["size"]
cfg_path = blobs / cfg_digest
if not cfg_path.exists():
raise SystemExit(f"config blob missing: {cfg_path}")
if cfg_path.stat().st_size != cfg_size:
raise SystemExit("config size mismatch")
if sha256(cfg_path) != cfg_digest:
raise SystemExit("config digest mismatch")
for layer in man_obj.get("layers", []):
ldigest = layer["digest"].split(":",1)[1]
lsize = layer["size"]
lpath = blobs / ldigest
if not lpath.exists():
raise SystemExit(f"layer blob missing: {lpath}")
if lpath.stat().st_size != lsize:
raise SystemExit("layer size mismatch")
if sha256(lpath) != ldigest:
raise SystemExit("layer digest mismatch")
print("OK: OCI layout verified")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Simple verifier for mirror-thin-v1 artefacts.
Checks:
1) SHA256 of manifest and tarball matches provided .sha256 files.
2) Manifest schema has required fields.
3) Tarball contains manifest.json, layers/, indexes/ with deterministic tar headers (mtime=0, uid/gid=0, sorted paths).
4) Tar content digests match manifest entries.
Usage:
python scripts/mirror/verify_thin_bundle.py out/mirror/thin/mirror-thin-v1.manifest.json out/mirror/thin/mirror-thin-v1.tar.gz
Exit code 0 on success; non-zero on any check failure.
"""
import json, tarfile, hashlib, sys, pathlib
REQUIRED_FIELDS = ["version", "created", "layers", "indexes"]
def sha256_file(path: pathlib.Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
def load_sha256_sidecar(path: pathlib.Path) -> str:
sidecar = path.with_suffix(path.suffix + ".sha256")
if not sidecar.exists():
raise SystemExit(f"missing sidecar {sidecar}")
return sidecar.read_text().strip().split()[0]
def check_schema(manifest: dict):
missing = [f for f in REQUIRED_FIELDS if f not in manifest]
if missing:
raise SystemExit(f"manifest missing fields: {missing}")
def normalize(name: str) -> str:
return name[2:] if name.startswith("./") else name
def check_tar_determinism(tar_path: pathlib.Path):
with tarfile.open(tar_path, "r:gz") as tf:
names = [normalize(n) for n in tf.getnames()]
if names != sorted(names):
raise SystemExit("tar entries not sorted")
for m in tf.getmembers():
if m.uid != 0 or m.gid != 0:
raise SystemExit(f"tar header uid/gid not zero for {m.name}")
if m.mtime != 0:
raise SystemExit(f"tar header mtime not zero for {m.name}")
def check_content_hashes(manifest: dict, tar_path: pathlib.Path):
with tarfile.open(tar_path, "r:gz") as tf:
def get(name: str):
try:
return tf.getmember(name)
except KeyError:
# retry with leading ./
return tf.getmember(f"./{name}")
for layer in manifest.get("layers", []):
name = layer["path"]
info = get(name)
data = tf.extractfile(info).read()
digest = hashlib.sha256(data).hexdigest()
if layer["digest"] != f"sha256:{digest}":
raise SystemExit(f"layer digest mismatch {name}: {digest}")
for idx in manifest.get("indexes", []):
name = idx['name']
if not name.startswith("indexes/"):
name = f"indexes/{name}"
info = get(name)
data = tf.extractfile(info).read()
digest = hashlib.sha256(data).hexdigest()
if idx["digest"] != f"sha256:{digest}":
raise SystemExit(f"index digest mismatch {name}: {digest}")
def main():
if len(sys.argv) != 3:
print(__doc__)
sys.exit(2)
manifest_path = pathlib.Path(sys.argv[1])
tar_path = pathlib.Path(sys.argv[2])
man_expected = load_sha256_sidecar(manifest_path)
tar_expected = load_sha256_sidecar(tar_path)
if sha256_file(manifest_path) != man_expected:
raise SystemExit("manifest sha256 mismatch")
if sha256_file(tar_path) != tar_expected:
raise SystemExit("tarball sha256 mismatch")
manifest = json.loads(manifest_path.read_text())
check_schema(manifest)
check_tar_determinism(tar_path)
check_content_hashes(manifest, tar_path)
print("OK: mirror-thin bundle verified")
if __name__ == "__main__":
main()