Skip to content

Commit 24fa128

Browse files
committed
Implement minPinLength
1 parent 0f667ee commit 24fa128

File tree

7 files changed

+415
-62
lines changed

7 files changed

+415
-62
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ I suggest [reading the FAQ](docs/FAQ.md) and perhaps [the security model](docs/s
7575
| CTAP2.1 credential management | Implemented |
7676
| CTAP2.1 enterprise attestation | Implemented but always rejected |
7777
| CTAP2.1 authenticator config | Implemented |
78+
| CTAP2.1 minPinLength extension | Implemented, zero RPID storage capacity |
7879
| CTAP2.1 credBlob extension | Implemented, discoverable creds only |
7980
| CTAP2.1 authenticatorLargeBlobs extension | Not implemented |
8081
| CTAP2.1 largeBlobKey extension | Not implemented |

docs/FAQ.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ It will store:
122122
- a boolean set to true on the first credential from a given RP ID, used
123123
to save state when enumerating and counting on-device RPs
124124
- a two-bit credProtect level value
125+
- the length of any credBlob stored with the credential
125126
- a four-byte counter value tracking which credential was most recently created
126127
- how many distinct RPs have valid keys on the device, unencrypted
127128
- how many total RPs are on the device, unencrypted

python_tests/ctap/test_cred_management.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .ctap_test import CTAPTestCase
88

99

10-
class CTAPPINTestCase(CTAPTestCase):
10+
class CredManagementTestCase(CTAPTestCase):
1111

1212
cp: ClientPin
1313

@@ -55,7 +55,7 @@ def test_disable_alwaysUv_without_pin_rejected(self):
5555
with self.assertRaises(CtapError) as e:
5656
Config(self.ctap2).toggle_always_uv()
5757

58-
self.assertEqual(CtapError.ERR.MISSING_PARAMETER, e.exception.code)
58+
self.assertEqual(CtapError.ERR.PUAT_REQUIRED, e.exception.code)
5959

6060
def test_toggling_alwaysUv_survives_soft_reset(self):
6161
Config(self.ctap2).toggle_always_uv()

python_tests/ctap/test_ctap_basics.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ def test_info_aaguid_none(self):
2222
info = self.ctap2.get_info()
2323
self.assertEqual(Aaguid.NONE, info.aaguid)
2424

25+
def test_info_uv_modality_hint(self):
26+
info = self.ctap2.get_info()
27+
self.assertEqual(0x0200, info.uv_modality)
28+
29+
def test_info_supported_algs_hint(self):
30+
info = self.ctap2.get_info()
31+
self.assertEqual([{'alg': -7, "type": "public-key"}], info.algorithms)
32+
2533
def test_make_credential_self_attestation(self):
2634
rp_id = secrets.token_hex(50)
2735
self.basic_makecred_params['rp']['id'] = rp_id
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import secrets
2+
from typing import Optional
3+
4+
from fido2.ctap import CtapError
5+
from fido2.ctap2 import Config, ClientPin, PinProtocolV2
6+
from parameterized import parameterized
7+
8+
from .ctap_test import CTAPTestCase
9+
10+
11+
class SetMinPinTestCase(CTAPTestCase):
12+
13+
cp: ClientPin
14+
pin: Optional[str] = None
15+
16+
def setUp(self, install_params: Optional[bytes] = None) -> None:
17+
super().setUp(install_params=install_params)
18+
self.cp = ClientPin(self.ctap2)
19+
20+
def get_cfg(self) -> Config:
21+
if self.pin is None:
22+
return Config(self.ctap2)
23+
uv = self.cp.get_pin_token(self.pin,
24+
permissions=ClientPin.PERMISSION.AUTHENTICATOR_CFG)
25+
return Config(self.ctap2, pin_uv_protocol=PinProtocolV2(), pin_uv_token=uv)
26+
27+
def test_info_min_pin_length(self):
28+
info = self.ctap2.get_info()
29+
self.assertEqual(4, info.min_pin_length)
30+
31+
def test_info_max_rps_for_setminpin(self):
32+
info = self.ctap2.get_info()
33+
self.assertEqual(0, info.max_rpids_for_min_pin)
34+
35+
def test_setminpin_option(self):
36+
info = self.ctap2.get_info()
37+
self.assertEqual(True, info.options.get("setMinPINLength"))
38+
39+
@parameterized.expand([
40+
("authenticated", True),
41+
("unauthenticated", False),
42+
])
43+
def test_setminpin_visible_in_info(self, _, setpin: bool):
44+
if setpin:
45+
self.pin = secrets.token_hex(10)
46+
self.cp.set_pin(self.pin)
47+
48+
self.get_cfg().set_min_pin_length(min_pin_length=8)
49+
50+
info = self.ctap2.get_info()
51+
self.assertEqual(8, info.min_pin_length)
52+
53+
def test_setminpin_requires_pin_when_set(self):
54+
self.pin = secrets.token_hex(10)
55+
self.cp.set_pin(self.pin)
56+
57+
with self.assertRaises(CtapError) as e:
58+
Config(self.ctap2).set_min_pin_length(min_pin_length=2)
59+
60+
self.assertEqual(CtapError.ERR.PUAT_REQUIRED, e.exception.code)
61+
62+
@parameterized.expand([
63+
(4, 3),
64+
(8, 7),
65+
(30, 4),
66+
])
67+
def test_setminpin_cannot_go_down(self, original_value, new_value):
68+
self.get_cfg().set_min_pin_length(min_pin_length=original_value)
69+
70+
with self.assertRaises(CtapError) as e:
71+
self.get_cfg().set_min_pin_length(min_pin_length=new_value)
72+
73+
self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code)
74+
75+
def test_setminpin_overlong(self):
76+
with self.assertRaises(CtapError) as e:
77+
self.get_cfg().set_min_pin_length(min_pin_length=70)
78+
79+
self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code)
80+
81+
def test_four_ascii_chars(self):
82+
self.cp.set_pin("aaaa")
83+
84+
def test_four_ascii_chars_rejected_when_length_increased(self):
85+
self.get_cfg().set_min_pin_length(min_pin_length=5)
86+
87+
with self.assertRaises(CtapError) as e:
88+
self.cp.set_pin("aaaa")
89+
90+
self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code)
91+
92+
def test_four_multibyte_chars_rejected_when_length_increased(self):
93+
self.get_cfg().set_min_pin_length(min_pin_length=5)
94+
95+
with self.assertRaises(CtapError) as e:
96+
self.cp.set_pin("✈✈✈✈")
97+
98+
self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code)
99+
100+
def test_five_multibyte_chars_accepted_when_length_increased(self):
101+
self.get_cfg().set_min_pin_length(min_pin_length=5)
102+
103+
self.cp.set_pin("✈✈ä✈✈")
104+
105+
def test_four_multibyte_chars_accepted_normally(self):
106+
self.cp.set_pin("✈✈✈✈")
107+
108+
def test_change_without_pin_does_not_force_change(self):
109+
self.get_cfg().set_min_pin_length(min_pin_length=10)
110+
info = self.ctap2.get_info()
111+
112+
self.assertEqual(False, info.force_pin_change)
113+
114+
def test_change_with_pin_does_force_change(self):
115+
self.pin = secrets.token_hex(10)
116+
self.cp.set_pin(self.pin)
117+
118+
self.get_cfg().set_min_pin_length(min_pin_length=10)
119+
info = self.ctap2.get_info()
120+
121+
self.assertEqual(True, info.force_pin_change)
122+
123+
def test_change_with_pin_to_same_length_does_not_force_change(self):
124+
self.pin = secrets.token_hex(10)
125+
self.cp.set_pin(self.pin)
126+
127+
self.get_cfg().set_min_pin_length(min_pin_length=4)
128+
info = self.ctap2.get_info()
129+
130+
self.assertEqual(False, info.force_pin_change)
131+
132+
def test_cannot_get_uv_when_change_forced(self):
133+
self.pin = secrets.token_hex(10)
134+
self.cp.set_pin(self.pin)
135+
self.get_cfg().set_min_pin_length(force_change_pin=True)
136+
137+
with self.assertRaises(CtapError) as e:
138+
self.cp.get_pin_token(self.pin)
139+
140+
self.assertEqual(CtapError.ERR.PIN_POLICY_VIOLATION, e.exception.code)
141+
142+
def test_cannot_force_change_without_pin(self):
143+
with self.assertRaises(CtapError) as e:
144+
self.get_cfg().set_min_pin_length(force_change_pin=True)
145+
146+
self.assertEqual(CtapError.ERR.PIN_NOT_SET, e.exception.code)

src/main/java/us/q3q/fido2/CannedCBOR.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public abstract class CannedCBOR {
6262

6363
static final byte[] AUTH_INFO_SECOND = {
6464
0x04, // map key: options
65-
(byte) 0xA9, // map: nine entries
65+
(byte) 0xAA, // map: ten entries
6666
0x62, // string: two bytes long
6767
0x65, 0x70, // ep
6868
(byte) 0xF4, // false
@@ -90,6 +90,11 @@ public abstract class CannedCBOR {
9090
static final byte[] MAKE_CRED_UV_NOT_REQD = {
9191
0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71, 0x64
9292
};
93+
94+
static final byte[] SET_MIN_PIN_LENGTH = {
95+
0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, // setMinPINLength
96+
};
97+
9398
static final byte[] PIN_UV_AUTH_TOKEN = {
9499
0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65, 0x6E
95100
};
@@ -161,4 +166,16 @@ public abstract class CannedCBOR {
161166
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79
162167
// p u b l i c - k e y
163168
};
169+
170+
static final byte[] ES256_ALG_TYPE = {
171+
(byte) 0x81, // array - one item
172+
(byte) 0xA2, // map - two entries
173+
0x63, // string - three bytes long
174+
0x61, 0x6C, 0x67, // alg
175+
0x26, // -7 (alg ID for ES256)
176+
0x64, // string - four bytes long
177+
0x74, 0x79, 0x70, 0x65, // type
178+
0x6A, // string - ten bytes long
179+
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key
180+
};
164181
}

0 commit comments

Comments
 (0)