AID-M-002.005: Hybrid Classical + Post-Quantum Integrity & Provenance for Long-Lived AI Assets

Version 1.20261015 · Actionable sub-technique

Preserve the integrity and provenance of long-lived AI assets—including model weights, datasets, checkpoints, evidence bundles, manifests, and restore artifacts—across years of storage, trust-anchor rotation, algorithm deprecation, and archive/restore cycles. Use an explicit dual-signature transition profile: keep a currently interoperable classical signature and add a separately generated FIPS 204 ML-DSA signature over the exact same canonical provenance statement. A separately signed local verifier policy must pin the exact current statement digest, chain root, allowed profiles, maximum lifetime, artifact and provenance locations, and approved trust anchors; verification must require both statement signatures and traverse the prior-statement digest chain.

Pillars: data, model, infra

Lifecycle: validation, operation, response

Scope boundary

Owns classical-plus-post-quantum provenance and integrity continuity for long-lived AI assets whose trust must survive cryptographic migration, including hybrid signatures, algorithm policy, verification history, and migration evidence.

AID-M-002.002: AID-M-002.005 preserves integrity trust across a long-lived classical-to-post-quantum migration; AID-M-002.002 performs ordinary whole-artifact digest and signature verification. Use the hybrid control only when cryptographic longevity and transition continuity are in scope; it does not replace routine artifact verification.

AID-H-003.008: AID-M-002.005 records and verifies long-lived hybrid provenance continuity; AID-H-003.008 enforces crypto-agile signing and distribution requirements when artifacts are admitted or delivered. The Model control establishes durable trust evidence, while the Harden control blocks distribution or admission that does not meet the active cryptographic policy.

Warning

Medium on signature size, archival overhead, and verifier interoperability

ML-DSA signatures are materially larger than classical signatures, and every verifier in the restore path must be tested against the selected parameter set. During migration, require both the classical and ML-DSA signatures; do not silently accept either one alone unless an explicitly versioned emergency policy says so.

Implementation guidance

AID-M-002.005-G001

Issue and verify separate classical cosign and ML-DSA signatures over one canonical, expiring provenance statement.
Concept:

The approved object is not just a signature pair. A protected approval workflow signs a local verifier policy that names the exact current statement digest and chain root. The restore verifier also requires the policy digest injected by the deployment control plane, which prevents an archived statement or an older still-valid policy from selecting itself.

Step 1: Create one canonical statement and immutable chain node
from __future__ import annotations

import math as _aidefend_math
import os as _aidefend_os

# Admission injects these required values from one signature-verified, versioned runtime profile.
def _aidefend_required(name):
    value = _aidefend_os.environ.get(name)
    if value is None or not value.strip():
        raise RuntimeError(f"required runtime-profile field is absent: {name}")
    return value.strip()


def _aidefend_positive_float(name):
    try:
        value = float(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not numeric: {name}") from error
    if not _aidefend_math.isfinite(value) or value <= 0:
        raise RuntimeError(f"runtime-profile field must be finite and positive: {name}")
    return value


def _aidefend_positive_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 1:
        raise RuntimeError(f"runtime-profile field must be positive: {name}")
    return value


def _aidefend_nonnegative_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 0:
        raise RuntimeError(f"runtime-profile field cannot be negative: {name}")
    return value


def _aidefend_fraction(name):
    value = _aidefend_positive_float(name)
    if value > 1:
        raise RuntimeError(f"runtime-profile field must be in (0, 1]: {name}")
    return value


RUNTIME_PROFILE_VERSION = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_VERSION")
RUNTIME_PROFILE_SHA256 = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_SHA256").lower()
if (len(RUNTIME_PROFILE_SHA256) != 64
        or set(RUNTIME_PROFILE_SHA256) - set("0123456789abcdef")):
    raise RuntimeError("runtime-profile digest must be lowercase SHA-256")
STATEMENT_LIFETIME_SECONDS = _aidefend_positive_int("M002005_STATEMENT_LIFETIME_SECONDS")

# File: archive/create_hybrid_statement.py

import hashlib
import json
import os
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path

ASSET_ID = "model-credit-risk-v12"
BASE = Path("archive")
ARTIFACT = BASE / "model-credit-risk-v12.safetensors"
PROVENANCE = BASE / "attestations/model-credit-risk-v12.intoto.jsonl"
CURRENT = BASE / "model-credit-risk-v12.provenance.json"
CHAIN_DIR = BASE / "statements"
SHA256 = re.compile(r"^[0-9a-f]{64}$")
KMS_ARN = re.compile(r"^arn:aws(?:-us-gov|-cn)?:kms:[a-z0-9-]+:[0-9]{12}:key/[A-Za-z0-9-]+$")

def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()

def canonical(value: dict) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")

for required in (ARTIFACT, PROVENANCE):
    if not required.is_file():
        raise SystemExit(f"required archive input missing: {required}")

pqc_key_arn = os.environ.get("ML_DSA_KMS_KEY_ARN", "")
if not KMS_ARN.fullmatch(pqc_key_arn):
    raise SystemExit("ML_DSA_KMS_KEY_ARN must be one resolved AWS KMS key ARN")

previous = os.environ.get("PREVIOUS_STATEMENT_SHA256") or None
if previous is not None:
    previous = previous.lower()
    if not SHA256.fullmatch(previous):
        raise SystemExit("PREVIOUS_STATEMENT_SHA256 must be a lowercase SHA-256")

now = datetime.now(timezone.utc).replace(microsecond=0)
statement = {
    "schema_version": "aidefend.long_lived_provenance.v1",
    "asset_id": ASSET_ID,
    "artifact_uri": "s3://ai-archive/model-credit-risk-v12.safetensors",
    "artifact_sha256": sha256_file(ARTIFACT),
    "provenance_uri": "s3://ai-archive/attestations/model-credit-risk-v12.intoto.jsonl",
    "provenance_sha256": sha256_file(PROVENANCE),
    "created_at": now.isoformat().replace("+00:00", "Z"),
    "expires_at": (now + timedelta(seconds=STATEMENT_LIFETIME_SECONDS)).isoformat().replace("+00:00", "Z"),
    "classical_profile": "cosign-ecdsa-p256",
    "pqc_profile": "ML_DSA_65/ML_DSA_SHAKE_256",
    "pqc_key_arn": pqc_key_arn,
    "previous_statement_sha256": previous,
}
encoded = canonical(statement)
if len(encoded) > 4096:
    raise SystemExit("canonical statement exceeds the AWS KMS RAW message limit")

BASE.mkdir(parents=True, exist_ok=True)
CHAIN_DIR.mkdir(parents=True, exist_ok=True)
temporary = CURRENT.with_suffix(".json.tmp")
temporary.write_bytes(encoded)
temporary.replace(CURRENT)
statement_sha256 = hashlib.sha256(encoded).hexdigest()
chain_node = CHAIN_DIR / f"{statement_sha256}.json"
if chain_node.exists() and chain_node.read_bytes() != encoded:
    raise SystemExit("immutable statement digest path already contains different bytes")
chain_node.write_bytes(encoded)
print(statement_sha256)
Step 2: Resolve one KMS ARN and produce both statement signatures
set -euo pipefail
KMS_KEY_ARN="$(aws kms describe-key --key-id alias/aidefend-archive-mldsa --query KeyMetadata.Arn --output text)"
test -n "$KMS_KEY_ARN"
export ML_DSA_KMS_KEY_ARN="$KMS_KEY_ARN"
python archive/create_hybrid_statement.py
cosign sign-blob --yes --key keys/archive-classical.key --bundle archive/model-credit-risk-v12.cosign.sig archive/model-credit-risk-v12.provenance.json
aws kms sign --key-id "$KMS_KEY_ARN" --message fileb://archive/model-credit-risk-v12.provenance.json --message-type RAW --signing-algorithm ML_DSA_SHAKE_256 --query Signature --output text | base64 --decode > archive/model-credit-risk-v12.mldsa.sig
aws kms get-public-key --key-id "$KMS_KEY_ARN" --query PublicKey --output text | base64 --decode > archive/archive-mldsa-public.der
Step 3: Build and sign the verifier policy in the approval job
from __future__ import annotations

import math as _aidefend_math
import os as _aidefend_os

# Admission injects these required values from one signature-verified, versioned runtime profile.
def _aidefend_required(name):
    value = _aidefend_os.environ.get(name)
    if value is None or not value.strip():
        raise RuntimeError(f"required runtime-profile field is absent: {name}")
    return value.strip()


def _aidefend_positive_float(name):
    try:
        value = float(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not numeric: {name}") from error
    if not _aidefend_math.isfinite(value) or value <= 0:
        raise RuntimeError(f"runtime-profile field must be finite and positive: {name}")
    return value


def _aidefend_positive_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 1:
        raise RuntimeError(f"runtime-profile field must be positive: {name}")
    return value


def _aidefend_nonnegative_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 0:
        raise RuntimeError(f"runtime-profile field cannot be negative: {name}")
    return value


def _aidefend_fraction(name):
    value = _aidefend_positive_float(name)
    if value > 1:
        raise RuntimeError(f"runtime-profile field must be in (0, 1]: {name}")
    return value


RUNTIME_PROFILE_VERSION = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_VERSION")
RUNTIME_PROFILE_SHA256 = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_SHA256").lower()
if (len(RUNTIME_PROFILE_SHA256) != 64
        or set(RUNTIME_PROFILE_SHA256) - set("0123456789abcdef")):
    raise RuntimeError("runtime-profile digest must be lowercase SHA-256")
VERIFIER_POLICY_LIFETIME_SECONDS = _aidefend_positive_int("M002005_VERIFIER_POLICY_LIFETIME_SECONDS")
MAX_STATEMENT_LIFETIME_SECONDS = _aidefend_positive_int("M002005_MAX_STATEMENT_LIFETIME_SECONDS")
MAX_FUTURE_SKEW_SECONDS = _aidefend_nonnegative_int("M002005_MAX_FUTURE_SKEW_SECONDS")

# File: archive/build_verifier_policy.py

import hashlib
import json
import os
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path

ASSET_ID = "model-credit-risk-v12"
BASE = Path("archive")
CURRENT = BASE / "model-credit-risk-v12.provenance.json"
CLASSICAL_PUBLIC_KEY = Path("trust/archive-classical.pub")
SHA256 = re.compile(r"^[0-9a-f]{64}$")

def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()

def canonical(value: dict) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")

raw_statement = CURRENT.read_bytes()
statement = json.loads(raw_statement)
if canonical(statement) != raw_statement:
    raise SystemExit("current statement is not canonical JSON")
current_head = hashlib.sha256(raw_statement).hexdigest()
chain_root = os.environ.get("CHAIN_ROOT_SHA256", current_head).lower()
if not SHA256.fullmatch(chain_root):
    raise SystemExit("CHAIN_ROOT_SHA256 must be a lowercase SHA-256")
if not (BASE / "statements" / f"{chain_root}.json").is_file():
    raise SystemExit("approved chain root is not present in the immutable statement store")

now = datetime.now(timezone.utc).replace(microsecond=0)
policy = {
    "schema_version": "aidefend.hybrid_verifier_policy.v1",
    "policy_version": os.environ.get("ARCHIVE_POLICY_VERSION", now.strftime("%Y%m%d%H%M%S")),
    "created_at": now.isoformat().replace("+00:00", "Z"),
    "expires_at": (now + timedelta(seconds=VERIFIER_POLICY_LIFETIME_SECONDS)).isoformat().replace("+00:00", "Z"),
    "expected_profiles": {
        "classical": "cosign-ecdsa-p256",
        "pqc": "ML_DSA_65/ML_DSA_SHAKE_256",
    },
    "max_statement_lifetime_seconds": MAX_STATEMENT_LIFETIME_SECONDS,
    "max_future_skew_seconds": MAX_FUTURE_SKEW_SECONDS,
    "classical_public_key": {
        "path": str(CLASSICAL_PUBLIC_KEY),
        "sha256": sha256_file(CLASSICAL_PUBLIC_KEY),
    },
    "trusted_pqc_key_arns": [statement["pqc_key_arn"]],
    "assets": {
        ASSET_ID: {
            "artifact_path": str(BASE / "model-credit-risk-v12.safetensors"),
            "artifact_uri": statement["artifact_uri"],
            "provenance_path": str(BASE / "attestations/model-credit-risk-v12.intoto.jsonl"),
            "provenance_uri": statement["provenance_uri"],
            "statement_path": str(CURRENT),
            "classical_signature_path": str(BASE / "model-credit-risk-v12.cosign.sig"),
            "pqc_signature_path": str(BASE / "model-credit-risk-v12.mldsa.sig"),
            "chain_directory": str(BASE / "statements"),
            "current_statement_sha256": current_head,
            "chain_root_sha256": chain_root,
        }
    },
}
Path("archive/verifier_policy.json").write_bytes(canonical(policy))
set -euo pipefail
export ARCHIVE_POLICY_VERSION="2026-07-10.1"
python archive/build_verifier_policy.py
cosign sign-blob --yes --key keys/verifier-policy-root.key --bundle archive/verifier_policy.cosign.sig archive/verifier_policy.json
sha256sum archive/verifier_policy.json
# Publish the printed digest as protected deployment variable APPROVED_ARCHIVE_POLICY_SHA256.
Step 4: Verify the protected policy, current head, chain, files, and both signatures
from __future__ import annotations

import math as _aidefend_math
import os as _aidefend_os

# Admission injects these required values from one signature-verified, versioned runtime profile.
def _aidefend_required(name):
    value = _aidefend_os.environ.get(name)
    if value is None or not value.strip():
        raise RuntimeError(f"required runtime-profile field is absent: {name}")
    return value.strip()


def _aidefend_positive_float(name):
    try:
        value = float(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not numeric: {name}") from error
    if not _aidefend_math.isfinite(value) or value <= 0:
        raise RuntimeError(f"runtime-profile field must be finite and positive: {name}")
    return value


def _aidefend_positive_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 1:
        raise RuntimeError(f"runtime-profile field must be positive: {name}")
    return value


def _aidefend_nonnegative_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 0:
        raise RuntimeError(f"runtime-profile field cannot be negative: {name}")
    return value


def _aidefend_fraction(name):
    value = _aidefend_positive_float(name)
    if value > 1:
        raise RuntimeError(f"runtime-profile field must be in (0, 1]: {name}")
    return value


RUNTIME_PROFILE_VERSION = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_VERSION")
RUNTIME_PROFILE_SHA256 = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_SHA256").lower()
if (len(RUNTIME_PROFILE_SHA256) != 64
        or set(RUNTIME_PROFILE_SHA256) - set("0123456789abcdef")):
    raise RuntimeError("runtime-profile digest must be lowercase SHA-256")
MAX_STATEMENT_LIFETIME_SECONDS = _aidefend_positive_int("M002005_MAX_STATEMENT_LIFETIME_SECONDS")
MAX_FUTURE_SKEW_SECONDS = _aidefend_nonnegative_int("M002005_MAX_FUTURE_SKEW_SECONDS")
MAX_VERIFIER_POLICY_LIFETIME_SECONDS = _aidefend_positive_int("M002005_MAX_VERIFIER_POLICY_LIFETIME_SECONDS")
ARCHIVE_VERIFY_COMMAND_TIMEOUT_SECONDS = _aidefend_positive_float("M002005_ARCHIVE_VERIFY_COMMAND_TIMEOUT_SECONDS")

# File: archive/verify_hybrid_statement.py

import hashlib
import json
import os
import re
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path

ASSET_ID = "model-credit-risk-v12"
POLICY_PATH = Path("archive/verifier_policy.json")
POLICY_SIGNATURE = Path("archive/verifier_policy.cosign.sig")
POLICY_ROOT_PUBLIC_KEY = Path("trust/verifier-policy-root.pub")
SHA256 = re.compile(r"^[0-9a-f]{64}$")
STATEMENT_FIELDS = {
    "schema_version", "asset_id", "artifact_uri", "artifact_sha256",
    "provenance_uri", "provenance_sha256", "created_at", "expires_at",
    "classical_profile", "pqc_profile", "pqc_key_arn",
    "previous_statement_sha256",
}
POLICY_FIELDS = {
    "schema_version", "policy_version", "created_at", "expires_at",
    "expected_profiles", "max_statement_lifetime_seconds",
    "max_future_skew_seconds", "classical_public_key",
    "trusted_pqc_key_arns", "assets",
}
ASSET_POLICY_FIELDS = {
    "artifact_path", "artifact_uri", "provenance_path", "provenance_uri",
    "statement_path", "classical_signature_path", "pqc_signature_path",
    "chain_directory", "current_statement_sha256", "chain_root_sha256",
}

def fail(message: str) -> None:
    raise SystemExit(message)

def exact_keys(value: object, expected: set[str], label: str) -> dict:
    if not isinstance(value, dict) or set(value) != expected:
        fail(f"{label} fields do not match the approved schema")
    return value

def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()

def canonical(value: dict) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")

def read_canonical(path: Path, label: str) -> tuple[dict, bytes]:
    raw = path.read_bytes()
    value = json.loads(raw)
    if canonical(value) != raw:
        fail(f"{label} is not canonical JSON")
    return value, raw

def parse_time(value: object, label: str) -> datetime:
    if not isinstance(value, str):
        fail(f"{label} must be an RFC 3339 timestamp")
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        fail(f"{label} is not a valid RFC 3339 timestamp")
    if parsed.utcoffset() is None:
        fail(f"{label} must include a timezone")
    return parsed.astimezone(timezone.utc)

approved_policy_sha = os.environ.get("APPROVED_ARCHIVE_POLICY_SHA256", "").lower()
if not SHA256.fullmatch(approved_policy_sha):
    fail("protected APPROVED_ARCHIVE_POLICY_SHA256 is missing or invalid")
if sha256_file(POLICY_PATH) != approved_policy_sha:
    fail("local verifier policy is not the deployment-approved policy")
subprocess.run([
    "cosign", "verify-blob", "--key", str(POLICY_ROOT_PUBLIC_KEY),
    "--bundle", str(POLICY_SIGNATURE), str(POLICY_PATH),
], check=True, capture_output=True, text=True, timeout=ARCHIVE_VERIFY_COMMAND_TIMEOUT_SECONDS)

policy, _ = read_canonical(POLICY_PATH, "verifier policy")
exact_keys(policy, POLICY_FIELDS, "verifier policy")
if policy["schema_version"] != "aidefend.hybrid_verifier_policy.v1":
    fail("unsupported verifier policy schema")
if not isinstance(policy["policy_version"], str) or not policy["policy_version"]:
    fail("policy_version must be a non-empty string")
if policy["expected_profiles"] != {
    "classical": "cosign-ecdsa-p256",
    "pqc": "ML_DSA_65/ML_DSA_SHAKE_256",
}:
    fail("verifier policy profile mismatch")
if not isinstance(policy["max_statement_lifetime_seconds"], int) or not 1 <= policy["max_statement_lifetime_seconds"] <= MAX_STATEMENT_LIFETIME_SECONDS:
    fail("invalid maximum statement lifetime")
if not isinstance(policy["max_future_skew_seconds"], int) or not 0 <= policy["max_future_skew_seconds"] <= MAX_FUTURE_SKEW_SECONDS:
    fail("invalid future-skew allowance")

now = datetime.now(timezone.utc)
policy_created = parse_time(policy["created_at"], "policy created_at")
policy_expires = parse_time(policy["expires_at"], "policy expires_at")
if policy_created > now + timedelta(seconds=policy["max_future_skew_seconds"]):
    fail("verifier policy is future-dated")
if policy_expires <= now or policy_expires <= policy_created:
    fail("verifier policy is expired or has an invalid interval")
if policy_expires - policy_created > timedelta(seconds=MAX_VERIFIER_POLICY_LIFETIME_SECONDS):
    fail("verifier policy lifetime exceeds the runtime-profile maximum")

key_policy = exact_keys(policy["classical_public_key"], {"path", "sha256"}, "classical public key policy")
classical_public_key = Path(key_policy["path"])
if not SHA256.fullmatch(str(key_policy["sha256"])) or sha256_file(classical_public_key) != key_policy["sha256"]:
    fail("classical statement verification key digest mismatch")
trusted_pqc = policy["trusted_pqc_key_arns"]
if not isinstance(trusted_pqc, list) or not trusted_pqc or len(trusted_pqc) != len(set(trusted_pqc)):
    fail("trusted_pqc_key_arns must be a non-empty unique list")
if not isinstance(policy["assets"], dict) or set(policy["assets"]) != {ASSET_ID}:
    fail("policy must contain exactly the expected asset")
asset_policy = exact_keys(policy["assets"][ASSET_ID], ASSET_POLICY_FIELDS, "asset verifier policy")
for digest_field in ("current_statement_sha256", "chain_root_sha256"):
    if not SHA256.fullmatch(str(asset_policy[digest_field])):
        fail(f"{digest_field} is not a SHA-256")

statement_path = Path(asset_policy["statement_path"])
statement, current_raw = read_canonical(statement_path, "current statement")
exact_keys(statement, STATEMENT_FIELDS, "current statement")
if hashlib.sha256(current_raw).hexdigest() != asset_policy["current_statement_sha256"]:
    fail("current statement does not match the signed policy head")

def validate_statement(value: dict, label: str, require_current_key: bool) -> datetime:
    exact_keys(value, STATEMENT_FIELDS, label)
    if value["schema_version"] != "aidefend.long_lived_provenance.v1":
        fail(f"{label} has an unsupported schema")
    if value["asset_id"] != ASSET_ID:
        fail(f"{label} asset_id mismatch")
    if value["classical_profile"] != policy["expected_profiles"]["classical"] or value["pqc_profile"] != policy["expected_profiles"]["pqc"]:
        fail(f"{label} signature profile mismatch")
    if require_current_key and value["pqc_key_arn"] not in trusted_pqc:
        fail("current statement uses an untrusted ML-DSA key")
    for field in ("artifact_sha256", "provenance_sha256"):
        if not SHA256.fullmatch(str(value[field])):
            fail(f"{label} {field} is invalid")
    previous = value["previous_statement_sha256"]
    if previous is not None and not SHA256.fullmatch(str(previous)):
        fail(f"{label} previous statement digest is invalid")
    created = parse_time(value["created_at"], f"{label} created_at")
    expires = parse_time(value["expires_at"], f"{label} expires_at")
    if expires <= created:
        fail(f"{label} has an invalid validity interval")
    if expires - created > timedelta(seconds=policy["max_statement_lifetime_seconds"]):
        fail(f"{label} exceeds maximum statement lifetime")
    return created

current_created = validate_statement(statement, "current statement", True)
current_expires = parse_time(statement["expires_at"], "current statement expires_at")
if current_created > now + timedelta(seconds=policy["max_future_skew_seconds"]):
    fail("current statement is future-dated")
if current_expires <= now:
    fail("current statement is expired")
if statement["artifact_uri"] != asset_policy["artifact_uri"] or statement["provenance_uri"] != asset_policy["provenance_uri"]:
    fail("statement URI does not match signed policy")
if sha256_file(Path(asset_policy["artifact_path"])) != statement["artifact_sha256"]:
    fail("artifact digest mismatch")
if sha256_file(Path(asset_policy["provenance_path"])) != statement["provenance_sha256"]:
    fail("provenance digest mismatch")

subprocess.run([
    "cosign", "verify-blob", "--key", str(classical_public_key),
    "--bundle", asset_policy["classical_signature_path"], str(statement_path),
], check=True, capture_output=True, text=True, timeout=ARCHIVE_VERIFY_COMMAND_TIMEOUT_SECONDS)
kms_result = subprocess.run([
    "aws", "kms", "verify", "--key-id", statement["pqc_key_arn"],
    "--message", "fileb://" + str(statement_path), "--message-type", "RAW",
    "--signature", "fileb://" + asset_policy["pqc_signature_path"],
    "--signing-algorithm", "ML_DSA_SHAKE_256", "--output", "json",
], check=True, capture_output=True, text=True, timeout=ARCHIVE_VERIFY_COMMAND_TIMEOUT_SECONDS)
if json.loads(kms_result.stdout).get("SignatureValid") is not True:
    fail("ML-DSA signature invalid")

head = asset_policy["current_statement_sha256"]
root = asset_policy["chain_root_sha256"]
chain_directory = Path(asset_policy["chain_directory"])
expected = head
child_created: datetime | None = None
seen: set[str] = set()
chain_length = 0
while True:
    if expected in seen or len(seen) >= 4096:
        fail("statement chain contains a cycle or exceeds its bound")
    seen.add(expected)
    node_path = chain_directory / f"{expected}.json"
    node, node_raw = read_canonical(node_path, f"statement chain node {expected}")
    if hashlib.sha256(node_raw).hexdigest() != expected:
        fail("statement chain node digest does not match its filename")
    if expected == head and node_raw != current_raw:
        fail("current statement differs from its immutable chain node")
    node_created = validate_statement(node, f"statement chain node {expected}", False)
    if child_created is not None and node_created >= child_created:
        fail("statement chain timestamps are not strictly ordered")
    for field in ("asset_id", "artifact_uri", "artifact_sha256", "provenance_uri", "provenance_sha256"):
        if node[field] != statement[field]:
            fail(f"statement chain changes immutable asset binding: {field}")
    chain_length += 1
    previous = node["previous_statement_sha256"]
    if expected == root:
        if previous is not None:
            fail("signed chain root is not the first statement")
        break
    if previous is None:
        fail("statement chain ended before the signed chain root")
    child_created = node_created
    expected = previous

evidence = {
    "schema_version": "aidefend.hybrid_verification_result.v1",
    "asset_id": ASSET_ID,
    "verified_at": now.replace(microsecond=0).isoformat().replace("+00:00", "Z"),
    "policy_sha256": approved_policy_sha,
    "policy_version": policy["policy_version"],
    "current_statement_sha256": head,
    "chain_root_sha256": root,
    "chain_length": chain_length,
    "artifact_sha256": statement["artifact_sha256"],
    "provenance_sha256": statement["provenance_sha256"],
    "classical_signature_verified": True,
    "mldsa_signature_verified": True,
}
result_path = Path("archive/review-results/model-credit-risk-v12.json")
result_path.parent.mkdir(parents=True, exist_ok=True)
result_path.write_bytes(canonical(evidence))
print(result_path)
set -euo pipefail
python archive/verify_hybrid_statement.py
cosign sign-blob --yes --key keys/archive-review.key --bundle archive/review-results/model-credit-risk-v12.cosign.sig archive/review-results/model-credit-risk-v12.json

Action: Store the artifact, provenance, canonical statement, digest-named chain nodes, both statement signatures, signed verifier policy, protected policy digest, and signed verification result in WORM storage. A missing, expired, replayed, or unverifiable proof is a hard restore failure.

Verify safely

A read-only identity that cannot alter the controlled asset must obtain canonical statement bytes, independent key registries, detached signatures, cryptographic policy, and prior continuity receipts; validate the effective asset, policy, input, and scope versions or digests; then re-canonicalize and hash the statement, independently verify both signature families and expiry, and traverse every signed rollover link without treating one signature family as sufficient.

AID-M-002.005-G002

Re-verify and roll long-lived provenance before expiry or trust-policy change, preserving an auditable continuity chain for restore.
Concept:

Policy renewal and statement rollover are separate operations. Renew the 30-day signed verifier policy without changing the approved head when only the approval window changes. Create a new statement only before its 180-day expiry, when a trust anchor or accepted profile changes, or after an approved archive migration. Every new statement must name the prior head, while every new policy pins the new head and the original chain root.

Rollover the statement without losing prior proofs
set -euo pipefail
export APPROVED_ARCHIVE_POLICY_SHA256
python archive/verify_hybrid_statement.py

OLD_HEAD="$(sha256sum archive/model-credit-risk-v12.provenance.json | cut -d' ' -f1)"
CHAIN_ROOT="$(python -c 'import json; print(json.load(open("archive/verifier_policy.json"))["assets"]["model-credit-risk-v12"]["chain_root_sha256"])')"
mkdir -p archive/statements archive/policies
cp --no-clobber archive/model-credit-risk-v12.cosign.sig "archive/statements/${OLD_HEAD}.cosign.sig"
cp --no-clobber archive/model-credit-risk-v12.mldsa.sig "archive/statements/${OLD_HEAD}.mldsa.sig"
cp --no-clobber archive/verifier_policy.json "archive/policies/${OLD_HEAD}.policy.json"
cp --no-clobber archive/verifier_policy.cosign.sig "archive/policies/${OLD_HEAD}.policy.cosign.sig"

KMS_KEY_ARN="$(aws kms describe-key --key-id alias/aidefend-archive-mldsa --query KeyMetadata.Arn --output text)"
test -n "$KMS_KEY_ARN"
export ML_DSA_KMS_KEY_ARN="$KMS_KEY_ARN"
export PREVIOUS_STATEMENT_SHA256="$OLD_HEAD"
export CHAIN_ROOT_SHA256="$CHAIN_ROOT"
python archive/create_hybrid_statement.py

cosign sign-blob --yes --key keys/archive-classical.key --bundle archive/model-credit-risk-v12.cosign.sig archive/model-credit-risk-v12.provenance.json
aws kms sign --key-id "$KMS_KEY_ARN" --message fileb://archive/model-credit-risk-v12.provenance.json --message-type RAW --signing-algorithm ML_DSA_SHAKE_256 --query Signature --output text | base64 --decode > archive/model-credit-risk-v12.mldsa.sig

export ARCHIVE_POLICY_VERSION="$(date -u +%Y%m%d%H%M%S)"
python archive/build_verifier_policy.py
cosign sign-blob --yes --key keys/verifier-policy-root.key --bundle archive/verifier_policy.cosign.sig archive/verifier_policy.json
NEW_POLICY_SHA256="$(sha256sum archive/verifier_policy.json | cut -d' ' -f1)"
printf 'submit protected APPROVED_ARCHIVE_POLICY_SHA256=%s\n' "$NEW_POLICY_SHA256"
# An independent approval job publishes that digest. The restore job never derives it from this archive.
Independent activation gate
set -euo pipefail
test "$APPROVED_ARCHIVE_POLICY_SHA256" = "$(sha256sum archive/verifier_policy.json | cut -d' ' -f1)"
python archive/verify_hybrid_statement.py
NEW_HEAD="$(sha256sum archive/model-credit-risk-v12.provenance.json | cut -d' ' -f1)"
cp --no-clobber archive/model-credit-risk-v12.cosign.sig "archive/statements/${NEW_HEAD}.cosign.sig"
cp --no-clobber archive/model-credit-risk-v12.mldsa.sig "archive/statements/${NEW_HEAD}.mldsa.sig"
# Replicate the digest-named statement, signatures, policies, and signed review result to WORM storage.

Freshness: Run the verifier daily and before every restore. Renew the signed verifier policy before its 30-day expiry, and roll the statement no later than 30 days before its 180-day expiry. The approval job must reject a head whose previous digest is not the currently approved head or whose chain does not terminate at the signed root.

Action: Emergency restore pressure does not authorize bypassing the protected policy digest, either signature, provenance digest, current-head approval, or chain verification.

Open-source tools

Source-available / open-weight tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-M-002.005: Hybrid Classical + Post-Quantum Integrity & Provenance for Long-Lived AI Assets | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version