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=qes returns 501 KSIGNER-EIDAS-1001 until 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 (20260523)
Parser.ParseLOTL — TSL header + OtherTSLPointer set with DER certs Stage 2a
Verifier.Verify — full XMLDSig with Exclusive C14N + Reference digests Stage 2b2 (202605-24) — backed by vendored goxmldsig
Refresher — Fetcher → Verifier → Parser → Store.Swap with snapshot-retention on failure Stage 2b-2
Refresher.traverseNationalTLs — perMS fetch + verify + parse, besteffort partial-trust publishing Stage 2c (20260524)
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 signature

    endtoend 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 + perMS 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:

  1. *irect match*— leaf.Raw == qc.Raw (TSP-root signing directly).
  2. *ssuer 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.

Spectocode map:

  • services/crypto/signer/backend/internal/trust/bundles/eu_lotl/qualified.go
  • services/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)

Source: ../home/koder/dev/koder/meta/docs/stack/specs/signing/eidas.kmd