openpgp-card-app/pytools/gpgapp/gpgcard.py
2024-03-01 16:13:21 +01:00

1301 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
# pylint: disable=import-error
from smartcard.System import readers # type: ignore
from smartcard.pcsc import PCSCReader # type: ignore
from smartcard import CardConnectionDecorator # 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
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.connection: CardConnectionDecorator = 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
"""
allreaders: list = readers()
for elt in allreaders:
if str(elt).startswith(device):
reader: PCSCReader.PCSCReader = elt
self.connection = reader.createConnection()
self.connection.connect()
return
print("")
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "No Reader detected!")
def disconnect(self):
"""Connect from the selected Reader"""
return self.connection.disconnect()
############### 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)
self.data.hw_features = int(self._get_data(DataObject.DO_GEN_FEATURES)[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] not in set(iter(PubkeyAlgo)):
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:
ret = "ECDSA"
if attributes[0] == PubkeyAlgo.ECDH:
ret = "ECDH"
if attributes[0] == PubkeyAlgo.EDDSA:
ret = "EDDSA"
else:
ret = ""
return ret
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
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")
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
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)
resp, sw1, sw2 = self.connection.transmit(list(data))
sw = (sw1 << 8) | sw2
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