DEV Community

Cover image for Building Tamper-Evident Audit Trails for Hiring AI: A Technical Deep Dive into VAP-PAP

Building Tamper-Evident Audit Trails for Hiring AI: A Technical Deep Dive into VAP-PAP

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For hiring AI, PAP defines:

  1. What to log: Candidate hash, model version, feature weights, decision, human override
  2. How to secure it: Hash chains + Ed25519 signatures + external anchoring
  3. 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
               └───────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
        }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Review the PAP specification: veritaschain.org/vap/pap
  2. Clone the reference implementation: pip install pynacl cryptography
  3. Integrate sidecar pattern: Hook into your scoring pipeline's output
  4. Set up external anchoring: RFC 3161 TSA or transparency log
  5. 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:

License: CC BY 4.0

Tags: #ai #compliance #python #cryptography #euaiact #hiring #security

Top comments (0)