diff --git a/readme.rst b/readme.rst index bfd76c5..e917846 100644 --- a/readme.rst +++ b/readme.rst @@ -76,8 +76,9 @@ TODO * Full certificate chain verification - * Automatic chain lookup and updates (for now, this is lazy and - there is a copy of public keychains in package). + * Verify revocation lists + + * Verify certificate validity time License ======= diff --git a/samples/exemple-attestation-vaccination-certifiee.txt b/samples/exemple-attestation-vaccination-certifiee.txt index 8cce265..e578ef1 100644 --- a/samples/exemple-attestation-vaccination-certifiee.txt +++ b/samples/exemple-attestation-vaccination-certifiee.txt @@ -1 +1 @@ -DC04FR0000011E6D1E6DL101FRL0DUPONTL1PAULL201011951L3COVID-19L4J07BX03L5PFIZER/BIONTECH - COMIRNATYL6PFIZER/BIONTECH - COMIRNATYL72L82L930042021LATECQW65D5MKNORNP2ZZJQECTCZAMG7H2ZTV35Z3PWPKJBM3DTA3YMBOD53OIEIXDD4WRSS46M7TG5EUWDVBEDLHAF7WTGU3GCTMHWRANQ +DC04FR0000011E6D1E6DL101FRL0DUPONTL1PAULL201011951L3COVID-19L4J07BX03L5PFIZER/BIONTECH - COMIRNATYL6PFIZER/BIONTECH - COMIRNATYL72L82L930042021LATECQW65D5MKNORNP2ZZJQECTCZAMG7H2ZTV35Z3PWPKJBM3DTA3YMBOD53OIEIXDD4WRSS46M7TG5EUWDVBEDLHAF7WTGU3GCTMHWRANQ \ No newline at end of file diff --git a/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-cycle.txt b/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-cycle.txt index 9cee48b..cf238bd 100644 --- a/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-cycle.txt +++ b/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-cycle.txt @@ -1 +1 @@ -DC04FR03AV011E6A1E6AL101FRL0THEOULE SUR MERL1JEAN PIERREL231051962L3COVID-19L4J07BX03L5COMIRNATY PFIZER/BIONTECHL6COMIRNATY PFIZER/BIONTECHL71L82L901032021LACOJQQOXUNIHI3JIQJIWNZ6LIZUB5PDMI46BBNQVDYJU3QLXI647IH3DML3UC5GJNIOPDLPDWNALTKUKMA5O7FZW5GLJCB2T7IDVRDPQSY +DC04FR03AV011E6A1E6AL101FRL0THEOULE SUR MERL1JEAN PIERREL231051962L3COVID-19L4J07BX03L5COMIRNATY PFIZER/BIONTECHL6COMIRNATY PFIZER/BIONTECHL71L82L901032021LACOJQQOXUNIHI3JIQJIWNZ6LIZUB5PDMI46BBNQVDYJU3QLXI647IH3DML3UC5GJNIOPDLPDWNALTKUKMA5O7FZW5GLJCB2T7IDVRDPQSY \ No newline at end of file diff --git a/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-test.txt b/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-test.txt index 742973c..e137110 100644 --- a/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-test.txt +++ b/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-invalid-test.txt @@ -1 +1 @@ -DC04FR03AHP11E5C1E5CB201FRF0F1SPECIMEN NOMF201012000F3MF43333F5XF6120420210800QEGUIDRKTYXUX7BVG5HQ6U3SOCLJSMRWXFVMLSQHPHT3XJCRKIFALVLWWHUIBHRBERKKY3MAOGM6CPMOQXPY2WIKWVJEHSAUUOKIGJY +DC04FR03AHP11E5C1E5CB201FRF0F1SPECIMEN NOMF201012000F3MF43333F5XF6120420210800QEGUIDRKTYXUX7BVG5HQ6U3SOCLJSMRWXFVMLSQHPHT3XJCRKIFALVLWWHUIBHRBERKKY3MAOGM6CPMOQXPY2WIKWVJEHSAUUOKIGJY \ No newline at end of file diff --git a/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-valid.txt b/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-valid.txt index 20ab6f3..c1ebedc 100644 --- a/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-valid.txt +++ b/samples/kit-deploiement_dispositif_de_controle_sanitaire_evenementiel/vaccine-valid.txt @@ -1 +1 @@ -DC04FR03AV011E6A1E6AL101FRL0AZAY LE RIDEAUL1JEAN-PIERREL204081952L3COVID-19L4J07BX03L5COMIRNATY PFIZER/BIONTECHL6COMIRNATY PFIZER/BIONTECHL72L82L931032021LATE53QFM66P34HCD24I46HH2XGIV5OXWQ2UR5DW5ZCQBLE4ME5WOPTR4S6MDYGYMRGFCRNFZHN26NA2KAJDNERKKGV4FSPCO6OLPTFELJQ +DC04FR03AV011E6A1E6AL101FRL0AZAY LE RIDEAUL1JEAN-PIERREL204081952L3COVID-19L4J07BX03L5COMIRNATY PFIZER/BIONTECHL6COMIRNATY PFIZER/BIONTECHL72L82L931032021LATE53QFM66P34HCD24I46HH2XGIV5OXWQ2UR5DW5ZCQBLE4ME5WOPTR4S6MDYGYMRGFCRNFZHN26NA2KAJDNERKKGV4FSPCO6OLPTFELJQ \ No newline at end of file diff --git a/setup.py b/setup.py index f7a9cdd..6317858 100644 --- a/setup.py +++ b/setup.py @@ -12,13 +12,15 @@ "Programming Language :: Python", ], package_data = { - 'tdd': ['chains/*.der'], + 'tdd': ['chains/*.der', 'chains/tsl_signed.xml'], }, include_package_data = True, use_2to3 = False, packages = find_packages(), install_requires = [ - "pycryptodome", + "cryptography", + "lxml", + "requests" ], dependency_links=[ ], diff --git a/tdd/chains/FR00.der b/tdd/chains/FR000001.der similarity index 100% rename from tdd/chains/FR00.der rename to tdd/chains/FR000001.der diff --git a/tdd/chains/FR01.der b/tdd/chains/FR01.der deleted file mode 100644 index 8f82eda..0000000 Binary files a/tdd/chains/FR01.der and /dev/null differ diff --git a/tdd/chains/FR02.der b/tdd/chains/FR02.der deleted file mode 100644 index e69de29..0000000 diff --git a/tdd/chains/FR03.der b/tdd/chains/FR03.der deleted file mode 100644 index cf2276d..0000000 Binary files a/tdd/chains/FR03.der and /dev/null differ diff --git a/tdd/chains/FR04.der b/tdd/chains/FR04.der deleted file mode 100644 index 88e933d..0000000 Binary files a/tdd/chains/FR04.der and /dev/null differ diff --git a/tdd/chains/FR05.der b/tdd/chains/FR05.der deleted file mode 100644 index 0043594..0000000 Binary files a/tdd/chains/FR05.der and /dev/null differ diff --git a/tdd/chains/Makefile b/tdd/chains/Makefile deleted file mode 100644 index 19d12bf..0000000 --- a/tdd/chains/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -all: FR01.der FR03.der FR04.der FR05.der - -tsl_signed.xml: - @echo "Fetching TSL" - @curl -s -o $@ https://ants.gouv.fr/files/25362bbf-a54e-4ed9-b98a-71e2382b54e0/tsl_signed.xml - -FR01.der FR03.der FR04.der FR05.der: tsl_signed.xml - @url=$$(xpath -q -e '//tsl:TSPTradeName[tsl:Name="$(subst .der,,$@)"]/../tsl:TSPInformationURI/tsl:URI[@xml:lang="fr"]/text()' $<) ; \ - echo "Fetching $@ from $${url}" ; \ - curl -s -o "$@" "$${url}" - -clean: - rm -f FR01.der FR03.der FR04.der FR05.der tsl_signed.xml - -# Unbundle DER from "multipart/mixed + chunked" response -# $ csplit --suppress-matched -f '/tmp/' -b foo-%02d-file.der -z FR03.der $'/^--End\r/+1' '{*} && sed -ri '/^\r*$|--End/d' /tmp/foo* -# Get individual CN -# $ for i in /tmp/foo-*; do openssl x509 -in $i -inform DER -subject -nameopt multiline -nocert; done|grep commonName|sort |uniq -dc diff --git a/tdd/chains/tsl_signed.xml b/tdd/chains/tsl_signed.xml index 0b9951d..ffe4c48 100644 --- a/tdd/chains/tsl_signed.xml +++ b/tdd/chains/tsl_signed.xml @@ -1,7 +1,7 @@ 5 - 18 + 22 http://uri.etsi.org/TrstSvc/eSigDir-1999-93-EC-TrustedList/TSLType/generic Agence Nationale des Titres Sécurisés @@ -24,7 +24,7 @@ - mailto:ants-cev-ac@interieur.gouv.fr + mailto:2ddoc-ants@interieur.gouv.fr @@ -46,12 +46,12 @@ https://ants.gouv.fr/nos-missions/les-solutions-numeriques/2d-doc 3653 - 2023-11-27T15:30:00Z + 2025-08-25T11:00:00Z - 2024-05-27T15:30:00Z + 2026-02-25T11:00:00Z - https://ants.gouv.fr/nos-missions/les-solutions-numeriques/2d-doc + https://pub.ants.gouv.fr/2D-DOC/V1/PRD/01_TSL/tsl_signed.xml @@ -281,5 +281,119 @@ + + + + IDnow SAS + IDnow SAS + + + FR06 + FR06 + + + + + 122, rue Robert Keller + Cesson Sevigne + France + 35510 + FR + + + 122, rue Robert Keller + Cesson Sevigne + France + 35510 + FR + + + + mailto:contact@idnow.io + + + + http://pki-g3.ariadnext.fr/pki-2ddoc.der + http://pki-g3.ariadnext.fr/pki-2ddoc.der + + + + + + http://uri.etsi.org/TrstSvc/Svctype/CA/PKC + + IDnow SAS FR06 2D-Doc Barcode signature + IDnow SAS FR06 2D-Doc Barcode signature + + + + + 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 + + + + http://uri.etsi.org/TrstSvc/Svcstatus/inaccord + 2024-03-20T23:00:00Z + + + + + + + + Goodflag + Goodflag + + + FR07 + FR07 + + + + + 9, Av Maréchal Leclerc + ST ANDRE LES VERGERS + France + 10120 + FR + + + 9, Av Maréchal Leclerc + ST ANDRE LES VERGERS + France + 10120 + FR + + + + mailto:pki@sunnystamp.com + + + + https://pki2.sunnystamp.com/certs/fr07.der + https://pki2.sunnystamp.com/certs/fr07.der + + + + + + http://uri.etsi.org/TrstSvc/Svctype/CA/PKC + + Goodflag FR07 2D-Doc Barcode signature + Goodflag FR07 2D-Doc Barcode signature + + + + + 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 + + + + http://uri.etsi.org/TrstSvc/Svcstatus/inaccord + 2024-10-09T11:39:40Z + + + + -/descendant::ds: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2023-11-27T14:25:22ZFfGoEUzk9kHYntyK4aWBpZ2apRk2LKDIbr+k2y6Pqx3kZejxrHTr0M/KhyBBio7zjgDxmZy2PsaTeo3iz5L1Ng==MIG7MIGkpIGhMIGeMQswCQYDVQQGEwJGUjEwMC4GA1UECgwnQWdlbmNlIE5hdGlvbmFsZSBkZXMgVGl0cmVzIFPDqWN1cmlzw6lzMRcwFQYDVQQLDA4wMDAyIDEzMDAwMzI2MjE4MDYGA1UEAwwvQXV0b3JpdMOpIGRlIENlcnRpZmljYXRpb24gU2VydmljZXMgQXBwbGljYXRpZnMxCjAIBgNVBAUTATMCEhEhkVnpA83YUNnHBzEhsVEEuQ==text/xml \ No newline at end of file +/descendant::ds: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2025-08-25T08:19:41Z0K5zi/6+OVrhIzEh65VuMDOC95mu2FZ2nddE2eEonG/BN5oO73rwq/JpJ2XEQf5yImOPCQvOL5qZRmXKFndsCw==MIG9MIGkpIGhMIGeMQswCQYDVQQGEwJGUjEwMC4GA1UECgwnQWdlbmNlIE5hdGlvbmFsZSBkZXMgVGl0cmVzIFPDqWN1cmlzw6lzMRcwFQYDVQQLDA4wMDAyIDEzMDAwMzI2MjE4MDYGA1UEAwwvQXV0b3JpdMOpIGRlIENlcnRpZmljYXRpb24gU2VydmljZXMgQXBwbGljYXRpZnMxCjAIBgNVBAUTATQCFGiMW7hkBhXrS/+ceMwN3QAgjO/Ztext/xml \ No newline at end of file diff --git a/tdd/doc.py b/tdd/doc.py index 0c48c27..fe1157e 100644 --- a/tdd/doc.py +++ b/tdd/doc.py @@ -1,6 +1,9 @@ from .header import Header from .message import C40Message from base64 import b32decode +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature __doc__ = """ Documentation representation. @@ -41,7 +44,12 @@ def from_code(cls, doc): def signature_is_valid(self, keychain): """ Check signature against given keychain. If key is not - available, KeyError is raised. + available, InvalidSignature is raised. """ cert = keychain.lookup(self.header.ca_id, self.header.cert_id) - return cert.pubkey.signature_is_valid(self.signed_data, self.signature) + r = int.from_bytes(self.signature[:32], "big") + s = int.from_bytes(self.signature[32:], "big") + signature = encode_dss_signature(r, s) + cert.public_key().verify(signature, self.signed_data, ec.ECDSA(hashes.SHA256())) + return True + diff --git a/tdd/keychain.py b/tdd/keychain.py index 7ddbb47..51459bd 100644 --- a/tdd/keychain.py +++ b/tdd/keychain.py @@ -1,157 +1,60 @@ -from Crypto.Util import asn1 +from base64 import b64decode +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.x509.oid import NameOID +from importlib.resources import files +from lxml import etree +import requests __doc__ = "Keychain management" -der_obj = lambda x: asn1.DerObject().decode(x).payload -der_seq = lambda x: asn1.DerSequence().decode(x) -der_set = lambda x: asn1.DerSetOf().decode(x) -der_oid = lambda x: asn1.DerObjectId().decode(x).value -der_int = lambda x: asn1.DerInteger().decode(x) -#der_str = lambda x: asn1.DerOctetString().decode(x) -der_str = lambda x: str(asn1.DerObject().decode(x).payload, "utf-8") -der_bits = lambda x: asn1.DerBitString().decode(x).value - -class PublicKey: - """ - A public key store. Only NIST P-256 is supported for now. - """ - id_prime256v1 = "1.2.840.10045.3.1.7" - id_ecPublicKey = "1.2.840.10045.2.1" - - def __init__(self, type, value): - self.type = type - self.key = value - - @classmethod - def from_der(cls, data): - from Crypto.PublicKey import ECC - - type, key = der_seq(data) - pub_priv, algo = der_seq(type) - pub_priv = der_oid(pub_priv) - try: - algo = der_oid(algo) - except ValueError: - algo = der_seq(algo) - - if pub_priv != cls.id_ecPublicKey: - raise ValueError("Not a public key") - if algo == cls.id_prime256v1: - type = 'p256v1' - - return cls(type, ECC.import_key(data)) - - def signature_is_valid(self, data, signature): - from Crypto.Signature import DSS - from Crypto.Hash import SHA256 - - verifier = DSS.new(self.key, 'deterministic-rfc6979') - try: - verifier.verify(SHA256.new(data), signature) - return True - except: - return False - -class Dn: - """ - X-509 Certificate DN low-tech parser/container - """ - def __init__(self, **kw): - self.__values = kw - - def __getitem__(self, name): - return self.__values[name] - - def __setitem__(self, name, value): - self.__values[name] = value - - - oids = { - "2.5.4.3": "commonName", - "2.5.4.4": "surname", - "2.5.4.5": "serialNumber", - "2.5.4.6": "countryName", - "2.5.4.7": "localityName", - "2.5.4.8": "stateOrProvinceName", - "2.5.4.9": "streetAddress", - "2.5.4.10": "organizationName", - "2.5.4.11": "organizationalUnitName", - "2.5.4.12": "title", - "2.5.4.13": "description", - "2.5.4.14": "searchGuide", - } - - @classmethod - def from_der(cls, data): - kv = {} - for item in der_seq(data): - s = der_seq(der_set(item)[0]) - oid = der_oid(s[0]) - try: - k = cls.oids[oid] - except KeyError: - continue - kv[k] = der_str(s[1]) - - return cls(**kv) - -class Certificate: - """ - X-509 Certificate DN low-tech parser. Does not verify cert signature. - """ - def __init__(self, issuer, subject, pubkey): - self.issuer = issuer - self.subject = subject - self.pubkey = pubkey - - @classmethod - def from_der(cls, data): - from Crypto.PublicKey import ECC - - der = der_seq(data) - - payload = der_seq(der[0]) - info = der[1] - sign = der[2] - - fields = list(payload) - while not isinstance(fields[0], int): - if fields[0][0] & 0xc0 == 0: - break - fields.pop(0) - - serial = fields[0] - signature = der_seq(fields[1]) - issuer = Dn.from_der(fields[2]) - subject = Dn.from_der(fields[4]) - pubkey = PublicKey.from_der(fields[5]) - - return cls(issuer, subject, pubkey) - class KeyChain: """ Certificate store, indexes certificates through common name of issuer and subject. This is somehow 2D-Doc specific. """ - def __init__(self, certs = []): - self.certs = list(certs) + def __init__(self, certs = {}, cas = {}): + self.certs = dict(certs) + self.cas = dict(cas) def lookup(self, ca_cn, cert_cn): - for c in self.certs: - if c.issuer["commonName"] == ca_cn and \ - c.subject["commonName"] == cert_cn: - return c - - raise KeyError((ca_cn, cert_cn)) + name = ca_cn + cert_cn + if name in self.certs: + return self.certs[name] + + if ca_cn in self.cas: + ca = self.cas[ca_cn] + else: + # Search for CA + chains = files('tdd.chains') + ca_file = chains.joinpath(ca_cn + ".der") + if not ca_file.is_file(): + self.download_ca(ca_cn) + + with ca_file.open("rb") as fd: + ca = x509.load_der_x509_certificate(fd.read()) + self.cas[ca_cn] = ca + + chains = files('tdd.chains') + chain_file = chains.joinpath(name + ".der") + if not chain_file.is_file(): + # Download certificates + self.download_certs(ca_cn) + with chain_file.open("rb") as fd: + cert = x509.load_der_x509_certificate(fd.read()) + + # Verify the certificate is signed by the CA + cert.verify_directly_issued_by(ca) + self.certs[name] = cert + return cert - def der_multipart_load(self, fd): + def der_multipart_load(self, blob): boundary = b"--End\r\n" end = b"--End--" - blob = fd.read() blob = blob.split(end)[0] certs = blob.split(boundary) - for i, c in enumerate(certs): + for _, c in enumerate(certs): if c.endswith(b'\r\n'): c = c[:-2] if not c: @@ -164,40 +67,104 @@ def der_multipart_load(self, fd): if k.lower() == "content-type": ct = v if ct == "application/pkix-cert": - self.der_add(data) + self.save_der(data) - def der_add(self, der): + def save_der(self, der, ca=False): try: - cert = Certificate.from_der(der) + cert = x509.load_der_x509_certificate(der) except ValueError: return - self.certs.append(cert) - -def internal(): - """ - Spawn a keychain with all built-in certificated loaded. - """ - from importlib.resources import files - - k = KeyChain() - chains = files('tdd.chains') - for chain in ["FR01", "FR02", "FR03", "FR04", "FR05"]: - chain_file = chains.joinpath(chain + ".der") - with chain_file.open('rb') as fd: - k.der_multipart_load(fd) - with chains.joinpath("FR00.der").open('rb') as fd: - k.der_add(fd.read()) - return k + + cn_attributes = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME) + issuer_cn = cn_attributes[0].value if cn_attributes else None + cn_attributes = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) + subject_cn = cn_attributes[0].value if cn_attributes else None + + if ca: + issuer_cn = "" + + chains = files('tdd.chains') + name = issuer_cn + subject_cn + chain_file = chains.joinpath(name + ".der") + with chain_file.open("wb") as f: + f.write( + cert.public_bytes( + encoding=serialization.Encoding.DER + ) + ) + + def download_ca(self, ca_cn): + ns = {"tsl": "http://uri.etsi.org/02231/v2#"} + + tsl = files('tdd.chains').joinpath("tsl_signed.xml") + with tsl.open("rb") as f: + tree = etree.parse(f) + + cert_b64_list = tree.xpath( + """ + //tsl:TSPTradeName[tsl:Name[@xml:lang='en']=$ac_name] + /ancestor::tsl:TrustServiceProvider + /tsl:TSPServices + /tsl:TSPService + /tsl:ServiceInformation + /tsl:ServiceDigitalIdentity + /tsl:DigitalId + /tsl:X509Certificate/text() + """, + ac_name=ca_cn, + namespaces=ns + ) + + if len(cert_b64_list) == 0: + print("CA not found") + return False + else: + self.save_der(b64decode(cert_b64_list[0]), ca=True) + + return True + + def download_certs(self, ca_cn): + tsl = files('tdd.chains').joinpath("tsl_signed.xml") + with tsl.open("rb") as f: + tree = etree.parse(f) + + ns = {"tsl": "http://uri.etsi.org/02231/v2#"} + uri_list = tree.xpath( + """ + //tsl:TSPTradeName[tsl:Name[@xml:lang='fr']=$ac_name] + /ancestor::tsl:TSPInformation + /tsl:TSPInformationURI + /tsl:URI[@xml:lang='fr']/text() + """, + ac_name=ca_cn, + namespaces=ns + ) + if len(uri_list) == 0: + print("URL not found") + else: + print(f"Loading: {uri_list[0]}") + headers = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64)" + } + with requests.get(uri_list[0], headers=headers) as response: + data = response.content + self.der_multipart_load(data) if __name__ == "__main__": import sys + k = KeyChain() if len(sys.argv) < 2: - k = internal() + # Test certificate + ca_cn = "FR00" + cert_cn = "001" + test_chains = files('tdd.chains').joinpath('FR000001.der') + with test_chains.open("rb") as fd: + test_cert = x509.load_der_x509_certificate(fd.read()) + k.certs["FR000001"] = test_cert else: - k = KeyChain() - for fn in sys.argv[1:]: - k.der_multipart_load(open(fn, 'rb')) + for name in sys.argv[1:]: + k.lookup(name[0:4], name[4:]) - for c in k.certs: - print(c.issuer["commonName"], c.subject["commonName"]) + for (name, c) in k.certs.items(): + print(f"name: {name} serial number: {c.serial_number}") diff --git a/tests/conftest.py b/tests/conftest.py index 75ab4d2..f5269f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,10 @@ Pytest fixtures for 2D-Doc specification tests. """ import pytest -from pathlib import Path -import yaml +from cryptography import x509 +from importlib.resources import files +from pathlib import Path def discover_test_cases(samples_dir: Path): """ @@ -36,5 +37,8 @@ def keychain(): """ Load the internal keychain with bundled certificates. """ - from tdd.keychain import internal - return internal() + from tdd.keychain import KeyChain + test_chains = files('tdd.chains').joinpath('FR000001.der') + with test_chains.open("rb") as fd: + test_cert = x509.load_der_x509_certificate(fd.read()) + return KeyChain(certs = {"FR000001": test_cert})