001/* 002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Wojciech Sulejman 016 * Florent Guillaume 017 * Vladimir Pasquier <vpasquier@nuxeo.com> 018 */ 019 020package org.nuxeo.ecm.platform.signature.core.sign; 021 022import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.SIGNED_CURRENT; 023import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.SIGNED_OTHER; 024import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.UNSIGNABLE; 025import static org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob.UNSIGNED; 026 027import java.io.File; 028import java.io.FileOutputStream; 029import java.io.IOException; 030import java.io.Serializable; 031import java.security.KeyPair; 032import java.security.KeyStore; 033import java.security.cert.Certificate; 034import java.security.cert.X509Certificate; 035import java.util.ArrayList; 036import java.util.Collections; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040 041import org.apache.commons.io.FilenameUtils; 042import org.apache.commons.lang.StringUtils; 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.ecm.core.api.Blob; 046import org.nuxeo.ecm.core.api.Blobs; 047import org.nuxeo.ecm.core.api.DocumentModel; 048import org.nuxeo.ecm.core.api.ListDiff; 049import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 050import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder; 051import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolder; 052import org.nuxeo.ecm.core.convert.api.ConversionException; 053import org.nuxeo.ecm.core.convert.api.ConversionService; 054import org.nuxeo.ecm.platform.signature.api.exception.AlreadySignedException; 055import org.nuxeo.ecm.platform.signature.api.exception.CertException; 056import org.nuxeo.ecm.platform.signature.api.exception.SignException; 057import org.nuxeo.ecm.platform.signature.api.pki.CertService; 058import org.nuxeo.ecm.platform.signature.api.sign.SignatureAppearanceFactory; 059import org.nuxeo.ecm.platform.signature.api.sign.SignatureLayout; 060import org.nuxeo.ecm.platform.signature.api.sign.SignatureService; 061import org.nuxeo.ecm.platform.signature.api.user.AliasType; 062import org.nuxeo.ecm.platform.signature.api.user.AliasWrapper; 063import org.nuxeo.ecm.platform.signature.api.user.CUserService; 064import org.nuxeo.runtime.api.Framework; 065import org.nuxeo.runtime.model.ComponentInstance; 066import org.nuxeo.runtime.model.DefaultComponent; 067 068import com.lowagie.text.DocumentException; 069import com.lowagie.text.Rectangle; 070import com.lowagie.text.pdf.AcroFields; 071import com.lowagie.text.pdf.PdfPKCS7; 072import com.lowagie.text.pdf.PdfReader; 073import com.lowagie.text.pdf.PdfSignatureAppearance; 074import com.lowagie.text.pdf.PdfStamper; 075 076/** 077 * Base implementation for the signature service (also a Nuxeo component). 078 * <p> 079 * The main document is signed. If it's not already a PDF, then a PDF conversion is done. 080 * <p> 081 * Once signed, it can replace the main document or be stored as the first attachment. If replacing the main document, 082 * an archive of the original can be kept. 083 * <p> 084 * <ul> 085 * <li> 086 */ 087public class SignatureServiceImpl extends DefaultComponent implements SignatureService { 088 089 private static final Log log = LogFactory.getLog(SignatureServiceImpl.class); 090 091 protected static final int SIGNATURE_FIELD_HEIGHT = 50; 092 093 protected static final int SIGNATURE_FIELD_WIDTH = 150; 094 095 protected static final int SIGNATURE_MARGIN = 10; 096 097 protected static final int PAGE_TO_SIGN = 1; 098 099 protected static final String XP_SIGNATURE = "signature"; 100 101 protected static final String ALREADY_SIGNED_BY = "This document has already been signed by "; 102 103 protected static final String MIME_TYPE_PDF = "application/pdf"; 104 105 /** From JODBasedConverter */ 106 protected static final String PDFA1_PARAM = "PDF/A-1"; 107 108 protected static final String FILE_CONTENT = "file:content"; 109 110 protected static final String FILES_FILES = "files:files"; 111 112 protected static final String FILES_FILE = "file"; 113 114 protected static final String FILES_FILENAME = "filename"; 115 116 protected static final String USER_EMAIL = "user:email"; 117 118 protected final Map<String, SignatureDescriptor> signatureRegistryMap; 119 120 public SignatureServiceImpl() { 121 signatureRegistryMap = new HashMap<String, SignatureDescriptor>(); 122 } 123 124 @Override 125 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 126 if (XP_SIGNATURE.equals(extensionPoint)) { 127 SignatureDescriptor signatureDescriptor = (SignatureDescriptor) contribution; 128 if (!signatureDescriptor.getRemoveExtension()) { 129 signatureRegistryMap.put(signatureDescriptor.getId(), signatureDescriptor); 130 } else { 131 signatureRegistryMap.remove(signatureDescriptor.getId()); 132 } 133 } 134 } 135 136 @Override 137 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 138 if (XP_SIGNATURE.equals(extensionPoint)) { 139 SignatureDescriptor signatureDescriptor = (SignatureDescriptor) contribution; 140 if (!signatureDescriptor.getRemoveExtension()) { 141 signatureRegistryMap.remove(signatureDescriptor.getId()); 142 } 143 } 144 } 145 146 // 147 // ----- SignatureService ----- 148 // 149 150 @Override 151 public StatusWithBlob getSigningStatus(DocumentModel doc, DocumentModel user) { 152 if (doc == null) { 153 return new StatusWithBlob(UNSIGNABLE, null, null, null); 154 } 155 StatusWithBlob blobAndStatus = getSignedPdfBlobAndStatus(doc, user); 156 if (blobAndStatus != null) { 157 return blobAndStatus; 158 } 159 BlobHolder mbh = doc.getAdapter(BlobHolder.class); 160 Blob blob; 161 if (mbh == null || (blob = mbh.getBlob()) == null) { 162 return new StatusWithBlob(UNSIGNABLE, null, null, null); 163 } 164 return new StatusWithBlob(UNSIGNED, blob, mbh, FILE_CONTENT); 165 } 166 167 protected int getSigningStatus(Blob pdfBlob, DocumentModel user) { 168 if (pdfBlob == null) { 169 return UNSIGNED; 170 } 171 List<X509Certificate> certificates = getCertificates(pdfBlob); 172 if (certificates.isEmpty()) { 173 return UNSIGNED; 174 } 175 if (user == null) { 176 return SIGNED_OTHER; 177 } 178 String email = (String) user.getPropertyValue(USER_EMAIL); 179 if (StringUtils.isEmpty(email)) { 180 return SIGNED_OTHER; 181 } 182 CertService certService = Framework.getLocalService(CertService.class); 183 for (X509Certificate certificate : certificates) { 184 String certEmail; 185 try { 186 certEmail = certService.getCertificateEmail(certificate); 187 } catch (CertException e) { 188 continue; 189 } 190 if (email.equals(certEmail)) { 191 return SIGNED_CURRENT; 192 } 193 } 194 return SIGNED_OTHER; 195 } 196 197 /** 198 * Finds the first signed PDF blob. 199 */ 200 protected StatusWithBlob getSignedPdfBlobAndStatus(DocumentModel doc, DocumentModel user) { 201 BlobHolder mbh = doc.getAdapter(BlobHolder.class); 202 if (mbh != null) { 203 Blob blob = mbh.getBlob(); 204 if (blob != null && MIME_TYPE_PDF.equals(blob.getMimeType())) { 205 int status = getSigningStatus(blob, user); 206 if (status != UNSIGNED) { 207 // TODO for File document it works, but for general 208 // blob holders the path may be incorrect 209 return new StatusWithBlob(status, blob, mbh, FILE_CONTENT); 210 } 211 } 212 } 213 @SuppressWarnings("unchecked") 214 List<Map<String, Serializable>> files = (List<Map<String, Serializable>>) doc.getPropertyValue(FILES_FILES); 215 int i = -1; 216 for (Map<String, Serializable> map : files) { 217 i++; 218 Blob blob = (Blob) map.get(FILES_FILE); 219 if (blob != null && MIME_TYPE_PDF.equals(blob.getMimeType())) { 220 int status = getSigningStatus(blob, user); 221 if (status != UNSIGNED) { 222 String pathbase = FILES_FILES + "/" + i + "/"; 223 String path = pathbase + FILES_FILE; 224 BlobHolder bh = new DocumentBlobHolder(doc, path, pathbase + FILES_FILENAME); 225 return new StatusWithBlob(status, blob, bh, path); 226 } 227 } 228 } 229 return null; 230 } 231 232 @Override 233 public Blob signDocument(DocumentModel doc, DocumentModel user, String keyPassword, String reason, boolean pdfa, 234 SigningDisposition disposition, String archiveFilename) { 235 236 StatusWithBlob blobAndStatus = getSignedPdfBlobAndStatus(doc, user); 237 if (blobAndStatus != null) { 238 // re-sign it 239 Blob signedBlob = signPDF(blobAndStatus.blob, doc, user, keyPassword, reason); 240 signedBlob.setFilename(blobAndStatus.blob.getFilename()); 241 // replace the previous blob with a new one 242 blobAndStatus.blobHolder.setBlob(signedBlob); 243 return signedBlob; 244 } 245 246 Blob originalBlob; 247 BlobHolder mbh = doc.getAdapter(BlobHolder.class); 248 if (mbh == null || (originalBlob = mbh.getBlob()) == null) { 249 return null; 250 } 251 252 Blob pdfBlob; 253 if (MIME_TYPE_PDF.equals(originalBlob.getMimeType())) { 254 pdfBlob = originalBlob; 255 } else { 256 // convert to PDF or PDF/A first 257 ConversionService conversionService = Framework.getLocalService(ConversionService.class); 258 Map<String, Serializable> parameters = new HashMap<String, Serializable>(); 259 if (pdfa) { 260 parameters.put(PDFA1_PARAM, Boolean.TRUE); 261 } 262 try { 263 BlobHolder holder = conversionService.convert("any2pdf", new SimpleBlobHolder(originalBlob), parameters); 264 pdfBlob = holder.getBlob(); 265 } catch (ConversionException conversionException) { 266 throw new SignException(conversionException); 267 } 268 } 269 270 Blob signedBlob = signPDF(pdfBlob, doc, user, keyPassword, reason); 271 signedBlob.setFilename(FilenameUtils.getBaseName(originalBlob.getFilename()) + ".pdf"); 272 273 Map<String, Serializable> map; 274 ListDiff listDiff; 275 switch (disposition) { 276 case REPLACE: 277 // replace main blob 278 mbh.setBlob(signedBlob); 279 break; 280 case ARCHIVE: 281 // archive as attachment 282 originalBlob.setFilename(archiveFilename); 283 map = new HashMap<String, Serializable>(); 284 map.put(FILES_FILE, (Serializable) originalBlob); 285 map.put(FILES_FILENAME, originalBlob.getFilename()); 286 listDiff = new ListDiff(); 287 listDiff.add(map); 288 doc.setPropertyValue(FILES_FILES, listDiff); 289 // and replace main blob 290 mbh.setBlob(signedBlob); 291 break; 292 case ATTACH: 293 // set as first attachment 294 map = new HashMap<String, Serializable>(); 295 map.put(FILES_FILE, (Serializable) signedBlob); 296 map.put(FILES_FILENAME, signedBlob.getFilename()); 297 listDiff = new ListDiff(); 298 listDiff.insert(0, map); 299 doc.setPropertyValue(FILES_FILES, listDiff); 300 break; 301 } 302 303 return signedBlob; 304 } 305 306 @Override 307 public Blob signPDF(Blob pdfBlob, DocumentModel doc, DocumentModel user, String keyPassword, String reason) { 308 CertService certService = Framework.getLocalService(CertService.class); 309 CUserService cUserService = Framework.getLocalService(CUserService.class); 310 try { 311 File outputFile = File.createTempFile("signed-", ".pdf"); 312 Blob blob = Blobs.createBlob(outputFile, MIME_TYPE_PDF); 313 Framework.trackFile(outputFile, blob); 314 315 PdfReader pdfReader = new PdfReader(pdfBlob.getStream()); 316 List<X509Certificate> pdfCertificates = getCertificates(pdfReader); 317 318 // allows for multiple signatures 319 PdfStamper pdfStamper = PdfStamper.createSignature(pdfReader, new FileOutputStream(outputFile), '\0', null, 320 true); 321 322 String userID = (String) user.getPropertyValue("user:username"); 323 AliasWrapper alias = new AliasWrapper(userID); 324 KeyStore keystore = cUserService.getUserKeystore(userID, keyPassword); 325 Certificate certificate = certService.getCertificate(keystore, alias.getId(AliasType.CERT)); 326 KeyPair keyPair = certService.getKeyPair(keystore, alias.getId(AliasType.KEY), alias.getId(AliasType.CERT), 327 keyPassword); 328 329 if (certificatePresentInPDF(certificate, pdfCertificates)) { 330 X509Certificate userX509Certificate = (X509Certificate) certificate; 331 String message = ALREADY_SIGNED_BY + userX509Certificate.getSubjectDN(); 332 log.debug(message); 333 throw new AlreadySignedException(message); 334 } 335 336 List<Certificate> certificates = new ArrayList<Certificate>(); 337 certificates.add(certificate); 338 339 Certificate[] certChain = certificates.toArray(new Certificate[0]); 340 PdfSignatureAppearance pdfSignatureAppearance = pdfStamper.getSignatureAppearance(); 341 pdfSignatureAppearance.setCrypto(keyPair.getPrivate(), certChain, null, PdfSignatureAppearance.SELF_SIGNED); 342 if (StringUtils.isBlank(reason)) { 343 reason = getSigningReason(); 344 } 345 pdfSignatureAppearance.setVisibleSignature(getNextCertificatePosition(pdfReader, pdfCertificates), 1, null); 346 getSignatureAppearanceFactory().format(pdfSignatureAppearance, doc, userID, reason); 347 348 pdfStamper.close(); // closes the file 349 350 log.debug("File " + outputFile.getAbsolutePath() + " created and signed with " + reason); 351 352 return blob; 353 } catch (IOException e) { 354 throw new SignException(e); 355 } catch (DocumentException e) { 356 // iText PDF stamping 357 throw new SignException(e); 358 } catch (IllegalArgumentException e) { 359 if (String.valueOf(e.getMessage()).contains("PdfReader not opened with owner password")) { 360 // iText PDF reading 361 throw new SignException("PDF is password-protected"); 362 } 363 throw new SignException(e); 364 } catch (InstantiationException e) { 365 throw new SignException(e); 366 } catch (IllegalAccessException e) { 367 throw new SignException(e); 368 } 369 } 370 371 /** 372 * @since 5.8 373 * @return the signature layout. Default one if no contribution. 374 */ 375 @Override 376 public SignatureLayout getSignatureLayout() { 377 for (SignatureDescriptor signatureDescriptor : signatureRegistryMap.values()) { 378 SignatureLayout signatureLayout = signatureDescriptor.getSignatureLayout(); 379 if (signatureLayout != null) { 380 return signatureLayout; 381 } 382 } 383 return new SignatureDescriptor.SignatureLayout(); 384 } 385 386 protected SignatureAppearanceFactory getSignatureAppearanceFactory() throws InstantiationException, IllegalAccessException { 387 for (SignatureDescriptor signatureDescriptor : signatureRegistryMap.values()) { 388 return signatureDescriptor.getAppearanceFatory(); 389 } 390 return new DefaultSignatureAppearanceFactory(); 391 } 392 393 protected String getSigningReason() throws SignException { 394 for (SignatureDescriptor sd : signatureRegistryMap.values()) { 395 String reason = sd.getReason(); 396 if (!StringUtils.isBlank(reason)) { 397 return reason; 398 } 399 } 400 throw new SignException("No default signing reason provided in configuration"); 401 } 402 403 protected boolean certificatePresentInPDF(Certificate userCert, List<X509Certificate> pdfCertificates) 404 throws SignException { 405 X509Certificate xUserCert = (X509Certificate) userCert; 406 for (X509Certificate xcert : pdfCertificates) { 407 // matching certificate found 408 if (xcert.getSubjectX500Principal().equals(xUserCert.getSubjectX500Principal())) { 409 return true; 410 } 411 } 412 return false; 413 } 414 415 /** 416 * @since 5.8 Provides the position rectangle for the next certificate. An assumption is made that all previous 417 * certificates in a given PDF were placed using the same technique and settings. New certificates are added 418 * depending of signature layout contributed. 419 */ 420 protected Rectangle getNextCertificatePosition(PdfReader pdfReader, List<X509Certificate> pdfCertificates) 421 throws SignException { 422 int numberOfSignatures = pdfCertificates.size(); 423 424 Rectangle pageSize = pdfReader.getPageSize(PAGE_TO_SIGN); 425 426 // PDF size 427 float width = pageSize.getWidth(); 428 float height = pageSize.getHeight(); 429 430 // Signature size 431 float rectangleWidth = width / getSignatureLayout().getColumns(); 432 float rectangeHeight = height / getSignatureLayout().getLines(); 433 434 // Signature location 435 int column = numberOfSignatures % getSignatureLayout().getColumns() + getSignatureLayout().getStartColumn(); 436 int line = numberOfSignatures / getSignatureLayout().getColumns() + getSignatureLayout().getStartLine(); 437 if (column > getSignatureLayout().getColumns()) { 438 column = column % getSignatureLayout().getColumns(); 439 line++; 440 } 441 442 // Skip rectangle display If number of signatures exceed free locations 443 // on pdf layout 444 if (line > getSignatureLayout().getLines()) { 445 return new Rectangle(0, 0, 0, 0); 446 } 447 448 // make smaller by page margin 449 float topRightX = rectangleWidth * column; 450 float bottomLeftY = height - rectangeHeight * line; 451 float bottomLeftX = topRightX - SIGNATURE_FIELD_WIDTH; 452 float topRightY = bottomLeftY + SIGNATURE_FIELD_HEIGHT; 453 454 // verify current position coordinates in case they were 455 // misconfigured 456 validatePageBounds(pdfReader, 1, bottomLeftX, true); 457 validatePageBounds(pdfReader, 1, bottomLeftY, false); 458 validatePageBounds(pdfReader, 1, topRightX, true); 459 validatePageBounds(pdfReader, 1, topRightY, false); 460 461 Rectangle positionRectangle = new Rectangle(bottomLeftX, bottomLeftY, topRightX, topRightY); 462 463 return positionRectangle; 464 } 465 466 /** 467 * Verifies that a provided value fits within the page bounds. If it does not, a sign exception is thrown. This is 468 * to verify externally configurable signature positioning. 469 * 470 * @param isHorizontal - if false, the current value is checked agains the vertical page dimension 471 */ 472 protected void validatePageBounds(PdfReader pdfReader, int pageNo, float valueToCheck, boolean isHorizontal) 473 throws SignException { 474 if (valueToCheck < 0) { 475 String message = "The new signature position " + valueToCheck 476 + " exceeds the page bounds. The position must be a positive number."; 477 log.debug(message); 478 throw new SignException(message); 479 } 480 481 Rectangle pageRectangle = pdfReader.getPageSize(pageNo); 482 if (isHorizontal && valueToCheck > pageRectangle.getRight()) { 483 String message = "The new signature position " + valueToCheck 484 + " exceeds the horizontal page bounds. The page dimensions are: (" + pageRectangle + ")."; 485 log.debug(message); 486 throw new SignException(message); 487 } 488 if (!isHorizontal && valueToCheck > pageRectangle.getTop()) { 489 String message = "The new signature position " + valueToCheck 490 + " exceeds the vertical page bounds. The page dimensions are: (" + pageRectangle + ")."; 491 log.debug(message); 492 throw new SignException(message); 493 } 494 } 495 496 @Override 497 public List<X509Certificate> getCertificates(DocumentModel doc) { 498 StatusWithBlob signedBlob = getSignedPdfBlobAndStatus(doc, null); 499 if (signedBlob == null) { 500 return Collections.emptyList(); 501 } 502 return getCertificates(signedBlob.blob); 503 } 504 505 protected List<X509Certificate> getCertificates(Blob pdfBlob) throws SignException { 506 try { 507 PdfReader pdfReader = new PdfReader(pdfBlob.getStream()); 508 return getCertificates(pdfReader); 509 } catch (IOException e) { 510 String message = ""; 511 if (e.getMessage().equals("PDF header signature not found.")) { 512 message = "PDF seems to be corrupted"; 513 } 514 throw new SignException(message, e); 515 } 516 } 517 518 protected List<X509Certificate> getCertificates(PdfReader pdfReader) throws SignException { 519 List<X509Certificate> pdfCertificates = new ArrayList<X509Certificate>(); 520 AcroFields acroFields = pdfReader.getAcroFields(); 521 @SuppressWarnings("unchecked") 522 List<String> signatureNames = acroFields.getSignatureNames(); 523 for (String signatureName : signatureNames) { 524 PdfPKCS7 pdfPKCS7 = acroFields.verifySignature(signatureName); 525 X509Certificate signingCertificate = pdfPKCS7.getSigningCertificate(); 526 pdfCertificates.add(signingCertificate); 527 } 528 return pdfCertificates; 529 } 530 531}