forked from Mapan/odoo17e
117 lines
6.0 KiB
Python
117 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
import base64
|
|
import logging
|
|
import ssl
|
|
|
|
from datetime import datetime
|
|
from pytz import timezone
|
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, load_pem_private_key
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.addons.account.tools.certificate import crypto_load_certificate, load_key_and_certificates
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Certificate(models.Model):
|
|
_name = 'l10n_cl.certificate'
|
|
_description = 'SII Digital Signature'
|
|
_rec_name = 'signature_filename'
|
|
_order = 'id desc'
|
|
|
|
signature_filename = fields.Char('Signature File Name')
|
|
signature_key_file = fields.Binary(string='Certificate Key', help='Certificate Key in PFX format', required=True)
|
|
signature_pass_phrase = fields.Char(string='Certificate Passkey', help='Passphrase for the certificate key',
|
|
copy=False, required=True)
|
|
private_key = fields.Text(compute='_check_credentials', string='Private Key', store=True, copy=False,
|
|
groups='base.group_system')
|
|
certificate = fields.Text(compute='_check_credentials', string='Certificate', store=True, copy=False)
|
|
cert_expiration = fields.Datetime(
|
|
compute='_check_credentials', string='Expiration date', help='The date on which the certificate expires',
|
|
store=True)
|
|
subject_serial_number = fields.Char(
|
|
compute='_check_serial_number', string='Subject Serial Number', store=True, readonly=False, copy=False,
|
|
help='This is the document of the owner of this certificate.'
|
|
'Some certificates does not provide this number and you must fill it by hand')
|
|
company_id = fields.Many2one(
|
|
'res.company', string='Company', default=lambda self: self.env.company.id, required=True, readonly=True)
|
|
user_id = fields.Many2one('res.users', 'Certificate Owner',
|
|
help='If this certificate has an owner, he will be the only user authorized to use it, '
|
|
'otherwise, the certificate will be shared with other users of the current company')
|
|
last_token = fields.Char('Last Token')
|
|
token_time = fields.Datetime('Token Time')
|
|
l10n_cl_is_there_shared_certificate = fields.Boolean(related='company_id.l10n_cl_is_there_shared_certificate')
|
|
|
|
def _get_data(self):
|
|
""" Return the signature_key_file (b64 encoded) and the certificate decrypted """
|
|
self.ensure_one()
|
|
try:
|
|
pkey, cert = load_key_and_certificates(base64.b64decode(self.with_context(bin_size=False).signature_key_file), self.signature_pass_phrase.encode())
|
|
except Exception as error:
|
|
raise UserError(error)
|
|
cer_pem = cert.public_bytes(encoding=Encoding.PEM)
|
|
cert = 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, cert, pkey.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
|
|
|
def _is_valid_certificate(self):
|
|
""" Search for a valid certificate that is available and not expired. """
|
|
chilean_current_dt = self.env['l10n_cl.edi.util']._get_cl_current_datetime()
|
|
return len(self.filtered(lambda x: chilean_current_dt <= fields.Datetime.context_timestamp(
|
|
x.with_context(tz='America/Santiago'), x.cert_expiration))) >= 1
|
|
|
|
@api.depends('signature_key_file', 'signature_pass_phrase')
|
|
def _check_serial_number(self):
|
|
"""
|
|
This method is only for the readonly false compute
|
|
"""
|
|
for record in self:
|
|
if not record.signature_key_file or not record.signature_pass_phrase:
|
|
continue
|
|
try:
|
|
certificate = record._get_data()
|
|
except Exception as e:
|
|
raise UserError(_('The certificate signature_key_file is invalid: %s.', e)) from e
|
|
record.subject_serial_number = certificate[1].get_subject().serialNumber
|
|
|
|
@api.depends('signature_key_file', 'signature_pass_phrase')
|
|
def _check_credentials(self):
|
|
"""
|
|
Check the validity of signature_key_file/key/signature_pass_phrase and fill the fields
|
|
with the certificate values.
|
|
"""
|
|
chilean_tz = timezone('America/Santiago')
|
|
chilean_current_dt = self.env['l10n_cl.edi.util']._get_cl_current_datetime()
|
|
date_format = '%Y%m%d%H%M%SZ'
|
|
for record in self:
|
|
if not record.signature_key_file or not record.signature_pass_phrase:
|
|
continue
|
|
try:
|
|
certificate = record._get_data()
|
|
cert_expiration = chilean_tz.localize(
|
|
datetime.strptime(certificate[1].get_notAfter().decode('utf-8'), date_format))
|
|
except Exception as e:
|
|
raise UserError(_('The certificate signature_key_file is invalid: %s.', e)) from e
|
|
# Assign extracted values from the certificate
|
|
record.cert_expiration = cert_expiration.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
record.certificate = certificate[0]
|
|
record.private_key = certificate[2]
|
|
if chilean_current_dt > cert_expiration:
|
|
raise UserError(_('The certificate is expired since %s', record.cert_expiration))
|
|
|
|
def _int_to_bytes(self, value, byteorder='big'):
|
|
return value.to_bytes((value.bit_length() + 7) // 8, byteorder=byteorder)
|
|
|
|
def _get_private_key_modulus(self):
|
|
key = load_pem_private_key(self.private_key.encode('ascii'), password=None, backend=default_backend())
|
|
return base64.b64encode(self._int_to_bytes(key.public_key().public_numbers().n)).decode('utf-8')
|
|
|
|
def _get_private_key_exponent(self):
|
|
key = load_pem_private_key(self.private_key.encode('ascii'), password=None, backend=default_backend())
|
|
return base64.b64encode(self._int_to_bytes(key.public_key().public_numbers().e)).decode('utf-8')
|