test(compliance): add independent conformance test suite for pades and xades and iti guide

This commit is contained in:
Claudecio Martins
2026-10-20 15:24:00 -03:00
parent 06c90b4e86
commit 4b64a113b5
2 changed files with 390 additions and 0 deletions
+65
View File
@@ -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 (`<xades:QualifyingProperties>`, `<xades:SigningTime>` e `<xades:SigningCertificate>`).
---
## 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.
@@ -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<ManagedCertificate> 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<PDSignature> 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 = """
<?xml version="1.0" encoding="UTF-8"?>
<declaracao id="dec-9988">
<titular>Carlos Eduardo da Silva</titular>
<cpf>12345678900</cpf>
<cidade>Brasilia</cidade>
</declaracao>
""";
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("<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"");
assertThat(signedXmlStr).contains("<xades:QualifyingProperties");
assertThat(signedXmlStr).contains("<xades:SignedProperties");
assertThat(signedXmlStr).contains("<xades:SigningTime");
assertThat(signedXmlStr).contains("<xades:SigningCertificate");
assertThat(signedXmlStr).contains("http://uri.etsi.org/01903#SignedProperties");
// 2. Validação criptográfica independente via API JSR-105 (DOMValidateContext)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
NodeList sigNodes = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
assertThat(sigNodes.getLength()).isEqualTo(1);
DOMValidateContext valContext = new DOMValidateContext(signerCert.getPublicKey(), sigNodes.item(0));
NodeList allNodes = doc.getElementsByTagName("*");
for (int n = 0; n < allNodes.getLength(); n++) {
if (allNodes.item(n) instanceof Element e) {
if (e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
if (e.hasAttribute("id")) {
valContext.setIdAttributeNS(e, null, "id");
}
}
}
XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM");
XMLSignature xmlSignature = sigFactory.unmarshalXMLSignature(valContext);
boolean coreValidity = xmlSignature.validate(valContext);
assertThat(coreValidity).isTrue();
// 3. Validação pós-adulteração: se alterar qualquer caractere do XML original, a assinatura deve falhar
byte[] tamperedXml = signedXmlStr.replace("Carlos Eduardo da Silva", "Atacante Malicioso").getBytes(StandardCharsets.UTF_8);
Document tamperedDoc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(tamperedXml));
NodeList tamperedSigNodes = tamperedDoc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
DOMValidateContext tamperedContext = new DOMValidateContext(signerCert.getPublicKey(), tamperedSigNodes.item(0));
NodeList allTamperedNodes = tamperedDoc.getElementsByTagName("*");
for (int n = 0; n < allTamperedNodes.getLength(); n++) {
if (allTamperedNodes.item(n) instanceof Element e) {
if (e.hasAttribute("Id")) {
tamperedContext.setIdAttributeNS(e, null, "Id");
}
if (e.hasAttribute("id")) {
tamperedContext.setIdAttributeNS(e, null, "id");
}
}
}
XMLSignature tamperedSig = sigFactory.unmarshalXMLSignature(tamperedContext);
boolean tamperedValidity = tamperedSig.validate(tamperedContext);
assertThat(tamperedValidity).isFalse();
}
}