diff --git a/report_qweb_signer/README.rst b/report_qweb_signer/README.rst new file mode 100644 index 00000000..4f526add --- /dev/null +++ b/report_qweb_signer/README.rst @@ -0,0 +1,116 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +======================= +Qweb PDF reports signer +======================= + +This module extends the functionality of report module to sign +PDFs using a PKCS#12 certificate. + + +Installation +============ + +To install this module, you need to install Java JDK:: + + apt-get install openjdk-7-jre-headless + + +Configuration +============= + +In order to start signing PDF documents you need to configure certificate(s) +to use in your company. + +* Go to ``Settings > Companies > Companies > Your company`` +* Go to ``Report configuration`` tab +* Click ``Edit`` +* Add a new item in ``PDF report certificates`` list +* Click ``Create`` +* Set name, certificate file, password file and model +* Optionally you can set a domain and filename pattern for saving as attachment + +For example, if you want to sign only customer invoices in open or paid state: + +* Model: ``account.invoice`` +* Domain: ``[('type','=','out_invoice'), ('state', 'in', ('open', 'paid'))]`` +* Save as attachment: ``(object.number or '').replace('/','_') + '.signed.pdf'`` + +**Note**: Linux user that executes Odoo server process must have +read access to certificate file and password file + + +Usage +===== + +User just prints PDF documents (only Qweb PDF reports supported) as usual, +but signed PDF is automatically downloaded if this document model is configured +as indicated above. + +If 'Save as attachment' is configured, signed PDF is saved as attachment and +next time saved one is downloaded without signing again. This is appropiate when +signing date is important, for example, when signing customer invoices. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/143/8.0 + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + + +Known issues / Roadmap +====================== + +* When signing multiple documents (if 'Allow only one document' is disable) + then 'Save as attachment' is not applied and signed result is not + saved as attachment. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + + +Credits +======= + +External utilities +------------------ + +* iText v1.4.8: © 2000-2006, Paulo Soares, Bruno Lowagie and others - License `MPL `_ or `LGPL2 `_ - http://sourceforge.net/projects/itext +* jPdfSign: © 2006 Jan Peter Stotz - License `MPL `_ or `LGPL2 `_ (inherited from iText) - http://private.sit.fraunhofer.de/~stotz/software/jpdfsign +* Modified jPdfSign: © 2015 Antonio Espinosa - License `MPL `_ or `LGPL2 `_ (inherited from iText) - static/src/java/JPdfSign.java + +Icon +---- + +`Created by Anton Noskov from the Noun Project `_ + +Contributors +------------ + +* Rafael Blasco +* Antonio Espinosa + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/report_qweb_signer/__init__.py b/report_qweb_signer/__init__.py new file mode 100644 index 00000000..ba025733 --- /dev/null +++ b/report_qweb_signer/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/report_qweb_signer/__openerp__.py b/report_qweb_signer/__openerp__.py new file mode 100644 index 00000000..cb388620 --- /dev/null +++ b/report_qweb_signer/__openerp__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Qweb PDF reports signer", + "summary": "Sign Qweb PDFs usign a PKCS#12 certificate", + "version": "8.0.1.0.0", + "category": "Reporting", + "website": "http://www.antiun.com", + "author": "Antiun Ingeniería S.L., " + "Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "report", + ], + "external_dependencies": { + "bin": ['/usr/bin/java'], + }, + "data": [ + "security/ir.model.access.csv", + "views/report_certificate_view.xml", + "views/res_company_view.xml", + ], + "demo": [ + "demo/report_partner.xml", + "demo/report_certificate.xml", + ], +} diff --git a/report_qweb_signer/demo/report_certificate.xml b/report_qweb_signer/demo/report_certificate.xml new file mode 100644 index 00000000..fba19870 --- /dev/null +++ b/report_qweb_signer/demo/report_certificate.xml @@ -0,0 +1,21 @@ + + + + + + + + Test OCA certificate + test.p12 + test.passwd + + [('customer', '=', True)] + + 'test_' + (object.name or '').replace(' ', '_').lower() + '.signed.pdf' + + + + diff --git a/report_qweb_signer/demo/report_partner.xml b/report_qweb_signer/demo/report_partner.xml new file mode 100644 index 00000000..90733b7a --- /dev/null +++ b/report_qweb_signer/demo/report_partner.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/report_qweb_signer/i18n/es.po b/report_qweb_signer/i18n/es.po new file mode 100644 index 00000000..ed6e1749 --- /dev/null +++ b/report_qweb_signer/i18n/es.po @@ -0,0 +1,161 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_qweb_signer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-22 19:28+0000\n" +"PO-Revision-Date: 2015-11-22 19:28+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: report_qweb_signer +#: field:report.certificate,allow_only_one:0 +msgid "Allow only one document" +msgstr "Sólo un documento" + +#. module: report_qweb_signer +#: field:report.certificate,path:0 +msgid "Certificate file path" +msgstr "Ruta al certificado" + +#. module: report_qweb_signer +#: view:res.company:report_qweb_signer.view_company_form +msgid "Certificates" +msgstr "Certificados" + +#. module: report_qweb_signer +#: model:ir.model,name:report_qweb_signer.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: report_qweb_signer +#: field:report.certificate,company_id:0 +msgid "Company" +msgstr "Compañía" + +#. module: report_qweb_signer +#: field:report.certificate,create_uid:0 +msgid "Created by" +msgstr "Creado por" + +#. module: report_qweb_signer +#: field:report.certificate,create_date:0 +msgid "Created on" +msgstr "Creado en" + +#. module: report_qweb_signer +#: field:report.certificate,domain:0 +msgid "Domain" +msgstr "Dominio" + +#. module: report_qweb_signer +#: help:report.certificate,domain:0 +msgid "Domain for filtering if sign or not the document" +msgstr "Dominio para filrar si firmar o no el documento" + +#. module: report_qweb_signer +#: help:report.certificate,attachment:0 +msgid "Filename used to store signed document as attachment. Keep empty to not save signed document." +msgstr "Nombre de fichero usado para guardar el documento firmado como adjunto. Dejar en blanco para no guardar el documento firmado." + +#. module: report_qweb_signer +#: field:report.certificate,id:0 +msgid "ID" +msgstr "ID" + +#. module: report_qweb_signer +#: help:report.certificate,allow_only_one:0 +msgid "If True, this certificate can not be used to sign a PDF from several documents." +msgstr "Si está activo, este certificado no puede usarse para firmar un PDF de varios documentos." + +#. module: report_qweb_signer +#: field:report.certificate,write_uid:0 +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: report_qweb_signer +#: field:report.certificate,write_date:0 +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: report_qweb_signer +#: field:report.certificate,model_id:0 +msgid "Model" +msgstr "Modelo" + +#. module: report_qweb_signer +#: help:report.certificate,model_id:0 +msgid "Model where apply this certificate" +msgstr "Modelo en el que usar este certificado para firmar" + +#. module: report_qweb_signer +#: field:report.certificate,name:0 +msgid "Name" +msgstr "Nombre" + +#. module: report_qweb_signer +#: model:ir.actions.act_window,name:report_qweb_signer.action_report_certificate +#: model:ir.ui.menu,name:report_qweb_signer.menu_report_certificate +msgid "PDF certificates" +msgstr "Certificados PDF" + +#. module: report_qweb_signer +#: view:report.certificate:report_qweb_signer.view_report_certificate_form +msgid "PDF report certificate" +msgstr "Certificado de informe PDF" + +#. module: report_qweb_signer +#: view:report.certificate:report_qweb_signer.view_report_certificate_tree +#: field:res.company,report_certificate_ids:0 +msgid "PDF report certificates" +msgstr "Certificados de informes PDF" + +#. module: report_qweb_signer +#: field:report.certificate,password_file:0 +msgid "Password file path" +msgstr "Ruta al fichero de contraseña" + +#. module: report_qweb_signer +#: help:report.certificate,path:0 +msgid "Path to PKCS#12 certificate file" +msgstr "Ruta al fichero de certificado PKCS#12" + +#. module: report_qweb_signer +#: help:report.certificate,password_file:0 +msgid "Path to certificate password file" +msgstr "Ruta al fichero que contiene la contraseña con la que se proteje el fichero de certificado" + +#. module: report_qweb_signer +#: code:addons/report_qweb_signer/models/report.py:77 +#, python-format +msgid "PortableSigner failed (error code: %s). Message: %s" +msgstr "PortableSigner falló (código de error: %s). Mensaje: %s" + +#. module: report_qweb_signer +#: model:ir.model,name:report_qweb_signer.model_report +msgid "Report" +msgstr "Informe" + +#. module: report_qweb_signer +#: field:report.certificate,attachment:0 +msgid "Save as attachment" +msgstr "Salvar como adjunto" + +#. module: report_qweb_signer +#: field:report.certificate,sequence:0 +msgid "Sequence" +msgstr "Secuencia" + +#. module: report_qweb_signer +#: code:addons/report_qweb_signer/models/report.py:76 +#, python-format +msgid "Signing report (PDF)" +msgstr "Firmando informe (PDF)" + diff --git a/report_qweb_signer/models/__init__.py b/report_qweb_signer/models/__init__.py new file mode 100644 index 00000000..605d2118 --- /dev/null +++ b/report_qweb_signer/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import report +from . import report_certificate +from . import res_company diff --git a/report_qweb_signer/models/report.py b/report_qweb_signer/models/report.py new file mode 100644 index 00000000..8245cd39 --- /dev/null +++ b/report_qweb_signer/models/report.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import base64 +from contextlib import closing +import os +import subprocess +import tempfile +import time + +from openerp import models, api, _ +from openerp.exceptions import Warning, AccessError +from openerp.tools.safe_eval import safe_eval + +import logging +_logger = logging.getLogger(__name__) + + +def _normalize_filepath(path): + path = path or '' + path = path.strip() + if not os.path.isabs(path): + me = os.path.dirname(__file__) + path = '{}/../static/certificate/'.format(me) + path + path = os.path.normpath(path) + return path if os.path.exists(path) else False + + +class Report(models.Model): + _inherit = 'report' + + def _certificate_get(self, cr, uid, ids, report, context=None): + if report.report_type != 'qweb-pdf': + _logger.info( + "Can only sign qweb-pdf reports, this one is '%s' type", + report.report_type) + return False + m_cert = self.pool['report.certificate'] + company_id = self.pool['res.users']._get_company(cr, uid) + certificate_ids = m_cert.search(cr, uid, [ + ('company_id', '=', company_id), + ('model_id', '=', report.model)], context=context) + if not certificate_ids: + _logger.info( + "No PDF certificate found for report '%s'", + report.report_name) + return False + for cert in m_cert.browse(cr, uid, certificate_ids, context=context): + # Check allow only one document + if cert.allow_only_one and len(ids) > 1: + _logger.info( + "Certificate '%s' allows only one document, " + "but printing %d documents", + cert.name, len(ids)) + continue + # Check domain + if cert.domain: + m_model = self.pool[cert.model_id.model] + domain = [('id', 'in', tuple(ids))] + domain = domain + safe_eval(cert.domain) + doc_ids = m_model.search(cr, uid, domain, context=context) + if not doc_ids: + _logger.info( + "Certificate '%s' domain not satisfied", cert.name) + continue + # Certificate match! + return cert + return False + + def _attach_filename_get(self, cr, uid, ids, certificate, context=None): + if len(ids) != 1: + return False + obj = self.pool[certificate.model_id.model].browse(cr, uid, ids[0]) + filename = safe_eval(certificate.attachment, { + 'object': obj, + 'time': time + }) + return filename + + def _attach_signed_read(self, cr, uid, ids, certificate, context=None): + if len(ids) != 1: + return False + filename = self._attach_filename_get( + cr, uid, ids, certificate, context=context) + if not filename: + return False + signed = False + m_attachment = self.pool['ir.attachment'] + attach_ids = m_attachment.search(cr, uid, [ + ('datas_fname', '=', filename), + ('res_model', '=', certificate.model_id.model), + ('res_id', '=', ids[0]) + ]) + if attach_ids: + signed = m_attachment.browse(cr, uid, attach_ids[0]).datas + signed = base64.decodestring(signed) + return signed + + def _attach_signed_write(self, cr, uid, ids, certificate, signed, + context=None): + if len(ids) != 1: + return False + filename = self._attach_filename_get( + cr, uid, ids, certificate, context=context) + if not filename: + return False + m_attachment = self.pool['ir.attachment'] + try: + attach_id = m_attachment.create(cr, uid, { + 'name': filename, + 'datas': base64.encodestring(signed), + 'datas_fname': filename, + 'res_model': certificate.model_id.model, + 'res_id': ids[0], + }) + except AccessError: + raise Warning( + _('Saving signed report (PDF): ' + 'You do not have enought access rights to save attachments')) + else: + _logger.info( + "The signed PDF document '%s' is now saved in the database", + filename) + return attach_id + + def _signer_bin(self, opts): + me = os.path.dirname(__file__) + java_bin = 'java -jar -Xms4M -Xmx4M' + jar = '{}/../static/jar/jPdfSign.jar'.format(me) + return '%s %s %s' % (java_bin, jar, opts) + + def pdf_sign(self, pdf, certificate): + pdfsigned = pdf + '.signed.pdf' + p12 = _normalize_filepath(certificate.path) + passwd = _normalize_filepath(certificate.password_file) + if not (p12 and passwd): + raise Warning( + _('Signing report (PDF): ' + 'Certificate or password file not found')) + signer_opts = '"%s" "%s" "%s" "%s"' % (p12, pdf, pdfsigned, passwd) + signer = self._signer_bin(signer_opts) + process = subprocess.Popen( + signer, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + out, err = process.communicate() + if process.returncode: + raise Warning( + _('Signing report (PDF): jPdfSign failed (error code: %s). ' + 'Message: %s. Output: %s') % + (process.returncode, err, out)) + return pdfsigned + + @api.v7 + def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, + context=None): + signed_content = False + report = self._get_report_from_name(cr, uid, report_name) + certificate = self._certificate_get( + cr, uid, ids, report, context=context) + if certificate and certificate.attachment: + signed_content = self._attach_signed_read( + cr, uid, ids, certificate, context=context) + if signed_content: + _logger.info("The signed PDF document '%s/%s' was loaded from " + "the database", report_name, ids) + return signed_content + content = super(Report, self).get_pdf( + cr, uid, ids, report_name, html=html, data=data, + context=context) + if certificate: + # Creating temporary origin PDF + pdf_fd, pdf = tempfile.mkstemp( + suffix='.pdf', prefix='report.tmp.') + with closing(os.fdopen(pdf_fd, 'w')) as pf: + pf.write(content) + _logger.info( + "Signing PDF document '%s/%s' with certificate '%s'", + report_name, ids, certificate.name) + signed = self.pdf_sign(pdf, certificate) + # Read signed PDF + if os.path.exists(signed): + with open(signed, 'rb') as pf: + content = pf.read() + # Manual cleanup of the temporary files + for fname in (pdf, signed): + try: + os.unlink(fname) + except (OSError, IOError): + _logger.error('Error when trying to remove file %s', fname) + if certificate.attachment: + self._attach_signed_write( + cr, uid, ids, certificate, content, context=context) + return content diff --git a/report_qweb_signer/models/report_certificate.py b/report_qweb_signer/models/report_certificate.py new file mode 100644 index 00000000..5bbb5290 --- /dev/null +++ b/report_qweb_signer/models/report_certificate.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import api, fields, models + + +class ReportCertificate(models.Model): + _name = 'report.certificate' + _order = 'sequence,id' + + @api.model + def _default_company(self): + m_company = self.env['res.company'] + return m_company._company_default_get('report.certificate') + + sequence = fields.Integer(default=10) + name = fields.Char(required=True) + path = fields.Char( + string="Certificate file path", required=True, + help="Path to PKCS#12 certificate file") + password_file = fields.Char( + string="Password file path", required=True, + help="Path to certificate password file") + model_id = fields.Many2one( + string="Model", required=True, + comodel_name='ir.model', + help="Model where apply this certificate") + domain = fields.Char( + string="Domain", + help="Domain for filtering if sign or not the document") + allow_only_one = fields.Boolean( + string="Allow only one document", default=True, + help="If True, this certificate can not be useb to sign " + "a PDF from several documents.") + attachment = fields.Char( + string="Save as attachment", + help="Filename used to store signed document as attachment. " + "Keep empty to not save signed document.") + company_id = fields.Many2one( + string='Company', comodel_name='res.company', + required=True, default=_default_company) diff --git a/report_qweb_signer/models/res_company.py b/report_qweb_signer/models/res_company.py new file mode 100644 index 00000000..5809175d --- /dev/null +++ b/report_qweb_signer/models/res_company.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, fields + + +class ResCompany(models.Model): + _inherit = 'res.company' + + report_certificate_ids = fields.One2many( + string="PDF report certificates", + comodel_name='report.certificate', + inverse_name='company_id') diff --git a/report_qweb_signer/security/ir.model.access.csv b/report_qweb_signer/security/ir.model.access.csv new file mode 100644 index 00000000..e04b17c6 --- /dev/null +++ b/report_qweb_signer/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_report_certificate_public","report_certificate group_public","model_report_certificate","base.group_user",1,0,0,0 +"access_report_certificate_manager","report_certificate group_manager","model_report_certificate","base.group_erp_manager",1,1,1,1 diff --git a/report_qweb_signer/static/certificate/test.p12 b/report_qweb_signer/static/certificate/test.p12 new file mode 100644 index 00000000..eca08285 Binary files /dev/null and b/report_qweb_signer/static/certificate/test.p12 differ diff --git a/report_qweb_signer/static/certificate/test.passwd b/report_qweb_signer/static/certificate/test.passwd new file mode 100644 index 00000000..f77b0040 --- /dev/null +++ b/report_qweb_signer/static/certificate/test.passwd @@ -0,0 +1 @@ +admin \ No newline at end of file diff --git a/report_qweb_signer/static/description/icon.png b/report_qweb_signer/static/description/icon.png new file mode 100644 index 00000000..9c3fc036 Binary files /dev/null and b/report_qweb_signer/static/description/icon.png differ diff --git a/report_qweb_signer/static/description/noun_65694_cc.svg b/report_qweb_signer/static/description/noun_65694_cc.svg new file mode 100644 index 00000000..3305279f --- /dev/null +++ b/report_qweb_signer/static/description/noun_65694_cc.svg @@ -0,0 +1 @@ +Created by Anton Noskovfrom the Noun Project \ No newline at end of file diff --git a/report_qweb_signer/static/jar/itext-1.4.8.jar b/report_qweb_signer/static/jar/itext-1.4.8.jar new file mode 100644 index 00000000..dbbd4280 Binary files /dev/null and b/report_qweb_signer/static/jar/itext-1.4.8.jar differ diff --git a/report_qweb_signer/static/jar/jPdfSign.jar b/report_qweb_signer/static/jar/jPdfSign.jar new file mode 100644 index 00000000..784b11fc Binary files /dev/null and b/report_qweb_signer/static/jar/jPdfSign.jar differ diff --git a/report_qweb_signer/static/src/java/JPdfSign.java b/report_qweb_signer/static/src/java/JPdfSign.java new file mode 100644 index 00000000..3f4f4165 --- /dev/null +++ b/report_qweb_signer/static/src/java/JPdfSign.java @@ -0,0 +1,333 @@ +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLDecoder; +import java.security.CodeSource; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.ProviderException; +import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.NoSuchElementException; +import java.util.ResourceBundle; + +import sun.security.pkcs11.SunPKCS11; + +import com.lowagie.text.pdf.PdfReader; +import com.lowagie.text.pdf.PdfSignatureAppearance; +import com.lowagie.text.pdf.PdfStamper; + +/* +import com.itextpdf.text.pdf.PdfReader; +import com.itextpdf.text.pdf.PdfSignatureAppearance; +import com.itextpdf.text.pdf.PdfStamper; +*/ + +/** + * + * @author Jan Peter Stotz + * + */ +public class JPdfSign { + + private static PrivateKey privateKey; + + private static Certificate[] certificateChain; + + private static ResourceBundle bundle = ResourceBundle.getBundle("strings"); + + private static String PRODUCTNAME = bundle.getString("productname"); + + private static String VERSION = bundle.getString("version"); + + private static String JAR_FILENAME = bundle.getString("jar-filename"); + + public static void main(String[] args) { + // for (int i = 0; i < args.length; i++) { + // System.out.println("arg[" + i + "] :" + args[i]); + // } + if (args.length < 2) + showUsage(); + + try { + + String pkcs12FileName = args[0].trim(); + String pdfInputFileName = args[1]; + String pdfOutputFileName = args[2]; + boolean usePKCS12 = !(pkcs12FileName.equals("-PKCS11")); + + String passwdfile = ""; + if (args.length == 4) { + passwdfile = args[3]; + } + // System.out.println(""); + // System.out.println("pdfInputFileName : " + pdfInputFileName); + // System.out.println("pdfOutputFileName: " + pdfOutputFileName); + + if (usePKCS12) + readPrivateKeyFromPKCS12(pkcs12FileName, passwdfile); + else + readPrivateKeyFromPKCS11(); + + PdfReader reader = null; + try { + reader = new PdfReader(pdfInputFileName); + } catch (IOException e) { + System.err + .println("An unknown error accoured while opening the input PDF file: \"" + + pdfInputFileName + "\""); + e.printStackTrace(); + System.exit(-1); + } + FileOutputStream fout = null; + try { + fout = new FileOutputStream(pdfOutputFileName); + } catch (FileNotFoundException e) { + System.err + .println("An unknown error accoured while opening the output PDF file: \"" + + pdfOutputFileName + "\""); + e.printStackTrace(); + System.exit(-1); + } + PdfStamper stp = null; + try { + stp = PdfStamper.createSignature(reader, fout, '\0', null, true); + PdfSignatureAppearance sap = stp.getSignatureAppearance(); + sap.setCrypto(privateKey, certificateChain, null, PdfSignatureAppearance.WINCER_SIGNED); + // sap.setCrypto(privateKey, certificateChain, null, null); + // sap.setReason("I'm the author"); + // sap.setLocation("Lisbon"); + // sap.setVisibleSignature(new Rectangle(100, 100, 200, 200), 1, + // null); + sap.setCertified(true); + stp.close(); + } catch (Exception e) { + System.err + .println("An unknown error accoured while signing the PDF file:"); + e.printStackTrace(); + System.exit(-1); + } + } catch (KeyStoreException kse) { + System.err + .println("An unknown error accoured while initializing the KeyStore instance:"); + kse.printStackTrace(); + System.exit(-1); + } + } + + private static void readPrivateKeyFromPKCS11() throws KeyStoreException { + // Initialize PKCS#11 provider from config file + String configFileName = getConfigFilePath("pkcs11.cfg"); + + Provider p = null; + try { + p = new SunPKCS11(configFileName); + Security.addProvider(p); + } catch (ProviderException e) { + System.err + .println("Unable to load PKCS#11 provider with config file: " + + configFileName); + e.printStackTrace(); + System.exit(-1); + } + String pkcs11PIN = "000000"; + System.out.print("Please enter the smartcard pin: "); + try { + BufferedReader in = new BufferedReader(new InputStreamReader( + System.in)); + pkcs11PIN = in.readLine(); + // System.out.println(pkcs11PIN); + // System.out.println(pkcs11PIN.length()); + } catch (Exception e) { + System.err + .println("An unknown error accoured while reading the PIN:"); + e.printStackTrace(); + System.exit(-1); + } + KeyStore ks = null; + try { + ks = KeyStore.getInstance("pkcs11", p); + ks.load(null, pkcs11PIN.toCharArray()); + } catch (NoSuchAlgorithmException e) { + System.err + .println("An unknown error accoured while reading the PKCS#11 smartcard:"); + e.printStackTrace(); + System.exit(-1); + } catch (CertificateException e) { + System.err + .println("An unknown error accoured while reading the PKCS#11 smartcard:"); + e.printStackTrace(); + System.exit(-1); + } catch (IOException e) { + System.err + .println("An unknown error accoured while reading the PKCS#11 smartcard:"); + e.printStackTrace(); + System.exit(-1); + } + + String alias = ""; + try { + alias = (String) ks.aliases().nextElement(); + privateKey = (PrivateKey) ks.getKey(alias, pkcs11PIN.toCharArray()); + } catch (NoSuchElementException e) { + System.err + .println("An unknown error accoured while retrieving the private key:"); + System.err + .println("The selected PKCS#12 file does not contain any private keys."); + e.printStackTrace(); + System.exit(-1); + } catch (NoSuchAlgorithmException e) { + System.err + .println("An unknown error accoured while retrieving the private key:"); + e.printStackTrace(); + System.exit(-1); + } catch (UnrecoverableKeyException e) { + System.err + .println("An unknown error accoured while retrieving the private key:"); + e.printStackTrace(); + System.exit(-1); + } + certificateChain = ks.getCertificateChain(alias); + } + + protected static void readPrivateKeyFromPKCS12(String pkcs12FileName, String pwdFile) + throws KeyStoreException { + String pkcs12Password = ""; + KeyStore ks = null; + if (!pwdFile.equals("")) { + try { + FileInputStream pwdfis = new FileInputStream(pwdFile); + byte[] pwd = new byte[1024]; + try { + do { + int r = pwdfis.read(pwd); + if (r < 0) { + break; + } + pkcs12Password += new String(pwd); + pkcs12Password = pkcs12Password.trim(); + } while (pwdfis.available() > 0); + pwdfis.close(); + } catch (IOException ex) { + System.err + .println("Can't read password file: " + pwdFile); + } + } catch (FileNotFoundException fnfex) { + System.err + .println("Password file not found: " + pwdFile); + } + } else { + System.out.print("Please enter the password for \"" + pkcs12FileName + + "\": "); + try { + BufferedReader in = new BufferedReader(new InputStreamReader( + System.in)); + pkcs12Password = in.readLine(); + } catch (Exception e) { + System.err + .println("An unknown error accoured while reading the password:"); + e.printStackTrace(); + System.exit(-1); + } + } + + try { + ks = KeyStore.getInstance("pkcs12"); + ks.load(new FileInputStream(pkcs12FileName), pkcs12Password + .toCharArray()); + } catch (NoSuchAlgorithmException e) { + System.err + .println("An unknown error accoured while reading the PKCS#12 file:"); + e.printStackTrace(); + System.exit(-1); + } catch (CertificateException e) { + System.err + .println("An unknown error accoured while reading the PKCS#12 file:"); + e.printStackTrace(); + System.exit(-1); + } catch (FileNotFoundException e) { + System.err.println("Unable to open the PKCS#12 keystore file \"" + + pkcs12FileName + "\":"); + System.err + .println("The file does not exists or missing read permission."); + System.exit(-1); + } catch (IOException e) { + System.err + .println("An unknown error accoured while reading the PKCS#12 file:"); + e.printStackTrace(); + System.exit(-1); + } + String alias = ""; + try { + alias = (String) ks.aliases().nextElement(); + privateKey = (PrivateKey) ks.getKey(alias, pkcs12Password + .toCharArray()); + } catch (NoSuchElementException e) { + System.err + .println("An unknown error accoured while retrieving the private key:"); + System.err + .println("The selected PKCS#12 file does not contain any private keys."); + e.printStackTrace(); + System.exit(-1); + } catch (NoSuchAlgorithmException e) { + System.err + .println("An unknown error accoured while retrieving the private key:"); + e.printStackTrace(); + System.exit(-1); + } catch (UnrecoverableKeyException e) { + System.err + .println("An unknown error accoured while retrieving the private key:"); + e.printStackTrace(); + System.exit(-1); + } + certificateChain = ks.getCertificateChain(alias); + } + + protected static String getConfigFilePath(String configFilename) { + CodeSource source = JPdfSign.class.getProtectionDomain() + .getCodeSource(); + URL url = source.getLocation(); + + String jarPath = URLDecoder.decode(url.getFile()); + File f = new File(jarPath); + try { + jarPath = f.getCanonicalPath(); + } catch (IOException e) { + } + if (!f.isDirectory()) { + f = new File(jarPath); + jarPath = f.getParent(); + } + System.out.println(jarPath); + if (jarPath.length() > 0) { + return jarPath + File.separator + configFilename; + } else + return configFilename; + } + + public static void showUsage() { + System.out.println("jPdfSign v" + VERSION + + " by Jan Peter Stotz - jpstotz@gmx.de\n"); + System.out.println(PRODUCTNAME + " usage:"); + System.out + .println("\nFor using a PKCS#12 (.p12) file as signature certificate and private key source:"); + System.out.print("\tjava -jar " + JAR_FILENAME); + System.out + .println(" pkcs12FileName pdfInputFileName pdfOutputFileName"); + System.out + .println("\nFor using a PKCS#11 smartcard as signature certificate and private key source:"); + System.out.print("\tjava -jar" + JAR_FILENAME); + System.out.println(" -PKCS11 pdfInputFileName pdfOutputFileName"); + System.exit(0); + } +} diff --git a/report_qweb_signer/static/src/java/strings.properties b/report_qweb_signer/static/src/java/strings.properties new file mode 100644 index 00000000..5e9054fb --- /dev/null +++ b/report_qweb_signer/static/src/java/strings.properties @@ -0,0 +1,3 @@ +productname=jPdfSign +version=0.3.1 +jar-filename=jPdfSign.jar \ No newline at end of file diff --git a/report_qweb_signer/views/report_certificate_view.xml b/report_qweb_signer/views/report_certificate_view.xml new file mode 100644 index 00000000..219059cc --- /dev/null +++ b/report_qweb_signer/views/report_certificate_view.xml @@ -0,0 +1,66 @@ + + + + + + + report.certificate.form + report.certificate + +
+ +
+
+ + + + + + + + + + + + + +
+
+
+
+ + + report.certificate.tree + report.certificate + + + + + + + + + + + + + + PDF certificates + report.certificate + form + tree,form + + + + +
+
diff --git a/report_qweb_signer/views/res_company_view.xml b/report_qweb_signer/views/res_company_view.xml new file mode 100644 index 00000000..cd8cac9e --- /dev/null +++ b/report_qweb_signer/views/res_company_view.xml @@ -0,0 +1,26 @@ + + + + + + + Add PDF report certificates list + + res.company + + + + + + + + + + + + +