Last active
February 2, 2024 11:35
-
-
Save rponte/646a4259a2e012e4aca50f2bb02e796f to your computer and use it in GitHub Desktop.
How to sign a XML with private key certificate in Java (using Java Key Store)
This file contains 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
package br.com.rponte.signature; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.security.KeyStore; | |
import java.security.KeyStoreException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.cert.CertificateException; | |
/** | |
* Holds information about the Java Key Store | |
*/ | |
public class keyStoreInfo { | |
private static final String KEY_STORE_TYPE = "JKS"; | |
private String alias; | |
private String password; | |
private KeyStore keyStore; | |
public keyStoreInfo(String alias, String password) { | |
this.alias = alias; | |
this.password = password; | |
} | |
/** | |
* Loads KeyStore from the given Private Key | |
*/ | |
public void load(InputStream privateKey) { | |
try { | |
this.keyStore = KeyStore.getInstance(KEY_STORE_TYPE); | |
this.keyStore.load(privateKey, password.toCharArray()); | |
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { | |
e.printStackTrace(); | |
throw new XmlSigningException("Error loading KeyStore", e); | |
} | |
} | |
public String getAlias() { | |
return alias; | |
} | |
public String getPassword() { | |
return password; | |
} | |
public KeyStore getKeyStore() { | |
return keyStore; | |
} | |
} |
This file contains 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
package br.com.mdias.rponte.signature; | |
import java.util.Base64; | |
/** | |
* Represents a signed XML | |
*/ | |
public class SignedXml { | |
private String content; | |
public SignedXml(String content) { | |
this.content = content; | |
} | |
public String getContent() { | |
return content; | |
} | |
public String toBase64() { | |
String base64Xml = Base64.getEncoder().encodeToString(content.getBytes()); | |
return base64Xml; | |
} | |
} |
This file contains 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
package br.com.rponte.signature; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.nio.charset.StandardCharsets; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.InvalidKeyException; | |
import java.security.Key; | |
import java.security.KeyStore; | |
import java.security.KeyStoreException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.SignatureException; | |
import java.security.UnrecoverableEntryException; | |
import java.security.cert.X509Certificate; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.List; | |
import javax.xml.crypto.MarshalException; | |
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.XMLSignature; | |
import javax.xml.crypto.dsig.XMLSignatureException; | |
import javax.xml.crypto.dsig.XMLSignatureFactory; | |
import javax.xml.crypto.dsig.dom.DOMSignContext; | |
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.keyinfo.X509IssuerSerial; | |
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; | |
import javax.xml.crypto.dsig.spec.TransformParameterSpec; | |
import javax.xml.parsers.DocumentBuilder; | |
import javax.xml.parsers.DocumentBuilderFactory; | |
import javax.xml.parsers.ParserConfigurationException; | |
import javax.xml.transform.TransformerConfigurationException; | |
import javax.xml.transform.TransformerException; | |
import javax.xml.transform.TransformerFactory; | |
import javax.xml.transform.TransformerFactoryConfigurationError; | |
import javax.xml.transform.dom.DOMSource; | |
import javax.xml.transform.stream.StreamResult; | |
import org.apache.commons.io.IOUtils; | |
import org.w3c.dom.Document; | |
import org.xml.sax.SAXException; | |
/** | |
* Responsible for signing a specific XML using private key certificate | |
*/ | |
public class XmlSigner { | |
private static final String C14NEXC = "http://www.w3.org/2001/10/xml-exc-c14n#"; | |
private keyStoreInfo keyStoreInfo; | |
private InputStream sourceXml; | |
/** | |
* Signs a specific XML using a private key via Java Key Store format | |
* | |
* More information: | |
* https://gist.github.com/rponte/4039958 | |
* https://github.com/SUNET/eduid-mm-service/tree/master/src/main/java/se/gov/minameddelanden/common | |
* https://stackoverflow.com/questions/5330049/java-equivalent-of-c-sharp-xml-signing-method | |
*/ | |
public SignedXml sign() throws NoSuchAlgorithmException, InvalidKeyException, KeyStoreException, SignatureException, IOException, InvalidAlgorithmParameterException, ParserConfigurationException, SAXException, MarshalException, XMLSignatureException, TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError, UnrecoverableEntryException { | |
KeyStore keyStore = keyStoreInfo.getKeyStore(); | |
String alias = keyStoreInfo.getAlias(); | |
char[] password = keyStoreInfo.getPassword().toCharArray(); | |
// Create a DOM XMLSignatureFactory that will be used to | |
// generate the enveloped signature. | |
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); | |
// Create a Reference to the enveloped document (in this case, | |
// you are signing the whole document, so a URI of "" signifies | |
// that, and also specify the SHA1 digest algorithm and | |
// the ENVELOPED Transform. | |
Transform envelopedTransform = fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null); | |
Transform c14NEXCTransform = fac.newTransform(C14NEXC, (TransformParameterSpec) null); | |
List<Transform> transforms = Arrays.asList(envelopedTransform, c14NEXCTransform); | |
DigestMethod digestMethod = fac.newDigestMethod(DigestMethod.SHA1, null); | |
Reference ref = fac.newReference("", digestMethod, transforms, null, null); | |
// Create the SignedInfo. | |
CanonicalizationMethod canonicalizationMethod = fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null); | |
SignatureMethod signatureMethod = fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null); | |
SignedInfo si = fac.newSignedInfo(canonicalizationMethod, signatureMethod, Collections.singletonList(ref)); | |
// Create the KeyInfo containing the X509Data. | |
KeyInfoFactory keyInfoFactory = fac.getKeyInfoFactory(); | |
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(alias); | |
X509Data newX509Data = keyInfoFactory.newX509Data(Arrays.asList(certificate)); | |
X509IssuerSerial issuer = keyInfoFactory.newX509IssuerSerial(certificate.getIssuerX500Principal().getName(), certificate.getSerialNumber()); | |
List<XMLStructure> data = Arrays.asList(newX509Data, issuer); | |
KeyInfo keyInfo = keyInfoFactory.newKeyInfo(data); | |
// Converts XML to Document | |
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); | |
dbf.setNamespaceAware(true); | |
DocumentBuilder builder = dbf.newDocumentBuilder(); | |
Document doc = builder.parse(sourceXml); | |
// Create a DOMSignContext and specify the RSA PrivateKey and | |
// location of the resulting XMLSignature's parent element. | |
Key key = keyStore.getKey(alias, password); | |
if (key == null) { | |
throw new XmlSigningException("Private Key not found for alias '" + alias + "'"); | |
} | |
DOMSignContext dsc = new DOMSignContext(key, doc.getDocumentElement()); | |
// Adds <Signature> tag before a specific tag inside XML - with or without namespace | |
/* | |
Node assertionTag = doc.getElementsByTagName("saml2:Assertion").item(0); | |
Node afterTag = doc.getElementsByTagName("saml2:Subject").item(0); | |
DOMSignContext dsc = new DOMSignContext(key, assertionTag, afterTag); | |
dsc.setDefaultNamespacePrefix("ds"); | |
*/ | |
// Create the XMLSignature, but don't sign it yet. | |
XMLSignature signature = fac.newXMLSignature(si, keyInfo); | |
signature.sign(dsc); // Marshal, generate, and sign the enveloped signature. | |
ByteArrayOutputStream output = new ByteArrayOutputStream(); | |
TransformerFactory.newInstance() | |
.newTransformer() | |
.transform(new DOMSource(doc), new StreamResult(output)); | |
String rawSignedXml = new String(output.toByteArray()); | |
SignedXml xml = new SignedXml(rawSignedXml); | |
return xml; | |
} | |
public XmlSigner withKeyStore(InputStream keyStore, String alias, String password) { | |
if (keyStore == null) | |
throw new XmlSigningException("KeyStore without private key"); | |
keyStoreInfo ksi = new keyStoreInfo(alias, password); | |
ksi.load(keyStore); | |
this.keyStoreInfo = ksi; | |
return this; | |
} | |
public XmlSigner withXml(InputStream sourceXml) { | |
if (sourceXml == null) | |
throw new XmlSigningException("XML can not be null"); | |
this.sourceXml = sourceXml; | |
return this; | |
} | |
public XmlSigner withXml(String sourceXml) { | |
InputStream input = IOUtils.toInputStream(sourceXml, StandardCharsets.UTF_8); | |
return withXml(input); | |
} | |
} |
This file contains 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
package br.com.mdias.rponte.signature; | |
import static org.assertj.core.api.Assertions.assertThat; | |
import java.io.InputStream; | |
import org.junit.Rule; | |
import org.junit.Test; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.apache.commons.io.IOUtils; | |
/** | |
* How to use XmlSigner | |
*/ | |
public class XmlSignerTest { | |
private static final Logger logger = LoggerFactory.getLogger(XmlSignerTest.class); | |
@Test | |
public void shouldSignXmlUsingAJavaKeyStore() throws Exception { | |
// scenario | |
InputStream xml = XmlSigner.class.getResourceAsStream("/xml-document-sample.xml"); | |
InputStream jks = XmlSigner.class.getResourceAsStream("/my-private-key.jks"); | |
String alias = "root"; | |
String password = "secret"; | |
// action | |
SignedXml signedXml = new XmlSigner() | |
.withXml( "\n<request> " | |
+ "\n <another-tag name='foo' /> " | |
+ "\n</request> ") // it supports InputStream as well | |
.withKeyStore(jks, alias, password) | |
.sign(); | |
// validation | |
String content = signedXml.getContent(); | |
logger.info("\n" + content); // just prints the result | |
assertThat(content) | |
.contains("<X509Certificate>") | |
.contains("</X509Data>") | |
.contains("</Signature>") | |
; | |
} | |
} |
This file contains 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
package br.com.mdias.rponte.signature; | |
public class XmlSigningException extends RuntimeException { | |
private static final long serialVersionUID = 1L; | |
public XmlSigningException(String message) { | |
super(message); | |
} | |
public XmlSigningException(String message, Throwable cause) { | |
super(message, cause); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hola, es posible utilizar este código para un fichero certificado .p12 para la firma ?