diff --git a/pynitrokey/cli/nk3/piv.py b/pynitrokey/cli/nk3/piv.py index 62152025..c5422d30 100644 --- a/pynitrokey/cli/nk3/piv.py +++ b/pynitrokey/cli/nk3/piv.py @@ -7,7 +7,6 @@ from asn1crypto import x509 from asn1crypto.csr import CertificationRequest, CertificationRequestInfo from asn1crypto.keys import PublicKeyInfo -from ber_tlv.tlv import Tlv from click_aliases import ClickAliasedGroup from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa @@ -16,6 +15,7 @@ from pynitrokey.cli.nk3 import nk3 from pynitrokey.helpers import local_critical, local_print from pynitrokey.nk3.piv_app import PivApp, find_by_id +from pynitrokey.tlv import Tlv @nk3.group(cls=ClickAliasedGroup) @@ -318,14 +318,19 @@ def generate_key( else: local_critical("Unimplemented algorithm", support_hint=False) - body = Tlv.build({0xAC: {0x80: algo_id}}) + body = Tlv.build([(0xAC, Tlv.build([(0x80, algo_id)]))]) ins = 0x47 p1 = 0 p2 = key_ref response = device.send_receive(ins, p1, p2, body) - data = Tlv.parse(response, recursive=False) - data = Tlv.parse(find_by_id(0x7F49, data), recursive=False) + data = Tlv.parse(response) + data_tmp = find_by_id(0x7F49, data) + if data_tmp is None: + local_critical("Device did not send public key data") + return + + data = Tlv.parse(data_tmp) if algo == "nistp256": key_data = find_by_id(0x86, data) @@ -512,10 +517,10 @@ def generate_key( } ).dump() payload = Tlv.build( - { - 0x5C: bytes(bytearray.fromhex(KEY_TO_CERT_OBJ_ID_MAP[key_hex])), - 0x53: Tlv.build({0x70: certificate, 0x71: bytes([0])}), - } + [ + (0x5C, bytes(bytearray.fromhex(KEY_TO_CERT_OBJ_ID_MAP[key_hex]))), + (0x53, Tlv.build([(0x70, certificate), (0x71, bytes([0]))])), + ] ) device.send_receive(0xDB, 0x3F, 0xFF, payload) @@ -587,10 +592,10 @@ def write_certificate(admin_key: str, format: str, key: str, path: str) -> None: cert_serialized = cert.public_bytes(Encoding.DER) payload = Tlv.build( - { - 0x5C: bytes(bytearray.fromhex(KEY_TO_CERT_OBJ_ID_MAP[key])), - 0x53: Tlv.build({0x70: cert_serialized, 0x71: bytes([0])}), - } + [ + (0x5C, bytes(bytearray.fromhex(KEY_TO_CERT_OBJ_ID_MAP[key]))), + (0x53, Tlv.build([(0x70, cert_serialized), (0x71, bytes([0]))])), + ] ) device.send_receive(0xDB, 0x3F, 0xFF, payload) diff --git a/pynitrokey/nk3/piv_app.py b/pynitrokey/nk3/piv_app.py index 30b1177d..52fccd95 100644 --- a/pynitrokey/nk3/piv_app.py +++ b/pynitrokey/nk3/piv_app.py @@ -2,7 +2,6 @@ import os from typing import Any, Callable, Optional, Sequence, Union -from ber_tlv.tlv import Tlv from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from smartcard.CardRequest import CardRequest @@ -11,6 +10,7 @@ from pynitrokey.helpers import local_critical from pynitrokey.start.gnuk_token import iso7816_compose +from pynitrokey.tlv import Tlv LogFn = Callable[[str], Any] @@ -202,14 +202,16 @@ def authenticate_admin(self, admin_key: bytes) -> None: support_hint=False, ) - challenge_body = Tlv.build({0x7C: {0x80: b""}}) + challenge_body = Tlv.build([(0x7C, Tlv.build([(0x80, b"")]))]) challenge_response = self.send_receive(0x87, algo_byte, 0x9B, challenge_body) + general_auth_data = find_by_id(0x7C, Tlv.parse(challenge_response)) + if general_auth_data is None: + local_critical("Failed to get response to GENERAL AUTHENTICATE") + return + challenge = find_by_id( 0x80, - Tlv.parse( - find_by_id(0x7C, Tlv.parse(challenge_response, recursive=False)), - recursive=False, - ), + Tlv.parse(general_auth_data), ) if challenge is None: @@ -227,15 +229,19 @@ def authenticate_admin(self, admin_key: bytes) -> None: response = encryptor.update(challenge) + encryptor.finalize() our_challenge_encrypted = decryptor.update(our_challenge) + decryptor.finalize() response_body = Tlv.build( - {0x7C: {0x80: response, 0x81: our_challenge_encrypted}} + [(0x7C, Tlv.build([(0x80, response), (0x81, our_challenge_encrypted)]))] ) final_response = self.send_receive(0x87, algo_byte, 0x9B, response_body) + general_auth_data = find_by_id(0x7C, Tlv.parse(final_response)) + if general_auth_data is None: + local_critical("Failed to get response to GENERAL AUTHENTICATE") + return + decoded_challenge = find_by_id( 0x82, Tlv.parse( - find_by_id(0x7C, Tlv.parse(final_response, recursive=False)), - recursive=False, + general_auth_data ), ) @@ -310,14 +316,16 @@ def sign_rsa2048(self, data: bytes, key: int) -> bytes: return self.raw_sign(payload, key, 0x07) def raw_sign(self, payload: bytes, key: int, algo: int) -> bytes: - body = Tlv.build({0x7C: {0x81: payload, 0x82: b""}}) + body = Tlv.build([(0x7C, Tlv.build([(0x81, payload), (0x82, b"")]))]) result = self.send_receive(0x87, algo, key, body) + general_auth_data = find_by_id(0x7C, Tlv.parse(result)) + if general_auth_data is None: + local_critical("Failed to get response to GENERAL AUTHENTICATE") + return bytes() signature = find_by_id( 0x82, - Tlv.parse( - find_by_id(0x7C, Tlv.parse(result, recursive=False)), recursive=False - ), + Tlv.parse(general_auth_data), ) if signature is None: @@ -337,23 +345,26 @@ def init(self) -> bytes: card_id = os.urandom(16) cardcaps = template_begin + card_id + template_end cardcaps_body = Tlv.build( - {0x5C: bytes(bytearray.fromhex("5fc107")), 0x53: bytes(cardcaps)} + [(0x5C, bytes(bytearray.fromhex("5fc107"))), (0x53, bytes(cardcaps))] ) self.send_receive(0xDB, 0x3F, 0xFF, cardcaps_body) pinfo_body = Tlv.build( - { - 0x5C: bytes(bytearray.fromhex("5FC109")), - 0x53: Tlv.build( - { - 0x01: "Nitrokey PIV user".encode("ascii"), - # TODO: use representation of real serial number of card (currently static value) - # Base 10 representation of - # https://github.com/Nitrokey/piv-authenticator/blob/2c948a966f3e410e9a4cee3c351ca20b956383e0/src/lib.rs#L197 - 0x05: "5437251".encode("ascii"), - } + [ + (0x5C, bytes(bytearray.fromhex("5FC109"))), + ( + 0x53, + Tlv.build( + [ + (0x01, "Nitrokey PIV user".encode("ascii")), + # TODO: use representation of real serial number of card (currently static value) + # Base 10 representation of + # https://github.com/Nitrokey/piv-authenticator/blob/2c948a966f3e410e9a4cee3c351ca20b956383e0/src/lib.rs#L197 + (0x05, "5437251".encode("ascii")), + ] + ), ), - } + ] ) self.send_receive(0xDB, 0x3F, 0xFF, pinfo_body) return card_id @@ -366,10 +377,15 @@ def reader(self) -> str: return self.cardservice.connection.getReader() def guid(self) -> bytes: - payload = Tlv.build({0x5C: bytes(bytearray.fromhex("5FC102"))}) + payload = Tlv.build([(0x5C, bytes(bytearray.fromhex("5FC102")))]) chuid = self.send_receive(0xCB, 0x3F, 0xFF, payload) - chuid_data = find_by_id(0x34, Tlv.parse(find_by_id(0x53, Tlv.parse(chuid)))) + chuid_tmp = find_by_id(0x53, Tlv.parse(chuid)) + if chuid_tmp is None: + local_critical("Failed to get chuid from device") + return b"" + + chuid_data = find_by_id(0x34, Tlv.parse(chuid_tmp)) if chuid_data is None: local_critical("Failed to get chuid from device") # Satisfy the type checker. @@ -379,10 +395,10 @@ def guid(self) -> bytes: return chuid_data def cert(self, container_id: bytes) -> Optional[bytes]: - payload = Tlv.build({0x5C: container_id}) + payload = Tlv.build([(0x5C, container_id)]) try: cert = self.send_receive(0xCB, 0x3F, 0xFF, payload) - parsed = Tlv.parse(cert, False, False) + parsed = Tlv.parse(cert) if len(parsed) != 1: local_critical("Bad number of elements", support_hint=False) @@ -390,7 +406,7 @@ def cert(self, container_id: bytes) -> Optional[bytes]: if tag != 0x53: local_critical("Bad tag", support_hint=False) - parsed = Tlv.parse(value, False, False) + parsed = Tlv.parse(value) if len(parsed) < 1: local_critical("Bad number of sub-elements", support_hint=False) diff --git a/pynitrokey/tlv.py b/pynitrokey/tlv.py new file mode 100644 index 00000000..c33040a5 --- /dev/null +++ b/pynitrokey/tlv.py @@ -0,0 +1,81 @@ +from typing import Optional, Sequence, Tuple + + +def build_one(tag: int, data: bytes) -> bytes: + data_len = len(data) + out = bytearray() + out += tag.to_bytes((tag.bit_length() + 7) // 8, byteorder="big") + if data_len <= 0x7F: + out.append(data_len) + elif data_len <= 0xFF: + out.append(0x81) + out.append(data_len) + else: + assert data_len <= 0xFFFF + out.append(0x82) + out += data_len.to_bytes((data_len.bit_length() + 7) // 8, byteorder="big") + + return out + data + + +def take_tag(data: bytes) -> Tuple[int, bytes]: + data_len = len(data) + if data_len == 0: + raise ValueError("Failed to parse TLV data: empty data when parsing tag") + + b1 = data[0] + if (b1 & 0x1F) == 0x1F: + if data_len < 2: + raise ValueError("Failed to parse TLV data: partial tag") + b2 = data[1] + return (int.from_bytes([b1, b2], byteorder="big"), data[2:]) + else: + return (int.from_bytes([0, b1], byteorder="big"), data[1:]) + + +def take_len(data: bytes) -> Tuple[int, bytes]: + data_len = len(data) + if data_len == 0: + raise ValueError("Failed to parse TLV data: empty data when parsing len") + + l1 = data[0] + if l1 <= 0x7F: + return (l1, data[1:]) + elif l1 == 0x81: + if data_len < 2: + raise ValueError("Failed to parse TLV data: partial len") + return (data[1], data[2:]) + elif l1 == 0x82: + if data_len < 3: + raise ValueError("Failed to parse TLV data: partial len") + l2 = data[1] + l3 = data[2] + return (int.from_bytes([l2, l3], byteorder="big"), data[3:]) + else: + raise ValueError("Failed to parse TLV data: invalid len") + + +def take_do(data: bytes) -> Tuple[int, bytes, bytes]: + tag, rem = take_tag(data) + len, rem = take_len(rem) + + return tag, rem[:len], rem[len:] + + +class Tlv: + @staticmethod + def build(input: Sequence[Tuple[int, bytes]]) -> bytes: + out = bytearray() + for tag, data in input: + out += build_one(tag, data) + return out + + @staticmethod + def parse(data: bytes) -> Sequence[Tuple[int, bytes]]: + res = [] + current = data + while len(current) != 0: + tag, value, rem = take_do(current) + res.append((tag, value)) + current = rem + return res diff --git a/pyproject.toml b/pyproject.toml index 0525f1f3..aa88f5ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ dependencies = [ "nethsm >= 1.0.0,<2", "pyscard", "asn1crypto", - "ber-tlv" ] dynamic = ["version", "description"]