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.
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_idensures 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_idin 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_tierevery 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
attestruns, no raw artifact bytes exist on disk, in logs, or in any cache — only theAttestationRecordwith its hash. This guards Art. 5(1)© directly and is expanded in the child implementation page’s verification section. - Clock-start attribution. Assert
start_clockrefuses a failed attestation and that a successful one anchors the deadline toattested_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.
Related
- DSR Architecture & Intake Routing — the intake control plane this verification gate anchors as Stage 2.
- Secure Intake Form Design — the signed, schema-valid payload this stage receives before proving identity.
- Jurisdiction Routing Logic — the routing decision that runs once identity is attested.
- 30-Day vs 45-Day SLA Mapping — how the attestation timestamp becomes a statutory deadline.
- Implementing Step-Up Authentication for High-Impact DSRs — the Tier 2 TOTP-plus-ephemeral-ID path built end to end.
- Audit Logging & Compliance Evidence — where the attestation event is recorded as tamper-evident evidence.