Java tutorial
/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.common.certification; import java.io.BufferedOutputStream; import java.io.BufferedWriter; 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.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.SignatureException; import java.security.cert.CRL; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.CertificateRevokedException; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import javax.xml.crypto.MarshalException; import javax.xml.crypto.XMLStructure; import javax.xml.crypto.dom.DOMStructure; 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.SignatureProperties; import javax.xml.crypto.dsig.SignatureProperty; import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.XMLObject; 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.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.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; 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.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.poi.poifs.crypt.dsig.KeyInfoKeySelector; import org.apache.xml.security.Init; import org.apache.xml.security.c14n.Canonicalizer; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.roda.core.data.common.RodaConstants; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.itextpdf.text.log.Logger; import com.itextpdf.text.log.LoggerFactory; public final class ODFSignatureUtils { private static final Logger LOGGER = LoggerFactory.getLogger(ODFSignatureUtils.class); private static final String META_INF_DOCUMENTSIGNATURES_XML = "META-INF/documentsignatures.xml"; private static final String OPENOFFICE = "urn:oasis:names:tc:opendocument:xmlns:digitalsignature:1.0"; /** Private empty constructor */ private ODFSignatureUtils() { } public static String runDigitalSignatureVerify(Path input) throws IOException, GeneralSecurityException { String result = "Passed"; ZipFile zipFile = new ZipFile(input.toString()); Enumeration<?> enumeration; for (enumeration = zipFile.entries(); enumeration.hasMoreElements();) { ZipEntry entry = (ZipEntry) enumeration.nextElement(); String entryName = entry.getName(); if (META_INF_DOCUMENTSIGNATURES_XML.equalsIgnoreCase(entryName)) { InputStream zipStream = zipFile.getInputStream(entry); InputSource inputSource = new InputSource(zipStream); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); try { DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Document document = documentBuilder.parse(inputSource); NodeList signatureNodeList = document.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); for (int i = 0; i < signatureNodeList.getLength(); i++) { Node signatureNode = signatureNodeList.item(i); verifyCertificates(input, signatureNode); } } catch (ParserConfigurationException | SAXException e) { result = "Signatures document can not be parsed"; } catch (CertificateExpiredException e) { result = "Contains expired certificates"; } catch (CertificateRevokedException e) { result = "Contains revoked certificates"; } catch (CertificateNotYetValidException e) { result = "Contains certificates not yet valid"; } catch (MarshalException | XMLSignatureException e) { result = "Digital signatures are not valid"; } IOUtils.closeQuietly(zipStream); } } zipFile.close(); return result; } public static List<Path> runDigitalSignatureExtract(Path input) throws SignatureException, IOException { List<Path> paths = new ArrayList<Path>(); ZipFile zipFile = new ZipFile(input.toString()); Enumeration<?> enumeration; for (enumeration = zipFile.entries(); enumeration.hasMoreElements();) { ZipEntry entry = (ZipEntry) enumeration.nextElement(); String entryName = entry.getName(); if (META_INF_DOCUMENTSIGNATURES_XML.equalsIgnoreCase(entryName)) { Path extractedSignature = Files.createTempFile("extraction", ".xml"); InputStream zipStream = zipFile.getInputStream(entry); FileUtils.copyInputStreamToFile(zipStream, extractedSignature.toFile()); paths.add(extractedSignature); IOUtils.closeQuietly(zipStream); } } zipFile.close(); return paths; } public static void runDigitalSignatureStrip(Path input, Path output) throws IOException { OutputStream os = new FileOutputStream(output.toFile()); BufferedOutputStream bos = new BufferedOutputStream(os); ZipOutputStream zout = new ZipOutputStream(bos); ZipFile zipFile = new ZipFile(input.toString()); Enumeration<?> enumeration; for (enumeration = zipFile.entries(); enumeration.hasMoreElements();) { ZipEntry entry = (ZipEntry) enumeration.nextElement(); String entryName = entry.getName(); if (!META_INF_DOCUMENTSIGNATURES_XML.equalsIgnoreCase(entryName) && entry.getSize() > 0) { InputStream zipStream = zipFile.getInputStream(entry); ZipEntry destEntry = new ZipEntry(entryName); zout.putNextEntry(destEntry); byte[] data = new byte[(int) entry.getSize()]; while ((zipStream.read(data, 0, (int) entry.getSize())) != -1) { } zout.write(data); zout.closeEntry(); IOUtils.closeQuietly(zipStream); } } IOUtils.closeQuietly(zout); IOUtils.closeQuietly(bos); IOUtils.closeQuietly(os); zipFile.close(); } public static Path runDigitalSignatureSign(Path input, String ks, String alias, String password, String fileFormat) throws Exception { Security.addProvider(new BouncyCastleProvider()); Path output = Files.createTempFile("odfsigned", "." + fileFormat); KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream storeStream = new FileInputStream(ks); keystore.load(storeStream, password.toCharArray()); X509Certificate certificate = (X509Certificate) keystore.getCertificate(keystore.aliases().nextElement()); Key key = keystore.getKey(alias, password.toCharArray()); IOUtils.closeQuietly(storeStream); ByteArrayInputStream bais = createSignature(input.toString(), certificate, key); File file = output.toFile(); if (file != null) { byte[] buffer = new byte[2048]; int length = 0; FileOutputStream fos = new FileOutputStream(file); while ((length = bais.read(buffer)) >= 0) { fos.write(buffer, 0, length); } IOUtils.closeQuietly(fos); } return output; } public static ByteArrayInputStream createSignature(String inputPath, X509Certificate certificate, Key key) { try { ZipFile zipFile = new ZipFile(new File(inputPath)); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Init.init(); XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM"); DigestMethod digestMethod = factory.newDigestMethod(DigestMethod.SHA1, null); InputStream manifest = zipFile.getInputStream(zipFile.getEntry("META-INF/manifest.xml")); Document docManifest = documentBuilder.parse(manifest); Element rootManifest = docManifest.getDocumentElement(); NodeList listFileEntry = rootManifest.getElementsByTagName("manifest:file-entry"); Document docSignatures; Element rootSignatures; if (zipFile.getEntry(META_INF_DOCUMENTSIGNATURES_XML) != null) { InputStream is = zipFile.getInputStream(zipFile.getEntry(META_INF_DOCUMENTSIGNATURES_XML)); docSignatures = documentBuilder.parse(is); rootSignatures = docSignatures.getDocumentElement(); IOUtils.closeQuietly(is); } else { docSignatures = documentBuilder.newDocument(); rootSignatures = docSignatures.createElement("document-signatures"); rootSignatures.setAttribute("xmlns", OPENOFFICE); docSignatures.appendChild(rootSignatures); Element nodeDocumentSignatures = docManifest.createElement("manifest:file-entry"); nodeDocumentSignatures.setAttribute("manifest:media-type", ""); nodeDocumentSignatures.setAttribute("manifest:full-path", META_INF_DOCUMENTSIGNATURES_XML); rootManifest.appendChild(nodeDocumentSignatures); Element nodeMetaInf = docManifest.createElement("manifest:file-entry"); nodeMetaInf.setAttribute("manifest:media-type", ""); nodeMetaInf.setAttribute("manifest:full-path", "META-INF/"); rootManifest.appendChild(nodeMetaInf); } List<Reference> referenceList = getReferenceList(zipFile, documentBuilder, factory, listFileEntry, digestMethod); digitalSign(factory, referenceList, digestMethod, certificate, docSignatures, rootSignatures, key); ByteArrayOutputStream baos = addSignatureToStream(zipFile, rootManifest, rootSignatures); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); baos.close(); return bais; } catch (Exception e) { LOGGER.debug("ODF signature creation went wrong."); return null; } } private static void writeXML(OutputStream outStream, Node node, boolean indent) throws TransformerFactoryConfigurationError, TransformerException { OutputStreamWriter osw = new OutputStreamWriter(outStream, Charset.forName(RodaConstants.DEFAULT_ENCODING)); BufferedWriter bw = new BufferedWriter(osw); TransformerFactory transformerFactory = new org.apache.xalan.processor.TransformerFactoryImpl(); Transformer serializer = transformerFactory.newTransformer(); serializer.setOutputProperty(OutputKeys.ENCODING, RodaConstants.DEFAULT_ENCODING); if (indent) { serializer.setOutputProperty(OutputKeys.INDENT, "yes"); } DOMSource domSource = new DOMSource(node); StreamResult streamResult = new StreamResult(bw); serializer.transform(domSource, streamResult); IOUtils.closeQuietly(bw); IOUtils.closeQuietly(osw); } private static void verifyCertificates(Path input, Node signatureNode) throws MarshalException, XMLSignatureException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, KeyStoreException { XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance("DOM"); DOMValidateContext domValidateContext = new DOMValidateContext(new KeyInfoKeySelector(), signatureNode); XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); xmlSignature.getSignatureValue().validate(domValidateContext); // xmlSignature.validate(domValidateContext); KeyInfo keyInfo = xmlSignature.getKeyInfo(); Iterator<?> it = keyInfo.getContent().iterator(); List<X509Certificate> certs = new ArrayList<X509Certificate>(); List<CRL> crls = new ArrayList<CRL>(); while (it.hasNext()) { XMLStructure content = (XMLStructure) it.next(); if (content instanceof X509Data) { X509Data certdata = (X509Data) content; Object[] entries = certdata.getContent().toArray(); for (int i = 0; i < entries.length; i++) { if (entries[i] instanceof X509CRL) { X509CRL crl = (X509CRL) entries[i]; crls.add(crl); } if (entries[i] instanceof X509Certificate) { X509Certificate cert = (X509Certificate) entries[i]; cert.checkValidity(); certs.add(cert); } } } } for (CRL c : crls) { for (X509Certificate cert : certs) { if (c.isRevoked(cert)) throw new CertificateRevokedException(null, null, null, null); } } } private static List<XMLObject> getXMLObjectList(XMLSignatureFactory factory, Document docSignatures, String signatureId, String signaturePropertyId) { Element content = docSignatures.createElement("dc:date"); content.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss,SS"); content.setTextContent(sdf.format(new Date())); XMLStructure str = new DOMStructure(content); List<XMLStructure> contentList = new ArrayList<XMLStructure>(); contentList.add(str); SignatureProperty sp = factory.newSignatureProperty(contentList, "#" + signatureId, signaturePropertyId); List<SignatureProperty> spList = new ArrayList<SignatureProperty>(); spList.add(sp); SignatureProperties sps = factory.newSignatureProperties(spList, null); List<SignatureProperties> spsList = new ArrayList<SignatureProperties>(); spsList.add(sps); XMLObject object = factory.newXMLObject(spsList, null, null, null); List<XMLObject> objectList = new ArrayList<XMLObject>(); objectList.add(object); return objectList; } private static KeyInfo getKeyInfo(XMLSignatureFactory factory, X509Certificate certificate) { KeyInfoFactory kif = factory.getKeyInfoFactory(); List<Object> x509Content = new ArrayList<Object>(); x509Content.add(certificate.getSubjectX500Principal().getName()); x509Content.add(certificate); X509Data cerData = kif.newX509Data(x509Content); KeyInfo ki = kif.newKeyInfo(Collections.singletonList(cerData), null); return ki; } private static ByteArrayOutputStream addSignatureToStream(ZipFile zipFile, Element rootManifest, Element rootSignatures) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos); Enumeration<?> enumeration; for (enumeration = zipFile.entries(); enumeration.hasMoreElements();) { ZipEntry zeOut = (ZipEntry) enumeration.nextElement(); String fileName = zeOut.getName(); if (!fileName.equals(META_INF_DOCUMENTSIGNATURES_XML) && !fileName.equals("META-INF/manifest.xml")) { zos.putNextEntry(zeOut); InputStream is = zipFile.getInputStream(zipFile.getEntry(fileName)); zos.write(IOUtils.toByteArray(is)); zos.closeEntry(); IOUtils.closeQuietly(is); } } ZipEntry zeDocumentSignatures = new ZipEntry(META_INF_DOCUMENTSIGNATURES_XML); zos.putNextEntry(zeDocumentSignatures); ByteArrayOutputStream baosXML = new ByteArrayOutputStream(); writeXML(baosXML, rootSignatures, false); zos.write(baosXML.toByteArray()); zos.closeEntry(); baosXML.close(); ZipEntry zeManifest = new ZipEntry("META-INF/manifest.xml"); zos.putNextEntry(zeManifest); ByteArrayOutputStream baosManifest = new ByteArrayOutputStream(); writeXML(baosManifest, rootManifest, false); zos.write(baosManifest.toByteArray()); zos.closeEntry(); baosManifest.close(); zos.close(); zipFile.close(); return baos; } private static List<Reference> getReferenceList(ZipFile zipFile, DocumentBuilder documentBuilder, XMLSignatureFactory factory, NodeList listFileEntry, DigestMethod digestMethod) throws Exception { Transform transform = factory.newTransform(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS, (TransformParameterSpec) null); List<Transform> transformList = new ArrayList<Transform>(); transformList.add(transform); MessageDigest messageDigest = MessageDigest.getInstance(RodaConstants.SHA1); List<Reference> referenceList = new ArrayList<Reference>(); for (int i = 0; i < listFileEntry.getLength(); i++) { String fullPath = ((Element) listFileEntry.item(i)).getAttribute("manifest:full-path"); Reference reference; if (!fullPath.endsWith("/") && !fullPath.equals(META_INF_DOCUMENTSIGNATURES_XML)) { if (fullPath.equals("content.xml") || fullPath.equals("meta.xml") || fullPath.equals("styles.xml") || fullPath.equals("settings.xml")) { InputStream xmlFile = zipFile.getInputStream(zipFile.getEntry(fullPath)); Element root = documentBuilder.parse(xmlFile).getDocumentElement(); IOUtils.closeQuietly(xmlFile); Canonicalizer canonicalizer = Canonicalizer .getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS); byte[] docCanonicalize = canonicalizer.canonicalizeSubtree(root); byte[] digestValue = messageDigest.digest(docCanonicalize); reference = factory.newReference(fullPath.replaceAll(" ", "%20"), digestMethod, transformList, null, null, digestValue); } else { InputStream is = zipFile.getInputStream(zipFile.getEntry(fullPath)); byte[] digestValue = messageDigest.digest(IOUtils.toByteArray(is)); IOUtils.closeQuietly(is); reference = factory.newReference(fullPath.replaceAll(" ", "%20"), digestMethod, null, null, null, digestValue); } referenceList.add(reference); } } return referenceList; } private static void digitalSign(XMLSignatureFactory factory, List<Reference> referenceList, DigestMethod digestMethod, X509Certificate certificate, Document docSignatures, Element rootSignatures, Key key) throws MarshalException, XMLSignatureException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { String signatureId = UUID.randomUUID().toString(); String signaturePropertyId = UUID.randomUUID().toString(); CanonicalizationMethod canMethod = factory.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null); SignatureMethod signMethod = factory.newSignatureMethod(SignatureMethod.RSA_SHA1, null); Reference signaturePropertyReference = factory.newReference("#" + signaturePropertyId, digestMethod); referenceList.add(signaturePropertyReference); SignedInfo si = factory.newSignedInfo(canMethod, signMethod, referenceList); KeyInfo ki = getKeyInfo(factory, certificate); List<XMLObject> objectList = getXMLObjectList(factory, docSignatures, signatureId, signaturePropertyId); XMLSignature signature = factory.newXMLSignature(si, ki, objectList, signatureId, null); DOMSignContext signContext = new DOMSignContext(key, rootSignatures); signature.sign(signContext); } }