eIDAS digital signature — Koder Signer EU profile (stub)
signing specs/signing/eidas.kmd
EU profile (`?jurisdiction=eu`) of the Koder Signer service per `rfcs/signing-RFC-001-multi-jurisdiction.kmd`. Covers eIDAS levels SES / AdES / QES, the EU LOTL trust source, qualified TSA selection, and per-level conformance checks. STUB — placeholder opened in signer#013 (wave C, 2026-05-23) so the spec slot exists in the registry; full normative content lands when wave D begins (see RFC §Phasing).
Quando esta spec se aplica
Triggers primários
- Start wave D of signing-RFC-001
- Open EU LOTL fetcher and verifier in services/crypto/signer
Todos os triggers
- Implement eIDAS digital signature with EU LOTL trust
- Add QSCD (qualified signature creation device) support
- Generate PAdES/CAdES/XAdES at AdES or QES level for EU consumers
- Verify eIDAS signature against LOTL-resolved trust chain
Corpo da especificação
Spec (partial) — eIDAS digital signature (Koder Signer EU profile)
Version: 0.9.0 — Partial (R1, R2, R3, R6, R7 normative; R4, R5, R8 still outlined) Status: Wave D stages 1+2a+2b+2b-2+2c+3 shipped 2026-05-23/24 (signer#014); stage 4 (QES wave E gate) remains.
What's normative as of v0.9.0: per-request
?level=(R1), real LOTL fetcher + Exclusive C14N verifier via vendored goxmldsig + national TL traversal + refresh loop + freshness policy (R2 + R3), qualified- cert check at format layer (R6), and the error map (R7) — all wired and tested end-to-end against the live Commission LOTL.What still ships in later stages: qualified-TSA selection (R4 — wave D stage 4), QSCD attestation (R5 — wave E), multi-tenant policy per-tenant trust customisation (R8 — wave E).
POST /v1/sign/pades?jurisdiction=eu&level={ses|ades}is operational end-to-end.level=qesreturns501 KSIGNER-EIDAS-1001until wave E. cades + xades level dispatch is sibling follow-up tracked as #015.
R1 — Signature levels (normative)
The EU profile accepts three level slugs via ?level= (default ades):
| Slug | Maps to | Required key source | Trust check | TSA check |
|---|---|---|---|---|
ses | Simple Electronic Signature | any cert (PFX/PKCS#11) | none — signature carries cert but no qualifier check | optional |
ades | Advanced Electronic Signature (B-T per ETSI EN 319 142) | any cert | cert must chain to a TSP listed in LOTL with Sie qualifier (R6) | qualified TSA from LOTL (R4) required |
qes | Qualified Electronic Signature | QSCD-resident key (R5) | cert must be qualified per LOTL + key on QSCD device list | qualified TSA required |
Requests are validated through the jurisdictions.Registry.ResolveLevel
seam at the gate; unknown level → KSIGNER-EIDAS-1000; level valid but
not yet implemented at format layer → KSIGNER-EIDAS-1001.
R2 — Trust source (normative — stages 2a + 2b + 2b-2 + 2c shipped)
EU LOTL fetched from https://ec.europa.eu/tools/lotl/eu-lotl.xml,
verified via Exclusive XML C14N XMLDSig against the cert in the
LOTL's KeyInfo (or against operator-pinned Commission certs per
Decision (EU) 2015/1505), parsed per ETSI TS 119 612 to extract 27+
national TL pointers (43 today, including non-EU observers
Norway/Iceland/Liechtenstein), each fetched + verified + parsed +
unioned into the in-memory eu_lotl.Store.
| Step | Status | Shipped in |
|---|---|---|
Fetcher.Fetch — HTTP GET + ETag round-trip + disk cache | ✅ | Stage 2a (2026-05-23) |
Parser.ParseLOTL — TSL header + OtherTSLPointer set with DER certs | ✅ | Stage 2a |
Verifier.Verify — full XMLDSig with Exclusive C14N + Reference digests | ✅ | Stage 2b-2 (2026-05-24) — backed by vendored goxmldsig |
Refresher — Fetcher → Verifier → Parser → Store.Swap with snapshot-retention on failure | ✅ | Stage 2b-2 |
Refresher.traverseNationalTLs — per-MS fetch + verify + parse, best-effort partial-trust publishing | ✅ | Stage 2c (2026-05-24) |
ParseNationalTL — TrustServiceProvider list with qualifiers + cert union | ✅ | Stage 2c |
| Operator-pinned Commission cert bundle (vs current TOFU) | 🔲 | Wave E hardening |
Implementation: services/crypto/signer/backend/internal/trust/bundles/eu_lotl/
ships Fetcher, Verifier, Parser, Store, Refresher. The
verifier uses an in-tree fork of github.com/russellhaering/goxmldsig
v1.6.0 at backend/third_party/goxmldsig/ with a single 7-line patch
that bumps etreeutils.NewDefaultNSContext defaultLimit from 1000 →
1_000_000 to accommodate the real LOTL's ~471 KB document. Diff vs
upstream is intentionally minimal for easy forward-port.
Integration test (KSIGNER_EU_LOTL_INTEGRATION=1):
TestVerify_RealLOTL— verifies the live Commission LOTL signature end-to-end against the cert pinned in its KeyInfo (2212-byte cert, passes goxmldsig + Exclusive C14N).TestRefresher_RunOnce_RealLOTL_TraversesNationalTLs— runs the full pipeline against live infrastructure (LOTL fetch + 43 national TL fetches + per-MS verify + parse). Best-effort tolerates a few MS TLs that may be transiently down or use uncommon signature variants.
R3 — LOTL freshness policy (normative — stage 2b-2b shipped)
Per RFC §Q1 (ratified 2026-05-20). Constants live in
eu_lotl/store.go::ADESStaleness = 24 * time.Hour; status enum is
FreshnessStatus ∈ {Unknown, Fresh, Stale}. Refresher defaults to
4 h cadence and runs the first refresh immediately at boot.
| Level | Behaviour on stale store | Implementation |
|---|---|---|
qes | Refuse, KSIGNER-EIDAS-3001 lotl_stale_for_qes | Wave E (reuses ADESStaleness check at gate) |
ades | Continue up to 24 h since last successful refresh; after 24 h → KSIGNER-EIDAS-3001 lotl_stale_for_ades (503) | pades.SignHandler checks euStore.FreshnessStatus() before R6 |
ses | Continue unconditionally — no LOTL dependency | No check |
A store that has never loaded returns FreshnessUnknown; ades requests
in that state get KSIGNER-EIDAS-3002 trust_not_loaded (503).
R4 — Qualified TSA selection (outlined, lands stage 2)
Per-jurisdiction TSA endpoint list resolved from each national TL's
TstS-qualified services. internal/tsa/ will grow a
SelectByJurisdiction(profile) method; for eu, pulls qualified TSAs
from the union store. Operator may pin a specific TSP via config.
R5 — QSCD attestation (outlined, lands wave E)
For qes, the key source MUST declare qscd=true and resolve to a
device on the EU Common List of certified QSCDs. Runtime check via
cert policy OID. Out of wave D scope per RFC §Phasing.
R6 — Qualified certificate check (normative — stage 3 shipped)
For level=ades, the signing cert MUST chain to a TSP listed in any
loaded national TL. Implementation: eu_lotl.Store.IsQualifiedSigningCert(leaf)
matches against each TSP's QualifiedCerts pool with two equivalent rules:
- Direct match —
leaf.Raw == qc.Raw(TSP-root signing directly). - Issuer DN match —
leaf.RawIssuer == qc.RawSubject(leaf was issued by this qualified cert).
Stage 3 (MVP) treats all qualifier categories (CA/QC, CA/PKC,
TSA/QTST, …) as equivalent — sufficient for the eIDAS ades gate but
does not yet split by qualifier. Wave E (qes) tightens to
QualifiedCertificate qualifiers only + QSCD attestation.
Failure modes (per R7):
- Match miss →
KSIGNER-EIDAS-4001 cert_not_in_eu_trust_store(422) - Store not loaded →
KSIGNER-EIDAS-3002 trust_not_loaded(503) - Store stale > 24 h →
KSIGNER-EIDAS-3001 lotl_stale_for_ades(503)
Success surfaces X-Koder-Signer-EIDAS-TSP + X-Koder-Signer-EIDAS-Country
response headers naming the qualifying TSP / Member State.
Spec-to-code map:
services/crypto/signer/backend/internal/trust/bundles/eu_lotl/qualified.goservices/crypto/signer/backend/internal/pades/handler.go(R6 gate)services/crypto/signer/backend/internal/jurisdictions/eu.go::LevelImplemented
R7 — Error map (normative)
EIDAS sub-category of the JURIS-6xxx range. Each code is also entered
in icp-brasil.kmd R7's master table for cross-spec consistency:
| Code | Category | Meaning | HTTP |
|---|---|---|---|
KSIGNER-EIDAS-1000 | level | ?level= slug not in profile's RequiredLevels() set | 400 |
KSIGNER-EIDAS-1001 | level | Level valid for profile but not yet implemented | 501 |
KSIGNER-EIDAS-2000 | trust | LOTL fetch failed | 502 |
KSIGNER-EIDAS-2001 | trust | LOTL XMLDSig invalid | 502 |
KSIGNER-EIDAS-3001 | freshness | LOTL stale beyond level's window | 503 |
KSIGNER-EIDAS-3002 | freshness | LOTL trust store not yet loaded (bootstrap) | 503 |
KSIGNER-EIDAS-4001 | trust | Signing cert not in EU trust store (R6 ades gate) | 422 |
KSIGNER-EIDAS-4002 | trust | TSA not qualified in LOTL (wave D stage 4) | 502 |
KSIGNER-EIDAS-5001 | qsccd | QSCD attestation missing or invalid (wave E) | 400 |
R8 — Multi-tenancy (outlined, inherits)
Inherits policies/multi-tenant-by-default.kmd. Per-tenant qualified-
cert + QSCD reference registry lands with wave E (per-tenant signing
identity already in scope via the broader signer multi-tenancy work).
Out (separate specs)
- EUDI Wallet integration (eIDAS 2.0 ARF) — too large for this spec; separate RFC if/when implementation surface warrants
- Listing Koder Signer itself as a qualified TSP — out of scope per RFC Q3 (ratified)
- BR ↔ EU mutual recognition — out of scope per RFC Q4 (ratified deferred)
Referências
meta/docs/stack/rfcs/signing-RFC-001-multi-jurisdiction.kmdmeta/docs/stack/specs/signing/icp-brasil.kmdmeta/docs/stack/specs/errors/user-facing-messages.kmdmeta/docs/stack/policies/self-hosted-first.kmdmeta/docs/stack/policies/reuse-first.kmdhttps://ec.europa.eu/tools/lotl/eu-lotl.xmlhttps://eur-lex.europa.eu/eli/reg/2014/910/ojhttps://eur-lex.europa.eu/eli/reg/2024/1183/oj