You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openpgp-card-app/pytools/gpgapp/gpgcard.py

1296 lines
43 KiB
Python

# -*- coding: utf-8 -*-
#*****************************************************************************
# Ledger App OpenPGP.
# (c) 2024 Ledger SAS.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#*****************************************************************************
import binascii
from datetime import datetime, timezone
import pickle
from hashlib import sha1
from typing import Optional, Tuple
from dataclasses import dataclass
from Crypto.PublicKey.RSA import construct
from gpgapp.gpgcmd import DataObject, ErrorCodes, KeyTypes, PassWord, PubkeyAlgo # type: ignore
from gpgapp.gpgcmd import KEY_OPERATIONS, KEY_TEMPLATES, USER_SALUTATION # type: ignore
# pylint: disable=import-error
from ledgercomm import Transport # type: ignore
# pylint: enable=import-error
APDU_MAX_SIZE: int = 0xFE
APDU_CHAINING_MODE: int = 0x10
class GPGCardExcpetion(Exception):
"""Exception handler.
Attributes:
code (int): Error code
message (str): Error message
"""
def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(self.message)
@dataclass
class KeyInfo:
"""Key description information"""
attribute: bytes = b""
fingerprint: bytes = b""
ca_fingerprint: bytes = b""
cert: str = ""
date: datetime = datetime.min
uif: int = 0
key: bytes = b""
def reset(self):
"""Reset the data to the initial value"""
self.attribute = b""
self.fingerprint = b""
self.ca_fingerprint = b""
self.cert = ""
self.date = datetime.min
self.uif = 0
self.key = b""
@dataclass
class CardInfo:
"""Card description information"""
#token info
AID: str = ""
ext_length: bytes = b""
ext_capabilities: bytes = b""
histo_bytes: bytes = b""
PW_status: bytes = b""
hw_features: int = 0
#user info
name: str = ""
login: str = ""
url: str = ""
lang: str = ""
salutation: str = ""
#keys info
rsa_pub_exp: int = 0
digital_counter: int = 0
sig: KeyInfo = KeyInfo()
dec: KeyInfo = KeyInfo()
aut: KeyInfo = KeyInfo()
#private info
private_01: bytes = b""
private_02: bytes = b""
private_03: bytes = b""
private_04: bytes = b""
def reset(self):
"""Reset the data to the initial value"""
#token info
self.AID = ""
self.ext_length = b""
self.ext_capabilities = b""
self.histo_bytes = b""
self.PW_status = b""
#user info
self.name = ""
self.login = ""
self.url = ""
self.lang = ""
self.salutation = ""
#keys info
self.rsa_pub_exp = 0
self.digital_counter = 0
self.sig.reset()
self.dec.reset()
self.aut.reset()
#private info
self.private_01 = b""
self.private_02 = b""
self.private_03 = b""
class GPGCard() :
def __init__(self) -> None:
self.log: bool = False
self.transport: Transport = None
self.slot_current: bytes = b"\x00"
self.slot_config: bytes = bytes(3)
self.data: CardInfo = CardInfo()
self.data.reset()
def connect(self, device: str) -> None:
"""Connect to the selected Reader
Args:
device (str): Reader device name
"""
if device == "speculos":
self.transport = Transport("tcp", server="127.0.0.1", port=9999, debug=False)
else:
self.transport = Transport("hid")
print("")
def disconnect(self):
"""Connect from the selected Reader"""
self.transport.close()
############### LOG interface ###############
def log_apdu(self, log: bool) -> None:
"""Control APDU debugging display
Args:
log (bool): Activate or not the debug print
"""
self.log = log
def add_log(self, mode: str, data: bytes, sw: int = 0) -> None:
"""Print APDU content
Args:
mode (str): Indicate the Send or Recv information
data (bytes): APDU content
sw (int): Returned Status
"""
if self.log:
sw_code = f" ({sw:04x})" if mode == "recv" else ""
print(f"{mode}:{sw_code} {''.join([f'{b:02x}' for b in data])}")
############### CARD interface ###############
def select(self):
"""Send SELECT APDU command"""
apdu = binascii.unhexlify(b"00A4040006D27600012401")
return self._exchange(apdu)
def activate(self):
"""Send ACTIVATE APDU command"""
apdu = binascii.unhexlify(b"00440000")
return self._exchange(apdu)
def terminate(self):
"""Send TERMINATE APDU command"""
apdu = binascii.unhexlify(b"00E60000")
return self._exchange(apdu)
def get_log(self):
"""Send GET_LOG APDU command"""
apdu = binascii.unhexlify(b"00040000")
return self._exchange(apdu)
############### API interfaces ###############
def get_all(self) -> None:
"""Retrieve all Data Object values from the Card"""
self.data.reset()
data: Optional[bytes] = b""
b_data: bytes = b""
s_data: str = ""
self.slot_current = self._get_data(DataObject.CMD_SLOT_CUR)
self.slot_config = self._get_data(DataObject.CMD_SLOT_CFG)
self.data.AID = self._get_data(DataObject.DO_AID).hex().upper()
self.data.login = self._get_data(DataObject.DO_LOGIN).decode("utf-8")
self.data.url = self._get_data(DataObject.DO_URL).decode("utf-8")
self.data.histo_bytes = self._get_data(DataObject.DO_HIST)
data = self._get_data(DataObject.DO_GEN_FEATURES)
if data:
self.data.hw_features = data[0]
data = self._get_data(DataObject.DO_CARDHOLDER_DATA)
tags = self._decode_tlv(data)
if DataObject.DO_CARD_NAME in tags:
self.data.name = tags[DataObject.DO_CARD_NAME].decode("utf-8")
if DataObject.DO_CARD_SALUTATION in tags:
s_data = tags[DataObject.DO_CARD_SALUTATION].decode("utf-8")
for k,v in USER_SALUTATION.items():
if v == s_data:
self.data.salutation = k
break
if DataObject.DO_CARD_LANG in tags:
self.data.lang = tags[DataObject.DO_CARD_LANG].decode("utf-8")
data = self._get_data(DataObject.DO_APP_DATA)
tags = self._decode_tlv(data)
if DataObject.DO_EXT_LEN in tags:
self.data.ext_length = tags[DataObject.DO_EXT_LEN]
if DataObject.DO_DISCRET_DATA in tags:
b_data = tags[DataObject.DO_DISCRET_DATA]
tags = self._decode_tlv(b_data)
if DataObject.DO_EXT_CAP in tags:
self.data.ext_capabilities = tags[DataObject.DO_EXT_CAP]
if DataObject.DO_SIG_ATTR in tags:
self.data.sig.attribute = tags[DataObject.DO_SIG_ATTR]
if DataObject.DO_DEC_ATTR in tags:
self.data.dec.attribute = tags[DataObject.DO_DEC_ATTR]
if DataObject.DO_AUT_ATTR in tags:
self.data.aut.attribute = tags[DataObject.DO_AUT_ATTR]
if DataObject.DO_PW_STATUS in tags:
self.data.PW_status = tags[DataObject.DO_PW_STATUS]
data = tags.get(DataObject.DO_FINGERPRINTS)
if data:
self.data.sig.fingerprint = data[0:20]
self.data.dec.fingerprint = data[20:40]
self.data.aut.fingerprint = data[40:60]
data = tags.get(DataObject.DO_CA_FINGERPRINTS)
if data:
self.data.sig.ca_fingerprint = data[0:20]
self.data.dec.ca_fingerprint = data[20:40]
self.data.aut.ca_fingerprint = data[40:60]
data = tags.get(DataObject.DO_KEY_DATES)
if data:
dates = tags[DataObject.DO_KEY_DATES]
self._conv_date_from_bytes(KeyTypes.KEY_SIG, dates[0:4])
self._conv_date_from_bytes(KeyTypes.KEY_DEC, dates[4:8])
self._conv_date_from_bytes(KeyTypes.KEY_AUT, dates[8:12])
data = self._get_data(DataObject.CMD_RSA_EXP)
self.data.rsa_pub_exp = self._get_int(data, 4)
self.data.aut.cert = self._get_data(DataObject.DO_CERT).decode("utf-8")
self.data.dec.cert = self._get_data(DataObject.DO_CERT, True).decode("utf-8")
self.data.sig.cert = self._get_data(DataObject.DO_CERT, True).decode("utf-8")
self.data.sig.uif = int(self._get_data(DataObject.DO_UIF_SIG)[0])
self.data.dec.uif = int(self._get_data(DataObject.DO_UIF_DEC)[0])
self.data.aut.uif = int(self._get_data(DataObject.DO_UIF_AUT)[0])
data = self._get_data(DataObject.DO_SEC_TEMPL)
tags = self._decode_tlv(data)
if DataObject.DO_SIG_COUNT in tags:
b_data = tags[DataObject.DO_SIG_COUNT]
self.data.digital_counter = self._get_int(b_data, 3)
if self.data.ext_capabilities[0] & 0x08:
self.data.private_01 = self._get_data(DataObject.DO_PRIVATE_01)
self.data.private_02 = self._get_data(DataObject.DO_PRIVATE_02)
self.data.private_03 = self._get_data(DataObject.DO_PRIVATE_03)
self.data.private_04 = self._get_data(DataObject.DO_PRIVATE_04)
self.data.sig.key = self._get_data(DataObject.DO_SIG_KEY)
self.data.dec.key = self._get_data(DataObject.DO_DEC_KEY)
self.data.aut.key = self._get_data(DataObject.DO_AUT_KEY)
def backup(self, file_name: str) -> None:
"""Backup data to backup file
Args:
file_name (str): Backup filename
"""
self.get_all()
with open(file_name, mode="w+b") as f:
pickle.dump(
(self.data.AID, self.data.PW_status, self.data.rsa_pub_exp, self.data.digital_counter,
self.data.private_01, self.data.private_02,
self.data.private_03, self.data.private_04,
self.data.name, self.data.login, self.data.salutation, self.data.url, self.data.lang,
self.data.sig.key, self.data.sig.uif, self.data.sig.attribute, self.data.sig.date,
self.data.sig.fingerprint, self.data.sig.ca_fingerprint, self.data.sig.cert,
self.data.dec.key, self.data.dec.uif, self.data.dec.attribute, self.data.dec.date,
self.data.dec.fingerprint, self.data.dec.ca_fingerprint, self.data.dec.cert,
self.data.aut.key, self.data.aut.uif, self.data.aut.attribute, self.data.aut.date,
self.data.aut.fingerprint, self.data.aut.ca_fingerprint, self.data.aut.cert),
f, 2)
def restore(self, file_name: str) -> None:
"""Restore data from backup file
Args:
file_name (str): Backup filename
"""
with open(file_name, mode="r+b") as f:
(self.data.AID, self.data.PW_status, self.data.rsa_pub_exp, self.data.digital_counter,
self.data.private_01, self.data.private_02, self.data.private_03, self.data.private_04,
self.data.name, self.data.login, self.data.salutation, self.data.url, self.data.lang,
self.data.sig.key, self.data.sig.uif, self.data.sig.attribute, self.data.sig.date,
self.data.sig.fingerprint, self.data.sig.ca_fingerprint, self.data.sig.cert,
self.data.dec.key, self.data.dec.uif, self.data.dec.attribute, self.data.dec.date,
self.data.dec.fingerprint, self.data.dec.ca_fingerprint, self.data.dec.cert,
self.data.aut.key, self.data.aut.uif, self.data.aut.attribute, self.data.aut.date,
self.data.aut.fingerprint, self.data.aut.ca_fingerprint, self.data.aut.cert) = pickle.load(f)
self._put_data(DataObject.DO_AID, bytes.fromhex(self.data.AID[20:28]))
self._put_data(DataObject.DO_PW_STATUS, self.data.PW_status)
self._put_data(DataObject.DO_PRIVATE_01, self.data.private_01)
self._put_data(DataObject.DO_PRIVATE_02, self.data.private_02)
self._put_data(DataObject.DO_PRIVATE_03, self.data.private_03)
self._put_data(DataObject.DO_PRIVATE_04, self.data.private_04)
self._put_data(DataObject.DO_CARD_NAME, self.data.name.encode("utf-8"))
self._put_data(DataObject.DO_LOGIN, self.data.login.encode("utf-8"))
self._put_data(DataObject.DO_CARD_LANG, self.data.lang.encode("utf-8"))
self._put_data(DataObject.DO_URL, self.data.url.encode("utf-8"))
if len(self.data.salutation) == 0:
self._put_data(DataObject.DO_CARD_SALUTATION, b'\x30')
else:
self._put_data(DataObject.DO_CARD_SALUTATION,
bytes.fromhex(USER_SALUTATION[self.data.salutation]))
self._put_data(DataObject.DO_SIG_ATTR, self.data.sig.attribute)
self._put_data(DataObject.DO_DEC_ATTR, self.data.dec.attribute)
self._put_data(DataObject.DO_AUT_ATTR, self.data.aut.attribute)
self._put_data(DataObject.DO_UIF_SIG, self.data.sig.uif.to_bytes(2, "little"))
self._put_data(DataObject.DO_UIF_DEC, self.data.dec.uif.to_bytes(2, "little"))
self._put_data(DataObject.DO_UIF_AUT, self.data.aut.uif.to_bytes(2, "little"))
self._put_data(DataObject.DO_SIG_COUNT, self.data.digital_counter.to_bytes(4, "big"))
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_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)
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_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)
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_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)
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)
def export_pub_key(self, pubkey: dict, file_name: str) -> None:
"""Export a Public to file
Args:
pubkey (dict): Public key parameters
file_name (str): Backup filename
"""
modulus = bytearray.fromhex(pubkey["Modulus"])
exponent = bytearray.fromhex(pubkey["Pub Exp"][2:])
key = construct((int.from_bytes(modulus, 'big'), int.from_bytes(exponent, 'big')))
public_key = key.publickey().export_key()
with open(file_name, mode="wb") as f:
f.write(public_key)
def seed_key(self) -> None:
"""Regenerate keys, based on seed mode"""
apdu = binascii.unhexlify(b"0047800102B600")
self._exchange(apdu)
apdu = binascii.unhexlify(b"0047800102B800")
self._exchange(apdu)
apdu = binascii.unhexlify(b"0047800102A400")
self._exchange(apdu)
############### Information decoding ###############
def decode_AID(self) -> dict:
"""Decode Application IDentity information"""
return {
"AID": f"{self.data.AID}",
"RID": f"{self.data.AID[0:10]}",
"Application": f"{self.data.AID[10:12]}",
"Version": f"{int(self.data.AID[12:14]):d}.{int(self.data.AID[14:16]):d}",
"Manufacturer": f"{self.data.AID[16:20]}",
"Serial": f"{self.data.AID[20:28]}"
}
def decode_histo(self) -> dict:
"""Decode Historical Bytes information"""
return {
"historical bytes": self.data.histo_bytes.hex()
}
def decode_extlength(self) -> dict:
"""Decode Extended Length information"""
d = {
"Command": "N/A",
"Response": "N/A",
}
if self.data.ext_length:
d["Command"] = f"{self._get_int(self.data.ext_length, offset=2):d}"
d["Response"] = f"{self._get_int(self.data.ext_length, offset=6):d}"
return d
def decode_ext_capabilities(self) -> dict:
"""Decode Extended Capabilities information"""
d = {}
b1 = self.data.ext_capabilities[0]
if b1 & 0x80:
if self.data.ext_capabilities[1] == 1:
d["Secure Messaging"] = "✓: AES 128 bits"
elif self.data.ext_capabilities[1] == 2:
d["Secure Messaging"] = "✓: AES 256 bits"
else:
d["Secure Messaging"] = "✓: ?? bits"
else:
d["Secure Messaging"] = ""
if b1 & 0x40:
max_val = self._get_int(self.data.ext_capabilities, offset=2)
d["Get Challenge"] = f"✓ (Max length: {max_val:d})"
else:
d["Get Challenge"] = ""
if b1 & 0x20:
d["Key import"] = ""
else:
d["Key import"] = ""
if b1 & 0x10:
d["PW status"] = "Changeable"
else:
d["PW status"] = "Fixed"
if b1 & 0x08:
d["Private DOs"] = ""
else:
d["Private DOs"] = ""
if b1 & 0x04:
d["Algo attributes"] = "Changeable"
else:
d["Algo attributes"] = "Fixed"
if b1 & 0x02:
d["PSO:DEC AES"] = ""
else:
d["PSO:DEC AES"] = ""
if b1 & 0x01:
d["Key Derived Format"] = ""
else:
d["Key Derived Format"] = ""
max_val = self._get_int(self.data.ext_capabilities, offset=4)
d["Max Cert len"] = f"{max_val:d}"
max_val = self._get_int(self.data.ext_capabilities, offset=6)
d["Max Special DO"] = f"{max_val:d}"
if self.data.ext_capabilities[8]:
d["PIN 2 format"] = ""
else:
d["PIN 2 format"] = ""
if self.data.ext_capabilities[9]:
d["MSE"] = ""
else:
d["MSE"] = ""
return d
def decode_pws(self) -> dict:
"""Decode Password information"""
if self.data.PW_status[0] == 0:
validity = "Only 1 PSO:CDS"
elif self.data.PW_status[0] == 1:
validity = "Several PSO:CDS"
else:
validity = f"unknown ({self.data.PW_status[0]:d})"
cfg = {
"PW1": {"format": 1, "counter": 4},
"Reset Counter": {"format": 2, "counter": 5},
"PW3": {"format": 3, "counter": 6},
}
d = {}
for name, pw in cfg.items():
if self.data.PW_status[pw["format"]] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.data.PW_status[pw["format"]] & 0x7f
counter = self.data.PW_status[pw['counter']]
d[name] = f"{fmt} ({pwlen:d} bytes), Error Counter={counter:d}"
if name == "PW1":
d[name] += f", Validity={validity}"
return d
def decode_hardware(self) -> dict:
"""Decode Hardware features information"""
d = {}
d["Display"] = "" if self.data.hw_features & 0x80 else ""
d["Biometric sensor"] = "" if self.data.hw_features & 0x40 else ""
d["Button/Keypad"] = "" if self.data.hw_features & 0x20 else ""
d["LED"] = "" if self.data.hw_features & 0x10 else ""
d["Loudspeaker"] = "" if self.data.hw_features & 0x08 else ""
d["Microphone"] = "" if self.data.hw_features & 0x04 else ""
d["Touchscreen"] = "" if self.data.hw_features & 0x02 else ""
d["Battery"] = "" if self.data.hw_features & 0x01 else ""
return d
############### SLOT interface ###############
def select_slot(self, slot: int) -> None:
"""Select the key slot
Args:
slot (int): slot id to select (0 to 3)
"""
self.slot_current = slot.to_bytes(1, "big")
self._put_data(DataObject.CMD_SLOT_CUR, self.slot_current)
def decode_slot(self) -> dict:
"""Decode Slots information
Returns:
Slots configuration dictionary
"""
d = {}
d["Number of Slots"] = str(self.slot_config[0])
d["Default Slot"] = str(self.slot_config[1] + 1)
d["Selection by APDU"] = "" if self.slot_config[2] & 0x01 else ""
d["Selection by screen"] = "" if self.slot_config[2] & 0x02 else ""
d["Current"] = str(int.from_bytes(self.slot_current, "big") + 1)
return d
############### USER interface ###############
def set_serial(self, serial: str) -> None:
"""Set the Card serial number
Args:
serial (str): New serial number
"""
if not self.data.AID:
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Invalid AID!")
self.data.AID = self.data.AID[0:20] + serial
self._put_data(DataObject.DO_AID, bytes.fromhex(serial))
def set_name(self, name: str) -> None:
"""Set the Card User name
Args:
name (str): New name
"""
self.data.name = name
self._put_data(DataObject.DO_CARD_NAME, name.encode("utf-8"))
def get_name(self) -> str:
"""Get the Card User name"""
return self.data.name
def set_login(self, login: str) -> None:
"""Set the Card User login
Args:
login (str): New login
"""
self.data.login = login
self._put_data(DataObject.DO_LOGIN, login.encode("utf-8"))
def get_login(self) -> str:
"""Get the Card User login"""
return self.data.login
def set_url(self, url: str) -> None:
"""Set the Card User URL
Args:
url (str): New URL
"""
self.data.url = url
self._put_data(DataObject.DO_URL, url.encode("utf-8"))
def get_url(self) -> str:
"""Get the Card User URL"""
return self.data.url
def set_lang(self, lang: str) -> None:
"""Set the Card User language
Args:
lang (str): New language
"""
self.data.lang = lang
self._put_data(DataObject.DO_CARD_LANG, lang.encode("utf-8"))
def get_lang(self) -> str:
"""Get the Card User language"""
return self.data.lang
def set_salutation(self, salutation: str) -> None:
"""Set the Card User salutation
Args:
salutation (str): New salutation
"""
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
self.data.salutation = salutation
self._put_data(DataObject.DO_CARD_SALUTATION, salutation_str)
def get_salutation(self) -> str:
"""Get the Card User salutation"""
return self.data.salutation
############### PASSWORD interface ###############
def verify_pin(self, pw: PassWord, value: str, pinpad: bool = False) -> bool:
"""Verify the password
Args:
pw (PassWord): Password type, corresponding to User, Admin
value (str) : Password value
pinpad (bool): Indicate to use pinpad
Return:
Success / KO boolean
"""
value = value if value else ""
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")
_, sw = self._exchange(apdu)
return sw == ErrorCodes.ERR_SUCCESS
def change_pin(self, pw: PassWord, cur_value: str, new_value: str) -> bool:
"""Update the password
Args:
pw (PassWord): Password type, corresponding to User, Admin
cur_value (str): Current password value
new_value (str): New password value
Return:
Success / KO boolean
"""
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")
_, sw = self._exchange(apdu)
return sw == ErrorCodes.ERR_SUCCESS
def set_RC(self, value: str) -> bool:
"""Set the User Password Resetting Code
Args:
value (str): Resetting Code value
Return:
Success / KO boolean
"""
b_value = value.encode("utf-8")
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
Args:
RC (str): Resetting Code value
value (str): User Password value
Return:
Success / KO boolean
"""
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")
_, sw = self._exchange(apdu)
return sw == ErrorCodes.ERR_SUCCESS
############### KEYS interface ###############
def decode_key_uif(self, key: str) -> str:
"""Decode the selected key User Interaction Flag
Args:
key (str): Key type (SIG, DC, AUT)
Return:
UIF status for the selected key
"""
uif = self._get_key_object(key).uif
if uif == 0:
return ""
if uif == 1:
return ""
if uif == 2:
return "✓ (Permanent)"
return ""
def get_key_date(self, key: str) -> str:
"""Get key Creation Date
Args:
key (str): Key type (SIG, DC, AUT)
Return:
Key Creation Date
"""
return str(self._get_key_object(key).date)
def get_sig_count(self) -> int:
"""Get Digital Signatures Count
Return:
Number of Digital Signatures Count
"""
return self.data.digital_counter
def get_rsa_pub_exp(self) -> int:
"""Get RSA Public Exponent
Return:
RSA Public Exponent
"""
return self.data.rsa_pub_exp
def get_key_cert(self, key: str) -> str:
"""Get key Certificate
Args:
key (str): Key type (SIG, DC, AUT)
Return:
Key Certificate
"""
return self._get_key_object(key).cert
def set_template(self, key: str, template: str) -> None:
"""Set key Template
Args:
key (str): Key type (SIG, DC, AUT)
template (str): Key template
"""
if template not in KEY_TEMPLATES:
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid template: {template}")
data = binascii.unhexlify(KEY_TEMPLATES[template])
if key == KeyTypes.KEY_SIG:
self._put_data(DataObject.DO_SIG_ATTR, data)
elif key == KeyTypes.KEY_DEC:
self._put_data(DataObject.DO_DEC_ATTR, data)
elif key == KeyTypes.KEY_AUT:
self._put_data(DataObject.DO_AUT_ATTR, data)
def set_key_fingerprint(self, key: str, data: bytes) -> None:
"""Set key fingerprint
Args:
key (str): Key type (SIG, DC, AUT)
data (bytes): Fingerprint
"""
if key == KeyTypes.KEY_SIG:
self.data.sig.fingerprint = data
self._put_data(DataObject.DO_FINGERPRINT_WR_SIG, data)
elif key == KeyTypes.KEY_AUT:
self.data.aut.fingerprint = data
self._put_data(DataObject.DO_FINGERPRINT_WR_AUT, data)
elif key == KeyTypes.KEY_DEC:
self.data.dec.fingerprint = data
self._put_data(DataObject.DO_FINGERPRINT_WR_DEC, data)
def get_key_fingerprint(self, key: str) -> str:
"""Get key fingerprint
Args:
key (str): Key type (SIG, DC, AUT)
Return:
Key Fingerprint
"""
fingerprint = self._get_key_object(key).fingerprint
sdata = binascii.hexlify(fingerprint).decode("ascii")
return sdata if sdata != "0"*40 else "N/A"
def get_key_CA_fingerprint(self, key: str) -> str:
"""Get key CA fingerprint
Args:
ey (str): Key type (SIG, DC, AUT)
Return:
Key CA Fingerprint
"""
fingerprint = self._get_key_object(key).ca_fingerprint
sdata = binascii.hexlify(fingerprint).decode("ascii")
return sdata if sdata != "0"*40 else "N/A"
def decode_attributes(self, key: str) -> str:
"""Decode key attribute
Args:
key (str): Key type (SIG, DC, AUT)
Return:
String with attributes and size
"""
attributes = self._get_key_object(key).attribute
if not attributes or len(attributes) == 0:
return ""
if attributes[0] == PubkeyAlgo.RSA:
if attributes[5] == 0:
fmt = "standard (e, p, q)"
elif attributes[5] == 1:
fmt = "standard with modulus (n)"
elif attributes[5] == 2:
fmt = "crt (Chinese Remainder Theorem)"
elif attributes[5] == 3:
fmt = "crt (Chinese Remainder Theorem) with modulus (n)"
ret = f"RSA-{self._get_int(attributes, offset=1)}"
ret += f", Format: {fmt}"
ret += f", Exponent size: {self._get_int(attributes, offset=3)}"
return ret
if attributes[0] == PubkeyAlgo.ECDSA:
return "ECDSA"
if attributes[0] == PubkeyAlgo.ECDH:
return "ECDH"
if attributes[0] == PubkeyAlgo.EDDSA:
return "EDDSA"
return ""
def decode_key(self, key: str) -> dict:
"""Get key parameters
Args:
key (str): Key type (SIG, DC, AUT)
Return:
Key information dictionary
"""
d = {}
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}"
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}"
offset += size
size = int.from_bytes(key_data[offset:offset + 4], 'big')
d["Private key size"] = str(size)
# offset += 4
# d["Private key encrypted"] = key_data[offset:offset + size].hex()
return d
def asymmetric_key(self, key: str, action: str) -> dict:
"""Asymmetric key operation
Args:
key (str): Key type (SIG, DC, AUT)
action (str): Generate or Read
Return:
Public key information:
RSA (Encrypt or Sign): {
"id": (int) 1,
"Modulus": (int)
"Pub Exp" (int)
}
ECDSA for PSO:CDS and INT-AUT: {
"id": (int) 19,
"OID": (bytes)
}
ECDH for PSO:DEC {
"id": (int) 18,
"OID": (bytes)
}
"""
if action not in KEY_OPERATIONS:
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid Key operation: {action}")
op = KEY_OPERATIONS[action]
attributes = None
b_key: int = 0
if key == KeyTypes.KEY_SIG:
attributes = self.data.sig.attribute
b_key = DataObject.DO_SIG_KEY
elif key == KeyTypes.KEY_DEC:
attributes = self.data.dec.attribute
b_key = DataObject.DO_DEC_KEY
elif key == KeyTypes.KEY_AUT:
attributes = self.data.aut.attribute
b_key = DataObject.DO_AUT_KEY
if not attributes or len(attributes) == 0:
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!")
d = {}
tags = self._asym_key_pair(op, b_key)
if attributes[0] == PubkeyAlgo.RSA:
d["ID"] = f"RSA-{self._get_int(attributes, offset=1)}"
d["Modulus"] = tags[0x81].hex()
d["Pub Exp"] = f"0x{tags[0x82].hex()}"
else:
if attributes[0] == PubkeyAlgo.ECDSA:
d["ID"] = "ECDSA"
if attributes[0] == PubkeyAlgo.ECDH:
d["ID"] = "ECDH"
if attributes[0] == PubkeyAlgo.EDDSA:
d["ID"] = "EDDSA"
d["oid"] = tags[0x86].hex()
if attributes[0] == PubkeyAlgo.RSA:
if action == "Generate":
# Set the generation date
self._set_key_date_now(key)
# Get the generation date
date = self.get_key_date(key)
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
kdate = int(dt.timestamp())
d["Creation date"] = date
# Compute the fingerprint https://www.rfc-editor.org/rfc/rfc4880#section-12.2
modulus: bytes = bytes.fromhex(d["Modulus"])
ksize: int = self._get_int(attributes, offset=1)
size: int = int(ksize / 8) + 0x0D # len(header + tag + pub exp)
header: bytes = bytes.fromhex(f"99{size:04x}04{kdate:08x}01{ksize:04x}")
footer: bytes = bytes.fromhex("0011010001")
data: bytes = header + modulus + footer
_hash = sha1()
_hash.update(data)
result: bytes = _hash.digest()
d["Fingerprint"] = result.hex()
if action == "Generate":
self.set_key_fingerprint(key, result)
return d
# ===============================================================
# Internal functions
# ===============================================================
def _get_key_object(self, key: str) -> KeyInfo:
"""Get KeyInfo data class object
Args:
key (str): Key type (SIG, DC, AUT)
Return:
KeyInfo Object
"""
if key == KeyTypes.KEY_SIG:
return self.data.sig
if key == KeyTypes.KEY_AUT:
return self.data.aut
if key == KeyTypes.KEY_DEC:
return self.data.dec
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid key type {key}!")
def _get_data(self, tag: int, bnext: bool = False) -> bytes:
"""Send APDU command to GET a specific Data Object
Args:
tag (int): Data Object tag
Return:
Data Object bytes
"""
ins = 0xCC if bnext else 0xCA
apdu = bytes.fromhex(f"00{ins:02x}{tag:04x}00")
resp, _ = self._exchange(apdu)
return resp
def _put_data(self, tag: int, data: bytes) -> int:
"""Send APDU command to PUT a Data Object value
Args:
tag (int): Data Object tag
data (bytes): Data Object bytes
Return:
Status Word
"""
apdu = bytes.fromhex(f"00DA{tag:04x}")
_, sw = self._exchange(apdu, data)
return sw
def _asym_key_pair(self, op: int, key: int) -> dict:
"""Asymmetric key pair operation
Args:
op (int): Operation to execute
key (int): Key type
Return:
Public key decoded data bytes
"""
apdu = bytes.fromhex(f"0047{op:02x}0002{key:02x}00")
resp, sw = self._exchange(apdu)
if sw != ErrorCodes.ERR_SUCCESS:
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Key operation error!")
tags = self._decode_tlv(resp)
return self._decode_tlv(tags[DataObject.DO_PUB_KEY])
def _conv_date_from_bytes(self, key: str, data: bytes) -> None:
"""Convert date from bytes to datetime local dataclass
Args:
key (str): Key type (SIG, DC, AUT)
data (bytes): Date value
"""
idate = int.from_bytes(data, "big")
self._get_key_object(key).date = datetime.utcfromtimestamp(idate)
def _set_key_date_now(self, key: str) -> None:
"""Set Key creation date
Args:
key (str): Key type (SIG, DC, AUT)
"""
dt = datetime.utcnow().replace(microsecond=0)
bdate = int(dt.timestamp()).to_bytes(4, "big")
tag: Optional[DataObject] = None
if key == KeyTypes.KEY_SIG:
self.data.sig.date = dt
tag = DataObject.DO_DATES_WR_SIG
elif key == KeyTypes.KEY_AUT:
self.data.aut.date = dt
tag = DataObject.DO_DATES_WR_AUT
elif key == KeyTypes.KEY_DEC:
self.data.dec.date = dt
tag = DataObject.DO_DATES_WR_DEC
if tag:
self._put_data(tag, bdate)
def _decode_tlv(self, tlv: bytes) -> dict:
"""Decode TLV fields
Args:
tlv (bytes): Input data bytes to parse
Returns:
dict {t: v, t:v, ...}
"""
tags = {}
while len(tlv):
o = 0
l = 0
if (tlv[0] & 0x1F) == 0x1F:
t = self._get_int(tlv)
o = 2
else:
t = tlv[0]
o = 1
l = tlv[o]
if l & 0x80 :
if (l & 0x7f) == 1:
l = tlv[o + 1]
o += 2
if (l & 0x7f) == 2:
l = self._get_int(tlv, offset=o + 1)
o += 3
else:
o += 1
v = tlv[o:o + l]
tags[t] = v
tlv = tlv[o + l:]
return tags
def _transmit(self, data: bytes, long_resp: bool = False) -> Tuple[bytes, int, int]:
"""Transmit data, and get the response
Args:
data (bytes): APDU to transmit
long_resp (bool): Indicate if long response is expected
Return:
Response data bytes and the Status Word
"""
self.add_log("send", data)
sw, resp = self.transport.exchange_raw(data)
sw1 = (sw >> 8) & 0xFF
sw2 = sw & 0xFF
self.add_log("recv", resp, sw)
if sw != ErrorCodes.ERR_SUCCESS and not long_resp:
raise GPGCardExcpetion(sw, "")
return resp, sw1, sw2
def _exchange(self, apdu: bytes, data: bytes = b"") -> Tuple[bytes, int]:
"""Exchange APDU, and get the response
Args:
apdu (bytes): APDU content
data (bytes): Data to transmit
Return:
Response data bytes and the Status Word
"""
#send
apdux: bytes = b""
resp: bytes = b""
if len(data) > 0:
if len(data) > APDU_MAX_SIZE:
cla: bytes = (apdu[0] | APDU_CHAINING_MODE).to_bytes(1, "big")
m_apdu: bytes = cla + apdu[1:5] + APDU_MAX_SIZE.to_bytes(1, "big")
while len(data) > APDU_MAX_SIZE:
apdux = m_apdu + data[0:APDU_MAX_SIZE]
self._transmit(apdux)
data = data[APDU_MAX_SIZE:]
apdu += len(data).to_bytes(1, "big") + data
resp, sw1, sw2 = self._transmit(apdu, True)
#receive
while sw1 == ErrorCodes.ERR_SW1_VALID:
apdux = bytes.fromhex(f"00c00000{sw2:02x}")
resp2, sw1, sw2 = self._transmit(apdux, True)
resp += resp2
sw = (sw1 << 8) | sw2
return bytes(resp), sw
def _get_int(self, buffer: bytes, size: int = 2, offset: int = 0) -> int:
"""Exchange APDU, and get the response
Args:
buffer (bytes): data content
offset (int): Offset of MSB
Return:
Converted 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]
if size == 4:
return (buffer[offset] << 24) | (buffer[offset + 1] << 16) | \
(buffer[offset + 2] << 8) | buffer[offset + 3]
return 0