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})