Skip to content

Commit bd2dbda

Browse files
Accept a CCID device instead of a ctaphid device
1 parent e1b97dc commit bd2dbda

File tree

5 files changed

+129
-21
lines changed

5 files changed

+129
-21
lines changed

poetry.lock

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
1+
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
22

33
[[package]]
44
name = "alabaster"
@@ -1069,6 +1069,38 @@ files = [
10691069
[package.extras]
10701070
windows-terminal = ["colorama (>=0.4.6)"]
10711071

1072+
[[package]]
1073+
name = "pyscard"
1074+
version = "2.3.1"
1075+
description = "Smartcard module for Python."
1076+
optional = false
1077+
python-versions = ">=3.9"
1078+
groups = ["main"]
1079+
files = [
1080+
{file = "pyscard-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5bd277142644441cba8fe71bc991fe83108147f3ba22c01e8b9a5a3f41a31f69"},
1081+
{file = "pyscard-2.3.1-cp310-cp310-win32.whl", hash = "sha256:717ea958ee77d7d4514ff0a6eb238082f9437e7882479ee6f742bb9db54419d4"},
1082+
{file = "pyscard-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:91b27548eddcd6c21f115d5e6151ced9b348aae989b0e4fcc1ad4c479e61610c"},
1083+
{file = "pyscard-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:341eca9151318093d5543fa812736546de19020d214e01aff9bbc231d1429949"},
1084+
{file = "pyscard-2.3.1-cp311-cp311-win32.whl", hash = "sha256:be78734964621b59f8b63c90b822169dc804571df57f574733ccf585a31769af"},
1085+
{file = "pyscard-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52da45a3becfb6807dcd9427d763aeb154c9c9e9ad324a13a2bf322fa31baca5"},
1086+
{file = "pyscard-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:e228c78e028b74e5411a604c765852bdc8fd04321f03e62d25cad8e90de27597"},
1087+
{file = "pyscard-2.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:acb9f4842cb08110d916e7e37ef225c755a1da627017364c55df547b6f187dcf"},
1088+
{file = "pyscard-2.3.1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:abf5091d5067ac4945eb2a283b60bddce7530595e585868449da12d3b9e885ee"},
1089+
{file = "pyscard-2.3.1-cp312-cp312-win32.whl", hash = "sha256:c5d9fde122ffd41a74af72364166e8b23760230163e20c4c130aa0616bf4a786"},
1090+
{file = "pyscard-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:561889413866141b3a44099f043d10344ee64b0604d2fecb275975b54aa584c6"},
1091+
{file = "pyscard-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:06ae5e5421a5dd380e8c80046b4242e7b98ed381247117f4cf2c1c8328f74bce"},
1092+
{file = "pyscard-2.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c11a596407e18cdcf16a4ccd8cdeaa55846d4f7ec2eefc529483e201f6906658"},
1093+
{file = "pyscard-2.3.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:72b1ab922fad5e050144ec72762e36741271b09d2389cda9b976b61ee0564e71"},
1094+
{file = "pyscard-2.3.1-cp313-cp313-win32.whl", hash = "sha256:a0b59d1961ff9fb15d980ad64edae13e4512b7e641ea8959e86133f34091aa5c"},
1095+
{file = "pyscard-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df2b256bc719b701807114bdd179f7b303f309a954d5689188b088adfc33ead2"},
1096+
{file = "pyscard-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:f58b46cd78455a29a0abceabff21b37da81a385a737b29e6dd5e25acb7e3f3da"},
1097+
{file = "pyscard-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a36bab071c6f4b1d74a8778a784ccf3cc04bf35a6c677bce15e70fdbeac5b932"},
1098+
{file = "pyscard-2.3.1.tar.gz", hash = "sha256:a24356f57a0a950740b6e54f51f819edd5296ee8892a6625b0da04724e9e6c13"},
1099+
]
1100+
1101+
[package.extras]
1102+
gui = ["wxPython"]
1103+
10721104
[[package]]
10731105
name = "pyserial"
10741106
version = "3.5"
@@ -1605,4 +1637,4 @@ files = [
16051637
[metadata]
16061638
lock-version = "2.1"
16071639
python-versions = ">=3.10, <4"
1608-
content-hash = "50d9098be4b28ec32e2a90380e814e870de37ea7947ca366e7c8a7eb864865ad"
1640+
content-hash = "49fe33bc1e81966a26952b14664aacdbd9855b07be2f8c5f998102406ce9c30f"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies = [
3131
# nrf52
3232
"protobuf >=5.26, <7",
3333
"pyserial >=3.5, <4",
34+
"pyscard >=2, <3"
3435
]
3536

3637
[project.urls]

src/nitrokey/nk3/_device.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
# http://opensource.org/licenses/MIT>, at your option. This file may not be
66
# copied, modified, or distributed except according to those terms.
77

8-
from typing import List
8+
from typing import List, Optional
99

1010
from fido2.hid import CtapHidDevice
11+
from smartcard.CardConnection import CardConnection
1112

1213
from nitrokey import _VID_NITROKEY
1314
from nitrokey.trussed import Fido2Certs, Model, TrussedDevice, Version
@@ -33,8 +34,12 @@
3334
class NK3(TrussedDevice):
3435
"""A Nitrokey 3 device running the firmware."""
3536

36-
def __init__(self, device: CtapHidDevice) -> None:
37-
super().__init__(device, FIDO2_CERTS)
37+
def __init__(
38+
self,
39+
device: Optional[CtapHidDevice],
40+
device_ccid: Optional[CardConnection] = None,
41+
) -> None:
42+
super().__init__(device, FIDO2_CERTS, device_ccid)
3843

3944
@property
4045
def model(self) -> Model:
@@ -51,8 +56,12 @@ def name(self) -> str:
5156
return "Nitrokey 3"
5257

5358
@classmethod
54-
def from_device(cls, device: CtapHidDevice) -> "NK3":
55-
return cls(device)
59+
def from_device(
60+
cls,
61+
device: Optional[CtapHidDevice],
62+
device_ccid: Optional[CardConnection] = None,
63+
) -> "NK3":
64+
return cls(device, device_ccid)
5665

5766
@classmethod
5867
def list(cls) -> List["NK3"]:

src/nitrokey/nkpk.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List, Optional, Sequence, Union
99

1010
from fido2.hid import CtapHidDevice
11+
from smartcard.CardConnection import CardConnection
1112

1213
from nitrokey import _VID_NITROKEY
1314
from nitrokey.trussed import Fido2Certs, TrussedDevice, Version
@@ -46,8 +47,16 @@
4647

4748

4849
class NKPK(TrussedDevice):
49-
def __init__(self, device: CtapHidDevice) -> None:
50-
super().__init__(device, _FIDO2_CERTS)
50+
def __init__(
51+
self,
52+
device: Optional[CtapHidDevice],
53+
device_ccid: Optional[CardConnection] = None,
54+
) -> None:
55+
super().__init__(
56+
device,
57+
_FIDO2_CERTS,
58+
device_ccid,
59+
)
5160

5261
@property
5362
def model(self) -> Model:
@@ -62,8 +71,12 @@ def name(self) -> str:
6271
return "Nitrokey Passkey"
6372

6473
@classmethod
65-
def from_device(cls, device: CtapHidDevice) -> "NKPK":
66-
return cls(device)
74+
def from_device(
75+
cls,
76+
device: Optional[CtapHidDevice],
77+
device_ccid: Optional[CardConnection] = None,
78+
) -> "NKPK":
79+
return cls(device, device_ccid)
6780

6881
@classmethod
6982
def list(cls) -> List["NKPK"]:

src/nitrokey/trussed/_device.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from typing import List, Optional, Sequence, TypeVar, Union
1515

1616
from fido2.hid import CtapHidDevice, list_descriptors, open_device
17+
from smartcard.CardConnection import CardConnection
18+
from smartcard.Exceptions import NoCardException
19+
from smartcard.System import readers
1720

1821
from ._base import TrussedBase
1922
from ._utils import Fido2Certs, Uuid
@@ -34,14 +37,19 @@ class App(Enum):
3437

3538
class TrussedDevice(TrussedBase):
3639
def __init__(
37-
self, device: CtapHidDevice, fido2_certs: Sequence[Fido2Certs]
40+
self,
41+
device: Optional[CtapHidDevice],
42+
fido2_certs: Sequence[Fido2Certs],
43+
device_ccid: Optional[CardConnection] = None,
3844
) -> None:
39-
self._validate_vid_pid(device.descriptor.vid, device.descriptor.pid)
45+
if device is not None:
46+
self._validate_vid_pid(device.descriptor.vid, device.descriptor.pid)
47+
self._path = _device_path_to_str(device.descriptor.path)
48+
self._logger = logger.getChild(self._path)
4049

4150
self.device = device
51+
self.device_ccid = device_ccid
4252
self.fido2_certs = fido2_certs
43-
self._path = _device_path_to_str(device.descriptor.path)
44-
self._logger = logger.getChild(self._path)
4553

4654
from .admin_app import AdminApp
4755

@@ -53,7 +61,11 @@ def path(self) -> str:
5361
return self._path
5462

5563
def close(self) -> None:
56-
self.device.close()
64+
if self.device is not None:
65+
self.device.close()
66+
if self.device_ccid is not None:
67+
self.device_ccid.disconnect()
68+
self.device_ccid.release()
5769

5870
def reboot(self) -> bool:
5971
from .admin_app import BootMode
@@ -64,7 +76,8 @@ def uuid(self) -> Optional[Uuid]:
6476
return self.admin.uuid()
6577

6678
def wink(self) -> None:
67-
self.device.wink()
79+
if self.device is not None:
80+
self.device.wink()
6881

6982
def _call(
7083
self,
@@ -73,7 +86,14 @@ def _call(
7386
response_len: Optional[int] = None,
7487
data: bytes = b"",
7588
) -> bytes:
76-
response = self.device.call(command, data=data)
89+
if self.device_ccid is not None:
90+
response = _call_ccid(self.device_ccid, command, data)
91+
elif self.device is not None:
92+
response = self.device.call(command, data=data)
93+
else:
94+
raise RuntimeError(
95+
"Nitrokey device needs either a valid CCID device or a valid CTAPHID device"
96+
)
7797
if response_len is not None and response_len != len(response):
7898
raise ValueError(
7999
f"The response for the CTAPHID {command_name} command has an unexpected length "
@@ -91,7 +111,11 @@ def _call_app(
91111

92112
@classmethod
93113
@abstractmethod
94-
def from_device(cls: type[T], device: CtapHidDevice) -> T: ...
114+
def from_device(
115+
cls: type[T],
116+
device: Optional[CtapHidDevice],
117+
device_ccid: Optional[CardConnection] = None,
118+
) -> T: ...
95119

96120
@classmethod
97121
def open(cls: type[T], path: str) -> Optional[T]:
@@ -104,7 +128,7 @@ def open(cls: type[T], path: str) -> Optional[T]:
104128
logger.warn(f"No CTAPHID device at path {path}", exc_info=sys.exc_info())
105129
return None
106130
try:
107-
return cls.from_device(device)
131+
return cls.from_device(device, device_ccid=None)
108132
except ValueError:
109133
logger.warn(f"No Nitrokey device at path {path}", exc_info=sys.exc_info())
110134
return None
@@ -120,7 +144,36 @@ def _list_vid_pid(cls: type[T], vid: int, pid: int) -> List[T]:
120144
for desc in list_descriptors() # type: ignore
121145
if desc.vid == vid and desc.pid == pid
122146
]
123-
return [cls.from_device(open_device(desc.path)) for desc in descriptors]
147+
return [
148+
cls.from_device(open_device(desc.path), device_ccid=None)
149+
for desc in descriptors
150+
]
151+
152+
@classmethod
153+
def _list_pcsc_atr(cls: type[T], atr: List[int]) -> List[T]:
154+
devices = []
155+
for r in readers():
156+
connection = r.createConnection()
157+
try:
158+
connection.connect()
159+
except NoCardException:
160+
continue
161+
if atr == connection.getATR():
162+
connection.disconnect()
163+
connection.release()
164+
continue
165+
devices.append(cls.from_device(None, device_ccid=connection))
166+
167+
return devices
168+
169+
170+
def _call_ccid(
171+
device: CardConnection,
172+
command: int,
173+
data: bytes = b"",
174+
response_len: Optional[int] = None,
175+
) -> bytes:
176+
raise NotImplementedError("TODO")
124177

125178

126179
def _device_path_to_str(path: Union[bytes, str]) -> str:

0 commit comments

Comments
 (0)