TL;DR: Resume screening AI is classified as "high-risk" under the EU AI Act (effective August 2026). Current systems lack tamper-evident logging, making them legally indefensible. This article shows how to implement cryptographically verifiable decision audit trails using the VAP-PAP (Public Administration Protocol) architecture.
The Problem: Your Hiring AI Has No Black Box
When a plane crashes, investigators recover the flight data recorder. Every parameter—altitude, airspeed, control inputs—is preserved in a tamper-evident format that courts accept as evidence.
When your hiring AI rejects a candidate, what do you have?
❌ "Score: 0.42 - Below threshold"
❌ Timestamp from system clock (trivially mutable)
❌ No cryptographic proof of non-tampering
❌ No chain of custody for audit trail
This isn't just a technical gap—it's a legal liability. Starting August 2026, AI-powered resume screening in the EU requires:
| EU AI Act Article | Requirement |
|---|---|
| Article 12 | Automatic logging of all AI decisions |
| Article 14 | Human oversight capability |
| Article 86 | Candidates can demand explanations |
Penalties: Up to €15M or 3% of global turnover.
VAP-PAP: The "Flight Recorder" for Public-Facing AI
The Verifiable AI Provenance (VAP) framework provides a 5-layer architecture for tamper-evident AI audit trails. PAP (Public Administration Protocol) is the domain profile for government and employment decisions.
┌─────────────────────────────────────────────┐
│ VAP Framework (Cross-Domain Standard) │
└─────────────────────┬───────────────────────┘
│
┌────────┬────────┼────────┬────────┐
▼ ▼ ▼ ▼ ▼
[VCP] [DVP] [MAP] [EIP] [PAP]
Finance Auto Medical Energy Public
For hiring AI, PAP defines:
- What to log: Candidate hash, model version, feature weights, decision, human override
- How to secure it: Hash chains + Ed25519 signatures + external anchoring
- How to explain it: SHAP values, counterfactuals, appeal rights
Architecture: Sidecar Pattern for Zero-Intrusion Logging
The key insight: don't modify your existing hiring system. Instead, deploy a "sidecar" logger that intercepts and signs decision events.
┌─────────────────────────────────────────────────────┐
│ Existing Hiring System │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Resume │──▶│ AI/ML │──▶│ Decision │ │
│ │ Parser │ │ Scoring │ │ Output │ │
│ └──────────┘ └────┬─────┘ └──────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ PAP Sidecar │ ← Intercepts events│
│ │ Logger │ │
│ └───────┬───────┘ │
└──────────────────────┼─────────────────────────────┘
│
┌───────▼───────┐
│ Hash Chain │ ← Cryptographically linked
│ + Ed25519 │ ← Digitally signed
│ + Timestamp │ ← Externally anchored
└───────────────┘
Implementation: Python Reference
Let's build a minimal PAP-compliant logger for hiring decisions.
1. Event Schema
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
import hashlib
import json
@dataclass
class HiringDecisionEvent:
"""PAP-compliant hiring decision event structure."""
# Identification (VAP L1)
event_id: str # UUID v7 (time-ordered)
timestamp_ns: int # Nanosecond precision
# Provenance (VAP L2)
candidate_id_hash: str # SHA-256 of candidate identifier
job_requisition_id: str
model_version: str
model_config_hash: str
training_date: str
# Decision (VAP L3)
feature_scores: dict # Per-criterion scores
final_score: float
threshold_applied: float
decision: str # PASS | FAIL | REVIEW
# Explainability (Article 86)
top_factors: List[dict] # SHAP-style contributions
counterfactual: Optional[dict]
# Human Oversight (Article 14)
human_override: Optional[dict]
# Integrity (VAP L1)
prev_hash: str
signature: str
2. Hash Chain Implementation
import nacl.signing
import nacl.encoding
import uuid
import time
class PAPHiringLogger:
"""Tamper-evident logger for hiring AI decisions."""
def __init__(self, signing_key: nacl.signing.SigningKey):
self.signing_key = signing_key
self.chain: List[HiringDecisionEvent] = []
self.prev_hash = "0" * 64 # Genesis
def _generate_event_id(self) -> str:
"""Generate UUID v7 (time-ordered) for event correlation."""
# UUID v7: timestamp in first 48 bits
timestamp_ms = int(time.time() * 1000)
uuid_bytes = timestamp_ms.to_bytes(6, 'big') + \
uuid.uuid4().bytes[6:]
return str(uuid.UUID(bytes=bytes(uuid_bytes), version=7))
def _compute_hash(self, event_dict: dict) -> str:
"""SHA-256 hash with JSON canonicalization (RFC 8785)."""
canonical = json.dumps(event_dict, sort_keys=True,
separators=(',', ':'))
return hashlib.sha256(canonical.encode()).hexdigest()
def _sign_event(self, event_hash: str) -> str:
"""Ed25519 signature of event hash."""
signed = self.signing_key.sign(event_hash.encode())
return signed.signature.hex()
def log_decision(
self,
candidate_id: str,
job_id: str,
model_version: str,
model_config: dict,
feature_scores: dict,
final_score: float,
threshold: float,
shap_values: List[dict],
human_override: Optional[dict] = None
) -> HiringDecisionEvent:
"""
Log a hiring decision with full provenance chain.
This creates an immutable, cryptographically signed record
that satisfies EU AI Act Article 12 requirements.
"""
# Generate identifiers
event_id = self._generate_event_id()
timestamp_ns = time.time_ns()
# Hash sensitive identifiers (GDPR compliance)
candidate_hash = hashlib.sha256(
candidate_id.encode()
).hexdigest()
config_hash = hashlib.sha256(
json.dumps(model_config, sort_keys=True).encode()
).hexdigest()
# Determine decision
decision = "PASS" if final_score >= threshold else "FAIL"
if human_override:
decision = human_override.get("override_decision", decision)
# Build event (without signature)
event_dict = {
"event_id": event_id,
"timestamp_ns": timestamp_ns,
"candidate_id_hash": candidate_hash,
"job_requisition_id": job_id,
"model_version": model_version,
"model_config_hash": config_hash,
"training_date": model_config.get("training_date", "unknown"),
"feature_scores": feature_scores,
"final_score": final_score,
"threshold_applied": threshold,
"decision": decision,
"top_factors": shap_values[:5], # Top 5 contributors
"counterfactual": self._compute_counterfactual(
feature_scores, final_score, threshold
),
"human_override": human_override,
"prev_hash": self.prev_hash
}
# Compute hash and sign
event_hash = self._compute_hash(event_dict)
signature = self._sign_event(event_hash)
# Create final event
event = HiringDecisionEvent(
**event_dict,
signature=signature
)
# Update chain
self.prev_hash = event_hash
self.chain.append(event)
return event
def _compute_counterfactual(
self,
features: dict,
score: float,
threshold: float
) -> Optional[dict]:
"""
Generate counterfactual explanation for Article 86.
"What would need to change for a different outcome?"
"""
if score >= threshold:
return None
gap = threshold - score
# Simplified: identify smallest change needed
return {
"score_gap": round(gap, 3),
"explanation": f"Score was {gap:.1%} below threshold",
"suggestion": "Additional qualifications in top factors would improve score"
}
def verify_chain(self) -> bool:
"""
Verify entire hash chain integrity.
Returns True if no tampering detected.
"""
prev = "0" * 64
for event in self.chain:
# Rebuild event dict without signature
event_dict = {
k: v for k, v in event.__dict__.items()
if k != 'signature'
}
computed_hash = self._compute_hash(event_dict)
# Verify chain linkage
if event.prev_hash != prev:
return False
# Verify signature
verify_key = self.signing_key.verify_key
try:
verify_key.verify(
computed_hash.encode(),
bytes.fromhex(event.signature)
)
except nacl.exceptions.BadSignature:
return False
prev = computed_hash
return True
3. External Anchoring (RFC 3161 TSA)
For legal defensibility, anchor your hash chain to an external timestamp authority:
import requests
from asn1crypto import tsp, core
class ExternalAnchor:
"""RFC 3161 Time-Stamp Authority anchoring."""
TSA_URL = "https://freetsa.org/tsr" # Example TSA
@classmethod
def anchor_hash(cls, event_hash: str) -> dict:
"""
Get RFC 3161 timestamp token for event hash.
This provides third-party proof of existence at specific time.
"""
# Create timestamp request
hash_bytes = bytes.fromhex(event_hash)
ts_req = tsp.TimeStampReq({
'version': 1,
'message_imprint': {
'hash_algorithm': {'algorithm': 'sha256'},
'hashed_message': hash_bytes
},
'cert_req': True
})
# Send to TSA
response = requests.post(
cls.TSA_URL,
data=ts_req.dump(),
headers={'Content-Type': 'application/timestamp-query'}
)
# Parse response
ts_resp = tsp.TimeStampResp.load(response.content)
return {
"tsa_url": cls.TSA_URL,
"token": response.content.hex(),
"timestamp": ts_resp['time_stamp_token']['tst_info']['gen_time'].native.isoformat()
}
Handling GDPR's Right to Erasure: Crypto-Shredding
Here's the dilemma: Article 12 says "keep logs," but GDPR Article 17 says "delete on request."
Solution: Encrypt personal data, destroy keys on erasure request. Hash chain integrity is preserved.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
class CryptoShredder:
"""
GDPR Article 17 compliant data destruction.
Personal data is encrypted; deletion destroys the key.
Hash chain remains valid but content is irrecoverable.
"""
def __init__(self):
self.key_store = {} # candidate_hash -> AES key
def encrypt_candidate_data(
self,
candidate_id: str,
personal_data: dict
) -> tuple[str, bytes]:
"""Encrypt personal data with unique key per candidate."""
candidate_hash = hashlib.sha256(
candidate_id.encode()
).hexdigest()
# Generate unique key for this candidate
key = AESGCM.generate_key(bit_length=256)
self.key_store[candidate_hash] = key
# Encrypt
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(
nonce,
json.dumps(personal_data).encode(),
None
)
return candidate_hash, nonce + ciphertext
def process_erasure_request(self, candidate_id: str) -> bool:
"""
GDPR Article 17: Right to erasure.
Destroys encryption key, making personal data
mathematically irrecoverable while preserving
hash chain integrity for audit purposes.
"""
candidate_hash = hashlib.sha256(
candidate_id.encode()
).hexdigest()
if candidate_hash in self.key_store:
# Secure deletion of key
del self.key_store[candidate_hash]
return True
return False
Compliance Checklist: EU AI Act + GDPR
| Requirement | Implementation | Status |
|---|---|---|
| Article 12: Automatic logging | Hash chain with all decision events | ✅ |
| Article 14: Human oversight |
human_override field in schema |
✅ |
| Article 19: 6-month retention | External anchoring + archive | ✅ |
| Article 86: Explanation right | SHAP values + counterfactuals | ✅ |
| GDPR Article 17: Right to erasure | Crypto-shredding | ✅ |
| Evidence admissibility | Ed25519 signatures + RFC 3161 TSA | ✅ |
Why This Matters: Real-World Litigation
| Case | What Happened | PAP Would Have... |
|---|---|---|
| Mobley v. Workday (2025) | Class action certified against AI vendor | Provided tamper-evident evidence of non-discrimination |
| EEOC v. iTutorGroup (2023) | $365K settlement for age discrimination | Documented decision factors for each candidate |
| UK ICO Audit (2024) | 296 recommendations issued | Proved no protected-attribute filtering |
Without cryptographic proof, you're defending with "trust us, the logs are accurate." Courts increasingly reject this.
Getting Started
- Review the PAP specification: veritaschain.org/vap/pap
-
Clone the reference implementation:
pip install pynacl cryptography - Integrate sidecar pattern: Hook into your scoring pipeline's output
- Set up external anchoring: RFC 3161 TSA or transparency log
-
Test chain verification: Run
verify_chain()regularly
Current Status
PAP is currently in research phase under the VAP Framework. The financial domain profile (VCP) is production-ready at v1.0. Hiring AI specifications are being developed with input from:
- EU AI Office
- National Data Protection Authorities
- HR technology vendors
Want to contribute?
📧 standards@veritaschain.org
🔗 github.com/veritaschain
📄 IETF draft-kamimura-scitt-vcp
Conclusion
August 2026 isn't far away. The EU AI Act transforms hiring AI from "nice to have logging" to "legally mandated tamper-evident audit trails."
The question isn't whether to implement cryptographic logging—it's whether you want to build it yourself or adopt an open standard.
VAP-PAP offers: Hash chains + Ed25519 + SHAP explainability + GDPR-compatible erasure + RFC 3161 anchoring.
Your hiring AI deserves a flight recorder. Your candidates have a legal right to one.
"No decision without justification. No log without proof."
— VeritasChain Standards Organization
Related Articles:
- Building Cryptographic Audit Trails for SEC Rule 17a-4
- VCP: The Flight Recorder for Algorithmic Trading
License: CC BY 4.0
Tags: #ai #compliance #python #cryptography #euaiact #hiring #security
Top comments (0)