Identity Verification & Attestation for Data Subject Requests

Within the broader DSR Architecture & Intake Routing framework, identity verification is the gate that decides whether the person in front of the pipeline is really the data subject before any personal data is retrieved, exported, or erased. It is Stage 2 of the intake control plane, sitting between secure ingestion and jurisdiction resolution, and it carries a specific compliance burden: a controller that acts on an unverified request risks disclosing personal data to the wrong person (a breach), while a controller that demands excessive proof for a trivial request over-collects sensitive data (a data-minimization violation). Both failures are reportable. This page specifies the middle path — a risk-based verification model that scales friction to the impact of the operation, hashes every verification artifact and purges the raw source, and treats the moment of successful attestation as the single legally defensible event that starts the statutory response clock.

Verification is not a yes/no checkpoint; it is a graded decision. A preference update from a logged-in session needs almost no additional proof, whereas an erasure of a legal record or a portability export of financial history needs step-up authentication and, sometimes, a government-issued identity document. The stage below resolves that gradient deterministically through four phases — risk scoring, challenge selection, artifact attestation, and clock start — each of which emits an audit event so the verification decision is as replayable as the routing decision that follows it.

Risk-based identity verification: from scoring through tiered challenges to attestation and clock start A verified intake payload enters a risk-scoring engine that classifies the request into one of three tiers. Tier zero, low-friction requests such as a preference update, clears with a one-time passcode over a previously verified channel. Tier one, standard requests such as access, requires a step-up one-time passcode plus session re-authentication. Tier two, high-impact operations such as erasure or portability, requires multi-factor authentication plus an ephemeral government-identity document. All three tiers converge on an artifact-attestation step that hashes every verification artifact with SHA-256 or BLAKE3 and immediately purges the raw source. Successful attestation emits the attestation event, which starts the statutory SLA clock and is written to the audit evidence ledger. Verified intake payload Risk-scoring engine Tier 0 — low frictionOTP over verified channel Tier 1 — standardstep-up OTP + re-auth Tier 2 — high impactMFA + ephemeral gov-ID Artifact attestationhash (SHA-256 / BLAKE3), purge raw Attestation event —SLA clock starts Audit evidenceledger low standard high impact identity attested
Risk-based verification: the scoring engine sizes friction to impact, three tiers converge on a single attestation step that hashes and purges artifacts, and the attestation event starts the statutory clock while feeding the audit ledger.

Phase 1 — Risk Scoring

Risk scoring is the phase that decides how hard to make identity proof, and it must be a pure function of the request rather than a reviewer’s mood. The inputs are the request type (erasure and portability carry the highest disclosure impact), the sensitivity of the data in scope, the strength of the channel the request arrived on (an authenticated session outranks an anonymous web form), and any anomaly signals such as a mismatch between the requester’s channel and the account of record. The output is a discrete tier, not a floating-point score exposed to downstream code — a tier is auditable, a raw score invites relitigation.

The compliance anchor here is GDPR Art. 12(6), which permits the controller to request additional information necessary to confirm the identity of the data subject where it has reasonable doubts. That “reasonable doubts” standard is exactly what risk scoring codifies: it converts an open-ended discretion into a rule that produces the same tier for the same inputs every time, so the decision to demand a government ID can be defended as consistent rather than arbitrary. Equally, Art. 12(2) obliges the controller to facilitate the exercise of data-subject rights — a reminder that friction is a cost, and Tier 0 exists precisely so a low-risk request is not buried under proof it does not need.

from enum import IntEnum

from pydantic import BaseModel, ConfigDict, Field


class VerificationTier(IntEnum):
    """Discrete assurance tiers — higher demands stronger proof."""

    LOW_FRICTION = 0   # preference update, opt-out over a known channel
    STANDARD = 1       # access, rectification
    HIGH_IMPACT = 2    # erasure, portability, legal-record correction


HIGH_IMPACT_TYPES = frozenset({"erasure", "portability"})


class RiskInputs(BaseModel):
    model_config = ConfigDict(strict=True, frozen=True)

    request_type: str = Field(min_length=3, max_length=32)
    channel_pre_verified: bool  # arrived on an authenticated/known channel
    special_category_in_scope: bool  # Art. 9 data implicated
    channel_account_mismatch: bool  # requester channel != account of record


def score_tier(inputs: RiskInputs) -> VerificationTier:
    """Deterministically map request risk to a verification tier."""
    if inputs.request_type in HIGH_IMPACT_TYPES or inputs.special_category_in_scope:
        return VerificationTier.HIGH_IMPACT
    if inputs.channel_account_mismatch or not inputs.channel_pre_verified:
        return VerificationTier.STANDARD
    return VerificationTier.LOW_FRICTION

Special-category data deserves its own line in the scoring table. Where the data in scope includes health, biometric, or other Art. 9 categories, the request escalates to the high-impact tier regardless of its nominal type, because an erroneous disclosure of special-category data is a graver harm than an ordinary one. Note the recursive hazard the diagram warns against: a government-ID document is itself Art. 9-adjacent special-category material, so collecting it to protect Art. 9 data must be tightly bounded in time — which is what Phase 3 enforces.

Phase 2 — Challenge Selection

With a tier assigned, challenge selection maps it to a concrete set of proofs. The mapping is a table, not code branches scattered across services, so it can be reviewed by a compliance officer without reading Python. The governing principle is proportionality in both directions: enough assurance to defend the disclosure, and no more collection than the assurance requires.

Tier Trigger examples Required challenge Artifact collected Compliance rationale
0 — low friction Opt-out, preference change over a signed-in session One-time passcode to the pre-verified email/phone on file OTP delivery + success flag (no new PII) Art. 12(2) — facilitate rights; do not over-verify a low-impact act
1 — standard Access (Art. 15), rectification (Art. 16) Step-up OTP plus session re-authentication OTP + auth assertion hash Art. 12(6) — reasonable-doubt proof proportionate to a disclosure
2 — high impact Erasure (Art. 17), portability (Art. 20), legal-record change MFA plus an ephemeral government-ID check Hash of the ID match decision only Art. 12(6) proof for irreversible acts; Art. 5(1)© bounds retention
2 + Art. 9 Any request touching health/biometric data Tier 2 challenge, escalated review Hash of the decision only Art. 9 special-category data warrants the strongest assurance

Two rules keep the table honest. First, a channel is only “pre-verified” if the pipeline itself verified it earlier — inheriting trust from an upstream system that never proved the channel is how attackers walk requests through Tier 0. Second, the government-ID step in Tier 2 is a match-and-discard operation: the document is compared to the account of record, a decision is recorded, and the image is never persisted to primary storage. The detailed implementation of the Tier 2 path — the TOTP step-up, the ephemeral upload, and the attestation record — is built end to end in Implementing Step-Up Authentication for High-Impact DSRs.

This stage assumes a signed, schema-valid payload from Secure Intake Form Design; it owns only the identity decision, and it hands the attested request forward to Jurisdiction Routing Logic once proof is complete.

Phase 3 — Artifact Attestation

Attestation is where the verification proof is reduced to something safe to keep. The naive instinct — retain the passport scan “so we can prove we checked” — is precisely the wrong move: holding a scanned identity document to evidence a verification is itself a data-minimization violation under GDPR Art. 5(1)©, which requires personal data to be adequate, relevant, and limited to what is necessary. The necessary artifact is not the document; it is the fact that a document was checked and matched. So the pipeline hashes the artifact, records the decision, and purges the raw bytes.

Hashing uses a NIST SP 800-107 approved function — SHA-256 or the faster BLAKE3 — over the canonical bytes of the artifact. The hash is a one-way commitment: it proves that a specific artifact was presented without letting anyone reconstruct the document, and it lets a later auditor confirm that the same artifact was seen if it is ever re-presented, without the controller ever storing the sensitive original.

import hashlib
from datetime import datetime, timezone

from pydantic import BaseModel, ConfigDict, Field


class AttestationRecord(BaseModel):
    """The only durable output of verification — no raw artifact retained."""

    model_config = ConfigDict(strict=True, frozen=True)

    request_id: str = Field(pattern=r"^dsr_[0-9a-f]{32}$")
    tier: int = Field(ge=0, le=2)
    method: str = Field(min_length=3, max_length=48)  # e.g. "totp+gov_id"
    artifact_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
    matched: bool
    attested_at: datetime


def attest(request_id: str, tier: int, method: str, artifact: bytes,
           matched: bool) -> AttestationRecord:
    """Hash the verification artifact, then discard the raw bytes.

    `artifact` (an ID image, an auth assertion) is hashed here and must not be
    persisted anywhere else — the caller passes bytes already in memory and
    lets them fall out of scope immediately after this call returns.
    """
    digest = hashlib.sha256(artifact).hexdigest()
    record = AttestationRecord(
        request_id=request_id,
        tier=tier,
        method=method,
        artifact_sha256=digest,
        matched=matched,
        attested_at=datetime.now(timezone.utc),
    )
    # No write of `artifact` to disk, cache, or log occurs anywhere in scope.
    return record

The discipline that makes this defensible is negative: it is defined by what the code does not do. The raw image is never written to object storage, never logged, never cached for “debugging”, and never attached to a support ticket. Enforcing that is partly architectural — the upload lands in an ephemeral buffer with a short TTL — and partly a test obligation, verified in the section below. The AttestationRecord is what flows onward; the document does not exist a second after the hash is computed.

Phase 4 — Clock Start

The final phase converts a completed attestation into the event that anchors every statutory deadline downstream. This matters because the clock cannot lawfully start on an unverified request. GDPR Art. 12(3) obliges action within one month “of receipt of the request”, but Art. 12(6) lets the controller pause for identity confirmation where it has reasonable doubts — so the operative start is the moment doubt is resolved, i.e. successful attestation, not the moment an anonymous form was submitted. Anchoring the timer to the attestation timestamp gives a start point that is both legally defensible and replayable years later during an audit.

from datetime import datetime, timedelta

# Statutory response windows keyed by resolved framework (baseline).
SLA_WINDOWS = {
    "GDPR": timedelta(days=30),     # Art. 12(3) — within one month
    "CCPA": timedelta(days=45),     # §1798.130(a)(2)
    "UNKNOWN": timedelta(days=30),  # most-protective fallback
}


def start_clock(record: AttestationRecord, framework: str) -> datetime:
    """Anchor the statutory deadline to attestation, never to submission."""
    if not record.matched:
        raise ValueError("cannot start the SLA clock on a failed attestation")
    if record.attested_at.tzinfo is None:
        raise ValueError("attested_at must be timezone-aware for defensible math")
    return record.attested_at + SLA_WINDOWS.get(framework, SLA_WINDOWS["UNKNOWN"])

The attestation event is not only a timer trigger; it is a first-class audit record. It flows into the Audit Logging & Compliance Evidence layer as a structured attested event carrying the tier, the method, the artifact hash, and the match decision — never the raw artifact. That single event lets a regulator later confirm why the clock started when it did and what proof justified the disclosure that followed, without the controller retaining any sensitive source document. The deadline itself is owned by the 30-Day vs 45-Day SLA Mapping stage, which consumes the attestation timestamp this phase produces.

Edge Cases & Conflict Resolution

Identity verification is where trust ambiguity surfaces. Resolve each case deterministically rather than defaulting to convenience:

  • Failed attestation. A mismatch or an abandoned challenge never starts the clock and never advances the request. The attempt is logged as a failed attestation event, and the request stays in a pending-verification state until proof completes or a retention limit expires — a failed check is not a silent drop.
  • Over-collection pressure. When a reviewer wants “just to keep the ID to be safe”, the answer is no: Art. 5(1)© makes that retention the violation. The hash plus decision is the defensible artifact; the document is a liability. Encode this as an architectural block, not a policy memo.
  • Channel takeover. If the pre-verified channel itself may be compromised (e.g. a reported account takeover), the pipeline must not trust a Tier 0 OTP to that channel — it escalates to a higher tier that proves identity out of band, because the “known” channel is exactly what the attacker controls.
  • Special-category recursion. Collecting a government ID to protect Art. 9 data introduces new Art. 9 data. Bound it: ephemeral buffer, short TTL, match-and-discard, hash-only retention. The strongest tier must also be the most disciplined about deletion.
  • Repeated / automated requests. Idempotency on request_id ensures a resubmitted request reuses the original attestation rather than restarting proof or the clock; a genuinely new request gets its own attestation and its own timestamp.
  • Reasonable-doubt escalation mid-flow. If an anomaly appears after a low tier was assigned (e.g. the requester suddenly asks to redirect delivery to a new address), the pipeline may re-score and demand step-up under Art. 12(6) before proceeding — the tier is a live decision, not a one-time stamp.

Performance & Scale Considerations

Verification sits on the intake hot path, so it must be cheap for the common case and isolate the expensive one:

  • Keep Tier 0 lightweight. The overwhelming majority of requests are low-impact; an OTP round-trip over a known channel should complete in the low hundreds of milliseconds and never touch document handling. Reserve the heavy Tier 2 machinery for the small fraction that needs it.
  • Ephemeral storage for artifacts. The government-ID upload lands in a short-TTL buffer (in-memory or an object store with an aggressive lifecycle expiry), never in primary storage. Sizing that buffer for peak concurrent Tier 2 uploads — not total volume — keeps the sensitive surface small.
  • Hash on the worker, not the gateway. Compute the SHA-256/BLAKE3 digest close to where the bytes already live so the raw artifact never travels further into the system than it must; ship only the hash onward.
  • Cache tier decisions per request, not per subject. Cache the resolved tier and attestation keyed on request_id in Redis to make retries idempotent, but never cache an attestation across requests — each request re-proves identity on its own merits.
  • Rate-limit challenge issuance. OTP and MFA endpoints are abuse targets; cap issuance per account and per channel so verification cannot be turned into an enumeration or spam oracle.

Testing & Compliance Verification

Treat verification as a compliance control and prove it before it fronts production requests:

  • Tier assignment matrix. Feed score_tier every combination of request type, channel state, and special-category flag; assert erasure and portability always reach Tier 2 and that Art. 9 data escalates regardless of nominal type.
  • Raw-artifact non-retention. The single most important test: assert that after attest runs, no raw artifact bytes exist on disk, in logs, or in any cache — only the AttestationRecord with its hash. This guards Art. 5(1)© directly and is expanded in the child implementation page’s verification section.
  • Clock-start attribution. Assert start_clock refuses a failed attestation and that a successful one anchors the deadline to attested_at, never to an earlier submission timestamp.
  • Failed-attestation containment. Assert a mismatch neither advances the request nor emits a clock-start event, and that it does emit a failed-attestation audit event.
  • Hash reproducibility. Assert the same artifact bytes always yield the same digest, so a re-presented document can be recognized against the stored hash without retaining the original.
def test_erasure_forces_high_impact_tier():
    inputs = RiskInputs(
        request_type="erasure",
        channel_pre_verified=True,
        special_category_in_scope=False,
        channel_account_mismatch=False,
    )
    assert score_tier(inputs) is VerificationTier.HIGH_IMPACT


def test_failed_attestation_never_starts_clock():
    record = AttestationRecord(
        request_id="dsr_" + "a" * 32,
        tier=2,
        method="totp+gov_id",
        artifact_sha256="0" * 64,
        matched=False,
        attested_at=datetime.now(timezone.utc),
    )
    try:
        start_clock(record, "GDPR")
        assert False, "clock must not start on a failed attestation"
    except ValueError:
        pass

Every assertion here maps to a specific obligation — tier assignment to Art. 12(6) reasonable doubt, non-retention to Art. 5(1)©, clock attribution to Art. 12(3) — so a passing suite is itself part of the evidence that the control operates as designed.

Frequently Asked Questions

Why not just require a government ID for every DSR to be safe?

Because over-collection is itself a violation. GDPR Art. 5(1)© limits personal data to what is necessary, and Art. 12(2) obliges controllers to facilitate rights rather than obstruct them. Demanding a passport for a one-click opt-out collects sensitive identity data with no proportionate justification, creating breach exposure and a data-minimization finding. Risk-based tiers reserve the strongest proof for the irreversible, high-impact operations that actually warrant it.

When exactly does the statutory SLA clock start?

At successful identity attestation, not at form submission. GDPR Art. 12(3) requires action within one month of receipt, but Art. 12(6) permits pausing to confirm identity where there are reasonable doubts. Until attestation resolves that doubt the request is not actionable, so the defensible start is the attestation timestamp. See 30-Day vs 45-Day SLA Mapping for how that anchor becomes a deadline.

Should we retain the ID document as proof that we verified identity?

No. Retaining the source document to “prove” verification is a data-minimization violation under GDPR Art. 5(1)©, and an ID image is Art. 9 special-category-adjacent data that raises the stakes further. Hash the artifact with a NIST SP 800-107 approved function (SHA-256 or BLAKE3), record the match decision and hash, and purge the raw bytes. The hash proves an artifact was checked without keeping the sensitive original.

How do we decide which requests need step-up authentication?

A deterministic risk-scoring function classifies each request by impact and context. Erasure and portability, requests touching Art. 9 special-category data, and any channel/account mismatch escalate to the high-impact tier that requires MFA plus an ephemeral government-ID check. Everything else clears with a one-time passcode over a pre-verified channel. The full high-impact path is implemented in Implementing Step-Up Authentication for High-Impact DSRs.

What does the attestation event feed downstream?

It feeds two things. It starts the statutory clock consumed by the SLA stage, and it is written as a structured event to the Audit Logging & Compliance Evidence layer carrying the tier, method, artifact hash, and match decision — never the raw artifact. That record lets a regulator later confirm why the clock started and what proof justified the disclosure, without any sensitive document being retained.