tba-protocol-webauthn 0.4.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. tba_protocol_webauthn/__init__.py +131 -0
  2. tba_protocol_webauthn/attestation/__init__.py +8 -0
  3. tba_protocol_webauthn/attestation/adapters.py +51 -0
  4. tba_protocol_webauthn/attestation/registry.py +59 -0
  5. tba_protocol_webauthn/bindings.py +46 -0
  6. tba_protocol_webauthn/ceremonies/__init__.py +21 -0
  7. tba_protocol_webauthn/ceremonies/authentication.py +169 -0
  8. tba_protocol_webauthn/ceremonies/registration.py +220 -0
  9. tba_protocol_webauthn/claims.py +5 -0
  10. tba_protocol_webauthn/codecs/__init__.py +23 -0
  11. tba_protocol_webauthn/codecs/attestation_object.py +55 -0
  12. tba_protocol_webauthn/codecs/authenticator_data.py +120 -0
  13. tba_protocol_webauthn/codecs/client_data.py +107 -0
  14. tba_protocol_webauthn/codecs/credential_public_key.py +10 -0
  15. tba_protocol_webauthn/compatibility.py +11 -0
  16. tba_protocol_webauthn/configuration.py +33 -0
  17. tba_protocol_webauthn/errors.py +29 -0
  18. tba_protocol_webauthn/extensions/__init__.py +17 -0
  19. tba_protocol_webauthn/extensions/registry.py +68 -0
  20. tba_protocol_webauthn/features.py +41 -0
  21. tba_protocol_webauthn/migrations.py +23 -0
  22. tba_protocol_webauthn/protocol.py +40 -0
  23. tba_protocol_webauthn/py.typed +0 -0
  24. tba_protocol_webauthn/related_origins.py +21 -0
  25. tba_protocol_webauthn/schemas.py +29 -0
  26. tba_protocol_webauthn/versions.py +54 -0
  27. tba_protocol_webauthn/wire_schemas/__init__.py +27 -0
  28. tba_protocol_webauthn/wire_schemas/common.py +113 -0
  29. tba_protocol_webauthn/wire_schemas/level2.py +29 -0
  30. tba_protocol_webauthn/wire_schemas/level3.py +29 -0
  31. tba_protocol_webauthn-0.4.0.dev2.dist-info/METADATA +27 -0
  32. tba_protocol_webauthn-0.4.0.dev2.dist-info/RECORD +33 -0
  33. tba_protocol_webauthn-0.4.0.dev2.dist-info/WHEEL +4 -0
@@ -0,0 +1,131 @@
1
+ """Versioned W3C Web Authentication relying-party protocol."""
2
+
3
+ from .attestation import (
4
+ AttestationVerificationInput,
5
+ AttestationVerifierRegistry,
6
+ build_attestation_registry,
7
+ )
8
+ from .bindings import CAPABILITY_REQUIREMENTS
9
+ from .ceremonies import (
10
+ AuthenticationExpectation,
11
+ RegistrationExpectation,
12
+ build_creation_options,
13
+ build_request_options,
14
+ verify_authentication_response,
15
+ verify_registration_response,
16
+ )
17
+ from .claims import CLAIM_NAMES
18
+ from .codecs import (
19
+ AttestationObject,
20
+ AttestedCredentialData,
21
+ AuthenticatorData,
22
+ AuthenticatorDataFlags,
23
+ ParsedClientData,
24
+ decode_attestation_object,
25
+ decode_authenticator_data,
26
+ decode_credential_public_key,
27
+ parse_client_data,
28
+ )
29
+ from .compatibility import compatible_features
30
+ from .configuration import WebAuthnConfiguration
31
+ from .errors import (
32
+ AuthenticationVerificationError,
33
+ AttestationObjectError,
34
+ AuthenticatorDataError,
35
+ ClientDataError,
36
+ RegistrationVerificationError,
37
+ WebAuthnProtocolError,
38
+ WebAuthnVerificationError,
39
+ )
40
+ from .extensions import (
41
+ ExtensionRegistry,
42
+ validate_appid,
43
+ validate_cred_props,
44
+ validate_credential_protection,
45
+ validate_large_blob,
46
+ validate_payment,
47
+ )
48
+ from .features import BASE_FEATURES, LEVEL_3_FEATURES, WebAuthnFeature, features_for
49
+ from .migrations import migration_path
50
+ from .protocol import WebAuthnProtocol
51
+ from .related_origins import origin_is_allowed
52
+ from .schemas import (
53
+ AuthenticatorAssertionResponse,
54
+ AuthenticatorAttestationResponse,
55
+ AuthenticatorSelectionCriteria,
56
+ CollectedClientData,
57
+ PublicKeyCredential,
58
+ PublicKeyCredentialCreationOptions,
59
+ PublicKeyCredentialDescriptor,
60
+ PublicKeyCredentialParameters,
61
+ PublicKeyCredentialRequestOptions,
62
+ PublicKeyCredentialRpEntity,
63
+ PublicKeyCredentialUserEntity,
64
+ )
65
+ from .versions import (
66
+ CURRENT_VERSION,
67
+ WEB_AUTHN_REVISIONS,
68
+ WebAuthnRevision,
69
+ WebAuthnVersion,
70
+ resolve_revision,
71
+ )
72
+
73
+ __all__ = [
74
+ "AttestationObject",
75
+ "AttestationVerificationInput",
76
+ "AttestationVerifierRegistry",
77
+ "AttestedCredentialData",
78
+ "AuthenticationExpectation",
79
+ "AuthenticationVerificationError",
80
+ "AuthenticatorAssertionResponse",
81
+ "AuthenticatorAttestationResponse",
82
+ "AuthenticatorData",
83
+ "AuthenticatorDataError",
84
+ "AuthenticatorDataFlags",
85
+ "AuthenticatorSelectionCriteria",
86
+ "BASE_FEATURES",
87
+ "CAPABILITY_REQUIREMENTS",
88
+ "CLAIM_NAMES",
89
+ "CURRENT_VERSION",
90
+ "ClientDataError",
91
+ "CollectedClientData",
92
+ "ExtensionRegistry",
93
+ "LEVEL_3_FEATURES",
94
+ "ParsedClientData",
95
+ "PublicKeyCredential",
96
+ "PublicKeyCredentialCreationOptions",
97
+ "PublicKeyCredentialDescriptor",
98
+ "PublicKeyCredentialParameters",
99
+ "PublicKeyCredentialRequestOptions",
100
+ "PublicKeyCredentialRpEntity",
101
+ "PublicKeyCredentialUserEntity",
102
+ "RegistrationExpectation",
103
+ "RegistrationVerificationError",
104
+ "WEB_AUTHN_REVISIONS",
105
+ "WebAuthnConfiguration",
106
+ "WebAuthnFeature",
107
+ "WebAuthnProtocol",
108
+ "WebAuthnProtocolError",
109
+ "WebAuthnRevision",
110
+ "WebAuthnVerificationError",
111
+ "WebAuthnVersion",
112
+ "build_attestation_registry",
113
+ "build_creation_options",
114
+ "build_request_options",
115
+ "compatible_features",
116
+ "decode_attestation_object",
117
+ "decode_authenticator_data",
118
+ "decode_credential_public_key",
119
+ "features_for",
120
+ "migration_path",
121
+ "origin_is_allowed",
122
+ "parse_client_data",
123
+ "resolve_revision",
124
+ "validate_appid",
125
+ "validate_cred_props",
126
+ "validate_credential_protection",
127
+ "validate_large_blob",
128
+ "validate_payment",
129
+ "verify_authentication_response",
130
+ "verify_registration_response",
131
+ ]
@@ -0,0 +1,8 @@
1
+ from .adapters import build_attestation_registry
2
+ from .registry import AttestationVerificationInput, AttestationVerifierRegistry
3
+
4
+ __all__ = [
5
+ "AttestationVerificationInput",
6
+ "AttestationVerifierRegistry",
7
+ "build_attestation_registry",
8
+ ]
@@ -0,0 +1,51 @@
1
+ """Normalize provider-specific attestation verifier call signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+
7
+ from .registry import AttestationVerifierRegistry
8
+
9
+
10
+ def build_attestation_registry(
11
+ *,
12
+ packed: Callable[..., object] | None = None,
13
+ u2f: Callable[..., object] | None = None,
14
+ tpm: Callable[..., object] | None = None,
15
+ ) -> AttestationVerifierRegistry:
16
+ registry = AttestationVerifierRegistry()
17
+ if packed:
18
+ registry.register(
19
+ "packed",
20
+ lambda value: packed(
21
+ statement=value.statement,
22
+ authenticator_data=value.authenticator_data,
23
+ client_data_hash=value.client_data_hash,
24
+ credential_public_key=value.credential_public_key,
25
+ ),
26
+ )
27
+ if u2f:
28
+ registry.register(
29
+ "fido-u2f",
30
+ lambda value: u2f(
31
+ statement=value.statement,
32
+ rp_id_hash=value.rp_id_hash,
33
+ client_data_hash=value.client_data_hash,
34
+ credential_id=value.credential_id,
35
+ credential_public_key=value.credential_public_key,
36
+ ),
37
+ )
38
+ if tpm:
39
+ registry.register(
40
+ "tpm",
41
+ lambda value: tpm(
42
+ statement=value.statement,
43
+ authenticator_data=value.authenticator_data,
44
+ client_data_hash=value.client_data_hash,
45
+ credential_public_key=value.credential_public_key,
46
+ ),
47
+ )
48
+ return registry
49
+
50
+
51
+ __all__ = ["build_attestation_registry"]
@@ -0,0 +1,59 @@
1
+ """Attestation format dispatch without provider dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass
7
+
8
+ from tba_contracts_identity.public_key_authentication import AttestationTrustResult
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class AttestationVerificationInput:
13
+ format: str
14
+ statement: Mapping[object, object]
15
+ authenticator_data: bytes
16
+ client_data_hash: bytes
17
+ rp_id_hash: bytes
18
+ credential_id: bytes
19
+ credential_public_key: bytes
20
+ aaguid: bytes
21
+
22
+
23
+ AttestationVerifier = Callable[[AttestationVerificationInput], AttestationTrustResult]
24
+
25
+
26
+ class AttestationVerifierRegistry:
27
+ def __init__(
28
+ self, verifiers: Mapping[str, AttestationVerifier] | None = None
29
+ ) -> None:
30
+ self._verifiers = dict(verifiers or {})
31
+
32
+ def register(self, format_identifier: str, verifier: AttestationVerifier) -> None:
33
+ if format_identifier in self._verifiers:
34
+ raise ValueError(
35
+ f"attestation verifier already registered: {format_identifier}"
36
+ )
37
+ self._verifiers[format_identifier] = verifier
38
+
39
+ def verify(self, value: AttestationVerificationInput) -> AttestationTrustResult:
40
+ try:
41
+ verifier = self._verifiers[value.format]
42
+ except KeyError as exc:
43
+ raise ValueError(
44
+ f"attestation format requires a configured provider: {value.format}"
45
+ ) from exc
46
+ result = verifier(value)
47
+ if not isinstance(result, AttestationTrustResult):
48
+ raise TypeError("attestation verifier must return AttestationTrustResult")
49
+ return result
50
+
51
+ def supported_formats(self) -> tuple[str, ...]:
52
+ return tuple(sorted(self._verifiers))
53
+
54
+
55
+ __all__ = [
56
+ "AttestationVerificationInput",
57
+ "AttestationVerifier",
58
+ "AttestationVerifierRegistry",
59
+ ]
@@ -0,0 +1,46 @@
1
+ """Map WebAuthn wire ceremonies to stable semantic capability operations."""
2
+
3
+ from tba_contracts_identity.capabilities import ProtocolCapabilityRequirement
4
+
5
+ from .versions import CURRENT_VERSION
6
+
7
+ CAPABILITY_REQUIREMENTS = (
8
+ ProtocolCapabilityRequirement(
9
+ "webauthn",
10
+ CURRENT_VERSION.identifier,
11
+ "webauthn-registration-begin",
12
+ "PublicKeyCredentialCreationOptions",
13
+ "credential.public-key.registration",
14
+ "begin_public_key_registration",
15
+ "public-key-registration-options",
16
+ ),
17
+ ProtocolCapabilityRequirement(
18
+ "webauthn",
19
+ CURRENT_VERSION.identifier,
20
+ "webauthn-registration-complete",
21
+ "AuthenticatorAttestationResponse",
22
+ "credential.public-key.registration",
23
+ "complete_public_key_registration",
24
+ "verified-public-key-registration",
25
+ ),
26
+ ProtocolCapabilityRequirement(
27
+ "webauthn",
28
+ CURRENT_VERSION.identifier,
29
+ "webauthn-authentication-begin",
30
+ "PublicKeyCredentialRequestOptions",
31
+ "credential.public-key.authentication",
32
+ "begin_public_key_authentication",
33
+ "public-key-authentication-options",
34
+ ),
35
+ ProtocolCapabilityRequirement(
36
+ "webauthn",
37
+ CURRENT_VERSION.identifier,
38
+ "webauthn-authentication-complete",
39
+ "AuthenticatorAssertionResponse",
40
+ "credential.public-key.authentication",
41
+ "complete_public_key_authentication",
42
+ "verified-public-key-assertion",
43
+ ),
44
+ )
45
+
46
+ __all__ = ["CAPABILITY_REQUIREMENTS"]
@@ -0,0 +1,21 @@
1
+ """WebAuthn registration and authentication ceremonies."""
2
+
3
+ from .authentication import (
4
+ AuthenticationExpectation,
5
+ build_request_options,
6
+ verify_authentication_response,
7
+ )
8
+ from .registration import (
9
+ RegistrationExpectation,
10
+ build_creation_options,
11
+ verify_registration_response,
12
+ )
13
+
14
+ __all__ = [
15
+ "AuthenticationExpectation",
16
+ "RegistrationExpectation",
17
+ "build_creation_options",
18
+ "build_request_options",
19
+ "verify_authentication_response",
20
+ "verify_registration_response",
21
+ ]
@@ -0,0 +1,169 @@
1
+ """WebAuthn Level 2 authentication ceremony processing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import secrets
7
+ from dataclasses import dataclass
8
+
9
+ from tba_contracts_identity.public_key_authentication import (
10
+ AuthenticatorProvenanceEvidence,
11
+ CredentialBackupEvidence,
12
+ CredentialLifecycleStatus,
13
+ PublicKeyAuthenticationEvidence,
14
+ PublicKeyCredentialSource,
15
+ UserPresenceEvidence,
16
+ UserVerificationEvidence,
17
+ VerifiedPublicKeyAssertion,
18
+ VerifierBindingEvidence,
19
+ )
20
+ from tba_provider_cose_cryptography import verify_detached_signature
21
+
22
+ from ..codecs import decode_authenticator_data, parse_client_data
23
+ from ..errors import AuthenticationVerificationError
24
+ from ..schemas import (
25
+ PublicKeyCredential,
26
+ PublicKeyCredentialDescriptor,
27
+ PublicKeyCredentialRequestOptions,
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class AuthenticationExpectation:
33
+ ceremony_id: str
34
+ rp_id: str
35
+ origin: str
36
+ challenge: bytes
37
+ credential: PublicKeyCredentialSource
38
+ user_verification: str = "preferred"
39
+
40
+
41
+ def build_request_options(
42
+ *,
43
+ rp_id: str,
44
+ allowed_credentials: tuple[PublicKeyCredentialSource, ...] = (),
45
+ user_verification: str = "preferred",
46
+ timeout_ms: int = 300_000,
47
+ challenge: bytes | None = None,
48
+ ) -> PublicKeyCredentialRequestOptions:
49
+ return PublicKeyCredentialRequestOptions(
50
+ challenge=challenge or secrets.token_bytes(32),
51
+ timeout=timeout_ms,
52
+ rp_id=rp_id,
53
+ allow_credentials=tuple(
54
+ PublicKeyCredentialDescriptor(
55
+ id=item.external_id, transports=item.transports
56
+ )
57
+ for item in allowed_credentials
58
+ ),
59
+ user_verification=user_verification,
60
+ )
61
+
62
+
63
+ def verify_authentication_response(
64
+ credential: PublicKeyCredential,
65
+ *,
66
+ expected: AuthenticationExpectation,
67
+ ) -> VerifiedPublicKeyAssertion:
68
+ try:
69
+ source = expected.credential
70
+ if source.status is not CredentialLifecycleStatus.ACTIVE:
71
+ raise AuthenticationVerificationError("credential is not active")
72
+ if source.binding.rp_id != expected.rp_id:
73
+ raise AuthenticationVerificationError(
74
+ "credential is not bound to the relying party"
75
+ )
76
+ if credential.type != "public-key" or not secrets.compare_digest(
77
+ credential.raw_id, source.external_id
78
+ ):
79
+ raise AuthenticationVerificationError(
80
+ "asserted credential identifier does not match"
81
+ )
82
+ response = credential.response
83
+ if not hasattr(response, "signature"):
84
+ raise AuthenticationVerificationError(
85
+ "authentication requires an assertion response"
86
+ )
87
+ client = parse_client_data(
88
+ response.client_data_json,
89
+ expected_type="webauthn.get",
90
+ expected_challenge=expected.challenge,
91
+ expected_origin=expected.origin,
92
+ )
93
+ if client.value.cross_origin:
94
+ raise AuthenticationVerificationError(
95
+ "cross-origin authentication is not enabled"
96
+ )
97
+ auth = decode_authenticator_data(response.authenticator_data)
98
+ if not auth.matches_rp_id_hash(
99
+ hashlib.sha256(expected.rp_id.encode("utf-8")).digest()
100
+ ):
101
+ raise AuthenticationVerificationError(
102
+ "relying-party identifier hash does not match"
103
+ )
104
+ if not auth.user_present:
105
+ raise AuthenticationVerificationError(
106
+ "authenticator did not report user presence"
107
+ )
108
+ if expected.user_verification == "required" and not auth.user_verified:
109
+ raise AuthenticationVerificationError(
110
+ "authenticator did not report user verification"
111
+ )
112
+ if response.user_handle is not None and source.binding.user_handle is not None:
113
+ if not secrets.compare_digest(
114
+ response.user_handle, source.binding.user_handle
115
+ ):
116
+ raise AuthenticationVerificationError(
117
+ "asserted user handle does not match"
118
+ )
119
+ if not verify_detached_signature(
120
+ public_key=source.public_key,
121
+ algorithm=source.algorithm,
122
+ message=response.authenticator_data + client.digest,
123
+ signature=response.signature,
124
+ ):
125
+ raise AuthenticationVerificationError("assertion signature is invalid")
126
+ if (
127
+ source.sign_count
128
+ and auth.sign_count
129
+ and auth.sign_count <= source.sign_count
130
+ ):
131
+ raise AuthenticationVerificationError(
132
+ "authenticator counter did not advance"
133
+ )
134
+ principal_id = source.binding.principal_id
135
+ if principal_id is None:
136
+ raise AuthenticationVerificationError(
137
+ "credential has no resolved principal binding"
138
+ )
139
+ evidence = PublicKeyAuthenticationEvidence(
140
+ credential_id=source.credential_id,
141
+ user_presence=UserPresenceEvidence(True),
142
+ user_verification=UserVerificationEvidence(auth.user_verified),
143
+ verifier_binding=VerifierBindingEvidence(
144
+ expected.rp_id, expected.origin, True
145
+ ),
146
+ backup=CredentialBackupEvidence(
147
+ eligible=bool(auth.flags & 0x08),
148
+ backed_up=bool(auth.flags & 0x10),
149
+ ),
150
+ provenance=AuthenticatorProvenanceEvidence(),
151
+ )
152
+ return VerifiedPublicKeyAssertion(
153
+ ceremony_id=expected.ceremony_id,
154
+ credential_id=source.credential_id,
155
+ principal_id=principal_id,
156
+ sign_count=auth.sign_count,
157
+ evidence=evidence,
158
+ )
159
+ except AuthenticationVerificationError:
160
+ raise
161
+ except Exception as exc:
162
+ raise AuthenticationVerificationError(str(exc)) from exc
163
+
164
+
165
+ __all__ = [
166
+ "AuthenticationExpectation",
167
+ "build_request_options",
168
+ "verify_authentication_response",
169
+ ]