AID-H-003.011: Attested Edge Model Entitlement & Key Release
Pillars: model, infra, app
Lifecycle: building, operation
Scope boundary
Owns entitlement and fresh platform-attestation enforcement at the protected client/edge model key-release point, including request binding, replay prevention, exact release identity, wrapped-key delivery, and release evidence. It does not sign model packages, establish the user identity, or claim that attestation prevents all post-release extraction.
AID-H-003.002: AID-H-003.002 signs, promotes, and distributes the exact model release package; AID-H-003.011 decides whether one client or edge subject may receive its decryption key. Artifact authenticity does not establish recipient entitlement, and an entitled recipient must still receive only an approved signed release.
AID-H-004.001: AID-H-004.001 establishes and authorizes the human or privileged subject; AID-H-003.011 consumes that authenticated identity together with device/app attestation at the model-key release point. Neither a valid login nor a valid device verdict alone is sufficient to release protected model bytes.
AID-H-009.005: AID-H-003.011 controls decryption-key release for a model intentionally distributed to a client or edge device; AID-H-009.005 protects inference inside an attested confidential-computing boundary. Client-side distribution accepts bounded residual extraction risk after release, while confidential inference keeps the model inside a protected execution environment.
AID-I-007: AID-H-003.011 decides whether an entitled, freshly attested client may receive the content key for an exact model release; AID-I-007 confines what the client-side model runtime can access after execution begins. Key-release admission does not prove client-runtime isolation, and isolation does not authorize delivery of protected model bytes.
AID-H-013: AID-H-003.011 controls entitlement and decryption-key release before protected model bytes become usable on a client; AID-H-013 embeds or evaluates bounded provenance and ownership signals after an artifact or its outputs may be copied. A watermark or fingerprint is not an entitlement decision, and attested delivery does not provide later ownership attribution.
Warning
Attestation Is a Release Signal, Not Extraction Proof
App and device attestation raises the cost of unauthorized download and key replay but cannot guarantee that plaintext weights remain secret after an entitled device receives them. Rooted devices, runtime instrumentation, memory capture, accessibility abuse, and compromised operating systems remain possible. Use this control only when distributing weights is an accepted architecture; otherwise keep sensitive models server-side or use confidential inference. Unsupported attestation is a reason code: deny the release and record evidence rather than inventing a sixth outcome state.
Implementation guidance
- AID-H-003.011-G001: Release a model content key only after exact entitlement, fresh request-bound platform attestation, one-time challenge consumption, and recipient-key binding all succeed.
AID-H-003.011-G001
Architecture
Encrypt each immutable model release with a unique data-encryption key (DEK), store only ciphertext plus the signed release manifest, and keep the wrapped DEK in KMS. The client obtains a one-time server challenge and requests a platform verdict whose request hash covers that challenge, the authenticated subject, exact release and policy digests, response expiry, and an ephemeral X25519 recipient key. The broker calls the platform's authoritative server verifier; it never trusts client-produced verdict JSON.
# File: edge_release/key_broker.py
from __future__ import annotations
import base64, hashlib, hmac, json, os
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
@dataclass(frozen=True)
class VerifiedAttestation:
platform: str
app_id: str
app_version: str
device_key_id: str
request_hash: str
issued_at: datetime
device_integrity: bool
app_integrity: bool
platform_policy_ok: bool
VerifyAttestation = Callable[[str], VerifiedAttestation]
ConsumeChallenge = Callable[[str, str], bool]
CheckEntitlement = Callable[[str, str], bool]
UnwrapDek = Callable[[str], bytearray]
def canonical(value: dict) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
def decode_key(value: str) -> bytes:
if not isinstance(value, str) or not value:
raise ValueError("recipient key is required")
if "=" in value:
raise ValueError("recipient key must be unpadded base64url")
try:
raw = base64.b64decode((value + "=" * (-len(value) % 4)).encode("ascii"), altchars=b"-_", validate=True)
except (ValueError, UnicodeEncodeError) as error:
raise ValueError("recipient key is not canonical base64url") from error
canonical_value = base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
if len(raw) != 32 or not hmac.compare_digest(canonical_value, value):
raise ValueError("recipient X25519 key must be one canonical 32-byte key")
return raw
def release_key(*, request: dict, subject: str, release: dict, max_age: int,
max_future_skew: int, max_key_lifetime: int,
verify: VerifyAttestation, consume: ConsumeChallenge,
entitled: CheckEntitlement, unwrap: UnwrapDek) -> dict:
fields = {"release_id", "release_sha256", "challenge_id", "recipient_key",
"attestation_token", "expires_at"}
if (not isinstance(request, dict) or set(request) != fields
or not isinstance(subject, str) or not subject or len(subject) > 256
or any(not isinstance(request[field], str) or not request[field]
for field in fields)):
raise PermissionError("request schema or authenticated subject differs")
release_fields = {"release_id", "release_sha256", "wrapped_dek_ref",
"allowed_app_versions", "allowed_attestation_platforms",
"policy_version", "policy_sha256"}
if (not isinstance(release, dict) or set(release) != release_fields
or request["release_id"] != release["release_id"]
or request["release_sha256"] != release["release_sha256"]):
raise PermissionError("request is not bound to the approved release")
for field in ("release_id", "wrapped_dek_ref", "policy_version"):
if not isinstance(release[field], str) or not release[field]:
raise ValueError("approved release identity is incomplete")
for field in ("release_sha256", "policy_sha256"):
if (not isinstance(release[field], str) or len(release[field]) != 64
or set(release[field]) - set("0123456789abcdef")):
raise ValueError("approved release digest is invalid")
versions = release["allowed_app_versions"]
platforms = release["allowed_attestation_platforms"]
if (not isinstance(versions, dict) or not versions
or any(not isinstance(app_id, str) or not app_id
or not isinstance(items, list) or not items
or len(items) != len(set(items))
or any(not isinstance(item, str) or not item for item in items)
for app_id, items in versions.items())
or not isinstance(platforms, list) or not platforms
or len(platforms) != len(set(platforms))
or any(not isinstance(item, str) or not item for item in platforms)):
raise ValueError("approved app or platform population is invalid")
if any(type(value) is not int or value < 0
for value in (max_age, max_future_skew, max_key_lifetime)) or min(max_age, max_key_lifetime) < 1:
raise ValueError("attestation and key-lifetime policy is invalid")
now = datetime.now(timezone.utc)
try:
expires_at = datetime.fromisoformat(request["expires_at"].replace("Z", "+00:00"))
except ValueError as error:
raise PermissionError("key response expiry is not RFC 3339") from error
if expires_at.tzinfo is None:
raise PermissionError("key response expiry lacks an offset")
remaining = (expires_at.astimezone(timezone.utc) - now).total_seconds()
if not 0 < remaining <= max_key_lifetime:
raise PermissionError("key response expiry is outside policy")
recipient = decode_key(request["recipient_key"])
binding = {"challenge_id": request["challenge_id"], "subject": subject,
"release_id": release["release_id"], "release_sha256": release["release_sha256"],
"recipient_key": request["recipient_key"], "expires_at": request["expires_at"],
"policy_version": release["policy_version"],
"policy_sha256": release["policy_sha256"]}
commitment = hashlib.sha256(canonical(binding)).hexdigest()
verdict = verify(request["attestation_token"])
if verdict.issued_at.tzinfo is None:
raise PermissionError("attestation time lacks an offset")
age = (now - verdict.issued_at.astimezone(timezone.utc)).total_seconds()
allowed_versions = versions.get(verdict.app_id, [])
if (verdict.request_hash != commitment or age < -max_future_skew or age > max_age
or not all((verdict.device_integrity, verdict.app_integrity, verdict.platform_policy_ok))
or verdict.platform not in platforms or verdict.app_version not in allowed_versions
or not isinstance(verdict.device_key_id, str) or not verdict.device_key_id):
raise PermissionError("attestation does not satisfy release policy")
if not entitled(subject, release["release_id"]):
raise PermissionError("subject is not entitled to this release")
if not consume(request["challenge_id"], commitment):
raise PermissionError("challenge is absent, stale, mismatched, or replayed")
dek = unwrap(release["wrapped_dek_ref"])
if type(dek) is not bytearray or len(dek) != 32:
if isinstance(dek, bytearray):
dek[:] = b"\x00" * len(dek)
raise RuntimeError("KMS adapter must return one mutable 32-byte model DEK")
try:
sender = X25519PrivateKey.generate()
shared = sender.exchange(X25519PublicKey.from_public_bytes(recipient))
wrap_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=bytes.fromhex(commitment),
info=b"aidefend.edge-model-key.v1").derive(shared)
nonce = os.urandom(12)
ciphertext = AESGCM(wrap_key).encrypt(nonce, bytes(dek), canonical(binding))
sender_public = sender.public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw)
finally:
dek[:] = b"\x00" * len(dek)
encode = lambda value: base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
return {"release_id": release["release_id"], "release_sha256": release["release_sha256"],
"sender_key": encode(sender_public), "nonce": encode(nonce),
"wrapped_dek": encode(ciphertext), "binding_sha256": commitment,
"expires_at": request["expires_at"], "policy_version": release["policy_version"],
"policy_sha256": release["policy_sha256"],
"attestation_platform": verdict.platform, "device_key_id": verdict.device_key_id}Platform verification and evidence
For Play Integrity, bind requestHash to the canonical request, decode the token through Google's server API, and verify request package, hash, timestamp, app recognition, licensing, device integrity, and policy-selected environment verdicts. For App Attest, validate the certificate chain, App ID, authenticator data, counter, server challenge, and assertion signature according to Apple's server contract. Treat the response expiry as a broker replay bound, not proof that a client erased a DEK it already received. Python bytearray overwrite is best-effort only; isolate and recycle the broker process or use a native/enclave key broker when stronger plaintext-memory handling is required. Keep raw tokens out of normal logs; retain token digest, verified fields, entitlement revision, release and policy digests, challenge-consumption receipt, recipient-key digest, KMS audit event, decision, and expiry.
Outcomes and tests
No protected edge distribution architecture is NOT_APPLICABLE. Refusing release because required verification is unsupported can satisfy the control; releasing anyway is FAIL. Missing evidence is INSUFFICIENT_DATA; verifier or KMS failure is ERROR. Independently test wrong subject, app, release, digest, challenge, recipient key, stale verdict, replay, unlicensed app, failed device integrity, revoked entitlement, KMS failure, and ciphertext substitution.
Action: Put this broker on the only model-key release path and release no key unless entitlement, fresh request binding, attestation, and one-time challenge consumption all succeed.
Open-source tools
- Cosign
- ORAS CLI
- pyca/cryptography
- Google Tink
Commercial / hosted tools
- Apple App Attest
- Google Play Integrity API
- AWS Key Management Service
- Azure Key Vault
- Google Cloud Key Management Service
Threat mappings
MITRE ATLAS
- AML.T0025 Exfiltration via Cyber Means (entitlement and key release prevent unauthorized direct download of distributed model bytes)
- AML.T0044 Full AI Model Access
MAESTRO
- Model Stealing (L1)
- Data Leakage (Cross-Layer)
OWASP LLM Top 10 2026
- LLM04:2026 Supply Chain (attested entitlement protects the final model distribution path)
OWASP ML Top 10 2023
- ML05:2023 Model Theft
- ML06:2023 AI Supply Chain Attacks
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-14.1.2 Insufficient Access Controls (Enforces entitled subject, device and release at the model-key broker)
- AITech-10.1 Model Extraction (Protects encrypted local model bytes and recipient-bound release keys)
- AITech-8.2 Data Exfiltration / Exposure (Protects encrypted local model bytes and recipient-bound release keys)
- AITech-8.3 Information Disclosure (Protects encrypted local model bytes and recipient-bound release keys)
Google Secure AI Framework 2.0 - Risks
- MXF: Model Exfiltration
Databricks AI Security Framework 3.0
- Model 7.2: Model assets leak
- Model Management 8.2: Model theft