Task — engineering-spec@1

"DOC eIDAS QTSP integration — GlobalSign or Cryptomathic partner for EU residency qualified signatures"

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

§1 — Description (BCP-14 normative)

The DOC service MUST ship eIDAS QTSP integration at services/doc/src/qtsp/ with partner abstraction, cert chain validation, PAdES-B-LT signatures, 4 memory audit kinds.

  1. MUST support 2 partners per DEC-1770 — GlobalSign + Cryptomathic; abstraction at abstraction.rs::dispatcher(partner).
  1. MUST validate qtsp_partner against closed enum per DEC-1771.
  1. MUST validate qtsp_request_kind against closed enum per DEC-1772.
  1. MUST dispatch per partner:
  1. MUST validate returned cert chain at cert_chain_validator.rs::validate(signature):
  1. MUST return PAdES-B-LT signature per DEC-1774 — embedded validation data (cert chain + OCSP/CRL + timestamp) per ETSI EN 319 142-1.
  1. MUST store QTSP creds in KMS per DEC-1773 — tenant_qtsp_creds.encrypted_creds_arn; CISO-only writes via TASK-AUTH-101.
  1. MUST define tables at migration 0008: ```sql CREATE TABLE tenant_qtsp_creds ( tenant_id UUID PRIMARY KEY, partner TEXT NOT NULL CHECK (partner IN ('globalsign','cryptomathic')), 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_qtsp_creds ENABLE ROW LEVEL SECURITY; CREATE POLICY qtsp_creds_rls ON tenant_qtsp_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_qtsp_creds TO cyberos_app;

CREATE TABLE doc_qtsp_signatures ( qtsp_sig_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, document_id UUID NOT NULL, signer_id UUID NOT NULL, partner TEXT NOT NULL CHECK (partner IN ('globalsign','cryptomathic')), request_kind TEXT NOT NULL CHECK (request_kind IN ('certificate_request','signature_request','validation_request','revocation_check')), cert_chain_pem TEXT NOT NULL, signature_value BYTEA NOT NULL, timestamp_token BYTEA NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','succeeded','failed','revoked')), failure_reason TEXT, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE doc_qtsp_signatures ENABLE ROW LEVEL SECURITY; CREATE POLICY qtsp_sigs_rls ON doc_qtsp_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_qtsp_signatures FROM cyberos_app; GRANT UPDATE (status, failure_reason) ON doc_qtsp_signatures TO cyberos_app; ```

  1. MUST expose endpoints: ``text PUT /v1/doc/qtsp/creds (CISO-only) POST /v1/doc/qtsp/sign (internal — called by TASK-DOC-005) GET /v1/doc/qtsp/signatures/{doc_id} (audit query) ``
  1. MUST emit 4 memory audit kinds per DEC-1775. PII per TASK-MEMORY-111: signature_value + cert_chain hashed; ids ok.
  1. MUST thread trace_id from TASK-DOC-005 request → partner call → cert validation → audit.
  1. MUST NOT allow non-CISO creds write per DEC-1773.
  1. MUST NOT skip cert chain validation per DEC-1774.

§2 — Why this design

Why partner abstraction (DEC-1770)? Vendor diversity reduces lock-in + enables failover; abstraction hides API differences.

Why GlobalSign + Cryptomathic (DEC-1771)? Both EU Trust List qualified; both have stable REST APIs; future partners extensible.

Why CISO-gated creds (DEC-1773)? QTSP creds = signing authority; compromise = mass fraud on EU contracts.

Why PAdES-B-LT (DEC-1774)? ETSI standard; enables LTV (long-term validation); TASK-DOC-011 re-stamps at year-9 to keep valid.


§3 — API contract

PUT    /v1/doc/qtsp/creds              body: {partner, creds, api_account_id}
POST   /v1/doc/qtsp/sign               body: {document_id, signer_id, signer_cert_request}
GET    /v1/doc/qtsp/signatures/{doc_id}

Sample sign request (internal, TASK-DOC-005 caller):

{
  "document_id": "uuid",
  "signer_id": "uuid",
  "signer_cert_request": "PEM-encoded CSR"
}

Response:

{
  "qtsp_sig_id": "uuid",
  "partner": "globalsign",
  "signature_value": "base64",
  "cert_chain_pem": "-----BEGIN CERTIFICATE-----...",
  "timestamp_token": "base64",
  "padesblt_blob_s3_key": "..."
}

§4 — Acceptance criteria

  1. 2 partners + enum cardinality test. 2. GlobalSign DSS REST works. 3. Cryptomathic Signer API works. 4. Cert chain validated to EU Trust List root. 5. OCSP/CRL revocation checked. 6. PAdES-B-LT format returned. 7. Timestamp authority signed. 8. request_kind enum cardinality 4. 9. CISO-only creds (403 for others). 10. Creds in KMS only. 11. 4 memory audit kinds emitted. 12. PII scrubbed (signature value + cert chain SHA256). 13. RLS denies cross-tenant. 14. Trace_id preserved. 15. TASK-DOC-005 integration works. 16. Append-only sigs table via REVOKE except status cols. 17. Revoked cert detection → status=revoked. 18. Partner failover (if A down, manual switch to B). 19. Composes with TASK-DOC-011 for LTV re-stamping. 20. Sandbox + prod environments per partner.

§5 — Verification

#[tokio::test]
async fn globalsign_signature_round_trip() {
    let ctx = TestContext::with_globalsign_creds_sandbox().await;
    let r = ctx.qtsp_sign(ctx.doc_id, ctx.signer_id).await;
    assert_eq!(r.partner, "globalsign");
    assert!(!r.signature_value.is_empty());
    let validated = ctx.validate_chain(&r.cert_chain_pem).await;
    assert!(validated.eu_trust_list_root);
}

#[tokio::test]
async fn cert_chain_revocation_caught() {
    let ctx = TestContext::with_revoked_cert().await;
    let r = ctx.qtsp_sign(ctx.doc_id, ctx.signer_id).await;
    assert!(r.is_err() || r.unwrap().status == "revoked");
}

#[tokio::test]
async fn padesblt_format_returned() {
    let ctx = TestContext::with_qtsp_creds().await;
    let r = ctx.qtsp_sign(ctx.doc_id, ctx.signer_id).await;
    let blob = ctx.fetch_s3(&r.padesblt_blob_s3_key).await;
    assert!(blob.starts_with(b"%PDF"));  // PAdES is PDF
    assert!(ctx.parse_pades_lt(blob).has_validation_data());
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-DOC-001. Cross-module: TASK-DOC-005 (caller), TASK-DOC-006 (verification gate), TASK-DOC-011 (LTV re-stamping), TASK-AUTH-105 (KMS), TASK-AUTH-101 (CISO role), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Partner API downretry 3xsev-1; status=failedswitch partner or retry
Cert chain invalidvalidatorstatus=failed; sev-1CSR re-issue
OCSP unreachablefallback to CRLsev-2; proceedinherent
TS authority downuse alternatesev-2inherent
Revoked signer certvalidatorstatus=revokednew cert
Creds expired401sev-1; CISO notifiedrotate
Sandbox vs prod confusionenv flaghard reject if mismatchinherent
Network partition mid-signretry idempotentlyinherentinherent
Cross-tenant cert useRLS403inherent
EU Trust List update lagrefresh cachesev-3maintenance

§11 — Implementation notes


End of TASK-DOC-002 spec.