Skip to content

Commit de14492

Browse files
authored
Merge pull request #9 from dany74q/upgrade-to-fido-1.0.0
Upgrade to fido2 1.0.0, add support for Python 3.10 by migrating winrt -> winsdk
2 parents 2f41f00 + 7af8b76 commit de14492

19 files changed

+107
-88
lines changed

.github/workflows/CD-pre-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ jobs:
1717
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1818
steps:
1919
- uses: actions/checkout@v2
20-
- name: Set up Python 3.9
20+
- name: Set up Python 3.10
2121
uses: actions/setup-python@v2
2222
with:
23-
python-version: 3.9
23+
python-version: '3.10'
2424
- name: Install dependencies
2525
run: |
2626
python -m pip install --upgrade pip

.github/workflows/CD.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ jobs:
1717
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1818
steps:
1919
- uses: actions/checkout@v2
20-
- name: Set up Python 3.9
20+
- name: Set up Python 3.10
2121
uses: actions/setup-python@v2
2222
with:
23-
python-version: 3.9
23+
python-version: '3.10'
2424
- name: Install dependencies
2525
run: |
2626
python -m pip install --upgrade pip

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
max-parallel: 12
1616
matrix:
1717
os: [macos-latest, windows-latest, ubuntu-latest]
18-
python-version: [3.6, 3.7, 3.8, 3.9]
18+
python-version: ['3.7', '3.8', '3.9', '3.10']
1919
steps:
2020
- uses: actions/checkout@v2
2121
- name: Set up Python ${{ matrix.python-version }}

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
default_language_version:
2-
python: python3.6
2+
python: python3.7
33

44
repos:
55
- repo: https://github.com/pre-commit/pre-commit-hooks
6-
rev: v3.2.0
6+
rev: v4.3.0
77
hooks:
88
- id: check-added-large-files
99
- id: check-ast
@@ -13,6 +13,6 @@ repos:
1313
- id: trailing-whitespace
1414
- id: fix-encoding-pragma
1515
- repo: https://github.com/psf/black
16-
rev: 20.8b1
16+
rev: 21.12b0
1717
hooks:
1818
- id: black

.readthedocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ sphinx:
44
configuration: docs/conf.py
55

66
python:
7-
version: 3.8
7+
version: '3.8'
88
install:
99
- requirements: docs/requirements.txt

CHANGES.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
2.0
2+
====
3+
This release contains breaking changes for dependents on fido2 <= v1.0.0:
4+
- Bumped min Python version from 3.6 to 3.7
5+
- Upgraded to fido2 v1.0.0, which had numerous breaking changes
6+
- Migrated from winrt to winsdk to support Python 3.10
7+
18
1.0
29
===
310

4-
Initial release based on Keyring 21.4.0.
11+
Initial release based on Keyring 21.4.0.

ctap_keyring_device/ctap_credential_maker.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
from typing import Type
2+
from typing import Type, Union
33
from uuid import uuid4, uuid5, NAMESPACE_OID
44

55
from fido2.cose import CoseKey
@@ -9,12 +9,12 @@
99

1010

1111
class CtapCredentialMaker:
12-
""" This class makes a new credential (key-pair) from the given COSE key type """
12+
"""This class makes a new credential (key-pair) from the given COSE key type"""
1313

1414
def __init__(self, cose_key_cls: Type[CoseKey]):
1515
self._cose_key_cls = cose_key_cls
1616

17-
def make_credential(self, user_id: str) -> Credential:
17+
def make_credential(self, user_id: Union[str, bytes]) -> Credential:
1818
"""
1919
Generates a new credential, with a 32-byte id consisting of:
2020
- uuid5(NAMESPACE_OID, user_id)
@@ -25,6 +25,9 @@ def make_credential(self, user_id: str) -> Credential:
2525
"""
2626
assert user_id
2727

28+
if isinstance(user_id, bytes):
29+
user_id = user_id.decode('utf-8')
30+
2831
private_key = CtapPrivateKeyWrapper.create(self._cose_key_cls)
2932
key_password = uuid4().bytes
3033
user_uuid = uuid5(NAMESPACE_OID, user_id).bytes

ctap_keyring_device/ctap_keyring_device.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from hashlib import sha256
55
from threading import Event
66
from types import FunctionType
7-
from typing import List, Optional
7+
from typing import List, Optional, Mapping, Any
88

99
import keyring
1010
from cryptography.hazmat.primitives import serialization
@@ -15,13 +15,11 @@
1515
from fido2 import webauthn
1616
from fido2.attestation import PackedAttestation
1717
from fido2.ctap import CtapError
18-
from fido2.ctap2 import (
18+
from fido2.ctap2 import AssertionResponse, AttestationResponse, Info
19+
from fido2.webauthn import (
20+
Aaguid,
1921
AttestedCredentialData,
2022
AuthenticatorData,
21-
AttestationObject,
22-
AssertionResponse,
23-
)
24-
from fido2.webauthn import (
2523
PublicKeyCredentialDescriptor,
2624
PublicKeyCredentialType,
2725
PublicKeyCredentialUserEntity,
@@ -71,27 +69,28 @@ class CtapKeyringDevice(ctap.CtapDevice):
7169

7270
SUPPORTED_CTAP_VERSIONS = ['FIDO_2_0']
7371
MAX_MSG_SIZE = 1 << 20
74-
AAGUID = b'pasten-ctap-1337'
72+
AAGUID = Aaguid(b'pasten-ctap-1337')
7573

7674
def __init__(self):
77-
self.capabilities = hid.CAPABILITY.CBOR
78-
7975
self._ctap2_cmd_to_handler = {
8076
ctap2.Ctap2.CMD.MAKE_CREDENTIAL: self.make_credential,
8177
ctap2.Ctap2.CMD.GET_ASSERTION: self.get_assertion,
8278
ctap2.Ctap2.CMD.GET_NEXT_ASSERTION: self.get_next_assertion,
8379
ctap2.Ctap2.CMD.GET_INFO: self.get_info,
8480
}
8581

86-
self._info = ctap2.Info.create(
87-
self.SUPPORTED_CTAP_VERSIONS,
82+
self._info = Info(
83+
versions=self.SUPPORTED_CTAP_VERSIONS,
84+
extensions=[],
8885
aaguid=self.AAGUID,
8986
options={
9087
CtapOptions.PLATFORM_DEVICE: True,
9188
CtapOptions.RESIDENT_KEY: True,
9289
CtapOptions.USER_PRESENCE: True,
9390
CtapOptions.USER_VERIFICATION: True,
91+
CtapOptions.CLIENT_PIN: True,
9492
},
93+
pin_uv_protocols=[ctap2.PinProtocolV2.VERSION],
9594
max_msg_size=self.MAX_MSG_SIZE,
9695
transports=[webauthn.AuthenticatorTransport.INTERNAL],
9796
algorithms=cose.CoseKey.supported_algorithms(),
@@ -100,6 +99,10 @@ def __init__(self):
10099
self._next_assertions_ctx: Optional[CtapGetNextAssertionContext] = None
101100
self._user_verifier = CtapUserVerifierFactory.create()
102101

102+
@property
103+
def capabilities(self) -> int:
104+
return hid.CAPABILITY.CBOR
105+
103106
@classmethod
104107
def list_devices(cls):
105108
if isinstance(keyring.get_keyring(), FailKeyring):
@@ -117,7 +120,7 @@ def call(
117120
# noinspection PyBroadException
118121
try:
119122
res = self._call(cmd, data, event, on_keepalive)
120-
return self._wrap_err_code(CtapError.ERR.SUCCESS) + res
123+
return self._wrap_err_code(CtapError.ERR.SUCCESS) + cbor.encode(res)
121124
except CtapError as e:
122125
return self._wrap_err_code(e.code)
123126
except Exception:
@@ -147,7 +150,7 @@ def _call(
147150

148151
# noinspection PyBroadException
149152
try:
150-
ctap2_req: dict = cbor.decode(data[1:])
153+
ctap2_req: Mapping[Any, Any] = cbor.decode(data[1:])
151154
except Exception:
152155
raise CtapError(CtapError.ERR.INVALID_CBOR)
153156

@@ -161,10 +164,10 @@ def _call(
161164
def _wrap_err_code(err: CtapError.ERR) -> bytes:
162165
return err.to_bytes(1, 'big')
163166

164-
def get_info(self) -> ctap2.Info:
167+
def get_info(self) -> Info:
165168
return self._info
166169

167-
def make_credential(self, make_credential_request: dict) -> AttestationObject:
170+
def make_credential(self, make_credential_request: dict) -> AttestationResponse:
168171
# noinspection PyBroadException
169172
request = CtapMakeCredentialRequest.create(make_credential_request)
170173
if (
@@ -189,10 +192,10 @@ def make_credential(self, make_credential_request: dict) -> AttestationObject:
189192
)
190193

191194
attestation_statement = {'alg': cred.algorithm, 'sig': signature}
192-
attestation_object = AttestationObject.create(
195+
attestation_response = AttestationResponse(
193196
PackedAttestation.FORMAT, authenticator_data, attestation_statement
194197
)
195-
return attestation_object
198+
return attestation_response
196199

197200
@classmethod
198201
def _create_credential(cls, request: CtapMakeCredentialRequest) -> Credential:
@@ -343,12 +346,18 @@ def _get_assertion(
343346
signature = self._generate_signature(
344347
authenticator_data, request.client_data_hash, cred.private_key
345348
)
346-
response = AssertionResponse.create(
347-
PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, cred.id),
349+
response = AssertionResponse(
350+
PublicKeyCredentialDescriptor(
351+
PublicKeyCredentialType.PUBLIC_KEY,
352+
cred.id,
353+
transports=[webauthn.AuthenticatorTransport.INTERNAL],
354+
).__dict__,
348355
authenticator_data,
349356
signature,
350-
user=PublicKeyCredentialUserEntity(cred.user_id, name=''),
351-
n_creds=len(ctx.creds),
357+
user=PublicKeyCredentialUserEntity(
358+
name='', id=cred.user_id.encode('utf-8'), display_name=''
359+
).__dict__,
360+
number_of_credentials=len(ctx.creds),
352361
)
353362
return response
354363

ctap_keyring_device/ctap_private_key_wrapper.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ class CtapPrivateKeyWrapper(metaclass=abc.ABCMeta):
1818

1919
@abc.abstractmethod
2020
def get_key(self):
21-
""" Returns the wrapped private key """
21+
"""Returns the wrapped private key"""
2222
raise NotImplementedError()
2323

2424
@abc.abstractmethod
2525
def sign(self, data: bytes) -> bytes:
26-
""" Signs the given data according to the algorithm """
26+
"""Signs the given data according to the algorithm"""
2727
raise NotImplementedError()
2828

2929
@classmethod
3030
@abc.abstractmethod
3131
def get_algorithm(cls) -> int:
32-
""" One of COSE defined algorithms, see https://www.iana.org/assignments/cose/cose.xhtml """
32+
"""One of COSE defined algorithms, see https://www.iana.org/assignments/cose/cose.xhtml"""
3333
raise NotImplementedError()
3434

3535
def get_public_key(self):
@@ -63,7 +63,7 @@ def create(
6363

6464

6565
class CtapEs256PrivateKeyWrapper(CtapPrivateKeyWrapper):
66-
""" ES256 (ECDSA w/ SHA-256) """
66+
"""ES256 (ECDSA w/ SHA-256)"""
6767

6868
def __init__(self, key: ec.EllipticCurvePrivateKey = None):
6969
self._key = key or ec.generate_private_key(curve=ec.SECP256R1())
@@ -90,7 +90,7 @@ def get_key(self):
9090

9191

9292
class CtapRs256KeyGeneratorSigner(CtapRsaPrivateKeyWrapper):
93-
""" RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256) """
93+
"""RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256)"""
9494

9595
def sign(self, data: bytes) -> bytes:
9696
return self._key.sign(data, padding.PKCS1v15(), hashes.SHA256())
@@ -101,7 +101,7 @@ def get_algorithm(cls) -> int:
101101

102102

103103
class CtapRs1PrivateKeyWrapper(CtapRsaPrivateKeyWrapper):
104-
""" RS1 (RSASSA-PKCS1-v1_5 w/ SHA-1) """
104+
"""RS1 (RSASSA-PKCS1-v1_5 w/ SHA-1)"""
105105

106106
def sign(self, data: bytes) -> bytes:
107107
return self._key.sign(data, padding.PKCS1v15(), hashes.SHA1())
@@ -112,7 +112,7 @@ def get_algorithm(cls) -> int:
112112

113113

114114
class CtapPs256PrivateKeyWrapper(CtapRsaPrivateKeyWrapper):
115-
""" PS256 (RSASSA-PSS w/ SHA-256) """
115+
"""PS256 (RSASSA-PSS w/ SHA-256)"""
116116

117117
def sign(self, data: bytes) -> bytes:
118118
return self._key.sign(
@@ -129,7 +129,7 @@ def get_algorithm(cls) -> int:
129129

130130

131131
class CtapEdDsaPrivateKeyWrapper(CtapPrivateKeyWrapper):
132-
""" EDDSA (Edwards-Curve DSA) """
132+
"""EDDSA (Edwards-Curve DSA)"""
133133

134134
def __init__(self, key: Ed25519PrivateKey = None):
135135
self._key = key or ed25519.Ed25519PrivateKey.generate()

ctap_keyring_device/ctap_strucs.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import base64
3+
from timeit import default_timer as timer
34
from typing import List
45

56
from cryptography.hazmat.primitives import serialization
@@ -14,8 +15,6 @@
1415

1516
from ctap_keyring_device.ctap_private_key_wrapper import CtapPrivateKeyWrapper
1617

17-
from timeit import default_timer as timer
18-
1918

2019
class CtapOptions:
2120
PLATFORM_DEVICE = 'plat'
@@ -26,7 +25,7 @@ class CtapOptions:
2625

2726

2827
class Credential:
29-
""" Represents a ctap-saved credential - with a credential id, private key, and a COSE algorithm. """
28+
"""Represents a ctap-saved credential - with a credential id, private key, and a COSE algorithm."""
3029

3130
def __init__(self, credential_id: bytes, private_key: CtapPrivateKeyWrapper):
3231
self.id = credential_id
@@ -72,7 +71,7 @@ def encoded(self) -> str:
7271

7372

7473
class CtapMakeCredentialRequest:
75-
""" Represents the cbor encoded MAKE_CREDENTIAL request """
74+
"""Represents the cbor encoded MAKE_CREDENTIAL request"""
7675

7776
CLIENT_DATA_HASH_KEY = 1
7877
RP_KEY = 2
@@ -119,16 +118,16 @@ def create(cls, make_credential_request: dict):
119118
# noinspection PyProtectedMember
120119
return CtapMakeCredentialRequest(
121120
client_data_hash=make_credential_request.get(cls.CLIENT_DATA_HASH_KEY),
122-
rp=PublicKeyCredentialRpEntity._wrap(
121+
rp=PublicKeyCredentialRpEntity.from_dict(
123122
make_credential_request.get(cls.RP_KEY)
124123
),
125-
user=PublicKeyCredentialUserEntity._wrap(
126-
make_credential_request.get(cls.USER_KEY)
124+
user=PublicKeyCredentialUserEntity.from_dict(
125+
make_credential_request.get(cls.USER_KEY),
127126
),
128-
public_key_credential_params=PublicKeyCredentialParameters._wrap_list(
127+
public_key_credential_params=PublicKeyCredentialParameters._deserialize_list(
129128
make_credential_request.get(cls.PUBLIC_KEY_CREDENTIAL_PARAMS_KEY)
130129
),
131-
exclude_list=PublicKeyCredentialDescriptor._wrap_list(
130+
exclude_list=PublicKeyCredentialDescriptor._deserialize_list(
132131
make_credential_request.get(cls.EXCLUDE_LIST_KEY)
133132
),
134133
extensions=make_credential_request.get(cls.EXTENSIONS_KEY),
@@ -139,7 +138,7 @@ def create(cls, make_credential_request: dict):
139138

140139

141140
class CtapGetAssertionRequest:
142-
""" Represents the cbor encoded GET_ASSERTION request """
141+
"""Represents the cbor encoded GET_ASSERTION request"""
143142

144143
RP_ID_KEY = 1
145144
CLIENT_DATA_HASH_KEY = 2
@@ -178,7 +177,7 @@ def create(cls, get_assertion_req: dict):
178177
return CtapGetAssertionRequest(
179178
rp_id=get_assertion_req.get(cls.RP_ID_KEY),
180179
client_data_hash=get_assertion_req.get(cls.CLIENT_DATA_HASH_KEY),
181-
allow_list=PublicKeyCredentialDescriptor._wrap_list(
180+
allow_list=PublicKeyCredentialDescriptor._deserialize_list(
182181
get_assertion_req.get(cls.ALLOW_LIST_KEY)
183182
),
184183
extensions=get_assertion_req.get(cls.EXTENSIONS_KEY),

0 commit comments

Comments
 (0)