Task — engineering-spec@1

"DOC VN CA chain — VNeID + VnPay/MK Group/Viettel-CA partners for VN-residency qualified digital signatures per Decree 130/2018"

draftTASK-DOC-004
module doc · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-DOC-001 · blocks none

§1 — Description (BCP-14 normative)

The DOC service MUST ship VN CA integration at services/doc/src/vn_ca/ with 3 partners + VNeID linkage + VN Root CA validation, 5 memory audit kinds.

  1. MUST support 3 partners per DEC-1790 — VnPay, MK Group, Viettel-CA; abstraction at abstraction.rs::dispatcher.
  1. MUST validate vn_ca_partner enum cardinality 3 per DEC-1791.
  1. MUST validate vn_ca_request_kind enum cardinality 5 per DEC-1792.
  1. MUST support VNeID identity linkage per DEC-1793 at vneid_linker.rs::link(signer, vneid_token):
  1. MUST dispatch per partner — each implements enroll_cert(signer, vneid_verified), sign(pdf_hash, cert_id), validate_chain(cert), revoke(cert_id).
  1. MUST validate chain to VN National Root CA per DEC-1790 at vn_root_validator.rs::validate(cert) — anchor must be in VN gov root list (refreshed quarterly).
  1. MUST return CMS signature with VN trust chain per DEC-1795; TASK-DOC-011 extends to LT.
  1. MUST store creds in KMS per DEC-1794 — CISO-only.
  1. MUST define tables at migration 0010: ```sql CREATE TABLE tenant_vn_ca_creds ( tenant_id UUID PRIMARY KEY, partner TEXT NOT NULL CHECK (partner IN ('vnpay','mk_group','viettel_ca')), encrypted_creds_arn TEXT NOT NULL, api_account_id TEXT, set_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE tenant_vn_ca_creds ENABLE ROW LEVEL SECURITY; CREATE POLICY vn_ca_creds_rls ON tenant_vn_ca_creds USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); GRANT UPDATE (partner, encrypted_creds_arn, api_account_id, set_by, updated_at) ON tenant_vn_ca_creds TO cyberos_app;

CREATE TABLE doc_vn_ca_signatures ( vn_ca_sig_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, document_id UUID NOT NULL, signer_id UUID NOT NULL, vneid_subject_id TEXT, partner TEXT NOT NULL CHECK (partner IN ('vnpay','mk_group','viettel_ca')), request_kind TEXT NOT NULL CHECK (request_kind IN ('certificate_enroll','signature_request','vneid_link','validation_check','revocation_check')), cert_chain_pem TEXT NOT NULL, signature_value BYTEA NOT NULL, timestamp_token BYTEA, vn_root_validated BOOLEAN NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','succeeded','failed','revoked')), trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE doc_vn_ca_signatures ENABLE ROW LEVEL SECURITY; CREATE POLICY vn_ca_sigs_rls ON doc_vn_ca_signatures USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON doc_vn_ca_signatures FROM cyberos_app; GRANT UPDATE (status) ON doc_vn_ca_signatures TO cyberos_app; ```

  1. MUST expose endpoints: ``text PUT /v1/doc/vn-ca/creds (CISO-only) POST /v1/doc/vn-ca/sign (internal — TASK-DOC-005 caller) POST /v1/doc/vn-ca/vneid-link (signer-initiated VNeID linkage) GET /v1/doc/vn-ca/signatures/{doc_id} ``
  1. MUST emit 5 memory audit kinds per DEC-1796. PII per TASK-MEMORY-111: vneid_subject_id SHA-256 hashed (national ID is sensitive); signature + cert chain hashed.
  1. MUST thread trace_id end-to-end.
  1. MUST NOT allow non-CISO creds per DEC-1794.
  1. MUST NOT skip VN Root CA validation per DEC-1790.
  1. MUST NOT sign without VNeID-linked identity for qualified signatures per DEC-1793.

§2 — Why this design

Why VN-domestic partners (DEC-1790)? Decree 130/2018 requires VN root CA chain for qualified signatures; EU/US CAs not accepted by VN courts.

Why 3 partners (DEC-1791)? Market diversity; CLO chooses based on industry vertical (VnPay = fintech focus, Viettel = enterprise, MK Group = SME).

Why VNeID linkage (DEC-1793)? Decree 130 requires verified national identity for qualified-level signatures; VNeID is the gov-blessed identity rail.

Why CISO creds (DEC-1794)? VN CA creds compromise = mass forgery on VN contracts; CISO has authority.


§3 — API contract

PUT    /v1/doc/vn-ca/creds                body: {partner, creds}
POST   /v1/doc/vn-ca/sign                 body: {document_id, signer_id, signer_cert_id}
POST   /v1/doc/vn-ca/vneid-link           body: {signer_id, vneid_oauth_callback_token}
GET    /v1/doc/vn-ca/signatures/{doc_id}

Sample VNeID link:

{
  "signer_id": "uuid",
  "vneid_oauth_callback_token": "encoded-token"
}

Response:

{
  "vneid_subject_id": "VN-citizen-id-hash",
  "cert_enrolled": true,
  "cert_id": "uuid",
  "expires_at": "2027-05-17"
}

§4 — Acceptance criteria

  1. 3 partners + cardinality test. 2. VnPay client works. 3. MK Group client works. 4. Viettel-CA client works. 5. VNeID link required for qualified. 6. VN Root CA validation enforced. 7. 5 request_kind enum. 8. CISO-only creds. 9. Creds in KMS. 10. 5 memory audit kinds emitted. 11. PII scrubbed (vneid_subject_id+signature+cert SHA256). 12. RLS denies cross-tenant. 13. Trace_id preserved. 14. TASK-DOC-005 integration. 15. Append-only sigs via REVOKE except status. 16. Non-VN chain rejected. 17. Sandbox + prod env per partner. 18. Cert revoke path. 19. Composes with TASK-DOC-011 for LT. 20. VNeID linkage 1-per-signer (idempotent).

§5 — Verification

#[tokio::test]
async fn vnpay_signature_with_vneid_link() {
    let ctx = TestContext::with_vnpay_creds_and_vneid_signer().await;
    let r = ctx.vn_ca_sign(ctx.doc_id, ctx.signer_id).await;
    assert!(r.vn_root_validated);
    assert_eq!(r.partner, "vnpay");
    assert!(r.vneid_subject_id.is_some());
}

#[tokio::test]
async fn non_vneid_signer_rejected_for_qualified() {
    let ctx = TestContext::vn_signer_no_vneid().await;
    let r = ctx.try_vn_ca_sign(ctx.doc_id, ctx.signer_id).await;
    assert!(r.is_err());
}

#[tokio::test]
async fn non_vn_root_chain_rejected() {
    let ctx = TestContext::with_eu_chain_cert().await;
    let r = ctx.try_vn_ca_sign(ctx.doc_id, ctx.signer_id).await;
    assert!(r.is_err());
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-DOC-001. Cross-module: TASK-DOC-005 (caller), TASK-DOC-006 (VNeID handler shared), TASK-DOC-011 (LT extend), TASK-AUTH-105 (KMS), TASK-AUTH-101 (CISO), TASK-MEMORY-111 (PII).

§10 — Failure modes (similar to DOC-002/003)

FailureDetectionOutcomeRecovery
Partner API downretrysev-1; failedswitch partner
VNeID OAuth deniedcallbackfailed_no_matchretry
Cert chain not VN rootvalidatorreject; sev-1new enroll
Cert revokedOCSPstatus=revokednew cert
Creds expired401sev-1; CISOrotate
VN Root CA list lagquarterly refreshsev-3maintenance
Sandbox vs prod confusionenv flaghard reject mismatchinherent
Cross-tenant cert useRLS403inherent
Signer VNeID not VN citizenVNeID rejectsfailed_no_matchinherent
Multi-partner conflict (tenant switched)use current creds rowlast-winsinherent

§11 — Implementation notes


End of TASK-DOC-004 spec.