eIDAS digital signature — Koder Signer EU profile (stub)
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).
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+2b2+2c+3 shipped 202605-23/24 (signer#014); stage 4 (QES wave E gate) remains.
*hat'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 endtoend against the live Commission LOTL.*hat 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 *perational*endtoend.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 NorwayIcelandLiechtenstein), 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 |
Parser.ParseLOTL — TSL header + OtherTSLPointer set with DER certs |
✅ | Stage 2a |
Verifier.Verify — full XMLDSig with Exclusive C14N + Reference digests |
✅ | Stage 2bgoxmldsig |
Refresher — Fetcher → Verifier → Parser → Store.Swap with snapshot-retention on failure |
✅ | Stage 2b-2 |
Refresher.traverseNationalTLs — per |
✅ | Stage 2c (2026 |
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 → 1000000 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 signatureend
toend against the cert pinned in its KeyInfo (2212-byte cert, passes goxmldsig + Exclusive C14N).TestRefresher_RunOnce_RealLOTL_TraversesNationalTLs— runs the fullpipeline against live infrastructure (LOTL fetch + 43 national TL fetches + per
MS verify + parse). Besteffort 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 20260520). 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:
- *irect match*—
leaf.Raw == qc.Raw(TSP-root signing directly). - *ssuer DN match*—
leaf.RawIssuer == qc.RawSubject(leaf wasissued 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.
Spectocode 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 subcategory of the JURIS6xxx 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. Pertenant 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)