From 664f033d09990264d6a92298594a2fe576042cdd Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 29 Apr 2026 21:09:47 +0200 Subject: [PATCH 1/6] Add ML-DSA support --- fido2/cose.py | 96 ++++++++++++++++++++++++++++++++------- fido2/hid/__init__.py | 2 +- tests/test_attestation.py | 23 +++++++++- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/fido2/cose.py b/fido2/cose.py index 1a67567a..143d1ac7 100644 --- a/fido2/cose.py +++ b/fido2/cose.py @@ -41,6 +41,10 @@ from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes +_backend = default_backend() +_mldsa = hasattr(_backend, "mldsa_supported") and _backend.mldsa_supported() + + class CoseKey(dict): """A COSE formatted public key. @@ -118,7 +122,7 @@ def parse(cose: Mapping[int, Any]) -> CoseKey: @staticmethod def supported_algorithms() -> Sequence[int]: """Get a list of all supported algorithm identifiers""" - algs: Sequence[type[CoseKey]] = [ + algs: list[type[CoseKey]] = [ ES256, EdDSA, ES384, @@ -127,6 +131,9 @@ def supported_algorithms() -> Sequence[int]: RS256, ES256K, ] + if _mldsa: + algs.extend([MLDSA44, MLDSA65, MLDSA87]) + return [cls.ALGORITHM for cls in algs] @@ -146,9 +153,7 @@ def verify(self, message, signature): raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256R1() - ).public_key(default_backend()).verify( - signature, message, ec.ECDSA(self._HASH_ALG) - ) + ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): @@ -188,9 +193,7 @@ def verify(self, message, signature): raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP384R1() - ).public_key(default_backend()).verify( - signature, message, ec.ECDSA(self._HASH_ALG) - ) + ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): @@ -221,9 +224,7 @@ def verify(self, message, signature): raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP521R1() - ).public_key(default_backend()).verify( - signature, message, ec.ECDSA(self._HASH_ALG) - ) + ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): @@ -251,7 +252,7 @@ class RS256(CoseKey): def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( - default_backend() + _backend ).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG) @classmethod @@ -267,7 +268,7 @@ class PS256(CoseKey): def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( - default_backend() + _backend ).verify( signature, message, @@ -342,7 +343,7 @@ class RS1(CoseKey): def verify(self, message, signature): rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key( - default_backend() + _backend ).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG) @classmethod @@ -361,9 +362,7 @@ def verify(self, message, signature): raise ValueError("Unsupported elliptic curve") ec.EllipticCurvePublicNumbers( bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256K1() - ).public_key(default_backend()).verify( - signature, message, ec.ECDSA(self._HASH_ALG) - ) + ).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG)) @classmethod def from_cryptography_key(cls, public_key): @@ -462,3 +461,68 @@ def derive_public_key(self, ikm: bytes, ctx: bytes) -> Tuple[CoseKey, Mapping]: } return pk_cose, args + + +# MLDSA support was added in cryptography 47.0.0, and requires a supported backend +if _mldsa: + from cryptography.hazmat.primitives.asymmetric import mldsa + + class MLDSA44(CoseKey): + ALGORITHM = -48 + + def verify(self, message, signature): + if self[1] != 7: + raise ValueError("Invalid key type") + pk = mldsa.MLDSA44PublicKey.from_public_bytes(self[-1]) + pk.verify(signature, message) + + @classmethod + def from_cryptography_key(cls, public_key): + assert isinstance(public_key, mldsa.MLDSA44PublicKey) # noqa: S101 + return cls( + { + 1: 7, + 3: cls.ALGORITHM, + -1: public_key.public_bytes_raw(), + } + ) + + class MLDSA65(CoseKey): + ALGORITHM = -49 + + def verify(self, message, signature): + if self[1] != 7: + raise ValueError("Invalid key type") + pk = mldsa.MLDSA65PublicKey.from_public_bytes(self[-1]) + pk.verify(signature, message) + + @classmethod + def from_cryptography_key(cls, public_key): + assert isinstance(public_key, mldsa.MLDSA65PublicKey) # noqa: S101 + return cls( + { + 1: 7, + 3: cls.ALGORITHM, + -1: public_key.public_bytes_raw(), + } + ) + + class MLDSA87(CoseKey): + ALGORITHM = -50 + + def verify(self, message, signature): + if self[1] != 7: + raise ValueError("Invalid key type") + pk = mldsa.MLDSA87PublicKey.from_public_bytes(self[-1]) + pk.verify(signature, message) + + @classmethod + def from_cryptography_key(cls, public_key): + assert isinstance(public_key, mldsa.MLDSA87PublicKey) # noqa: S101 + return cls( + { + 1: 7, + 3: cls.ALGORITHM, + -1: public_key.public_bytes_raw(), + } + ) diff --git a/fido2/hid/__init__.py b/fido2/hid/__init__.py index 3ce21107..faa79df0 100644 --- a/fido2/hid/__init__.py +++ b/fido2/hid/__init__.py @@ -237,7 +237,7 @@ def _do_call(self, cmd, data, event, on_keepalive): else: # Continuation packet r_seq = struct.unpack_from(">B", recv)[0] recv = recv[1:] - if r_seq != seq: + if r_seq != seq & 0x7F: raise ConnectionFailure("Wrong sequence number") seq += 1 diff --git a/tests/test_attestation.py b/tests/test_attestation.py index cbe6d9e4..2cf1fae4 100644 --- a/tests/test_attestation.py +++ b/tests/test_attestation.py @@ -28,7 +28,7 @@ import unittest from cryptography.exceptions import UnsupportedAlgorithm, _Reasons - +from fido2 import cbor from fido2.attestation import ( AndroidSafetynetAttestation, AppleAttestation, @@ -44,6 +44,7 @@ UnsupportedType, verify_x509_chain, ) +from fido2.ctap2.base import AttestationResponse from fido2.webauthn import AuthenticatorData # GS Root R2 (https://pki.goog/) @@ -340,3 +341,23 @@ def test_apple_attestation(self): self.assertEqual(res.attestation_type, AttestationType.ANON_CA) self.assertEqual(len(res.trust_path), 2) verify_x509_chain(res.trust_path) + + +def test_mldsa87_attestation(): + from fido2.cose import _mldsa + + if not _mldsa: + raise unittest.SkipTest( + "MLDSA87 is not supported by this version of cryptography" + ) + + client_data_hash = bytes.fromhex( + "8DD60738663AA38FE50FD17668AEA9B451FB07E255B8D6241D4C3BAE6D4E5B9E" + ) + resp = bytes.fromhex( + "a301667061636b656402590aa2a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce19474100000003f8a011f38c0a4d15800617111f9edc7d0041b4521c8292e229d7e1288e890fb2c428bfbbf22a128992f23bea11ed7f0297aad31117161dbe644b41ab9856967cc2cac498f2bed4d9b97dabd33be53f6d65a73ca3010703383120590a2030409622070f2403c8078f60d0659a576861b8ab96d09ccaafd6c4c62002ce5ff836937a3f53559ce7067f86d8581aaa4d0e03ab3cc2d48084834723fb309ed6dbe4b591dd25f207eebeb0f9c17cb162187d43507f3a3cd968c691f8e00d5b16148105447f20c779e9f2665aa1407c783ba4df241996a28902d5797cedd5c9842b7c16e1069d4341daa9952dfe6d35255fab874df83e06c4ac34a30b9e2f2b564582305143dee7e0876501ca5188824a5bbafaf456f0e0be7d59ee0a1402df0d7fb1af6c0cff70c93d18c4c212b2499088562cca2fb94b741493a77a96b863ed30555f61026c058617ca1ccae558b95b3180f1e2906a50c96653eb24b9667dc229a3bed47960fbbde7f5c22692a5130ace2df12f8b0afc7fc64db60496d1bb6b799c87eb684c07bc488eef3a3d4b05c0406951de71912569cc5e1e38e4924f8fabd0a592a50760a682198c3460b99b88e36c8b91c95f89cc1a945284c52050e43b86cc985c9eb6a654b9dc9dba5a50aa6e547efa0700656195239fd022686d8c299f6ab19bc8bc43f6ebdd9dab60131fb1696ec7399ac87d23cf8c199ab235d423362fb2ca0bf32736605f7c397b3d9238ee15240366c9fd9e70235f8c2af31ded369080d92116b57a4a98b64374c9d5064def862def8772b74f94e768d1352d3ff07f8809a9d462b5a95a1409645d5876806db4b8d8094315f5f3af1a784758c35af911de8492be3260aa490a1a22088b730f30aa885348646c852535342f0933379dac7eee436ebda40345763225fac4d0b9c313508feeb0c8fa7a133f34f9c6f688537a085dc40dac2a4dcf9a70910a2c0b1925017977f30cfbd4ddf3ab064a75c35c94449dbeefd8d5f6d088c80850b327eb522f70b43e48960dbc5722ed278bf0c7df773575edfc56d7c891096766742863c533f20f60398e8f9420d12d1a47fba7173c87ebf80edccf9ab309c28d926e2783e3628645692ccc714887953e66302d7d19eeeee204247c975d040b728b1403ed8ceffa3746dd4614f250a5a593157d9e48b31a8d04363168db6a74645d12ad459d707433c57b21475c2b6035431e551daa8670684882d3355c43662a1e6e3ac52a0286a57035fddb56046d9094acb7be51a80e0449027184f62e79443a923179e2e7dc62abfe0854d2e82272bbd676ef23f378cc975f4aad219bc6e51964da9b69d20c4da6600acc66cb7442894d1bf41d6818ebb19b20482fc12e9322698de6f6ab2bace6d3176d11a1ab41b2f05c64153ac477329760e0c37e3c9656a48f36d4d180de7508dab01be4cd1f1b3a2d7cf8da6d95222800b4a439a4a2de08a43628887102a69c11302f5ad90bfdcfe1c9648785194b77cf7ad830a5f8fc3752784bb99942e575080b3027e715b11384f78c89b95c68aa788c7223f4b65c669244435664045a558496cb16e9babc54dc126dd35326de9f4060c833177fff86cfc6be8d85300e821b5922428d8a6c18a5975848858cf70c4fd37c0242897485f0bd0b000fb54d659adfd481ea91bdec2c690ba8245d1314b20428cde1683765aa82ad5070797c904ff317959d257314b5f2201eb12a1d341d17eb8d23ff27c97fe48b5f0b01d952bbb3231a9ffa163d3f782af313bcff044541e53779712b4372337577f0021471b72d905059da03bdd9d1abda2846c68f6a27283deba3e5634528f75fd41ba1a3e60996dc5cc9ad36befb0e97fc412b264a41ddd8716218f738a8e0eb2bdf0b360e12c3a58100c75206a1aff563e7caed10b6fccd03b0908689ae6ba850b2635a63d9530d47095e764f4672a7ac72df16f0428b05eb4ef41a4af0fd6930bc371ed92d44e879e0d18b8804ba3e35bfcfc42038deb5580a6802a1c791458040af3f1cb41f8a26be32ed48287b9d7f3b37e5971c3a84553bd3da38c5fc688531a99555bbb6bd9d893b80bebb706f910c0272ff118e0ad2854d67bb8aafa477f6974bca0c24decf7cf75c167176fa77894857e2af8cc18e65879d62adae469bc007954e16ad88e1b469b0f12bf7022a126ee16c180aaecb1c62bfb09df905cf8b4bc9307dfaebb992db5c3f19b18d0b9a227c26b340c80c3542273a46869e7a5ae58d007a8e22bcaf09230cfc0498a12038c44e6897a9eedd629090a7f4df3dc44c85a395445583fee3f95083e2616aebc88d779d4c48582c790452d42062a2ea4ae72e63390b5b6624509195eecf3986211e105f79ac6716de688c956a267acc9f6313ff14208b8361e4c97bd7f9752057079b81d7c55484b1518d6d15da7aa5d1b2e1bffa0d2a187da06330ef395d26d59b53b7964d7f0f30d10eb5c1e2e404d6d2578d48421831ee5e997289a3b3ae31523e4a952c9771e3c8983927a29bd4f05318b77304310eabfedd038e4c73e3d7ecbe602f84c5ad18c71631e71f25e3c3d7bb13f3377d3677d91bafe9f4b5b2d895f671ec87c05d5ec61153902e6fb36e82a36862152e8ed02fa1755889fafa5ceca09401291a82ae9260e6dcbe88108afe82ab2a10b1cb60123361535a6468f9eec28b69eae05aab813e80d22e687ee43a8842bf860c6ff9db3e443e65323a1116f42b57cd0e2e73890c8c4edbe0485ead2acdb3b31e0c27a7901441972368613d8477850c508e671cd718832edc817d2390e1cd455ef9ccc40502ffd2dd398e85b689f0765246a3060a252a2dc6f5e3ab9a996146a286bbcdc850cd0f6520e7e2f51a145f7a787a7b38f8af93b6a13a53a3153361dba00d55a674a0086b2176dde8692c1f4f4adeafd46b1d2afa925aec978cc39d63a802f4bb18ac5596daeb00a53ad03ec39906e71b6465f0e68351fba15915597bf0c4a5c52916e8257d0063ba64b066a827e74bce5ebb7967a322a2b2da4e5412afd9a0d732921c7765c2b82ab93fe28706b6302bca7adb5e10d7a0d829895b2009e7edc0085a689c7da14270681133185d7277b0942d43d186a97093f5edb1d1c7abcba3fb87c9888aeffbf38ef1a13e386daa28b5001f3704dcc916d4b0794b35a99587428834fe9c7fa5d1fb6621cb7de532070f5a787a15b3cdc405d58d0c73055719a3d8c67277fdd8d02ab7bcaf380fcd3b881eb45ca66a4d08695a10604533e175a6fc299a4a917cd91b2a3ef0da1e07c2f2208c6cf9f57e5e1386baa04d246b9b26c82174b3bc3350ebac34563f6ffb316c8850abaac7f15e65d50f64857af1ff1f0b1e4deab1275b64237ee05c9aed9ae178b98b4d474281c062d57d8b3a6d4c39dea7f458ee93c79420339e234abcf7bfb1b2cae01dd26f53e94c9d56c2caa72f565d02d74e72d2b581eae5f2908f386a7b9c916372166a31fd3db538779ab10db8a3dc8cbbe269b2068324c7f09d1e1eec879b080f9031fd05fac02b3613ddb354065b4298ca072ed0036971be5b54b13a6ad12ceae332ede0cf16255256d6d65d79a67a0d1d6755ccc09af78c323953c6030de19e14f7087540073652308abc1df88cda159db3081e777e75344d594aabc2123fe4c07f81841fb422f282633a9046ee4fa5a4dbac522f41ba012fc9aa1f1b65484bef151b7bb342c1b50002f322cd7052028c85e6ffefb133e4495319d1dcc60e6b930896090a922683c25f5c3b5624a04b02d557027ff0574910e677f9294a5adb9914abcdef4303a363616c6738316373696759121381468d5f35d2bfc2298bef515f902cffe7761fcaf8c3357434b8d15b352b90df83d99b1c1d88231d36a88d67c0a0a0d5568d8da11bff430fd61b7b5bf08bd383d81958d35e151c698756adb8cde80d576350461d9798f1866c405010eea5b4cebc3d7b8588bed1c37f98ca11adb38ac61f8406e8d5a140a9d4e0ffaf0d36b01d7405d23d51a07721b7e9c790e4524143b7f415bc5a543731529f64e7b94cde924047902f1b6f645a1eeff51cc13bb75de40cd96c66ac831636f702b411dd0e1172a207c5a9f87bb1d700d236547c08a12afcb796a9eb388d4eea5ea04fb3260e1b4b0ddc2d5a780d6601889fbb3082e00878bcd24996e35c4bc732886223eeed9663561d81a946d28ff660936f4b59ed31542d6241209790e3483fcf6876c9a44e76b85ca77d5fc2775ed8f1481f9c1e1a52a1465623faa219bd2d484aab19ad6139eb3a411f96072bdf2c143bd0ff39e7e0cd63edd0615ae61fa4589bd4f8d12cdfd8c2431146658f25ab2ac437b9c291e1d77b939a5b8222787306e2eb8121602b994109b39d6100ed4f0a4282e2298010c42cb9344b06e0930afbcbd02ecf4ee1a0ff1736cd14cbf10d1c70474a0599a626fdba35e21215bb0cf069e58eac85e0d20c0fabed199f00c5172300a09b4b78eb452d42abbfca9219ec29ddef06b89067dbbf345f1e931fe8126f6764461bcbd1565a766dd129ae6a80ae8056774e231310def5e6ba75ca113099aa986ab18c89e5476bd02becb03633f5c5f14e6968311345bdae8614aac484bbc7455969010befa707fe20311ac96fae8f6d963709fda623cbb960aa957a012b54293a35a63b49f439a428f6ce2f5ec5c4ce6955438cadd27e40a41173bc8f2dc378ea4e7dc711d1ce82b2ef5da271998207eabf0e87cf8fa44cc502ae32dab6df34e655cf2d23469faee996ada8f8acfa95d16c04921b1f6d743389645a6222e642924ee7f289edcc3e46546ad23ac5c584f2b44bc96dafc97f3ce8bd4cf7d38c17827620d2d40ece12cb2afa510058802f0fb0c60013fc835b67e29f104dae89b4c523180224995b03baf469291fef3e76e19726a7282a22a5158017f9d498b18ad6416c4678a29fb947e4a0cae32c9a7d1b30941afea40b73015db678579317a810d132fbc5225d13d610f00a97cfb2515c5adc94e61244fe805c0202a6d4325a691566bc3d371177795a5e6d537a17f3cf2c8cfb3c18d8ae04ce8505f3a125ab9efd31ce201e860b3ab4a12d13ad05842a26a589f8753fe2a887cae1d6f48c91900acc1fa22265facf1d265e98ce1168e8b39b52386a4ee16accf9a6727c7bd7268f7ae75e86057c538f07b56f43be4e710d3cda5bd0656c0bcac47d254772de8c4cc9e0c947043606607b49c520ff1b21a7817a439e427d060ae2b521ed7680ddfedfa1840181049d5a057e546ebbb07b5875feef823e9bdce512ffecda5553f7572baad732d65abcc8c42fbb6b118e78368a5bc276604050d075949bbc473bd9d8a52e975cad39c5031969edea125810d13034f536e5fb7d41aab0197055c596c043dbd4917f876da02c57c50a12ed4934b38a6a5c169fdd19279b9803f7193e2aee4cd3c85346acd04f710baaa8120b8e692bb69a2db6ebc5c34cc9918a8dae32d6d8f959eb4ec7c3efbc4cf61ac6cb0a7fac5528cd874465d73d60597041cabb5bd72c632594ecbd43cc0bc86327aaa1795061272379246ae69f812b65a44f49cd0d839b45af839f533f412da32b6e257b998c19fb5fbcea2868a77c90454c594f4c01889afc90df7a20c3087b7d52e798a1155c1e0e1241212c21c5b24d3a6943a0e42e1bc4119d25e02b663ae297015920d2aff27716b2966f7ccf9f1785065c22fc2584ca49f9dc97b4dfd578f05cb47e160ff3c33b27d2e5e83d8caecba8990fde99de5501ed6db6798b195ec94d7743a4da05882fdc06358a8cd57b55133cd53bb635625939d69e762c19b4bf8df8fc497ea7163f7920754a75fe571c919491925a8832de66d25ca45bdfa30f119a7ee7f31b3edfe260de933c1de194e08891379dd605c5fc2d89b4993216fe1b351a261c2c247eb03f712b85933353276dc73f512c614405fcc6b1b2b27b7943fc4aaacd32cfa5767cc7ae0de289931fab1ea55ec3753db4fc1b3bdd7f8f6e622054ab2c039aa1dbd00f0707c99b62d698e95662e1a80dfd424f9f250fcdacccbbc85196591e6fda02ee2b2f5d9db14c8fa014e641c84f53b68387e48878e680927e9e46219e4bdad9f775f3e2abe1d6c054d6f998d99986ea95de202ed88a1f56f1af2fda3f14a5e17e1d3d6abb003c73901c65f785d5d6cc2da6f7e3bfb3fa1db058976ebd0fc64aa4b3f3c29cd2aacec6c51892187bbd1ae624c080558f7cb93363e88b2788793c48a6884cbacb87c906f6a3d2192b289810439cebac32f97b7b65ba9feb33657c5ff619c36b66937cf4d123983d632152a6473714e72d972fee817981ee26dcc0eeddd5bd26aac6471f7119fc96c73ee1282fde34f3574559db323d73f992bba96d40a5e5eb816d481d0a6560520f08fde0af490a852deeffaeb09472389384262a4ed6d9c84bdda940b8c29170d6f4f1827e939e7209f0ab595890b65375aa69238a0a08796d7316e95c733eabf1133f36737fd8d118fb22bab1c1992a503758409da8fbd387cb28ba83d1f3cc4a8132c587e27030f5294fc8374756bf9d71b83d80d314634e973c10b86811b8441236859515613b352d90fbf0618300048e747c4fd77b1249fe7a77e544d1ec9ccbe31a675c7d176929b840dce7dfe4ee6fafd42f523ac6d152018e812bba0872742b31f08cb8e44ad7b3a036e764cf6301e59fa81d975c904f5cc766c4dd0ea072e2655c45771b495e0c734e7a3d1d61e660d94429786f0478e7ee6916e91e315ba719837d7f208c593f33861bc6913591178e00f20a52ce42fcddda7a4416c13f960e4febaeb6da30ad3f589bb28189f3bd4bc1abbf1a5734433ab794b3b22df56a16d71aed2e9f3732a138fbbb3bca6a86120fcfa7900851fa304a79f0126f79ea22103aeb5eb8966c2b7feff3dd04368d700bb4ff664f65f0366f3e5eabe89afa5b448e7f9521cac0f406b38558083b7783bd290aec4781df2fb0229dc20e0c1ac7a7170074526cbdb09aca36bb994d08c6dc14f052b9069b4ee59f75221e996f869dd874d12e8c3efda52a3e4826d59581cb7b58774a86c18f2d11c86367c0ea52b29fdb32bd33a3a228d0246fb490d2f7b814e73511afb51dd45d7e699d73d2765bb183a47e141c05a9ef249b7343cd952c270f11aa22b79eab6adaba0c85d2dbd03d568a24bd1243ddae92a3e9f63e33fbd701bb132f4a8a2b4168dfde37fc0534af250d07e5b6af9f166e15cc6ea64ee06ba56c02fed641762c64927482382853ae01cc3405fe34f8b1302959d16b43dca6188f779338c6692ba65655e89724bd8e4662f60a905f920df8e9ce1bbcab0d96bceff13847547491baaf08dace871269e9dd5d074224a5afdf9f975ea84091594a009ba77a0f7cd3b84c29e66dddf8c06a8099eed2a228eaa79e6f342d9c3a9537416aa62684c03ded88258fbb8e39897b77153a50a93a791552d77a64a3e90dd23ba2eb40a020d03f962db6df86afe397834f0d727cf022b2cd3c01b6af91344e62d6f5b947d079566b37d93e5e08c32f83d66dccc050e5eb85425e805c7c9d8e97b07164cc7864f40fc7de5967786217859f145690a165a99b36935bdf56a22d167044a9de0deb9d028659fa539457fd02f76790290a86f0de12026b97d340f7f86fc3dd58c204e12ff980f3f5fc20384f391310699804eb443e67050003df75da106987418384686b386dc50f6d21cb56977543ec5eab013a00f84f74498c9aa5cdbf056ee8c36ee7032750794af1f8d218e436729696326038b2e2b5d08a9d7a9de70a4776fb65c2dac2af17ecb678ac731ca75e2fcfe6d12ace21aecdc27f9e32ab5e61aa9616637eb4d75e0d9730ed54a8789db77419e4f1949792265ce7cb69ef1c637b00aed172b6002e43a5864607e17366300a0c4eff5a0a9a8489c18a3a1c00c5a35b44bf47b5293708382dbcdbc872ec62d641df1e63e4af16206de50b69f118ed9f1ac486d9fdfe632e46043a845c42ca1166cbdef6e2290d17fe22152f9a46f8590342370240fb7c18d9f00f6cb5ed2965ab4b0de718d81ecddc7928c0cc873e96840bd99a4432699edd037c031fca395f4809734003aed842e82f665f222736e34e7724155c52b285399a67a288a4d101717836ddb3c25f68d276c47de55e29d6cdf59798d0bbf87b98e323c77287b02fbd217c873680087b887d83b07888f0b56709a32a65f2768588691a70071806cfae2b407511fb907b3ff0d4c26563669392c8cc385ba70e30b573b53bb13f9c0335b9dbd4a9e296a3b346da0b956a4eea0d5cd243daa2cec52d5e8290f10923142d6105915515ae8df41fc4d9bdff0b108edd089cbd603b5b487c5669c7ac1854e76b46bcb85a0d59aaf5b570b4e30c345b8a8640e48eb18daed7ae09e66837efa8f81ac2134fa505fd6699707d3374e6a94cec46de5b93a6c1d328752a769740cff68d15f32311c126bb87c586a8bd162b865c77fa1fafbc68ce71d903c3cecdcfdf9ca23f323b3bfc7b4a07703b7c88811bc3911f4b8f5a45eb44423a0612b8daba9e4fdcf331e9bf0af75395ec430bd0a3f33bb0fe80bea6df2d466e5b2ce65cc1e92550f85013671de9550283a2ec0ed995401b22521cf6a1b5273795481214ade5c8362bd0f764591ea2a0a858520229673d6f3702de61f7a213d55736389aed83e511b243ff59ec6a5fa03c944babb5571be90ccb57a19bdfe291c2f7b7da5af8fc7f821f403601263a5ac1fce88502e7c485e6ecebcf414af6c46e4dc19abbb2684a23a27bca51b0747a76ff1361f119b8317b3df40fc3f9dbb8cd3b64040cbfde305f0234fa06e937404ad5446e5f428822b807b7529b765f1a54eaba462c6997a9ec91a3795ad80591709f36563a531dcb1657b27a1a0dca1b8e3ea615b6b019a8dd18f5873b7f8bb27fcc5c660560d63b912527f8f8b2ad25c444dc8053e3155915a88517f1cdda84a5bcf61dc3c6b3c4c5cb89c49ad68e6187c4f9b59861c9f7fdd8ecc237af4324cffa1b5bda0545a44b53f4aab053fe50d890e31e33870ca8293d177be8a08b9b8fbdf2b241461a551f28f867797a175c241169b458d96767314f0a4b3b519ddebcb690e14a82104d47c4a04ffff906a62222208b95c96ed8c50a83aeebda60559aee315b471a4fdb606deacc29e05b1f7505cb3e5d19ac3d104803195f273f91945e46efab4d6f3e79d6a3b8700953b81157690e1436085b56f14c197faae2f6e72570828127a6d0637114d5ffe6520ebddb661d5634ce23cbfa95dfc93c5cb95ac7a206bf50f21589d83d27c4397dd9369f194ce1a54fc8b373b976a942dc712fe971a6bb5dc68da40c0088a02fb7297f378334a7e88f33b440ded8bb05b29441ec582ac53edb866d9074ccd466a495683441b5b72ef8e996d04d8b3bd3172a8fea2f37524de7cd60e7784c20535a1c8709845d52b059c9713e4c7787496398751468a75daf64e56847a837030b739f508d217b157fec6c85dd24768fe560016b18863ad56065c0d13309e06776ab559fed13c4127755753734ef02709c8b23a534f13692d65cc777a431bfe049e46a5096e4f327a5e4b688a6051b1c72c53101543fec80f06ef0fc9e6ab47f0f5cc27eb0cd7a40e45d2418f02d72331451187fdce5a26bd2da3ad9a141a2b77fd4feeb612f93feee5492b20f53d560e27c528d1ef2f54a3119a143c3a27b046d7e4e4529342a00d75c051ac15895c04ae510f1bfce0e5fa84119f2971aab694b68e53e72f861441be302b84933a5e2ffc9e879916f56bd3649afa02845f9f93093ac4fa9e1ad57c304096ace2562e529c39e90a3434de66ced0cbd7ed05d3a2f7e03578269bd3b7fb5aa125bf595fbf2ff58e69bbbaa4d72765972c61995b1f63c03d2aafdbb1f72fdfa19d2cf80c3f3bb18fec9585c8e16eae4b4573af710248d50a2f4080afbf106d408da650fb2f237329e6d7b74ad554d4a97e567ddf6558042b4ec6dfa986821032ac2af721c6eed0097c5d4db382f56ac86b336ce833a5a4f068abab20951f342352c89ccbccfdb112e8c5680b32976e7cc1b196ad2afe9293686c14af107bd5f393701eb4f50df0029baa3b4623e00e7defc37675d2af93eec763fd0bee174b2484a912a51dd58bb495b66af214f698f01e8ff35a0f1834ca4e26c02277966018399c815d217801249b59b6352970cf6195eb6f1164300625e6eaad4c7dece840356d90363f6620969495f0e0631c6d673e82686af0d040e7d8cb3f861b0d1343c4a5b658fa8b0bbdef11f83afcfd0e5282de6f1f82223357dfe0a162172768bafb4dd2f84c1c8ced10000000000000000000000000000000000000000000000000609141a1f242d336378356381591dca30821dc630820b9da003020102021472959f358629caa169f55fbc058b715ef19a53cb300b06096086480165030403133070310b300906035504061302534531153013060355040a0c0c4578616d706c6520436f727031223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3126302406035504030c1d4578616d706c65204d4c2d4453412d3837204174746573746174696f6e301e170d3236303432393037343930325a170d3336303432363037343930325a3070310b300906035504061302534531153013060355040a0c0c4578616d706c6520436f727031223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3126302406035504030c1d4578616d706c65204d4c2d4453412d3837204174746573746174696f6e30820a32300b060960864801650304031303820a2100d7af87deaf76f6a88f81f168aadb9ea9774e280044a7d6023f53952b05df7cec43fb07424fc3b85c1a283dec9842bb96ff7e21ae0e4624af0ba2809322b11a9ca920a6b97e23ac7660dac426cb73385afb5060700f898dca9cf49a168d0644c8cf296835b3acefe1b14b8221465c9b3ce1fc77a50aee149f865a8f52776fe6f785fc33ba0197efbcd93846c0b3bf614a318ceb913c1f38d9b37de40c4b3bf50f075f501f024c6e6b61a0f96212b595fe978c100de7d6be0a962a668ce10ed06f0e19ca11e5f3d848c34482e7f96ecfa62a4a7131a654c03bbbe3a6b59ec06fe8e03e36fbb982360d9b858dad1022017e5a05b4a1851b95a2d0ea52357bda198d296b384b56918f3333f9baff6ebc684204c8cecaf67689c5f9bac7651c95867aa40c08aba1ab4b26e05ab1c7480e21c68b3a5f67cb00bb7b23084c476411d1bfb49665723b5ab005742d59f9051933a18bc9ed6de6b0ef3026932fc60bdb50f8de1de2d661979c923342170227c21226057fc4c00b41696a21e1961df9405d04f267ad7bbda9864cb986293a7f3c6ec74bd802cae7a3e8ea636829ff703c98e6d813d4dd2812da219d76cc3fefbe1e6b25faf841b61faefe35258cdb7f2cec8fc5a19480b073ab1ea37abd7ee9bd96a1b6c97a130ae15ec70ae23b57a830bda276ebb7883e33987d203713d6d3b26ff75ee667f603a1bfabd657fca2654f4c96054509bbb06a9ac379e3993d93a8c80ed0c44f588e1338a941fb0b9630c35bedf3afb6e6750a6bc0c3747133025161a3414445bc95cb9e62958da7e119cb5cf7f0def16e8b23a6020e3746a29b17d53043b5bd0f416ab9e1be925ef349c2b245e284ecd822a31843d0a89fec960d511bee240737e4142c40585c05fb9953a3d520636cabdb195a2a1705ae94556289e64695825cda07f0c4d08c33791f629479a09112f60756a44fa300d2cd4d7899732176d9affd024dd1329d48534b90f492b8bdf16bea27ff951c784bd2bef1dbec0658c760d15c9b23341aa0065e7ff40025422ec3ea9f5df12f8fc4b33a93c9843217c224e8d0950744c6912510be048338cc6896fdbf6c25afc4382b1121592b718561b81249c282765856613b3d63a193fb3bb91f8cd4cd1a08b5ebc9ba8752556416ec733925c38a91679df8e5e46cfb62cfd0f68701a304b259a667b4924b89abf901f732dd37cb9cbbbadd30267b3210e708551b06849f9075cfab0e037701c5acb679818b2bf0ad61e5735905e331e5fbd16016b95b0deb036d5dabdb0b56f5266cf8869841225c866c802f38fe92f0d6aa9b91008a70cf0903ef7260d120c05081a626c220dc15380d070d8153bb8835073a530c32e840478e0751d4b17db64aa8de121f75aeabbd9171aa6d5b1909eaa723739d6a95dc9f1df4b373ad32e053cba2e71b86bd212b181faf4b01ea976fbe42e57c5fa386014296ccf0971cdc28b70f203d207c3bc25a98e029df75a3abbf42d81a9c232b3d37ec18db3bccad95e7d2288c2e2e90b9c0250a99e7f381e5d9dc61585ab014b4e2fcfc497eafbb76adf389f901f69eb71df9ce80f1505116da63dd0915db353ea5f525098a7cd66728b03860af56af78b89b1a979ed5dbe23fea7941081bed5d3a4ae3ce740236bd5af50fa2c2ddab8d1f7b9caf8a1aff5583f00e585b605078f9639e4fadd98856a8876dd8e977b39556fd672894699528bf4809dc3ab54092c545adef73b1cb10843105bb0daee3698129161391d0957588ce2126beba30ba05d71f4061543f5558e3abf075bf7a45e55532053fcff42938f52af5d38cb43347a4b6f3d8b71ce5fc43b577d2ff8136c2fdba32046e40f9eacddadf9cb8e5bc3a4c6169cece70beefb80a39231c261b845484fd2bd150302aaa0a37228ee11ade6b99d8eb586f2cfd5225fa4ea365f0f5ae073909c6f958b57de5fc17b7f30b5821ab7a9da44c511b66cad88d6e429b203b4f1a1dc74574ee7bd71086583aec421460c9c26fea403030bbbba23e9d5dfde62da8e32cdd06fb3bc809e408fd55e908c547e58d7337879d634141faa00693b67dcb36453a9b552e7a2a1d5f0b97b12eab3c8e0dbcf772e1f4ecc8a4ec7224c21bdbdf83e3aa25794ceef5c5ad225aea0692a78c124b863aa330a184f4b66bf5b5ace7db77f13a29821b052afe1e42de50a1f8f3c4911908da0de68d79c8735ae9283df57dbe518316297dbabcb1a8a016445d035a2efa2c964edd85934aa182c783812083dad3b7254ac8a09d4ab89648cac82959a714c4e1e7e13f22c9612aee9343d754e78ccd708b7da5ecffb293012d65fd2c21d1f04f8ae7bd4be3e1486d6c175447179be7ed2639924ba41daf70f5b2ab0eb5ee3f1b34f614d71cd0fb526e557a44343e6f2813895ac6ecbf0fd188ab4540bf91149fac70516235ae0004a8deee6f58f9e0b31dc02ac0a1ec17f83f712151a6a929709e2f8593173bb9a142334d20e2d7096e031cb3d3635d83a2db985c532d236612c149a611d93b24bcf591992b45b9be55adbfdec1766ec8c99cebf93205259d4cc3e225425d3f1dcc837ac36984ee2d987351ea704f841897cb09f2935d7ec0f370089218b284951bcc58dd140544f802d32deb14015992d3fa7737baf99c90fd15e4da359d7e071c8285a51bb346b134245557fb2a5140cf0a5c942f155558d5576de2ebef8059e37bd3eaf4315fb50e9d4270c981ff52de4fb6e39e403a87ec0564f57ee316ec0d2b5936485e1d914cab3c8b33396116d91c64059479ccd7436af13d3d9df614a80170a68549960af855684d816afa2257444a753e244a34d30636b0c947a7dabcbbaa182f529f25b5f6685c3f7c5d5246c4d3d797f82b8dc1c3d0deb756fe207f4477b4ba9e6288f4921c2d1f842b985492636b9384961a666c9f6562bb3134c2d22a65d617e1b387a7de0dbdb1f0df49eb659d02efa40afd0d50e35209c9cc13d49358df76da480313c83601b1a1aee548b54a59efb6f408ab8734e09c568fcac3cefce9bb3da9219263958a152a7b126d2a28a72724a79b09c2a888b9a2a2ab4eb0bdcb933e284c880d08ceb8001836186f1bc15c9381c144514562ce98f61c1c80f43256c14a2a79f988a3f974195826f96dc01ca099c57dd7feac93dc4407bc6e23b9e1efffd393d7be96f4287943ed073a02a6de2399f21c2df7445283b0b9826d53999057f5622d01c8c2a607e8f9918dc3f608918a73d77c047da313f08d9908926b02652ec8206ddc035ebb2e850f1f23c4a8aa113a91636caed080b27579b01580ffabca3d4b74a56c162a3ceb26567e8cf72d65f53fd8918104e50335357752652f3684fd44b3804cfd0c3f0a7123290c3752ea46ff852190716277dab953e61ed240158dff4e96445454c2062ccb3118b188ba05a36579397d9d2a9c21530c9a8654fd1104e66ba7d3d9bed43f6f05e02789932f62a0788588598c755dfad710b834694c333362be9bfb7ca40080268e21541c29f31c60a906b2e4ea94984e65ac392088f306ca846892ebe6498fb7e7e1f7e327792278dd893f29730b276667e65a19b0e128c561ca4300fb550fc1dcbdfef7f0ed923be8bdb932e409cce75f749ba5e3244fb5219094391aa71676db1e701d18c2b2e72b5ef312e480a339303730090603551d1304023000300b0603551d0f040403020780301d0603551d0e041604146e661c60219f6e8a268de56a8e04fe5c69833978300b060960864801650304031303821214008f24b6055e806ba036ceca76e5298d84937f1af4a74ef30050ad6be1ec90df142fead4472ffed346581829c095176d559dcbe34783f67ff930c464a6ed7db301ccf415366ce283136c14a755bb8aeacb858f312ae9ddfce5db362bd869c3c713d9a6eeed9602b510a9eb6dc542c21b1a5e6fd403a85ac2e34c2a98c1e78c41a74e852687a486bcb834c5114cfed64de80da8c81d3b553140b6fcc44d4421675e66e1b726c58bb0d74c945a68f01a203092a6a430ad02af74eb2da1fe67aa5a2ba6414e74ebf69e37698df1e49684d85ae653cd523a8ae18528842e56c627437cde2f552ede9eb93b1b7655e9733431b2c214ad820deca32bae73550fa68dc7e536f72bdd47e356f37b3ecbd0586b5991aa8cbc3d46b8799d96620d2e0a3ce3cb539e88a369044e240b9cad8ece06b71695d7d3eafa6d77c1c8b9ac3b562ac5c686938f811c22a97d6c4800c820593c1c6443fb4ad15f93fb863ce5787fc3ce4e1bba5dd17ee60b73dd50a1dbffd11803a895d46d7cfe021ce18348dd2e0324cc24aefcd489acf49ce126d1de76d4b7033e4e39b2599c93dbd6cdab90a03cc170cf552da0da690162a800e4f5ae708f1bf515dbf2c493f2b89ca5d0e85f38e5654dadacf7ece59ed0a26c2b8dea1aea2508aaf63e67b784323c2be560fec35dc9b5362c8ba73d8c140f450d8f5b3df50e7796b589c68e052786ce44880f8d7fdf38977d78fc27ed3f9353215b2bc94324d0632f5b0c0bf4c246def1cd0003193f4c21f4771e6365314b00951978a9d47ba1ed6e614e5a83db96a31430a14acaeac3ece14e3aec8c96b04cccbdf4b9f0e0cb2b5c9fe04a31ea07abd9dbc3c96e0f522665b14e5eea4700e6941b3a620762794f1f8c7eacca898a1257285e74c2d72817464492ba578ec5cbb9c2916fb15199f8aff0668b381563f550994cdaa0a16f68cf1b408097f8486b165d6fc8b67bcfc9234c6e9f6abc2a5ea44a82e4ba56160cdb764fc6334a54e7bb1aec125e5bec6c6b8c97fbfab6b74d72ece70918e1d33348affca7a817260666d8a05d462fe2d18bdb1e8ddd9519f1f5e62f404ea834bf89c8e434dffa564f95f6de0453f75f5d804088723208a5c7f826a7f492bdad4b12245c2bfdae4b0ca364468ba88c387157f5a316a65d7620b0aeef72703a6925ced1fa089c5620ae3077db98df6aff2f43f242c4dcbbe445dd6c7370a3da63852c56d42cdd1ad6e7d5a178b69b50099db7d7fe34b8ea3fb5f01383010f4672583e05b4aa5ddf014bb87ee4b9224e3dc5daebaf4a495d6f718ce7d97bb362e1090b60dacb193377dc85cdf37687f889546497b52e8ccf19f6cda6454c5076fca423d90b668a0f4e835422de48d4fa5b7c4cf22cc497af2fe12462b33006f2fc7c679a1e5c77fa6603a23d35653a6e6755a44dc701065df73be4108030115f12f4546e84587117dee3ae2d480d172b6194e443ff15f81f29b638c8333f94b5bcab9ccf53f1d0023a8c5b49833c5d516c03d7fe0a2ccaa7ee638f24aa646355964a9a912cfed1cdf36c1677f382e8aac1d3c8921f0416c61bc9431504d3532c5657001d838fd3717ea93746323c13afef99b13889b1bccaac52cec854c484184188b55506c42f95b4459d84fa95b0fd9bfe2611d6bb8a19d7d8db8219fa9fd8d63eaf3fa85bfaf327eae79cf18cd9bff68e6e2d79f2e93d5015ce638229f8ea1d904642d3af50fcfd26cf08d7620ec4554ee85394c2835874fd417f10061f27647123e6bee26a7183803bf9c5378db8da29769cc5b45251cc05d8d4585dd1fe3bb6b61bd9e7667768298e27b6aa5c08ce9020c70fff97a67ffcdfc3c349b8e0da03f857d3211c3ff54524fee01e4908fdf8e9df13979ff9722c7d62591e4898a80c854ac916e15d3f8c8fdd9882a87065e095083da37b350290090ac1b9df97f36e7b6ca6a6713b3dc85a1ab5304aa4858c57c86a1eb4e960b57ec754c785b930d017b85b37f1ab31e72f756b9f6c08b42f5bcef9c4436eacd37b95c7eb9d813939083949a573848e67dea95391cc0a0d0775c95fc64a317bd19e494948ac1d37d5c83a946286e9d73451ae5f5a3356f170dcca03ddf2b7909b9bcd45d115783395b3d3dfa70808c93221e1371c6ff5b6880f6afb653b0c99328c2d4f9006f4131f576ab4ea28eb8c4e8749ecfca56a91e93ef82f4515f90ef90f943625087dfe16fd4edb20951889e6c027b6ee4d3652c15e3daf975217b819d092f902e11e4c7ce6138827a35f0cfd00feaae5b60f00ea996fcf96a8dfe91d2c4a63cb4897ce540c7371729db74dd891193da2ee798f7065019b2f2561bcb8470709d6d87aaec6d8652e2f3534a044d8231bd3cb4a1b05a730c702b5c487c2d0f03e2755cd699a4d930c906f0b16ec03c28af4ba75d22b15a91476d0acab50e90fcd66a7358949e6a6adaf3a9ba7d34c02dd879b6f982ca415e67591384832264eb8553ff3342a0e5cc91469e3ff0593755f0ec89c98c709584bc116796d0e17b6469161b1516e14509131115c46668126820047da7a5c192ed3ba6ad8120de2e7b43d86598bc2549d5ab46a7003aa55f6aa2ea1cf27f5fffa6a3563b575eed79c2080ad602576fafd4961fe7ce63d6c6d3dfa86bd969a0fe81c77ac0922120bc17513986b6f682a161a5e93265c16f1e124271b4a81788163abac2b8919757929dc8b6b399f53cf1797c5942f896109f89d03ee6b56ed41de4f8429cd516ce0bdd3384ece31d0543166d8fe4586529962e24b4a780cfa87abebcaeee1e3ed12c36c09368b1f4bf418ed97dac350d1184f78f89e843722c1c3e8e42935b9f8dc94ea1355cd656baef4925d7a389af7130bb18a8d7b1e5bf7d801aeda769e8e54bac6b944a3a8571e4ce6483863904a0adf1f1dd2fd380c42fcde7240efcf3cbb9ec3e2fd4aece490288e389a683862e2d1e20937c8a0a7eb53417e00467950472f309a7e13beec22409a69293be7cb64e263e40c74c4697b9d985da370bf74f6b50e6ede437c5e5df36e8fb8dbb65cb00f4748b2d4760a00a06c294612587baa27cb6d22eb189f257b354034f871ef61f4d81fe23c2475649733f1c3746303d3f804fe2cb9439d278fa9ab0280c47c8ada712780d7bcdcee9b7d2f44a0dfa37abfdc1c30b9b3f48b39054bb323032a416f981be7c8f925f363279efb73595556c8ee76f7bcab881fd96ae010ff40c81c0497c843075a81d84deb759f4fcce87a9a4cb847486a9ace373ca0418dcc2feeeaf7926b94a9169e215ec49f22e8841c1b4b6d06c9f1382dbf982990ff3dcf7f4dbe6b5bb2206104432dbe39d7e188480b471a63df2cdb778934ab02e4eb43b15b2fd765a09383cdd5cd89f8035c3f221947f1bb933c5c3cbe9096852314d3620e125f151074b1e4ddc317c495d8aa9be7ef0d238d9430eed9f3a1d0ae9c0b37e94527d9d54d7cf7298f1fb60ece02677e4c03e30ab3afc48efef86fb939128ad248b4b01c0486c4589638a4b94ea6e69f9124cb4c866a735fd314a89e11d1f6fed7c75fef97cac76a8423be0f3448a1d0347dabb09b52aca28e59b3ada509826f87af7620aa6a15a8a8bc3a501085aa2f0d1761899d7014dbf67f71f2ba16d58fa08b45ecfcd6eeaf09410ef9c0830627a46d0c2185fe526cc200a31e351ab5a0f55348e8643f147dac90e15b0291e91d85136d7d85b0d02fc0662ff7ac7ca3950cf9f521c6377d9da659e13df9f557c57c77fdd9edf80209fc74fe5f88c54868a1e9422e456014a9630453d96ecf0e8289f7a502f878c1acb0ae0335429258ead534e58a8007934380a1e8d918c4d37a9ef53384eb28e72a54dec56a651fe8253c302949b1a36aa600d7eaad7e06aaef7d97d27a5ad60b60d8b4787329a60a7c120e70afcedf29e901a508a938e62c6b270b6336f3b18686a111964a7917903827b1e965821ac234cd7e85cc684a94d395cee29d6d71b06c524c49b7efebe05c4fed81db0563941314c8a91a044ed61be7674ff5b8f7207a1775ed881060badd1956a0aae9426767d06dd66aed113e89fb699331ecfb6b98297442abb77d6b9210da523e2b9626e8d30cac77334d0a768aec01cf1c34aca078d9569a4eceeb9e4c6ee7767bf6f99a3d418fa3efc1bfadbcccc2a7ef1e3072be53efa674bbce0246daf03fc4852208c2bf9f72115cbc8314c28a9f1876cfbd12fa559703aeadfc06e32e4d1014d00caa6185216fe2f30e18b91480f18fd74bcea53295fd079f9c781550673552e356ba0ccd14732f68c5150487247ec8561b333fe06e12f06e14d85c7a2376aca16d8d76adb5f4892faa95f1b60daae11d53ff64a699e7f334a9e0d1ab43f53d671352b87c78abce928c8d5e4be6b835fd2c3fe53d6a2c15e83ecf154db7cc02ff8241b205cce262615ecaa335eea5f7f3bf4c48a2a5fa5ecb08160826804f55564370b427dd2250d2af9b824e9c92c60ef5c44ba29795f15072c7252db8bafe8d43e382cbd07726a0b62817553bac4502d529263bff35ee3852280303f45a86f5a37d99d53871ce9a1f03f8c2eb89bf2accfabc26ab3cbe093c5885c26ebd2023f19545989cc546be51da391979ff5bf4d917dcb5f4e7d41d436bf439120b79b4c00d2d869b8deaeb7a916f150ce84e27041a8ecdb6bd9bca32f40fb07ff1357286173f471a985211f5d5ba8aabaca16d963abb6496ccd9355c60f985591d87696845974e09ce254ae6834edaac9d8d793d33506762a9a3c117e895e66acf7d30c1bb9b570038dc69a1564b4a0dd8703634c1c8d78eb03ff35980603357a83371db71d5f6c2ff328d776bd6880eb54919f7b3d92f72084ea41d6997159cd24ff395a7b247dfe83eadc141861031bf38ac68c594221e4a7e7421a5247c525dcc5fc0a61bd639797ef6a909c114eac430c145a2e8131d1fd5c392a6eb39fd1217b42e057d8f76c2ccb42e5e542b5585b49963377b61a784ba7f9e5db5fbcc9c4b11d7b26d84ac3be8432d5bbf1f7a6c5eb799647cb1c904810a23cbb27960ff1d1be0deea6294b2d36ec96e34a9d62c9fdcd52a0866fd7ef2f4c65ec176dc7d8283ee3706242e610bd2eb0db3ce47d7e6e59cf48e43a91206493b0fe3fad337882c4ddbf139fb5b95216ceae5317fb720d57da119d6f232c569efc190d51c3ae2ce1b067a3a7fc2a8da7b7f464a18cdcc5139393ad1b559bc1d48aa90170b9fcc98a6581c39668e7e9ab887bced51fe91a14cef0fcdba07c636cd2b816c527e9bb79b388552cd06a09508df6260e91fb0de20f08540ca43e80c0b26a47a20d3c9c2a5d61eb850eb60cd38b5e0d3da09eb5f0cb32c7460421fb2af638e1b3473d10fda367e04f12475a96ddff7472ceba56d4e4da44d390208a7f48b4c8363248e69fcd2423670a9f77263669928793f137d930ee00226eb5ee49c74497af65a78edc06242556a887379426ac3e2792f217c4a6ff96e280b179072baff6198b465cd9f60481d062f0b44bb604e012b936dbbecf4a9ae2bc8e946ed60a58d986fee472666b8574038a8a522ab2a275f8f94d8a7988eb8ed21740243c1b17012bd84c3f4c93de73ad88aa40a7f732e1a2c60a5708536f6b886f0a8b204d718e5790a2a0cf76204ced014f94b175e76526d2ed7dbb4518b62bcbf800e36b1338390151aaebd6f429a119f3a563e157c5a63539bc717468e7b8c91638f0f9166b69bfbdfbd064b22d20b213a6d9ce3753256ee6c17934d4e5d133e2ac83153f5aee3df93b51a3953a2a395b6bafc0ea131c6c6b556144e9c67a0c03d50bb3db50ebbf6f06a63a73545b36f9f2eac18dc3b184021bc2dd82d5b27225fadc34520cd773371bf16e015897e93df981a536b371bca2ad73ce883be83315d26e8163f639f0dc41015d8c4c5025285a0b87a72fdf0961ce44d713c9b991e0322b0acc26539af6438686d7d7e833989dc831158e6de4f571a7f7f67e50d2d97c03235f08c3d04e69207090811d481517e0bd14e745d4383c54d160682c5980aa46ffbc13431ee1f4d5d703745cf801c4b6c019efc4c5bd9a3f2f53f32e1bbf03e57c0421584b4031894e6e2965308f4ef8e914bd460cdb7708b80222cbee9be169a1c50bd3dfc8a22d8e27344e688cca1b1dc73322a49f85fca77408180a6bef17ecc827d6828a38671e000b4d49e7309e18da878b8c66b50e28a8d700721a40ea9a03386d8b4b648961a5d6a271a574470b0f99921575fa39a98883f5ed66d35b32015473c2dd2fbcf86f0f44ad256075fc643e6174c2cb9d79471f34bde88794a4cd4339b5c8bd9606a494f73cbb546b85461e90bb3107d7f06e5f0b3e349560f0167eee31c6777841117284c72dec2e9fbc8521838a0106bf64ef89933f866db0d31d5b68ca397d434438090a2a6814b23cfea4968d2d7010923247099abb10e65899ca3d3df06354f6e84c1c5d3e2f5fe3c52afcd1523a4a5a70b585e6cadc3d4ddf42991c1df0000000000000000000000000000000000000000000000040c131e22273034" + ) + att_resp = AttestationResponse.from_dict(cbor.decode(resp)) # type: ignore + packed = PackedAttestation() + + packed.verify(att_resp.att_stmt, att_resp.auth_data, client_data_hash) From 1cc72417864c715065307c5053c5b9b13e23a3c6 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 29 Jun 2026 14:45:29 +0200 Subject: [PATCH 2/6] Bump version --- NEWS | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 5af96e56..0d7ab4b2 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ +* Version 2.2.1 (unreleased) + ** Server example: Migrate build tool from poetry to uv. + ** Fix: Correctly format att_obj in previewSign. + * Version 2.2.0 (released 2026-04-15) ** Restrict DLL search paths (YSA-2026-01). ** Add support for experimental previewSign extension: diff --git a/pyproject.toml b/pyproject.toml index e9b15898..1b98a76e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fido2" -version = "2.2.1-dev.0" +version = "2.2.1" description = "FIDO2/WebAuthn library for implementing clients and servers." authors = [{ name = "Dain Nilsson", email = "" }] readme = "README.adoc" From 27e09990721f341200b98308116848d26b1c1375 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 29 Jun 2026 18:53:35 +0200 Subject: [PATCH 3/6] Fix examples for WindowsClient --- examples/large_blobs.py | 2 +- examples/resident_key.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/large_blobs.py b/examples/large_blobs.py index ca30eaf5..55553f59 100644 --- a/examples/large_blobs.py +++ b/examples/large_blobs.py @@ -44,7 +44,7 @@ # LargeBlob requires UV if it is configured uv = "discouraged" -if info.options.get("clientPin"): +if info and info.options.get("clientPin"): uv = "required" diff --git a/examples/resident_key.py b/examples/resident_key.py index bfb9cccc..621bef93 100644 --- a/examples/resident_key.py +++ b/examples/resident_key.py @@ -41,7 +41,7 @@ # Prefer UV if supported and configured uv = "discouraged" -if info and info.options.get("uv") or info.options.get("bioEnroll"): +if info and (info.options.get("uv") or info.options.get("bioEnroll")): uv = "preferred" print("Authenticator is configured for User Verification") From e5b022d4c9caf267b21de281f1ab90ae60f0a79a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 29 Jun 2026 19:12:56 +0200 Subject: [PATCH 4/6] Use hmac_mc in prf example --- examples/prf.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/examples/prf.py b/examples/prf.py index a02b3813..8713d8bd 100644 --- a/examples/prf.py +++ b/examples/prf.py @@ -54,11 +54,15 @@ authenticator_attachment="cross-platform", ) +# Generate salts for PRF: +salt = websafe_encode(os.urandom(32)) +salt2 = websafe_encode(os.urandom(32)) + # Create a credential result = client.make_credential( { **create_options["publicKey"], - "extensions": {"prf": {}}, + "extensions": {"prf": {"eval": {"first": salt, "second": salt2}}}, } ) @@ -74,33 +78,34 @@ # the credential wasn't made with it, so keep going print("Failed to create credential with PRF, it might not work") -print("New credential created, with the PRF extension.") - # If created with UV, keep using UV if auth_data.is_user_verified(): uv = "required" -# Generate a salt for PRF: -salt = websafe_encode(os.urandom(32)) -print("Authenticate with salt:", salt) +print("First salt:", salt) # Prepare parameters for getAssertion credentials = [credential] request_options, state = server.authenticate_begin(credentials, user_verification=uv) -# Authenticate the credential -result = client.get_assertion( - { - **request_options["publicKey"], - "extensions": {"prf": {"eval": {"first": salt}}}, - } -) +prf_results = result.client_extension_results.get("prf", {}).get("results") +if prf_results: + output1 = prf_results["first"] + print("Credential created, with salt. Secret:", output1) +else: + # Authenticate the credential + result = client.get_assertion( + { + **request_options["publicKey"], + "extensions": {"prf": {"eval": {"first": salt}}}, + } + ) -# Only one cred in allowCredentials, only one response. -response = result.get_response(0) + # Only one cred in allowCredentials, only one response. + response = result.get_response(0) -output1 = response.client_extension_results["prf"]["results"]["first"] -print("Authenticated, secret:", output1) + output1 = response.client_extension_results["prf"]["results"]["first"] + print("Authenticated, with salt. Secret:", output1) # Authenticate again, using two salts to generate two secrets. @@ -108,8 +113,6 @@ # credentials which use different salts. Here it is not needed, but provided for # completeness of the example. -# Generate a second salt for PRF: -salt2 = websafe_encode(os.urandom(32)) print("Authenticate with second salt:", salt2) # The first salt is reused, which should result in the same secret. From 018ef972eabbc1408d096941cc1967c83db967fc Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 29 Jun 2026 19:25:55 +0200 Subject: [PATCH 5/6] Set date in NEWS --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 0d7ab4b2..eeccea11 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -* Version 2.2.1 (unreleased) +* Version 2.2.1 (released 2026-06-29) ** Server example: Migrate build tool from poetry to uv. ** Fix: Correctly format att_obj in previewSign. From cd89f2dadc38ab6984608feb79184725494f7b3a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 29 Jun 2026 19:58:51 +0200 Subject: [PATCH 6/6] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b98a76e..33356007 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fido2" -version = "2.2.1" +version = "2.2.2-dev.0" description = "FIDO2/WebAuthn library for implementing clients and servers." authors = [{ name = "Dain Nilsson", email = "" }] readme = "README.adoc"