GDPR Cryptographic Erasure
VeriProof implements GDPR Article 17 (Right to Erasure) using cryptographic erasure — destroying the key material used to derive cryptographic commitments, rather than deleting the data itself. This approach satisfies the “no longer identifiable” standard of erasure while preserving the structural integrity of blockchain anchoring records that cannot be removed from Solana.
This page is a technical deep dive. For the practical implementation guide, see GDPR Compliance.
Why Cryptographic Erasure?
Traditional data deletion is administratively straightforward but technically incomplete: database records can be deleted, but blockchain anchors on a public distributed ledger cannot. If you anchor session records to Solana and then attempt to comply with a right to erasure by deleting the database row, the blockchain record still exists — and it continues to reference a real AI decision, even if the details are gone.
Cryptographic erasure resolves this by separating two concerns:
- The record — the session data in PostgreSQL and the Merkle root on Solana
- The key — the cryptographic material needed to compute or verify that commitment
When you erase the key, the record becomes permanently unverifiable. The blockchain anchor still exists, but it proves nothing — you cannot compute the Merkle root that would match it without the key, so it cannot be used to verify claimed content. The link between the anchor and any identifiable individual is severed.
This approach is consistent with GDPR Recital 26, which defines personal data as data that “can be attributed” to a natural person. Once the key is destroyed, the session record cannot be attributed to an identified or identifiable individual.
Key Derivation Architecture
The commitment for each session is computed using a two-level HKDF key derivation (NIST SP 800-108):
Azure Key Vault
│
│ (master pepper — 512-bit, HKDF-SHA256 IKM)
▼
Level 1 Key Derivation
HKDF-SHA256(
IKM: master_pepper,
info: "veriproof-v1" || customer_id,
length: 32 bytes
)
│
▼
Level 1 Key (256-bit, tenant-scoped)
│
│ + per-subject salt (32 bytes, stored in data_subjects.salt)
▼
Level 2 Key Derivation
HKDF-SHA256(
IKM: level1_key,
info: "veriproof-subject-v1" || data_subject_id || salt,
length: 32 bytes
)
│
▼
Level 2 Key (256-bit, subject-scoped)
│
▼
Commitment = HMAC-SHA256(level2_key, session_json)
= stored in sessions.merkle_rootErasure target: The data_subjects.salt column. Setting it to NULL makes the
Level 2 key derivation impossible. The Level 1 key and master pepper are untouched
and continue to protect other subjects.
Data Subject Lifecycle
Creation
When a session is submitted for a specific user, create a data subject record:
subject = client.data_subjects.create_or_get(
external_id="user_12345", # your internal user identifier
display_name="User 12345", # optional; stored for internal reference
)The API is idempotent — calling it again with the same external_id returns the
existing subject. On creation, a 256-bit cryptographically random salt is generated
using RandomNumberGenerator.GetBytes() and stored with the subject record.
Session Linking
Submit sessions with the data_subject_id included:
from veriproof.sdk import Session
session = Session(
trace_id="trace_abc123",
data_subject_id=subject.id,
# ... other session fields
)
client.sessions.submit(session)The session commitment is computed using the Level 2 key derived from this subject’s salt.
Sessions without a data_subject_id are linked to the tenant’s base key and are not
erasable on a per-subject basis.
Erasure Request
When a user exercises their Article 17 right:
client.data_subjects.request_erasure(
subject_id=subject.id,
reason="User right to erasure request under GDPR Article 17",
reference="ticket_99887", # your support ticket reference
)This transitions the subject to ErasureRequested status. The erasure is not
executed immediately.
30-Day Hold Period
The hold period serves two purposes:
- Error recovery: Erasure requests made in error (or by an attacker) can be cancelled within 30 days
- Legal hold checking: A daily background worker verifies whether any active legal hold prevents erasure before executing it
To cancel a pending erasure within the hold period, open Compliance → Privacy &
Data Rights in the Customer Portal, select the Data Subjects tab, find the
subject, and click Cancel Erasure. Enter the reason for cancellation. The subject
status returns to Active and the erasure request is nullified.
Erasure Execution
The background worker runs daily at 02:00 UTC. For each subject with an expired hold period and no active legal hold:
- Fetches the data subject record
- Verifies no legal hold is active
- Sets
data_subjects.salt = NULL - Updates status to
Erased, recordserased_attimestamp - Writes an audit log entry
After erasure, any attempt to verify a session commitment for this subject returns
"Data subject has been erased". The commitment values and blockchain anchors remain
in the database and on Solana, but they are permanently unverifiable.
Legal Holds
A legal hold prevents erasure when records must be retained for legal proceedings or regulatory investigations. To place a legal hold, open Compliance → Privacy & Data Rights → Data Subjects, find the subject, and click Place Legal Hold. Enter the reason (for example, the litigation reference number) and the expiry date.
Attempting to erase a subject with an active hold is blocked automatically by the background worker — the erasure is deferred until all active holds have expired.
Once the hold expires, the standard erasure request and 30-day hold sequence applies.
Verification Testing
To verify that your erasure implementation is working correctly:
Create a test data subject and session
subject = client.data_subjects.create_or_get(external_id="erasure-test-001")
session = client.sessions.submit(Session(data_subject_id=subject.id, ...))Verify the session (should pass)
result = client.sessions.verify(session.id)
assert result.is_valid # TrueRequest erasure and wait for execution
In production, wait 30 days. In a test environment, use an API flag to bypass the hold:
client.data_subjects.request_erasure(
subject_id=subject.id,
bypass_hold=True, # TEST ENVIRONMENTS ONLY — executes immediately
)Verify the session (should fail)
result = client.sessions.verify(session.id)
assert not result.is_valid
assert result.error == "Data subject has been erased"Audit Trail
Every erasure lifecycle event is recorded in a tamper-resistant audit log:
| Event | Recorded fields |
|---|---|
SubjectCreated | subject_id, external_id, created_at, created_by |
ErasureRequested | subject_id, reason, reference, requested_at, requested_by |
ErasureCancelled | subject_id, cancelled_at, cancelled_by |
LegalHoldCreated | subject_id, reason, expires_at |
LegalHoldExpired | subject_id, expired_at |
ErasureExecuted | subject_id, erased_at, worker_run_id |
Next Steps
- GDPR Compliance — practical implementation guide
- Encryption at Rest — encryption architecture the erasure builds on
- Evidence Export — including GDPR audit log in compliance packages