Created
January 28, 2026 01:10
-
-
Save robsonkades/a677f8b69995dec1a73825e8c1c2df8f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.springframework.boot.ssl.SslBundle; | |
| import org.springframework.boot.ssl.SslBundleKey; | |
| import org.springframework.boot.ssl.SslBundleRegistry; | |
| import org.springframework.boot.ssl.SslBundles; | |
| import org.springframework.boot.ssl.jks.JksSslStoreBundle; | |
| import org.springframework.boot.ssl.jks.JksSslStoreDetails; | |
| import org.springframework.http.MediaType; | |
| import org.springframework.http.ResponseEntity; | |
| import org.springframework.web.bind.annotation.*; | |
| @RestController | |
| @RequestMapping("/ssl-bundles") | |
| public class SslBundleController { | |
| private final SslBundleRegistry sslBundleRegistry; | |
| private final SslBundles sslBundles; | |
| private final RestClientFactory restClientFactory; | |
| public SslBundleController(SslBundleRegistry sslBundleRegistry, SslBundles sslBundles, RestClientFactory restClientFactory) { | |
| this.sslBundleRegistry = sslBundleRegistry; | |
| this.sslBundles = sslBundles; | |
| this.restClientFactory = restClientFactory; | |
| } | |
| public static String minifyBetweenTags(String raw) { | |
| if (raw == null || raw.isBlank()) return ""; | |
| String s = raw.trim(); | |
| s = s.replaceFirst("^<\\?xml[^>]+\\?>", "").trim(); // remove <?xml ... ?> | |
| // Collapse spaces between '>' and '<' (i.e., between elements), preserve spaces inside text | |
| s = s.replaceAll(">\\s+<", "><"); | |
| // Also collapse multiple spaces immediately inside tags if accidentally introduced in template | |
| s = s.replaceAll("\\s+>", ">"); | |
| s = s.replaceAll("<\\s+", "<"); | |
| return s; | |
| } | |
| @PostMapping | |
| public void register(@RequestBody BundleRequest req) { | |
| JksSslStoreDetails keyStore = new JksSslStoreDetails( | |
| req.keyStoreType(), // "JKS" | "PKCS12" | |
| null, | |
| req.keyStoreLocation(), // "file:C:/certs/client.pfx" ou "classpath:client.pfx" | |
| req.keyStorePassword() | |
| ); | |
| JksSslStoreDetails trustStore = (req.trustStoreLocation() != null) | |
| ? new JksSslStoreDetails( | |
| req.trustStoreType(), null, | |
| req.trustStoreLocation(), | |
| req.trustStorePassword() | |
| ) | |
| : null; | |
| JksSslStoreBundle stores = new JksSslStoreBundle(keyStore, trustStore); | |
| SslBundleKey key = (req.keyAlias() != null || req.keyPassword() != null) ? SslBundleKey.of(req.keyPassword(), req.keyAlias()) : SslBundleKey.NONE; | |
| SslBundle bundle = SslBundle.of(stores, key); | |
| // registra ou atualiza | |
| if (sslBundles.getBundleNames().contains(req.name())) { | |
| sslBundleRegistry.updateBundle(req.name(), bundle); | |
| } else { | |
| sslBundleRegistry.registerBundle(req.name(), bundle); | |
| } | |
| } | |
| @PostMapping(value = "/xml", consumes = "application/xml") | |
| public String xml(@RequestBody String xml) throws Exception { | |
| SslBundle httpOrSigningBundle = sslBundles.getBundle("test"); | |
| XmlSigningBundle signing = new XmlSigningBundleFactory().fromSslBundle(httpOrSigningBundle); | |
| return XMLSignature.signed(xml, signing); | |
| } | |
| @GetMapping | |
| public ResponseEntity<String> test() throws Exception { | |
| String url = "https://homologacao.nfe.fazenda.sp.gov.br/CTeWS/WS/CTeStatusServicoV4.asmx"; | |
| String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + | |
| "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n" + | |
| " <soap:Body>\n" + | |
| " <cteDadosMsg xmlns=\"http://www.portalfiscal.inf.br/cte/wsdl/CTeStatusServicoV4\">\n" + | |
| " <consStatServCTe versao=\"4.00\"\n" + | |
| "\t\t\t\txmlns=\"http://www.portalfiscal.inf.br/cte\">\n" + | |
| " <tpAmb>2</tpAmb>\n" + | |
| " <cUF>35</cUF>\n" + | |
| " <xServ>STATUS</xServ>\n" + | |
| " </consStatServCTe>\n" + | |
| " </cteDadosMsg>\n" + | |
| " </soap:Body>\n" + | |
| "</soap:Envelope>"; | |
| xml = minifyBetweenTags(xml); | |
| return restClientFactory.create("test") | |
| .post() | |
| .uri(url) | |
| .body(xml) | |
| .contentType(MediaType.parseMediaType("application/soap+xml")) | |
| .accept(MediaType.parseMediaType("application/soap+xml")) | |
| .retrieve() | |
| .toEntity(String.class); | |
| } | |
| public record BundleRequest( | |
| String name, | |
| String keyStoreType, | |
| String keyStoreLocation, | |
| String keyStorePassword, | |
| String keyAlias, | |
| String keyPassword, | |
| String trustStoreType, | |
| String trustStoreLocation, | |
| String trustStorePassword | |
| ) { | |
| } | |
| } | |
| import org.apache.hc.client5.http.config.RequestConfig; | |
| import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | |
| import org.apache.hc.client5.http.impl.classic.HttpClients; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; | |
| import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; | |
| import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; | |
| import org.apache.hc.core5.http.io.SocketConfig; | |
| import org.apache.hc.core5.pool.PoolConcurrencyPolicy; | |
| import org.apache.hc.core5.pool.PoolReusePolicy; | |
| import org.apache.hc.core5.reactor.ssl.SSLBufferMode; | |
| import org.apache.hc.core5.util.Timeout; | |
| import org.springframework.boot.ssl.SslBundle; | |
| import org.springframework.boot.ssl.SslBundles; | |
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |
| import org.springframework.stereotype.Component; | |
| import org.springframework.web.client.RestClient; | |
| import java.time.Duration; | |
| import java.util.function.Consumer; | |
| @Component | |
| public class RestClientFactory { | |
| /** | |
| * Timeout de socket (inatividade na camada de leitura/escrita) utilizado pelo {@link SocketConfig}. | |
| * Valor padrão: 60 segundos. | |
| */ | |
| public static final Timeout SO_TIMEOUT = Timeout.ofSeconds(60); | |
| /** | |
| * Timeout máximo aguardando uma conexão do pool para o {@link RequestConfig}. | |
| * Valor padrão: 10 segundos. | |
| */ | |
| public static final Timeout REQUEST_CONFIG_CONNECTION_REQUEST_TIMEOUT = Timeout.ofSeconds(10); | |
| /** | |
| * Timeout de resposta do request no {@link RequestConfig} (tempo total aguardando a resposta). | |
| * Valor padrão: 60 segundos. | |
| */ | |
| public static final Timeout REQUEST_CONFIG_RESPONSE_TIMEOUT = Timeout.ofSeconds(60); | |
| /** | |
| * Número máximo de conexões totais no pool do HttpClient. | |
| * Valor padrão: 100. | |
| */ | |
| public static final int MAX_CONN_TOTAL = 20; | |
| /** | |
| * Número máximo de conexões por rota (host) no pool do HttpClient. | |
| * Valor padrão: 20. | |
| */ | |
| public static final int MAX_CONN_PER_ROUTE = 5; | |
| /** | |
| * Configuração de socket padrão utilizada pelo pool de conexões. | |
| * Ativa TCP no-delay, mantém keep-alive e reuso de endereço, além do {@link #SO_TIMEOUT}. | |
| */ | |
| public static final SocketConfig SOCKET_CONFIG = SocketConfig.custom() | |
| .setTcpNoDelay(true) | |
| .setSoTimeout(SO_TIMEOUT) | |
| .setSoReuseAddress(true) | |
| .setSoKeepAlive(true) | |
| .build(); | |
| /** | |
| * Configuração de request padrão aplicada ao {@link CloseableHttpClient}. | |
| * Inclui timeouts de espera por conexão do pool e de resposta. | |
| */ | |
| public static final RequestConfig REQUEST_CONFIG = RequestConfig.custom() | |
| .setConnectionRequestTimeout(REQUEST_CONFIG_CONNECTION_REQUEST_TIMEOUT) | |
| .setResponseTimeout(REQUEST_CONFIG_RESPONSE_TIMEOUT) | |
| .build(); | |
| /** | |
| * Timeout de conexão (handshake TCP) aplicado na request factory do Spring. | |
| * Valor padrão: 5 segundos. | |
| */ | |
| private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5); | |
| /** | |
| * Timeout para aguardar uma conexão disponível no pool aplicado na request factory do Spring. | |
| * Valor padrão: 10 segundos. | |
| */ | |
| private static final Duration CONNECTION_REQUEST_TIMEOUT = Duration.ofSeconds(10); | |
| /** | |
| * Timeout de leitura (entre bytes) aplicado na request factory do Spring. | |
| * Valor padrão: 60 segundos. | |
| */ | |
| private static final Duration READ_TIMEOUT = Duration.ofSeconds(60); | |
| /** | |
| * Builder do Spring para criação de {@link RestClient}. | |
| */ | |
| private final RestClient.Builder restClientBuilder; | |
| /** | |
| * Registro de bundles SSL configurados na aplicação. | |
| */ | |
| private final SslBundles registerBundle; | |
| public RestClientFactory(RestClient.Builder restClientBuilder, SslBundles registerBundle, SslBundles sslBundles) { | |
| this.restClientBuilder = restClientBuilder; | |
| this.registerBundle = registerBundle; | |
| } | |
| public RestClient create(String bundleName) { | |
| CloseableResources closeableResources = closeableResources(bundleName); | |
| return restClientBuilder | |
| .apply(fromHttpClient(closeableResources.httpClient())) | |
| .build(); | |
| } | |
| public Consumer<RestClient.Builder> fromHttpClient(CloseableHttpClient httpClient) { | |
| return (builder) -> { | |
| HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory(httpClient); | |
| rf.setConnectTimeout(CONNECT_TIMEOUT); | |
| rf.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT); | |
| rf.setReadTimeout(READ_TIMEOUT); | |
| builder.requestFactory(rf); | |
| }; | |
| } | |
| public CloseableResources closeableResources(String bundleName) { | |
| SslBundle bundle = registerBundle.getBundle(bundleName); | |
| DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy( | |
| bundle.createSslContext(), | |
| new String[]{"TLSv1.2", "TLSv1.3"}, | |
| null, | |
| SSLBufferMode.DYNAMIC, | |
| NoopHostnameVerifier.INSTANCE); | |
| PoolingHttpClientConnectionManager connectionManager = | |
| PoolingHttpClientConnectionManagerBuilder.create() | |
| .setTlsSocketStrategy(tlsStrategy) | |
| .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) | |
| .setConnPoolPolicy(PoolReusePolicy.FIFO) | |
| .setDefaultSocketConfig(SOCKET_CONFIG) | |
| .setMaxConnTotal(MAX_CONN_TOTAL) | |
| .setMaxConnPerRoute(MAX_CONN_PER_ROUTE) | |
| .build(); | |
| CloseableHttpClient httpClient = HttpClients.custom() | |
| .setConnectionManager(connectionManager) | |
| .setDefaultRequestConfig(REQUEST_CONFIG) | |
| .setConnectionManagerShared(true) | |
| .build(); | |
| return new CloseableResources(httpClient, connectionManager); | |
| } | |
| } | |
| public record XmlSigningBundle(PrivateKey privateKey, X509Certificate[] certificateChain, String alias) { | |
| public XmlSigningBundle { | |
| Objects.requireNonNull(privateKey, "privateKey"); | |
| Objects.requireNonNull(certificateChain, "certificateChain"); | |
| certificateChain = Arrays.copyOf(certificateChain, certificateChain.length); | |
| } | |
| public X509Certificate leafCertificate() { | |
| return (this.certificateChain.length > 0) ? this.certificateChain[0] : null; | |
| } | |
| } | |
| import org.springframework.boot.ssl.SslBundle; | |
| import org.springframework.boot.ssl.SslBundleKey; | |
| import org.springframework.boot.ssl.SslStoreBundle; | |
| import java.security.KeyStore; | |
| import java.security.PrivateKey; | |
| import java.security.cert.Certificate; | |
| import java.security.cert.X509Certificate; | |
| import java.util.ArrayList; | |
| import java.util.Enumeration; | |
| import java.util.List; | |
| public final class XmlSigningBundleFactory { | |
| public XmlSigningBundle fromSslBundle(SslBundle bundle) { | |
| if (bundle == null) { | |
| throw new IllegalArgumentException("SslBundle must not be null"); | |
| } | |
| SslStoreBundle stores = bundle.getStores(); | |
| if (stores == null || stores.getKeyStore() == null) { | |
| throw new IllegalStateException("SslBundle does not provide a keyStore"); | |
| } | |
| return fromKeyStore(stores.getKeyStore(), stores.getKeyStorePassword(), | |
| bundle.getKey() != null ? bundle.getKey() : SslBundleKey.NONE); | |
| } | |
| public XmlSigningBundle fromKeyStore(KeyStore keyStore, String storePassword, SslBundleKey bundleKey) { | |
| char[] password = (storePassword != null) ? storePassword.toCharArray() : null; | |
| try { | |
| return fromKeyStore(keyStore, password, bundleKey); | |
| } finally { | |
| if (password != null) { | |
| java.util.Arrays.fill(password, '\0'); | |
| } | |
| } | |
| } | |
| public XmlSigningBundle fromKeyStore(KeyStore keyStore, char[] storePassword, SslBundleKey bundleKey) { | |
| if (keyStore == null) { | |
| throw new IllegalArgumentException("KeyStore must not be null"); | |
| } | |
| SslBundleKey key = (bundleKey != null) ? bundleKey : SslBundleKey.NONE; | |
| String alias = resolveAlias(keyStore, key.getAlias()); | |
| char[] keyPassword = resolveKeyPassword(key, storePassword); | |
| try { | |
| PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword); | |
| if (privateKey == null) { | |
| throw new IllegalStateException("No private key found for alias '" + alias + "'"); | |
| } | |
| X509Certificate[] chain = certificateChainForAlias(keyStore, alias); | |
| return new XmlSigningBundle(privateKey, chain, alias); | |
| } catch (Exception ex) { | |
| throw new IllegalStateException("Unable to load XML signing material", ex); | |
| } finally { | |
| if (keyPassword != null) { | |
| java.util.Arrays.fill(keyPassword, '\0'); | |
| } | |
| } | |
| } | |
| private String resolveAlias(KeyStore keyStore, String alias) { | |
| try { | |
| if (alias != null && !alias.isBlank()) { | |
| if (!keyStore.containsAlias(alias)) { | |
| throw new IllegalStateException("Keystore does not contain alias '" + alias + "'"); | |
| } | |
| return alias; | |
| } | |
| Enumeration<String> aliases = keyStore.aliases(); | |
| while (aliases.hasMoreElements()) { | |
| String candidate = aliases.nextElement(); | |
| if (keyStore.isKeyEntry(candidate)) { | |
| return candidate; | |
| } | |
| } | |
| throw new IllegalStateException("No key entries found in keystore"); | |
| } catch (Exception ex) { | |
| throw new IllegalStateException("Unable to resolve key alias", ex); | |
| } | |
| } | |
| private char[] resolveKeyPassword(SslBundleKey key, char[] storePassword) { | |
| if (key != null && key.getPassword() != null) { | |
| return key.getPassword().toCharArray(); | |
| } | |
| if (storePassword == null) { | |
| return null; | |
| } | |
| return java.util.Arrays.copyOf(storePassword, storePassword.length); | |
| } | |
| private X509Certificate[] certificateChainForAlias(KeyStore keyStore, String alias) { | |
| try { | |
| Certificate[] chain = keyStore.getCertificateChain(alias); | |
| if (chain == null || chain.length == 0) { | |
| Certificate certificate = keyStore.getCertificate(alias); | |
| if (certificate instanceof X509Certificate x509Certificate) { | |
| return new X509Certificate[]{x509Certificate}; | |
| } | |
| return new X509Certificate[0]; | |
| } | |
| List<X509Certificate> certificates = new ArrayList<>(); | |
| for (Certificate certificate : chain) { | |
| if (certificate instanceof X509Certificate x509Certificate) { | |
| certificates.add(x509Certificate); | |
| } | |
| } | |
| return certificates.toArray(X509Certificate[]::new); | |
| } catch (Exception ex) { | |
| throw new IllegalStateException("Unable to read certificate chain for alias '" + alias + "'", ex); | |
| } | |
| } | |
| } | |
| @Configuration | |
| public class ConfigureRestClientSsl implements RestClientSsl { | |
| private final ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder; | |
| private final SslBundles sslBundles; | |
| ConfigureRestClientSsl(ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder, | |
| SslBundles sslBundles) { | |
| this.clientHttpRequestFactoryBuilder = clientHttpRequestFactoryBuilder; | |
| this.sslBundles = sslBundles; | |
| } | |
| @Override | |
| public Consumer<RestClient.Builder> fromBundle(String bundleName) { | |
| return fromBundle(this.sslBundles.getBundle(bundleName)); | |
| } | |
| @Override | |
| public Consumer<RestClient.Builder> fromBundle(SslBundle bundle) { | |
| if (bundle == null) { | |
| return (builder) -> { | |
| }; | |
| } | |
| return (builder) -> { | |
| ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.ofSslBundle(bundle); | |
| ClientHttpRequestFactory requestFactory = this.clientHttpRequestFactoryBuilder.build(settings); | |
| builder.requestFactory(requestFactory); | |
| }; | |
| } | |
| } | |
| @Configuration | |
| class SslBundleConfig { | |
| @Bean | |
| @ConditionalOnMissingBean | |
| DefaultSslBundleRegistry sslBundleRegistry() { | |
| return new DefaultSslBundleRegistry(); | |
| } | |
| } | |
| import org.w3c.dom.Document; | |
| import org.w3c.dom.Element; | |
| import org.w3c.dom.NodeList; | |
| import javax.xml.XMLConstants; | |
| import javax.xml.crypto.AlgorithmMethod; | |
| import javax.xml.crypto.KeySelector; | |
| import javax.xml.crypto.KeySelectorException; | |
| import javax.xml.crypto.KeySelectorResult; | |
| import javax.xml.crypto.XMLCryptoContext; | |
| import javax.xml.crypto.XMLStructure; | |
| import javax.xml.crypto.dsig.CanonicalizationMethod; | |
| import javax.xml.crypto.dsig.DigestMethod; | |
| import javax.xml.crypto.dsig.Reference; | |
| import javax.xml.crypto.dsig.SignatureMethod; | |
| import javax.xml.crypto.dsig.SignedInfo; | |
| import javax.xml.crypto.dsig.Transform; | |
| import javax.xml.crypto.dsig.XMLSignatureFactory; | |
| import javax.xml.crypto.dsig.dom.DOMSignContext; | |
| import javax.xml.crypto.dsig.dom.DOMValidateContext; | |
| import javax.xml.crypto.dsig.keyinfo.KeyInfo; | |
| import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; | |
| import javax.xml.crypto.dsig.keyinfo.X509Data; | |
| import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; | |
| import javax.xml.crypto.dsig.spec.TransformParameterSpec; | |
| import javax.xml.namespace.NamespaceContext; | |
| import javax.xml.parsers.DocumentBuilder; | |
| import javax.xml.parsers.DocumentBuilderFactory; | |
| import javax.xml.parsers.ParserConfigurationException; | |
| import javax.xml.transform.OutputKeys; | |
| import javax.xml.transform.Source; | |
| import javax.xml.transform.Transformer; | |
| import javax.xml.transform.TransformerException; | |
| import javax.xml.transform.TransformerFactory; | |
| import javax.xml.transform.dom.DOMSource; | |
| import javax.xml.transform.stream.StreamResult; | |
| import javax.xml.transform.stream.StreamSource; | |
| import javax.xml.xpath.XPath; | |
| import javax.xml.xpath.XPathConstants; | |
| import javax.xml.xpath.XPathExpression; | |
| import javax.xml.xpath.XPathExpressionException; | |
| import javax.xml.xpath.XPathFactory; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.StringWriter; | |
| import java.nio.charset.StandardCharsets; | |
| import java.security.KeyStore; | |
| import java.security.PrivateKey; | |
| import java.security.PublicKey; | |
| import java.security.cert.X509Certificate; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.Iterator; | |
| import br.com.cloudstack.sefazurl.ssl.XmlSigningBundle; | |
| public class XMLSignature { | |
| private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = createDocumentBuilderFactory(); | |
| private static final KeySelector KEY_SELECTOR = new KeyValueKeySelector(); | |
| private static final XMLSignatureFactory XML_SIGNATURE_FACTORY = XMLSignatureFactory.getInstance("DOM", new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI()); | |
| public static Document createDocument(InputStream inputStream) throws Exception { | |
| try (inputStream) { | |
| DocumentBuilder documentBuilder = createDocumentBuilder(); | |
| return documentBuilder.parse(inputStream); | |
| } | |
| } | |
| public static boolean isValid(String xml) throws Exception { | |
| try (InputStream sanitizedStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) { | |
| return isValid(sanitizedStream); | |
| } | |
| } | |
| // public static boolean isValid(InputStream inputStream) throws Exception { | |
| // byte[] data = inputStream.readAllBytes(); | |
| // } | |
| public static boolean isValid(InputStream inputStream) throws Exception { | |
| Document document = createDocument(inputStream); | |
| Element elementWithId = findElementWithId(document); | |
| elementWithId.setIdAttribute("Id", true); | |
| NodeList signatureElement = document.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "Signature"); | |
| if (signatureElement.getLength() == 0) { | |
| throw new Exception("Elemento de assinatura não encontrado."); | |
| } | |
| NodeList keyInfoList = document.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "KeyInfo"); | |
| if (keyInfoList.getLength() == 0) { | |
| throw new Exception("Elemento KeyInfo não encontrado."); | |
| } | |
| DOMValidateContext domValidateContext = new DOMValidateContext(KEY_SELECTOR, signatureElement.item(0)); | |
| javax.xml.crypto.dsig.XMLSignature signature = XML_SIGNATURE_FACTORY.unmarshalXMLSignature(domValidateContext); | |
| return signature.validate(domValidateContext); | |
| } | |
| public static String signed(String xml, XmlSigningBundle bundle) throws Exception { | |
| try (InputStream sanitizedStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) { | |
| return signed(sanitizedStream, bundle); | |
| } | |
| } | |
| public static String signed(InputStream inputStream, XmlSigningBundle bundle) throws Exception { | |
| Document document = createDocument(inputStream); | |
| PrivateKey privateKey = bundle.privateKey(); | |
| X509Certificate certificate = bundle.leafCertificate(); | |
| if (certificate == null) { | |
| throw new IllegalStateException("XML signing bundle does not contain a leaf certificate"); | |
| } | |
| Element elementToSign = findElementWithId(document); | |
| elementToSign.setIdAttribute("Id", true); | |
| String id = elementToSign.getAttribute("Id"); | |
| Reference ref = XML_SIGNATURE_FACTORY.newReference( | |
| "#" + id, | |
| XML_SIGNATURE_FACTORY.newDigestMethod(DigestMethod.SHA256, null), | |
| Arrays.asList( | |
| XML_SIGNATURE_FACTORY.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null), | |
| XML_SIGNATURE_FACTORY.newTransform(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null) | |
| ), | |
| null, | |
| null | |
| ); | |
| SignedInfo signedInfo = XML_SIGNATURE_FACTORY.newSignedInfo( | |
| XML_SIGNATURE_FACTORY.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), | |
| XML_SIGNATURE_FACTORY.newSignatureMethod(SignatureMethod.RSA_SHA256, null), | |
| Collections.singletonList(ref) | |
| ); | |
| KeyInfoFactory keyInfoFactory = XML_SIGNATURE_FACTORY.getKeyInfoFactory(); | |
| X509Data x509Data = keyInfoFactory.newX509Data(Collections.singletonList(certificate)); | |
| KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(x509Data)); | |
| DOMSignContext domSignContext = new DOMSignContext(privateKey, document.getDocumentElement()); | |
| javax.xml.crypto.dsig.XMLSignature xmlSignature = XML_SIGNATURE_FACTORY.newXMLSignature(signedInfo, keyInfo); | |
| xmlSignature.sign(domSignContext); | |
| return transformDocumentToString(document); | |
| } | |
| public static String signed(String xml, String password, byte[] certificate) throws Exception { | |
| try (InputStream sanitizedStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) { | |
| return signed(sanitizedStream, password, certificate); | |
| } | |
| } | |
| public static String signed(InputStream inputStream, String password, byte[] pfxPath) throws Exception { | |
| Document document = createDocument(inputStream); | |
| KeyStore keyStore = KeyStore.getInstance("PKCS12"); | |
| try (var keyStream = new ByteArrayInputStream(pfxPath)) { | |
| keyStore.load(keyStream, password.toCharArray()); | |
| } | |
| String alias = keyStore.aliases().nextElement(); | |
| PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray()); | |
| X509Certificate certificate = (X509Certificate) keyStore.getCertificate(alias); | |
| Element elementToSign = findElementWithId(document); | |
| elementToSign.setIdAttribute("Id", true); | |
| String id = elementToSign.getAttribute("Id"); | |
| // Referência com as transformações exigidas | |
| Reference ref = XML_SIGNATURE_FACTORY.newReference( | |
| "#" + id, | |
| XML_SIGNATURE_FACTORY.newDigestMethod(DigestMethod.SHA256, null), | |
| Arrays.asList( | |
| XML_SIGNATURE_FACTORY.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null), | |
| XML_SIGNATURE_FACTORY.newTransform(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null) | |
| ), | |
| null, | |
| null | |
| ); | |
| // Informação assinada | |
| SignedInfo signedInfo = XML_SIGNATURE_FACTORY.newSignedInfo( | |
| XML_SIGNATURE_FACTORY.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), | |
| XML_SIGNATURE_FACTORY.newSignatureMethod(SignatureMethod.RSA_SHA256, null), | |
| Collections.singletonList(ref) | |
| ); | |
| // Adicionar informações da chave | |
| KeyInfoFactory keyInfoFactory = XML_SIGNATURE_FACTORY.getKeyInfoFactory(); | |
| X509Data x509Data = keyInfoFactory.newX509Data(Collections.singletonList(certificate)); | |
| KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(x509Data)); | |
| DOMSignContext domSignContext = new DOMSignContext(privateKey, document.getDocumentElement()); | |
| javax.xml.crypto.dsig.XMLSignature xmlSignature = XML_SIGNATURE_FACTORY.newXMLSignature(signedInfo, keyInfo); | |
| xmlSignature.sign(domSignContext); | |
| return transformDocumentToString(document); | |
| } | |
| private static String transformDocumentToString(Document document) throws TransformerException { | |
| TransformerFactory tf = TransformerFactory.newInstance(); | |
| Transformer transformer = tf.newTransformer(); | |
| StringWriter writer = new StringWriter(); | |
| transformer.transform(new DOMSource(document), new StreamResult(writer)); | |
| return writer.toString(); | |
| } | |
| private static synchronized DocumentBuilder createDocumentBuilder() throws ParserConfigurationException { | |
| return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); | |
| } | |
| private static DocumentBuilderFactory createDocumentBuilderFactory() { | |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | |
| factory.setNamespaceAware(true); | |
| try { | |
| factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); | |
| factory.setFeature("http://xml.org/sax/features/external-general-entities", false); | |
| factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); | |
| factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); | |
| } catch (ParserConfigurationException e) { | |
| throw new IllegalStateException("Erro ao configurar DocumentBuilderFactory", e); | |
| } | |
| return factory; | |
| } | |
| private static InputStream sanitizeAndConvertToStream(InputStream inputStream) throws TransformerException, IOException { | |
| try (StringWriter writer = new StringWriter()) { | |
| TransformerFactory transformerFactory = TransformerFactory.newInstance(); | |
| transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); | |
| Transformer transformer = transformerFactory.newTransformer(); | |
| transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); | |
| transformer.setOutputProperty(OutputKeys.INDENT, "no"); | |
| transformer.setOutputProperty(OutputKeys.METHOD, "xml"); | |
| Source source = new StreamSource(inputStream); | |
| transformer.transform(source, new StreamResult(writer)); | |
| String sanitizedXml = writer.toString() | |
| .replaceAll("\\s+", " ") | |
| .replaceAll("\\s*>\\s*", ">") | |
| .replaceAll("\\s*<\\s*", "<") | |
| .trim(); | |
| return new ByteArrayInputStream(sanitizedXml.getBytes(StandardCharsets.UTF_8)); | |
| } | |
| } | |
| private static Element findElementWithId(Document document) throws Exception { | |
| XPathFactory xpathFactory = XPathFactory.newInstance(); | |
| XPath xpath = xpathFactory.newXPath(); | |
| xpath.setNamespaceContext(new NamespaceContext() { | |
| @Override | |
| public String getNamespaceURI(String prefix) { | |
| return switch (prefix.toLowerCase()) { | |
| case "nfe" -> "http://www.portalfiscal.inf.br/nfe"; | |
| case "cte" -> "http://www.portalfiscal.inf.br/cte"; | |
| default -> XMLConstants.NULL_NS_URI; | |
| }; | |
| } | |
| @Override | |
| public String getPrefix(String namespaceURI) { | |
| return null; | |
| } | |
| @Override | |
| public Iterator<String> getPrefixes(String namespaceURI) { | |
| return Collections.emptyIterator(); | |
| } | |
| }); | |
| try { | |
| XPathExpression expr = xpath.compile("//*[starts-with(@Id, 'NFe') or starts-with(@Id, 'ID') or starts-with(@Id, 'CTe')]"); | |
| NodeList nodes = (NodeList) expr.evaluate(document, XPathConstants.NODESET); | |
| if (nodes.getLength() == 0) { | |
| throw new Exception("Elemento com Id não encontrado. Estrutura do XML:\n"); | |
| } | |
| return (Element) nodes.item(0); | |
| } catch (XPathExpressionException e) { | |
| throw new Exception("Erro ao buscar elemento com Id", e); | |
| } | |
| } | |
| // Classe interna permanece inalterada, exceto para o uso de exceções específicas. | |
| private static class KeyValueKeySelector extends KeySelector { | |
| public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException { | |
| if (keyInfo == null) { | |
| throw new KeySelectorException("Nenhum KeyInfo encontrado!"); | |
| } | |
| for (XMLStructure info : keyInfo.getContent()) { | |
| if (info instanceof X509Data x509Data) { | |
| try { | |
| for (Object data : x509Data.getContent()) { | |
| if (data instanceof X509Certificate x509Certificate) { | |
| PublicKey pk = x509Certificate.getPublicKey(); | |
| return new SimpleKeySelectorResult(pk); | |
| } | |
| } | |
| } catch (Exception e) { | |
| throw new KeySelectorException(e); | |
| } | |
| } | |
| } | |
| throw new KeySelectorException("Nenhum KeyValue encontrado!"); | |
| } | |
| } | |
| private record SimpleKeySelectorResult(PublicKey pk) implements KeySelectorResult { | |
| public PublicKey getKey() { | |
| return pk; | |
| } | |
| } | |
| } | |
| { | |
| "name": "test", | |
| "keyStoreType": "PKCS12", | |
| "keyStoreLocation": "classpath:", | |
| "keyStorePassword": "", | |
| "trustStoreType": "JKS", | |
| "trustStoreLocation": "classpath:truststore.jks", | |
| "trustStorePassword": "" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment