Skip to content

Commit

Permalink
pem key tool, cert tool
Browse files Browse the repository at this point in the history
  • Loading branch information
gimre committed Mar 31, 2021
1 parent c522946 commit 26d4a00
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 62 deletions.
140 changes: 79 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,79 @@
# symbol-node-configurator

[![Build Status](https://travis-ci.com/nemtech/symbol-node-configurator.svg?branch=dev)](https://travis-ci.com/nemtech/symbol-node-configurator)

This repository contains two helper tools:
* votingkey.py - this is alternative voting key file generator
* generate.py - actual catapult.server configuration generator, that will be described below

## Prerequisites:

python3 -m pip install -r requirements.txt

Generator tool assumes that in working directory, there is directory called `certificates`
that contains:
* `ca.pubkey.pem`
* `ca.crt.pem`
* `node.full.crt.pem`
* `node.crt.pem`
* `node.key.pem`

When running with `--harvesting` switch, generator tool additionally expects:
* `private.harvesting.txt` and `private.vrf.txt`, containing respectively harvesting private key and vrf private key in hex OR
* `private.harvesting.pem` and `private.vrf.pem` pem files, containing private keys, if pem files are password protected
`--ask-pass` switch needs to be present

When running with `--voting` switch, generator tool additionally expects voting key file:
* `private_key_tree*.dat` - the file will be MOVED to destination directory,

note: in case of `--voting` if destination directory contains file with given name, the index will be incremented, until "empty" one
will be found

## Examples:

All examples assume the script is started from PARENT directory


### Create configuration for api node

python3 symbol-node-configurator/generator.py --mode api --output ../settings

### Create configuration for harvesting peer node

To create a configuration for harvesting node two files are needed, with keys in hex:
* private.harvesting.txt
* private.vrf.txt

echo "C0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FF" > private.harvesting.txt
echo "B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007" > private.vrf.txt
python3 symbol-node-configurator/generator.py --mode peer --output ../settings

### Create configuration for harvesting and voting peer node

echo "C0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FF" > private.harvesting.txt
echo "B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007" > private.vrf.txt
python3 symbol-node-configurator/generator.py --mode peer --output ../settings --voting

### Create configuration for harvesting and voting DUAL node

echo "C0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FF" > private.harvesting.txt
echo "B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007" > private.vrf.txt
python3 symbol-node-configurator/generator.py --mode dual --output ../settings --voting
# symbol-node-configurator

[![Build Status](https://travis-ci.com/nemtech/symbol-node-configurator.svg?branch=main)](https://travis-ci.com/nemtech/symbol-node-configurator)

This repository contains a few tools:
* pemtool.py - allows conversion from raw private key to (encrypted) PEM file (openssl format)
* certtool.py - openssl wrapper for easy certificate generation
* votingkey.py - this is alternative voting key file generator
* generate.py - actual catapult.server configuration generator, that will be described below

## Prerequisites:

apt-get install python3 python3-pip openssl
python3 -m pip install -r requirements.txt

All commands below assume scripts are started from PARENT directory.

## Pem tool

Get help:
python3 symbol-node-configurator/pemtool.py --help

Convert private key in hex format to encrypted PEM file
python3 symbol-node-configurator/pemtool.py --output ca.key.pem --ask-pass

## Cert tool

Get help:
python3 symbol-node-configurator/certtool.py --help

Create symbol server certificates using ca.key.pem
python3 symbol-node-configurator/certtool.py --output certificates --ca ca.key.pem

## Generate - config generator

Generator tool assumes that in working directory, there is directory called `certificates`
that contains:
* `ca.pubkey.pem`
* `ca.crt.pem`
* `node.full.crt.pem`
* `node.crt.pem`
* `node.key.pem`

When running with `--harvesting` switch, generator tool additionally expects:
* `private.harvesting.txt` and `private.vrf.txt`, containing respectively harvesting private key and vrf private key in hex OR
* `private.harvesting.pem` and `private.vrf.pem` pem files, containing private keys, if pem files are password protected
`--ask-pass` switch needs to be present

When running with `--voting` switch, generator tool additionally expects voting key file:
* `private_key_tree*.dat` - the file will be MOVED to destination directory,

note: in case of `--voting` if destination directory contains file with given name, the index will be incremented, until "empty" one
will be found

### Create configuration for api node

python3 symbol-node-configurator/generator.py --mode api --output ../settings

### Create configuration for harvesting peer node

To create a configuration for harvesting node two files are needed, with keys in hex:
* private.harvesting.txt
* private.vrf.txt

echo "C0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FF" > private.harvesting.txt
echo "B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007" > private.vrf.txt
python3 symbol-node-configurator/generator.py --mode peer --output ../settings

### Create configuration for harvesting and voting peer node

echo "C0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FF" > private.harvesting.txt
echo "B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007" > private.vrf.txt
python3 symbol-node-configurator/generator.py --mode peer --output ../settings --voting

### Create configuration for harvesting and voting DUAL node

echo "C0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FFEC0FF" > private.harvesting.txt
echo "B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007B007" > private.vrf.txt
python3 symbol-node-configurator/generator.py --mode dual --output ../settings --voting
161 changes: 161 additions & 0 deletions certtool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env python3

import argparse
import os
import re
import shutil
import sys
from pathlib import Path
from subprocess import PIPE, STDOUT, Popen

from symbolchain.core.CryptoTypes import PrivateKey
from symbolchain.core.PrivateKeyStorage import PrivateKeyStorage
from symbolchain.core.sym.KeyPair import KeyPair
from zenlog import log


def run_openssl(args, show_output=True):
command_line = ['openssl'] + args
process = Popen(command_line, stdout=PIPE, stderr=STDOUT)

all_lines = []
for line_bin in iter(process.stdout.readline, b''):
line = line_bin.decode('ascii')
all_lines.append(line)

if show_output:
sys.stdout.write(line)
sys.stdout.flush()

process.wait()

return all_lines


def check_openssl_version():
version_output = ''.join(run_openssl(['version', '-v'], False))
match = re.match(r'^OpenSSL +([^ ]*) ', version_output)
if not match or not match.group(1).startswith('1.1.1'):
raise RuntimeError('{} requires openssl version >=1.1.1'.format(__file__))


def get_common_name(default_value, prompt):
if default_value:
return default_value

return input('Enter {}: '.format(prompt)).strip()


def prepare_ca_config(ca_pem_path, ca_cn):
with open('ca.cnf', 'wt') as output_file:
output_file.write('''[ca]
default_ca = CA_default
[CA_default]
new_certs_dir = ./new_certs
database = index.txt
serial = serial.dat
private_key = {private_key_path}
certificate = ca.crt.pem
policy = policy_catapult
[policy_catapult]
commonName = supplied
[req]
prompt = no
distinguished_name = dn
[dn]
CN = {cn}
'''.format(private_key_path=ca_pem_path, cn=ca_cn))

os.makedirs('new_certs')
os.chmod('new_certs', 0o700)

with open('index.txt', 'wt') as output_file:
output_file.write('')


def prepare_node_config(node_cn):
with open('node.cnf', 'wt') as output_file:
output_file.write('''[req]
prompt = no
distinguished_name = dn
[dn]
CN = {cn}
'''.format(cn=node_cn))


def main():
parser = argparse.ArgumentParser(description='Cert generation tool', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--output', help='certificates working directory (all files will be created inside)', default='certificates')
parser.add_argument('--ca', help='path to key PEM file that will be used as a CA key', default='ca.key.pem')
parser.add_argument('--name-ca', help='use provided name as CA CN (common name) - suggested: account name')
parser.add_argument('--name-node', help='use provided name as node CN (common name) - suggested: node name, host or ip')
parser.add_argument('--force', help='overwrite output directory if it already exists', action='store_true')
args = parser.parse_args()

filepath = Path(args.output)
if filepath.exists():
if not args.force:
raise FileExistsError('output directory ({}) already exists, use --force to overwrite'.format(filepath))
shutil.rmtree(filepath)

check_openssl_version()

# obtain full path prior to switching directory
ca_path = Path(args.ca).absolute()

os.makedirs(filepath)
os.chdir(filepath)

log.info('creating ca.pubkey.pem')
run_openssl(['pkey', '-in', ca_path, '-pubout', '-out', 'ca.pubkey.pem'])

log.info('creating random node.key.pem')
node_key_pair = KeyPair(PrivateKey.random())
PrivateKeyStorage('.', None).save('node.key', node_key_pair.private_key)

log.info('preparing configuration files')
ca_cn = get_common_name(args.name_ca, 'CA common name')
node_cn = get_common_name(args.name_node, 'node common name')
prepare_ca_config(ca_path, ca_cn)
prepare_node_config(node_cn)

log.info('creating CA certificate')
run_openssl([
'req',
'-config', 'ca.cnf',
'-keyform', 'PEM',
'-key', ca_path,
'-new', '-x509',
'-days', '7300',
'-out', 'ca.crt.pem'])

# prepare node CSR
run_openssl([
'req',
'-config', 'node.cnf',
'-key', 'node.key.pem',
'-new',
'-out', 'node.csr.pem'])

log.info('signing node certificate')
run_openssl(['rand', '-out', './serial.dat', '-hex', '19'])

run_openssl([
'ca',
'-config', 'ca.cnf',
'-days', '375',
'-notext',
'-batch',
'-in', 'node.csr.pem',
'-out', 'node.crt.pem'])

log.info('certificates generated in {} directory'.format(args.output))


if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@


def main():
parser = argparse.ArgumentParser(description='Node configurator generator')
parser = argparse.ArgumentParser(description='Node configurator generator', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--mode', help='node type', choices=('api', 'peer', 'dual'), required=True)
parser.add_argument('--voting', help='node will be voting', action='store_true')
parser.add_argument('--harvesting', help='node will be harvesting', action='store_true')
Expand Down
52 changes: 52 additions & 0 deletions pemtool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3

import argparse
import getpass
from binascii import unhexlify
from pathlib import Path

from symbolchain.core.CryptoTypes import PrivateKey
from symbolchain.core.PrivateKeyStorage import PrivateKeyStorage


def read_key(filename):
return open(filename).read()


def get_private_key(filename):
private_key = getpass.getpass('Enter private key (in hex): ') if not filename else read_key(filename)
return private_key.strip()


def main():
parser = argparse.ArgumentParser(description='PEM tool')
parser.add_argument('--output', help='output PEM key file', required=True)
parser.add_argument('--input', help='input private key file (optional)')
parser.add_argument('--ask-pass', help='encrypt PEM with a password (password prompt will be shown)', action='store_true')
parser.add_argument('--force', help='overwrite output file if it already exists', action='store_true')
args = parser.parse_args()

output_name = args.output
if output_name.endswith('.pem'):
output_name = output_name[:-4]

filepath = Path(output_name + '.pem')
if filepath.exists() and not args.force:
raise FileExistsError('output file ({}) already exists, use --force to overwrite'.format(filepath))

private_key = PrivateKey(unhexlify(get_private_key(args.input)))

password = None
if args.ask_pass:
password = getpass.getpass('Provide {} password: '.format(filepath))
confirmation = getpass.getpass('Confirm {} password: '.format(filepath))
if confirmation != password:
raise RuntimeError('Provided passwords do not match')

storage = PrivateKeyStorage('.', password)
storage.save(output_name, private_key)
print('saved {}'.format(filepath))


if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions votingkey.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env python3

import argparse
import asyncio
import os
Expand Down

0 comments on commit 26d4a00

Please sign in to comment.