From 3ebd1d36cde41bba5f357d456f380d063984e520 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 25 Sep 2017 23:36:20 +0200 Subject: [PATCH 1/5] [IMP] Added DSA algorithm --- src/xmlsig/algorithms/__init__.py | 1 + src/xmlsig/algorithms/dsa.py | 122 ++++++++++++++++++++++++++++++ src/xmlsig/constants.py | 4 +- tests/test_dsa.py | 29 +++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/xmlsig/algorithms/dsa.py create mode 100644 tests/test_dsa.py diff --git a/src/xmlsig/algorithms/__init__.py b/src/xmlsig/algorithms/__init__.py index e1fb5b4..823e9e5 100644 --- a/src/xmlsig/algorithms/__init__.py +++ b/src/xmlsig/algorithms/__init__.py @@ -1,2 +1,3 @@ from .hmac import HMACAlgorithm from .rsa import RSAAlgorithm +from .dsa import DSAAlgorithm diff --git a/src/xmlsig/algorithms/dsa.py b/src/xmlsig/algorithms/dsa.py new file mode 100644 index 0000000..523cc3d --- /dev/null +++ b/src/xmlsig/algorithms/dsa.py @@ -0,0 +1,122 @@ +# © 2017 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import struct +from base64 import b64decode, b64encode + +from asn1crypto.algos import DSASignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import dsa +from xmlsig.algorithms.base import Algorithm +from xmlsig.ns import NS_MAP, DSigNs +from xmlsig.utils import USING_PYTHON2, b64_print, create_node, long_to_bytes + + +def i2osp(x, x_len): + if x >= pow(256, x_len): + raise ValueError("integer too large") + digits = [] + while x: + digits.append(int(x % 256)) + x //= 256 + for _i in range(x_len - len(digits)): + digits.append(0) + return bytearray(digits[::-1]) + + +def os2ip(arr): + x_len = len(arr) + x = 0 + for i in range(x_len): + if USING_PYTHON2: + val = struct.unpack("B", arr[i])[0] + else: + val = arr[i] + x = x + (val * pow(256, x_len - i - 1)) + return x + + +def to_int(element): + if USING_PYTHON2: + return struct.unpack("B", element)[0] + return element + + +class DSAAlgorithm(Algorithm): + private_key_class = dsa.DSAPrivateKey + public_key_class = dsa.DSAPublicKey + + @staticmethod + def sign(data, private_key, digest): + return DSASignature.load(private_key.sign(data, digest())).to_p1363() + + @staticmethod + def verify(signature_value, data, public_key, digest): + public_key.verify( + DSASignature.from_p1363(b64decode(signature_value)).dump(), data, digest() + ) + + @staticmethod + def key_value(node, public_key): + result = create_node("DSAKeyValue", node, DSigNs, "\n", "\n") + public_numbers = public_key.public_numbers() + if public_numbers.parameter_numbers.p is not None: + create_node( + "P", + result, + DSigNs, + tail="\n", + text="\n" + + b64_print( + b64encode(long_to_bytes(public_numbers.parameter_numbers.p)) + ) + + "\n", + ) + if public_numbers.parameter_numbers.q is not None: + create_node( + "Q", + result, + DSigNs, + tail="\n", + text="\n" + + b64_print( + b64encode(long_to_bytes(public_numbers.parameter_numbers.q)) + ) + + "\n", + ) + if public_numbers.parameter_numbers.g is not None: + create_node( + "G", + result, + DSigNs, + tail="\n", + text="\n" + + b64_print( + b64encode(long_to_bytes(public_numbers.parameter_numbers.g)) + ) + + "\n", + ) + if public_numbers.y is not None: + create_node( + "Y", + result, + DSigNs, + tail="\n", + text="\n" + + b64_print(b64encode(long_to_bytes(public_numbers.y))) + + "\n", + ) + return result + + @staticmethod + def get_public_key(key_info, context): + key = key_info.find("ds:KeyInfo/ds:KeyValue/ds:DSAKeyValue", namespaces=NS_MAP) + if key is not None: + p = os2ip(b64decode(key.find("ds:P", namespaces=NS_MAP).text)) + q = os2ip(b64decode(key.find("ds:Q", namespaces=NS_MAP).text)) + g = os2ip(b64decode(key.find("ds:G", namespaces=NS_MAP).text)) + y = os2ip(b64decode(key.find("ds:Y", namespaces=NS_MAP).text)) + return dsa.DSAPublicNumbers(y, dsa.DSAParameterNumbers(p, q, g)).public_key( + default_backend() + ) + return super(DSAAlgorithm, DSAAlgorithm).get_public_key(key_info, context) diff --git a/src/xmlsig/constants.py b/src/xmlsig/constants.py index 8ab0b45..1efc245 100644 --- a/src/xmlsig/constants.py +++ b/src/xmlsig/constants.py @@ -3,7 +3,7 @@ from cryptography.hazmat.primitives import hashes -from .algorithms import HMACAlgorithm, RSAAlgorithm +from .algorithms import DSAAlgorithm, HMACAlgorithm, RSAAlgorithm from .ns import NS_MAP # noqa:F401 from .ns import DSignNsMore, DSigNs, DSigNs11, EncNs @@ -102,6 +102,8 @@ TransformHmacSha256: {"digest": hashes.SHA256, "method": HMACAlgorithm}, TransformHmacSha384: {"digest": hashes.SHA384, "method": HMACAlgorithm}, TransformHmacSha512: {"digest": hashes.SHA512, "method": HMACAlgorithm}, + TransformDsaSha1: {"digest": hashes.SHA1, "method": DSAAlgorithm}, + TransformDsaSha256: {"digest": hashes.SHA256, "method": DSAAlgorithm}, } TransformUsageEncryptionMethod = {} diff --git a/tests/test_dsa.py b/tests/test_dsa.py new file mode 100644 index 0000000..7eb78bf --- /dev/null +++ b/tests/test_dsa.py @@ -0,0 +1,29 @@ +import unittest +from os import path + +import xmlsig +from cryptography.hazmat.primitives.serialization import pkcs12 + +from .base import BASE_DIR, parse_xml + + +class TestDSASignature(unittest.TestCase): + def test_dsa(self): + root = parse_xml("data/sign-dsa-in.xml") + sign = root.xpath("//ds:Signature", namespaces={"ds": xmlsig.constants.DSigNs})[ + 0 + ] + self.assertIsNotNone(sign) + ctx = xmlsig.SignatureContext() + with open(path.join(BASE_DIR, "data/dsacred.p12"), "rb") as key_file: + ctx.load_pkcs12(pkcs12.load_key_and_certificates(key_file.read(), None)) + ctx.sign(sign) + ctx.verify(sign) + + def test_verify(self): + ctx = xmlsig.SignatureContext() + root = parse_xml("data/sign-dsa-out.xml") + sign = root.xpath("//ds:Signature", namespaces={"ds": xmlsig.constants.DSigNs})[ + 0 + ] + ctx.verify(sign) From 74b535a223fd75c88f0564109b87b48a1a3e3618 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 16 Oct 2019 19:35:06 -0500 Subject: [PATCH 2/5] [ADD] xmlsig 2 related constants --- src/xmlsig/constants.py | 5 ++++- src/xmlsig/ns.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/xmlsig/constants.py b/src/xmlsig/constants.py index 1efc245..b2b8685 100644 --- a/src/xmlsig/constants.py +++ b/src/xmlsig/constants.py @@ -5,7 +5,7 @@ from .algorithms import DSAAlgorithm, HMACAlgorithm, RSAAlgorithm from .ns import NS_MAP # noqa:F401 -from .ns import DSignNsMore, DSigNs, DSigNs11, EncNs +from .ns import DSignNsMore, DSigNs, DSigNs2, DSigNs11, EncNs ID_ATTR = "Id" @@ -18,6 +18,7 @@ TransformEnveloped = DSigNs + "enveloped-signature" TransformXPath = "http://www.w3.org/TR/1999/REC-xpath-19991116" TransformXPath2 = "" +TransformXPathFilter2 = "http://www.w3.org/2002/06/xmldsig-filter2" TransformXPointer = "" TransformXslt = "http://www.w3.org/TR/1999/REC-xslt-19991116" TransformRemoveXmlTagsC14N = "" @@ -61,6 +62,8 @@ TransformSha384 = DSignNsMore + "sha384" TransformSha512 = EncNs + "sha512" +TransformXmlSig2Tranform = DSigNs2 + "transform" + TransformUsageUnknown = {} TransformUsageDSigTransform = [TransformEnveloped, TransformBase64] TransformUsageC14NMethod = { diff --git a/src/xmlsig/ns.py b/src/xmlsig/ns.py index c37f0ce..8848514 100644 --- a/src/xmlsig/ns.py +++ b/src/xmlsig/ns.py @@ -3,6 +3,7 @@ DSigNs = "http://www.w3.org/2000/09/xmldsig#" DSigNs11 = "http://www.w3.org/2009/xmldsig11#" +DSigNs2 = "http://www.w3.org/2010/xmldsig2#" DSignNsMore = "http://www.w3.org/2001/04/xmldsig-more#" EncNs = "http://www.w3.org/2001/04/xmlenc#" XPathNs = "http://www.w3.org/TR/1999/REC-xpath-19991116" From a46f71ec77b8da21b2ff15bc0d95dfe75c06b27d Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 16 Oct 2019 19:40:12 -0500 Subject: [PATCH 3/5] [LINT] be a little more descriptive and concise (just to help myself out for the following commits) --- src/xmlsig/signature_context.py | 67 +++++++++++++++------------------ 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/src/xmlsig/signature_context.py b/src/xmlsig/signature_context.py index ae93c61..c5b9066 100644 --- a/src/xmlsig/signature_context.py +++ b/src/xmlsig/signature_context.py @@ -11,6 +11,7 @@ from lxml import etree from . import constants +from .constants import NS_MAP from .utils import b64_print, create_node, get_rdns_name @@ -27,24 +28,24 @@ def __init__(self): self.key_name = None self.ca_certificates = [] - def sign(self, node): + def sign(self, DS_Signature): """ Signs a Signature node :param node: Signature node :type node: lxml.etree.Element :return: None """ - signed_info = node.find("ds:SignedInfo", namespaces=constants.NS_MAP) + signed_info = DS_Signature.find("ds:SignedInfo", namespaces=NS_MAP) signature_method = signed_info.find( - "ds:SignatureMethod", namespaces=constants.NS_MAP + "ds:SignatureMethod", namespaces=NS_MAP ).get("Algorithm") - key_info = node.find("ds:KeyInfo", namespaces=constants.NS_MAP) + key_info = DS_Signature.find("ds:KeyInfo", namespaces=NS_MAP) if key_info is not None: - self.fill_key_info(key_info, signature_method) - self.fill_signed_info(signed_info) - self.calculate_signature(node) + self._fill_key_info(key_info, signature_method) + self._fill_signed_info(signed_info) + self.calculate_signature(DS_Signature) - def fill_key_info(self, key_info, signature_method): + def _fill_key_info(self, key_info, signature_method): """ Fills the KeyInfo node :param key_info: KeyInfo node @@ -53,13 +54,13 @@ def fill_key_info(self, key_info, signature_method): :type signature_method: str :return: None """ - x509_data = key_info.find("ds:X509Data", namespaces=constants.NS_MAP) + x509_data = key_info.find("ds:X509Data", namespaces=NS_MAP) if x509_data is not None: - self.fill_x509_data(x509_data) - key_name = key_info.find("ds:KeyName", namespaces=constants.NS_MAP) + self._fill_x509_data(x509_data) + key_name = key_info.find("ds:KeyName", namespaces=NS_MAP) if key_name is not None and self.key_name is not None: key_name.text = self.key_name - key_value = key_info.find("ds:KeyValue", namespaces=constants.NS_MAP) + key_value = key_info.find("ds:KeyValue", namespaces=NS_MAP) if key_value is not None: key_value.text = "\n" signature = constants.TransformUsageSignatureMethod[signature_method] @@ -70,37 +71,33 @@ def fill_key_info(self, key_info, signature_method): raise Exception("Key not compatible with signature method") signature["method"].key_value(key_value, key) - def fill_x509_data(self, x509_data): + def _fill_x509_data(self, x509_data): """ Fills the X509Data Node :param x509_data: X509Data Node :type x509_data: lxml.etree.Element :return: None """ - x509_issuer_serial = x509_data.find( - "ds:X509IssuerSerial", namespaces=constants.NS_MAP - ) + x509_issuer_serial = x509_data.find("ds:X509IssuerSerial", namespaces=NS_MAP) if x509_issuer_serial is not None: - self.fill_x509_issuer_name(x509_issuer_serial) + self._fill_x509_issuer_name(x509_issuer_serial) - x509_crl = x509_data.find("ds:X509CRL", namespaces=constants.NS_MAP) + x509_crl = x509_data.find("ds:X509CRL", namespaces=NS_MAP) if x509_crl is not None and self.crl is not None: x509_data.text = base64.b64encode( self.crl.public_bytes(serialization.Encoding.DER) ) - x509_subject = x509_data.find("ds:X509SubjectName", namespaces=constants.NS_MAP) + x509_subject = x509_data.find("ds:X509SubjectName", namespaces=NS_MAP) if x509_subject is not None: x509_subject.text = get_rdns_name(self.x509.subject.rdns) - x509_ski = x509_data.find("ds:X509SKI", namespaces=constants.NS_MAP) + x509_ski = x509_data.find("ds:X509SKI", namespaces=NS_MAP) if x509_ski is not None: x509_ski.text = base64.b64encode( self.x509.extensions.get_extension_for_oid( ExtensionOID.SUBJECT_KEY_IDENTIFIER ).value.digest ) - x509_certificate = x509_data.find( - "ds:X509Certificate", namespaces=constants.NS_MAP - ) + x509_certificate = x509_data.find("ds:X509Certificate", namespaces=NS_MAP) if x509_certificate is not None: s = base64.b64encode( self.x509.public_bytes(encoding=serialization.Encoding.DER) @@ -117,7 +114,7 @@ def fill_x509_data(self, x509_data): ) x509_certificate.addnext(certificate_node) - def fill_x509_issuer_name(self, x509_issuer_serial): + def _fill_x509_issuer_name(self, x509_issuer_serial): """ Fills the X509IssuerSerial node :param x509_issuer_serial: X509IssuerSerial node @@ -125,29 +122,27 @@ def fill_x509_issuer_name(self, x509_issuer_serial): :return: None """ x509_issuer_name = x509_issuer_serial.find( - "ds:X509IssuerName", namespaces=constants.NS_MAP + "ds:X509IssuerName", namespaces=NS_MAP ) if x509_issuer_name is not None: x509_issuer_name.text = get_rdns_name(self.x509.issuer.rdns) x509_issuer_number = x509_issuer_serial.find( - "ds:X509SerialNumber", namespaces=constants.NS_MAP + "ds:X509SerialNumber", namespaces=NS_MAP ) if x509_issuer_number is not None: x509_issuer_number.text = str(self.x509.serial_number) - def fill_signed_info(self, signed_info): + def _fill_signed_info(self, signed_info): """ Fills the SignedInfo node :param signed_info: SignedInfo node :type signed_info: lxml.etree.Element :return: None """ - for reference in signed_info.findall( - "ds:Reference", namespaces=constants.NS_MAP - ): + for reference in signed_info.findall("ds:Reference", namespaces=NS_MAP): self.calculate_reference(reference, True) - def verify(self, node): + def verify(self, DS_Signature): """ Verifies a signature :param node: Signature node @@ -159,18 +154,16 @@ def verify(self, node): path.join(path.dirname(__file__), "data/xmldsig-core-schema.xsd"), "rb" ) as file: schema = etree.XMLSchema(etree.fromstring(file.read())) - schema.assertValid(node) + schema.assertValid(DS_Signature) # Validates reference value - signed_info = node.find("ds:SignedInfo", namespaces=constants.NS_MAP) - for reference in signed_info.findall( - "ds:Reference", namespaces=constants.NS_MAP - ): + signed_info = DS_Signature.find("ds:SignedInfo", namespaces=NS_MAP) + for reference in signed_info.findall("ds:Reference", namespaces=NS_MAP): if not self.calculate_reference(reference, False): raise Exception( 'Reference with URI:"' + reference.get("URI", "") + '" failed' ) # Validates signature value - self.calculate_signature(node, False) + self.calculate_signature(DS_Signature, False) def transform(self, transform, node): """ From 4ed20b2561d9f3fe9e2608f44332541dc9abdecf Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 16 Oct 2019 19:41:45 -0500 Subject: [PATCH 4/5] [STYLE] be a little more verbose about b64 stuff Again, it's me, I'm regularly confused about it. --- src/xmlsig/signature_context.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/xmlsig/signature_context.py b/src/xmlsig/signature_context.py index c5b9066..2191b83 100644 --- a/src/xmlsig/signature_context.py +++ b/src/xmlsig/signature_context.py @@ -99,10 +99,9 @@ def _fill_x509_data(self, x509_data): ) x509_certificate = x509_data.find("ds:X509Certificate", namespaces=NS_MAP) if x509_certificate is not None: - s = base64.b64encode( - self.x509.public_bytes(encoding=serialization.Encoding.DER) - ) - x509_certificate.text = b64_print(s) + certificate = self.x509.public_bytes(encoding=serialization.Encoding.DER) + certificate_b64 = base64.b64encode(certificate) + x509_certificate.text = b64_print(certificate_b64) # Cosmetics for certificate in self.ca_certificates: certificate_node = create_node( "X509Certificate", None, constants.DSigNs, tail="\n" From e46085e05e8af5929bc01ff18cbf65ea2381cda7 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 25 Apr 2022 09:51:59 +0200 Subject: [PATCH 5/5] [STYLE] pre-commit autoupdate --- .pre-commit-config.yaml | 14 +++++++------- src/xmlsig/algorithms/dsa.py | 1 + tests/test_dsa.py | 3 ++- tests/test_hmac.py | 3 ++- tests/test_rsa.py | 3 ++- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ed41d2..fb8b5ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,11 +14,11 @@ default_language_version: python: python3 repos: - repo: https://github.com/psf/black - rev: 19.10b0 + rev: 22.3.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-eslint - rev: v6.8.0 + rev: v8.14.0 hooks: - id: eslint verbose: true @@ -26,7 +26,7 @@ repos: - --color - --fix - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v4.2.0 hooks: - id: trailing-whitespace # exclude autogenerated files @@ -48,7 +48,7 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.9.2 hooks: - id: flake8 name: flake8 except __init__.py @@ -60,7 +60,7 @@ repos: files: /__init__\.py$ additional_dependencies: ["flake8-bugbear==19.8.0"] - repo: https://github.com/pre-commit/mirrors-pylint - rev: v2.5.3 + rev: v3.0.0a4 hooks: - id: pylint name: pylint with optional checks @@ -70,11 +70,11 @@ repos: name: pylint with mandatory checks args: ["--rcfile=.pylintrc-mandatory"] - repo: https://github.com/asottile/pyupgrade - rev: v1.26.2 + rev: v2.32.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 + rev: v5.10.1 hooks: - id: isort name: isort except __init__.py diff --git a/src/xmlsig/algorithms/dsa.py b/src/xmlsig/algorithms/dsa.py index 523cc3d..163ed78 100644 --- a/src/xmlsig/algorithms/dsa.py +++ b/src/xmlsig/algorithms/dsa.py @@ -7,6 +7,7 @@ from asn1crypto.algos import DSASignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import dsa + from xmlsig.algorithms.base import Algorithm from xmlsig.ns import NS_MAP, DSigNs from xmlsig.utils import USING_PYTHON2, b64_print, create_node, long_to_bytes diff --git a/tests/test_dsa.py b/tests/test_dsa.py index 7eb78bf..ffebbdc 100644 --- a/tests/test_dsa.py +++ b/tests/test_dsa.py @@ -1,9 +1,10 @@ import unittest from os import path -import xmlsig from cryptography.hazmat.primitives.serialization import pkcs12 +import xmlsig + from .base import BASE_DIR, parse_xml diff --git a/tests/test_hmac.py b/tests/test_hmac.py index 96b4ec4..b8638d3 100644 --- a/tests/test_hmac.py +++ b/tests/test_hmac.py @@ -1,9 +1,10 @@ import base64 import unittest -import xmlsig from lxml import etree +import xmlsig + from .base import compare, parse_xml diff --git a/tests/test_rsa.py b/tests/test_rsa.py index 775428f..19dae19 100644 --- a/tests/test_rsa.py +++ b/tests/test_rsa.py @@ -1,12 +1,13 @@ import unittest from os import path -import xmlsig from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.serialization import pkcs12 from cryptography.x509 import load_pem_x509_certificate +import xmlsig + from .base import BASE_DIR, compare, parse_xml