AID-H-013.002: White-Box Artifact & Tensor-State Ownership Verification
Pillars: model
Lifecycle: building, validation, operation
Scope boundary
Owns exact authorized suspect-byte comparison by recomputing complete artifact-tree and canonical tensor-state digests against a signature-verified release fingerprint. A no-match result does not prove unrelated lineage after transformation.
AID-H-003.006: AID-H-013.002 independently compares suspect bytes with the release fingerprint; AID-H-003.006 produces and signs the model SBOM and provenance fingerprint manifest. Fingerprint production and independent suspect comparison must remain separate.
AID-H-013.003: AID-H-013.002 performs exact white-box artifact and tensor comparison when complete suspect bytes are available; AID-H-013.003 performs calibrated black-box behavioral challenges when only a compatible endpoint is accessible. White-box exact match is stronger but less broadly applicable than statistical endpoint evidence.
Warning
Exact-Match Evidence, Not General Lineage Proof
Artifact-tree and canonical tensor-state comparison is strong for byte-identical or canonical tensor-identical copies, but expected transformations can erase an exact match. Treat a positive match as bounded technical attribution evidence and a no-exact-match result only as absence of this signal, not proof that the suspect model is unrelated.
Implementation guidance
- AID-H-013.002-G001: Compare authorized model artifacts against a signed release fingerprint using both complete artifact-tree and canonical safetensors tensor-state digests.
AID-H-013.002-G001
Use this white-box control only when investigators are authorized to read the complete suspect model artifact and both the reference and suspect use safetensors for all model weights. It consumes a signed fingerprint manifest exported by AID-H-003.006 after release. The consumer verifies the producer identity, schema, reference asset/version, complete file population, artifact-tree digest, canonical tensor-state digest, tokenizer/config digests, signature, and freshness. The upstream PASS is dependency evidence and never becomes this control's PASS.
Runnable signed-manifest loader and canonical file inventory
# File: model_ip/whitebox_fingerprint_verifier.py
from __future__ import annotations
import hashlib
import hmac
import json
import math
import os
import subprocess
import tempfile
from contextlib import ExitStack
from datetime import datetime, timezone
from pathlib import Path
import torch
from safetensors import safe_open
def _required_positive_seconds(name: str) -> float:
value = float(os.environ[name])
if not math.isfinite(value) or value <= 0:
raise RuntimeError(f"{name} must be a finite positive number of seconds")
return value
MODEL_FINGERPRINT_MANIFEST_VERIFY_TIMEOUT_SECONDS = _required_positive_seconds("VERIFIED_H013_MODEL_FINGERPRINT_MANIFEST_VERIFY_TIMEOUT_SECONDS")
class NotApplicable(RuntimeError):
pass
class InsufficientData(RuntimeError):
pass
class ContractViolation(RuntimeError):
pass
MANIFEST_FIELDS = {
"schema_version", "manifest_version", "model_id", "release_id",
"artifact_tree_sha256", "tensor_state_sha256",
"safetensors_file_count", "tensor_count",
"tokenizer_sha256", "config_sha256", "not_after",
}
DIGEST_FIELDS = {
"artifact_tree_sha256", "tensor_state_sha256",
"tokenizer_sha256", "config_sha256",
}
def canonical_json(value: object) -> bytes:
return json.dumps(
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
).encode("utf-8")
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def sha256_file(path: Path) -> str:
if path.is_symlink():
raise ContractViolation("fingerprint input is a symbolic link")
before = path.stat()
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
after = path.stat()
identity_before = (
before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns
)
identity_after = (
after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns
)
if identity_before != identity_after:
raise InsufficientData("fingerprint input changed during hashing")
return digest.hexdigest()
def verified_manifest(
path: Path,
bundle: Path,
certificate_identity: str,
oidc_issuer: str,
) -> tuple[dict, str]:
raw = path.read_bytes()
bundle_raw = bundle.read_bytes()
if not raw or not bundle_raw:
raise InsufficientData("manifest or Sigstore bundle is empty")
with tempfile.TemporaryDirectory(prefix="model-fingerprint-") as directory:
root = Path(directory)
os.chmod(root, 0o700)
manifest_snapshot = root / "manifest.json"
bundle_snapshot = root / "bundle.json"
manifest_snapshot.write_bytes(raw)
bundle_snapshot.write_bytes(bundle_raw)
os.chmod(manifest_snapshot, 0o400)
os.chmod(bundle_snapshot, 0o400)
subprocess.run([
"cosign", "verify-blob",
"--bundle", str(bundle_snapshot),
"--certificate-identity", certificate_identity,
"--certificate-oidc-issuer", oidc_issuer,
str(manifest_snapshot),
], check=True, capture_output=True, text=True, timeout=MODEL_FINGERPRINT_MANIFEST_VERIFY_TIMEOUT_SECONDS)
if not hmac.compare_digest(manifest_snapshot.read_bytes(), raw):
raise ContractViolation("verified manifest snapshot changed")
manifest = json.loads(raw.decode("utf-8", errors="strict"))
if not isinstance(manifest, dict) or set(manifest) != MANIFEST_FIELDS:
raise ContractViolation("fingerprint manifest schema differs")
if (
manifest["schema_version"]
!= "aidefend.model-fingerprint-manifest.v1"
):
raise ContractViolation("unsupported fingerprint manifest")
for field in {"manifest_version", "model_id", "release_id"}:
if not isinstance(manifest[field], str) or not manifest[field].strip():
raise ContractViolation("manifest identity field is empty")
for field in DIGEST_FIELDS:
value = manifest[field]
if (
not isinstance(value, str)
or len(value) != 64
or value != value.lower()
):
raise ContractViolation("manifest digest is not lower-case SHA-256")
try:
int(value, 16)
except ValueError as exc:
raise ContractViolation("manifest digest is not hexadecimal") from exc
for field in {"safetensors_file_count", "tensor_count"}:
if (
isinstance(manifest[field], bool)
or not isinstance(manifest[field], int)
or manifest[field] < 1
):
raise ContractViolation("manifest population count is invalid")
expires = datetime.fromisoformat(
str(manifest["not_after"]).replace("Z", "+00:00")
)
if expires.tzinfo is None:
raise ContractViolation("manifest expiry lacks a timezone")
if expires.astimezone(timezone.utc) <= datetime.now(timezone.utc):
raise ContractViolation("fingerprint manifest is expired")
return manifest, sha256_bytes(raw)
def regular_file_population(root: Path) -> list[Path]:
root = root.resolve(strict=True)
entries = sorted(root.rglob("*"), key=lambda p: p.relative_to(root).as_posix())
if any(path.is_symlink() for path in entries):
raise ContractViolation("model root contains a symbolic link")
if any(not path.is_dir() and not path.is_file() for path in entries):
raise ContractViolation("model root contains a non-regular entry")
files = [path for path in entries if path.is_file()]
if not files:
raise InsufficientData("model artifact tree is empty")
relative = [path.relative_to(root).as_posix() for path in files]
if len(relative) != len(set(relative)):
raise ContractViolation("normalized artifact paths are duplicated")
return files
def artifact_tree(root: Path) -> tuple[str, list[dict]]:
root = root.resolve(strict=True)
rows = [
{
"path": path.relative_to(root).as_posix(),
"sha256": sha256_file(path),
"size_bytes": path.stat().st_size,
}
for path in regular_file_population(root)
]
return sha256_bytes(canonical_json(rows)), rows
def tensor_state(root: Path) -> tuple[str, int, int, list[dict]]:
root = root.resolve(strict=True)
files = [
path for path in regular_file_population(root)
if path.suffix.lower() == ".safetensors"
]
if not files:
raise NotApplicable("model root has no safetensors weight population")
locations: dict[str, tuple[Path, object]] = {}
digest = hashlib.sha256()
rows: list[dict] = []
with ExitStack() as stack:
handles = {
path: stack.enter_context(
safe_open(str(path), framework="pt", device="cpu")
)
for path in files
}
for path, handle in handles.items():
for name in handle.keys():
if name in locations:
raise ContractViolation(
"tensor name is duplicated across safetensors shards"
)
locations[name] = (path, handle)
if not locations:
raise InsufficientData("safetensors population contains no tensors")
for name in sorted(locations):
path, handle = locations[name]
tensor = handle.get_tensor(name).detach().cpu().contiguous()
raw = tensor.view(torch.uint8).numpy().tobytes(order="C")
metadata = {
"name": name,
"dtype": str(tensor.dtype).removeprefix("torch."),
"shape": list(tensor.shape),
"byte_length": len(raw),
}
metadata_bytes = canonical_json(metadata)
digest.update(len(metadata_bytes).to_bytes(8, "big"))
digest.update(metadata_bytes)
digest.update(len(raw).to_bytes(8, "big"))
digest.update(raw)
rows.append({
**metadata,
"tensor_sha256": sha256_bytes(raw),
"source_path": path.relative_to(root).as_posix(),
})
return digest.hexdigest(), len(files), len(locations), rows
def fingerprint(root: Path, require_metadata: bool) -> dict:
root = root.resolve(strict=True)
tree_sha256, file_rows = artifact_tree(root)
tensor_sha256, file_count, tensor_count, tensor_rows = tensor_state(root)
config = root / "config.json"
tokenizer = root / "tokenizer.json"
if require_metadata and (not config.is_file() or not tokenizer.is_file()):
raise ContractViolation(
"reference root lacks config.json or tokenizer.json"
)
return {
"artifact_tree_sha256": tree_sha256,
"tensor_state_sha256": tensor_sha256,
"safetensors_file_count": file_count,
"tensor_count": tensor_count,
"tokenizer_sha256": sha256_file(tokenizer) if tokenizer.is_file() else None,
"config_sha256": sha256_file(config) if config.is_file() else None,
"file_population": file_rows,
"tensor_population": tensor_rows,
}
def compare_artifacts(
reference_root: Path,
suspect_root: Path,
manifest_path: Path,
manifest_bundle: Path,
certificate_identity: str,
oidc_issuer: str,
expected_model_id: str,
expected_release_id: str,
suspect_asset_id: str,
) -> dict:
if not reference_root.is_dir() or not suspect_root.is_dir():
raise InsufficientData("reference or suspect artifact root is unavailable")
if not all(
isinstance(value, str) and value.strip()
for value in {
expected_model_id, expected_release_id, suspect_asset_id
}
):
raise ContractViolation("case binding is incomplete")
manifest, manifest_sha256 = verified_manifest(
manifest_path, manifest_bundle, certificate_identity, oidc_issuer
)
if (
manifest["model_id"] != expected_model_id
or manifest["release_id"] != expected_release_id
):
raise ContractViolation("manifest is bound to another model release")
try:
reference = fingerprint(reference_root, require_metadata=True)
except NotApplicable as exc:
raise ContractViolation(
"signed reference release is outside the supported format"
) from exc
expected = {
field: manifest[field]
for field in {
"artifact_tree_sha256", "tensor_state_sha256",
"safetensors_file_count", "tensor_count",
"tokenizer_sha256", "config_sha256",
}
}
actual = {field: reference[field] for field in expected}
if not hmac.compare_digest(
canonical_json(actual), canonical_json(expected)
):
raise ContractViolation(
"reference bytes do not reproduce the signed fingerprint"
)
suspect = fingerprint(suspect_root, require_metadata=False)
if (
suspect["artifact_tree_sha256"]
== manifest["artifact_tree_sha256"]
):
finding = "EXACT_ARTIFACT_MATCH"
elif (
suspect["tensor_state_sha256"]
== manifest["tensor_state_sha256"]
):
finding = "CANONICAL_TENSOR_MATCH"
else:
finding = "NO_EXACT_MATCH"
return {
"schema_version": "aidefend.whitebox-ownership-evidence.v1",
"status": "PASS",
"finding": finding,
"model_id": manifest["model_id"],
"release_id": manifest["release_id"],
"suspect_asset_id": suspect_asset_id,
"manifest_sha256": manifest_sha256,
"manifest_bundle_sha256": sha256_file(manifest_bundle),
"certificate_identity": certificate_identity,
"oidc_issuer": oidc_issuer,
"reference": reference,
"suspect": suspect,
}
def run_with_status(**kwargs) -> dict:
try:
return compare_artifacts(**kwargs)
except NotApplicable as exc:
return {"status": "NOT_APPLICABLE", "reason": str(exc)}
except ContractViolation as exc:
return {"status": "FAIL", "reason": str(exc)}
except (InsufficientData, PermissionError) as exc:
return {"status": "INSUFFICIENT_DATA", "reason": str(exc)}
except Exception as exc:
return {"status": "ERROR", "reason": type(exc).__name__}
def run_and_write(evidence_path: Path, **kwargs) -> dict:
result = run_with_status(**kwargs)
payload = canonical_json(result) + b"\n"
evidence_path.parent.mkdir(parents=True, exist_ok=True)
if evidence_path.exists():
raise FileExistsError("refusing to overwrite ownership evidence")
descriptor, temporary_name = tempfile.mkstemp(
prefix=evidence_path.name + ".", dir=evidence_path.parent
)
temporary_path = Path(temporary_name)
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.chmod(temporary_path, 0o400)
os.link(temporary_path, evidence_path)
finally:
temporary_path.unlink(missing_ok=True)
return result
Execution, independent replay, evidence, and outcomes
Run the verifier inside a network-denied, read-only investigation worker against immutable snapshots of both roots. The certificate identity and issuer must come from the verifier image's signed deployment policy, not case-supplied values. Write the canonical result, sign it with an external evidence identity, and store it with the case manifest in append-only storage:
python -m model_ip.run_whitebox_case --reference-root /evidence/reference --suspect-root /evidence/suspect --manifest /policy/model-fingerprint.json --manifest-bundle /policy/model-fingerprint.sigstore.json --evidence /out/whitebox-ownership.json
cosign sign-blob --yes --bundle /out/whitebox-ownership.sigstore.json /out/whitebox-ownership.json
A second read-only runner must verify both Sigstore bundles, re-enumerate every regular file, recompute every file and tensor digest with the same pinned Python, PyTorch, and safetensors versions, and compare its canonical result digest with the first runner. Retain the signed manifest, both bundles, model/release/case IDs, immutable worker-image and dependency-lock digests, complete reference and suspect file/tensor populations, all aggregate and per-object digests, result finding, writer receipt, and independent replay receipt. Restrict these records because filenames and tensor metadata may expose model architecture.
Action: Reproduce the signed reference fingerprint, compare the entire suspect artifact and canonical tensor populations, preserve the bounded finding as signed evidence, and require an independent byte-level replay before using it in an ownership investigation.
Open-source tools
- Cosign
- safetensors
- PyTorch
- Sigstore model-signing (complete model artifact-tree signing and verification)
Threat mappings
MITRE ATLAS
- AML.T0048.004 External Harms: AI Intellectual Property Theft
MAESTRO
- Model Stealing (L1)
OWASP LLM Top 10 2026
- LLM06:2026 Unbounded Consumption (ownership fingerprinting supports investigation of extracted or cloned models but does not prevent abusive queries at inference)
OWASP ML Top 10 2023
- ML05:2023 Model Theft
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AITech-8.2 Data Exfiltration / Exposure (exact artifact/tensor fingerprint comparison within a suspected unauthorized-distribution incident)
- AITech-8.3 Information Disclosure (exact artifact/tensor fingerprint comparison within a suspected unauthorized-distribution incident)
Google Secure AI Framework 2.0 - Risks
- MXF: Model Exfiltration
Databricks AI Security Framework 3.0
- Model Management 8.2: Model theft
- Model 7.2: Model assets leak