Skip to content

Commit 3b8f2c9

Browse files
committed
chore: Update to SignXML 4.0.3 version
- Updated error messages and expected exception in tests. - Updated `add_pem_cert_header_footer` and now `signxml.util.add_pem_header` returns a byte object. - Removed use of `crypto_utils._X509CertOpenSsl` in `verify_xml_signature` as SignXML has deprecated PyOpenSSL. Ref: https://app.shortcut.com/cordada/story/11838/ [sc-11838]
1 parent 3e07095 commit 3b8f2c9

File tree

4 files changed

+106
-66
lines changed

4 files changed

+106
-66
lines changed

src/cl_sii/libs/crypto_utils.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,7 @@ def add_pem_cert_header_footer(pem_cert: bytes) -> bytes:
157157
"""
158158
pem_value_str = pem_cert.decode('ascii')
159159
# note: it would be great if 'add_pem_header' did not forcefully convert bytes to str.
160-
mod_pem_value_str = signxml.util.add_pem_header(pem_value_str)
161-
mod_pem_value: bytes = mod_pem_value_str.encode('ascii')
160+
mod_pem_value: bytes = signxml.util.add_pem_header(pem_value_str)
162161
return mod_pem_value
163162

164163

src/cl_sii/libs/xml_utils.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -440,14 +440,8 @@ def verify_xml_signature(
440440
)
441441

442442
if isinstance(trusted_x509_cert, crypto_utils._X509CertOpenSsl):
443-
trusted_x509_cert_open_ssl = trusted_x509_cert
444-
elif isinstance(trusted_x509_cert, crypto_utils.X509Cert):
445-
trusted_x509_cert_open_ssl = crypto_utils._X509CertOpenSsl.from_cryptography(
446-
trusted_x509_cert
447-
)
448-
elif trusted_x509_cert is None:
449-
trusted_x509_cert_open_ssl = None
450-
else:
443+
trusted_x509_cert = trusted_x509_cert.to_cryptography()
444+
elif not (isinstance(trusted_x509_cert, crypto_utils.X509Cert) or trusted_x509_cert is None):
451445
# A 'crypto_utils._X509CertOpenSsl' is ok but we prefer 'crypto_utils.X509Cert'.
452446
raise TypeError("'trusted_x509_cert' must be a 'crypto_utils.X509Cert' instance, or None.")
453447

@@ -481,10 +475,10 @@ def verify_xml_signature(
481475
# https://github.com/XML-Security/signxml/commit/ef15da8dbb904f1dedfdd210ae3e0df5da535612
482476
result = xml_verifier.verify(
483477
data=tmp_bytes,
484-
require_x509=True,
485-
x509_cert=trusted_x509_cert_open_ssl,
486-
ignore_ambiguous_key_info=True,
478+
x509_cert=trusted_x509_cert,
487479
expect_config=signxml.verifier.SignatureConfiguration(
480+
require_x509=True,
481+
ignore_ambiguous_key_info=True,
488482
signature_methods=frozenset([signxml.algorithms.SignatureMethod.RSA_SHA1]),
489483
digest_algorithms=frozenset([signxml.algorithms.DigestAlgorithm.SHA1]),
490484
),

src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml

+34-47
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@
7070
</Reference>
7171
</SignedInfo>
7272
<SignatureValue>
73-
fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
74-
ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
75-
wqSxDcYjTT6vXsLPrZk=
73+
wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD
74+
MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4
75+
FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1
76+
GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp
77+
0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==
7678
</SignatureValue>
7779
<KeyInfo>
7880
<KeyValue>
@@ -87,50 +89,35 @@ Uavs/9J+gR9BBMs/eYE=
8789
</KeyValue>
8890
<X509Data>
8991
<X509Certificate>
90-
MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU
91-
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw
92-
IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz
93-
NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh
94-
bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds
95-
ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49
96-
AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ
97-
gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF
98-
BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu
99-
Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs
100-
b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv
101-
gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz
102-
LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO
103-
Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC
104-
DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u
105-
Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j
106-
b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou
107-
Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs
108-
ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu
109-
Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n
110-
b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n
111-
c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp
112-
Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq
113-
LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl
114-
ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt
115-
Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb
116-
ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu
117-
Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0
118-
aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t
119-
ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK
120-
d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0
121-
aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow
122-
LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp
123-
BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O
124-
BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw
125-
FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD
126-
MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n
127-
L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y
128-
ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ
129-
IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb
130-
+DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl
131-
g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP
132-
MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA
133-
ow==
92+
MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx
93+
HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE
94+
ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD
95+
EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW
96+
GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa
97+
MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh
98+
MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE
99+
CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM
100+
b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI
101+
hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx
102+
O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC
103+
/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3
104+
FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV
105+
HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S
106+
ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu
107+
Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6
108+
Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1
109+
MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC
110+
AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu
111+
Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp
112+
AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A
113+
IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk
114+
AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA
115+
cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA
116+
mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD
117+
qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un
118+
+/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud
119+
B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY
120+
+kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==
134121
</X509Certificate>
135122
</X509Data>
136123
</KeyInfo>

src/tests/test_libs_xml_utils.py

+66-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import datetime
12
import io
23
import unittest
4+
from typing import Any
5+
from unittest import mock
36

47
import lxml.etree
8+
import signxml
59

6-
from cl_sii.libs.crypto_utils import load_pem_x509_cert
10+
from cl_sii.base.constants import SII_OFFICIAL_TZ
11+
from cl_sii.libs.crypto_utils import _X509CertOpenSsl, load_pem_x509_cert
12+
from cl_sii.libs.tz_utils import convert_naive_dt_to_tz_aware
713
from cl_sii.libs.xml_utils import ( # noqa: F401
814
XmlElement,
915
XmlFeatureForbidden,
@@ -185,6 +191,28 @@ def test_ok_external_trusted_cert(self) -> None:
185191
signature_xml_bytes = f.getvalue()
186192
self.assertEqual(signature_xml_bytes, self.with_valid_signature_signature_xml)
187193

194+
def test_ok_external_trusted_open_ssl_cert_with_signature(self) -> None:
195+
xml_doc = parse_untrusted_xml(self.with_valid_signature)
196+
cert = load_pem_x509_cert(self.xml_doc_cert_pem_bytes)
197+
198+
open_ssl_cert = _X509CertOpenSsl.from_cryptography(cert)
199+
200+
signed_data, signed_xml, signature_xml = verify_xml_signature(
201+
xml_doc, trusted_x509_cert=open_ssl_cert
202+
)
203+
204+
self.assertEqual(signed_data, self.with_valid_signature_signed_data)
205+
206+
f = io.BytesIO()
207+
write_xml_doc(signed_xml, f)
208+
signed_xml_bytes = f.getvalue()
209+
self.assertEqual(signed_xml_bytes, self.with_valid_signature_signed_xml)
210+
211+
f = io.BytesIO()
212+
write_xml_doc(signature_xml, f)
213+
signature_xml_bytes = f.getvalue()
214+
self.assertEqual(signature_xml_bytes, self.with_valid_signature_signature_xml)
215+
188216
def test_ok_cert_in_signature(self) -> None:
189217
# TODO: implement!
190218

@@ -221,7 +249,7 @@ def test_fail_verify_with_other_cert(self) -> None:
221249
verify_xml_signature(xml_doc, trusted_x509_cert=cert)
222250
self.assertEqual(
223251
cm.exception.args,
224-
("Signature verification failed: wrong signature length",),
252+
("Signature verification failed: ",),
225253
)
226254

227255
def test_bad_cert_included(self) -> None:
@@ -244,26 +272,58 @@ def test_bad_cert_included(self) -> None:
244272
)
245273

246274
def test_fail_replaced_cert(self) -> None:
275+
"""
276+
Tests that the signature verification fails
277+
when the certificate is not the one that was used to sign the document.
278+
"""
247279
xml_doc = parse_untrusted_xml(self.with_replaced_cert)
248-
cert = load_pem_x509_cert(self.any_x509_cert_pem_file)
280+
cert = load_pem_x509_cert(self.xml_doc_cert_pem_bytes)
249281

250282
with self.assertRaises(XmlSignatureInvalid) as cm:
251283
verify_xml_signature(xml_doc, trusted_x509_cert=cert)
252284
self.assertEqual(
253285
cm.exception.args,
254-
("Signature verification failed: []",),
286+
("Signature verification failed: ",),
255287
)
256288

257289
def test_fail_included_cert_not_from_a_known_ca(self) -> None:
258290
xml_doc = parse_untrusted_xml(self.with_valid_signature)
291+
xml_doc_signature_timestamp = convert_naive_dt_to_tz_aware(
292+
dt=datetime.datetime.fromisoformat('2019-04-01T01:36:40'), # From XML doc’s <TmstFirma>
293+
tz=SII_OFFICIAL_TZ,
294+
)
295+
296+
def _get_cert_chain_verifier(
297+
*args: Any, **kwargs: Any
298+
) -> signxml.util.X509CertChainVerifier:
299+
# The default signature verification time is the current time (see
300+
# https://cryptography.io/en/43.0.3/x509/verification/#cryptography.x509.verification.PolicyBuilder.time
301+
# ), but that causes verification to fail with the message
302+
# “validation failed: cert is not valid at validation time”.
303+
# To avoid that, we set the verification time to the time of the signature.
304+
return signxml.util.X509CertChainVerifier(
305+
ca_pem_file=kwargs['ca_pem_file'], verification_time=xml_doc_signature_timestamp
306+
)
259307

260308
# Without cert: fails because the issuer of the cert in the signature is not a known CA.
261-
with self.assertRaises(XmlSignatureInvalidCertificate) as cm:
309+
with self.assertRaises(XmlSignatureInvalidCertificate) as cm, mock.patch.object(
310+
signxml.verifier.XMLVerifier,
311+
'get_cert_chain_verifier',
312+
side_effect=_get_cert_chain_verifier,
313+
) as mock_get_cert_chain_verifier:
262314
verify_xml_signature(xml_doc, trusted_x509_cert=None)
263315
self.assertEqual(
264316
cm.exception.args,
265-
('unable to get local issuer certificate',),
317+
# According to some test cases from https://x509-limbo.com/, OpenSSL’s error message
318+
# “unable to get local issuer certificate” seems to be equivalent to PyCA Cryptography’s
319+
# error message below:
320+
(
321+
'validation failed:'
322+
' candidates exhausted:'
323+
' all candidates exhausted with no interior errors',
324+
),
266325
)
326+
mock_get_cert_chain_verifier.assert_called_once_with(ca_pem_file=None)
267327

268328
def test_fail_signed_data_modified(self) -> None:
269329
xml_doc = parse_untrusted_xml(self.with_signature_and_modified)

0 commit comments

Comments
 (0)