1
- # Copyright (c) 2019-2022 by Ron Frederick <ronf@timeheart.net> and others.
1
+ # Copyright (c) 2019-2024 by Ron Frederick <ronf@timeheart.net> and others.
2
2
#
3
3
# This program and the accompanying materials are made available under
4
4
# the terms of the Eclipse Public License v2.0 which accompanies this
20
20
21
21
"""U2F security key handler"""
22
22
23
+ from base64 import urlsafe_b64encode
24
+ import ctypes
23
25
from hashlib import sha256
24
26
import hmac
25
27
import time
@@ -54,6 +56,12 @@ def _decode_public_key(alg: int, public_key: Mapping[int, object]) -> bytes:
54
56
return b'\x04 ' + result + cast (bytes , public_key [- 3 ])
55
57
56
58
59
+ def _verify_rp_id (_rp_id : str , _origin : str ):
60
+ """Allow any relying party name -- SSH encodes the application here"""
61
+
62
+ return True
63
+
64
+
57
65
def _ctap1_poll (poll_interval : float , func : Callable [..., _PollResult ],
58
66
* args : object ) -> _PollResult :
59
67
"""Poll until a CTAP1 response is received"""
@@ -69,29 +77,28 @@ def _ctap1_poll(poll_interval: float, func: Callable[..., _PollResult],
69
77
70
78
71
79
def _ctap1_enroll (dev : 'CtapHidDevice' , alg : int ,
72
- application : bytes ) -> Tuple [bytes , bytes ]:
80
+ application : str ) -> Tuple [bytes , bytes ]:
73
81
"""Enroll a new security key using CTAP version 1"""
74
82
75
83
ctap1 = Ctap1 (dev )
76
84
77
85
if alg != SSH_SK_ECDSA :
78
86
raise ValueError ('Unsupported algorithm' )
79
87
80
- app_hash = sha256 (application ).digest ()
88
+ app_hash = sha256 (application . encode ( 'utf-8' ) ).digest ()
81
89
registration = _ctap1_poll (_CTAP1_POLL_INTERVAL , ctap1 .register ,
82
90
_dummy_hash , app_hash )
83
91
84
92
return registration .public_key , registration .key_handle
85
93
86
94
87
- def _ctap2_enroll (dev : 'CtapHidDevice' , alg : int , application : bytes ,
95
+ def _ctap2_enroll (dev : 'CtapHidDevice' , alg : int , application : str ,
88
96
user : str , pin : Optional [str ],
89
97
resident : bool ) -> Tuple [bytes , bytes ]:
90
98
"""Enroll a new security key using CTAP version 2"""
91
99
92
100
ctap2 = Ctap2 (dev )
93
101
94
- application = application .decode ('utf-8' )
95
102
rp = {'id' : application , 'name' : application }
96
103
user_cred = {'id' : user .encode ('utf-8' ), 'name' : user }
97
104
key_params = [{'type' : 'public-key' , 'alg' : alg }]
@@ -118,13 +125,31 @@ def _ctap2_enroll(dev: 'CtapHidDevice', alg: int, application: bytes,
118
125
return _decode_public_key (alg , cdata .public_key ), cdata .credential_id
119
126
120
127
121
- def _ctap1_sign (dev : 'CtapHidDevice' , message_hash : bytes , application : bytes ,
128
+ def _win_enroll (alg : int , application : str , user : str ) -> Tuple [bytes , bytes ]:
129
+ """Enroll a new security key using Windows WebAuthn API"""
130
+
131
+ client = WindowsClient (application , verify = _verify_rp_id )
132
+
133
+ rp = {'id' : application , 'name' : application }
134
+ user_cred = {'id' : user .encode ('utf-8' ), 'name' : user }
135
+ key_params = [{'type' : 'public-key' , 'alg' : alg }]
136
+ options = {'rp' : rp , 'user' : user_cred , 'challenge' : b'' ,
137
+ 'pubKeyCredParams' : key_params }
138
+
139
+ result = client .make_credential (options )
140
+ cdata = result .attestation_object .auth_data .credential_data
141
+
142
+ # pylint: disable=no-member
143
+ return _decode_public_key (alg , cdata .public_key ), cdata .credential_id
144
+
145
+
146
+ def _ctap1_sign (dev : 'CtapHidDevice' , message_hash : bytes , application : str ,
122
147
key_handle : bytes ) -> Tuple [int , int , bytes ]:
123
148
"""Sign a message with a security key using CTAP version 1"""
124
149
125
150
ctap1 = Ctap1 (dev )
126
151
127
- app_hash = sha256 (application ).digest ()
152
+ app_hash = sha256 (application . encode ( 'utf-8' ) ).digest ()
128
153
129
154
auth_response = _ctap1_poll (_CTAP1_POLL_INTERVAL , ctap1 .authenticate ,
130
155
message_hash , app_hash , key_handle )
@@ -137,13 +162,12 @@ def _ctap1_sign(dev: 'CtapHidDevice', message_hash: bytes, application: bytes,
137
162
138
163
139
164
def _ctap2_sign (dev : 'CtapHidDevice' , message_hash : bytes ,
140
- application : bytes , key_handle : bytes ,
165
+ application : str , key_handle : bytes ,
141
166
touch_required : bool ) -> Tuple [int , int , bytes ]:
142
167
"""Sign a message with a security key using CTAP version 2"""
143
168
144
169
ctap2 = Ctap2 (dev )
145
170
146
- application = application .decode ('utf-8' )
147
171
allow_creds = [{'type' : 'public-key' , 'id' : key_handle }]
148
172
options = {'up' : touch_required }
149
173
@@ -160,10 +184,38 @@ def _ctap2_sign(dev: 'CtapHidDevice', message_hash: bytes,
160
184
return auth_data .flags , auth_data .counter , assertion .signature
161
185
162
186
163
- def sk_enroll (alg : int , application : bytes , user : str ,
164
- pin : Optional [str ], resident : bool ) -> Tuple [bytes , bytes ]:
187
+ def _win_sign (data : bytes , application : str ,
188
+ key_handle : bytes ) -> Tuple [int , int , bytes , bytes ]:
189
+ """Sign a message with a security key using Windows WebAuthn API"""
190
+
191
+ client = WindowsClient (application , verify = _verify_rp_id )
192
+
193
+ creds = [{'type' : 'public-key' , 'id' : key_handle }]
194
+ options = {'challenge' : data , 'rpId' : application ,
195
+ 'allowCredentials' : creds }
196
+
197
+ result = client .get_assertion (options ).get_response (0 )
198
+ auth_data = result .authenticator_data
199
+
200
+ return auth_data .flags , auth_data .counter , \
201
+ result .signature , bytes (result .client_data )
202
+
203
+
204
+ def sk_webauthn_prefix (data : bytes , application : str ) -> bytes :
205
+ """Calculate a WebAuthn request prefix"""
206
+
207
+ return b'{"type":"webauthn.get","challenge":"' + \
208
+ urlsafe_b64encode (data ).rstrip (b'=' ) + b'","origin":"' + \
209
+ application .encode ('utf-8' ) + b'"'
210
+
211
+
212
+ def sk_enroll (alg : int , application : str , user : str , pin : Optional [str ],
213
+ resident : bool ) -> Tuple [bytes , bytes ]:
165
214
"""Enroll a new security key"""
166
215
216
+ if sk_use_webauthn :
217
+ return _win_enroll (alg , application , user )
218
+
167
219
try :
168
220
dev = next (CtapHidDevice .list_devices ())
169
221
except StopIteration :
@@ -187,22 +239,35 @@ def sk_enroll(alg: int, application: bytes, user: str,
187
239
dev .close ()
188
240
189
241
190
- def sk_sign (message_hash : bytes , application : bytes , key_handle : bytes ,
191
- flags : int ) -> Tuple [int , int , bytes ]:
242
+ def sk_sign (data : bytes , application : str , key_handle : bytes , flags : int ,
243
+ is_webauthn : bool = False ) -> Tuple [int , int , bytes , bytes ]:
192
244
"""Sign a message with a security key"""
193
245
194
246
touch_required = bool (flags & SSH_SK_USER_PRESENCE_REQD )
195
247
248
+ if is_webauthn and sk_use_webauthn :
249
+ return _win_sign (data , application , key_handle )
250
+
251
+ if is_webauthn :
252
+ data = sk_webauthn_prefix (data , application ) + b'}'
253
+
254
+ message_hash = sha256 (data ).digest ()
255
+
196
256
for dev in CtapHidDevice .list_devices ():
197
257
try :
198
- return _ctap2_sign (dev , message_hash , application ,
199
- key_handle , touch_required )
258
+ flags , counter , sig = _ctap2_sign (dev , message_hash , application ,
259
+ key_handle , touch_required )
260
+
261
+ return flags , counter , sig , data
200
262
except CtapError as exc :
201
263
if exc .code != CtapError .ERR .NO_CREDENTIALS :
202
264
raise ValueError (str (exc )) from None
203
265
except ValueError :
204
266
try :
205
- return _ctap1_sign (dev , message_hash , application , key_handle )
267
+ flags , counter , sig = _ctap1_sign (dev , message_hash ,
268
+ application , key_handle )
269
+
270
+ return flags , counter , sig , data
206
271
except ApduError as exc :
207
272
if exc .code != APDU .WRONG_DATA :
208
273
raise ValueError (str (exc )) from None
@@ -212,11 +277,11 @@ def sk_sign(message_hash: bytes, application: bytes, key_handle: bytes,
212
277
raise ValueError ('Security key credential not found' )
213
278
214
279
215
- def sk_get_resident (application : bytes , user : Optional [str ],
280
+ def sk_get_resident (application : str , user : Optional [str ],
216
281
pin : str ) -> Sequence [_SKResidentKey ]:
217
282
"""Get keys resident on a security key"""
218
283
219
- app_hash = sha256 (application ).digest ()
284
+ app_hash = sha256 (application . encode ( 'utf-8' ) ).digest ()
220
285
result : List [_SKResidentKey ] = []
221
286
222
287
for dev in CtapHidDevice .list_devices ():
@@ -262,13 +327,18 @@ def sk_get_resident(application: bytes, user: Optional[str],
262
327
263
328
264
329
try :
265
- from fido2 .hid import CtapHidDevice
330
+ from fido2 .client import WindowsClient
266
331
from fido2 .ctap import CtapError
267
332
from fido2 .ctap1 import Ctap1 , APDU , ApduError
268
333
from fido2 .ctap2 import Ctap2 , ClientPin , PinProtocolV1
269
334
from fido2 .ctap2 import CredentialManagement
335
+ from fido2 .hid import CtapHidDevice
270
336
271
337
sk_available = True
338
+
339
+ sk_use_webauthn = WindowsClient .is_available () and \
340
+ hasattr (ctypes , 'windll' ) and \
341
+ not ctypes .windll .shell32 .IsUserAnAdmin ()
272
342
except (ImportError , OSError , AttributeError ): # pragma: no cover
273
343
sk_available = False
274
344
0 commit comments