package es.uji.security.crypto.pdf; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.security.InvalidKeyException; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.Provider; import java.security.Security; import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Vector; import org.apache.log4j.Logger; import org.bouncycastle.asn1.x509.X509Name; import org.bouncycastle.jce.PrincipalUtil; import org.bouncycastle.jce.X509Principal; import org.bouncycastle.util.encoders.Hex; import com.lowagie.text.DocumentException; import com.lowagie.text.Element; import com.lowagie.text.Font; import com.lowagie.text.Image; import com.lowagie.text.Phrase; import com.lowagie.text.Rectangle; import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.ColumnText; import com.lowagie.text.pdf.PdfAnnotation; import com.lowagie.text.pdf.PdfContentByte; import com.lowagie.text.pdf.PdfDate; import com.lowagie.text.pdf.PdfDictionary; import com.lowagie.text.pdf.PdfFormField; import com.lowagie.text.pdf.PdfName; import com.lowagie.text.pdf.PdfReader; import com.lowagie.text.pdf.PdfSignature; import com.lowagie.text.pdf.PdfSignatureAppearance; import com.lowagie.text.pdf.PdfStamper; import com.lowagie.text.pdf.PdfString; import com.lowagie.text.pdf.PdfTemplate; import com.lowagie.text.pdf.PdfWriter; import es.uji.security.crypto.ISignFormatProvider; import es.uji.security.crypto.SignatureOptions; import es.uji.security.crypto.SignatureResult; import es.uji.security.crypto.config.ConfigManager; import es.uji.security.crypto.config.OS; import es.uji.security.util.i18n.LabelManager; public class PDFSignatureFactory implements ISignFormatProvider { private Logger log = Logger.getLogger(PDFSignatureFactory.class); private static final int PADDING = 3; private PrivateKey privateKey; private Provider provider; private ConfigManager conf = ConfigManager.getInstance(); private ConfigurationAdapter confAdapter; private Font font; private void initFontDefinition() { log.debug("VisibleAreaTextSize: " + confAdapter.getVisibleAreaTextSize()); font = new Font(); font.setSize(confAdapter.getVisibleAreaTextSize()); } protected byte[] genPKCS7Signature(InputStream data, String tsaUrl, PrivateKey pk, Provider provider, Certificate[] chain) throws Exception { PdfPKCS7TSA sgn = new PdfPKCS7TSA(pk, chain, null, "SHA1", provider, true); byte[] buff = new byte[2048]; int len = 0; while ((len = data.read(buff)) > 0) { sgn.update(buff, 0, len); } return sgn.getEncodedPKCS7(null, null, tsaUrl, null); } private void sign(PdfStamper pdfStamper, PdfSignatureAppearance pdfSignatureAppearance, Certificate[] chain) throws Exception { // Check if TSA support is enabled boolean enableTSP = false; if (confAdapter.isTimestamping() && confAdapter.getTsaURL() != null) { enableTSP = true; } // Add configured values if (enableTSP) { PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKMS, PdfName.ADBE_PKCS7_SHA1); dic.setReason(confAdapter.getReason()); dic.setLocation(confAdapter.getLocation()); dic.setContact(confAdapter.getContact()); dic.setDate(new PdfDate(pdfSignatureAppearance.getSignDate())); // time-stamp will // over-rule this pdfSignatureAppearance.setCryptoDictionary(dic); pdfSignatureAppearance.setCrypto((PrivateKey) privateKey, chain, null, null); int contentEst = 15000; HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>(); exc.put(PdfName.CONTENTS, new Integer(contentEst * 2 + 2)); pdfSignatureAppearance.preClose(exc); // Get the true data signature, including a true time stamp token byte[] encodedSig = genPKCS7Signature(pdfSignatureAppearance.getRangeStream(), confAdapter.getTsaURL(), privateKey, provider, chain); if (contentEst + 2 < encodedSig.length) { throw new Exception("Timestamp size estimate " + contentEst + " is too low for actual " + encodedSig.length); } // Copy signature into a zero-filled array, padding it up to estimate byte[] paddedSig = new byte[contentEst]; System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length); // Finally, load zero-padded signature into the signature field /Content PdfDictionary dic2 = new PdfDictionary(); dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true)); pdfSignatureAppearance.close(dic2); } else { pdfSignatureAppearance.setProvider(provider.getName()); pdfSignatureAppearance.setCrypto(privateKey, chain, null, PdfSignatureAppearance.WINCER_SIGNED); pdfSignatureAppearance.setReason(confAdapter.getReason()); pdfSignatureAppearance.setLocation(confAdapter.getLocation()); pdfSignatureAppearance.setContact(confAdapter.getContact()); pdfStamper.close(); } } private void createVisibleSignature(PdfSignatureAppearance sap, int numSignatures, String pattern, Map<String, String> bindValues) throws MalformedURLException, IOException, DocumentException { float x1 = confAdapter.getVisibleAreaX(); float y1 = confAdapter.getVisibleAreaY(); float x2 = confAdapter.getVisibleAreaX2(); float y2 = confAdapter.getVisibleAreaY2(); float offsetX = ((x2 - x1) * numSignatures) + 10; float offsetY = ((y2 - y1) * numSignatures) + 10; log.debug("VisibleArea: " + x1 + "," + y1 + "," + x2 + "," + y2 + " offsetX:" + offsetX + ", offsetY:" + offsetY); // Position of the visible signature Rectangle rectangle = null; if (confAdapter.getVisibleAreaRepeatAxis().equals("Y")) { rectangle = new Rectangle(x1, y1 + offsetY, x2, y2 + offsetY); } else { rectangle = new Rectangle(x1 + offsetX, y1, x2 + offsetX, y2); } log.debug("VisibleAreaPage: " + confAdapter.getVisibleAreaPage()); //sap.setVisibleSignature(rectangle, Integer.parseInt(confAdapter.getVisibleAreaPage()), null); sap.setVisibleSignature("CryptoAppletSignatureReference" + numSignatures); sap.setAcro6Layers(true); sap.setLayer2Font(font); // Compute pattern String signatureText = null; if (pattern != null && pattern.length() > 0) { PatternParser patternParser = new PatternParser(pattern); signatureText = patternParser.parse(bindValues); } // Determine the visible signature type String signatureType = confAdapter.getVisibleSignatureType(); log.debug("VisibleSignatureType: " + signatureType); if (signatureType.equals("GRAPHIC_AND_DESCRIPTION")) { updateLayerGraphiAndDescription(sap, rectangle, signatureText); sap.setRender(PdfSignatureAppearance.SignatureRenderGraphicAndDescription); } else if (signatureType.equals("DESCRIPTION")) { sap.setLayer2Text(signatureText); sap.setRender(PdfSignatureAppearance.SignatureRenderDescription); } else if (signatureType.equals("NAME_AND_DESCRIPTION")) { sap.setLayer2Text(signatureText); sap.setRender(PdfSignatureAppearance.SignatureRenderNameAndDescription); } } private void updateLayerGraphiAndDescription(PdfSignatureAppearance pdfSignatureAppearance, Rectangle rectangle, String signatureText) throws DocumentException, IOException { // Retrieve image log.debug("VisibleAreaImgFile: " + confAdapter.getVisibleAreaImgFile()); byte[] imageData = OS.inputStreamToByteArray(PDFSignatureFactory.class.getClassLoader() .getResourceAsStream(confAdapter.getVisibleAreaImgFile())); Image image = Image.getInstance(imageData); if (signatureText != null) { // Retrieve and reset Layer2 PdfTemplate pdfTemplate = pdfSignatureAppearance.getLayer(2); pdfTemplate.reset(); float width = Math.abs(rectangle.getWidth()); float height = Math.abs(rectangle.getHeight()); pdfTemplate.addImage(image, height, 0, 0, height, PADDING, PADDING); // Add text ColumnText ct = new ColumnText(pdfTemplate); ct.setRunDirection(PdfWriter.RUN_DIRECTION_DEFAULT); ct.setSimpleColumn(new Phrase(signatureText, font), height + PADDING * 2, 0, width - PADDING, height, font.getSize(), Element.ALIGN_LEFT); ct.go(); } else { pdfSignatureAppearance.setSignatureGraphic(image); } } private Certificate findCACertificateFor(Certificate cert) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { Integer n = new Integer(conf.getProperty("DIGIDOC_CA_CERTS")); Certificate CACert = null; for (int i = 1; i <= n; i++) { CACert = ConfigManager.readCertificate(conf.getProperty("DIGIDOC_CA_CERT" + i)); try { cert.verify(CACert.getPublicKey()); break; } catch (SignatureException e) { // The actual CACert does not match with the // signer certificate. CACert = null; } } return CACert; } public SignatureResult formatSignature(SignatureOptions signatureOptions) throws Exception { log.debug("Init PDF signature configuration"); this.confAdapter = new ConfigurationAdapter(signatureOptions); initFontDefinition(); byte[] datos = OS.inputStreamToByteArray(signatureOptions.getDataToSign()); X509Certificate certificate = signatureOptions.getCertificate(); this.privateKey = signatureOptions.getPrivateKey(); this.provider = signatureOptions.getProvider(); String reference = signatureOptions.getDocumentReference(); String validationUrl = signatureOptions.getDocumentReferenceVerificationUrl(); if (Security.getProvider(this.provider.getName()) == null && this.provider != null) { Security.addProvider(this.provider); } Certificate caCertificate = findCACertificateFor(certificate); if (caCertificate == null) { SignatureResult signatureResult = new SignatureResult(); signatureResult.setValid(false); signatureResult.addError(LabelManager.get("ERROR_CERTIFICATE_NOT_ALLOWED")); return signatureResult; } PdfReader reader = new PdfReader(datos); ByteArrayOutputStream sout = new ByteArrayOutputStream(); int numSignatures = reader.getAcroFields().getSignatureNames().size(); if (numSignatures == 0 && reference != null) { datos = addFooterMessage(reference, validationUrl, reader, sout); } datos = addSignaturePlaceholders(datos, numSignatures); sout = addVisibleSignatureAndSign(signatureOptions, datos, certificate, caCertificate, numSignatures); SignatureResult signatureResult = new SignatureResult(); signatureResult.setValid(true); signatureResult.setSignatureData(new ByteArrayInputStream(sout.toByteArray())); return signatureResult; } private ByteArrayOutputStream addVisibleSignatureAndSign(SignatureOptions signatureOptions, byte[] datos, X509Certificate certificate, Certificate caCertificate, int numSignatures) throws Exception { PdfReader reader; ByteArrayOutputStream sout; reader = new PdfReader(datos); sout = new ByteArrayOutputStream(); PdfStamper pdfStamper = PdfStamper.createSignature(reader, sout, '\0', null, true); PdfSignatureAppearance pdfSignatureAppareance = pdfStamper.getSignatureAppearance(); log.debug("VisibleSignature: " + confAdapter.isVisibleSignature()); if (confAdapter.isVisibleSignature()) { String pattern = confAdapter.getVisibleAreaTextPattern(); log.debug("VisibleAreaTextPattern: " + pattern); Map<String, String> bindValues = generateBindValues(signatureOptions); createVisibleSignature(pdfSignatureAppareance, numSignatures, pattern, bindValues); } sign(pdfStamper, pdfSignatureAppareance, new Certificate[] { certificate, caCertificate }); return sout; } private byte[] addSignaturePlaceholders(byte[] datos, int numSignatures) throws IOException, DocumentException { PdfReader reader;ByteArrayOutputStream sout;Rectangle rectangle = computeSignatureRectangle(numSignatures); reader = new PdfReader(datos); sout = new ByteArrayOutputStream(); PdfStamper pdfStamper = new PdfStamper(reader, sout, '\0', true); PdfFormField sig1 = PdfFormField.createSignature(pdfStamper.getWriter()); if (confAdapter.isVisibleSignature()) { sig1.setWidget(rectangle, null); sig1.setFlags(PdfAnnotation.FLAGS_PRINT); sig1.put(PdfName.DA, new PdfString("/Helv 0 Tf 0 g")); sig1.setFieldName("CryptoAppletSignatureReference" + numSignatures); sig1.setPage(1); String visibleAreaPage = confAdapter.getVisibleAreaPage(); if (visibleAreaPage == null || "ALL".equals(visibleAreaPage)) { log.debug("Final visible page value: " + visibleAreaPage + "."); for (int pageNumber = 1; pageNumber <= reader.getNumberOfPages(); pageNumber++) { log.debug("Generating annotation for page " + pageNumber); pdfStamper.addAnnotation(sig1, pageNumber); } } else { int targetPage = 1; if ("LAST".equals(visibleAreaPage)) { targetPage = reader.getNumberOfPages(); } else { targetPage = Integer.valueOf(visibleAreaPage); } log.debug("Generating annotation for page " + targetPage); pdfStamper.addAnnotation(sig1, targetPage); } } pdfStamper.close(); sout.close(); datos = sout.toByteArray(); return datos; } private byte[] addFooterMessage(String reference, String validationUrl, PdfReader reader, ByteArrayOutputStream sout) throws DocumentException, IOException { byte[] datos; String footerMessage = ""; if (validationUrl != null) { footerMessage = MessageFormat.format( "Puede validar este documento en {1} introduciendo la referencia {0}", reference, validationUrl); } else { footerMessage = MessageFormat.format("La referencia de este documento es {0}", reference, validationUrl); } PdfStamper stamper = new PdfStamper(reader, sout); PdfContentByte canvas = stamper.getOverContent(1); Font font = new Font(BaseFont.createFont(BaseFont.TIMES_ROMAN, "Cp1252", false)); font.setSize(9); ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(footerMessage, font), 20, 20, 0); stamper.close(); datos = sout.toByteArray(); return datos; } private Rectangle computeSignatureRectangle(int numSignatures) { float x1 = confAdapter.getVisibleAreaX(); float y1 = confAdapter.getVisibleAreaY(); float x2 = confAdapter.getVisibleAreaX2(); float y2 = confAdapter.getVisibleAreaY2(); float offsetX = ((x2 - x1) * numSignatures) + 10; float offsetY = ((y2 - y1) * numSignatures) + 10; log.debug("VisibleArea: " + x1 + "," + y1 + "," + x2 + "," + y2 + " offsetX:" + offsetX + ", offsetY:" + offsetY); // Position of the visible signature Rectangle rectangle = null; if (confAdapter.getVisibleAreaRepeatAxis().equals("Y")) { rectangle = new Rectangle(x1, y1 + offsetY, x2, y2 + offsetY); } else { rectangle = new Rectangle(x1 + offsetX, y1, x2 + offsetX, y2); } return rectangle; } private Map<String, String> generateBindValues(SignatureOptions signatureOptions) throws CertificateEncodingException, NoSuchAlgorithmException { Map<String, String> bindValues = signatureOptions.getVisibleSignatureTextBindValues(); X509Certificate certificate = signatureOptions.getCertificate(); if (bindValues != null) { final X509Principal principal = PrincipalUtil.getSubjectX509Principal(certificate); final Vector<?> values = principal.getValues(X509Name.CN); String certificateCN = (String) values.get(0); bindValues.put("%s", certificateCN); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); String currentDate = simpleDateFormat.format(new Date()); bindValues.put("%t", currentDate); String pdfReason = this.confAdapter.getReason(); bindValues.put("%reason", pdfReason); String pdfLocation = this.confAdapter.getLocation(); bindValues.put("%location", pdfLocation); String pdfContact = this.confAdapter.getContact(); bindValues.put("%contact", pdfContact); MessageDigest sha1 = MessageDigest.getInstance("SHA1"); sha1.update(certificate.getEncoded()); byte[] sha1Digest = sha1.digest(); String certificateHash = new String(Hex.encode(sha1Digest)); String certificateHashWithColons = certificateHash.replaceAll("(?<=..)(..)", ":$1"); bindValues.put("%certificateHash", certificateHashWithColons); } for (Map.Entry<String, String> bindValue : bindValues.entrySet()) { log.debug("Bind value " + bindValue.getKey() + ": " + bindValue.getValue()); } return bindValues; } }