cryptnox-id-cli 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cryptnox_id_cli/__init__.py +22 -0
- cryptnox_id_cli/__main__.py +6 -0
- cryptnox_id_cli/applets/__init__.py +2 -0
- cryptnox_id_cli/applets/fido/__init__.py +7 -0
- cryptnox_id_cli/applets/fido/authdata.py +81 -0
- cryptnox_id_cli/applets/fido/constants.py +155 -0
- cryptnox_id_cli/applets/fido/ctap.py +430 -0
- cryptnox_id_cli/applets/fido/errors.py +90 -0
- cryptnox_id_cli/applets/fido/pinproto.py +166 -0
- cryptnox_id_cli/applets/genuine/__init__.py +12 -0
- cryptnox_id_cli/applets/genuine/constants.py +32 -0
- cryptnox_id_cli/applets/genuine/genuine.py +85 -0
- cryptnox_id_cli/applets/genuine/verify.py +150 -0
- cryptnox_id_cli/applets/mifare/__init__.py +10 -0
- cryptnox_id_cli/applets/mifare/desfire.py +353 -0
- cryptnox_id_cli/applets/mifare/ev2.py +421 -0
- cryptnox_id_cli/applets/piv/__init__.py +9 -0
- cryptnox_id_cli/applets/piv/admin.py +182 -0
- cryptnox_id_cli/applets/piv/apt.py +58 -0
- cryptnox_id_cli/applets/piv/constants.py +66 -0
- cryptnox_id_cli/applets/piv/keyimport.py +244 -0
- cryptnox_id_cli/applets/piv/objects.py +126 -0
- cryptnox_id_cli/applets/piv/perso.py +138 -0
- cryptnox_id_cli/applets/piv/piv.py +171 -0
- cryptnox_id_cli/applets/piv/preperso.py +174 -0
- cryptnox_id_cli/applets/piv/profiles.py +486 -0
- cryptnox_id_cli/applets/piv/slots.py +44 -0
- cryptnox_id_cli/cli/__init__.py +1 -0
- cryptnox_id_cli/cli/commands/__init__.py +1 -0
- cryptnox_id_cli/cli/commands/apdu.py +123 -0
- cryptnox_id_cli/cli/commands/doctor.py +144 -0
- cryptnox_id_cli/cli/commands/factory.py +312 -0
- cryptnox_id_cli/cli/commands/fido.py +729 -0
- cryptnox_id_cli/cli/commands/genuine.py +192 -0
- cryptnox_id_cli/cli/commands/info.py +94 -0
- cryptnox_id_cli/cli/commands/mifare.py +998 -0
- cryptnox_id_cli/cli/commands/piv.py +2558 -0
- cryptnox_id_cli/cli/commands/readers.py +64 -0
- cryptnox_id_cli/cli/commands/report.py +217 -0
- cryptnox_id_cli/cli/commands/shell.py +130 -0
- cryptnox_id_cli/cli/context.py +71 -0
- cryptnox_id_cli/cli/dryrun.py +181 -0
- cryptnox_id_cli/cli/main.py +117 -0
- cryptnox_id_cli/crypto/__init__.py +1 -0
- cryptnox_id_cli/crypto/attestation.py +169 -0
- cryptnox_id_cli/crypto/csr.py +135 -0
- cryptnox_id_cli/crypto/piv_objects.py +79 -0
- cryptnox_id_cli/crypto/x509util.py +37 -0
- cryptnox_id_cli/output/__init__.py +5 -0
- cryptnox_id_cli/output/render.py +106 -0
- cryptnox_id_cli/secrets/__init__.py +5 -0
- cryptnox_id_cli/secrets/redaction.py +141 -0
- cryptnox_id_cli/secrets/resolver.py +93 -0
- cryptnox_id_cli/state/__init__.py +19 -0
- cryptnox_id_cli/state/detector.py +273 -0
- cryptnox_id_cli/state/model.py +147 -0
- cryptnox_id_cli/transport/__init__.py +28 -0
- cryptnox_id_cli/transport/apdu.py +72 -0
- cryptnox_id_cli/transport/elevation.py +165 -0
- cryptnox_id_cli/transport/errors.py +185 -0
- cryptnox_id_cli/transport/pcsc.py +351 -0
- cryptnox_id_cli/transport/scp02.py +247 -0
- cryptnox_id_cli/transport/scp03.py +213 -0
- cryptnox_id_cli/trust/__init__.py +90 -0
- cryptnox_id_cli/trust/genuine/cryptnox-attestation-ca.pem +18 -0
- cryptnox_id_cli/trust/genuine/cryptnox-dlt-cards-ca.pem +17 -0
- cryptnox_id_cli/trust/genuine/cryptnox-genuineness-ca.pem +18 -0
- cryptnox_id_cli/trust/genuine/cryptnox-intermediate-ca-2.pem +19 -0
- cryptnox_id_cli/trust/genuine/cryptnox-intermediate-ca.pem +19 -0
- cryptnox_id_cli/trust/genuine/cryptnox-root-ca.pem +19 -0
- cryptnox_id_cli/util/__init__.py +1 -0
- cryptnox_id_cli/util/hexutil.py +36 -0
- cryptnox_id_cli/util/tlv.py +144 -0
- cryptnox_id_cli-1.0.2.dist-info/METADATA +227 -0
- cryptnox_id_cli-1.0.2.dist-info/RECORD +79 -0
- cryptnox_id_cli-1.0.2.dist-info/WHEEL +5 -0
- cryptnox_id_cli-1.0.2.dist-info/entry_points.txt +4 -0
- cryptnox_id_cli-1.0.2.dist-info/licenses/LICENSE +165 -0
- cryptnox_id_cli-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Read-only PIV applet operations over a :class:`CardSession`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from cryptnox_id_cli.applets.piv import constants as c
|
|
8
|
+
from cryptnox_id_cli.applets.piv import objects as obj
|
|
9
|
+
from cryptnox_id_cli.applets.piv.apt import APTInfo, parse_apt
|
|
10
|
+
from cryptnox_id_cli.transport.apdu import APDU, Response
|
|
11
|
+
from cryptnox_id_cli.transport.errors import AppletNotFoundError, StatusWordError
|
|
12
|
+
from cryptnox_id_cli.transport.pcsc import CardSession
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PinStatus:
|
|
17
|
+
ref: int
|
|
18
|
+
configured: bool
|
|
19
|
+
blocked: bool
|
|
20
|
+
verified: bool
|
|
21
|
+
retries: int | None # remaining tries (None if unknown / not configured)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, object]:
|
|
24
|
+
return {
|
|
25
|
+
"ref": f"{self.ref:02X}",
|
|
26
|
+
"configured": self.configured,
|
|
27
|
+
"blocked": self.blocked,
|
|
28
|
+
"verified": self.verified,
|
|
29
|
+
"retries_remaining": self.retries,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _pin_status_from_sw(ref: int, resp: Response) -> PinStatus:
|
|
34
|
+
sw = resp.sw
|
|
35
|
+
if sw == 0x9000: # already verified in this session
|
|
36
|
+
return PinStatus(ref, configured=True, blocked=False, verified=True, retries=None)
|
|
37
|
+
if resp.sw1 == 0x63 and (resp.sw2 & 0xF0) == 0xC0:
|
|
38
|
+
tries = resp.sw2 & 0x0F
|
|
39
|
+
return PinStatus(ref, configured=True, blocked=tries == 0, verified=False, retries=tries)
|
|
40
|
+
if sw == 0x6983: # blocked
|
|
41
|
+
return PinStatus(ref, configured=True, blocked=True, verified=False, retries=0)
|
|
42
|
+
if sw in (0x6A88, 0x6A80, 0x6A82): # reference not configured
|
|
43
|
+
return PinStatus(ref, configured=False, blocked=False, verified=False, retries=None)
|
|
44
|
+
# Unknown — report not-configured-ish but keep retries None.
|
|
45
|
+
return PinStatus(ref, configured=False, blocked=False, verified=False, retries=None)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _pad_pin(pin: bytes) -> bytes:
|
|
49
|
+
"""PIV PINs are 8 bytes, padded with 0xFF."""
|
|
50
|
+
if len(pin) > 8:
|
|
51
|
+
raise ValueError("PIN must be at most 8 bytes")
|
|
52
|
+
return bytes(pin) + b"\xff" * (8 - len(pin))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PivApplet:
|
|
56
|
+
"""Thin, read-only PIV client. Mutating commands arrive in later phases."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, session: CardSession) -> None:
|
|
59
|
+
self.session = session
|
|
60
|
+
self._apt: APTInfo | None = None
|
|
61
|
+
|
|
62
|
+
# -- selection ---------------------------------------------------------- #
|
|
63
|
+
def select(self) -> APTInfo:
|
|
64
|
+
resp = self.session.transmit(
|
|
65
|
+
APDU(0x00, c.INS_SELECT, 0x04, 0x00, data=c.PIV_AID, le=256), context="SELECT PIV"
|
|
66
|
+
)
|
|
67
|
+
# On the multi-applet card, the first case-4 ISO SELECT issued right after a
|
|
68
|
+
# DESFire native-command session is rejected with 6700 ("wrong length"); the
|
|
69
|
+
# identical SELECT sent as case-3 (no Le) is accepted and still returns the
|
|
70
|
+
# FCI. The composite StateDetector probes DESFire first, so without this the
|
|
71
|
+
# PIV applet is mis-reported as Unknown over a contactless reader. Retry once
|
|
72
|
+
# without Le. See docs/troubleshooting.md.
|
|
73
|
+
if resp.sw == 0x6700:
|
|
74
|
+
resp = self.session.transmit(
|
|
75
|
+
APDU(0x00, c.INS_SELECT, 0x04, 0x00, data=c.PIV_AID), context="SELECT PIV (no Le)"
|
|
76
|
+
)
|
|
77
|
+
if resp.sw == 0x6A82:
|
|
78
|
+
raise AppletNotFoundError("PIV applet not found on this card.")
|
|
79
|
+
if not resp.ok:
|
|
80
|
+
raise StatusWordError(resp.sw1, resp.sw2, context="SELECT PIV")
|
|
81
|
+
self._apt = parse_apt(resp.data)
|
|
82
|
+
return self._apt
|
|
83
|
+
|
|
84
|
+
def try_select(self) -> APTInfo | None:
|
|
85
|
+
try:
|
|
86
|
+
return self.select()
|
|
87
|
+
except AppletNotFoundError:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def apt(self) -> APTInfo | None:
|
|
92
|
+
return self._apt
|
|
93
|
+
|
|
94
|
+
# -- data objects ------------------------------------------------------- #
|
|
95
|
+
def get_data(self, oid: bytes) -> Response:
|
|
96
|
+
return self.session.transmit(obj.get_data_apdu(oid), context="GET DATA")
|
|
97
|
+
|
|
98
|
+
def read_object(self, oid: bytes) -> bytes | None:
|
|
99
|
+
"""Return the unwrapped object content, or None if absent (6A82)."""
|
|
100
|
+
resp = self.get_data(oid)
|
|
101
|
+
if resp.sw == 0x6A82:
|
|
102
|
+
return None
|
|
103
|
+
if not resp.ok:
|
|
104
|
+
# Access-gated objects (e.g. PIN-protected) may return 6982 — surface as absent-ish.
|
|
105
|
+
if resp.sw in (0x6982, 0x6983):
|
|
106
|
+
return None
|
|
107
|
+
raise StatusWordError(resp.sw1, resp.sw2, context="GET DATA")
|
|
108
|
+
return obj.unwrap(oid, resp.data)
|
|
109
|
+
|
|
110
|
+
def object_present(self, oid: bytes) -> bool:
|
|
111
|
+
"""True if the object holds content (or exists but is access-gated).
|
|
112
|
+
|
|
113
|
+
Two distinct "absent" cases per SP 800-73 4.1.1 (same clause in -4 and
|
|
114
|
+
-5): a container never created at pre-personalization returns 6A82,
|
|
115
|
+
while a created-but-unused container returns a zero-length object
|
|
116
|
+
(9000). Both count as absent for every caller's purpose (status, slots,
|
|
117
|
+
validate, quickstart).
|
|
118
|
+
"""
|
|
119
|
+
resp = self.get_data(oid)
|
|
120
|
+
if resp.sw in (0x6982, 0x6983): # exists but access-gated still counts
|
|
121
|
+
return True
|
|
122
|
+
if not resp.ok:
|
|
123
|
+
return False
|
|
124
|
+
return bool(obj.unwrap(oid, resp.data))
|
|
125
|
+
|
|
126
|
+
# -- PIN / PUK ---------------------------------------------------------- #
|
|
127
|
+
def pin_status(self, ref: int = c.REF_PIV_PIN) -> PinStatus:
|
|
128
|
+
"""Non-decrementing status query (VERIFY with empty body)."""
|
|
129
|
+
resp = self.session.transmit(APDU(0x00, c.INS_VERIFY, 0x00, ref), context="VERIFY (status)")
|
|
130
|
+
return _pin_status_from_sw(ref, resp)
|
|
131
|
+
|
|
132
|
+
def verify_pin(self, pin: bytes, ref: int = c.REF_PIV_PIN) -> Response:
|
|
133
|
+
"""Verify a PIN. NOTE: a wrong PIN decrements the retry counter."""
|
|
134
|
+
self.session.redactor.register(pin)
|
|
135
|
+
padded = _pad_pin(pin)
|
|
136
|
+
self.session.redactor.register(padded)
|
|
137
|
+
return self.session.transmit(
|
|
138
|
+
APDU(0x00, c.INS_VERIFY, 0x00, ref, data=padded), context="VERIFY PIN"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def change_reference(self, old: bytes, new: bytes, ref: int = c.REF_PIV_PIN) -> Response:
|
|
142
|
+
"""Cardholder CHANGE REFERENCE DATA (INS 24, P1=00): swap a PIN or PUK value.
|
|
143
|
+
|
|
144
|
+
Body is the current value then the new value, each 0xFF-padded to 8 bytes.
|
|
145
|
+
A wrong ``old`` decrements the ref's retry counter (same as VERIFY). No admin
|
|
146
|
+
channel: the cardholder proves knowledge of the current value in-band.
|
|
147
|
+
"""
|
|
148
|
+
for secret in (old, new):
|
|
149
|
+
self.session.redactor.register(secret)
|
|
150
|
+
body = _pad_pin(old) + _pad_pin(new)
|
|
151
|
+
self.session.redactor.register(body)
|
|
152
|
+
return self.session.transmit(
|
|
153
|
+
APDU(0x00, c.INS_CHANGE_REFERENCE_DATA, 0x00, ref, data=body),
|
|
154
|
+
context="CHANGE REFERENCE DATA",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def unblock_pin(self, puk: bytes, new_pin: bytes, ref: int = c.REF_PIV_PIN) -> Response:
|
|
158
|
+
"""RESET RETRY COUNTER (INS 2C): unblock the PIN by proving the PUK.
|
|
159
|
+
|
|
160
|
+
Body is the PUK then the new PIN, each 0xFF-padded to 8 bytes. Resets the
|
|
161
|
+
PIN retry counter to its configured maximum. A wrong PUK decrements the PUK's
|
|
162
|
+
own counter; if the PUK blocks too, the PIN is unrecoverable.
|
|
163
|
+
"""
|
|
164
|
+
for secret in (puk, new_pin):
|
|
165
|
+
self.session.redactor.register(secret)
|
|
166
|
+
body = _pad_pin(puk) + _pad_pin(new_pin)
|
|
167
|
+
self.session.redactor.register(body)
|
|
168
|
+
return self.session.transmit(
|
|
169
|
+
APDU(0x00, c.INS_RESET_RETRY, 0x00, ref, data=body),
|
|
170
|
+
context="RESET RETRY COUNTER",
|
|
171
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""OpenFIPS201 PUT DATA ADMIN grammar (pre-personalization / factory).
|
|
2
|
+
|
|
3
|
+
Mirrors the applet's own perso toolkit (``tools/perso/piv_admin.py``) byte-for-byte.
|
|
4
|
+
Builders emit only the CDATA; the SCP03 layer wraps it into the secured APDU
|
|
5
|
+
``84 DB 3F 00 <Lc> <payload> <C-MAC>``.
|
|
6
|
+
|
|
7
|
+
Create-object parse order enforced by the applet (sequential TLVReader):
|
|
8
|
+
common prefix: 8B object-id, 8C mode-contact, 8D mode-contactless, [91 admin-key]
|
|
9
|
+
CONTAINER (0x64): nothing further
|
|
10
|
+
VERIFIER (0x65): 8E min-len, 8F max-len, 90 retries-contact, 91 retries-contactless,
|
|
11
|
+
[92 charset] [93 history] [94 sequence] [95 repeat] [96 restrict-update]
|
|
12
|
+
KEY (0x66): 8E mechanism, 8F role, 90 attributes
|
|
13
|
+
The order is significant — do not reorder elements.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
20
|
+
from cryptnox_id_cli.transport.apdu import APDU
|
|
21
|
+
from cryptnox_id_cli.util.tlv import build, build_constructed, concat
|
|
22
|
+
|
|
23
|
+
# PUT DATA ADMIN command header.
|
|
24
|
+
INS_PUT_DATA = 0xDB
|
|
25
|
+
PUT_DATA_P1 = 0x3F
|
|
26
|
+
PUT_DATA_P2_ADMIN = 0x00
|
|
27
|
+
|
|
28
|
+
# Operation tags.
|
|
29
|
+
OP_CREATE_CONTAINER = 0x64
|
|
30
|
+
OP_CREATE_VERIFIER = 0x65
|
|
31
|
+
OP_CREATE_KEY = 0x66
|
|
32
|
+
OP_UPDATE_CONFIG = 0x68
|
|
33
|
+
OP_SECURE_APPLET = 0x5F
|
|
34
|
+
OP_BULK_REQUEST = 0x6A
|
|
35
|
+
|
|
36
|
+
# Common element tags.
|
|
37
|
+
TAG_OBJECT_ID = 0x8B
|
|
38
|
+
TAG_MODE_CONTACT = 0x8C
|
|
39
|
+
TAG_MODE_CONTACTLESS = 0x8D
|
|
40
|
+
TAG_ADMIN_KEY = 0x91 # optional, container/key only (NOT verifier)
|
|
41
|
+
|
|
42
|
+
# Key element tags.
|
|
43
|
+
TAG_KEY_MECHANISM = 0x8E
|
|
44
|
+
TAG_KEY_ROLE = 0x8F
|
|
45
|
+
TAG_KEY_ATTRIBUTE = 0x90
|
|
46
|
+
|
|
47
|
+
# Verifier element tags.
|
|
48
|
+
TAG_PIN_MIN_LENGTH = 0x8E
|
|
49
|
+
TAG_PIN_MAX_LENGTH = 0x8F
|
|
50
|
+
TAG_PIN_RETRIES_CONTACT = 0x90
|
|
51
|
+
TAG_PIN_RETRIES_CONTACTLESS = 0x91
|
|
52
|
+
TAG_PIN_RULE_CHARSET = 0x92
|
|
53
|
+
TAG_PIN_RULE_HISTORY = 0x93
|
|
54
|
+
TAG_PIN_RULE_SEQUENCE = 0x94
|
|
55
|
+
TAG_PIN_RULE_REPEAT = 0x95
|
|
56
|
+
TAG_PIN_RESTRICT_UPDATE = 0x96
|
|
57
|
+
|
|
58
|
+
# Access-mode bytes. A mode is a bitmap EXCEPT ALWAYS which is the special 0x3F.
|
|
59
|
+
MODE_NEVER = 0x00
|
|
60
|
+
MODE_PIN = 0x01
|
|
61
|
+
MODE_OCC = 0x04
|
|
62
|
+
MODE_SM = 0x40
|
|
63
|
+
MODE_USER_ADMIN = 0x80
|
|
64
|
+
MODE_ALWAYS = 0x3F
|
|
65
|
+
MODE_VCI = MODE_SM # 0x40 (contactless read gated to a PIV-SM session)
|
|
66
|
+
MODE_VCI_PIN = MODE_SM | MODE_PIN # 0x41
|
|
67
|
+
|
|
68
|
+
# Key roles and attributes (bitmaps).
|
|
69
|
+
ROLE_AUTHENTICATE = 0x01
|
|
70
|
+
ROLE_KEY_ESTABLISH = 0x02
|
|
71
|
+
ROLE_SIGN = 0x04
|
|
72
|
+
|
|
73
|
+
ATTR_PERMIT_EXTERNAL = 0x04
|
|
74
|
+
ATTR_PERMIT_MUTUAL = 0x08
|
|
75
|
+
ATTR_IMPORTABLE = 0x10
|
|
76
|
+
ATTR_RSA_CRT = 0x20
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _object_id_bytes(object_id: int | bytes | Sequence[int]) -> bytes:
|
|
80
|
+
if isinstance(object_id, int):
|
|
81
|
+
return bytes([object_id])
|
|
82
|
+
return bytes(object_id)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_container(
|
|
86
|
+
object_id: int | bytes,
|
|
87
|
+
mode_contact: int,
|
|
88
|
+
mode_contactless: int,
|
|
89
|
+
admin_key: int | None = None,
|
|
90
|
+
) -> bytes:
|
|
91
|
+
parts = [
|
|
92
|
+
build(TAG_OBJECT_ID, _object_id_bytes(object_id)),
|
|
93
|
+
build(TAG_MODE_CONTACT, bytes([mode_contact])),
|
|
94
|
+
build(TAG_MODE_CONTACTLESS, bytes([mode_contactless])),
|
|
95
|
+
]
|
|
96
|
+
if admin_key is not None:
|
|
97
|
+
parts.append(build(TAG_ADMIN_KEY, bytes([admin_key])))
|
|
98
|
+
return build_constructed(OP_CREATE_CONTAINER, *parts)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_verifier(
|
|
102
|
+
verifier_id: int,
|
|
103
|
+
mode_contact: int,
|
|
104
|
+
mode_contactless: int,
|
|
105
|
+
min_length: int,
|
|
106
|
+
max_length: int,
|
|
107
|
+
retries_contact: int,
|
|
108
|
+
retries_contactless: int,
|
|
109
|
+
charset: int | None = None,
|
|
110
|
+
history: int | None = None,
|
|
111
|
+
sequence: int | None = None,
|
|
112
|
+
repeat: int | None = None,
|
|
113
|
+
restrict_update: int | None = None,
|
|
114
|
+
) -> bytes:
|
|
115
|
+
parts = [
|
|
116
|
+
build(TAG_OBJECT_ID, bytes([verifier_id])),
|
|
117
|
+
build(TAG_MODE_CONTACT, bytes([mode_contact])),
|
|
118
|
+
build(TAG_MODE_CONTACTLESS, bytes([mode_contactless])),
|
|
119
|
+
build(TAG_PIN_MIN_LENGTH, bytes([min_length])),
|
|
120
|
+
build(TAG_PIN_MAX_LENGTH, bytes([max_length])),
|
|
121
|
+
build(TAG_PIN_RETRIES_CONTACT, bytes([retries_contact])),
|
|
122
|
+
build(TAG_PIN_RETRIES_CONTACTLESS, bytes([retries_contactless])),
|
|
123
|
+
]
|
|
124
|
+
for tag, val in (
|
|
125
|
+
(TAG_PIN_RULE_CHARSET, charset),
|
|
126
|
+
(TAG_PIN_RULE_HISTORY, history),
|
|
127
|
+
(TAG_PIN_RULE_SEQUENCE, sequence),
|
|
128
|
+
(TAG_PIN_RULE_REPEAT, repeat),
|
|
129
|
+
(TAG_PIN_RESTRICT_UPDATE, restrict_update),
|
|
130
|
+
):
|
|
131
|
+
if val is not None:
|
|
132
|
+
parts.append(build(tag, bytes([val])))
|
|
133
|
+
return build_constructed(OP_CREATE_VERIFIER, *parts)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def create_key(
|
|
137
|
+
key_id: int,
|
|
138
|
+
mode_contact: int,
|
|
139
|
+
mode_contactless: int,
|
|
140
|
+
mechanism: int,
|
|
141
|
+
role: int,
|
|
142
|
+
attributes: int,
|
|
143
|
+
admin_key: int | None = None,
|
|
144
|
+
) -> bytes:
|
|
145
|
+
parts = [
|
|
146
|
+
build(TAG_OBJECT_ID, bytes([key_id])),
|
|
147
|
+
build(TAG_MODE_CONTACT, bytes([mode_contact])),
|
|
148
|
+
build(TAG_MODE_CONTACTLESS, bytes([mode_contactless])),
|
|
149
|
+
]
|
|
150
|
+
if admin_key is not None:
|
|
151
|
+
parts.append(build(TAG_ADMIN_KEY, bytes([admin_key])))
|
|
152
|
+
parts.extend(
|
|
153
|
+
[
|
|
154
|
+
build(TAG_KEY_MECHANISM, bytes([mechanism])),
|
|
155
|
+
build(TAG_KEY_ROLE, bytes([role])),
|
|
156
|
+
build(TAG_KEY_ATTRIBUTE, bytes([attributes])),
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
return build_constructed(OP_CREATE_KEY, *parts)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def secure_applet() -> bytes:
|
|
163
|
+
"""SECURE APPLET (0x5F) — primitive, empty body. IRREVERSIBLE (locks the applet)."""
|
|
164
|
+
return build(OP_SECURE_APPLET, b"")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_bulk(ops: Sequence[bytes]) -> bytes:
|
|
168
|
+
"""Wrap operation TLVs into a BULK request (0x6A)."""
|
|
169
|
+
return build_constructed(OP_BULK_REQUEST, concat(*ops))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def put_data_admin_apdu(payload: bytes) -> APDU:
|
|
173
|
+
"""The (unsecured) PUT DATA ADMIN command; the SCP03 layer secures/wraps it."""
|
|
174
|
+
return APDU(0x00, INS_PUT_DATA, PUT_DATA_P1, PUT_DATA_P2_ADMIN, data=payload)
|