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

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:

Format Carrier Use case
PAdES (PDF Advanced Electronic Signature) PDF document Contracts, certificates, official documents
CAdES (CMS Advanced Electronic Signature) .p7s file (detached) or embedded Generic binary documents, XML, archives
XAdES (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. 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):

Policy Name Includes
AD-RB Assinatura Digital com Referência Básica Signer cert + chain
AD-RT Assinatura Digital com Referência de Tempo AD-RB + qualified timestamp (RFC 3161 TSA)
AD-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 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 v2v3v4 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:

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 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

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 server-side via PKCS#11; AD-RT 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

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