Task — engineering-spec@1

"DOC identity verification — 4 methods (WebAuthn / VNeID / SMS-OTP / email-link) with per-document method selection + audit"

draftTASK-DOC-006
module doc · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-AUTH-105 · blocks TASK-DOC-005

§1 — Description (BCP-14 normative)

The DOC service MUST ship identity verification at services/doc/src/verification/ supporting 4 methods with eIDAS level mapping, immutable audit, 4 memory audit kinds.

  1. MUST support 4 methods per DEC-1740 — webauthn / vneid / sms_otp / email_link.
  1. MUST validate verification_method against closed enum per DEC-1741, verification_result per DEC-1742.
  1. MUST dispatch per method:
  1. MUST map to eIDAS assurance level per DEC-1743:
  1. MUST enforce minimum level per document per DEC-1743 — if document requires 'substantial', sms_otp rejected.
  1. MUST define audit table at migration 0005: ``sql CREATE TABLE doc_identity_verifications ( verification_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, document_id UUID NOT NULL, signer_id UUID NOT NULL, method TEXT NOT NULL CHECK (method IN ('webauthn','vneid','sms_otp','email_link')), result TEXT NOT NULL CHECK (result IN ('verified','failed_invalid','failed_expired','failed_no_match')), assurance_level TEXT NOT NULL CHECK (assurance_level IN ('low','substantial','high')), challenge_id TEXT, verified_at TIMESTAMPTZ, failure_reason TEXT, ip_address TEXT, -- hashed per TASK-MEMORY-111 user_agent TEXT, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX verifications_doc_signer_idx ON doc_identity_verifications(tenant_id, document_id, signer_id); ALTER TABLE doc_identity_verifications ENABLE ROW LEVEL SECURITY; CREATE POLICY verif_rls ON doc_identity_verifications USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON doc_identity_verifications FROM cyberos_app; -- Audit immutable per DEC-1744 ``
  1. MUST expose endpoints: ``text POST /v1/doc/documents/{id}/verify/start body: {method, signer_id} POST /v1/doc/documents/{id}/verify/complete body: {challenge_id, response_data} GET /v1/doc/documents/{id}/verifications (signer-scoped list) ``
  1. MUST emit 4 memory audit kinds per DEC-1745. PII per TASK-MEMORY-111: ip_address SHA-256 hashed; signer_id (uuid) ok.
  1. MUST thread trace_id from start → complete → audit.
  1. MUST enforce challenge expiry: webauthn 5min, otp 10min, email_link 24h.
  1. MUST NOT allow lower-than-required assurance level per DEC-1743.
  1. MUST NOT mutate verification audit per DEC-1744.

§2 — Why this design

Why 4 methods (DEC-1740)? Covers global (webauthn/email), VN (vneid), and fallback (sms_otp); each tenant picks per-document.

Why eIDAS levels (DEC-1743)? EU contracts need 'substantial' or 'high' for legal validity; mapping enables enforcement.

Why immutable audit (DEC-1744)? Court evidence: years later, must prove who verified, when, how.

Why per-method handler files (DEC-1740)? Each has distinct protocols (FIDO2 vs OAuth vs SMS vs magic-link); separation enables independent testing.


§3 — API contract

POST   /v1/doc/documents/{id}/verify/start
POST   /v1/doc/documents/{id}/verify/complete
GET    /v1/doc/documents/{id}/verifications

Sample start:

{
  "method": "webauthn",
  "signer_id": "uuid"
}

Response:

{
  "challenge_id": "uuid",
  "challenge_data": "base64-challenge-bytes",
  "expires_at": "2026-05-17T10:05:00Z"
}

§4 — Acceptance criteria

  1. 4 methods enum + cardinality test. 2. 4 results enum + cardinality test. 3. Assurance levels mapped correctly. 4. Min level enforced per document. 5. WebAuthn FIDO2 flow works. 6. VNeID OAuth callback works. 7. SMS OTP 6-digit format. 8. Email magic-link token. 9. Challenge expiry enforced (5/10/24min/h). 10. 4 memory audit kinds emitted. 11. PII scrubbed (IP SHA256). 12. RLS denies cross-tenant. 13. Audit immutable (no UPDATE/DELETE grant). 14. Trace_id preserved. 15. Multiple verifications per signer allowed (retry on fail). 16. Multiple signers per doc. 17. Verification status visible to AM. 18. Failed reasons categorized (invalid/expired/no_match). 19. Challenge_id one-time use. 20. eIDAS level downgrade attempt rejected.

§5 — Verification

#[tokio::test]
async fn webauthn_high_assurance() {
    let ctx = TestContext::with_webauthn_credential().await;
    let r = ctx.verify_webauthn(ctx.doc_id, ctx.signer_id).await;
    assert_eq!(r.result, "verified");
    assert_eq!(r.assurance_level, "high");
}

#[tokio::test]
async fn sms_otp_low_level() {
    let ctx = TestContext::with_phone().await;
    let start = ctx.start_verify(ctx.doc_id, "sms_otp").await;
    let r = ctx.complete_verify(start.challenge_id, "123456").await;
    assert_eq!(r.assurance_level, "low");
}

#[tokio::test]
async fn min_level_enforced() {
    let ctx = TestContext::doc_requires_substantial().await;
    let r = ctx.try_verify_method(ctx.doc_id, "sms_otp").await;
    assert!(r.is_err());  // sms_otp is 'low', doc requires 'substantial'
}

#[tokio::test]
async fn challenge_expiry() {
    let ctx = TestContext::otp_started().await;
    ctx.advance_time(Duration::minutes(11)).await;
    let r = ctx.complete_verify(ctx.challenge_id, "123456").await;
    assert_eq!(r.result, "failed_expired");
}

// 5.5..5.10

§7 — Dependencies

Upstream: TASK-AUTH-105. Downstream: TASK-DOC-005 (multi-party signing uses this). Cross-module: TASK-MEMORY-111 (PII), TASK-DOC-001 (document RLS context).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Challenge expiredtimestamp checkfailed_expired; new challengeretry
OTP code wrongcomparefailed_invalidretry within window
WebAuthn signature invalidverifyfailed_invalidretry
VNeID OAuth deniedcallback errfailed_no_matchretry
Email link reuseone-time-use flagfailed_invalidnew link
SMS provider downretrysev-2; fall back to emailinherent
Phone number changedchallenge to currentinherentdata update
WebAuthn no credentialenroll firstsev-2; pick other methodenroll
eIDAS level downgrade attemptreject403use higher method
Cross-tenant verificationRLS404inherent

§11 — Implementation notes


End of TASK-DOC-006 spec.