File size: 18,202 Bytes
c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 c427231 4be6a52 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | """CPU-only verification of downloaded repositories against a trusted local manifest.
The trust anchor must come from independently retained local release preparation,
not from the same unverified download. This checks integrity against that anchor,
not publisher authenticity, model output parity, or whether a download was fresh.
Model and dataset are always required. Demo verification requires its directory,
unless the caller explicitly selects --skip-demo; skipped scope is recorded.
Only these unlisted Hugging Face bookkeeping files are tolerated: root
.gitattributes; .cache/huggingface/{.gitignore,.gitignore.lock,CACHEDIR.TAG}; and
download metadata/empty locks corresponding to expected payloads or .gitattributes;
and complete revision-bound tree caches matching those payloads and their hashes.
Incomplete downloads, upload metadata, arbitrary cache files and symlinks fail.
Ignored files are individually listed in the verification report.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import re
import stat
from pathlib import Path, PurePosixPath
from typing import Any
from stackcraft.data import audit_dataset
BUNDLES = ("model", "dataset", "demo")
HEX64 = re.compile(r"[0-9a-f]{64}\Z")
HEX_COMMIT = re.compile(r"[0-9a-f]{40}\Z")
CACHE_TAG_PREFIX = "Signature: 8a477f597d28d172789f06886806bc55\n"
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1_048_576):
digest.update(chunk)
return digest.hexdigest()
def safe_relative(value: Any) -> PurePosixPath:
if not isinstance(value, str) or not value or "\\" in value or ":" in value or "\x00" in value:
raise ValueError("manifest paths must be nonempty portable relative paths")
path = PurePosixPath(value)
if path.is_absolute() or ".." in path.parts or not path.parts or path.as_posix() != value:
raise ValueError("manifest paths must be canonical and cannot traverse directories")
return path
def regular_input(path: Path, *, directory: bool = False) -> Path:
# Resolve only after rejecting links in every supplied path component.
absolute = path.absolute()
if any(parent.is_symlink() for parent in (absolute, *absolute.parents)):
raise ValueError(f"input paths cannot contain symlinks: {path}")
if not (path.is_dir() if directory else path.is_file()):
raise ValueError(f"expected {'directory' if directory else 'regular file'}: {path}")
if not directory and not stat.S_ISREG(path.stat().st_mode):
raise ValueError(f"input must be a regular file: {path}")
return path.resolve()
def _file_map(value: Any) -> dict[str, dict[str, Any]]:
if not isinstance(value, dict) or not value:
raise ValueError("manifest files must be a nonempty mapping")
for name, entry in value.items():
safe_relative(name)
if not isinstance(entry, dict) or set(entry) != {"sha256", "bytes"}:
raise ValueError("file manifest entries need exact sha256 and bytes fields")
if not isinstance(entry["sha256"], str) or not HEX64.fullmatch(entry["sha256"]):
raise ValueError("file SHA256 must be lowercase hexadecimal")
if type(entry["bytes"]) is not int or entry["bytes"] < 0:
raise ValueError("file sizes must be nonnegative integers")
return value
def _inventory(root: Path) -> tuple[dict[str, Path], set[str]]:
files = {}
directories = set()
for folder, names, filenames in os.walk(root, followlinks=False):
for name in (*names, *filenames):
path = Path(folder) / name
relative = path.relative_to(root).as_posix()
safe_relative(relative)
mode = path.lstat().st_mode
if stat.S_ISLNK(mode):
raise ValueError(f"download contains a symlink: {relative}")
if stat.S_ISDIR(mode):
directories.add(relative)
elif stat.S_ISREG(mode):
files[relative] = path
else:
raise ValueError(f"download contains a non-regular payload: {relative}")
return files, directories
def _hf_metadata(relative: str, path: Path, expected: set[str]) -> bool:
if relative == ".gitattributes":
return True # HF creates this repository file; it is not model/data code.
if relative == ".cache/huggingface/.gitignore":
return path.stat().st_size <= 2 and path.read_bytes() in (b"*", b"*\n")
if relative == ".cache/huggingface/.gitignore.lock":
return path.stat().st_size == 0
if relative == ".cache/huggingface/CACHEDIR.TAG":
if path.stat().st_size > 4096:
return False
text = path.read_text()
return text.startswith(CACHE_TAG_PREFIX) and all(
not line or line.startswith("#") for line in text.splitlines()[1:]
)
prefix = ".cache/huggingface/download/"
if not relative.startswith(prefix):
return False
suffix = (
".metadata"
if relative.endswith(".metadata")
else ".lock"
if relative.endswith(".lock")
else None
)
if suffix is None:
return False
payload = relative[len(prefix) : -len(suffix)]
if payload not in expected | {".gitattributes"}:
return False
if suffix == ".lock":
return path.stat().st_size == 0
if path.stat().st_size > 4096:
return False
lines = path.read_text().splitlines()
if (
len(lines) != 3
or not HEX_COMMIT.fullmatch(lines[0])
or not (HEX_COMMIT.fullmatch(lines[1]) or HEX64.fullmatch(lines[1]))
):
return False
try:
timestamp = float(lines[2])
except ValueError:
return False
return math.isfinite(timestamp) and timestamp >= 0
def _parent_directories(paths: set[str]) -> set[str]:
return {
parent.as_posix()
for value in paths
for parent in PurePosixPath(value).parents
if parent.as_posix() != "."
}
def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError("tree cache contains duplicate JSON keys")
result[key] = value
return result
def _hf_tree_cache(
relative: str,
path: Path,
expected: dict[str, dict[str, Any]],
actual: dict[str, Path],
) -> bool:
"""Validate HF 1.33's full local-dir tree listing, never arbitrary cache JSON.
Cache revisions must match each payload's download metadata. These local
consistency checks do not independently authenticate a Hub commit; payload
SHA256 remains bound to the caller's trusted release manifest.
"""
match = re.fullmatch(r"\.cache/huggingface/trees/([0-9a-f]{40})\.json", relative)
if match is None:
return False
payloads = set(expected) | ({".gitattributes"} if ".gitattributes" in actual else set())
# More than enough room for the documented fields and JSON-escaped names.
limit = 4096 + sum(6 * len(name.encode()) + 1024 for name in payloads)
if path.stat().st_size > limit:
return False
try:
tree = json.loads(path.read_text(), object_pairs_hook=_unique_json_object)
if (
not isinstance(tree, dict)
or set(tree) != {"format_version", "files"}
or type(tree["format_version"]) is not int
or tree["format_version"] != 1
or not isinstance(tree["files"], dict)
or set(tree["files"]) != payloads
):
return False
for name, info in tree["files"].items():
safe_relative(name)
if not isinstance(info, dict) or not {"size", "blob_id"} <= set(info):
return False
if set(info) - {"size", "blob_id", "lfs_sha256", "lfs_size", "xet_hash"}:
return False
payload = actual[name]
size = expected[name]["bytes"] if name in expected else payload.stat().st_size
if (
type(info["size"]) is not int
or info["size"] != size
or payload.stat().st_size != size
or not isinstance(info["blob_id"], str)
or not HEX_COMMIT.fullmatch(info["blob_id"])
):
return False
lfs = "lfs_sha256" in info or "lfs_size" in info
if lfs:
if (
name not in expected
or info.get("lfs_sha256") != expected[name]["sha256"]
or type(info.get("lfs_size")) is not int
or info["lfs_size"] != size
):
return False
etag = info["lfs_sha256"]
else:
digest = hashlib.sha1(f"blob {size}\0".encode())
with payload.open("rb") as stream:
while chunk := stream.read(1_048_576):
digest.update(chunk)
if digest.hexdigest() != info["blob_id"]:
return False
etag = info["blob_id"]
if "xet_hash" in info and (
not lfs
or not isinstance(info["xet_hash"], str)
or not re.fullmatch(r"[0-9a-fA-F]{64}", info["xet_hash"])
):
return False
metadata_name = f".cache/huggingface/download/{name}.metadata"
metadata = actual.get(metadata_name)
if metadata is None or not _hf_metadata(metadata_name, metadata, set(expected)):
return False
lines = metadata.read_text().splitlines()
if lines[:2] != [match[1], etag]:
return False
except (OSError, ValueError, KeyError, TypeError):
return False
return True
def verify_bundle(
root: Path, expected: dict[str, dict[str, Any]], checkpoint: Any
) -> dict[str, Any]:
actual, directories = _inventory(root)
if missing := set(expected) - set(actual):
raise ValueError(f"download is missing payloads: {sorted(missing)}")
ignored = []
for name in sorted(set(actual) - set(expected)):
if not _hf_metadata(name, actual[name], set(expected)) and not _hf_tree_cache(
name, actual[name], expected, actual
):
raise ValueError(f"download contains unexpected payload: {name}")
ignored.append(name)
allowed_directories = _parent_directories(set(expected) | set(ignored))
# HF can leave empty bookkeeping directories after finishing a download.
allowed_directories.update({".cache", ".cache/huggingface", ".cache/huggingface/download"})
allowed_directories.update(
_parent_directories({f".cache/huggingface/download/{name}.metadata" for name in expected})
)
if extra_directories := directories - allowed_directories:
raise ValueError(f"download contains unexpected directories: {sorted(extra_directories)}")
for name, entry in expected.items():
path = actual[name]
if path.stat().st_size != entry["bytes"]:
raise ValueError(f"payload size mismatch: {name}")
if sha256(path) != entry["sha256"]:
raise ValueError(f"payload SHA256 mismatch: {name}")
if "release-files.json" not in expected:
raise ValueError("trusted manifest must include each uploaded release-files.json")
uploaded = json.loads(actual["release-files.json"].read_text())
if (
type(uploaded.get("schema_version")) is not int
or uploaded.get("manifest_excludes_itself") is not True
):
raise ValueError("uploaded release-files.json has invalid schema fields")
_file_map(uploaded.get("files"))
expected_uploaded = {
"schema_version": 1,
"manifest_excludes_itself": True,
"checkpoint_sha256": checkpoint,
"files": {key: value for key, value in expected.items() if key != "release-files.json"},
}
if uploaded != expected_uploaded:
raise ValueError("uploaded release-files.json contradicts the trusted root manifest")
return {
"verified_files": len(expected),
"verified_bytes": sum(entry["bytes"] for entry in expected.values()),
"release_files_sha256": expected["release-files.json"]["sha256"],
"ignored_huggingface_metadata": ignored,
}
def verify_release(
trusted_manifest: Path,
roots: dict[str, Path],
output: Path,
*,
skip_demo: bool = False,
) -> dict[str, Any]:
if output.exists() or output.is_symlink():
raise FileExistsError("verification output already exists; choose a new file")
trusted_manifest = regular_input(trusted_manifest)
if type(skip_demo) is not bool:
raise ValueError("skip_demo must be an explicit boolean")
bundles = BUNDLES[:2] if skip_demo else BUNDLES
if set(roots) != set(bundles):
raise ValueError(
"skip-demo verification needs only model and dataset directories"
if skip_demo
else "verification needs model, dataset and demo directories"
)
roots = {name: regular_input(path, directory=True) for name, path in roots.items()}
for name, root in roots.items():
if any(
root.is_relative_to(other) or other.is_relative_to(root)
for key, other in roots.items()
if key != name
):
raise ValueError("download roots must be separate, non-overlapping directories")
if output.resolve().is_relative_to(root):
raise ValueError("verification output must be outside downloaded payloads")
manifest = json.loads(trusted_manifest.read_text())
if (
type(manifest.get("schema_version")) is not int
or manifest["schema_version"] != 1
or manifest.get("kind") != "local-release-bundle"
or manifest.get("manifest_excludes_itself") is not True
):
raise ValueError("unsupported trusted release manifest")
files = _file_map(manifest.get("files"))
expected: dict[str, dict[str, dict[str, Any]]] = {bundle: {} for bundle in BUNDLES}
for relative, entry in files.items():
path = safe_relative(relative)
if len(path.parts) < 2 or path.parts[0] not in expected:
raise ValueError("trusted release files must belong to model, dataset or demo")
expected[path.parts[0]][PurePosixPath(*path.parts[1:]).as_posix()] = entry
checkpoint = manifest.get("checkpoint_sha256")
if not isinstance(checkpoint, dict) or not checkpoint:
raise ValueError("trusted release needs a checkpoint hash mapping")
for relative, digest in checkpoint.items():
safe_relative(relative)
if not isinstance(digest, str) or not HEX64.fullmatch(digest):
raise ValueError("trusted checkpoint SHA256 must be lowercase hexadecimal")
if expected["model"].get(f"checkpoint/{relative}", {}).get("sha256") != digest:
raise ValueError("trusted checkpoint hashes disagree with payload inventory")
verified = {
bundle: verify_bundle(
roots[bundle], expected[bundle], checkpoint if bundle == "model" else None
)
for bundle in bundles
}
dataset = roots["dataset"]
if sha256(dataset / "manifest.json") != manifest.get("dataset_manifest_sha256"):
raise ValueError("dataset manifest differs from the trusted dataset identity")
dataset_manifest = json.loads((dataset / "manifest.json").read_text())
records = {
split: [json.loads(line) for line in (dataset / f"{split}.jsonl").read_text().splitlines()]
for split in ("train", "validation")
}
counts = audit_dataset(records, dataset_manifest)
result = {
"schema_version": 1,
"status": "verified",
"scope": (
"CPU model/dataset payload integrity and dataset audit against caller-supplied "
"trust anchor; demo explicitly skipped and not checked"
if skip_demo
else "CPU model/dataset/demo payload integrity and dataset audit against "
"caller-supplied trust anchor"
),
"skipped_bundles": ["demo"] if skip_demo else [],
"trusted_manifest_sha256": sha256(trusted_manifest),
"bundles": verified,
"dataset_counts": counts,
"dataset_manifest_sha256": manifest["dataset_manifest_sha256"],
"gpu_parity_checked": False,
"publisher_authenticity_checked": False,
"download_freshness_checked": False,
}
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("x") as stream:
stream.write(json.dumps(result, indent=2, sort_keys=True, allow_nan=False) + "\n")
return result
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--trusted-manifest", type=Path, required=True)
for bundle in BUNDLES[:2]:
parser.add_argument(f"--{bundle}", type=Path, required=True)
demo = parser.add_mutually_exclusive_group(required=True)
demo.add_argument("--demo", type=Path, help="Downloaded demo directory to verify")
demo.add_argument(
"--skip-demo", action="store_true", help="Explicitly verify only model and dataset"
)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
try:
report = verify_release(
args.trusted_manifest,
{
bundle: getattr(args, bundle)
for bundle in (BUNDLES[:2] if args.skip_demo else BUNDLES)
},
args.output,
skip_demo=args.skip_demo,
)
except (OSError, ValueError, KeyError, TypeError) as error:
parser.error(str(error))
print(
json.dumps(
{
"status": report["status"],
"output": str(args.output),
"gpu_parity_checked": False,
"skipped_bundles": report["skipped_bundles"],
}
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|