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 multi
jurisdiction architecture (202605-20).*Perrfcs/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 whenbris 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-RTThe 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_Signvia the driver. - *lient-side token* end user has the token at their machine.
Signer generates the hash
tosign, returns it; client signs with the token; signed hash is returned to Signer for finalization. REQUIRESkoder_kitPKCS#11 binding (orkoder_web_kitWebAuthn 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/notAftervalid for signing time- KeyUsage contains
digitalSignature(signer cert) orkeyCertSign(CAs)
- ExtendedKeyUsage compatible with intended action
- BasicConstraints CA-flag correct for chain position
- Subject DN includes
OU=ICP-Brasilfor ICP certs - Cert is not revoked (see R5)
R5 — Revocation checking
Signer *UST*check certificate revocation status before producing a signature, in this order:
- *CSP*— if cert carries an
AuthorityInformationAccessOCSPresponder URL, query it. Response cached per ICP-Brasil policy (max 7 days, but typically 1-24h).
- *RL*— if OCSP unavailable, fall back to CRL listed in
CRLDistributionPoints. CRL freshness: max 24h.
Failure modes:
- Revocation status =
revoked→ reject; emitKSIGNER-SIGN-3001 - OCSP+CRL unreachable → soft
fail OR hardfail based on policy(default: soft
fail 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 optionalworkspace_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 → errorKSIGNER-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 SERPROand by Koder Signer produces semantically equivalent containers (byte-identical not required; both verify green viaopenssl cms -verify -policy …and ITI Verificador). - *6 — Round
trip* sign → embed in PDF → reopen → extractsignature → 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-limitapplies after 3 attempts in 60s window.
- *3 — TSA unreachable* AD
RT requested but TSA down → hardfailwith
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 crossborder interoperability (eIDAS bridge). - Long
term archival format LTV / PAdESLTA (planned for v0.3). - Signer
asHSM (Signer hosting keys directly, not just orchestratingsignature 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 |
#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-euvia 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.)