Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding embedded application ELF file metadata parser #24

Merged
merged 2 commits into from
Mar 26, 2024
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [0.6.0] - 2024-03-26

### Added

- ledger-binary: Adding an utilitary to parse embedded application ELF file metadatas.

### Changed

- Renamed 'Europa' with official product name 'Flex'


## [0.5.0] - 2024-03-11

### Added
Expand Down
44 changes: 44 additions & 0 deletions doc/binary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Binary parser

Ledger embedded application ELF file contains metadata injected during the compilation. These
information are used by tools like [Speculos](https://speculos.ledger.com) to ease the manipulation
of the binaries (for instance: no need to explicit the device type, the SDK version, etc.).

For easier integration in Python code, `Ledgered` integrates a small binary parser to extract these
data. This code is the `ledgered.binary` library, and `Ledgered` also provides a CLI entrypoint
(`ledger-binary`).

## `ledger-binary` utilitary

### Example

Let's suppose we're compiling of the [Boilerplate application](https://github.com/LedgerHQ/app-boilerplate)
for Stax:

```bash
bash-5.1# make -j BOLOS_SDK=$STAX_SDK
```

The resulting ELF file is stored at (relative path) `./build/stax/bin/app.elf`. Let's use
`ledger-binary` to inspect this file:

```bash
$ ledger-binary build/stax/bin/app.elf
api_level 15
app_name Boilerplate
app_version 2.1.0
sdk_graphics bagl
sdk_hash a23bad84cbf39a5071644d2191b177191c089b23
sdk_name ledger-secure-sdk
sdk_version v15.1.0
target stax
target_id 0x33200004
target_name TARGET_STAX
```

It is also possible to ask for a JSON-like output:

```bash
$ ledger-binary build/stax/bin/app.elf -j
{'api_level': '15', 'app_name': 'Boilerplate', 'app_version': '2.1.0', 'sdk_graphics': 'bagl', 'sdk_hash': 'a23bad84cbf39a5071644d2191b177191c089b23', 'sdk_name': 'ledger-secure-sdk', 'sdk_version': 'v15.1.0', 'target': 'stax', 'target_id': '0x33200004', 'target_name': 'TARGET_STAX'}
```
2 changes: 1 addition & 1 deletion doc/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This manifest contains application metadata such as build directory, compatible
and is used by several tools to know how to build or test the application.

The `ledgered.utils.manifest` library is used to parse and manipulate application manifests
in Python code. `Ledgered` also provides a cli entrypoint (`ledger-manifest`) to parse, extract
in Python code. `Ledgered` also provides a CLI entrypoint (`ledger-manifest`) to parse, extract
and check information from manifests.

## Manifest content
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dynamic = [ "version" ]
requires-python = ">=3.7"
dependencies = [
"toml",
"pyelftools",
]

[project.optional-dependencies]
Expand All @@ -41,6 +42,7 @@ Home = "https://github.com/LedgerHQ/ledgered"

[project.scripts]
ledger-manifest = "ledgered.manifest.cli:main"
ledger-binary = "ledgered.binary:main"

[tool.setuptools_scm]
write_to = "src/ledgered/__version__.py"
Expand Down
78 changes: 78 additions & 0 deletions src/ledgered/binary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import logging
from argparse import ArgumentParser
from dataclasses import asdict, dataclass
from elftools.elf.elffile import ELFFile
from pathlib import Path
from typing import Optional, Union

from ledgered.serializers import Jsonable

LEDGER_PREFIX = "ledger."
DEFAULT_GRAPHICS = "bagl"


@dataclass
class Sections(Jsonable):
api_level: Optional[str] = None
app_name: Optional[str] = None
app_version: Optional[str] = None
sdk_graphics: str = DEFAULT_GRAPHICS
sdk_hash: Optional[str] = None
sdk_name: Optional[str] = None
sdk_version: Optional[str] = None
target: Optional[str] = None
target_id: Optional[str] = None
target_name: Optional[str] = None

def __str__(self) -> str:
return "\n".join(f"{key} {value}" for key, value in sorted(asdict(self).items()))


class LedgerBinaryApp:

def __init__(self, binary_path: Union[str, Path]):
if isinstance(binary_path, str):
binary_path = Path(binary_path)
self._path = binary_path = binary_path.resolve()
logging.info("Parsing binary '%s'", self._path)
with self._path.open("rb") as filee:
sections = {
s.name.replace(LEDGER_PREFIX, ""): s.data().decode()
for s in ELFFile(filee).iter_sections() if LEDGER_PREFIX in s.name
}
self._sections = Sections(**sections)

@property
def sections(self) -> Sections:
return self._sections


def set_parser() -> ArgumentParser:
parser = ArgumentParser(prog="ledger-binary",
description="Utilitary to parse Ledger embedded application ELF file "
"and output metadatas")
parser.add_argument('-v', '--verbose', action='count', default=0)
parser.add_argument("binary", type=Path, help="The ledger embedded application ELF file")
parser.add_argument("-j",
"--json",
required=False,
action="store_true",
help="outputs as JSON rather than text")
return parser


def main() -> None:
args = set_parser().parse_args()
assert args.binary.is_file(), f"'{args.binary.resolve()}' does not appear to be a file."

# verbosity
if args.verbose == 1:
logging.root.setLevel(logging.INFO)
elif args.verbose > 1:
logging.root.setLevel(logging.DEBUG)

app = LedgerBinaryApp(args.binary)
if args.json:
print(app.sections.json)
else:
print(app.sections)
2 changes: 1 addition & 1 deletion src/ledgered/manifest/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from pathlib import Path
from typing import Iterable, Union

from ledgered.serializers import Jsonable, JsonSet
from .constants import EXISTING_DEVICES
from .types import Jsonable, JsonSet


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion src/ledgered/manifest/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
EXISTING_DEVICES = ["nanos", "nanox", "nanos+", "stax", "europa"]
EXISTING_DEVICES = ["nanos", "nanox", "nanos+", "stax", "flex"]
MANIFEST_FILE_NAME = "ledger_app.toml"
DEFAULT_USE_CASE = "default"
2 changes: 1 addition & 1 deletion src/ledgered/manifest/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from pathlib import Path
from typing import Dict, IO, Optional, Union

from ledgered.serializers import Jsonable
from .app import AppConfig
from .constants import MANIFEST_FILE_NAME
from .tests import TestsConfig
from .types import Jsonable
from .use_cases import UseCasesConfig


Expand Down
2 changes: 1 addition & 1 deletion src/ledgered/manifest/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from typing import Dict, List, Optional, Union
from urllib.parse import urlparse

from ledgered.serializers import Jsonable, JsonDict, JsonSet
from .constants import DEFAULT_USE_CASE
from .errors import MissingField
from .types import Jsonable, JsonDict, JsonSet
from .utils import getLogger

APPLICATION_DIRECTORY_KEY = "application_directory"
Expand Down
2 changes: 1 addition & 1 deletion src/ledgered/manifest/use_cases.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from dataclasses import dataclass
from typing import Dict, Optional

from ledgered.serializers import Jsonable, JsonDict
from .constants import DEFAULT_USE_CASE
from .types import Jsonable, JsonDict


@dataclass
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions tests/_data/ledger_app.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[app]
sdk = "Rust"
build_directory = ""
devices = ["nanos", "Stax", "EUROPA"]
devices = ["nanos", "Stax", "FLEX"]

[tests]
unit_directory = "unit"
pytest_directory = "pytest"
pytest_directory = "pytest"
4 changes: 2 additions & 2 deletions tests/unit/manifest/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class TestManifest(TestCase):

def check_ledger_app_toml(self, manifest: Manifest) -> None:
self.assertEqual(manifest.app.sdk, "rust")
self.assertEqual(manifest.app.devices, {"nanos", "stax", "europa"})
self.assertEqual(manifest.app.devices, {"nanos", "stax", "flex"})
self.assertEqual(manifest.app.build_directory, Path(""))
self.assertTrue(manifest.app.is_rust)
self.assertFalse(manifest.app.is_c)
Expand All @@ -19,7 +19,7 @@ def check_ledger_app_toml(self, manifest: Manifest) -> None:
self.assertEqual(manifest.tests.pytest_directory, Path("pytest"))

def test___init__ok(self):
app = {"sdk": "rust", "devices": ["NANOS", "stAX", "europa"], "build_directory": ""}
app = {"sdk": "rust", "devices": ["NANOS", "stAX", "flex"], "build_directory": ""}
tests = {"unit_directory": "unit", "pytest_directory": "pytest"}
self.check_ledger_app_toml(Manifest(app, tests))

Expand Down
77 changes: 77 additions & 0 deletions tests/unit/test_binary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from dataclasses import dataclass
from unittest import TestCase
from unittest.mock import MagicMock, patch
from pathlib import Path
from typing import Any

from ledgered import binary as B


class TestSections(TestCase):

def setUp(self):
self.inputs = {
"api_level": "api_level",
"app_name": "app_name",
"app_version": "app_version",
"sdk_graphics": "sdk_graphics",
"sdk_hash": "sdk_hash",
"sdk_name": "sdk_name",
"sdk_version": "sdk_version",
"target": "target",
"target_id": "target_id",
"target_name": "target_name"
}

def test___init__empty(self):
sections = B.Sections()
self.assertIsNone(sections.api_level)
self.assertIsNone(sections.app_name)
self.assertIsNone(sections.app_version)
self.assertEqual(sections.sdk_graphics, B.DEFAULT_GRAPHICS)
self.assertIsNone(sections.sdk_hash)
self.assertIsNone(sections.sdk_name)
self.assertIsNone(sections.sdk_version)
self.assertIsNone(sections.target)
self.assertIsNone(sections.target_id)
self.assertIsNone(sections.target_name)

def test___str__(self):
sections = B.Sections(**self.inputs)
self.assertEqual("\n".join(f"{k} {v}"
for k,v in sorted(self.inputs.items())),
str(sections))

def test_json(self):
sections = B.Sections(**self.inputs)
self.assertDictEqual(self.inputs, sections.json)


@dataclass
class Section:
name: str
_data: Any
def data(self) -> Any:
return self._data


class TestLedgerBinaryApp(TestCase):

def test___init__(self):
path = Path("/dev/urandom")
api_level, sdk_hash = "something", "some hash"
expected = B.Sections(api_level=api_level, sdk_hash=sdk_hash)
with patch("ledgered.binary.ELFFile") as elfmock:
elfmock().iter_sections.return_value = [
Section("unused", 1),
Section("ledger.api_level", api_level.encode()),
Section("ledger.sdk_hash", sdk_hash.encode()),
Section("still not used", b"some data")
]
bin = B.LedgerBinaryApp(path)
self.assertEqual(bin.sections, expected)

def test___init__from_str(self):
path = "/dev/urandom"
with patch("ledgered.binary.ELFFile") as elfmock:
B.LedgerBinaryApp(path)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from unittest import TestCase

from ledgered.manifest.types import Jsonable, JsonList, JsonSet, JsonDict
from ledgered.serializers import Jsonable, JsonList, JsonSet, JsonDict


@dataclass
Expand Down