diff --git a/docs/compliance-testing.md b/docs/compliance-testing.md new file mode 100644 index 0000000..999e43b --- /dev/null +++ b/docs/compliance-testing.md @@ -0,0 +1,65 @@ +# Guia de Testes de Conformidade ICP-Brasil e Validação Oficial no ITI + +> **Procedimentos de Verificação Criptográfica Automatizada e Manual para Assinaturas PAdES e XAdES-BES** + +--- + +## 1. Visão Geral + +O **CyberCore Signer** implementa assinaturas digitais em conformidade com as normas técnicas da **Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil)**, especificamente: + +- **PDF (PAdES):** Padrão ISO 32000-1 e ETSI TS 102 778, com envelope CMS desacoplado (`/adbe.pkcs7.detached`) e algoritmo `SHA256withRSA` (alinhado ao perfil AD-RB). +- **XML (XAdES-BES / XMLDSig):** Padrão ETSI TS 101 903 e norma **DOC-ICP-15.03**, contendo propriedades qualificadas assinadas (``, `` e ``). + +--- + +## 2. Suíte de Testes Automatizados (`ComplianceSignatureSuiteTest`) + +A conformidade técnica do código é garantida de forma contínua pelo teste automatizado: +`modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/compliance/ComplianceSignatureSuiteTest.java` + +### O que o teste automatizado valida: +1. **Cadeia ICP-Brasil Simulada em Memória:** + - Cria uma hierarquia criptográfica completa (AC Raiz de Teste -> AC Intermediária -> Certificado Signatário com extensão OID e-CPF `2.16.76.1.3.1`). + - Garante que nenhum certificado real ou dado pessoal seja exposto em testes. +2. **PAdES (PDF):** + - Criação de documento PDF original. + - Aplicação de selo visual conforme as coordenadas calculadas. + - Verificação da integridade do `/ByteRange` cobrindo o PDF integral exceto os bytes de `/Contents`. + - Validação criptográfica independente via Bouncy Castle `CMSSignedData.verify()`. + - Comparação do digest SHA-256 do documento contra o atributo assinado `messageDigest`. +3. **XAdES-BES (XML):** + - Assinatura de documento XML com geração de identificadores de referência (`Reference URI`). + - Validação independente via motor nativo Java JSR-105 (`javax.xml.crypto.dsig.XMLSignature`). + - Teste de inviolabilidade (Tamper Test): verificação de que qualquer modificação de um caractere nos dados de negócio invalida sumariamente o `coreValidity`. + +--- + +## 3. Validação Manual no Verificador de Conformidade do ITI + +O **Instituto Nacional de Tecnologia da Informação (ITI)** disponibiliza a ferramenta governamental oficial para validação de assinaturas digitais brasileiras: + +👉 **URL Oficial:** [https://verificador.iti.gov.br](https://verificador.iti.gov.br) + +### 3.1 Procedimento Passo a Passo para Teste Manual + +1. **Gerar ou Exportar o Documento Assinado:** + - Através da interface gráfica do CyberCore Signer, assine um documento PDF ou XML utilizando seu certificado digital A1 ou A3. + - O arquivo assinado resultante será gravado em disco (ex: `contrato_assinado.pdf` ou `documento_assinado.xml`). + +2. **Submissão no Verificador do ITI:** + - Acesse [https://verificador.iti.gov.br](https://verificador.iti.gov.br) em qualquer navegador web moderno. + - Clique em **"Selecionar assinatura"** e faça o upload do arquivo gerado pelo CyberCore Signer. + - Clique no botão **"Verificar conformidade"**. + +3. **Interpretação do Relatório de Conformidade:** + +#### Cenário A: Assinatura com Certificado de Produção ICP-Brasil (Certisign, Serasa, Serpro, Valid, etc.) +- **Status Geral:** `APROVADO` / `CONFORME`. +- **Validação Criptográfica:** Íntegra (o hash bate e a assinatura RSA é autêntica). +- **Cadeia de Certificação:** Confiável (ancorada até a AC Raiz da ICP-Brasil oficial). +- **Atributos do Signatário:** Nome completo e CPF/CNPJ extraídos e validados na Receita Federal. + +#### Cenário B: Assinatura com Certificados de Teste / Desenvolvimento (Gerados na Suíte) +- **Status Criptográfico:** `VÁLIDO` (integridade matemática e estrutura PAdES/XAdES aprovadas). +- **Alerta de Cadeia:** O verificador do ITI reportará: *"Cadeia de Certificação Não Confiável / AC não credenciada na ICP-Brasil"*. Este é o comportamento **correto e esperado**, uma vez que certificados gerados em testes locais não pertencem à lista de certificados confiáveis (LCR oficial) publicada pelo ITI. diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/compliance/ComplianceSignatureSuiteTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/compliance/ComplianceSignatureSuiteTest.java new file mode 100644 index 0000000..f4b5d4a --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/compliance/ComplianceSignatureSuiteTest.java @@ -0,0 +1,325 @@ +package br.com.cybercore.signer.core.compliance; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.pdf.PdfSealPosition; +import br.com.cybercore.signer.core.pdf.PdfSignatureOptions; +import br.com.cybercore.signer.core.pdf.PdfSigningService; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +import br.com.cybercore.signer.core.validation.SignatureValidationService; +import br.com.cybercore.signer.core.validation.ValidationResult; +import br.com.cybercore.signer.core.validation.ValidationStatus; +import br.com.cybercore.signer.core.xml.XmlSignatureOptions; +import br.com.cybercore.signer.core.xml.XmlSignatureProfile; +import br.com.cybercore.signer.core.xml.XmlSigningService; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Suíte de testes automatizados de conformidade criptográfica para assinaturas + * PAdES (PDF) e XAdES-BES / XMLDSig (XML), utilizando cadeia ICP-Brasil de teste gerada em memória. + */ +@DisplayName("Suíte de Testes de Conformidade ICP-Brasil (PAdES e XAdES-BES)") +class ComplianceSignatureSuiteTest { + + private static CertificateManager certificateManager; + private static PdfSigningService pdfSigningService; + private static XmlSigningService xmlSigningService; + private static SignatureValidationService validationService; + + private static String testEcpfCertId; + private static X509Certificate rootCaCert; + private static X509Certificate intermediateCaCert; + private static X509Certificate signerCert; + + @BeforeAll + static void initTestingCaAndServices() throws Exception { + certificateManager = new CertificateManager(); + validationService = new SignatureValidationService(); + + // 1. Gera par de chaves da AC Raiz ICP-Brasil de Teste + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair rootKeyPair = kpg.generateKeyPair(); + + long now = System.currentTimeMillis(); + Date notBefore = new Date(now - 100_000); + Date notAfter = new Date(now + 365L * 24 * 3600 * 1000); + + X500Name rootSubject = new X500Name("CN=Autoridade Certificadora Raiz ICP-Brasil Teste v1, OU=Testes Criptograficos, O=ICP-Brasil, C=BR"); + X509v3CertificateBuilder rootBuilder = new JcaX509v3CertificateBuilder( + rootSubject, + BigInteger.valueOf(1), + notBefore, + notAfter, + rootSubject, + rootKeyPair.getPublic() + ); + rootBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + rootBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); + + ContentSigner rootSigner = new JcaContentSignerBuilder("SHA256withRSA").build(rootKeyPair.getPrivate()); + rootCaCert = new JcaX509CertificateConverter().getCertificate(rootBuilder.build(rootSigner)); + + // 2. Gera AC Intermediária de Teste assinada pela Raiz + KeyPair intermediateKeyPair = kpg.generateKeyPair(); + X500Name intermediateSubject = new X500Name("CN=AC Autoridade Certificadora Intermediaria Teste, OU=Testes ICP-Brasil, O=ICP-Brasil, C=BR"); + X509v3CertificateBuilder interBuilder = new JcaX509v3CertificateBuilder( + rootSubject, + BigInteger.valueOf(2), + notBefore, + notAfter, + intermediateSubject, + intermediateKeyPair.getPublic() + ); + interBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + interBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); + + ContentSigner interSigner = new JcaContentSignerBuilder("SHA256withRSA").build(rootKeyPair.getPrivate()); + intermediateCaCert = new JcaX509CertificateConverter().getCertificate(interBuilder.build(interSigner)); + + // 3. Gera Certificado de Usuário Final (e-CPF com OID ICP-Brasil 2.16.76.1.3.1) + KeyPair userKeyPair = kpg.generateKeyPair(); + X500Name userSubject = new X500Name("CN=CARLOS EDUARDO DA SILVA:12345678900, OU=e-CPF Brasil, O=ICP-Brasil, C=BR"); + X509v3CertificateBuilder userBuilder = new JcaX509v3CertificateBuilder( + intermediateSubject, + BigInteger.valueOf(3), + notBefore, + notAfter, + userSubject, + userKeyPair.getPublic() + ); + userBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); + userBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)); + + // Adiciona extensão SubjectAlternativeName com OtherName contendo OID ICP-Brasil 2.16.76.1.3.1 + ASN1EncodableVector ecpfVector = new ASN1EncodableVector(); + ecpfVector.add(new ASN1ObjectIdentifier("2.16.76.1.3.1")); + ecpfVector.add(new DEROctetString("01011990123456789001234567890123456789012345678".getBytes(StandardCharsets.UTF_8))); + GeneralName otherName = new GeneralName(GeneralName.otherName, new DERSequence(ecpfVector)); + userBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(otherName)); + + ContentSigner userSigner = new JcaContentSignerBuilder("SHA256withRSA").build(intermediateKeyPair.getPrivate()); + signerCert = new JcaX509CertificateConverter().getCertificate(userBuilder.build(userSigner)); + + // Carrega em KeyStore PKCS12 em memória com a cadeia completa + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + char[] pwd = "test-compliance-pwd".toCharArray(); + ks.setKeyEntry("ecpf-compliance-cert", userKeyPair.getPrivate(), pwd, new Certificate[]{signerCert, intermediateCaCert, rootCaCert}); + + Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("compliance-p12-provider", "Provedor Compliance Teste"); + provider.setKeyStore(ks, pwd); + certificateManager.registerProvider(provider); + + List certs = certificateManager.discoverCertificates(); + testEcpfCertId = certs.get(0).id(); + + pdfSigningService = new PdfSigningService(certificateManager); + xmlSigningService = new XmlSigningService(certificateManager); + } + + @Test + @DisplayName("PAdES: Deve gerar documento PDF assinado com envelope CMS desacoplado e validar conformidade estrutural") + void shouldGenerateAndValidateCompliantPadesSignature() throws Exception { + // Cria documento PDF em memória + byte[] originalPdf; + try (PDDocument doc = new PDDocument()) { + PDPage page = new PDPage(); + doc.addPage(page); + try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { + cs.beginText(); + cs.setFont(new org.apache.pdfbox.pdmodel.font.PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + cs.newLineAtOffset(50, 700); + cs.showText("CONTRATO DE PRESTACAO DE SERVICOS - COMPLIANCE TEST"); + cs.endText(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + doc.save(baos); + originalPdf = baos.toByteArray(); + } + + // Assina via PdfSigningService + PdfSignatureOptions options = PdfSignatureOptions.builder(testEcpfCertId) + .position(PdfSealPosition.BOTTOM_RIGHT) + .visibleSeal(true) + .reason("Aprovacao Contratual") + .location("Brasilia, DF") + .build(); + byte[] signedPdf = pdfSigningService.signPdf(originalPdf, options); + + assertThat(signedPdf).isNotEmpty(); + assertThat(signedPdf.length).isGreaterThan(originalPdf.length); + + // 1. Validação independente via Apache PDFBox + try (PDDocument signedDoc = Loader.loadPDF(signedPdf)) { + List signatures = signedDoc.getSignatureDictionaries(); + assertThat(signatures).hasSize(1); + + PDSignature sig = signatures.get(0); + assertThat(sig.getFilter()).isEqualTo("Adobe.PPKLite"); + assertThat(sig.getSubFilter()).isEqualTo("adbe.pkcs7.detached"); + assertThat(sig.getReason()).isEqualTo("Aprovacao Contratual"); + assertThat(sig.getLocation()).isEqualTo("Brasilia, DF"); + + int[] byteRange = sig.getByteRange(); + assertThat(byteRange).hasSize(4); + assertThat(byteRange[0]).isEqualTo(0); + assertThat(byteRange[1] + byteRange[3]).isEqualTo(signedPdf.length - (byteRange[2] - byteRange[1])); + + // 2. Validação criptográfica do envelope CMS/PKCS#7 com Bouncy Castle independente + byte[] signedContentBytes = sig.getSignedContent(signedPdf); + byte[] cmsBytes = sig.getContents(signedPdf); + CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(signedContentBytes), cmsBytes); + + assertThat(signedData.getSignerInfos().getSigners()).hasSize(1); + SignerInformation signerInfo = signedData.getSignerInfos().getSigners().iterator().next(); + + // Verifica se a assinatura bate com a chave pública do certificado do signatário + boolean verified = signerInfo.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signerCert)); + assertThat(verified).isTrue(); + + // Verifica se o digest embutido no CMS corresponde ao digest calculado dos bytes do documento + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] calculatedDigest = md.digest(signedContentBytes); + + byte[] messageDigestAttr = signerInfo.getSignedAttributes() + .get(org.bouncycastle.asn1.cms.CMSAttributes.messageDigest) + .getAttrValues().getObjectAt(0).toASN1Primitive().getEncoded(); + // Atributo ASN1 DER OctetString contém o digest + assertThat(messageDigestAttr).contains(calculatedDigest); + } + + // 3. Validação através do SignatureValidationService do CyberCore + ValidationResult valResult = validationService.validateAttached(signedPdf, "documento_assinado.pdf"); + assertThat(valResult.status()).isEqualTo(ValidationStatus.VALID); + assertThat(valResult.signers()).hasSize(1); + assertThat(valResult.signers().get(0).signerName()).contains("CARLOS EDUARDO DA SILVA"); + } + + @Test + @DisplayName("XAdES-BES: Deve gerar documento XML assinado com propriedades qualificadas e validar via JSR-105") + void shouldGenerateAndValidateCompliantXadesBesSignature() throws Exception { + String originalXml = """ + + + Carlos Eduardo da Silva + 12345678900 + Brasilia + + """; + + XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testEcpfCertId, null, XmlSignatureProfile.XADES_BES); + + byte[] signedXmlBytes = xmlSigningService.signXml(originalXml.getBytes(StandardCharsets.UTF_8), options); + String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8); + + // 1. Verificação de tags obrigatórias do XAdES-BES (ETSI TS 101 903 e DOC-ICP-15.03) + assertThat(signedXmlStr).contains("