Skip to content

Commit

Permalink
Remove dependency on ber-tlv
Browse files Browse the repository at this point in the history
It has only 3 github stars, and is mostly unused
  • Loading branch information
sosthene-nitrokey committed Feb 29, 2024
1 parent 7321509 commit d24302b
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 45 deletions.
29 changes: 17 additions & 12 deletions pynitrokey/cli/nk3/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
78 changes: 46 additions & 32 deletions pynitrokey/nk3/piv_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]

Expand Down Expand Up @@ -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:
Expand All @@ -227,16 +229,18 @@ 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,
),
Tlv.parse(general_auth_data),
)

if decoded_challenge != our_challenge:
Expand Down Expand Up @@ -310,14 +314,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:
Expand All @@ -337,23 +343,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
Expand All @@ -366,10 +375,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.
Expand All @@ -379,18 +393,18 @@ 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)

tag, value = parsed[0]
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)

Expand Down
81 changes: 81 additions & 0 deletions pynitrokey/tlv.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ dependencies = [
"nethsm >= 1.0.0,<2",
"pyscard",
"asn1crypto",
"ber-tlv"
]
dynamic = ["version", "description"]

Expand Down

0 comments on commit d24302b

Please sign in to comment.