Skip to content

Commit

Permalink
Merge pull request #27 from lpascal-ledger/tests/unit
Browse files Browse the repository at this point in the history
Bugfix + unit testing framework
  • Loading branch information
jibeee authored Jun 21, 2022
2 parents 664bafe + 2a36bda commit a985c86
Show file tree
Hide file tree
Showing 12 changed files with 362 additions and 8 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Build and test LedgerWallet

on:
push:
branches:
- master
- develop
pull_request:
branches:
- develop
- master

jobs:
build_install_test:
name: Build, install and test LedgerWallet
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v2
- name: Install (with dependencies)
run: pip install .
- name: Install test dependencies
run: pip install nose coverage
- name: Running unit tests
run: nosetests --with-coverage --cover-package ledgerwallet --cover-xml tests/unit/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
name: codecov-ledgerwallet
10 changes: 5 additions & 5 deletions ledgerwallet/crypto/ecc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self, pubkey: bytes):

self.vk = VerifyingKey.from_string(pubkey[1:], SECP256k1, validate_point=True)

def serialize(self, compressed=True):
def serialize(self, compressed: bool = True) -> bytes:
if compressed:
raise NotImplementedError
return b"\x04" + self.vk.to_string()
Expand All @@ -31,20 +31,20 @@ def verify(


class PrivateKey(object):
def __init__(self, sk=None):
def __init__(self, sk: bytes = None):
if sk is None:
self.sk = SigningKey.generate(SECP256k1)
else:
self.sk = SigningKey.from_string(sk, SECP256k1)

@property
def pubkey(self):
def pubkey(self) -> PublicKey:
return PublicKey(b"\x04" + self.sk.get_verifying_key().to_string())

def serialize(self):
def serialize(self) -> bytes:
return self.sk.to_string()

def sign(self, msg, raw=False, hashfunc=hashlib.sha256):
def sign(self, msg, raw=False, hashfunc=hashlib.sha256) -> bytes:
if not raw:
signature = self.sk.sign(
msg, hashfunc=hashfunc, sigencode=ecdsa.util.sigencode_der_canonize
Expand Down
4 changes: 2 additions & 2 deletions ledgerwallet/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ def __init__(self, filename):
assert "targetId" in self.json and "binary" in self.json

@property
def app_name(self):
def app_name(self) -> str:
return self.json["name"]

@property
def data_size(self):
def data_size(self) -> int:
if "dataSize" not in self.json:
return 0
else:
Expand Down
2 changes: 1 addition & 1 deletion ledgerwallet/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def _parse(self, stream, context, path):
encoded_len = stream_read(stream, num_bytes, path)
num = 0
for len_byte in encoded_len:
num = num << 8 + len_byte
num = (num << 8) + len_byte
return num

def _build(self, obj, stream, context, path):
Expand Down
Empty file added tests/unit/crypto/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions tests/unit/crypto/test_ecc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from unittest import TestCase

from ledgerwallet.crypto import ecc


RAW_PRIVATE = bytes.fromhex("c2cdf0a8b0a83b35ace53f097b5e6e6a0a1f2d40535eff1cf434f52a43d59d8f")
RAW_PUBLIC = bytes.fromhex("6fcc37ea5e9e09fec6c83e5fbd7a745e3eee81d16ebd861c9e66f55518c19798" +
"4e9f113c07f875691df8afc1029496fc4cb9509b39dcd38f251a83359cc8b4f7")


class PrivateKeyTest(TestCase):
def setUp(self):
self.key = ecc.PrivateKey(RAW_PRIVATE)

def test_pubkey(self):
self.assertEqual(self.key.pubkey.serialize(compressed=False),
bytes([0x04]) + RAW_PUBLIC)

def test_serialize(self):
self.assertEqual(self.key.serialize(), RAW_PRIVATE)

def test_sign(self):
blob = b'someblobofdata'
signature = self.key.sign(blob)
self.assertTrue(self.key.pubkey.verify(blob, signature))


class PublickKeyTest(TestCase):
def setUp(self):
self.public = bytes([0x04]) + RAW_PUBLIC
self.key = ecc.PublicKey(self.public)

def test___init__fail(self):
with self.assertRaises(ValueError):
ecc.PublicKey(b"") # not 65-bytes long
with self.assertRaises(ValueError):
self.public = bytes([0x00]) + self.public[1:]
ecc.PublicKey(self.public) # must start with 0x04

def test_serialize_uncompressed(self):
self.assertEqual(self.key.serialize(compressed=False), self.public)

def test_serialize_compressed(self):
with self.assertRaises(NotImplementedError):
self.key.serialize()
1 change: 1 addition & 0 deletions tests/unit/data/empty_manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
13 changes: 13 additions & 0 deletions tests/unit/data/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"targetId": "1234",
"binary": "some binary",
"name": "some name",
"flags": "9999",
"dataSize": 42,
"version": "2.9.4-debug",
"icon": "pied.jpeg",
"derivationPath": {
"curves": ["secp256k1", "ed25519"],
"paths": ["44'/0'/255", "44'/0'/0'/1/400"]
}
}
4 changes: 4 additions & 0 deletions tests/unit/data/minimal_manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"targetId": "1234",
"binary": "some binary"
}
68 changes: 68 additions & 0 deletions tests/unit/test_manifest_Manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import json
from pathlib import Path
from unittest import TestCase
from unittest.mock import patch

from ledgerwallet.manifest import AppManifest


class ManifestTest(TestCase):
def setUp(self):
self.data_dir = Path(__file__).parent / "data"
self.json_path = self.data_dir / "manifest.json"
self.assertTrue(self.json_path.is_file())
self.manifest = AppManifest(str(self.json_path))

def test___init__(self):
self.assertEqual(self.manifest.path, str(self.json_path.parent))
with self.json_path.open() as filee:
self.assertEqual(self.manifest.json, json.load(filee))

def test___init__error(self):
with self.assertRaises(AssertionError):
AppManifest(str(self.data_dir / "empty_manifest.json")) # missing key
with self.assertRaises(FileNotFoundError):
AppManifest("/this/path/should/not/exist") # file does not exist (right?)

def test_app_name(self):
self.assertEqual(self.manifest.app_name, "some name")

def test_data_size(self):
self.assertEqual(self.manifest.data_size, 42)

def test_get_application_flags(self):
self.assertEqual(self.manifest.get_application_flags(), 0x9999)

def test_get_binary(self):
self.assertEqual(self.manifest.get_binary(), str(self.data_dir / "some binary"))

def test_get_target_id(self):
self.assertEqual(self.manifest.get_target_id(), 0x1234)

def test_properties_with_minimal_manifest(self):
manifest = AppManifest(str(self.data_dir / "minimal_manifest.json"))
with self.assertRaises(KeyError):
manifest.app_name
self.assertEqual(manifest.data_size, 0)
self.assertEqual(manifest.get_application_flags(), 0)
self.assertEqual(manifest.get_binary(), str(self.data_dir / "some binary"))
self.assertEqual(manifest.get_target_id(), 0x1234)
self.assertEqual(manifest.serialize_parameters(), b"")

def test_serialize_parameters(self):
expected = bytes.fromhex(
"01" + # BolosTag 'AppName'
"09" + "736f6d65206e616d65" + # "some name"
"02" + # BolosTag 'Version'
"0b" + "322e392e342d6465627567" + # 2.9.4-debug
"03" + # BolosTag 'Icon'
"04" + "01020304" + # mocked icon
"04" + # BolosTag 'DerivationPath'
"23" + # 35: following size
"05" + # secp256k1 (1) + ed25519 (4)
"03" + "8000002c80000000000000ff" + # "44'/0'/255"
"05" + "8000002c80000000800000000000000100000190" # "44'/0'/0'/1/400"
)
with patch('ledgerwallet.manifest.icon_from_file', lambda x: b'\x01\x02\x03\x04'):
result = self.manifest.serialize_parameters()
self.assertEqual(result, expected)
167 changes: 167 additions & 0 deletions tests/unit/test_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from unittest import TestCase

from construct import StreamError

from ledgerwallet.params import Asn1Length, Bip32Path, DerivationPath, \
Dependency, Dependencies


class Asn1LengthTest(TestCase):
sample = {
0: bytes.fromhex("00"),
4: bytes.fromhex("04"),
127: bytes.fromhex("7f"),
128: bytes.fromhex("8180"),
160: bytes.fromhex("81a0"),
255: bytes.fromhex("81ff"),
256: bytes.fromhex("820100")
}

def test_parse(self):
for k, v in self.sample.items():
self.assertEquals(Asn1Length.parse(v), k)

def test_build(self):
for k, v in self.sample.items():
self.assertEquals(Asn1Length.build(k), v)


class Bip32PathTest(TestCase):
sample = {
"1": bytes.fromhex("01 00000001"),
"1'": bytes.fromhex("01 80000001"),
"0'/0": bytes.fromhex("02 80000000 00000000"),
"44'/91223'/2": bytes.fromhex('03 8000002c 80016457 00000002'),
"44'/0'/0'/1/400": bytes.fromhex("05 8000002c 80000000 80000000 00000001 00000190"),
}

def test_parse(self):
for k, v in self.sample.items():
self.assertEquals(Bip32Path.parse(v), k)

def test_parse_empty(self):
# not in sample as not the parse/build behavior is not symetrical
self.assertEqual(Bip32Path.parse(bytes.fromhex("00")), str())

def test_build(self):
for k, v in self.sample.items():
self.assertEquals(Bip32Path.build(k), v)

def test_parse_error(self):
errors = [
b"", # empty string is not parsable
bytes.fromhex("01"), # expecting 4 more bytes (Int32ub)
]
for error in errors:
with self.assertRaises(StreamError):
Bip32Path.parse(error)


class DerivationPathTest(TestCase):

def test_parse_empty(self):
result = DerivationPath.parse(bytes.fromhex("01 00"))
self.assertFalse(result.curve.secp256k1)
self.assertFalse(result.curve.prime256r1)
self.assertFalse(result.curve.ed25519)
self.assertFalse(result.curve.bls12381g1)
self.assertFalse(result.paths)

def test_parse_keys(self):
# [secp256k1, prime256r1, ed25519, bls12381g1]
keys = [
[False, False, False, False],
[True, False, False, False],
[False, True, False, False],
[True, True, False, False],
[False, False, True, False],
[True, False, True, False],
[False, True, True, False],
[True, True, True, False],
[False, False, False, True],
[True, False, False, True],
[False, True, False, True],
[True, True, False, True],
[False, False, True, True],
[True, False, True, True],
[False, True, True, True],
[True, True, True, True],
]
for i in range(8):
result = DerivationPath.parse(bytes([1, i]))
self.assertListEqual(
[result.curve.secp256k1, result.curve.prime256r1,
result.curve.ed25519, result.curve.bls12381g1],
keys[i]
)
# bls12381g1 store on 5th bit (16), not 4th (8)
# so there is a bit gap from 8 to 16
for i in range(16, 24):
result = DerivationPath.parse(bytes([1, i]))
self.assertListEqual(
[result.curve.secp256k1, result.curve.prime256r1,
result.curve.ed25519, result.curve.bls12381g1],
keys[i-8]
)

def test_parse_with_paths(self):
path1 = (bytes.fromhex("05 8000002c 80000000 80000000 00000001 00000190"),
"44'/0'/0'/1/400")
path2 = (bytes.fromhex("03 8000002c 80000000 000000ff"),
"44'/0'/255")
key = bytes.fromhex("11")
size = len(path1[0]) + len(path2[0]) + len(key)
result = DerivationPath.parse(bytes([size]) + key + path1[0] + path2[0])
self.assertTrue(result.curve.secp256k1)
self.assertFalse(result.curve.prime256r1)
self.assertFalse(result.curve.ed25519)
self.assertTrue(result.curve.bls12381g1)
self.assertEqual(result.paths, [path1[1], path2[1]])

def test_parse_error(self):
errors = [
b"", # empty string is not parsable
bytes.fromhex("01"), # expecting more bytes (curve)
]
for error in errors:
with self.assertRaises(StreamError):
DerivationPath.parse(error)


class DependencyTest(TestCase):

def test_parse_no_version(self):
name = 'name'
asn1_name = bytes([len(name)]) + name.encode()
result = Dependency.parse(bytes([len(asn1_name)]) + asn1_name)
self.assertEqual(result.name, name)
self.assertIsNone(result.version)

def test_parse_with_version(self):
name = "name"
version = "1.0.1"
asn1_name = bytes([len(name)]) + name.encode()
asn1_version = bytes([len(version)]) + version.encode()
result = Dependency.parse(bytes([len(asn1_name + asn1_version)])
+ asn1_name
+ asn1_version)
self.assertEqual(result.name, name)
self.assertEqual(result.version, version)


class DependenciesTest(TestCase):

def test_parse(self):
name1 = "name1"
version = "1.0.1"
asn1_name1 = bytes([len(name1)]) + name1.encode()
asn1_version = bytes([len(version)]) + version.encode()
dep1 = bytes([len(asn1_name1 + asn1_version)]) + asn1_name1 + asn1_version
name2 = "name2"
asn1_name2 = bytes([len(name2)]) + name2.encode()
dep2 = bytes([len(asn1_name2)]) + asn1_name2
result = Dependencies.parse(bytes([len(dep1 + dep2)]) + dep1 + dep2)
self.assertEqual(result[0].name, name1)
self.assertEqual(result[0].version, version)
self.assertEqual(result[1].name, name2)
self.assertIsNone(result[1].version)
Loading

0 comments on commit a985c86

Please sign in to comment.