ICP-Brasil digital signature — Koder Signer contract

Normative contract for the Koder Signer service (`services/crypto/signer/`) covering ICP-Brasil digital signature: supported formats (PAdES, CAdES, XAdES), signature policies (AD-RB, AD-RT, AD-RV), hardware token integration (A3 via PKCS#11), file certificate (A1 PFX) loading, certificate chain validation, timestamp authority (TSA) interaction, and revocation checking (CRL + OCSP). Applies to every Koder component that needs digital signature with legal validity in Brazil (per MP 2.200-2/2001 art. 10 §1º). Other Koder components consume Signer via REST/gRPC, never reimplementing PKI primitives locally (per `policies/reuse-first.kmd`).

Spec — ICP-Brasil digital signature (Koder Signer contract)

Version: 0.1.0 — Draft Status: Proposed (20260513)

*osition in multijurisdiction architecture (202605-20).*Per rfcs/signing-RFC-001-multi-jurisdiction.kmd (draft), Koder Signer is designed as a single service issuing signatures under three jurisdictions (BR, EU, US). This spec is the *R profile*of that design — it stays normative for everything ICP-Brasil related; sibling profiles (eidas.kmd, esign.kmd) open when their waves begin. Consumers select the jurisdiction per request via ?jurisdiction=br|eu|us; this spec applies when br is selected.

*cope.*This spec defines the contract Koder Signer (services/crypto/signer/) exposes for digital signature with legal validity in Brazil. It governs both the *nternal implementation*of Signer and the *onsumer contract*that every other Koder component (Koder Sign, Flow, custom integrations) follows when requesting a signature. The provider side (key generation, HSM integration, root CA store) is covered separately when those sub-components mature.

*egal anchor.*MP 2.200-2/2001 (still in force) distinguishes two types of electronic signature in Brazil:

  • *rt. 10 §1º*— Signatures via ICP-Brasil PKI carry legal

    validity equivalent to handwritten signature by presumption.

  • *rt. 10 §2º*— Other forms (drawn, typed, OTP-based)

    are valid when both parties agree.

Koder Sign (products/horizontal/sign/) currently implements only §2º (drawn/typed + email OTP). This spec covers what is needed for §1º — strict ICP-Brasil compliance.


R1 — Supported signature formats

Signer *UST*support these output formats:

Format Carrier Use case
*AdES*(PDF Advanced Electronic Signature) PDF document Contracts, certificates, official documents
*AdES*(CMS Advanced Electronic Signature) .p7s file (detached) or embedded Generic binary documents, XML, archives
*AdES*(XML Advanced Electronic Signature) XML element NFe, eSocial, structured government docs

ETSI TS 103 171 / TS 103 173 / TS 103 172 normative reference for PAdESCAdESXAdES respectively. ICPBrasil profile DOCICP-15 adds Brazilian-specific OIDs and policy URLs.

PDF Signature Visual representation: required when signature should appear graphically in the document; SHOULD respect specs/koder-app/ visual conventions when rendered by Koder Sign.


R2 — Signature policies

Signer *UST*support these ICP-Brasil signature policies (in order of cryptographic strength):

Policy Name Includes
*D-RB* Assinatura Digital com Referência Básica Signer cert + chain
*D-RT* Assinatura Digital com Referência de Tempo AD-RB + qualified timestamp (RFC 3161 TSA)
*D-RV* Assinatura Digital com Referência para Validação AD-RT + complete CRL/OCSP responses for chain

The policy is encoded as an OID + URL in the signed attributes per DOCICP15.

*efault* ADRT (timestamp gives nonrepudiation across cert expiry). Caller MAY request ADRB (lightweight) or ADRV (long-term archival).


R3 — Key material sources

Signer *UST*accept two key sources:

R3.1 — A1 certificate (file-based)

PKCS#12 (.pfx/.p12) file containing the private key encrypted with a passphrase. Loaded via the API:

POST /v1/sign/pades
Content-Type: multipart/form-data
  document: <PDF>
  cert: <PFX>
  passphrase: <string>
  policy: AD-RT

The passphrase *UST NOT*be persisted server-side after the request completes. The PFX *UST*be wiped from memory using crypto/subtlestyle constanttime zeroing.

R3.2 — A3 hardware token (PKCS#11)

Smartcard or USB token (SafeNet eToken, Watchdata Proxkey, Gemalto IDPrime, Morpho, etc.) accessed via PKCS#11 driver. Two deployment modes:

  • *erver-side token* Signer host has the token attached + driver

    installed. API receives PIN, calls C_Sign via the driver.

  • *lient-side token* end user has the token at their machine.

    Signer generates the hashtosign, returns it; client signs with the token; signed hash is returned to Signer for finalization. REQUIRES koder_kit PKCS#11 binding (or koder_web_kit WebAuthn bridge for browser flow).

The PIN *UST NOT*be persisted. Failed PIN attempts *UST*be rate-limited (token has hardware lockout typically at 3 attempts; Signer should also enforce its own backoff to avoid lockout).


R4 — Certificate chain validation

Signer *UST*validate every certificate against the ICP-Brasil chain rooted at the official AC-Raiz (current generation as of 2026: ACRaiz v5; older v2v3v4 trusted for legacy verifyonly).

The full chain set is published by ITI; Signer *UST*ship with a bundled icp-brasil-chains.pem and refresh it on a schedule (default: daily check + reload). Bundle versioning *UST*be recorded in audit logs (which chain version validated which signature).

Required checks per cert in chain:

  • notBefore / notAfter valid for signing time
  • KeyUsage contains digitalSignature (signer cert) or keyCertSign

    (CAs)

  • ExtendedKeyUsage compatible with intended action
  • BasicConstraints CA-flag correct for chain position
  • Subject DN includes OU=ICP-Brasil for ICP certs
  • Cert is not revoked (see R5)

R5 — Revocation checking

Signer *UST*check certificate revocation status before producing a signature, in this order:

  1. *CSP*— if cert carries an AuthorityInformationAccess OCSP

    responder URL, query it. Response cached per ICP-Brasil policy (max 7 days, but typically 1-24h).

  2. *RL*— if OCSP unavailable, fall back to CRL listed in

    CRLDistributionPoints. CRL freshness: max 24h.

Failure modes:

  • Revocation status = revoked → reject; emit KSIGNER-SIGN-3001
  • OCSP+CRL unreachable → softfail OR hardfail based on policy

    (default: softfail for ADRB, hardfail for ADRT/AD-RV)

  • Cert revoked before signing time but signing requested

    retroactively → reject

For *D-RV* the CRL/OCSP responses *UST*be embedded in the signature container for offline verification later.


R6 — Timestamp authority (TSA)

For ADRT and ADRV, Signer *UST*obtain a qualified timestamp from an ICPBrasil accredited TSA. Default: ICPBrasil public TSA at ITI. Custom TSA configurable per deployment.

TSA protocol: RFC 3161 over HTTPS. Signer *UST*verify the TSA response signature against the TSA cert (also validated per R4-R5).

Time-stamp policy OIDs are encoded in the signature container.


R7 — Error map

Userfacing errors follow `specserrorsuserfacing-messages.kmd with the KSIGNER` product prefix:

Code Category Meaning
KSIGNER-CERT-1001 cert Invalid PFX / passphrase
KSIGNER-CERT-1002 cert Cert expired
KSIGNER-CERT-1003 cert Cert not in ICP-Brasil chain
KSIGNER-CERT-1004 cert Cert KeyUsage incompatible
KSIGNER-TOKEN-2001 token PKCS#11 driver not found
KSIGNER-TOKEN-2002 token Token not present
KSIGNER-TOKEN-2003 token PIN incorrect
KSIGNER-TOKEN-2004 token Token locked out (hardware)
KSIGNER-REV-3001 revocation Cert revoked
KSIGNER-REV-3002 revocation OCSP+CRL both unreachable (hard-fail)
KSIGNER-TSA-4001 timestamp TSA unreachable
KSIGNER-TSA-4002 timestamp TSA response invalid
KSIGNER-FMT-5001 format Input document corrupted
KSIGNER-FMT-5002 format Signature placement conflict (PDF)
KSIGNER-JURIS-6000 jurisdiction Unsupported jurisdiction name (400) — added in signer#013 wave C
KSIGNER-JURIS-6001 jurisdiction Jurisdiction not implemented (501) — wave D promoted EU/US to implemented; reserved for future profiles
KSIGNER-JURIS-6099 jurisdiction Internal resolver error (500, defensive)
KSIGNER-EIDAS-1000 level ?level= slug not in profile's RequiredLevels() (400) — added in signer#014 wave D stage 1; also fires for br?level=anything
KSIGNER-EIDAS-1001 level Level valid but not yet implemented at format layer (501) — current state for eu/us all levels until stages 2-4

All error codes localized enUS + ptBR per policies/language.kmd.


R8 — Multi-tenancy

Signer *UST*comply with policies/multi-tenant-by-default.kmd:

  • Every signature request carries koder_user_id (and optional

    workspace_id).

  • Audit logs include tenant scope (who signed, on whose behalf, for

    which workspace).

  • Cross-tenant access returns 404, not 403.

Tenant isolation does *ot*apply to ICP-Brasil chain bundles (global, read-only) or TSA cache (global but keyed by hash, no PII).


T1-T6 — Test contract

Every Signer implementation *UST*pass:

  • *1 — Valid A1 sign* PFX + correct passphrase → valid PAdES with

    policy AD-RT; ITI Verificador validates green.

  • *2 — Valid A3 sign (server-side)* mocked PKCS#11 with valid

    cert + PIN → valid CAdES; verifies against bundled chain.

  • *3 — Reject expired cert* PFX with notAfter < now → error

    KSIGNER-CERT-1002; no partial output written.

  • *4 — Reject revoked cert* cert in test CRL → error

    KSIGNER-REV-3001.

  • *5 — Cross-validate with reference signer* same input signed by

    signer-cli SERPRO and by Koder Signer produces semantically equivalent containers (byte-identical not required; both verify green via openssl cms -verify -policy … and ITI Verificador).

  • *6 — Roundtrip* sign → embed in PDF → reopen → extract

    signature → verify signature is intact, policy is preserved, TSA proof present (for AD-RT).

Negative-path tests:

  • *1 — Tampered document* modify PDF byte after sign → verify

    detects tamper.

  • *2 — Wrong passphrase* error KSIGNER-CERT-1001; rate-limit

    applies after 3 attempts in 60s window.

  • *3 — TSA unreachable* ADRT requested but TSA down → hardfail

    with KSIGNER-TSA-4001; no partial signature output.


Out of scope (v0.1.0)

  • Signature visual templates (graphic representation in PDF).

    Tracked separately when Signer reaches consumer UI integration.

  • ICPBrasil crossborder interoperability (eIDAS bridge).
  • Longterm archival format LTV / PAdESLTA (planned for v0.3).
  • SignerasHSM (Signer hosting keys directly, not just orchestrating

    signature against external token/file). Out of scope until KMS Sector (services/crypto/kms/) ships.


Roadmap

Phase Deliverable Tickets
0.1.0 This spec; CLI prototype ksigner sign --a1 cert.pfx --policy AD-RB doc.pdf servicescryptosigner#001-003
0.2.0 A3 serverside via PKCS#11; ADRT timestamp; CRL/OCSP #004-006
0.3.0 A3 client-side bridge (koder_kit PKCS#11 binding) #007-008
0.4.0 Koder Sign integration (refactor internal/crypto/ to consume Signer) sign#XXX
0.5.0 XAdES + NFe profile; PAdES-LTA archival #009+

Open questions

  • *mplementation language* Go (consistent with foundation/) vs.

    reuse JVM lib dss-eu via JNI bridge (mature ICP-Brasil support but adds JVM dependency). Decision deferred to ticket #003.

  • *SA failover* ICP-Brasil has only ~3 public TSAs. Should Signer

    ship with a configurable fallback list, or fail-open if primary is down? (Affects R6.)

  • *ert chain bundle distribution* ship inside the Signer binary

    (small, no fetch) vs. fetch on startup from ITI (always fresh, but bootstrap dependency)? Default proposal: ship + daily refresh.


Audit hooks

(Reserved for koder-spec-audit signing once a T1-T6 implementation template exists. Workflow path: .gitea/workflows/audit-signing.yml.)

Source: ../home/koder/dev/koder/meta/docs/stack/specs/signing/icp-brasil.kmd