package li.strolch.utils.helper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.security.KeyStore; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStore.TrustedCertificateEntry; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.UUID; 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.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.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import li.strolch.utils.dbc.DBC; public class XmlDomSigner { private static final Logger logger = LoggerFactory.getLogger(XmlDomSigner.class); private KeyStore keyStore; private String privateKeyAlias; private String trustAlias; private char[] password; public XmlDomSigner(File keyStorePath, String privateKeyAlias, String trustAlias, char[] password) { DBC.PRE.assertNotEmpty("privateKeyAlias", privateKeyAlias); DBC.PRE.assertNotEmpty("trustAlias", trustAlias); try { KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(new FileInputStream(keyStorePath), password); this.keyStore = keyStore; this.privateKeyAlias = privateKeyAlias; this.trustAlias = trustAlias; this.password = password; } catch (Exception e) { throw new RuntimeException("Failed to read keystore " + keyStorePath); } } public void sign(Document document) throws RuntimeException { try { String id = "Signed_" + UUID.randomUUID().toString(); Element rootElement = document.getDocumentElement(); rootElement.setAttribute("ID", id); rootElement.setIdAttribute("ID", true); // 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. List<Transform> transforms = new ArrayList<>(); transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)); transforms.add(fac.newTransform(CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec) null)); DigestMethod digestMethod = fac.newDigestMethod(DigestMethod.SHA1, null); Reference ref = fac.newReference("#" + id, digestMethod, transforms, null, null); // Create the SignedInfo. SignedInfo signedInfo = fac.newSignedInfo( fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null), // fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null), // Collections.singletonList(ref)); // Load the KeyStore and get the signing key and certificate. PrivateKeyEntry keyEntry = (PrivateKeyEntry) this.keyStore.getEntry(this.privateKeyAlias, new KeyStore.PasswordProtection(this.password)); PrivateKey privateKey = keyEntry.getPrivateKey(); X509Certificate cert = (X509Certificate) keyEntry.getCertificate(); // Create the KeyInfo containing the X509Data. KeyInfoFactory kif = fac.getKeyInfoFactory(); List<Object> x509Content = new ArrayList<>(); x509Content.add(cert.getSubjectX500Principal().getName()); x509Content.add(cert); X509Data xd = kif.newX509Data(x509Content); KeyInfo keyInfo = kif.newKeyInfo(Collections.singletonList(xd)); // Create a DOMSignContext and specify the RSA PrivateKey and // location of the resulting XMLSignature's parent element. DOMSignContext dsc = new DOMSignContext(privateKey, rootElement); //dsc.setDefaultNamespacePrefix("samlp"); dsc.putNamespacePrefix(XMLSignature.XMLNS, "ds"); // Create the XMLSignature, but don't sign it yet. XMLSignature signature = fac.newXMLSignature(signedInfo, keyInfo); // Marshal, generate, and sign the enveloped signature. signature.sign(dsc); } catch (Exception e) { throw new RuntimeException("Failed to sign document", e); } } public void validate(Document doc) throws RuntimeException { try { // Create a DOM XMLSignatureFactory that will be used to // generate the enveloped signature. XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); // Find Signature element. NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); if (nl.getLength() == 0) { throw new Exception("Cannot find Signature element!"); } else if (nl.getLength() > 1) { throw new Exception("Found multiple Signature elements!"); } // Load the KeyStore and get the signing key and certificate. TrustedCertificateEntry entry = (TrustedCertificateEntry) this.keyStore.getEntry(trustAlias, null); PublicKey publicKey = entry.getTrustedCertificate().getPublicKey(); // Create a DOMValidateContext and specify a KeySelector // and document context. Node signatureNode = nl.item(0); DOMValidateContext valContext = new DOMValidateContext(publicKey, signatureNode); // Unmarshal the XMLSignature. valContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); XMLSignature signature = fac.unmarshalXMLSignature(valContext); // Validate the XMLSignature. boolean coreValidity = signature.validate(valContext); // Check core validation status. if (!coreValidity) { logger.error("Signature failed core validation"); boolean sv = signature.getSignatureValue().validate(valContext); logger.error("signature validation status: " + sv); if (!sv) { // Check the validation status of each Reference. Iterator<?> i = signature.getSignedInfo().getReferences().iterator(); for (int j = 0; i.hasNext(); j++) { boolean refValid = ((Reference) i.next()).validate(valContext); logger.error("ref[" + j + "] validity status: " + refValid); } } throw new RuntimeException("Uh-oh validation, failed!"); } } catch (Exception e) { if (e instanceof RuntimeException) throw (RuntimeException) e; throw new RuntimeException("Failed to validate document", e); } } public static byte[] transformToBytes(Document doc) { return transformToBytes(doc, false); } public static byte[] transformToBytes(Document doc, boolean indent) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); if (indent) { transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); //$NON-NLS-1$ //$NON-NLS-2$ } transformer.transform(new DOMSource(doc), new StreamResult(out)); return out.toByteArray(); } catch (TransformerFactoryConfigurationError | TransformerException e) { throw new RuntimeException("Failed to transform document to bytes!", e); } } public static void writeTo(Document doc, File file) { try { writeTo(doc, new FileOutputStream(file)); } catch (FileNotFoundException e) { throw new RuntimeException("Failed to write document to " + file.getAbsolutePath(), e); } } public static void writeTo(Document doc, OutputStream out) { try { TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.transform(new DOMSource(doc), new StreamResult(out)); } catch (Exception e) { throw new RuntimeException("Failed to write document to output stream!", e); } } public static Document parse(byte[] bytes) { return parse(new ByteArrayInputStream(bytes)); } public static Document parse(File signedXmlFile) { try { return parse(new FileInputStream(signedXmlFile)); } catch (Exception e) { throw new RuntimeException("Failed to parse signed file at " + signedXmlFile.getAbsolutePath(), e); } } public static Document parse(InputStream in) { Document doc; try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); doc = dbf.newDocumentBuilder().parse(in); } catch (Exception e) { throw new RuntimeException("Failed to parse input stream", e); } return doc; } }