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