Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support python-fido2 v1.2 #4

Open
wants to merge 11 commits into
base: trunk
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion pindeps
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/sh

pip-compile --resolver=backtracking --no-emit-index-url --extra=dev
pip-compile --upgrade --no-emit-index-url --extra=dev
2 changes: 1 addition & 1 deletion pindeps.bat
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pip-compile --resolver=backtracking --no-emit-index-url --extra=dev --output-file=requirements.windows.txt pyproject.toml
pip-compile --upgrade --no-emit-index-url --extra=dev --output-file=requirements.windows.txt pyproject.toml
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "tokenring"
version = "2023.4.22"
dependencies = [
"fido2",
"fido2>=1.2.0",
"cryptography",
"keyring",
"pyuac",
Expand Down
42 changes: 22 additions & 20 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,48 +1,50 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --extra=dev --no-emit-index-url --resolver=backtracking
# pip-compile --extra=dev --no-emit-index-url
#
build==0.10.0
build==1.2.2.post1
# via tokenring (pyproject.toml)
cffi==1.15.1
cffi==1.17.1
# via cryptography
click==8.1.3
click==8.1.7
# via tokenring (pyproject.toml)
cryptography==40.0.1
cryptography==44.0.0
# via
# fido2
# tokenring (pyproject.toml)
decorator==5.1.1
# via pyuac
fido2==1.1.1
fido2==1.2.0
# via tokenring (pyproject.toml)
importlib-metadata==6.3.0
jaraco-classes==3.4.0
# via keyring
jaraco-classes==3.2.3
jaraco-context==6.0.1
# via keyring
keyring==23.13.1
jaraco-functools==4.1.0
# via keyring
keyring==25.5.0
# via tokenring (pyproject.toml)
more-itertools==9.1.0
# via jaraco-classes
mypy==1.2.0
more-itertools==10.5.0
# via
# jaraco-classes
# jaraco-functools
mypy==1.13.0
# via tokenring (pyproject.toml)
mypy-extensions==1.0.0
# via mypy
packaging==23.1
packaging==24.2
# via build
pycparser==2.21
pycparser==2.22
# via cffi
pyproject-hooks==1.0.0
pyproject-hooks==1.2.0
# via build
pyuac==0.0.3
# via tokenring (pyproject.toml)
tee==0.0.3
# via pyuac
types-pywin32==306.0.0.1
types-pywin32==308.0.0.20241128
# via tokenring (pyproject.toml)
typing-extensions==4.5.0
typing-extensions==4.12.2
# via mypy
zipp==3.15.0
# via importlib-metadata
55 changes: 33 additions & 22 deletions requirements.windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,67 @@
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --extra=dev --no-emit-index-url --output-file=requirements.windows.txt --resolver=backtracking pyproject.toml
# pip-compile --extra=dev --no-emit-index-url --output-file=requirements.windows.txt pyproject.toml
#
build==0.10.0
backports-tarfile==1.2.0
# via jaraco-context
build==1.2.2.post1
# via tokenring (pyproject.toml)
cffi==1.15.1
cffi==1.17.1
# via cryptography
click==8.1.7
# via tokenring (pyproject.toml)
colorama==0.4.6
# via build
cryptography==40.0.1
# via
# build
# click
cryptography==44.0.0
# via
# fido2
# tokenring (pyproject.toml)
decorator==5.1.1
# via pyuac
fido2==1.1.1
fido2==1.2.0
# via tokenring (pyproject.toml)
importlib-metadata==6.3.0
importlib-metadata==8.5.0
# via keyring
jaraco-classes==3.2.3
jaraco-classes==3.4.0
# via keyring
keyring==23.13.1
jaraco-context==6.0.1
# via keyring
jaraco-functools==4.1.0
# via keyring
keyring==25.5.0
# via tokenring (pyproject.toml)
more-itertools==9.1.0
# via jaraco-classes
mypy==1.2.0
more-itertools==10.5.0
# via
# jaraco-classes
# jaraco-functools
mypy==1.13.0
# via tokenring (pyproject.toml)
mypy-extensions==1.0.0
# via mypy
packaging==23.1
packaging==24.2
# via build
pycparser==2.21
pycparser==2.22
# via cffi
pyproject-hooks==1.0.0
pyproject-hooks==1.2.0
# via build
pyuac==0.0.3
# via tokenring (pyproject.toml)
pywin32==306 ; os_name == "nt"
pywin32==308 ; os_name == "nt"
# via tokenring (pyproject.toml)
pywin32-ctypes==0.2.0
pywin32-ctypes==0.2.3
# via keyring
tee==0.0.3
# via pyuac
tomli==2.0.1
tomli==2.2.1
# via
# build
# mypy
# pyproject-hooks
types-pywin32==306.0.0.1
types-pywin32==308.0.0.20241128
# via tokenring (pyproject.toml)
typing-extensions==4.5.0
typing-extensions==4.12.2
# via mypy
zipp==3.15.0
zipp==3.21.0
# via importlib-metadata
3 changes: 0 additions & 3 deletions src/tokenring/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
from multiprocessing.connection import Listener
from pathlib import Path

from pyuac import main_requires_admin # type:ignore[import]

import click

from .agent.common import address, auth_key, family
Expand Down Expand Up @@ -55,7 +53,6 @@ def set(servicename: str, username: str) -> None:
real_argv = argv[:]
@cli.command()
@click.argument("vault_path", required=False, type=click_path)
@main_requires_admin(cmdLine=real_argv)
def agent(vault_path: Path | None) -> None:

local_ring = (
Expand Down
19 changes: 11 additions & 8 deletions src/tokenring/fidoclient.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
from __future__ import annotations

from typing import (
Callable,
Iterable,
Sequence,
TYPE_CHECKING,
)
import os
import ctypes
from typing import TYPE_CHECKING, Callable, Iterable, Sequence

from fido2.client import Fido2Client, UserInteraction, WindowsClient
from fido2.ctap2.extensions import HmacSecretExtension
from fido2.hid import CtapHidDevice
import ctypes

try:
from fido2.pcsc import CtapPcscDevice
Expand Down Expand Up @@ -46,14 +43,15 @@ def enumerate_clients(
if WindowsClient.is_available():
is_admin: bool = ctypes.windll.shell32.IsUserAnAdmin() # type:ignore
if not is_admin:
yield (WindowsClient(fake_url), None)
yield (WindowsClient(fake_url, allow_hmac_secret=True), None)
return
for dev in enumerate_devices():
yield (
Fido2Client(
dev,
fake_url,
user_interaction=interaction,
extensions=[HmacSecretExtension(allow_hmac_secret=True)],
),
dev,
)
Expand All @@ -63,6 +61,11 @@ def extension_required(client: AnyFidoClient) -> bool:
"""
Client filter for clients that support the hmac-secret extension.
"""
if os.name == 'nt':
# TODO: report this upstream; Windows (without administrator access, at
# least) reports an empty extension list, even if your device can do
# hmac-secret.
return True
has_extension = "hmac-secret" in client.info.extensions
return has_extension

Expand Down
26 changes: 20 additions & 6 deletions src/tokenring/handles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from base64 import urlsafe_b64encode as encode_fernet_key
from dataclasses import dataclass
from typing import (
Any,
ClassVar,
Sequence,
TypedDict,
Expand All @@ -23,9 +24,22 @@

from .fidoclient import AnyFidoClient


SerializedCredentialHandle = dict[str, str]

def platform_specific_extract_extension_results(results: Any)->bytes:
"""
There's a bug in python-fido2 which reflects extension output values as
literal dictionaries full of bytes on Windows (which is what it used to do
everywhere) and magical dict-proxy-but-also-has-some-attributes objects on
all other platforms, where the other platforms reflect the dict-ish values
as base64-encoded strings and the extra attributes they provide (but do not
provide type annotations for) are the original bytes.
"""
if os.name == 'nt':
return results["hmacGetSecret"]["output1"]
else:
return results.hmacGetSecret.output1


@dataclass
class CredentialHandle:
Expand Down Expand Up @@ -82,7 +96,7 @@ def new_credential(cls, client: AnyFidoClient) -> CredentialHandle:
assert credential is not None
return CredentialHandle(client=client, credential_id=credential.credential_id)

def key_from_salt(self, salt) -> bytes:
def key_from_salt(self, salt: bytes) -> bytes:
"""
Get the actual secret key from the hardware.

Expand All @@ -103,9 +117,9 @@ def key_from_salt(self, salt) -> bytes:
)
# Only one cred in allowList, only one response.
assertion_itself = self.client.get_assertion(options)
assertion_result = assertion_itself.get_response(0)
assertion_result: Any = assertion_itself.get_response(0)
assert assertion_result.extension_results is not None
output1 = assertion_result.extension_results["hmacGetSecret"]["output1"]
output1: bytes = platform_specific_extract_extension_results(assertion_result.extension_results)
return output1

def serialize(self) -> SerializedCredentialHandle:
Expand Down Expand Up @@ -176,7 +190,7 @@ def encrypt_bytes(self, plaintext: bytes) -> bytes:
"""
Encrypt some plaintext bytes.
"""
key_bytes = self.key_as_bytes()
key_bytes: bytes = self.key_as_bytes()
fernet_key = encode_fernet_key(key_bytes)
fernet = Fernet(fernet_key)
ciphertext = fernet.encrypt(plaintext)
Expand All @@ -186,7 +200,7 @@ def decrypt_bytes(self, ciphertext: bytes) -> bytes:
"""
Decrypt some enciphered bytes.
"""
key_bytes = self.key_as_bytes()
key_bytes: bytes = self.key_as_bytes()
fernet_key = encode_fernet_key(key_bytes)
fernet = Fernet(fernet_key)
plaintext = fernet.decrypt(ciphertext)
Expand Down