1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_mx_edi/models/l10n_mx_edi_certificate.py
2024-12-10 09:04:09 +07:00

167 lines
6.2 KiB
Python

# -*- coding: utf-8 -*-
import base64
import logging
import ssl
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from datetime import datetime
from lxml import etree
from pytz import timezone
from odoo import _, api, fields, models, tools
from odoo.addons.account.tools.certificate import crypto_load_certificate
from odoo.exceptions import ValidationError, UserError
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
try:
from OpenSSL import crypto
except ImportError:
_logger.warning('OpenSSL library not found. If you plan to use l10n_mx_edi, please install the library from https://pypi.python.org/pypi/pyOpenSSL')
class Certificate(models.Model):
_name = 'l10n_mx_edi.certificate'
_description = 'SAT Digital Sail'
_order = "date_start desc, id desc"
content = fields.Binary(
string='Certificate',
help='Certificate in .cer format',
required=True,
attachment=False,
)
company_id = fields.Many2one(
comodel_name='res.company',
string="Company",
required=True,
default=lambda self: self.env.company,
ondelete='cascade',
)
key = fields.Binary(
string='Certificate Key',
help='Certificate Key in .key format',
required=True,
attachment=False,
)
password = fields.Char(
string='Certificate Password',
help='Password for the Certificate Key',
required=True,
)
serial_number = fields.Char(
string='Serial number',
help='The serial number to add to electronic documents',
readonly=True,
index=True,
)
date_start = fields.Datetime(
string='Available date',
help='The date on which the certificate starts to be valid',
readonly=True,
)
date_end = fields.Datetime(
string='Expiration date',
help='The date on which the certificate expires',
readonly=True,
)
@tools.ormcache('content')
def _get_pem_cer(self, content):
'''Get the current content in PEM format
'''
self.ensure_one()
return ssl.DER_cert_to_PEM_cert(base64.decodebytes(content)).encode('UTF-8')
@tools.ormcache('key', 'password')
def _get_pem_key(self, key, password):
'''Get the current key in PEM format
'''
self.ensure_one()
private_key = serialization.load_der_private_key(base64.b64decode(key), password.encode())
return private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
@api.model
def _get_timezone(self):
""" Get the timezone to consider for the CFDI. """
return timezone('America/Mexico_City')
def _get_data(self):
'''Return the content (b64 encoded) and the certificate decrypted
'''
self.ensure_one()
cer_pem = self._get_pem_cer(self.content)
certificate = crypto_load_certificate(cer_pem)
for to_del in ['\n', ssl.PEM_HEADER, ssl.PEM_FOOTER]:
cer_pem = cer_pem.replace(to_del.encode('UTF-8'), b'')
return cer_pem, certificate
def _get_valid_certificate(self):
'''Search for a valid certificate that is available and not expired. '''
tz = self._get_timezone()
now = datetime.now(tz)
for record in self:
date_start = tz.localize(record.date_start)
date_end = tz.localize(record.date_end)
if date_start <= now <= date_end:
return record
return None
def _get_encrypted_cadena(self, cadena):
'''Encrypt the cadena using the private key.
'''
self.ensure_one()
key_pem = self._get_pem_key(self.key, self.password)
private_key = serialization.load_pem_private_key(bytes(key_pem), password=None)
cadena_crypted = private_key.sign(bytes(cadena.encode()), padding.PKCS1v15(), hashes.SHA256())
return base64.b64encode(cadena_crypted)
@api.model
def _get_cadena_chain(self, xml_tree, xslt_path):
""" Use the provided XSLT document to generate a pipe-delimited string
:param xml_tree: the source lxml document
:param xslt_path: Path to the XSLT document
:return: string
"""
cadena_transformer = etree.parse(tools.file_open(xslt_path))
return str(etree.XSLT(cadena_transformer)(xml_tree))
@api.constrains('content', 'key', 'password')
def _check_credentials(self):
'''Check the validity of content/key/password and fill the fields
with the certificate values.
'''
tz = self._get_timezone()
now = datetime.now(tz)
date_format = '%Y%m%d%H%M%SZ'
for record in self:
# Try to decrypt the certificate
try:
certificate = record._get_data()[1]
before = tz.localize(datetime.strptime(certificate.get_notBefore().decode("utf-8"), date_format))
after = tz.localize(datetime.strptime(certificate.get_notAfter().decode("utf-8"), date_format))
serial_number = certificate.get_serial_number()
except UserError as exc_orm: # ;-)
raise exc_orm
except Exception as e:
raise ValidationError(_('The certificate content is invalid %s.', e))
# Assign extracted values from the certificate
record.serial_number = ('%x' % serial_number)[1::2]
record.date_start = before.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
record.date_end = after.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
if now > after:
raise ValidationError(_('The certificate is expired since %s', record.date_end))
# Check the pair key/password
try:
key_pem = self._get_pem_key(self.key, self.password)
crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
except Exception:
raise ValidationError(_('The certificate key and/or password is/are invalid.'))