mirror of
https://github.com/LedgerHQ/openpgp-card-app
synced 2024-11-09 07:10:30 +00:00
319 lines
8.8 KiB
Python
319 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# SPDX-FileCopyrightText: 2023 Ledger SAS
|
|
# SPDX-License-Identifier: LicenseRef-LEDGER
|
|
"""
|
|
This module provides Ragger tests utility functions
|
|
"""
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
import re
|
|
from Crypto.PublicKey import RSA, ECC
|
|
from Crypto.Util.number import bytes_to_long
|
|
from Crypto.Signature import eddsa
|
|
|
|
from ragger.navigator import NavInsID, NavIns, Navigator
|
|
from ragger.firmware import Firmware
|
|
|
|
from application_client.command_sender import CommandSender
|
|
from application_client.app_def import Errors, PassWord, DataObject, PubkeyAlgo
|
|
|
|
ROOT_SCREENSHOT_PATH = Path(__file__).parent.resolve()
|
|
|
|
KEY_TEMPLATES = {
|
|
"rsa2048" : "010800002001",
|
|
"rsa3072" : "010C00002001",
|
|
# "rsa4096" : "011000002001", Not supported yet
|
|
"nistp256": "132A8648CE3D030107",
|
|
"ed25519" : "162B06010401DA470F01",
|
|
"cv25519" : "122B060104019755010501"
|
|
}
|
|
|
|
# digestInfo header: https://www.rfc-editor.org/rfc/rfc8017#section-9.2
|
|
SHA256_DIGEST_INFO = b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"
|
|
|
|
|
|
def util_navigate(
|
|
firmware: Firmware,
|
|
navigator: Navigator,
|
|
test_name: Path,
|
|
text: str = "",
|
|
) -> None:
|
|
"""Navigate in the menus with conditions """
|
|
|
|
assert text
|
|
valid_instr: list[NavIns | NavInsID] = []
|
|
|
|
if firmware.device == "nanos":
|
|
text, txt_cfg = text.split("_")
|
|
nav_inst = NavInsID.RIGHT_CLICK
|
|
if txt_cfg == "Yes":
|
|
valid_instr.append(NavInsID.RIGHT_CLICK)
|
|
elif txt_cfg == "No":
|
|
valid_instr.append(NavInsID.LEFT_CLICK)
|
|
else:
|
|
raise ValueError(f'Wrong text "{text}"')
|
|
|
|
elif firmware.device.startswith("nano"):
|
|
text = text.split("_")[1]
|
|
nav_inst = NavInsID.RIGHT_CLICK
|
|
valid_instr.append(NavInsID.BOTH_CLICK)
|
|
|
|
else:
|
|
text, txt_cfg = text.split("_")
|
|
if txt_cfg == "Yes":
|
|
nav_inst = NavInsID.USE_CASE_CHOICE_CONFIRM
|
|
valid_instr.append(NavInsID.USE_CASE_CHOICE_CONFIRM)
|
|
elif txt_cfg == "No":
|
|
nav_inst = NavInsID.USE_CASE_REVIEW_REJECT
|
|
valid_instr.append(NavInsID.USE_CASE_CHOICE_REJECT)
|
|
else:
|
|
raise ValueError(f'Wrong text "{text}"')
|
|
|
|
# Do not wait last screen change because home screen contain a "random" ID
|
|
navigator.navigate_until_text_and_compare(nav_inst,
|
|
valid_instr,
|
|
text,
|
|
ROOT_SCREENSHOT_PATH,
|
|
test_name,
|
|
screen_change_after_last_instruction=False)
|
|
|
|
|
|
def generate_key(client: CommandSender, key_tag: DataObject, seed: bool = False) -> None:
|
|
"""Generate a Asymmetric key
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
key_tag (DataObject): Tag identifying the key
|
|
seed (bool): Generate a key in SEED mode
|
|
"""
|
|
|
|
# Verify PW3 (Admin)
|
|
check_pincode(client, PassWord.PW3)
|
|
|
|
# Generate the SIG Key Pair
|
|
rapdu = client.generate_key(key_tag, seed)
|
|
assert rapdu.status == Errors.SW_OK
|
|
|
|
|
|
def get_key_attributes(client: CommandSender, key_attr: int) -> Tuple[PubkeyAlgo, int]:
|
|
"""Send and check the pincode
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
key_attr (int): Key related attribute to be parsed
|
|
|
|
Returns:
|
|
Public-Key Algorithm ID and Key size
|
|
"""
|
|
|
|
rapdu = client.get_data(DataObject.DO_APP_DATA)
|
|
assert rapdu.status == Errors.SW_OK
|
|
|
|
tags = decode_tlv(rapdu.data)
|
|
data1 = tags.get(DataObject.DO_DISCRET_DATA, b"")
|
|
if not data1:
|
|
return PubkeyAlgo.INVALID, 0
|
|
data2 = decode_tlv(data1)
|
|
attr = data2.get(key_attr, b"")
|
|
if not attr:
|
|
return PubkeyAlgo.INVALID, 0
|
|
key_algo = attr[0]
|
|
print(f" Key ID: {key_algo}")
|
|
if key_algo == PubkeyAlgo.RSA:
|
|
key_size = (attr[1] << 8) | attr[2]
|
|
print(f" Key size: {key_size}")
|
|
else:
|
|
key_size = 0
|
|
return key_algo, key_size
|
|
|
|
|
|
def get_EDDSA_pub_key(client: CommandSender, key_tag: DataObject) -> ECC.EccKey:
|
|
"""Read the Public Key and generate a EccKey object
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
key_tag (DataObject): Tag identifying the key to read
|
|
|
|
Returns:
|
|
EccKey
|
|
"""
|
|
|
|
# Extract Pub key parameters
|
|
data = _get_pub_key(client, key_tag)
|
|
|
|
oid = data[0x86]
|
|
print(f" OID[{len(oid)}]: {oid.hex()}")
|
|
assert len(oid) == 32
|
|
return eddsa.import_public_key(oid)
|
|
|
|
|
|
def get_ECDSA_pub_key(client: CommandSender, key_tag: DataObject) -> ECC.EccKey:
|
|
"""Read the Public Key and generate a EccKey object
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
key_tag (DataObject): Tag identifying the key to read
|
|
|
|
Returns:
|
|
EccKey
|
|
"""
|
|
|
|
# Extract Pub key parameters
|
|
data = _get_pub_key(client, key_tag)
|
|
|
|
oid = data[0x86][1:]
|
|
pt_len = int(len(oid) / 2)
|
|
x = oid[:pt_len]
|
|
y = oid[pt_len:]
|
|
print(f" X[{len(x)}]: {x.hex()}")
|
|
print(f" Y[{len(y)}]: {y.hex()}")
|
|
assert len(x) == len(y)
|
|
return ECC.construct(curve="P-256", point_x=bytes_to_long(x), point_y=bytes_to_long(y))
|
|
|
|
|
|
def get_RSA_pub_key(client: CommandSender, key_tag: DataObject) -> RSA.RsaKey:
|
|
"""Read the Public Key and generate a RsaKey object
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
key_tag (DataObject): Tag identifying the key to read
|
|
|
|
Returns:
|
|
RsaKey
|
|
"""
|
|
|
|
# Extract Pub key parameters
|
|
data = _get_pub_key(client, key_tag)
|
|
|
|
exponent = data[0x82]
|
|
modulus = data[0x81]
|
|
print(f" Key Exponent: 0x{exponent.hex()}")
|
|
print(f" Key Modulus[{len(modulus)}]: {modulus.hex()}")
|
|
|
|
return RSA.construct((int.from_bytes(modulus, 'big'), int.from_bytes(exponent, 'big')))
|
|
|
|
|
|
def check_pincode(client: CommandSender, pwd: PassWord) -> None:
|
|
"""Send and check the pincode
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
pwd (PassWord): Password to be verified
|
|
"""
|
|
|
|
if pwd in (PassWord.PW1, PassWord.PW2):
|
|
pincode = "123456"
|
|
elif pwd == PassWord.PW3:
|
|
pincode = "12345678"
|
|
# Verify PW2 (User)
|
|
rapdu = client.send_verify_pw(pwd, pincode)
|
|
assert rapdu.status == Errors.SW_OK
|
|
|
|
|
|
def verify_version(version: str) -> None:
|
|
"""Verify the app version, based on defines in Makefile
|
|
|
|
Args:
|
|
Version (str): Version to be checked
|
|
"""
|
|
|
|
vers_dict = {}
|
|
vers_str = ""
|
|
lines = _read_makefile()
|
|
version_re = re.compile(r"^APPVERSION_(?P<part>\w)\s?=\s?(?P<val>\d)", re.I)
|
|
for line in lines:
|
|
info = version_re.match(line)
|
|
if info:
|
|
dinfo = info.groupdict()
|
|
vers_dict[dinfo["part"]] = dinfo["val"]
|
|
try:
|
|
vers_str = f"{vers_dict['M']}.{vers_dict['N']}.{vers_dict['P']}"
|
|
except KeyError:
|
|
pass
|
|
assert version == vers_str
|
|
|
|
|
|
def verify_name(name: str) -> None:
|
|
"""Verify the app name, based on defines in Makefile
|
|
|
|
Args:
|
|
name (str): Name to be checked
|
|
"""
|
|
|
|
name_str = ""
|
|
lines = _read_makefile()
|
|
name_re = re.compile(r"^APPNAME\s?=\s?(?P<val>\w+)", re.I)
|
|
for line in lines:
|
|
info = name_re.match(line)
|
|
if info:
|
|
dinfo = info.groupdict()
|
|
name_str = dinfo["val"]
|
|
assert name == name_str
|
|
|
|
|
|
def decode_tlv(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 = (tlv[0] << 8) | tlv[1]
|
|
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 = (tlv[o + 1] << 8) | tlv[o + 2]
|
|
o += 3
|
|
else:
|
|
o += 1
|
|
v = tlv[o:o + l]
|
|
tags[t] = v
|
|
tlv = tlv[o + l:]
|
|
return tags
|
|
|
|
|
|
def _read_makefile() -> List[str]:
|
|
"""Read lines from the parent Makefile """
|
|
|
|
parent = Path(ROOT_SCREENSHOT_PATH).parent.resolve()
|
|
makefile = f"{parent}/Makefile"
|
|
print(f"Analyzing {makefile}...")
|
|
with open(makefile, "r", encoding="utf-8") as f_p:
|
|
lines = f_p.readlines()
|
|
|
|
return lines
|
|
|
|
|
|
def _get_pub_key(client: CommandSender, key_tag: DataObject) -> dict:
|
|
"""Read the Public Key parameters
|
|
|
|
Args:
|
|
client (CommandSender): Application object
|
|
key_tag (DataObject): Tag identifying the key to read
|
|
|
|
Returns:
|
|
Dictionary with key parameters
|
|
"""
|
|
|
|
rapdu = client.read_key(key_tag)
|
|
assert rapdu.status == Errors.SW_OK
|
|
|
|
# Extract Pub key parameters
|
|
tags = decode_tlv(rapdu.data)
|
|
return decode_tlv(tags[DataObject.DO_PUB_KEY])
|