Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Clang format
e4b833168418ce435358d76b2e266d8e9104f915
# Ruff
31dda9512e63b3c3dafbdd17962e8fbca9c98f8a
f8d89a3c4b99d2760ef9eb363eb6143bc7a3b2a8
2 changes: 2 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
ignore_missing_imports = True
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests/ragger
pythonpath = tests/ragger
26 changes: 8 additions & 18 deletions pytools/backup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# *****************************************************************************
# Ledger App OpenPGP.
# (c) 2024 Ledger SAS.
Expand All @@ -18,9 +17,10 @@
# *****************************************************************************

import sys
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from pathlib import Path
from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
from gpgapp.gpgcard import GPGCard, PassWord, GPGCardExcpetion

from gpgapp.gpgcard import GPGCard, GPGCardExcpetion, PassWord


# ===============================================================================
Expand All @@ -42,23 +42,13 @@ def get_argparser() -> Namespace:
)

parser.add_argument("--apdu", action="store_true", help="Log APDU exchange")
parser.add_argument(
"--slot", type=int, choices=range(1, 4), help="Select slot (1 to 3)"
)
parser.add_argument("--slot", type=int, choices=range(1, 4), help="Select slot (1 to 3)")

parser.add_argument(
"--pinpad", action="store_true", help="PIN validation delegated to pinpad"
)
parser.add_argument(
"--adm-pin", metavar="PIN", help="Admin PIN (if pinpad not used)"
)
parser.add_argument(
"--user-pin", metavar="PIN", help="User PIN (if pinpad not used)"
)
parser.add_argument("--pinpad", action="store_true", help="PIN validation delegated to pinpad")
parser.add_argument("--adm-pin", metavar="PIN", help="Admin PIN (if pinpad not used)")
parser.add_argument("--user-pin", metavar="PIN", help="User PIN (if pinpad not used)")

parser.add_argument(
"--restore", action="store_true", help="Perform a Restore instead of Backup"
)
parser.add_argument("--restore", action="store_true", help="Perform a Restore instead of Backup")

parser.add_argument(
"--file",
Expand Down
135 changes: 44 additions & 91 deletions pytools/gpgapp/gpgcard.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# Ledger App OpenPGP.
# (c) 2024 Ledger SAS.
Expand All @@ -18,20 +17,27 @@

import base64
import binascii
import os
from datetime import datetime, timezone
import json
from hashlib import sha1
from typing import Optional, Tuple
import os
from dataclasses import dataclass, field
from datetime import UTC, datetime
from hashlib import sha1

# pylint: disable=import-error
from Crypto.PublicKey.RSA import construct
from ledgercomm import Transport # type: ignore
# pylint: enable=import-error

from gpgapp.gpgcmd import DataObject, ErrorCodes, KeyTypes, PassWord, PubkeyAlgo # type: ignore
from gpgapp.gpgcmd import KEY_OPERATIONS, KEY_TEMPLATES, USER_SALUTATION # type: ignore
# pylint: enable=import-error
from gpgapp.gpgcmd import ( # type: ignore # type: ignore
KEY_OPERATIONS,
KEY_TEMPLATES,
USER_SALUTATION,
DataObject,
ErrorCodes,
KeyTypes,
PassWord,
PubkeyAlgo,
)

APDU_MAX_SIZE: int = 0xFE
APDU_CHAINING_MODE: int = 0x10
Expand Down Expand Up @@ -157,9 +163,7 @@ def connect(self, device: str) -> None:
"""

if device == "speculos":
self.transport = Transport(
"tcp", server="127.0.0.1", port=9999, debug=False
)
self.transport = Transport("tcp", server="127.0.0.1", port=9999, debug=False)
else:
self.transport = Transport("hid")
print("")
Expand Down Expand Up @@ -201,12 +205,7 @@ def add_log(self, mode: str, data: bytes, sw: int = 0) -> None:
if len(data) > 1 and data[1] in _SENSITIVE_INS:
print(f"{mode}: [REDACTED SENSITIVE APDU ins={data[1]:02x}]")
return
self._last_sent_was_key_read = (
len(data) >= 4
and data[1] == 0xCA
and data[2] == 0x00
and data[3] in _KEY_READ_DOS
)
self._last_sent_was_key_read = len(data) >= 4 and data[1] == 0xCA and data[2] == 0x00 and data[3] in _KEY_READ_DOS
elif mode == "recv":
if self._last_sent_was_key_read:
self._last_sent_was_key_read = False
Expand Down Expand Up @@ -246,7 +245,7 @@ def get_all(self) -> None:
"""Retrieve all Data Object values from the Card"""

self.data.reset()
data: Optional[bytes] = b""
data: bytes | None = b""
b_data: bytes = b""
s_data: str = ""

Expand Down Expand Up @@ -391,7 +390,7 @@ def restore(self, file_name: str) -> None:
def _fromb64(s: str) -> bytes:
return base64.b64decode(s)

with open(file_name, mode="r", encoding="utf-8") as f:
with open(file_name, encoding="utf-8") as f:
payload = json.load(f)

self.data.AID = payload["AID"]
Expand Down Expand Up @@ -451,40 +450,32 @@ def _fromb64(s: str) -> bytes:
# The digital signature counter (DO 0x93) is read-only on the card (no
# write access path in the firmware): kept in the backup for reference
# but not written back here.
self._put_data(
DataObject.CMD_RSA_EXP, self.data.rsa_pub_exp.to_bytes(4, "little")
)
self._put_data(DataObject.CMD_RSA_EXP, self.data.rsa_pub_exp.to_bytes(4, "little"))

self._put_data(DataObject.DO_CERT, self.data.aut.cert.encode("utf-8"))
self._put_data(DataObject.DO_CERT, self.data.dec.cert.encode("utf-8"))
self._put_data(DataObject.DO_CERT, self.data.sig.cert.encode("utf-8"))

self._put_data(
DataObject.DO_CA_FINGERPRINT_WR_SIG, self.data.sig.ca_fingerprint
)
self._put_data(DataObject.DO_CA_FINGERPRINT_WR_SIG, self.data.sig.ca_fingerprint)
self._put_data(DataObject.DO_FINGERPRINT_WR_SIG, self.data.sig.fingerprint)
date = str(self.data.sig.date)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
bdate = int(dt.timestamp()).to_bytes(4, "big")
self._put_data(DataObject.DO_DATES_WR_SIG, bdate)
self._put_data(DataObject.DO_SIG_KEY, self.data.sig.key)

self._put_data(
DataObject.DO_CA_FINGERPRINT_WR_DEC, self.data.dec.ca_fingerprint
)
self._put_data(DataObject.DO_CA_FINGERPRINT_WR_DEC, self.data.dec.ca_fingerprint)
self._put_data(DataObject.DO_FINGERPRINT_WR_DEC, self.data.dec.fingerprint)
date = str(self.data.dec.date)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
bdate = int(dt.timestamp()).to_bytes(4, "big")
self._put_data(DataObject.DO_DATES_WR_DEC, bdate)
self._put_data(DataObject.DO_DEC_KEY, self.data.dec.key)

self._put_data(
DataObject.DO_CA_FINGERPRINT_WR_AUT, self.data.aut.ca_fingerprint
)
self._put_data(DataObject.DO_CA_FINGERPRINT_WR_AUT, self.data.aut.ca_fingerprint)
self._put_data(DataObject.DO_FINGERPRINT_WR_AUT, self.data.aut.fingerprint)
date = str(self.data.aut.date)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
bdate = int(dt.timestamp()).to_bytes(4, "big")
self._put_data(DataObject.DO_DATES_WR_AUT, bdate)
self._put_data(DataObject.DO_AUT_KEY, self.data.aut.key)
Expand All @@ -503,9 +494,7 @@ def export_pub_key(self, pubkey: dict, file_name: str) -> None:
if key_id.startswith("RSA"):
modulus = bytearray.fromhex(pubkey["Modulus"])
exponent = bytearray.fromhex(pubkey["Pub Exp"][2:])
key = construct(
(int.from_bytes(modulus, "big"), int.from_bytes(exponent, "big"))
)
key = construct((int.from_bytes(modulus, "big"), int.from_bytes(exponent, "big")))
public_key = key.publickey().export_key()
with open(file_name, mode="wb", encoding="utf-8") as f:
f.write(public_key)
Expand All @@ -528,9 +517,7 @@ def export_pub_key(self, pubkey: dict, file_name: str) -> None:
f.write(f"{key}: {value}\n")

else:
raise GPGCardExcpetion(
ErrorCodes.ERR_INTERNAL, f"Unsupported key type for export: {key_id}"
)
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Unsupported key type for export: {key_id}")

def seed_key(self) -> None:
"""Regenerate keys, based on seed mode"""
Expand All @@ -542,8 +529,7 @@ def seed_key(self) -> None:
):
_, sw = self._exchange(binascii.unhexlify(apdu_hex))
assert sw == ErrorCodes.ERR_SUCCESS, (
f"{name} key generation failed (sw={sw:#06x})"
" — check seed mode is ON and PIN is verified"
f"{name} key generation failed (sw={sw:#06x}) — check seed mode is ON and PIN is verified"
)

############### Information decoding ###############
Expand Down Expand Up @@ -796,9 +782,7 @@ def set_salutation(self, salutation: str) -> None:
try:
salutation_str = USER_SALUTATION[salutation].encode("utf-8")
except KeyError as err:
raise GPGCardExcpetion(
ErrorCodes.ERR_INTERNAL, f"Invalid salutation value ({salutation})!"
) from err
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid salutation value ({salutation})!") from err

self.data.salutation = salutation
self._put_data(DataObject.DO_CARD_SALUTATION, salutation_str)
Expand All @@ -825,9 +809,7 @@ def verify_pin(self, pw: PassWord, value: str, pinpad: bool = False) -> bool:
if pinpad:
apdu = bytes.fromhex(f"EF2000{pw:02x}00")
else:
apdu = bytes.fromhex(f"002000{pw:02x}{len(value):02x}") + value.encode(
"utf-8"
)
apdu = bytes.fromhex(f"002000{pw:02x}{len(value):02x}") + value.encode("utf-8")
_, sw = self._exchange(apdu)
return sw == ErrorCodes.ERR_SUCCESS

Expand All @@ -844,11 +826,7 @@ def change_pin(self, pw: PassWord, cur_value: str, new_value: str) -> bool:
"""

lc = len(cur_value) + len(new_value)
apdu = (
bytes.fromhex(f"002400{pw:02x}{lc:02x}")
+ cur_value.encode("utf-8")
+ new_value.encode("utf-8")
)
apdu = bytes.fromhex(f"002400{pw:02x}{lc:02x}") + cur_value.encode("utf-8") + new_value.encode("utf-8")
_, sw = self._exchange(apdu)
return sw == ErrorCodes.ERR_SUCCESS

Expand All @@ -863,9 +841,7 @@ def set_RC(self, value: str) -> bool:
"""

b_value = value.encode("utf-8")
return (
self._put_data(DataObject.DO_RESET_CODE, b_value) == ErrorCodes.ERR_SUCCESS
)
return self._put_data(DataObject.DO_RESET_CODE, b_value) == ErrorCodes.ERR_SUCCESS

def reset_PW1(self, RC: str, value: str) -> bool:
"""Reset the User Password with Resetting Code
Expand All @@ -880,11 +856,7 @@ def reset_PW1(self, RC: str, value: str) -> bool:

p1 = 2 if len(RC) == 0 else 0
lc = len(RC) + len(value)
apdu = (
bytes.fromhex(f"002C{p1:02x}81{lc:02x}")
+ RC.encode("utf-8")
+ value.encode("utf-8")
)
apdu = bytes.fromhex(f"002C{p1:02x}81{lc:02x}") + RC.encode("utf-8") + value.encode("utf-8")
_, sw = self._exchange(apdu)
return sw == ErrorCodes.ERR_SUCCESS

Expand Down Expand Up @@ -959,9 +931,7 @@ def set_template(self, key: str, template: str) -> None:
"""

if template not in KEY_TEMPLATES:
raise GPGCardExcpetion(
ErrorCodes.ERR_INTERNAL, f"Invalid template: {template}"
)
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid template: {template}")

data = binascii.unhexlify(KEY_TEMPLATES[template])
if key == KeyTypes.KEY_SIG:
Expand Down Expand Up @@ -1067,19 +1037,15 @@ def decode_key(self, key: str) -> dict:
offset: int = 0
key_data = self._get_key_object(key).key

d["OS Target ID"] = (
f"0x{int.from_bytes(key_data[offset : offset + 4], 'big'):04x}"
)
d["OS Target ID"] = f"0x{int.from_bytes(key_data[offset : offset + 4], 'big'):04x}"
offset += 4
d["API Level"] = str(int.from_bytes(key_data[offset : offset + 4], "big"))
offset += 4
size = int.from_bytes(key_data[offset : offset + 4], "big")
# Should be Public key here from doc, but only Public Exp from the code
d["Public exp size"] = str(size)
offset += 4
d["Public exp"] = (
f"0x{int.from_bytes(key_data[offset : offset + 4], 'big'):06x}"
)
d["Public exp"] = f"0x{int.from_bytes(key_data[offset : offset + 4], 'big'):06x}"
offset += size
size = int.from_bytes(key_data[offset : offset + 4], "big")
d["Private key size"] = str(size)
Expand Down Expand Up @@ -1113,9 +1079,7 @@ def asymmetric_key(self, key: str, action: str, seed: bool = False) -> dict:
"""

if action not in KEY_OPERATIONS:
raise GPGCardExcpetion(
ErrorCodes.ERR_INTERNAL, f"Invalid Key operation: {action}"
)
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid Key operation: {action}")

op = KEY_OPERATIONS[action]
attributes = None
Expand All @@ -1133,9 +1097,7 @@ def asymmetric_key(self, key: str, action: str, seed: bool = False) -> dict:
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Invalid key attribute!")

if attributes[0] not in set(iter(PubkeyAlgo)):
raise GPGCardExcpetion(
ErrorCodes.ERR_INTERNAL, "Invalid key ID in attribute!"
)
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Invalid key ID in attribute!")

d = {}
tags = self._asym_key_pair(op, b_key, seed)
Expand All @@ -1160,9 +1122,7 @@ def asymmetric_key(self, key: str, action: str, seed: bool = False) -> dict:

# Get the generation date
date = self.get_key_date(key)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(
tzinfo=timezone.utc
)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
kdate = int(dt.timestamp())
d["Creation date"] = date

Expand Down Expand Up @@ -1276,7 +1236,7 @@ def _set_key_date_now(self, key: str) -> None:

dt = datetime.utcnow().replace(microsecond=0)
bdate = int(dt.timestamp()).to_bytes(4, "big")
tag: Optional[DataObject] = None
tag: DataObject | None = None
if key == KeyTypes.KEY_SIG:
self.data.sig.date = dt
tag = DataObject.DO_DATES_WR_SIG
Expand Down Expand Up @@ -1323,7 +1283,7 @@ def _decode_tlv(self, tlv: bytes) -> dict:
tlv = tlv[offset + length :]
return tags

def _transmit(self, data: bytes, long_resp: bool = False) -> Tuple[bytes, int, int]:
def _transmit(self, data: bytes, long_resp: bool = False) -> tuple[bytes, int, int]:
"""Transmit data, and get the response

Args:
Expand All @@ -1343,7 +1303,7 @@ def _transmit(self, data: bytes, long_resp: bool = False) -> Tuple[bytes, int, i
raise GPGCardExcpetion(sw, "")
return resp, sw1, sw2

def _exchange(self, apdu: bytes, data: bytes = b"") -> Tuple[bytes, int]:
def _exchange(self, apdu: bytes, data: bytes = b"") -> tuple[bytes, int]:
"""Exchange APDU, and get the response

Args:
Expand Down Expand Up @@ -1391,14 +1351,7 @@ def _get_int(self, buffer: bytes, size: int = 2, offset: int = 0) -> int:
if size == 2:
return (buffer[offset] << 8) | buffer[offset + 1]
if size == 3:
return (
(buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]
)
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]
if size == 4:
return (
(buffer[offset] << 24)
| (buffer[offset + 1] << 16)
| (buffer[offset + 2] << 8)
| buffer[offset + 3]
)
return (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | buffer[offset + 3]
return 0
Loading
Loading