From bc6763ae65a82e30c23afd3ddf145f53366d8794 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 6 May 2025 12:59:11 +0200 Subject: [PATCH 01/10] Initialize certificate store --- .gitmodules | 3 +++ backend/apps/ifc_validation/checks/signatures/store | 1 + 2 files changed, 4 insertions(+) create mode 160000 backend/apps/ifc_validation/checks/signatures/store diff --git a/.gitmodules b/.gitmodules index ffa044a..51a6a5b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = backend/apps/ifc_validation_models url = https://github.com/buildingSMART/ifc-validation-data-model branch = development +[submodule "backend/apps/ifc_validation/checks/signatures/store"] + path = backend/apps/ifc_validation/checks/signatures/store + url = https://github.com/buildingsmart-certificates/validation-service-vendor-certificates diff --git a/backend/apps/ifc_validation/checks/signatures/store b/backend/apps/ifc_validation/checks/signatures/store new file mode 160000 index 0000000..afb7017 --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/store @@ -0,0 +1 @@ +Subproject commit afb70170e53b99634d742210816b1301ec3521c7 From 92c67ba9ac0f2635d24edc0a3a1f6f8f8861f065 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 8 May 2025 20:33:07 +0200 Subject: [PATCH 02/10] Digital signatures --- .../checks/signatures/check_signatures.py | 338 ++++++++++++++++++ .../ifc_validation/checks/signatures/store | 2 +- .../signatures/test_check_signatures.py | 24 ++ .../test_files/fail_invalid_signature.ifc | 47 +++ .../test_files/fail_truncated_signature.ifc | 46 +++ .../signatures/test_files/na_no_signature.ifc | 10 + .../test_files/pass_multiple_signatures.ifc | 85 +++++ .../test_files/pass_valid_signature.ifc | 47 +++ backend/apps/ifc_validation/tasks.py | 169 ++++++--- .../apps/ifc_validation_bff/views_legacy.py | 9 +- backend/apps/ifc_validation_models | 2 +- backend/requirements.txt | 1 + frontend/src/DashboardTable.js | 8 + frontend/src/Report.js | 35 ++ 14 files changed, 773 insertions(+), 50 deletions(-) create mode 100644 backend/apps/ifc_validation/checks/signatures/check_signatures.py create mode 100644 backend/apps/ifc_validation/checks/signatures/test_check_signatures.py create mode 100644 backend/apps/ifc_validation/checks/signatures/test_files/fail_invalid_signature.ifc create mode 100644 backend/apps/ifc_validation/checks/signatures/test_files/fail_truncated_signature.ifc create mode 100644 backend/apps/ifc_validation/checks/signatures/test_files/na_no_signature.ifc create mode 100644 backend/apps/ifc_validation/checks/signatures/test_files/pass_multiple_signatures.ifc create mode 100644 backend/apps/ifc_validation/checks/signatures/test_files/pass_valid_signature.ifc diff --git a/backend/apps/ifc_validation/checks/signatures/check_signatures.py b/backend/apps/ifc_validation/checks/signatures/check_signatures.py new file mode 100644 index 0000000..4a151ad --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/check_signatures.py @@ -0,0 +1,338 @@ +import binascii +from dataclasses import asdict, dataclass, fields +import datetime +import glob +import json +import os +import subprocess +import sys +import tempfile +from typing import Any, List, Optional +from typing import Tuple +from enum import Enum, auto + +# @nb These (rather incomplete) bindings are no +# longer needed, we just use the openssl executable +# from asn1crypto import cms +# from OpenSSL import crypto + +# pip install python-ranges +from ranges import Range, RangeSet + +import re +import base64 + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa, ec +from cryptography.exceptions import InvalidSignature +from cryptography.x509.oid import ExtensionOID + + +class VerificationResult(Enum): + invalid = auto() + valid_unknown_cert = auto() + valid_known_cert = auto() + + +@dataclass +class signature_data: + payload: str + start: int # start position of the signature block, the beginning of /* within the file + end: int # end position of the signature block, the character after */ within the file + + @property + def signature(self): + return base64.b64decode(self.payload.encode("ascii")) + + def as_dict(self): + return {k: format(getattr(self, k)) for k in (f.name for f in fields(self))} + + def verify_pkcs7_openssl( + self, ca: "ca_bundle", data: bytes + ) -> "Tuple[VerificationResult, Optional[certificate_data]]": + sig_fd, sig_path = tempfile.mkstemp(suffix=".p7s") + data_fd, data_path = tempfile.mkstemp(suffix=".dat") + certout = tempfile.NamedTemporaryFile(delete=False).name + try: + with os.fdopen(sig_fd, "wb") as f: + f.write(self.signature) + with os.fdopen(data_fd, "wb") as f: + f.write(data) + + cert_data = None + + # Perform verification up to two times, second time we also verify the chain to + # see if we have a known vendor root cert. Terminate as soon as verification fails. + # `-certsout` will write the accepted certificate to disk which we can parse + for verify_chain in (False, True): + cmd = [ + "openssl", + "cms", + "-verify", + "-inform", + "DER", + "-in", + sig_path, + "-content", + data_path, + "-CAfile", + ca.filepath, + # "-cmsout", "-print", + "-certsout", + certout, + *(("-noverify",) if not verify_chain else ()), + ] + result = subprocess.run(cmd, capture_output=True, text=False) + if result.returncode != 0: + return ( + VerificationResult.valid_unknown_cert if verify_chain else VerificationResult.invalid, + cert_data, + ) + cert_data = certificate_data.from_file(certout, verify=False) + return VerificationResult.valid_known_cert, cert_data + + finally: + # always clean up + try: + os.remove(sig_path) + except OSError: + pass + try: + os.remove(data_path) + except OSError: + pass + try: + os.remove(certout) + except OSError: + pass + + +@dataclass +class certificate_data: + certificate: Any + not_valid_before: datetime + not_valid_after: datetime + signature_hash_algorithm_name: str + rsa_key_size: int + subject: str + issuer: str + fingerprint_hex: str + serial_number: int + + @staticmethod + def from_file(fn, verify=True): + cert = x509.load_pem_x509_certificate(open(fn, "rb").read(), default_backend()) + + now = datetime.datetime.now(datetime.timezone.utc) + if verify and now < cert.not_valid_before_utc: + raise ValueError("Certificate is not yet valid.") + elif verify and now > cert.not_valid_after_utc: + raise ValueError("Certificate has expired.") + + # Check signature hash algorithm + sig_algo = cert.signature_hash_algorithm + if verify and sig_algo is None: + raise ValueError("Signature hash algorithm could not be determined.") + else: + algo_name = sig_algo.name.lower() + if verify and algo_name != "sha256": + raise ValueError("Signature hash algorithm {algo_name} not supported or deprecated.") + + # Check public key algorithm and parameters + public_key = cert.public_key() + key_size = None + if isinstance(public_key, rsa.RSAPublicKey): + key_size = public_key.key_size + if verify and key_size < 2048: + raise ValueError("RSA key size of {key_size} is less than 2048 bits.") + elif verify and isinstance(public_key, ec.EllipticCurvePublicKey): + raise ValueError("Only RSA currently supported") + # curve_name = public_key.curve.name + # key_size = public_key.key_size + # # Recommend using one of the common secure curves. + # if curve_name not in ['secp256r1', 'secp384r1', 'secp521r1']: + # print("Warning: Uncommon elliptic curve used, verify it meets security requirements.") + elif verify: + raise ValueError("Unrecognized public key type.") + + if verify and cert.version != x509.Version.v3: + raise ValueError(f"Certificate version {cert.version.name} is not X.509 v3") + + subject = set(f"{list(v)[0].rfc4514_attribute_name}={list(v)[0].value}" for v in cert.subject.rdns) + issuer = set(f"{list(v)[0].rfc4514_attribute_name}={list(v)[0].value}" for v in cert.issuer.rdns) + fingerprint = cert.fingerprint(hashes.SHA256()) + fh = fingerprint.hex().upper() + fingerprint_hex = ":".join(fh[i : i + 2] for i in range(0, len(fh), 2)) + + return certificate_data( + cert, + cert.not_valid_before_utc, + cert.not_valid_after_utc, + algo_name, + key_size, + subject, + issuer, + fingerprint_hex, + cert.serial_number, + ) + + def verify_pkcs7_python(self, signature: signature_data, content: str) -> bool: + """ + @nb this is wrong, but leaving it in here in case we do need to do more forensics on the + CMS structure later on. + """ + raise NotImplementedError() + ci = cms.ContentInfo.load(signature.signature) + if ci["content_type"].native == "signed_data": + sd = ci["content"] + eci = sd["encap_content_info"] + data = eci["content"].native or content[: signature.start].encode("ascii") + + for si in sd["signer_infos"]: + sid = si["sid"] + # match by issuer+serial or SKI + if sid.name == "issuer_and_serial_number": + ias = sid.chosen + ias_issuer = set( + f"{v['type'].human_friendly[0]}={v['value'].native}" for vs in ias["issuer"].chosen for v in vs + ) + + if ias_issuer != self.issuer or ias["serial_number"].native != self.serial_number: + continue + else: + ski_ext = self.certificate.extensions.get_extension_for_oid( + ExtensionOID.SUBJECT_KEY_IDENTIFIER + ).value.digest + if sid.chosen.native != ski_ext: + continue + + sig_bytes = si["signature"].native + hash_algo = si["digest_algorithm"]["algorithm"].native.upper() + try: + self.certificate.public_key().verify( + sig_bytes, + data, + padding.PKCS1v15(), + getattr(hashes, hash_algo)(), + ) + return True + except InvalidSignature: + return False + + return False + + def verify_pkcs1(self, signature, content) -> bool: + expected_size = self.certificate.public_key().key_size // 8 + if len(signature.signature) != expected_size: + return False + # raise InvalidSignature( + # f"Bad signature length: expected {expected_size} bytes, got {len(signature.signature)}" + # ) + try: + self.certificate.public_key().verify( + signature.signature, + content[: signature.start].encode("ascii"), + padding.PKCS1v15(), + hashes.SHA256(), + ) + return True + except binascii.Error as e: + return False + except InvalidSignature as e: + return False + + def as_dict(self): + excluded_fields = ("certificate",) + + def format(k, v): + if k in excluded_fields: + return None + elif isinstance(v, datetime.datetime): + return str(v) + elif isinstance(v, set): + return ",".join(sorted(v)) + else: + return v + + return { + k: format(k, getattr(self, k)) + for k in (f.name for f in fields(self)) + if format(k, getattr(self, k)) is not None + } + + +class ca_bundle: + def __init__(self, filepath: str): + self.filepath = filepath + + @staticmethod + def from_path(dirpath: str): + ca_bundle_path = tempfile.NamedTemporaryFile(suffix=".pem", delete=False).name + with open(ca_bundle_path, "wb") as cabundle: + for pem_path in glob.glob(os.path.join(dirpath, "*.pem")): + with open(pem_path, "rb") as f: + cabundle.write(f.read()) + return ca_bundle(ca_bundle_path) + + def __del__(self): + try: + os.remove(self.filepath) + except OSError: + pass + + +def get_signatures(data: str): + pattern = r"/\*\s*SIGNATURE;(.+?)ENDSEC;\s*\*/" + matches = re.finditer(pattern, data, re.DOTALL) + yield from (signature_data(m.group(1).strip(), *m.span()) for m in matches) + + +def strip_content(data: str) -> str: + return "".join(char for char in data if 0x20 <= ord(char) <= 0xFF and ord(char) != 0x7F) + + +def run(fn): + """ + # This was for earlier unsuccessful attempts, still leaving it here in case + # we need to revisit this or fallback to PKCS#1 + + store = crypto.X509Store() + certificate_store: List[certificate_data] = [] + + for fn in glob.glob(os.path.join(os.path.dirname(__file__), "store/*.pem")): + certificate_store.append(certificate_data.from_file(fn)) + + for certdata in certificate_store: + # `certdata.certificate` is a cryptography.X509Certificate; + # PyOpenSSL needs an OpenSSL.crypto.X509, so we round-trip via PEM: + pem = certdata.certificate.public_bytes(Encoding.PEM) + store.add_cert(crypto.load_certificate(crypto.FILETYPE_PEM, pem)) + """ + + ca = ca_bundle.from_path(os.path.join(os.path.dirname(__file__), "store")) + + with open(fn, "r", encoding="ascii") as f: + ifc_file = strip_content(f.read()) + + sigs = list(get_signatures(ifc_file)) + + if not sigs: + return + + non_signature_ranges = list(Range(0, len(ifc_file)) - RangeSet(Range(sig.start, sig.end) for sig in sigs)) + + if len(non_signature_ranges) != 1 or non_signature_ranges[0].start != 0: + yield {"signature": "invalid"} + return + + for sig in sigs: + content_bytes = ifc_file[: sig.start].encode("ascii") + status, cert = sig.verify_pkcs7_openssl(ca, content_bytes) + yield {"signature": status.name, **(cert.as_dict() if cert else {}), **sig.as_dict()} + + +if __name__ == "__main__": + for res in run(sys.argv[1]): + print(json.dumps(res)) diff --git a/backend/apps/ifc_validation/checks/signatures/store b/backend/apps/ifc_validation/checks/signatures/store index afb7017..16359ea 160000 --- a/backend/apps/ifc_validation/checks/signatures/store +++ b/backend/apps/ifc_validation/checks/signatures/store @@ -1 +1 @@ -Subproject commit afb70170e53b99634d742210816b1301ec3521c7 +Subproject commit 16359eac0ce07d5e898c98802d357171a7484453 diff --git a/backend/apps/ifc_validation/checks/signatures/test_check_signatures.py b/backend/apps/ifc_validation/checks/signatures/test_check_signatures.py new file mode 100644 index 0000000..2d82eb9 --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/test_check_signatures.py @@ -0,0 +1,24 @@ +import pytest +from pathlib import Path +import check_signatures +import sys + + +@pytest.mark.parametrize("fn", (Path(__file__).parent / "test_files").glob("*.ifc")) +def test_invocation(fn): + fragment = fn.name.split("_")[0] + if fragment == "pass": + assert [1 for res in check_signatures.run(fn) if res.get("signature", "").startswith("valid_")] + elif fragment == "fail": + assert [1 for res in check_signatures.run(fn) if res.get("signature", "") == "invalid"] + elif fragment == "na": + assert len(list(check_signatures.run(fn))) == 0 + else: + assert False + + +if __name__ == "__main__": + if len(sys.argv) == 2: + check_signatures.run(sys.argv[1]) + else: + pytest.main(["-sv", __file__]) diff --git a/backend/apps/ifc_validation/checks/signatures/test_files/fail_invalid_signature.ifc b/backend/apps/ifc_validation/checks/signatures/test_files/fail_invalid_signature.ifc new file mode 100644 index 0000000..75ef099 --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/test_files/fail_invalid_signature.ifc @@ -0,0 +1,47 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1'); +FILE_NAME('na_no_signature.ifc','2025-02-13T15:58:45',('jdoe'),('Acme Inc.'),'ABC rel. 0.1.2','Acme Inc. - MyFabTool - 2025.1','IFC4 model'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#23516=IFCPROJECT('0pYKP47wH3MwYXYFzgmYrB',$,'My_Project',$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; +/*SIGNATURE; +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 +WyOi3gSill0a +ENDSEC;*/ diff --git a/backend/apps/ifc_validation/checks/signatures/test_files/fail_truncated_signature.ifc b/backend/apps/ifc_validation/checks/signatures/test_files/fail_truncated_signature.ifc new file mode 100644 index 0000000..20b2a00 --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/test_files/fail_truncated_signature.ifc @@ -0,0 +1,46 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1'); +FILE_NAME('na_no_signature.ifc','2025-02-13T15:58:45',('jdoe'),('Acme Inc.'),'ABC rel. 0.1.2','Acme Inc. - MyFabTool - 2025.1','IFC4 model'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#23516=IFCPROJECT('0pYKP47wH3MwYXYFzgmYrB',$,'My_Project',$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; +/*SIGNATURE; +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 +ENDSEC;*/ diff --git a/backend/apps/ifc_validation/checks/signatures/test_files/na_no_signature.ifc b/backend/apps/ifc_validation/checks/signatures/test_files/na_no_signature.ifc new file mode 100644 index 0000000..862ca3c --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/test_files/na_no_signature.ifc @@ -0,0 +1,10 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1'); +FILE_NAME('na_no_signature.ifc','2025-02-13T15:58:45',('jdoe'),('Acme Inc.'),'ABC rel. 0.1.2','Acme Inc. - MyFabTool - 2025.1','IFC4 model'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#23515=IFCPROJECT('0pYKP47wH3MwYXYFzgmYrB',$,'My_Project',$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; \ No newline at end of file diff --git a/backend/apps/ifc_validation/checks/signatures/test_files/pass_multiple_signatures.ifc b/backend/apps/ifc_validation/checks/signatures/test_files/pass_multiple_signatures.ifc new file mode 100644 index 0000000..e517a0c --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/test_files/pass_multiple_signatures.ifc @@ -0,0 +1,85 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1'); +FILE_NAME('na_no_signature.ifc','2025-02-13T15:58:45',('jdoe'),('Acme Inc.'),'ABC rel. 0.1.2','Acme Inc. - MyFabTool - 2025.1','IFC4 model'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#23515=IFCPROJECT('0pYKP47wH3MwYXYFzgmYrB',$,'My_Project',$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; +/*SIGNATURE; +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 +tI4HyH3RIKh2 +ENDSEC;*/ + +/*SIGNATURE; +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 +If5KAUB+t/jc +ENDSEC;*/ diff --git a/backend/apps/ifc_validation/checks/signatures/test_files/pass_valid_signature.ifc b/backend/apps/ifc_validation/checks/signatures/test_files/pass_valid_signature.ifc new file mode 100644 index 0000000..5a6b3c7 --- /dev/null +++ b/backend/apps/ifc_validation/checks/signatures/test_files/pass_valid_signature.ifc @@ -0,0 +1,47 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1'); +FILE_NAME('na_no_signature.ifc','2025-02-13T15:58:45',('jdoe'),('Acme Inc.'),'ABC rel. 0.1.2','Acme Inc. - MyFabTool - 2025.1','IFC4 model'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#23515=IFCPROJECT('0pYKP47wH3MwYXYFzgmYrB',$,'My_Project',$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; +/*SIGNATURE; +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 +kUtvAzRpOhlc +ENDSEC;*/ diff --git a/backend/apps/ifc_validation/tasks.py b/backend/apps/ifc_validation/tasks.py index a561020..3205de6 100644 --- a/backend/apps/ifc_validation/tasks.py +++ b/backend/apps/ifc_validation/tasks.py @@ -23,6 +23,39 @@ logger = get_task_logger(__name__) +PROGRESS_INCREMENTS = { + 'instance_completion_subtask': 5, + 'syntax_validation_subtask': 10, + 'parse_info_subtask': 10, + 'prerequisites_subtask': 10, + 'schema_validation_subtask': 10, + 'digital_signatures_subtask': 5, + 'bsdd_validation_subtask': 0, + 'normative_rules_ia_validation_subtask': 20, + 'normative_rules_ip_validation_subtask': 20, + 'industry_practices_subtask': 10 +} + +assert sum(PROGRESS_INCREMENTS.values()) == 100 + + +def update_progress(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + return_value = func(self, *args, **kwargs) + try: + request_id = args[1] + # @nb not the most efficient because we fetch the ValidationRequest anew, but + # assuming django will cache this efficiently enough for us to keep the code clean + request = ValidationRequest.objects.get(pk=request_id) + increment = PROGRESS_INCREMENTS.get(func.__name__, 0) + request.progress = min(request.progress + increment, 100) + request.save() + except Exception as e: + print(f"Error updating progress for {func.__name__}: {e}") + return return_value + return wrapper + @functools.lru_cache(maxsize=1024) def get_absolute_file_path(file_name): @@ -168,6 +201,7 @@ def ifc_file_validation_task(self, id, file_name, *args, **kwargs): ) parallel_tasks = group([ + digital_signatures_subtask.s(id, file_name), schema_validation_subtask.s(id, file_name), #bsdd_validation_subtask.s(id, file_name), # disabled normative_rules_ia_validation_subtask.s(id, file_name), @@ -193,17 +227,12 @@ def ifc_file_validation_task(self, id, file_name, *args, **kwargs): @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def instance_completion_subtask(self, prev_result, id, file_name, *args, **kwargs): - # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 5 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() - # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.INSTANCE_COMPLETION) @@ -246,17 +275,12 @@ def instance_completion_subtask(self, prev_result, id, file_name, *args, **kwarg @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def syntax_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): - # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # set overall progress - PROGRESS_INCREMENT = 10 - request.progress = PROGRESS_INCREMENT - request.save() - # determine program/script to run check_program = [sys.executable, "-m", "ifcopenshell.simple_spf", '--json', file_path] logger.debug(f'Command for {self.__qualname__}: {" ".join(check_program)}') @@ -338,6 +362,7 @@ def syntax_validation_subtask(self, prev_result, id, file_name, *args, **kwargs) @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def parse_info_subtask(self, prev_result, id, file_name, *args, **kwargs): """" Parses and validates the file header @@ -346,11 +371,6 @@ def parse_info_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - - # increment overall progress - PROGRESS_INCREMENT = 10 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.PARSE_INFO) @@ -495,17 +515,13 @@ def parse_info_subtask(self, prev_result, id, file_name, *args, **kwargs): @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def prerequisites_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 10 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() - # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.PREREQUISITES) @@ -581,17 +597,13 @@ def prerequisites_subtask(self, prev_result, id, file_name, *args, **kwargs): @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def schema_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 10 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() - # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.SCHEMA) @@ -716,16 +728,91 @@ def is_schema_error(line): @shared_task(bind=True) @log_execution @requires_django_user_context -def bsdd_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): +@update_progress +def digital_signatures_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 10 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() + # add task + task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.DIGITAL_SIGNATURES) + + prev_result_succeeded = prev_result is not None and prev_result['is_valid'] is True + if prev_result_succeeded: + + task.mark_as_initiated() + + # determine program/script to run + check_script = os.path.join(os.path.dirname(__file__), "checks", "signatures", "check_signatures.py") + check_program = [sys.executable, check_script, file_path] + logger.debug(f'Command for {self.__qualname__}: {" ".join(check_program)}') + + # check schema + try: + # note: use run instead of Popen b/c PIPE output can be very big... + proc = subprocess.run( + check_program, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=TASK_TIMEOUT_LIMIT + ) + task.set_process_details(None, check_program) # run() has no pid... + except subprocess.TimeoutExpired as err: + task.mark_as_failed(err) + raise + except Exception as err: + task.mark_as_failed(err) + raise + + output = list(map(json.loads, filter(None, map(lambda s: s.strip(), proc.stdout.split("\n"))))) + success = proc.returncode >= 0 + valid = all(m['signature'] != "invalid" for m in output) + + with transaction.atomic(): + + # create or retrieve Model info + model = get_or_create_ifc_model(id) + model.status_signatures = Model.Status.NOT_APPLICABLE if not output else Model.Status.VALID if valid else Model.Status.INVALID + + def create_outcome(di): + return ValidationOutcome( + severity=ValidationOutcome.OutcomeSeverity.ERROR if di.get("signature") == "invalid" else ValidationOutcome.OutcomeSeverity.PASSED, + outcome_code=ValidationOutcome.ValidationOutcomeCode.VALUE_ERROR if di.get("signature") == "invalid" else ValidationOutcome.ValidationOutcomeCode.PASSED, + observed=di, + feature=json.dumps({'digital_signature': 1}), + validation_task = task + ) + + ValidationOutcome.objects.bulk_create(list(map(create_outcome, output)), batch_size=DJANGO_DB_BULK_CREATE_BATCH_SIZE) + + model.save(update_fields=['status_signatures']) + + if success: + reason = 'Digital signature check completed' + task.mark_as_completed(reason) + return {'is_valid': True, 'reason': reason} + else: + reason = f"Script returned exit code {proc.returncode} and {proc.stderr}" + task.mark_as_completed(reason) + return {'is_valid': False, 'reason': reason} + + else: + reason = f'Skipped as prev_result = {prev_result}.' + task.mark_as_skipped(reason) + return {'is_valid': None, 'reason': reason} + + +@shared_task(bind=True) +@log_execution +@requires_django_user_context +@update_progress +def bsdd_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): + + # fetch request info + request = ValidationRequest.objects.get(pk=id) + file_path = get_absolute_file_path(request.file.name) # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.BSDD) @@ -820,17 +907,13 @@ def bsdd_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def normative_rules_ia_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 15 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() - # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.NORMATIVE_IA) @@ -898,17 +981,13 @@ def normative_rules_ia_validation_subtask(self, prev_result, id, file_name, *arg @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def normative_rules_ip_validation_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 15 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() - # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.NORMATIVE_IP) @@ -974,17 +1053,13 @@ def normative_rules_ip_validation_subtask(self, prev_result, id, file_name, *arg @shared_task(bind=True) @log_execution @requires_django_user_context +@update_progress def industry_practices_subtask(self, prev_result, id, file_name, *args, **kwargs): # fetch request info request = ValidationRequest.objects.get(pk=id) file_path = get_absolute_file_path(request.file.name) - # increment overall progress - PROGRESS_INCREMENT = 10 - request.progress = min(request.progress + PROGRESS_INCREMENT, 100) - request.save() - # add task task = ValidationTask.objects.create(request=request, type=ValidationTask.Type.INDUSTRY_PRACTICES) diff --git a/backend/apps/ifc_validation_bff/views_legacy.py b/backend/apps/ifc_validation_bff/views_legacy.py index 8ae8966..69ddd12 100644 --- a/backend/apps/ifc_validation_bff/views_legacy.py +++ b/backend/apps/ifc_validation_bff/views_legacy.py @@ -184,6 +184,7 @@ def format_request(request): "p" if (request.model is None or request.model.status_ip is None) else request.model.status_ip ), "status_ind": "p" if (request.model is None or request.model.status_industry_practices is None) else request.model.status_industry_practices, + "status_signatures": "p" if (request.model is None or request.model.status_signatures is None) else request.model.status_signatures, "deleted": 0, # TODO "commit_id": None # TODO } @@ -571,6 +572,11 @@ def report(request, id: str): logger.info('Fetching and mapping bSDD done.') + signatures = [] + if report_type == "file": + task = ValidationTask.objects.filter(request_id=request.id, type=ValidationTask.Type.DIGITAL_SIGNATURES).last() + signatures = [t.observed for t in task.outcomes.iterator()] + response_data = { 'instances': instances, 'model': model, @@ -592,7 +598,8 @@ def report(request, id: str): "prereq_rules": { "counts": [grouped_gherkin_outcomes_counts["prerequisites"]], "results": grouped_gherkin_outcomes["prerequisites"] - } + }, + "signatures": signatures } } diff --git a/backend/apps/ifc_validation_models b/backend/apps/ifc_validation_models index 8231dcb..bd46388 160000 --- a/backend/apps/ifc_validation_models +++ b/backend/apps/ifc_validation_models @@ -1 +1 @@ -Subproject commit 8231dcb7e59aa3d88afc24ca7da9b2f4ecc37d52 +Subproject commit bd4638828f1a5d8f35df8d3e902f7cc491a92a36 diff --git a/backend/requirements.txt b/backend/requirements.txt index 9a51cdd..fe00cf0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -43,6 +43,7 @@ networkx pyyaml mpmath shapely +python-ranges # dev django-debug-toolbar diff --git a/frontend/src/DashboardTable.js b/frontend/src/DashboardTable.js index c9551f5..5fcee2b 100644 --- a/frontend/src/DashboardTable.js +++ b/frontend/src/DashboardTable.js @@ -14,6 +14,7 @@ import Typography from '@mui/material/Typography'; import Checkbox from '@mui/material/Checkbox'; import IconButton from '@mui/material/IconButton'; import InfoIcon from '@mui/icons-material/Info'; +import MailLockIcon from '@mui/icons-material/MailLock'; import Tooltip from '@mui/material/Tooltip'; import DeleteIcon from '@mui/icons-material/Delete'; //import ReplayIcon from '@mui/icons-material/Replay'; @@ -400,6 +401,13 @@ export default function DashboardTable({ models }) { ? `/sandbox/report_file/${context.sandboxId}/${row.code}` : `/report_file/${row.code}` )} +   + {row.status_signatures === 'v' && + + } + {row.status_signatures === 'i' && + + } {wrap_status(row.status_syntax, context.sandboxId ? `/sandbox/report_syntax/${context.sandboxId}/${row.code}` : `/report_syntax/${row.code}`)} diff --git a/frontend/src/Report.js b/frontend/src/Report.js index 73f177b..8889a09 100644 --- a/frontend/src/Report.js +++ b/frontend/src/Report.js @@ -14,6 +14,12 @@ import FeedbackWidget from './FeedbackWidget'; import SelfDeclarationDialog from './SelfDeclarationDialog'; import SearchOffOutlinedIcon from '@mui/icons-material/SearchOffOutlined'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; import { useEffect, useState, useContext } from 'react'; import { FETCH_PATH } from './environment' @@ -85,6 +91,15 @@ function Report({ kind }) { if (isLoggedIn) { console.log("Report data ", reportData); + const toTitle = s => + s.replace(/(^|_)([a-z])/g, (_, p1, p2) => (p1 ? ' ' : '') + p2.toUpperCase()); + function formatSignatureValue(v) { + if (typeof v === 'string' && v.length > 64) { + return v.substring(0, 61) + '...'; + } else { + return v; + } + } return (
} + {(kind === "file" && reportData && reportData.results && reportData.results.signatures) && + <> +

Digital signatures

+ {reportData.results.signatures.map((sig, sigIndex) => ( + + + + {["issuer", "subject", "signature_hash_algorithm_name", "rsa_key_size", "not_valid_after", "not_valid_before", "fingerprint_hex", "payload", "start", "end"].filter(x => sig[x]).map((item, itemIndex) => + + + {toTitle(item).replace('Rsa', 'RSA')} + + {formatSignatureValue(sig[item])} + + )} + +
+
+ ))} + }