Skip to content

Commit

Permalink
Add helper script to generate dynamic network configuration APDU
Browse files Browse the repository at this point in the history
  • Loading branch information
cedelavergne-ledger committed Sep 18, 2024
1 parent ca8b882 commit 657057d
Showing 1 changed file with 272 additions and 0 deletions.
272 changes: 272 additions & 0 deletions tools/gen_dynamic_network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
#!/usr/bin/env python3

import os
import subprocess
import sys
import struct
import logging
import re
from enum import IntEnum
from typing import List, Optional
from pathlib import Path
from binascii import crc32
import argparse

# Resolve the parent directory and append to sys.path
parent = Path(__file__).parent.parent.resolve()
sys.path.append(f"{parent}/client/src/ledger_app_clients/ethereum")
# Import the required module
from tlv import format_tlv # type: ignore

# Retrieve the SDK path from the environment variable
sdk_path = os.getenv('BOLOS_SDK')
if sdk_path:
# Import the library dynamically
sys.path.append(f"{sdk_path}/lib_nbgl/tools")
from icon2glyph import open_image, compute_app_icon_data # type: ignore
else:
print("Environment variable BOLOS_SDK is not set")
sys.exit(1)


class NetworkInfoTag(IntEnum):
STRUCTURE_TYPE = 0x01
CHAIN_ID = 0x11
NETWORK_NAME = 0x12
NETWORK_TICKER = 0x13
NETWORK_ICON = 0x14
SIGNATURE = 0x20
CRC32 = 0x21


class P1Type(IntEnum):
SIGN_FIRST_CHUNK = 0x00
SIGN_SUBSQT_CHUNK = 0x80


PROVIDE_NETWORK_INFORMATION = 0x30
CLA = 0xE0

logger = logging.getLogger(__name__)


# ===============================================================================
# Parameters
# ===============================================================================
def init_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Generate hex string for network icon, in NBGL format.")
parser.add_argument("--icon", "-i", help="Input icon to process.")
parser.add_argument("--name", "-n", required=True, help="Network name")
parser.add_argument("--ticker", "-t", required=True, help="Network ticker")
parser.add_argument("--chainid", "-c", type=int, required=True, help="Network chain_id")
parser.add_argument("--verbose", "-v", action='store_true', help="Verbose mode")
return parser


# ===============================================================================
# Logging
# ===============================================================================
def set_logging(verbose: bool = False) -> None:
if verbose:
logger.setLevel(level=logging.DEBUG)
else:
logger.setLevel(level=logging.INFO)
logger.handlers.clear()
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
logger.addHandler(handler)


# ===============================================================================
# Check icon - Extracted from ledger-app-workflow/scripts/check_icon.sh
# ===============================================================================
def check_glyph(file: str) -> bool:
extension = os.path.splitext(file)[1][1:]
if extension not in ["gif", "bmp", "png"]:
logger.error(f"Glyph extension should be '.gif', '.bmp', or '.png', not '.{extension}'")
return False

try:
content = subprocess.check_output(["identify", "-verbose", file], text=True)
except subprocess.CalledProcessError as e:
logger.error(f"Failed to identify file: {e}")
return False

if "Alpha" in content:
logger.error("Glyph should have no alpha channel")
return False

x = re.search(r"Colors: (.*)", content)
if x is None:
logger.error("Glyph should have the colors defined")
return False
nb_colors = int(x.group(1))
if "Type: Bilevel" in content:
logger.debug("Monochrome image type")

if nb_colors != 2:
logger.error("Glyph should have only 2 colors")
return False

if re.search(r"0.*0.*0.*black", content) is None:
logger.error("Glyph should have the black color defined")
return False

if re.search(r"255.*255.*255.*white", content) is None:
logger.error("Glyph should have the white color defined")
return False

if not any(depth in content for depth in ["Depth: 1-bit", "Depth: 8/1-bit"]):
logger.error("Glyph should have 1 bit depth")
return False

elif "Type: Grayscale" in content:
logger.debug("Grayscale image type")

if nb_colors > 16:
logger.error(f"4bpp glyphs can't have more than 16 colors, {nb_colors} found")
return False

if not any(depth in content for depth in ["Depth: 8-bit", "Depth: 8/8-bit"]):
logger.error("Glyph should have 8 bits depth")
return False

else:
logger.error("Glyph should be Monochrome or Grayscale")
return False

logger.info(f"Glyph '{file}' is compliant")
return True


# ===============================================================================
# Prepare APDU - Extracted from python client
# ===============================================================================
def serialize(p1: int, cdata: bytes) -> bytes:
"""
Serializes the provided network information into a specific byte format.
Args:
p1 (int): A parameter to be included in the header.
cdata (bytes): The network information data to be serialized.
Returns:
bytes: The serialized byte sequence combining the header and the data.
"""

# Initialize a bytearray to construct the header
header = bytearray()
header.append(CLA)
header.append(PROVIDE_NETWORK_INFORMATION)
header.append(p1)
header.append(0x00) # P2
header.append(len(cdata))
# Return the concatenation of the header and cdata
return header + cdata


def generate_tlv_payload(name: str,
ticker: str,
chain_id: int,
icon: Optional[bytes] = None) -> bytes:
"""
Generates a TLV (Type-Length-Value) payload for the given network information.
Args:
name (str): The name of the blockchain network.
ticker (str): The ticker symbol of the blockchain network.
chain_id (int): The unique identifier for the blockchain network.
icon (Optional[bytes]): Optional icon data in bytes for the blockchain network.
Returns:
bytes: The generated TLV payload.
"""

tlv_payload: bytes = format_tlv(NetworkInfoTag.STRUCTURE_TYPE, 1)
tlv_payload += format_tlv(NetworkInfoTag.CHAIN_ID, chain_id.to_bytes(8, 'big'))
tlv_payload += format_tlv(NetworkInfoTag.NETWORK_NAME, name.encode('utf-8'))
tlv_payload += format_tlv(NetworkInfoTag.NETWORK_TICKER, ticker.encode('utf-8'))
if icon:
# Network Icon
tlv_payload += bytes([NetworkInfoTag.NETWORK_ICON])
tlv_payload += len(icon).to_bytes(2, 'big')
tlv_payload += icon

# Append the data CRC32
tlv_payload += format_tlv(NetworkInfoTag.CRC32, crc32(tlv_payload).to_bytes(4, 'big'))

# Return the constructed TLV payload as bytes
return tlv_payload


def prepare_network_information(name: str,
ticker: str,
chain_id: int,
icon: Optional[bytes] = None) -> List[bytes]:
"""
Prepares network information for a given blockchain network.
Args:
name (str): The name of the blockchain network.
ticker (str): The ticker symbol of the blockchain network.
chain_id (int): The unique identifier for the blockchain network.
icon (Optional[bytes]): Optional icon data in bytes for the blockchain network.
Returns:
List[bytes]: A list of byte chunks representing the network information.
"""

# Initialize an empty list to store the byte chunks
chunks: List[bytes] = []
# Generate the TLV payload
tlv_payload = generate_tlv_payload(name, ticker, chain_id, icon)

payload = struct.pack(">H", len(tlv_payload))
payload += tlv_payload
p1 = P1Type.SIGN_FIRST_CHUNK
while payload:
chunks.append(serialize(p1, payload[:0xff]))
payload = payload[0xff:]
p1 = P1Type.SIGN_SUBSQT_CHUNK
return chunks


# ===============================================================================
# Main entry
# ===============================================================================
def main() -> None:
parser = init_parser()
args = parser.parse_args()

set_logging(args.verbose)

if args.icon:
if not os.access(args.icon, os.R_OK):
logger.error(f"Cannot read file {args.icon}")
sys.exit(1)

# Open image in luminance format
im, bpp = open_image(args.icon)
if im is None:
logger.error(f"Unable to access icon file {args.icon}")
sys.exit(1)

# Check icon
if not check_glyph(args.icon):
logger.error(f"Invalid icon file {args.icon}")
sys.exit(1)

# Prepare and print app icon data
_, image_data = compute_app_icon_data(False, im, bpp, False)
logger.debug(f"image_data={image_data.hex()}")
else:
image_data = None

chunks = prepare_network_information(args.name, args.ticker, args.chainid, image_data)
# Print each chunk with its index
for i, chunk in enumerate(chunks):
logger.info(f"Chunk {i}: {chunk.hex()}")


if __name__ == "__main__":
main()

0 comments on commit 657057d

Please sign in to comment.