Skip to content

ICP-Brasil digital signature — Koder Signer contract

signing specs/signing/icp-brasil.kmd

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`).

When this spec applies

Primary triggers

All triggers

Specification body

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

Version: 0.1.0 — Draft Status: Proposed (2026-05-13)

Position in multi-jurisdiction architecture (2026-05-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 BR 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.

Scope. This spec defines the contract Koder Signer (services/crypto/signer/) exposes for digital signature with legal validity in Brazil. It governs both the internal implementation of Signer and the consumer 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.

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

  • Art. 10 §1º — Signatures via ICP-Brasil PKI carry legal validity equivalent to handwritten signature by presumption.
  • Art. 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 MUST support these output formats:

FormatCarrierUse case
PAdES (PDF Advanced Electronic Signature)PDF documentContracts, certificates, official documents
CAdES (CMS Advanced Electronic Signature).p7s file (detached) or embeddedGeneric binary documents, XML, archives
XAdES (XML Advanced Electronic Signature)XML elementNFe, eSocial, structured government docs

ETSI TS 103 171 / TS 103 173 / TS 103 172 normative reference for PAdES/CAdES/XAdES respectively. ICP-Brasil profile DOC-ICP-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 MUST support these ICP-Brasil signature policies (in order of cryptographic strength):

PolicyNameIncludes
AD-RBAssinatura Digital com Referência BásicaSigner cert + chain
AD-RTAssinatura Digital com Referência de TempoAD-RB + qualified timestamp (RFC 3161 TSA)
AD-RVAssinatura Digital com Referência para ValidaçãoAD-RT + complete CRL/OCSP responses for chain

The policy is encoded as an OID + URL in the signed attributes per DOC-ICP-15.

Default: AD-RT (timestamp gives non-repudiation across cert expiry). Caller MAY request AD-RB (lightweight) or AD-RV (long-term archival).


R3 — Key material sources

Signer MUST 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 MUST NOT be persisted server-side after the request completes. The PFX MUST be wiped from memory using crypto/subtle-style constant-time 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:

  • Server-side token: Signer host has the token attached + driver installed. API receives PIN, calls C_Sign via the driver.
  • Client-side token: end user has the token at their machine. Signer generates the hash-to-sign, 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 MUST NOT be persisted. Failed PIN attempts MUST 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 MUST validate every certificate against the ICP-Brasil chain rooted at the official AC-Raiz (current generation as of 2026: AC-Raiz v5; older v2/v3/v4 trusted for legacy verify-only).

The full chain set is published by ITI; Signer MUST ship with a bundled icp-brasil-chains.pem and refresh it on a schedule (default: daily check + reload). Bundle versioning MUST 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 MUST check certificate revocation status before producing a signature, in this order:

  1. OCSP — if cert carries an AuthorityInformationAccess OCSP responder URL, query it. Response cached per ICP-Brasil policy (max 7 days, but typically 1-24h).
  2. CRL — 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 → soft-fail OR hard-fail based on policy (default: soft-fail for AD-RB, hard-fail for AD-RT/AD-RV)
  • Cert revoked before signing time but signing requested retroactively → reject

For AD-RV, the CRL/OCSP responses MUST be embedded in the signature container for offline verification later.


R6 — Timestamp authority (TSA)

For AD-RT and AD-RV, Signer MUST obtain a qualified timestamp from an ICP-Brasil accredited TSA. Default: ICP-Brasil public TSA at ITI. Custom TSA configurable per deployment.

TSA protocol: RFC 3161 over HTTPS. Signer MUST 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

User-facing errors follow specs/errors/user-facing-messages.kmd with the KSIGNER product prefix:

CodeCategoryMeaning
KSIGNER-CERT-1001certInvalid PFX / passphrase
KSIGNER-CERT-1002certCert expired
KSIGNER-CERT-1003certCert not in ICP-Brasil chain
KSIGNER-CERT-1004certCert KeyUsage incompatible
KSIGNER-TOKEN-2001tokenPKCS#11 driver not found
KSIGNER-TOKEN-2002tokenToken not present
KSIGNER-TOKEN-2003tokenPIN incorrect
KSIGNER-TOKEN-2004tokenToken locked out (hardware)
KSIGNER-REV-3001revocationCert revoked
KSIGNER-REV-3002revocationOCSP+CRL both unreachable (hard-fail)
KSIGNER-TSA-4001timestampTSA unreachable
KSIGNER-TSA-4002timestampTSA response invalid
KSIGNER-FMT-5001formatInput document corrupted
KSIGNER-FMT-5002formatSignature placement conflict (PDF)
KSIGNER-JURIS-6000jurisdictionUnsupported jurisdiction name (400) — added in signer#013 wave C
KSIGNER-JURIS-6001jurisdictionJurisdiction not implemented (501) — wave D promoted EU/US to implemented; reserved for future profiles
KSIGNER-JURIS-6099jurisdictionInternal resolver error (500, defensive)
KSIGNER-EIDAS-1000level?level= slug not in profile's RequiredLevels() (400) — added in signer#014 wave D stage 1; also fires for br?level=anything
KSIGNER-EIDAS-1001levelLevel valid but not yet implemented at format layer (501) — current state for eu/us all levels until stages 2-4

All error codes localized en-US + pt-BR per policies/language.kmd.


R8 — Multi-tenancy

Signer MUST 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 not 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 MUST pass:

  • T1 — Valid A1 sign: PFX + correct passphrase → valid PAdES with policy AD-RT; ITI Verificador validates green.
  • T2 — Valid A3 sign (server-side): mocked PKCS#11 with valid cert + PIN → valid CAdES; verifies against bundled chain.
  • T3 — Reject expired cert: PFX with notAfter < now → error KSIGNER-CERT-1002; no partial output written.
  • T4 — Reject revoked cert: cert in test CRL → error KSIGNER-REV-3001.
  • T5 — 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).
  • T6 — Round-trip: sign → embed in PDF → re-open → extract signature → verify signature is intact, policy is preserved, TSA proof present (for AD-RT).

Negative-path tests:

  • N1 — Tampered document: modify PDF byte after sign → verify detects tamper.
  • N2 — Wrong passphrase: error KSIGNER-CERT-1001; rate-limit applies after 3 attempts in 60s window.
  • N3 — TSA unreachable: AD-RT requested but TSA down → hard-fail 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.
  • ICP-Brasil cross-border interoperability (eIDAS bridge).
  • Long-term archival format LTV / PAdES-LTA (planned for v0.3).
  • Signer-as-HSM (Signer hosting keys directly, not just orchestrating signature against external token/file). Out of scope until KMS Sector (services/crypto/kms/) ships.

Roadmap

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

Open questions

  • Implementation 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.
  • TSA 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.)
  • Cert 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.)

References