Skip to content

Commit

Permalink
Improve CRL init command
Browse files Browse the repository at this point in the history
  • Loading branch information
elonen committed Sep 21, 2024
1 parent 5ff4061 commit 9f1156b
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 34 deletions.
89 changes: 66 additions & 23 deletions hsm_secrets/x509/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from cryptography.hazmat.primitives import serialization, hashes
from hsm_secrets.config import HSMConfig, HSMKeyID, HSMOpaqueObject, X509CA, click_hsm_obj_auto_complete, find_config_items_of_class

from hsm_secrets.utils import HSMAuthMethod, HsmSecretsCtx, cli_result, cli_warn, confirm_and_delete_old_yubihsm_object_if_exists, open_hsm_session, cli_code_info, pass_common_args, cli_info
from hsm_secrets.utils import HSMAuthMethod, HsmSecretsCtx, cli_confirm, cli_result, cli_warn, confirm_and_delete_old_yubihsm_object_if_exists, open_hsm_session, cli_code_info, pass_common_args, cli_info

from hsm_secrets.x509.cert_builder import X509CertBuilder, sign_hash_algo_for_key
from hsm_secrets.x509.cert_checker import X509IntermediateCACertificateChecker, X509RootCACertificateChecker
Expand Down Expand Up @@ -229,36 +229,79 @@ def _do_it(ses: HSMSession|None):

@cmd_x509_crl.command('init')
@pass_common_args
@click.option('--ca', '-c', required=True, help="CA cert ID or label to sign the CRL", shell_complete=click_hsm_obj_auto_complete(HSMOpaqueObject))
@click.option('--out', '-o', required=True, type=click.Path(dir_okay=False), help="Output CRL file")
@click.option('--validity', '-v', default=7, help="CRL validity period in days")
@click.argument('cacerts', nargs=-1, type=str, metavar='<cacert-id|label>...', shell_complete=click_hsm_obj_auto_complete(HSMOpaqueObject))
@click.option('--out', '-o', required=False, type=click.Path(dir_okay=False), help="Output CRL file (default: from config)")
@click.option('--period', '-v', type=int, default=None, help="CRL update period in days")
@click.option('--this-update', type=click.DateTime(), default=None, help="This Update date (default: now)")
@click.option('--next-update', type=click.DateTime(), default=None, help="Next Update date (default: now + validity)")
@click.option('--next-update', type=click.DateTime(), default=None, help="Next Update date (default: cert's expiry date)")
@click.option('--crl-number', type=int, default=1, help="CRL Number (default: 1)")
def init_crl(ctx: HsmSecretsCtx, ca: str, out: str, validity: int, this_update: Optional[datetime.datetime],
next_update: Optional[datetime.datetime], crl_number: int):
"""Create empty CRL signed by a CA"""
ca_cert_def = ctx.conf.find_def(ca, HSMOpaqueObject)
ca_x509_def = find_ca_def(ctx.conf, ca_cert_def.id)
assert ca_x509_def, f"CA cert ID not found: 0x{ca_cert_def.id:04x}"
@click.option('--force', '-f', is_flag=True, default=False, help="Overwrite existing CRL file(s)")
def init_crl(ctx: HsmSecretsCtx, cacerts: list[str], out: str|None, period: int|None, this_update: datetime.datetime|None,
next_update: datetime.datetime|None, crl_number: int, force: bool):
"""Create empty CRL for a CA
Given CA certificate must be present in the HSM, as it will be fetched and
its subject used as the new CRL's issuer name. (For cross-signed CAs, pick any of the
certs, as their subject will be the same.)
Options `--validity` and `--next-update` are mutually exclusive.
If neither is specified, the next update will be set to the CA cert's expiry date.
Clients may check the CRL more frequently than the next update period, but it's not
guaranteed, so if your use case has frequent revocations, set a shorter period.
"""
if (period is not None) and (next_update is not None):
raise click.ClickException("Error: --period and --next-update options are mutually exclusive.")

if len(cacerts) > 1 and out is not None:
raise click.ClickException("Error: Output file name option is not supported for multiple CA certs.")

defs: list[tuple[HSMOpaqueObject, X509CA, Path]] = []
for ca in cacerts:

# Find CA definition for the given cert ID
ca_cert_def = ctx.conf.find_def(ca, HSMOpaqueObject)
ca_def = find_ca_def(ctx.conf, ca_cert_def.id)
if not ca_def:
raise click.ClickException(f"CA cert ID not found: 0x{ca_cert_def.id:04x}")

# Determine output file name
if not out:
if not ca_def.crl_distribution_points:
raise click.ClickException(f"Error: CRL DP not set for CA '{ca_cert_def.label}' (0x{ca_cert_def.id:04x}), cannot determine output file.")
outfile = Path(ca_def.crl_distribution_points[0].split('/')[-1])
else:
outfile = Path(out)

if outfile.exists() and not force:
cli_confirm(f"Overwrite the existing file: {outfile}?", abort=True)
defs.append((ca_cert_def, ca_def, outfile))

# Create the CRLs
with open_hsm_session(ctx) as ses:
ca_cert = ses.get_certificate(ca_cert_def)
ca_key = ses.get_private_key(ca_x509_def.key)
for ca_cert_def, ca_def, outfile in defs:
ca_cert = ses.get_certificate(ca_cert_def)
ca_key = ses.get_private_key(ca_def.key)

builder = x509.CertificateRevocationListBuilder()
builder = builder.issuer_name(ca_cert.subject)
builder = builder.last_update(this_update or datetime.datetime.now(datetime.UTC))
builder = builder.next_update(next_update or (datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=validity)))
builder = builder.add_extension(x509.CRLNumber(crl_number), critical=False)
this_update = this_update or datetime.datetime.now(datetime.UTC)

crl = builder.sign(ca_key, sign_hash_algo_for_key(ca_key))
if next_update is None:
if period is not None:
next_update = this_update + datetime.timedelta(days=period)
else:
next_update = ca_cert.not_valid_after - datetime.timedelta(seconds=1)

crl_pem = crl.public_bytes(encoding=serialization.Encoding.PEM)
Path(out).write_bytes(crl_pem)
builder = x509.CertificateRevocationListBuilder()
builder = builder.issuer_name(ca_cert.subject)
builder = builder.last_update(this_update)
builder = builder.next_update(next_update)
builder = builder.add_extension(x509.CRLNumber(crl_number), critical=False)

crl = builder.sign(ca_key, sign_hash_algo_for_key(ca_key))

crl_pem = crl.public_bytes(encoding=serialization.Encoding.PEM)
outfile.write_bytes(crl_pem)

cli_code_info(f"Initialized CRL signed by CA `{ca_cert_def.label}` (0x{ca_cert_def.id:04x})")
cli_code_info(f"CRL written to: `{out}`")
cli_code_info(f"Initialized CRL for `{ca_def.key.label}` (0x{ca_def.key.id:04x}), written to: `./{str(outfile)}`")

# ---------------

Expand Down
30 changes: 19 additions & 11 deletions run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ trap "rm -rf $TEMPDIR" EXIT
cp hsm-conf.yml $TEMPDIR/
MOCKDB="$TEMPDIR/mock.pickle"
#CMD="./_venv/bin/hsm-secrets -c $TEMPDIR/hsm-conf.yml --mock $MOCKDB"
CMD="./_venv/bin/coverage run --parallel-mode --source=hsm_secrets ./_venv/bin/hsm-secrets -c $TEMPDIR/hsm-conf.yml --mock $MOCKDB"
CURDIR=$(realpath $(dirname $0))
CMD="$CURDIR/_venv/bin/coverage run --parallel-mode --source=hsm_secrets $CURDIR/_venv/bin/hsm-secrets -c $TEMPDIR/hsm-conf.yml --mock $MOCKDB"


# Helpers for `expect` calls:
Expand Down Expand Up @@ -81,8 +82,8 @@ EOF
# ------------------ test cases -------------------------

test_pytest() {
./_venv/bin/pip install pytest
./_venv/bin/pytest --cov=hsm_secrets --cov-append --cov-report='' -v hsm_secrets
$CURDIR/_venv/bin/pip install pytest
$CURDIR/_venv/bin/pytest --cov=hsm_secrets --cov-append --cov-report='' -v hsm_secrets
}

test_fresh_device() {
Expand Down Expand Up @@ -299,8 +300,15 @@ test_piv_dc_certificate() {
test_crl_commands() {
setup

# Initialize a new CRL
run_cmd x509 crl init --ca 0x0211 --out $TEMPDIR/test.crl --validity 30
# Try generating multiple CRLs first, with defaults
cd "$TEMPDIR"
run_cmd x509 crl init cert_tls-t1-ecp384_rsa3072-root cert_nac-n1-ecp256
assert_success
[ -f $TEMPDIR/tls-t1-ecp384.crl ] || { echo "ERROR: CRL file not created"; return 1; }
[ -f $TEMPDIR/nac-n1-ecp256.crl ] || { echo "ERROR: CRL file not created"; return 1; }

# Initialize a test CRL
run_cmd x509 crl init --out $TEMPDIR/test.crl --period 30 0x0211
assert_success
[ -f $TEMPDIR/test.crl ] || { echo "ERROR: CRL file not created"; return 1; }

Expand Down Expand Up @@ -464,7 +472,7 @@ test_codesign_sign_osslsigncode_hash() {
assert_success

# Create a CRL
run_cmd x509 crl init --ca cert_ca-root-a1-rsa3072 -o "$test_dir/crl.pem"
run_cmd x509 crl init -o "$test_dir/crl.pem" cert_ca-root-a1-rsa3072
assert_success

# Attach the signature to the executable
Expand Down Expand Up @@ -576,7 +584,7 @@ run_test() {
}

# Reset previous coverage files before accumulating new data
./_venv/bin/pip install coverage pytest-cov
$CURDIR/_venv/bin/pip install coverage pytest-cov
rm -f .coverage .coverage.*

echo "Running tests:"
Expand All @@ -601,10 +609,10 @@ run_test test_logging_commands
echo "---"

echo "Running coverage report:"
./_venv/bin/coverage combine --append
./_venv/bin/coverage report
./_venv/bin/coverage html
./_venv/bin/coverage xml
$CURDIR/_venv/bin/coverage combine --append
$CURDIR/_venv/bin/coverage report
$CURDIR/_venv/bin/coverage html
$CURDIR/_venv/bin/coverage xml

echo "---"
echo "OK. All tests passed successfully!"

0 comments on commit 9f1156b

Please sign in to comment.