Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ __pycache__
.pytest_cache
definitions-cache.json
definitions-latest
definitions-latest-v*
*.log
definitions.tar.xz
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ This script will automatically create a commit with these changes.
To prevent incorrect/malicious definitions from being supplied to `Trezor`, they need to be signed before using them.

Signing has the following steps:
- run `python cli.py computed-merkle-root` to get the `merkle_root` computed from the definitions data rather than trusting the value stored in `definitions-latest-metadata-v1.json::merkle_root`
- run `python cli.py computed-merkle-root --version <version>` to get the `merkle_root` computed from the definitions data rather than trusting the value stored in `definitions-latest-metadata-v<version>.json::merkle_root`
- sign it with appropriate keys (outside of definitions repo)
- get the signature and provide it as an argument to `do_sign.sh`, e.g. `./do_sign.sh abcd...`
- get the signature and provide it as an argument to `do_sign.sh`, e.g. `./do_sign.sh 2 abcd...` (version first, signature second)
- the results should look something like this signing commit - https://github.com/trezor/definitions/commit/42d3093e83c85dade59af92a37fb3c33d3b047eb
- `definitions.tar.gz` file should also be created, containing signed definitions, ready for deployment

Metadata (merkle root, signature, format version) are version-specific and live in `definitions-latest-metadata-v<version>.json`, separate from the coin data in `definitions-latest.json`.
Metadata (merkle root, signature, format version) are version-specific and live in `definitions-latest-metadata-v<version>.json`, separate from the coin data in `definitions-latest.json`. Every active version therefore has its own Merkle root and signature, and is signed separately. Version 2 differs from version 1 in the payload header (`trzd2` instead of `trzd1`) and in requiring only one CoSi signature instead of two.
15 changes: 12 additions & 3 deletions definitions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,17 @@

DEFINITIONS_PATH = ROOT / "definitions-latest.json"
DISPLAY_FORMATS_LOG_PATH = ROOT / "definitions-latest.log"
GENERATED_DEFINITIONS_DIR = ROOT / "definitions-latest"

# Definitions format versions the tooling currently produces metadata for.
# Metadata (merkle root, signature) is version-specific, so one metadata file
# per active version is generated.
ACTIVE_VERSIONS: tuple[int, ...] = (1,)
# per active version is generated.
ACTIVE_VERSIONS: tuple[int, ...] = (1, 2)

# Number of CoSi signatures required by definition format version.
SIGNATURES_REQUIRED: dict[int, int] = {
1: 2,
2: 1,
}

CURRENT_TIME = datetime.datetime.now(datetime.timezone.utc)
TIMESTAMP_FORMAT = "%d.%m.%Y %X%z"
Expand All @@ -59,6 +64,10 @@ def metadata_path(version: int) -> Path:
return ROOT / f"definitions-latest-metadata-v{version}.json"


def generated_definitions_dir(version: int) -> Path:
return ROOT / f"definitions-latest-v{version}"


def validate_version(version: int) -> int:
if version not in ACTIVE_VERSIONS:
supported = ", ".join(str(v) for v in ACTIVE_VERSIONS)
Expand Down
12 changes: 8 additions & 4 deletions definitions/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from trezorlib import cosi, definitions
from trezorlib.merkle_tree import MerkleTree

from .common import SIGNATURES_REQUIRED

HERE = Path(__file__).parent

PRIVATE_KEYS_DEV = [byte * 32 for byte in (b"\xdd", b"\xde", b"\xdf")]
Expand Down Expand Up @@ -38,23 +40,25 @@ def get_dev_public_key() -> bytes:
return cosi.combine_keys([cosi.pubkey_from_privkey(sk) for sk in PRIVATE_KEYS_DEV])


def _combine_public_key(sigmask: int) -> bytes:
def _combine_public_key(sigmask: int, version: int) -> bytes:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a docstring or a comment explaining this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have 3 public keys. sigmask signalizes which ones are used for signing, so for version 1, it can be e.g. 110 (first two chosen), or 101 (first and last), or 111 (all of them). For version 2, it can e.g. 100 (only 1st), 010 (only 2nd) etc. The added assert checks that the sigmask was correctly chosen.

selected_keys = [
k
for i, k in enumerate(definitions.DEFINITIONS_PUBLIC_KEYS)
if sigmask & (1 << i)
]
assert len(selected_keys) >= 2
assert len(selected_keys) >= SIGNATURES_REQUIRED[version]
return cosi.combine_keys(selected_keys)
Comment on lines +49 to 50


def verify_signature(signature: bytes, root_hash: bytes, dev: bool = False) -> None:
def verify_signature(
signature: bytes, root_hash: bytes, version: int, dev: bool = False
) -> None:
sigmask, signature = signature[0], signature[1:]
if dev:
assert sigmask == 0b111
public_key = get_dev_public_key()
else:
public_key = _combine_public_key(sigmask)
public_key = _combine_public_key(sigmask, version)

ed25519.checkvalid(signature, root_hash, public_key)

Expand Down
15 changes: 9 additions & 6 deletions definitions/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

from . import crypto
from .common import (
GENERATED_DEFINITIONS_DIR,
DefinitionsData,
generated_definitions_dir,
load_definitions_data,
resolve_default_version,
setup_logging,
Expand Down Expand Up @@ -163,8 +163,9 @@ def create_deploy_tar(src_dir: Path, out_file: Path) -> None:
"-o",
"--outdir",
type=click.Path(resolve_path=True, file_okay=False, writable=True, path_type=Path),
default=GENERATED_DEFINITIONS_DIR,
help="Output directory for generated definitions.",
default=None,
help="Output directory for generated definitions. "
"Defaults to definitions-latest-v<version>.",
)
@click.option("-d", "--dev-sign", is_flag=True, help="Sign with dev keys.")
@click.option(
Expand All @@ -176,7 +177,7 @@ def create_deploy_tar(src_dir: Path, out_file: Path) -> None:
)
@click.option("-v", "--verbose", is_flag=True, help="Display more info.")
def generate_definitions(
outdir: Path,
outdir: Path | None,
dev_sign: bool,
version: int | None,
verbose: bool,
Expand All @@ -189,10 +190,12 @@ def generate_definitions(
if version is None:
version = resolve_default_version()
validate_version(version)
if outdir is None:
outdir = generated_definitions_dir(version)
if (
outdir.is_dir()
and list(outdir.iterdir())
and outdir != GENERATED_DEFINITIONS_DIR
and outdir != generated_definitions_dir(version)
and not click.confirm(
f"Directory {outdir} is not empty. Contents will be DELETED. Continue?"
)
Expand Down Expand Up @@ -239,7 +242,7 @@ def generate_definitions(
)

try:
crypto.verify_signature(signature_bytes, root_hash, dev=dev_sign)
crypto.verify_signature(signature_bytes, root_hash, version, dev=dev_sign)
except InvalidSignature:
raise click.ClickException(
"Signature is not valid for computed "
Expand Down
2 changes: 1 addition & 1 deletion definitions/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def sign_definitions(

# Verify signature
try:
verify_signature(signature_bytes, root_hash)
verify_signature(signature_bytes, root_hash, version)
except InvalidSignature:
raise click.ClickException(
"Provided signature is not valid for current "
Expand Down
22 changes: 21 additions & 1 deletion definitions/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ def test_validate_version_accepts_active(version):


def test_validate_version_rejects_inactive():
inactive_version = max(common.ACTIVE_VERSIONS) + 1
with pytest.raises(click.ClickException, match="Unsupported definitions version"):
validate_version(2)
validate_version(inactive_version)


def test_resolve_default_version():
Expand Down Expand Up @@ -143,3 +144,22 @@ def test_store_metadata_writes_per_version_files(tmp_root):
assert metadata_path(version).is_file()
stored = json.loads(metadata_path(version).read_text())
assert stored["version"] == version


# ====== payload header (magic + version) ======


@pytest.mark.parametrize("version", common.ACTIVE_VERSIONS)
def test_payload_header_contains_magic_and_version(version):
from .ethereum.serialize import serialize_token

token = {
"address": "0x" + "ab" * 20,
"chain_id": 1,
"shortcut": "ABC",
"decimals": 18,
"name": "Test Token",
}
serialized = serialize_token(token, 1234567890, version)
assert serialized[:4] == b"trzd"
assert serialized[4:5] == str(version).encode("ascii")
27 changes: 27 additions & 0 deletions definitions/test_crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest

from .common import SIGNATURES_REQUIRED
from .crypto import _combine_public_key


@pytest.mark.parametrize("version", sorted(SIGNATURES_REQUIRED))
def test_combine_public_key_accepts_required_key_count(version):
sigmask = (1 << SIGNATURES_REQUIRED[version]) - 1
assert _combine_public_key(sigmask, version)


@pytest.mark.parametrize("version", sorted(SIGNATURES_REQUIRED))
def test_combine_public_key_rejects_too_few_keys(version):
sigmask = (1 << (SIGNATURES_REQUIRED[version] - 1)) - 1
with pytest.raises(AssertionError):
_combine_public_key(sigmask, version)
Comment on lines +1 to +17


def test_v1_requires_two_keys_v2_one_key():
# one key set in sigmask
assert _combine_public_key(0b001, 2)
with pytest.raises(AssertionError):
_combine_public_key(0b001, 1)
# two keys set in sigmask work for both
assert _combine_public_key(0b011, 1)
assert _combine_public_key(0b011, 2)
17 changes: 10 additions & 7 deletions do_sign.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

set -e # Exit on any error

if [ -z "$1" ]; then
echo "Usage: $0 <signature hex>"
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <version> <signature hex>"
exit 1
fi

VERSION="$1"
SIGNATURE="$2"

function are_there_git_changes {
! git diff-index --quiet HEAD
}
Expand All @@ -17,16 +20,16 @@ if are_there_git_changes; then
exit 1
fi

MERKLE_ROOT=$(python cli.py current-merkle-root)
MERKLE_ROOT=$(python cli.py current-merkle-root --version "$VERSION")

python cli.py sign --verify "$1"
git add definitions-latest.json definitions-latest-metadata-v1.json
git commit -m "Sign definitions for $MERKLE_ROOT"
python cli.py sign --verify --version "$VERSION" "$SIGNATURE"
git add definitions-latest.json "definitions-latest-metadata-v${VERSION}.json"
git commit -m "Sign definitions v${VERSION} for $MERKLE_ROOT"

# update the signed branch
git branch --force signed HEAD

python cli.py generate
python cli.py generate --version "$VERSION"

echo "Don't forget to push main & signed branches:"
echo " git push origin main"
Expand Down
13 changes: 10 additions & 3 deletions do_update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ for arg in "$@"; do
esac
done

# Definitions format versions to generate (kept in sync with definitions/common.py)
ACTIVE_VERSIONS=( $(python -c "from definitions.common import ACTIVE_VERSIONS; print(' '.join(map(str, ACTIVE_VERSIONS)))") )

# Fast local iteration on the ERC-7730 registry: refresh only the display
# formats and rebuild the signed tarball. Skips the git-clean gate, the
# submodule update, the CoinGecko-heavy coins details, and the auto-commit.
if [[ -n "$ERC7730_ONLY" ]]; then
python cli.py download -v --erc7730-only $SHOW_ADDED
python cli.py generate --dev-sign
for VERSION in "${ACTIVE_VERSIONS[@]}"; do
python cli.py generate --dev-sign --version "$VERSION"
done
exit 0
fi

Expand All @@ -38,8 +43,10 @@ git submodule update -- "ethereum/clear-signing-erc7730-registry"
# Download definitions
python cli.py download -v --sleep-duration 2.5 $SHOW_ADDED

# Sign them with dev private keys
python cli.py generate --dev-sign
# Sign them with dev private keys (per format version)
for VERSION in "${ACTIVE_VERSIONS[@]}"; do
python cli.py generate --dev-sign --version "$VERSION"
done

# Generate coins details
python coins_details/coins_details.py
Expand Down