# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
+import tempfile
from requests import Session
@@ -138,12 +139,22 @@ def _connect_aeat(self, mapping_key):
company=self.company_id
)
params = self._connect_params_aeat(mapping_key)
- session = Session()
- session.cert = (public_crt, private_key)
- transport = Transport(session=session)
- history = HistoryPlugin()
- client = Client(wsdl=params["wsdl"], transport=transport, plugins=[history])
- return self._bind_service(client, params["port_name"], params["address"])
+ # Create temporary files to store the certificate and key
+ with (
+ tempfile.NamedTemporaryFile(delete=False, suffix=".crt") as cert_file,
+ tempfile.NamedTemporaryFile(delete=False, suffix=".key") as key_file,
+ ):
+ cert_file.write(public_crt)
+ key_file.write(private_key)
+ cert_file.flush()
+ key_file.flush()
+ # Set up session with certificate and key file paths
+ session = Session()
+ session.cert = (cert_file.name, key_file.name) # Provide file paths
+ transport = Transport(session=session)
+ history = HistoryPlugin()
+ client = Client(wsdl=params["wsdl"], transport=transport, plugins=[history])
+ return self._bind_service(client, params["port_name"], params["address"])
def _get_aeat_country_code(self):
self.ensure_one()
diff --git a/l10n_es_aeat/readme/ROADMAP.md b/l10n_es_aeat/readme/ROADMAP.md
index 865323be490..b61a51baf82 100644
--- a/l10n_es_aeat/readme/ROADMAP.md
+++ b/l10n_es_aeat/readme/ROADMAP.md
@@ -1,3 +1,7 @@
- La configuración de exportación a BOE no se filtran ni se
auto-selecciona por fechas de validez.
- Las partes específicas de las Diputaciones Forales no están incluidas.
+- El módulo de certificate no incluye la funcionalidad de especificar la carpeta
+ dónde se almacena el certificado, se opta por eliminar la funcionalidad en v18
+- El módulo de certificate guarda el password en el certificado, esa información
+ no está disponible antes de migrar por lo que se dejará vacía
\ No newline at end of file
diff --git a/l10n_es_aeat/security/ir.model.access.csv b/l10n_es_aeat/security/ir.model.access.csv
index 89ba628cd0f..f422e7a5f92 100644
--- a/l10n_es_aeat/security/ir.model.access.csv
+++ b/l10n_es_aeat/security/ir.model.access.csv
@@ -19,4 +19,3 @@ access_l10n_es_aeat_soap,access_l10n_es_aeat_soap,model_l10n_es_aeat_soap,group_
access_l10n_es_aeat_report_compare_boe_file,access_l10n_es_aeat_report_compare_boe_file,model_l10n_es_aeat_report_compare_boe_file,group_account_aeat,1,1,1,0
access_l10n_es_aeat_report_compare_boe_file_line,access_l10n_es_aeat_report_compare_boe_file_line,model_l10n_es_aeat_report_compare_boe_file_line,group_account_aeat,1,1,1,0
access_l10n_es_aeat_report_export_to_boe,access_l10n_es_aeat_report_export_to_boe,model_l10n_es_aeat_report_export_to_boe,group_account_aeat,1,1,1,0
-access_l10n_es_aeat_certificate_password,access_l10n_es_aeat_certificate_password,model_l10n_es_aeat_certificate_password,group_account_aeat,1,1,1,0
diff --git a/l10n_es_aeat/static/description/index.html b/l10n_es_aeat/static/description/index.html
index 52b588d50b0..09f787da129 100644
--- a/l10n_es_aeat/static/description/index.html
+++ b/l10n_es_aeat/static/description/index.html
@@ -473,6 +473,12 @@
auto-selecciona por fechas de validez.
Las partes específicas de las Diputaciones Forales no están
incluidas.
+El módulo de certificate no incluye la funcionalidad de especificar
+la carpeta dónde se almacena el certificado, se opta por eliminar la
+funcionalidad en v18
+El módulo de certificate guarda el password en el certificado, esa
+información no está disponible antes de migrar por lo que se dejará
+vacía
diff --git a/l10n_es_aeat/tests/test_l10n_es_aeat_certificate.py b/l10n_es_aeat/tests/test_l10n_es_aeat_certificate.py
index e71faacfa6a..eeb65981b8b 100644
--- a/l10n_es_aeat/tests/test_l10n_es_aeat_certificate.py
+++ b/l10n_es_aeat/tests/test_l10n_es_aeat_certificate.py
@@ -2,133 +2,88 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import base64
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
-import cryptography
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.primitives.serialization import BestAvailableEncryption, pkcs12
-from cryptography.x509 import oid
-from odoo import exceptions
+from odoo.exceptions import UserError
+from odoo.tests import TransactionCase, tagged
-from odoo.addons.base.tests.common import BaseCommon
-CRYPTOGRAPHY_VERSION_3 = tuple(map(int, cryptography.__version__.split("."))) >= (3, 0)
-if not CRYPTOGRAPHY_VERSION_3:
- from cryptography.hazmat.backends import default_backend
-
- def generate_private_key(public_exponent, key_size):
- return rsa.generate_private_key(
- public_exponent=public_exponent,
- key_size=key_size,
- backend=default_backend(),
- )
-
- from OpenSSL import crypto
+@tagged("post_install", "-at_install")
+class TestKeysCertificates(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
- def serialize_key_and_certificates(private_key, certificate, password):
- p12 = crypto.PKCS12()
- p12.set_privatekey(
- crypto.load_privatekey(
- crypto.FILETYPE_PEM,
- private_key.private_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PrivateFormat.PKCS8,
- encryption_algorithm=serialization.NoEncryption(),
- ),
- )
- )
- p12.set_certificate(
- crypto.load_certificate(
- crypto.FILETYPE_PEM,
- certificate.public_bytes(
- encoding=serialization.Encoding.PEM,
+ cls.subject = cls.issuer = x509.Name(
+ [
+ x509.NameAttribute(x509.oid.NameOID.COUNTRY_NAME, "BE"),
+ x509.NameAttribute(
+ x509.oid.NameOID.STATE_OR_PROVINCE_NAME, "Brabant wallon"
),
- )
+ x509.NameAttribute(x509.oid.NameOID.LOCALITY_NAME, "Grand Rosière"),
+ x509.NameAttribute(x509.oid.NameOID.ORGANIZATION_NAME, "Odoo S.A."),
+ x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "odoo.com"),
+ ]
)
- p12data = p12.export(password)
- return p12data
-
-else:
- generate_private_key = rsa.generate_private_key
- def serialize_key_and_certificates(private_key, certificate, password):
- return pkcs12.serialize_key_and_certificates(
- None,
- private_key,
- certificate,
- None,
- BestAvailableEncryption(password),
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ cls.test_key_1 = cls.env["certificate.key"].create(
+ {
+ "name": "Test key",
+ "content": base64.b64encode(
+ private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+ ),
+ }
)
-
-class TestL10nEsAeatCertificateBase(BaseCommon):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- cls.certificate_password = b"794613"
- private_key = generate_private_key(public_exponent=65537, key_size=2048)
- public_key = private_key.public_key()
- builder = x509.CertificateBuilder()
- cls.certificate_name = "Test Certificate"
- one_day = timedelta(1, 0, 0)
- builder = (
- builder.subject_name(
- x509.Name(
- [x509.NameAttribute(oid.NameOID.COMMON_NAME, cls.certificate_name)]
- )
- )
- .issuer_name(
- x509.Name(
- [
- x509.NameAttribute(oid.NameOID.COMMON_NAME, "cryptography.io"),
- ]
- )
- )
- .not_valid_before(datetime.today() - one_day)
- .not_valid_after(datetime.today() + (one_day * 30))
+ cls.certificate_1 = (
+ x509.CertificateBuilder()
+ .subject_name(cls.subject)
+ .issuer_name(cls.issuer)
+ .public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
- .public_key(public_key)
+ .not_valid_before(datetime.now(timezone.utc) - timedelta(days=10))
+ .not_valid_after(datetime.now(timezone.utc) + timedelta(days=10))
+ .add_extension(
+ x509.SubjectAlternativeName([x509.DNSName("localhost")]),
+ critical=False,
+ )
+ .sign(private_key, hashes.SHA256())
)
- sign_params = {"private_key": private_key, "algorithm": hashes.SHA256()}
- if not CRYPTOGRAPHY_VERSION_3:
- sign_params["backend"] = default_backend()
- certificate = builder.sign(**sign_params)
- content = serialize_key_and_certificates(
- private_key,
- certificate,
- cls.certificate_password,
+
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ certificate = cls.env["certificate.certificate"].create(
+ {
+ "name": "Test AEAT Certificate",
+ "content": base64.b64encode(
+ cls.certificate_1.public_bytes(encoding=serialization.Encoding.PEM)
+ ),
+ "private_key_id": cls.test_key_1.id,
+ }
)
cls.sii_cert = cls.env["l10n.es.aeat.certificate"].create(
{
- "folder": "Test folder",
- "file": base64.b64encode(content),
+ "certificate_id": certificate.id,
+ "state": "active",
}
)
- def _activate_certificate(self, passwd=None):
- """Obtain Keys from .pfx and activate the cetificate"""
- if not passwd:
- passwd = self.certificate_password
- wizard = self.env["l10n.es.aeat.certificate.password"].create(
- {"password": passwd}
+ def test_get_certificates(self):
+ pem_certificate, private_key = self.sii_cert.get_certificates()
+ self.assertEqual(pem_certificate, self.sii_cert.certificate_id.pem_certificate)
+ self.assertEqual(
+ private_key, self.sii_cert.certificate_id.private_key_id.pem_key
)
- wizard.with_context(active_id=self.sii_cert.id).get_keys()
- self.sii_cert.action_active()
- self.sii_cert.company_id.write(
- {"name": "ENTIDAD FICTICIO ACTIVO", "vat": "ESJ7102572J"}
- )
- self.assertEqual(self.certificate_name, self.sii_cert.name)
-
-class TestL10nEsAeatCertificate(TestL10nEsAeatCertificateBase):
- def test_activate_certificate(self):
- self.assertRaises(
- exceptions.ValidationError,
- self._activate_certificate,
- b"Wrong passwd",
- )
- self._activate_certificate(self.certificate_password)
- self.assertEqual(self.sii_cert.state, "active")
+ # Test that an error is raised when no valid certificates exist
+ self.sii_cert.state = "draft"
+ with self.assertRaises(UserError):
+ self.sii_cert.get_certificates()
diff --git a/l10n_es_aeat/views/aeat_certificate_view.xml b/l10n_es_aeat/views/aeat_certificate_view.xml
index 7161fc884f1..1f42c15f477 100644
--- a/l10n_es_aeat/views/aeat_certificate_view.xml
+++ b/l10n_es_aeat/views/aeat_certificate_view.xml
@@ -6,11 +6,6 @@
diff --git a/l10n_es_aeat/wizard/__init__.py b/l10n_es_aeat/wizard/__init__.py
index 2b4caa90125..0f0df051f5e 100644
--- a/l10n_es_aeat/wizard/__init__.py
+++ b/l10n_es_aeat/wizard/__init__.py
@@ -1,3 +1,2 @@
from . import compare_boe_file
from . import export_to_boe
-from . import aeat_certificate_password
diff --git a/l10n_es_aeat/wizard/aeat_certificate_password.py b/l10n_es_aeat/wizard/aeat_certificate_password.py
deleted file mode 100644
index 9e334041146..00000000000
--- a/l10n_es_aeat/wizard/aeat_certificate_password.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright 2017 Diagram Software S.L.
-# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
-
-
-import base64
-import contextlib
-import logging
-import os
-import tempfile
-
-from odoo import fields, models, release
-from odoo.exceptions import ValidationError
-from odoo.tools import config
-
-_logger = logging.getLogger(__name__)
-
-try:
- import cryptography
- from cryptography import x509
- from cryptography.hazmat.primitives import serialization
- from cryptography.hazmat.primitives.serialization import pkcs12
-
- CRYPTOGRAPHY_VERSION_3 = tuple(map(int, cryptography.__version__.split("."))) >= (
- 3,
- 0,
- )
- if not CRYPTOGRAPHY_VERSION_3:
- from cryptography.hazmat.backends import default_backend
-
- def load_key_and_certificates(*args, **kwargs):
- return pkcs12.load_key_and_certificates(
- *args, **kwargs, backend=default_backend()
- )
-
- else:
- load_key_and_certificates = pkcs12.load_key_and_certificates
-except (OSError, ImportError) as err:
- _logger.debug(err)
-
-# FIXME To be removed in v16, as it is now specified in the manifest
-if tuple(map(int, cryptography.__version__.split("."))) < (3, 0):
- _logger.warning(
- "Cryptography version is not supported. Upgrade to 3.0.0 or greater."
- )
-
-
-@contextlib.contextmanager
-def pfx_to_pem(p12, directory=None):
- with tempfile.NamedTemporaryFile(
- prefix="private_", suffix=".pem", delete=False, dir=directory
- ) as t_pem:
- with open(t_pem.name, "wb") as f_pem:
- f_pem.write(
- p12[0].private_bytes(
- serialization.Encoding.PEM,
- format=serialization.PrivateFormat.TraditionalOpenSSL,
- encryption_algorithm=serialization.NoEncryption(),
- )
- )
- f_pem.close()
- yield t_pem.name
-
-
-@contextlib.contextmanager
-def pfx_to_crt(p12, directory=None):
- with tempfile.NamedTemporaryFile(
- prefix="public_", suffix=".crt", delete=False, dir=directory
- ) as t_crt:
- with open(t_crt.name, "wb") as f_crt:
- f_crt.write(p12[1].public_bytes(serialization.Encoding.PEM))
- f_crt.close()
- yield t_crt.name
-
-
-class L10nEsAeatCertificatePassword(models.TransientModel):
- _name = "l10n.es.aeat.certificate.password"
- _description = "Wizard to Load AEAT Certificate"
-
- password = fields.Char(required=True)
-
- def get_keys(self):
- record = self.env["l10n.es.aeat.certificate"].browse(
- self.env.context.get("active_id")
- )
- directory = os.path.join(
- os.path.abspath(config["data_dir"]),
- "certificates",
- release.series,
- self.env.cr.dbname,
- record.folder,
- )
- file = base64.decodebytes(record.file)
- try:
- if directory and not os.path.exists(directory):
- os.makedirs(directory)
- pfx_password = self.password
- if isinstance(pfx_password, str):
- pfx_password = bytes(pfx_password, "utf-8")
- p12 = load_key_and_certificates(file, pfx_password)
- vals = self._process_certificate_vals(record, p12, directory)
- record.write(vals)
- except Exception as e:
- if e.args:
- args = list(e.args)
- raise ValidationError(args[-1]) from e
-
- def _process_certificate_vals(self, record, p12, directory):
- vals = {}
- with pfx_to_pem(p12, directory) as private_key:
- vals["private_key"] = private_key
- with pfx_to_crt(p12, directory) as public_key:
- vals["public_key"] = public_key
- certificate = p12[1]
- vals["date_start"] = certificate.not_valid_before
- vals["date_end"] = certificate.not_valid_after
- if not record.name:
- name = certificate.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
- if name:
- vals["name"] = name[0].value
- return vals
diff --git a/l10n_es_aeat/wizard/aeat_certificate_password_view.xml b/l10n_es_aeat/wizard/aeat_certificate_password_view.xml
deleted file mode 100644
index a3bcb32b649..00000000000
--- a/l10n_es_aeat/wizard/aeat_certificate_password_view.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- l10n.es.aeat.certificate.password.wizard
- l10n.es.aeat.certificate.password
-
-
-
-
-