"DOC eIDAS QTSP integration — GlobalSign or Cryptomathic partner for EU residency qualified signatures"
§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.
- MUST support 2 partners per DEC-1770 — GlobalSign + Cryptomathic; abstraction at
abstraction.rs::dispatcher(partner).
- MUST validate
qtsp_partneragainst closed enum per DEC-1771.
- MUST validate
qtsp_request_kindagainst closed enum per DEC-1772.
- MUST dispatch per partner:
globalsign_client.rs::request_signature(pdf_hash, signer_cert_id, ts_authority)— GlobalSign DSS RESTcryptomathic_client.rs::sign(pdf, signer_cert_id)— Cryptomathic Signer API
- MUST validate returned cert chain at
cert_chain_validator.rs::validate(signature):
- All certs in chain non-expired at signature time.
- Issuer chain to EU Trust List root.
- OCSP/CRL revocation check.
- Timestamp authority signed.
- MUST return PAdES-B-LT signature per DEC-1774 — embedded validation data (cert chain + OCSP/CRL + timestamp) per ETSI EN 319 142-1.
- MUST store QTSP creds in KMS per DEC-1773 —
tenant_qtsp_creds.encrypted_creds_arn; CISO-only writes via TASK-AUTH-101.
- 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; ```
- 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)``
- MUST emit 4 memory audit kinds per DEC-1775. PII per TASK-MEMORY-111: signature_value + cert_chain hashed; ids ok.
- MUST thread trace_id from TASK-DOC-005 request → partner call → cert validation → audit.
- MUST NOT allow non-CISO creds write per DEC-1773.
- 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
- 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
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Partner API down | retry 3x | sev-1; status=failed | switch partner or retry |
| Cert chain invalid | validator | status=failed; sev-1 | CSR re-issue |
| OCSP unreachable | fallback to CRL | sev-2; proceed | inherent |
| TS authority down | use alternate | sev-2 | inherent |
| Revoked signer cert | validator | status=revoked | new cert |
| Creds expired | 401 | sev-1; CISO notified | rotate |
| Sandbox vs prod confusion | env flag | hard reject if mismatch | inherent |
| Network partition mid-sign | retry idempotently | inherent | inherent |
| Cross-tenant cert use | RLS | 403 | inherent |
| EU Trust List update lag | refresh cache | sev-3 | maintenance |
§11 — Implementation notes
- §11.1 GlobalSign DSS: REST endpoint per partner docs; signs PDF hash, returns CMS.
- §11.2 Cryptomathic: similar DSS shape; both return PAdES-compatible CMS structure.
- §11.3 Cert chain validator uses
x509-parser+oid-registry; root list from EU Trust List XML. - §11.4 memory audit body: doc_id, signer_id, partner, status; signature_value + cert_chain SHA256.
- §11.5 Future partners: add new enum value + new client file; abstraction is the seam.
End of TASK-DOC-002 spec.