Java tutorial
/* * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl-2.1.html * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * Contributors: * Wojciech Sulejman * Florent Guillaume * Vladimir Pasquier <vpasquier@nuxeo.com> */ package org.nuxeo.ecm.platform.signature.core.sign; import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.SIGNED_CURRENT; import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.SIGNED_OTHER; import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.UNSIGNABLE; import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.UNSIGNED; import java.awt.Color; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.Serializable; import java.security.KeyPair; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.Blobs; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.ListDiff; import org.nuxeo.ecm.core.api.blobholder.BlobHolder; import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder; import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolder; import org.nuxeo.ecm.core.convert.api.ConversionException; import org.nuxeo.ecm.core.convert.api.ConversionService; import org.nuxeo.ecm.platform.signature.api.exception.AlreadySignedException; import org.nuxeo.ecm.platform.signature.api.exception.CertException; import org.nuxeo.ecm.platform.signature.api.exception.SignException; import org.nuxeo.ecm.platform.signature.api.pki.CertService; import org.nuxeo.ecm.platform.signature.api.sign.SignatureService; import org.nuxeo.ecm.platform.signature.api.user.AliasType; import org.nuxeo.ecm.platform.signature.api.user.AliasWrapper; import org.nuxeo.ecm.platform.signature.api.user.CUserService; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; import com.lowagie.text.DocumentException; import com.lowagie.text.Font; import com.lowagie.text.FontFactory; import com.lowagie.text.Rectangle; import com.lowagie.text.pdf.AcroFields; import com.lowagie.text.pdf.PdfPKCS7; import com.lowagie.text.pdf.PdfReader; import com.lowagie.text.pdf.PdfSignatureAppearance; import com.lowagie.text.pdf.PdfStamper; /** * Base implementation for the signature service (also a Nuxeo component). * <p> * The main document is signed. If it's not already a PDF, then a PDF conversion is done. * <p> * Once signed, it can replace the main document or be stored as the first attachment. If replacing the main document, * an archive of the original can be kept. * <p> * <ul> * <li> */ public class SignatureServiceImpl extends DefaultComponent implements SignatureService { private static final Log log = LogFactory.getLog(SignatureServiceImpl.class); protected static final int SIGNATURE_FIELD_HEIGHT = 50; protected static final int SIGNATURE_FIELD_WIDTH = 150; protected static final int SIGNATURE_MARGIN = 10; protected static final int PAGE_TO_SIGN = 1; protected static final String XP_SIGNATURE = "signature"; protected static final String ALREADY_SIGNED_BY = "This document has already been signed by "; protected static final String MIME_TYPE_PDF = "application/pdf"; /** From JODBasedConverter */ protected static final String PDFA1_PARAM = "PDF/A-1"; protected static final String FILE_CONTENT = "file:content"; protected static final String FILES_FILES = "files:files"; protected static final String FILES_FILE = "file"; protected static final String FILES_FILENAME = "filename"; protected static final String USER_EMAIL = "user:email"; protected final Map<String, SignatureDescriptor> signatureRegistryMap; public SignatureServiceImpl() { signatureRegistryMap = new HashMap<String, SignatureDescriptor>(); } @Override public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (XP_SIGNATURE.equals(extensionPoint)) { SignatureDescriptor signatureDescriptor = (SignatureDescriptor) contribution; if (!signatureDescriptor.getRemoveExtension()) { signatureRegistryMap.put(signatureDescriptor.getId(), signatureDescriptor); } } } @Override public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (XP_SIGNATURE.equals(extensionPoint)) { SignatureDescriptor signatureDescriptor = (SignatureDescriptor) contribution; if (!signatureDescriptor.getRemoveExtension()) { signatureRegistryMap.remove(signatureDescriptor.getId()); } } } // // ----- SignatureService ----- // @Override public StatusWithBlob getSigningStatus(DocumentModel doc, DocumentModel user) throws ClientException { if (doc == null) { return new StatusWithBlob(UNSIGNABLE, null, null, null); } StatusWithBlob blobAndStatus = getSignedPdfBlobAndStatus(doc, user); if (blobAndStatus != null) { return blobAndStatus; } BlobHolder mbh = doc.getAdapter(BlobHolder.class); Blob blob; if (mbh == null || (blob = mbh.getBlob()) == null) { return new StatusWithBlob(UNSIGNABLE, null, null, null); } return new StatusWithBlob(UNSIGNED, blob, mbh, FILE_CONTENT); } protected int getSigningStatus(Blob pdfBlob, DocumentModel user) throws ClientException { if (pdfBlob == null) { return UNSIGNED; } List<X509Certificate> certificates = getCertificates(pdfBlob); if (certificates.isEmpty()) { return UNSIGNED; } if (user == null) { return SIGNED_OTHER; } String email = (String) user.getPropertyValue(USER_EMAIL); if (StringUtils.isEmpty(email)) { return SIGNED_OTHER; } CertService certService = Framework.getLocalService(CertService.class); for (X509Certificate certificate : certificates) { String certEmail; try { certEmail = certService.getCertificateEmail(certificate); } catch (CertException e) { continue; } if (email.equals(certEmail)) { return SIGNED_CURRENT; } } return SIGNED_OTHER; } /** * Finds the first signed PDF blob. */ protected StatusWithBlob getSignedPdfBlobAndStatus(DocumentModel doc, DocumentModel user) throws ClientException { BlobHolder mbh = doc.getAdapter(BlobHolder.class); if (mbh != null) { Blob blob = mbh.getBlob(); if (blob != null && MIME_TYPE_PDF.equals(blob.getMimeType())) { int status = getSigningStatus(blob, user); if (status != UNSIGNED) { // TODO for File document it works, but for general // blob holders the path may be incorrect return new StatusWithBlob(status, blob, mbh, FILE_CONTENT); } } } @SuppressWarnings("unchecked") List<Map<String, Serializable>> files = (List<Map<String, Serializable>>) doc.getPropertyValue(FILES_FILES); int i = -1; for (Map<String, Serializable> map : files) { i++; Blob blob = (Blob) map.get(FILES_FILE); if (blob != null && MIME_TYPE_PDF.equals(blob.getMimeType())) { int status = getSigningStatus(blob, user); if (status != UNSIGNED) { String pathbase = FILES_FILES + "/" + i + "/"; String path = pathbase + FILES_FILE; BlobHolder bh = new DocumentBlobHolder(doc, path, pathbase + FILES_FILENAME); return new StatusWithBlob(status, blob, bh, path); } } } return null; } @Override public Blob signDocument(DocumentModel doc, DocumentModel user, String keyPassword, String reason, boolean pdfa, SigningDisposition disposition, String archiveFilename) throws ClientException { StatusWithBlob blobAndStatus = getSignedPdfBlobAndStatus(doc, user); if (blobAndStatus != null) { // re-sign it Blob signedBlob = signPDF(blobAndStatus.blob, user, keyPassword, reason); signedBlob.setFilename(blobAndStatus.blob.getFilename()); // replace the previous blob with a new one blobAndStatus.blobHolder.setBlob(signedBlob); return signedBlob; } Blob originalBlob; BlobHolder mbh = doc.getAdapter(BlobHolder.class); if (mbh == null || (originalBlob = mbh.getBlob()) == null) { return null; } Blob pdfBlob; if (MIME_TYPE_PDF.equals(originalBlob.getMimeType())) { pdfBlob = originalBlob; } else { // convert to PDF or PDF/A first ConversionService conversionService = Framework.getLocalService(ConversionService.class); Map<String, Serializable> parameters = new HashMap<String, Serializable>(); if (pdfa) { parameters.put(PDFA1_PARAM, Boolean.TRUE); } try { BlobHolder holder = conversionService.convert("any2pdf", new SimpleBlobHolder(originalBlob), parameters); pdfBlob = holder.getBlob(); } catch (ConversionException conversionException) { throw new SignException(conversionException); } } Blob signedBlob = signPDF(pdfBlob, user, keyPassword, reason); signedBlob.setFilename(FilenameUtils.getBaseName(originalBlob.getFilename()) + ".pdf"); Map<String, Serializable> map; ListDiff listDiff; switch (disposition) { case REPLACE: // replace main blob mbh.setBlob(signedBlob); break; case ARCHIVE: // archive as attachment originalBlob.setFilename(archiveFilename); map = new HashMap<String, Serializable>(); map.put(FILES_FILE, (Serializable) originalBlob); map.put(FILES_FILENAME, originalBlob.getFilename()); listDiff = new ListDiff(); listDiff.add(map); doc.setPropertyValue(FILES_FILES, listDiff); // and replace main blob mbh.setBlob(signedBlob); break; case ATTACH: // set as first attachment map = new HashMap<String, Serializable>(); map.put(FILES_FILE, (Serializable) signedBlob); map.put(FILES_FILENAME, signedBlob.getFilename()); listDiff = new ListDiff(); listDiff.insert(0, map); doc.setPropertyValue(FILES_FILES, listDiff); break; } return signedBlob; } @Override public Blob signPDF(Blob pdfBlob, DocumentModel user, String keyPassword, String reason) throws ClientException { CertService certService = Framework.getLocalService(CertService.class); CUserService cUserService = Framework.getLocalService(CUserService.class); try { File outputFile = File.createTempFile("signed-", ".pdf"); Blob blob = Blobs.createBlob(outputFile, MIME_TYPE_PDF); Framework.trackFile(outputFile, blob); PdfReader pdfReader = new PdfReader(pdfBlob.getStream()); List<X509Certificate> pdfCertificates = getCertificates(pdfReader); // allows for multiple signatures PdfStamper pdfStamper = PdfStamper.createSignature(pdfReader, new FileOutputStream(outputFile), '\0', null, true); PdfSignatureAppearance pdfSignatureAppearance = pdfStamper.getSignatureAppearance(); String userID = (String) user.getPropertyValue("user:username"); AliasWrapper alias = new AliasWrapper(userID); KeyStore keystore = cUserService.getUserKeystore(userID, keyPassword); Certificate certificate = certService.getCertificate(keystore, alias.getId(AliasType.CERT)); KeyPair keyPair = certService.getKeyPair(keystore, alias.getId(AliasType.KEY), alias.getId(AliasType.CERT), keyPassword); if (certificatePresentInPDF(certificate, pdfCertificates)) { X509Certificate userX509Certificate = (X509Certificate) certificate; String message = ALREADY_SIGNED_BY + userX509Certificate.getSubjectDN(); log.debug(message); throw new AlreadySignedException(message); } List<Certificate> certificates = new ArrayList<Certificate>(); certificates.add(certificate); Certificate[] certChain = certificates.toArray(new Certificate[0]); pdfSignatureAppearance.setCrypto(keyPair.getPrivate(), certChain, null, PdfSignatureAppearance.SELF_SIGNED); if (StringUtils.isBlank(reason)) { reason = getSigningReason(); } pdfSignatureAppearance.setReason(reason); pdfSignatureAppearance.setAcro6Layers(true); Font layer2Font = FontFactory.getFont(FontFactory.TIMES, getSignatureLayout().getTextSize(), Font.NORMAL, new Color(0x00, 0x00, 0x00)); pdfSignatureAppearance.setLayer2Font(layer2Font); pdfSignatureAppearance.setRender(PdfSignatureAppearance.SignatureRenderDescription); pdfSignatureAppearance.setVisibleSignature(getNextCertificatePosition(pdfReader, pdfCertificates), 1, null); pdfStamper.close(); // closes the file log.debug("File " + outputFile.getAbsolutePath() + " created and signed with " + reason); return blob; } catch (IOException e) { throw new SignException(e); } catch (DocumentException e) { // iText PDF stamping throw new SignException(e); } catch (IllegalArgumentException e) { if (String.valueOf(e.getMessage()).contains("PdfReader not opened with owner password")) { // iText PDF reading throw new SignException("PDF is password-protected"); } throw new SignException(e); } } /** * @since 5.8 * @return the signature layout. Default one if no contribution. */ protected SignatureDescriptor.SignatureLayout getSignatureLayout() { for (SignatureDescriptor signatureDescriptor : signatureRegistryMap.values()) { SignatureDescriptor.SignatureLayout signatureLayout = signatureDescriptor.getSignatureLayout(); if (signatureLayout != null) { return signatureLayout; } } return new SignatureDescriptor.SignatureLayout(); } protected String getSigningReason() throws SignException { for (SignatureDescriptor sd : signatureRegistryMap.values()) { String reason = sd.getReason(); if (!StringUtils.isBlank(reason)) { return reason; } } throw new SignException("No default signing reason provided in configuration"); } protected boolean certificatePresentInPDF(Certificate userCert, List<X509Certificate> pdfCertificates) throws SignException { X509Certificate xUserCert = (X509Certificate) userCert; for (X509Certificate xcert : pdfCertificates) { // matching certificate found if (xcert.getSubjectX500Principal().equals(xUserCert.getSubjectX500Principal())) { return true; } } return false; } /** * @since 5.8 Provides the position rectangle for the next certificate. An assumption is made that all previous * certificates in a given PDF were placed using the same technique and settings. New certificates are added * depending of signature layout contributed. */ protected Rectangle getNextCertificatePosition(PdfReader pdfReader, List<X509Certificate> pdfCertificates) throws SignException { int numberOfSignatures = pdfCertificates.size(); Rectangle pageSize = pdfReader.getPageSize(PAGE_TO_SIGN); // PDF size float width = pageSize.getWidth(); float height = pageSize.getHeight(); // Signature size float rectangleWidth = width / getSignatureLayout().getColumns(); float rectangeHeight = height / getSignatureLayout().getLines(); // Signature location int column = numberOfSignatures % getSignatureLayout().getColumns() + getSignatureLayout().getStartColumn(); int line = numberOfSignatures / getSignatureLayout().getColumns() + getSignatureLayout().getStartLine(); if (column > getSignatureLayout().getColumns()) { column = column % getSignatureLayout().getColumns(); line++; } // Skip rectangle display If number of signatures exceed free locations // on pdf layout if (line > getSignatureLayout().getLines()) { return new Rectangle(0, 0, 0, 0); } // make smaller by page margin float topRightX = rectangleWidth * column; float bottomLeftY = height - rectangeHeight * line; float bottomLeftX = topRightX - SIGNATURE_FIELD_WIDTH; float topRightY = bottomLeftY + SIGNATURE_FIELD_HEIGHT; // verify current position coordinates in case they were // misconfigured validatePageBounds(pdfReader, 1, bottomLeftX, true); validatePageBounds(pdfReader, 1, bottomLeftY, false); validatePageBounds(pdfReader, 1, topRightX, true); validatePageBounds(pdfReader, 1, topRightY, false); Rectangle positionRectangle = new Rectangle(bottomLeftX, bottomLeftY, topRightX, topRightY); return positionRectangle; } /** * Verifies that a provided value fits within the page bounds. If it does not, a sign exception is thrown. This is * to verify externally configurable signature positioning. * * @param isHorizontal - if false, the current value is checked agains the vertical page dimension */ protected void validatePageBounds(PdfReader pdfReader, int pageNo, float valueToCheck, boolean isHorizontal) throws SignException { if (valueToCheck < 0) { String message = "The new signature position " + valueToCheck + " exceeds the page bounds. The position must be a positive number."; log.debug(message); throw new SignException(message); } Rectangle pageRectangle = pdfReader.getPageSize(pageNo); if (isHorizontal && valueToCheck > pageRectangle.getRight()) { String message = "The new signature position " + valueToCheck + " exceeds the horizontal page bounds. The page dimensions are: (" + pageRectangle + ")."; log.debug(message); throw new SignException(message); } if (!isHorizontal && valueToCheck > pageRectangle.getTop()) { String message = "The new signature position " + valueToCheck + " exceeds the vertical page bounds. The page dimensions are: (" + pageRectangle + ")."; log.debug(message); throw new SignException(message); } } @Override public List<X509Certificate> getCertificates(DocumentModel doc) throws ClientException { StatusWithBlob signedBlob = getSignedPdfBlobAndStatus(doc, null); if (signedBlob == null) { return Collections.emptyList(); } return getCertificates(signedBlob.blob); } protected List<X509Certificate> getCertificates(Blob pdfBlob) throws SignException { try { PdfReader pdfReader = new PdfReader(pdfBlob.getStream()); return getCertificates(pdfReader); } catch (IOException e) { String message = ""; if (e.getMessage().equals("PDF header signature not found.")) { message = "PDF seems to be corrupted"; } throw new SignException(message, e); } } protected List<X509Certificate> getCertificates(PdfReader pdfReader) throws SignException { List<X509Certificate> pdfCertificates = new ArrayList<X509Certificate>(); AcroFields acroFields = pdfReader.getAcroFields(); @SuppressWarnings("unchecked") List<String> signatureNames = acroFields.getSignatureNames(); for (String signatureName : signatureNames) { PdfPKCS7 pdfPKCS7 = acroFields.verifySignature(signatureName); X509Certificate signingCertificate = pdfPKCS7.getSigningCertificate(); pdfCertificates.add(signingCertificate); } return pdfCertificates; } }