Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,3 @@ ignore_missing_imports = True

[mypy-cbor2.*]
ignore_missing_imports = True

[mypy-OpenSSL.*]
ignore_missing_imports = True
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ black==24.8.0
cbor2==5.6.5
cffi==1.17.1
click==8.1.7
cryptography==44.0.2
cryptography==45.0.2
mccabe==0.7.0
mypy==1.11.2
mypy-extensions==1.0.0
Expand All @@ -12,7 +12,6 @@ platformdirs==4.3.6
pycodestyle==2.12.1
pycparser==2.22
pyflakes==3.2.0
pyOpenSSL==25.0.0
regex==2024.11.6
six==1.16.0
toml==0.10.2
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def find_version(*file_paths):
install_requires=[
"asn1crypto>=1.5.1",
"cbor2>=5.6.5",
"cryptography>=44.0.2",
"pyOpenSSL>=25.0.0",
"cryptography>=45.0.0",
],
)
48 changes: 0 additions & 48 deletions tests/helpers/x509store.py

This file was deleted.

24 changes: 9 additions & 15 deletions tests/test_validate_certificate_chain.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import datetime
from unittest import TestCase
from datetime import datetime
from OpenSSL.crypto import X509Store

from webauthn.helpers.exceptions import InvalidCertificateChain
from webauthn.helpers.known_root_certs import (
Expand All @@ -11,8 +10,8 @@
validate_certificate_chain,
)

from .helpers.x509store import patch_validate_certificate_chain_x509store_getter

# Not Valid Before: 2021-08-31 23:02:07+00:00
# Not Valid After: 2021-09-03 23:02:07+00:00
apple_x5c_certs = [
# 2021-08-31 @ 23:02:07Z <-> 2021-09-03 @ 23:02:07Z
bytes.fromhex(
Expand All @@ -26,35 +25,30 @@


class TestValidateCertificateChain(TestCase):
def setUp(self):
def test_validates_certificate_chain(self) -> None:
# Setting the time to something that satisfies all these:
# (Leaf) 20210831230207Z <-> 20210903230207Z <- Earliest expiration
# (Int.) 20200318183801Z <-> 20300313000000Z
# (Root) 20200318182132Z <-> 20450315000000Z
self.x509store_time = datetime(2021, 9, 1, 0, 0, 0)

@patch_validate_certificate_chain_x509store_getter
def test_validates_certificate_chain(self, patched_x509store: X509Store) -> None:
patched_x509store.set_time(self.x509store_time)

time = datetime.datetime(2021, 9, 1, 13, 5, 3, 5353, datetime.timezone.utc)
try:
validate_certificate_chain(
x5c=apple_x5c_certs,
pem_root_certs_bytes=[apple_webauthn_root_ca],
time=time,
)
except Exception as err:
print(err)
self.fail("validate_certificate_chain failed when it should have succeeded")

@patch_validate_certificate_chain_x509store_getter
def test_throws_on_bad_root_cert(self, patched_x509store: X509Store) -> None:
patched_x509store.set_time(self.x509store_time)

def test_throws_on_bad_root_cert(self) -> None:
time = datetime.datetime(2021, 9, 1, 13, 5, 3, 5353, datetime.timezone.utc)
with self.assertRaises(InvalidCertificateChain):
validate_certificate_chain(
x5c=apple_x5c_certs,
# An obviously invalid root cert for these x5c certs
pem_root_certs_bytes=[globalsign_root_ca],
time=time,
)

def test_passes_on_no_root_certs(self):
Expand Down
22 changes: 7 additions & 15 deletions tests/test_verify_registration_response_android_key.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import datetime
from unittest import TestCase
from datetime import datetime
from OpenSSL.crypto import X509Store

from webauthn.helpers import base64url_to_bytes
from webauthn.helpers.structs import AttestationFormat
from webauthn import verify_registration_response

from .helpers.x509store import patch_validate_certificate_chain_x509store_getter


class TestVerifyRegistrationResponseAndroidKey(TestCase):
@patch_validate_certificate_chain_x509store_getter
def test_verify_attestation_android_key_hardware_authority(
self,
patched_x509store: X509Store,
) -> None:
def test_verify_attestation_android_key_hardware_authority(self):
"""
This android-key attestation was generated on a Pixel 8a in January 2025 via an origin
trial. Google will be sunsetting android-safetynet attestation for android-key attestations
Expand All @@ -36,24 +29,23 @@ def test_verify_attestation_android_key_hardware_authority(
},
"authenticatorAttachment": "platform"
}"""

challenge = base64url_to_bytes("t4LWI0iYJSTWPl9WXUdNhdHAnrPDLF9eWAP9lHgmHP8")
rp_id = "localhost"
expected_origin = "http://localhost:8000"

# Setting the time to something that satisfies all these:
# (Leaf) 19700101000000Z <-> 20480101000000Z
# (Int.) 20250107170843Z <-> 20250202103527Z <- Earliest expiration
# (Int.) 20241209062853Z <-> 20250217062852Z
# (Int.) 20220126224945Z <-> 20370122224945Z
# (Root) 20191122203758Z <-> 20341118203758Z
patched_x509store.set_time(datetime(2025, 1, 8, 0, 0, 0))
time = datetime.datetime(2025, 1, 30, 17, 8, 43, 5353, datetime.timezone.utc)
challenge = base64url_to_bytes("t4LWI0iYJSTWPl9WXUdNhdHAnrPDLF9eWAP9lHgmHP8")
rp_id = "localhost"
expected_origin = "http://localhost:8000"

verification = verify_registration_response(
credential=credential,
expected_challenge=challenge,
expected_origin=expected_origin,
expected_rp_id=rp_id,
time=time,
)

assert verification.fmt == AttestationFormat.ANDROID_KEY
Expand Down
36 changes: 13 additions & 23 deletions tests/test_verify_registration_response_android_safetynet.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import datetime
from unittest import TestCase
from unittest.mock import MagicMock, patch
from datetime import datetime

from OpenSSL.crypto import X509Store

from webauthn.helpers import parse_attestation_object, parse_registration_credential_json
from webauthn.helpers.structs import AttestationStatement
Expand All @@ -11,20 +9,9 @@
verify_android_safetynet,
)

from .helpers.x509store import patch_validate_certificate_chain_x509store_getter


class TestVerifyRegistrationResponseAndroidSafetyNet(TestCase):
@patch_validate_certificate_chain_x509store_getter
def test_verify_attestation_android_safetynet(self, patched_x509store: X509Store) -> None:
# Setting the time to something that satisfies all these:
# (Leaf) 20210719131342Z <-> 20211017131341Z <- Earliest expiration
# (Int.) 20200813000042Z <-> 20270930000042Z
# (Int.) 20200619000042Z <-> 20280128000042Z
# (Root) 20061215080000Z <-> 20211215080000Z
# (Root) 19980901120000Z <-> 20280128120000Z
patched_x509store.set_time(datetime(2021, 9, 1, 0, 0, 0))

def test_verify_attestation_android_safetynet(self) -> None:
credential = parse_registration_credential_json(
"""{
"id": "AePltP2wAoNYwG5XGc9sfleGgDxQRHdkX8vphNIHv3HylIj_nZo9ncs7bLL65AGmVAc69pS4l64hgOBJU9o2jCQ",
Expand All @@ -38,6 +25,12 @@ def test_verify_attestation_android_safetynet(self, patched_x509store: X509Store
}
"""
)
# (Leaf) 20210719131342Z <-> 20211017131341Z <- Earliest expiration
# (Int.) 20200813000042Z <-> 20270930000042Z
# (Int.) 20200619000042Z <-> 20280128000042Z
# (Root) 20061215080000Z <-> 20211215080000Z
# (Root) 19980901120000Z <-> 20280128120000Z
time = datetime.datetime(2021, 9, 4, 0, 39, 28, 5353, datetime.timezone.utc)

parsed_attestation_object = parse_attestation_object(
credential.response.attestation_object
Expand All @@ -50,27 +43,24 @@ def test_verify_attestation_android_safetynet(self, patched_x509store: X509Store
client_data_json=credential.response.client_data_json,
pem_root_certs_bytes=[],
verify_timestamp_ms=False,
time=time,
)

assert verified is True

@patch("OpenSSL.crypto.X509StoreContext.verify_certificate")
@patch("base64.b64encode")
@patch("cbor2.loads")
def test_verify_attestation_android_safetynet_basic_integrity_true_cts_profile_match_false(
self,
mock_cbor2_loads: MagicMock,
mock_b64encode: MagicMock,
mock_verify_certificate: MagicMock,
):
"""
We're not working with a full WebAuthn response here so we have to mock out some values
because all we really want to test is that such a response is allowed through
"""
mock_cbor2_loads.return_value = {"authData": bytes()}
mock_b64encode.return_value = "3N7YJmISsFM0cdvMAYcHcw==".encode("utf-8")
# Cert chain validation is not important to this test
mock_verify_certificate.return_value = True

# basicIntegrity: True, ctsProfileMatch: False
jws_result_only_fail_cts_check = (
Expand Down Expand Up @@ -108,6 +98,7 @@ def test_verify_attestation_android_safetynet_basic_integrity_true_cts_profile_m
"S2W1MzvpXwq1KMFvrcka7C4t5vyOhMMYwY6BWEnAGcx5_tpJsqegXTgTHSrr4TFQJzsa-H8wb1"
"YaxlMcRVSqOew"
)
time = datetime.datetime(2021, 9, 4, 0, 39, 28, 5353, datetime.timezone.utc)

attestation_statement = AttestationStatement(
ver="0",
Expand All @@ -120,27 +111,24 @@ def test_verify_attestation_android_safetynet_basic_integrity_true_cts_profile_m
client_data_json=bytes(),
pem_root_certs_bytes=[],
verify_timestamp_ms=False,
time=time,
)

assert verified is True

@patch("OpenSSL.crypto.X509StoreContext.verify_certificate")
@patch("base64.b64encode")
@patch("cbor2.loads")
def test_raise_attestation_android_safetynet_basic_integrity_false_cts_profile_match_false(
self,
mock_cbor2_loads: MagicMock,
mock_b64encode: MagicMock,
mock_verify_certificate: MagicMock,
):
"""
We're not working with a full WebAuthn response here so we have to mock out some values
because all we really want to test is that a response fails the basicIntegrity check
"""
mock_cbor2_loads.return_value = {"authData": bytes()}
mock_b64encode.return_value = "NumMA+QH27ik6Mu737RgWg==".encode("utf-8")
# Cert chain validation is not important to this test
mock_verify_certificate.return_value = True

# basicIntegrity: False, ctsProfileMatch: False
jws_result_fail = (
Expand Down Expand Up @@ -178,6 +166,7 @@ def test_raise_attestation_android_safetynet_basic_integrity_false_cts_profile_m
"4OVdwMd5seh5483VEpqAmzX7NcZ0aoiMl5PhLGgzHZTrsd1Mc-RZqgc3hAYjnubxONN8vOWGzP"
"gI2Vzgr4VzLOZsWfYwKSR5g"
)
time = datetime.datetime(2019, 10, 20, 0, 39, 28, 5353, datetime.timezone.utc)

attestation_statement = AttestationStatement(
ver="0",
Expand All @@ -194,4 +183,5 @@ def test_raise_attestation_android_safetynet_basic_integrity_false_cts_profile_m
client_data_json=bytes(),
pem_root_certs_bytes=[],
verify_timestamp_ms=False,
time=time,
)
24 changes: 8 additions & 16 deletions tests/test_verify_registration_response_apple.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import datetime
from unittest import TestCase
from datetime import datetime

from OpenSSL.crypto import X509Store

from webauthn.helpers import base64url_to_bytes
from webauthn.helpers.structs import AttestationFormat
from webauthn import verify_registration_response

from .helpers.x509store import patch_validate_certificate_chain_x509store_getter


class TestVerifyRegistrationResponseApple(TestCase):
@patch_validate_certificate_chain_x509store_getter
def test_verify_attestation_apple_passkey(
self,
patched_x509store: X509Store,
) -> None:
# Setting the time to something that satisfies all these:
# (Leaf) 20210831230207Z <-> 20210903230207Z <- Earliest expiration
# (Int.) 20200318183801Z <-> 20300313000000Z
# (Root) 20200318182132Z <-> 20450315000000Z
patched_x509store.set_time(datetime(2021, 9, 1, 0, 0, 0))

def test_verify_attestation_apple_passkey(self) -> None:
credential = """{
"id": "0yhsKG_gCzynIgNbvXWkqJKL8Uc",
"rawId": "0yhsKG_gCzynIgNbvXWkqJKL8Uc",
Expand All @@ -37,12 +23,18 @@ def test_verify_attestation_apple_passkey(
)
rp_id = "dev2.dontneeda.pw"
expected_origin = "https://dev2.dontneeda.pw:5000"
# Setting the time to something that satisfies all these:
# (Leaf) 20210831230207Z <-> 20210903230207Z <- Earliest expiration
# (Int.) 20200318183801Z <-> 20300313000000Z
# (Root) 20200318182132Z <-> 20450315000000Z
time = datetime.datetime(2021, 9, 1, 0, 39, 28, 5353, datetime.timezone.utc)

verification = verify_registration_response(
credential=credential,
expected_challenge=challenge,
expected_origin=expected_origin,
expected_rp_id=rp_id,
time=time,
)

assert verification.fmt == AttestationFormat.APPLE
Expand Down
Loading