Skip to content

Commit

Permalink
[tools/shoestring] fix: renew certs generates a new ca cert
Browse files Browse the repository at this point in the history
problem: renew certificates command creates a new ca cert without renew-ca option
solution: only generate a new ca cert with the renew-ca option
  • Loading branch information
Wayonb authored Nov 20, 2024
1 parent 8817984 commit 8aceea3
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 3 deletions.
6 changes: 5 additions & 1 deletion tools/shoestring/shoestring/commands/renew_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ async def run_main(args):

openssl_executor = OpensslExecutor(os.environ.get('OPENSSL_EXECUTABLE', 'openssl'))
with CertificateFactory(openssl_executor, ca_key_path, config.node.ca_password) as factory:
factory.generate_ca_certificate(config.node.ca_common_name)
if args.renew_ca:
factory.generate_ca_certificate(config.node.ca_common_name)
else: # health node full certificate check needs current ca cert to pass
factory.reuse_ca_certificate(config.node.ca_common_name, directories.certificates)

factory.generate_random_node_private_key()
factory.generate_node_certificate(config.node.node_common_name)
factory.create_node_certificate_chain()
Expand Down
18 changes: 16 additions & 2 deletions tools/shoestring/shoestring/internal/CertificateFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def _generate_random_private_key(self, name):
'-algorithm', 'ed25519'
])

def generate_ca_certificate(self, ca_cn, days=7300):
"""Generates a CA certificate."""
def _prepare_ca_certificate(self, ca_cn):
"""Prepare CA certificate environment."""

if not ca_cn:
raise RuntimeError('CA common name cannot be empty')
Expand Down Expand Up @@ -106,6 +106,20 @@ def generate_ca_certificate(self, ca_cn, days=7300):
with open('index.txt', 'wt', encoding='utf8') as outfile:
outfile.write('')

def reuse_ca_certificate(self, ca_cn, ca_cert_path):
"""Setup current CA certificate."""

# prepare CA config
self._prepare_ca_certificate(ca_cn)

shutil.copy(ca_cert_path / 'ca.crt.pem', '.')

def generate_ca_certificate(self, ca_cn, days=7300):
"""Generates a CA certificate."""

# prepare CA config
self._prepare_ca_certificate(ca_cn)

# actually generate CA certificate
self.openssl_executor.dispatch(self._add_ca_password([
'req',
Expand Down
20 changes: 20 additions & 0 deletions tools/shoestring/tests/commands/test_renew_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ def _create_configuration(output_directory, ca_password, ca_common_name, node_co
filename=filename)


def _load_binary_file_data(filename):
with open(filename, 'rb') as infile:
return infile.read()


def _assert_node_full_certificate(ca_certificate_filepath, node_certificate_filepath, certificates_path):
node_full_crt_data = _load_binary_file_data(certificates_path / 'node.full.crt.pem')
node_crt_data = _load_binary_file_data(node_certificate_filepath)
ca_crt_data = _load_binary_file_data(ca_certificate_filepath)
assert node_full_crt_data == node_crt_data + ca_crt_data


async def _assert_can_renew_node_certificate(ca_password=None):
# Arrange:
with tempfile.TemporaryDirectory() as output_directory:
Expand Down Expand Up @@ -76,6 +88,9 @@ async def _assert_can_renew_node_certificate(ca_password=None):
create_openssl_executor().dispatch(['verify', '-CAfile', ca_certificate_path, ca_certificate_path])
assert ca_certificate_last_modified_time == ca_certificate_path.stat().st_mtime

# verify that node.full == node.crt + ca.crt
_assert_node_full_certificate(ca_certificate_path, node_certificate_path, preparer.directories.certificates)


async def test_can_renew_node_certificate():
await _assert_can_renew_node_certificate()
Expand All @@ -90,6 +105,8 @@ async def test_can_renew_node_certificate_with_ca_password():
# region CA and node certificates renewal

async def _assert_can_renew_ca_and_node_certificates(ca_password=None, use_relative_path=None):
# pylint: disable=too-many-locals

# Arrange:
with tempfile.TemporaryDirectory() as output_directory:
config_filepath_1 = _create_configuration(output_directory, ca_password, 'ORIGINAL CA CN', 'ORIGINAL NODE CN', '1.shoestring.ini')
Expand Down Expand Up @@ -137,6 +154,9 @@ async def _assert_can_renew_ca_and_node_certificates(ca_password=None, use_relat
create_openssl_executor().dispatch(['verify', '-CAfile', ca_certificate_path, ca_certificate_path])
assert ca_certificate_last_modified_time != ca_certificate_path.stat().st_mtime

# verify that node.full == node.crt + ca.crt
_assert_node_full_certificate(ca_certificate_path, node_certificate_path, preparer.directories.certificates)


async def test_can_renew_ca_and_node_certificates():
await _assert_can_renew_ca_and_node_certificates()
Expand Down
37 changes: 37 additions & 0 deletions tools/shoestring/tests/internal/test_CertificateFactory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import os
import re
import shutil
import tempfile
import unittest
from pathlib import Path
Expand Down Expand Up @@ -269,6 +270,42 @@ def test_cannot_generate_node_certificate_without_common_name(self):
with self.assertRaisesRegex(RuntimeError, 'Node common name cannot be empty'):
factory.generate_node_certificate('')

def test_can_reuse_ca_certificate(self):
def _load_binary_file_data(filename):
with open(filename, 'rb') as infile:
return infile.read()

# Arrange:
with tempfile.TemporaryDirectory() as package_directory:
ca_certificate_filepath = Path(package_directory) / 'ca.crt.pem'

# - create a CA private key
with tempfile.TemporaryDirectory() as certificate_directory:
ca_private_key_path = self._create_ca_private_key(certificate_directory)

with CertificateFactory(self._create_executor(), ca_private_key_path) as factory:
# create CA certificate
ca_common_name = 'my CA common name'
factory.generate_ca_certificate(ca_common_name)
factory.package(package_directory)

# Sanity:
shutil.rmtree(certificate_directory)
assert not ca_private_key_path.exists()

# Act:
with CertificateFactory(self._create_executor(), ca_private_key_path) as renew_factory:
renew_factory.reuse_ca_certificate(ca_common_name, Path(package_directory))

# Assert:
assert Path('ca.crt.pem').exists()
assert Path('ca.cnf').exists()
assert ca_certificate_filepath.exists()

original_ca_crt_data = _load_binary_file_data(ca_certificate_filepath)
copied_ca_crt_data = _load_binary_file_data('ca.crt.pem')
assert original_ca_crt_data == copied_ca_crt_data

# endregion

# region packaging
Expand Down

0 comments on commit 8aceea3

Please sign in to comment.