You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
8.7 KiB
Python

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import functools
import typing
from base64 import urlsafe_b64decode
from base64 import urlsafe_b64encode
from enum import Enum
class Protocol(Enum):
"""
Protocol to communicate with the authenticator.
"""
CTAP2 = "ctap2"
U2F = "ctap1/u2f"
class Transport(Enum):
"""
Transport method to communicate with the authenticator.
"""
BLE = "ble"
USB = "usb"
NFC = "nfc"
INTERNAL = "internal"
class VirtualAuthenticatorOptions:
Protocol = Protocol
Transport = Transport
def __init__(self) -> None:
"""Constructor. Initialize VirtualAuthenticatorOptions object.
:default:
- protocol: Protocol.CTAP2
- transport: Transport.USB
- hasResidentKey: False
- hasUserVerification: False
- isUserConsenting: True
- isUserVerified: False
"""
self._protocol: Protocol = Protocol.CTAP2
self._transport: Transport = Transport.USB
self._has_resident_key: bool = False
self._has_user_verification: bool = False
self._is_user_consenting: bool = True
self._is_user_verified: bool = False
@property
def protocol(self) -> str:
return self._protocol.value
@protocol.setter
def protocol(self, protocol: Protocol) -> None:
self._protocol = protocol
@property
def transport(self) -> str:
return self._transport.value
@transport.setter
def transport(self, transport: Transport) -> None:
self._transport = transport
@property
def has_resident_key(self) -> bool:
return self._has_resident_key
@has_resident_key.setter
def has_resident_key(self, value: bool) -> None:
self._has_resident_key = value
@property
def has_user_verification(self) -> bool:
return self._has_user_verification
@has_user_verification.setter
def has_user_verification(self, value: bool) -> None:
self._has_user_verification = value
@property
def is_user_consenting(self) -> bool:
return self._is_user_consenting
@is_user_consenting.setter
def is_user_consenting(self, value: bool) -> None:
self._is_user_consenting = value
@property
def is_user_verified(self) -> bool:
return self._is_user_verified
@is_user_verified.setter
def is_user_verified(self, value: bool) -> None:
self._is_user_verified = value
def to_dict(self) -> typing.Dict[str, typing.Any]:
return {
"protocol": self.protocol,
"transport": self.transport,
"hasResidentKey": self.has_resident_key,
"hasUserVerification": self.has_user_verification,
"isUserConsenting": self.is_user_consenting,
"isUserVerified": self.is_user_verified,
}
class Credential:
def __init__(
self,
credential_id: bytes,
is_resident_credential: bool,
rp_id: str,
user_handle: typing.Optional[bytes],
private_key: bytes,
sign_count: int,
):
"""Constructor. A credential stored in a virtual authenticator.
https://w3c.github.io/webauthn/#credential-parameters
:Args:
- credential_id (bytes): Unique base64 encoded string.
is_resident_credential (bool): Whether the credential is client-side discoverable.
rp_id (str): Relying party identifier.
user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string. Can be None.
private_key (bytes): Base64 encoded PKCS#8 private key.
sign_count (int): intital value for a signature counter.
"""
self._id = credential_id
self._is_resident_credential = is_resident_credential
self._rp_id = rp_id
self._user_handle = user_handle
self._private_key = private_key
self._sign_count = sign_count
@property
def id(self) -> str:
return urlsafe_b64encode(self._id).decode()
@property
def is_resident_credential(self) -> bool:
return self._is_resident_credential
@property
def rp_id(self) -> str:
return self._rp_id
@property
def user_handle(self) -> typing.Optional[str]:
if self._user_handle:
return urlsafe_b64encode(self._user_handle).decode()
return None
@property
def private_key(self) -> str:
return urlsafe_b64encode(self._private_key).decode()
@property
def sign_count(self) -> int:
return self._sign_count
@classmethod
def create_non_resident_credential(cls, id: bytes, rp_id: str, private_key: bytes, sign_count: int) -> "Credential":
"""Creates a non-resident (i.e. stateless) credential.
:Args:
- id (bytes): Unique base64 encoded string.
- rp_id (str): Relying party identifier.
- private_key (bytes): Base64 encoded PKCS
- sign_count (int): intital value for a signature counter.
:Returns:
- Credential: A non-resident credential.
"""
return cls(id, False, rp_id, None, private_key, sign_count)
@classmethod
def create_resident_credential(
cls, id: bytes, rp_id: str, user_handle: typing.Optional[bytes], private_key: bytes, sign_count: int
) -> "Credential":
"""Creates a resident (i.e. stateful) credential.
:Args:
- id (bytes): Unique base64 encoded string.
- rp_id (str): Relying party identifier.
- user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string.
- private_key (bytes): Base64 encoded PKCS
- sign_count (int): intital value for a signature counter.
:returns:
- Credential: A resident credential.
"""
return cls(id, True, rp_id, user_handle, private_key, sign_count)
def to_dict(self) -> typing.Dict[str, typing.Any]:
credential_data = {
"credentialId": self.id,
"isResidentCredential": self._is_resident_credential,
"rpId": self.rp_id,
"privateKey": self.private_key,
"signCount": self.sign_count,
}
if self.user_handle:
credential_data["userHandle"] = self.user_handle
return credential_data
@classmethod
def from_dict(cls, data: typing.Dict[str, typing.Any]) -> "Credential":
_id = urlsafe_b64decode(f"{data['credentialId']}==")
is_resident_credential = bool(data["isResidentCredential"])
rp_id = data.get("rpId", None)
private_key = urlsafe_b64decode(f"{data['privateKey']}==")
sign_count = int(data["signCount"])
user_handle = urlsafe_b64decode(f"{data['userHandle']}==") if data.get("userHandle", None) else None
return cls(_id, is_resident_credential, rp_id, user_handle, private_key, sign_count)
def __str__(self) -> str:
return f"Credential(id={self.id}, is_resident_credential={self.is_resident_credential}, rp_id={self.rp_id},\
user_handle={self.user_handle}, private_key={self.private_key}, sign_count={self.sign_count})"
def required_chromium_based_browser(func):
"""
A decorator to ensure that the client used is a chromium based browser.
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
assert self.caps["browserName"].lower() not in [
"firefox",
"safari",
], "This only currently works in Chromium based browsers"
return func(self, *args, **kwargs)
return wrapper
def required_virtual_authenticator(func):
"""
A decorator to ensure that the function is called with a virtual authenticator.
"""
@functools.wraps(func)
@required_chromium_based_browser
def wrapper(self, *args, **kwargs):
if not self.virtual_authenticator_id:
raise ValueError("This function requires a virtual authenticator to be set.")
return func(self, *args, **kwargs)
return wrapper