Skip to content

Commit

Permalink
[tools/shoestring]: add 'import-harvesters' command
Browse files Browse the repository at this point in the history
 problem: there is no way to import a harvesters.dat file to a previously deployed node
solution: add functionality for inspecting and importing harvester.dat files
  • Loading branch information
Jaguar0625 committed Apr 19, 2024
1 parent 9bee751 commit b131142
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 42 deletions.
15 changes: 15 additions & 0 deletions tools/shoestring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ import-bootstrap --config CONFIG --bootstrap BOOTSTRAP
--bootstrap BOOTSTRAP path to bootstrap target directory
```

### import-harvesters

Imports harvesters from an existing harvesters.dat file.


```
import-harvesters --config CONFIG --in-harvesters IN_HARVESTERS --in-pem IN_PEM [--out-harvesters OUT_HARVESTERS] [--out-pem OUT_PEM]
--config CONFIG path to shoestring configuration file
--in-harvesters IN_HARVESTERS input harvesters.dat file that is encrypted with in-pem
--in-pem IN_PEM PEM file that can be used to decrypt in-harvesters
--out-harvesters OUT_HARVESTERS output harvesters.dat file that will be encrypted with out-pem
--out-pem OUT_PEM PEM file that can be used to encrypt out-harvesters
```

### pemtool

Generates a main private key PEM file that can be used by shoestring.
Expand Down
1 change: 1 addition & 0 deletions tools/shoestring/shoestring/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def parse_args(args):
register_subcommand(subparsers, 'announce-transaction', _('main-announce-transaction-help'))
register_subcommand(subparsers, 'health', _('main-health-help'))
register_subcommand(subparsers, 'import-bootstrap', _('main-import-bootstrap-help'))
register_subcommand(subparsers, 'import-harvesters', _('main-import-harvesters-help'))
register_subcommand(subparsers, 'init', _('main-init-help'))
register_subcommand(subparsers, 'min-cosignatures-count', _('main-min-cosignatures-count-help'))
register_subcommand(subparsers, 'pemtool', _('main-pemtool-help'))
Expand Down
104 changes: 104 additions & 0 deletions tools/shoestring/shoestring/commands/import_harvesters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import sys
from functools import partial
from pathlib import Path

from symbolchain.CryptoTypes import PrivateKey, PublicKey
from symbolchain.impl.CipherHelpers import decode_aes_gcm, encode_aes_gcm
from symbolchain.symbol.KeyPair import KeyPair
from symbolchain.symbol.SharedKey import SharedKey
from zenlog import log

from shoestring.internal.PemUtils import read_private_key_from_private_key_pem_file
from shoestring.internal.ShoestringConfiguration import parse_shoestring_configuration

HARVESTER_ENTRY_PAYLOAD_SIZE = sum([
PublicKey.SIZE, # ephemeral public key
16, # aes gcm tag
12, # aes gcm initialization vector
2 * PrivateKey.SIZE, # encrypted harvester signing private key | encrypted harvester vrf private key
])


def _private_key_to_address(network, private_key):
public_key = KeyPair(private_key).public_key
address = network.public_key_to_address(public_key)
return address


def _visit_harvesters(harvesters_filepath, encryption_key_pair, visit):
with open(harvesters_filepath, 'rb') as infile:
for harvester_entry_payload in iter(partial(infile.read, HARVESTER_ENTRY_PAYLOAD_SIZE), b''):
ephemeral_public_key = PublicKey(harvester_entry_payload[:PublicKey.SIZE])
encrypted_payload = harvester_entry_payload[PublicKey.SIZE:]

decrypted_payload = decode_aes_gcm(SharedKey, encryption_key_pair, ephemeral_public_key, encrypted_payload)

signing_private_key = PrivateKey(decrypted_payload[:PrivateKey.SIZE])
vrf_private_key = PrivateKey(decrypted_payload[PrivateKey.SIZE:])

visit(signing_private_key, vrf_private_key)


def print_all_harvester_addresses(network, harvesters_filepath, key_pair):
class ConsolePrinter:
def __init__(self):
self.identifier = 1

def print(self, signing_private_key, _vrf_private_key):
log.info(_private_key_to_address(network, signing_private_key))

self.identifier += 1

log.info(_('import-harvesters-list-header').format(filepath=harvesters_filepath, public_key=key_pair.public_key))

printer = ConsolePrinter()
_visit_harvesters(harvesters_filepath, key_pair, printer.print)


class HarvesterEncrypter:
def __init__(self, encryption_public_key, outfile):
self.encryption_public_key = encryption_public_key
self.outfile = outfile

def append(self, signing_private_key, vrf_private_key):
payload = signing_private_key.bytes + vrf_private_key.bytes
ephemeral_key_pair = KeyPair(PrivateKey.random())
(tag, initialization_vector, encrypted_payload) = encode_aes_gcm(SharedKey, ephemeral_key_pair, self.encryption_public_key, payload)
self.outfile.write(ephemeral_key_pair.public_key.bytes)
self.outfile.write(tag)
self.outfile.write(initialization_vector)
self.outfile.write(encrypted_payload)


def run_main(args):
if args.in_harvesters == args.out_harvesters:
log.error(_('import-harvesters-error-in-harvesters-is-equal-to-out-harvesters'))
sys.exit(1)

config = parse_shoestring_configuration(args.config)

in_harvesters_key_pair = KeyPair(read_private_key_from_private_key_pem_file(args.in_pem))
print_all_harvester_addresses(config.network, args.in_harvesters, in_harvesters_key_pair)

if not args.out_harvesters:
return

out_harvesters_filepath = Path(args.out_harvesters)
with open(out_harvesters_filepath, 'wb') as outfile:
out_harvesters_key_pair = KeyPair(read_private_key_from_private_key_pem_file(args.out_pem))
encrypter = HarvesterEncrypter(out_harvesters_key_pair.public_key, outfile)
_visit_harvesters(args.in_harvesters, in_harvesters_key_pair, encrypter.append)

out_harvesters_filepath.chmod(0o600)
print_all_harvester_addresses(config.network, out_harvesters_filepath, out_harvesters_key_pair)


def add_arguments(parser):
parser.add_argument('--config', help=_('argument-help-config'), required=True)
parser.add_argument('--in-harvesters', help=_('argument-help-import-harvesters-in-harvesters'), required=True)
parser.add_argument('--in-pem', help=_('argument-help-import-harvesters-in-pem'), required=True)

parser.add_argument('--out-harvesters', help=_('argument-help-import-harvesters-out-harvesters'))
parser.add_argument('--out-pem', help=_('argument-help-import-harvesters-out-pem'))

parser.set_defaults(func=run_main)
56 changes: 42 additions & 14 deletions tools/shoestring/shoestring/lang/en/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ msgid "argument-help-ca-key-path"
msgstr "path to main private key PEM file"

#: shoestring/commands/announce_transaction.py:35
#: shoestring/commands/health.py:75 shoestring/commands/import_bootstrap.py:33
#: shoestring/commands/init.py:29
#: shoestring/commands/health.py:74 shoestring/commands/import_bootstrap.py:33
#: shoestring/commands/import_harvesters.py:94 shoestring/commands/init.py:29
#: shoestring/commands/min_cosignatures_count.py:34
#: shoestring/commands/renew_certificates.py:29
#: shoestring/commands/renew_voting_keys.py:112
Expand All @@ -36,7 +36,7 @@ msgstr "path to main private key PEM file"
msgid "argument-help-config"
msgstr "path to shoestring configuration file"

#: shoestring/commands/health.py:76
#: shoestring/commands/health.py:75
#: shoestring/commands/renew_certificates.py:30
#: shoestring/commands/renew_voting_keys.py:113
#: shoestring/commands/reset_data.py:88 shoestring/commands/setup.py:138
Expand All @@ -47,6 +47,22 @@ msgstr "installation directory (default: {default_path})"
msgid "argument-help-import-bootstrap-bootstrap"
msgstr "path to bootstrap target directory"

#: shoestring/commands/import_harvesters.py:95
msgid "argument-help-import-harvesters-in-harvesters"
msgstr "input harvesters.dat file that is encrypted with in-pem"

#: shoestring/commands/import_harvesters.py:96
msgid "argument-help-import-harvesters-in-pem"
msgstr "PEM file that can be used to decrypt in-harvesters"

#: shoestring/commands/import_harvesters.py:98
msgid "argument-help-import-harvesters-out-harvesters"
msgstr "output harvesters.dat file that will be encrypted with out-pem"

#: shoestring/commands/import_harvesters.py:99
msgid "argument-help-import-harvesters-out-pem"
msgstr "PEM file that can be used to encrypt out-harvesters"

#: shoestring/commands/min_cosignatures_count.py:36
msgid "argument-help-min-cosignatures-count-update"
msgstr "update the shoestring configuration file"
Expand Down Expand Up @@ -134,11 +150,11 @@ msgstr "copying TREE {source_path} to {destination_path}"
msgid "general-created-aggregate-transaction"
msgstr "created aggregate transaction with hash {transaction_hash}"

#: shoestring/healthagents/peer_api.py:19
#: shoestring/healthagents/peer_api.py:25
msgid "health-peer-api-error"
msgstr "cannot access peer API at {host} on port {port}"

#: shoestring/healthagents/peer_api.py:17
#: shoestring/healthagents/peer_api.py:23
msgid "health-peer-api-success"
msgstr "peer API accessible, height = {height}"

Expand Down Expand Up @@ -186,7 +202,7 @@ msgstr "HTTPS certificate looks invalid: {error_message}"
msgid "health-rest-https-certificate-valid"
msgstr "HTTPS certificate looks ok: valid from {start_date} to {end_date}"

#: shoestring/commands/health.py:70
#: shoestring/commands/health.py:69
msgid "health-running-health-agent"
msgstr "running health agent for {module_name}"

Expand Down Expand Up @@ -244,6 +260,14 @@ msgstr ""
"bootstrap directory provided ({directory}) does not look like bootstrap's"
" target directory, nothing to import"

#: shoestring/commands/import_harvesters.py:74
msgid "import-harvesters-error-in-harvesters-is-equal-to-out-harvesters"
msgstr "in-harvesters and out-harvesters must be different"

#: shoestring/commands/import_harvesters.py:51
msgid "import-harvesters-list-header"
msgstr "listing harvesters in {filepath} using public key {public_key}"

#: shoestring/__main__.py:20
msgid "main-announce-transaction-help"
msgstr "announces a transaction to the network"
Expand All @@ -257,34 +281,38 @@ msgid "main-import-bootstrap-help"
msgstr "imports settings from a bootstap installation"

#: shoestring/__main__.py:23
msgid "main-import-harvesters-help"
msgstr "imports harvesters from an existing harvesters.dat file"

#: shoestring/__main__.py:24
msgid "main-init-help"
msgstr "extracts a template shoestring configuration file from a package"

#: shoestring/__main__.py:24
#: shoestring/__main__.py:25
msgid "main-min-cosignatures-count-help"
msgstr "detects minimum cosignatures required for an account"

#: shoestring/__main__.py:25
#: shoestring/__main__.py:26
msgid "main-pemtool-help"
msgstr "generates PEM files"

#: shoestring/__main__.py:26
#: shoestring/__main__.py:27
msgid "main-renew-certificates-help"
msgstr "renews certificates"

#: shoestring/__main__.py:27
#: shoestring/__main__.py:28
msgid "main-renew-voting-keys-help"
msgstr "renews voting keys"

#: shoestring/__main__.py:28
#: shoestring/__main__.py:29
msgid "main-reset-data-help"
msgstr "resets data to allow a resync from scratch"

#: shoestring/__main__.py:29
#: shoestring/__main__.py:30
msgid "main-setup-help"
msgstr "sets up a node"

#: shoestring/__main__.py:30
#: shoestring/__main__.py:31
msgid "main-signer-help"
msgstr "signs a transaction"

Expand All @@ -296,7 +324,7 @@ msgstr "valid subcommands"
msgid "main-title"
msgstr "Shoestring Tool"

#: shoestring/__main__.py:31
#: shoestring/__main__.py:32
msgid "main-upgrade-help"
msgstr "upgrades a node to the latest client version"

Expand Down
Loading

0 comments on commit b131142

Please sign in to comment.