forked from Mapan/odoo17e
125 lines
6.1 KiB
Python
125 lines
6.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import hashlib
|
|
import ssl
|
|
from base64 import b64decode, b64encode
|
|
from copy import deepcopy
|
|
from cryptography.hazmat.primitives import serialization
|
|
from lxml import etree
|
|
from pytz import timezone
|
|
from datetime import datetime
|
|
from OpenSSL import crypto
|
|
|
|
from odoo import _, api, fields, models, tools
|
|
from odoo.addons.account.tools.certificate import load_key_and_certificates
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class Certificate(models.Model):
|
|
_name = 'l10n_pe_edi.certificate'
|
|
_description = 'Sunat Digital Certificate'
|
|
_order = 'date_start desc, id desc'
|
|
_rec_name = 'serial_number'
|
|
|
|
content = fields.Binary(string="Certificate", required=True, help="PFX Certificate")
|
|
password = fields.Char(help="Passphrase for the PFX certificate")
|
|
serial_number = fields.Char(readonly=True, index=True, help="The serial number to add to electronic documents")
|
|
date_start = fields.Datetime(readonly=True, help="The date on which the certificate starts to be valid")
|
|
date_end = fields.Datetime(readonly=True, help="The date on which the certificate expires")
|
|
company_id = fields.Many2one(comodel_name='res.company', required=True, default=lambda self: self.env.company)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# HELPERS
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.model
|
|
def _get_pe_current_datetime(self):
|
|
"""Get the current datetime with the Peruvian timezone. """
|
|
peruvian_tz = timezone('America/Lima')
|
|
return datetime.now(peruvian_tz)
|
|
|
|
@tools.ormcache('self.content', 'self.password')
|
|
def _decode_certificate(self):
|
|
"""Return the content (DER encoded) and the certificate decrypted based in the point 3.1 from the RS 097-2012
|
|
http://www.vauxoo.com/r/manualdeautorizacion#page=21
|
|
"""
|
|
self.ensure_one()
|
|
private_key, certificate = load_key_and_certificates(b64decode(self.content), self.password.encode())
|
|
pem_certificate = certificate.public_bytes(serialization.Encoding.PEM)
|
|
pem_private_key = private_key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())
|
|
|
|
# Cleanup pem_content.
|
|
for to_clean in ('\n', ssl.PEM_HEADER, ssl.PEM_FOOTER):
|
|
pem_certificate = pem_certificate.replace(to_clean.encode('UTF-8'), b'')
|
|
|
|
return pem_certificate, pem_private_key, certificate
|
|
|
|
# -------------------------------------------------------------------------
|
|
# LOW-LEVEL METHODS
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
certificates = super().create(vals_list)
|
|
|
|
peruvian_tz = timezone('America/Lima')
|
|
peruvian_dt = self._get_pe_current_datetime()
|
|
date_format = '%Y%m%d%H%M%SZ'
|
|
for certificate in certificates:
|
|
try:
|
|
dummy, dummy, certif = certificate._decode_certificate()
|
|
cert_date_start = peruvian_tz.localize(certif.not_valid_before, date_format)
|
|
cert_date_end = peruvian_tz.localize(certif.not_valid_after, date_format)
|
|
serial_number = certif.serial_number
|
|
except:
|
|
raise ValidationError(_('There has been a problem with the certificate, some usual problems can be:\n'
|
|
'- The password given or the certificate are not valid.\n'
|
|
'- The certificate content is invalid.'))
|
|
# Assign extracted values from the certificate
|
|
certificate.write({
|
|
'serial_number': ('%x' % serial_number)[1::2],
|
|
'date_start': fields.Datetime.to_string(cert_date_start),
|
|
'date_end': fields.Datetime.to_string(cert_date_end),
|
|
})
|
|
if peruvian_dt > cert_date_end:
|
|
raise ValidationError(_('The certificate is expired since %s', certificate.date_end))
|
|
return certificates
|
|
|
|
# -------------------------------------------------------------------------
|
|
# BUSINESS METHODS
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _sign(self, edi_tree):
|
|
self.ensure_one()
|
|
pem_certificate, pem_private_key, dummy = self._decode_certificate()
|
|
namespaces = {'ds': 'http://www.w3.org/2000/09/xmldsig#'}
|
|
|
|
edi_tree_copy = deepcopy(edi_tree)
|
|
signature_element = edi_tree_copy.xpath('.//ds:Signature', namespaces=namespaces)[0]
|
|
signature_element.getparent().remove(signature_element)
|
|
|
|
edi_tree_c14n_str = etree.tostring(edi_tree_copy, method='c14n', exclusive=True, with_comments=False)
|
|
digest_b64 = b64encode(hashlib.new('sha1', edi_tree_c14n_str).digest())
|
|
signature_str = self.env['ir.qweb']._render(
|
|
'l10n_pe_edi.ubl_pe_21_signature_template',
|
|
{'digest_value': digest_b64.decode()}
|
|
)
|
|
|
|
# Eliminate all non useful spaces and new lines in the stream
|
|
signature_str = signature_str.replace('\n', '').replace(' ', '')
|
|
|
|
signature_tree = etree.fromstring(signature_str)
|
|
signed_info_element = signature_tree.xpath('.//ds:SignedInfo', namespaces=namespaces)[0]
|
|
signature = etree.tostring(signed_info_element, method='c14n', exclusive=True, with_comments=False)
|
|
private_pem_key = crypto.load_privatekey(crypto.FILETYPE_PEM, pem_private_key)
|
|
signature_b64_hash = b64encode(crypto.sign(private_pem_key, signature, 'sha1'))
|
|
|
|
signature_tree.xpath('.//ds:SignatureValue', namespaces=namespaces)[0].text = signature_b64_hash
|
|
signature_tree.xpath('.//ds:X509Certificate', namespaces=namespaces)[0].text = pem_certificate
|
|
signed_edi_tree = deepcopy(edi_tree)
|
|
signature_element = signed_edi_tree.xpath('.//ds:Signature', namespaces=namespaces)[0]
|
|
for child_element in signature_tree:
|
|
signature_element.append(child_element)
|
|
return signed_edi_tree
|