Skip to content

Instantly share code, notes, and snippets.

@robsonkades
Created January 28, 2026 01:10
Show Gist options
  • Select an option

  • Save robsonkades/a677f8b69995dec1a73825e8c1c2df8f to your computer and use it in GitHub Desktop.

Select an option

Save robsonkades/a677f8b69995dec1a73825e8c1c2df8f to your computer and use it in GitHub Desktop.
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