diff --git a/.gitignore b/.gitignore index cc613bd..cc73431 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ dist/ dist_deb build/ +.coverage +coverage.xml +htmlcov + .DS_Store *.pickle diff --git a/hsm_secrets/ssh/openssh/signing.py b/hsm_secrets/ssh/openssh/signing.py index d071df9..39180c7 100644 --- a/hsm_secrets/ssh/openssh/signing.py +++ b/hsm_secrets/ssh/openssh/signing.py @@ -73,12 +73,12 @@ def sign_ssh_cert(cert: OpenSSHCertificate, private_key: PrivateKey) -> None: # ---------- +''' def verify_ssh_cert(cert: OpenSSHCertificate) -> bool: """ Verify an SSH certificate with a public key. :param cert: The SSH certificate to verify - :param encoded_public_key: The public key to verify the certificate with (in OpenSSH format: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB...") :return: True if the certificate is verified, False otherwise """ try: @@ -147,3 +147,4 @@ def verify_ssh_cert(cert: OpenSSHCertificate) -> bool: except cryptography.exceptions.InvalidSignature as e: return False +''' diff --git a/hsm_secrets/ssh/openssh/ssh_certificate_test.py b/hsm_secrets/ssh/openssh/ssh_certificate_test.py deleted file mode 100644 index 6c9edd3..0000000 --- a/hsm_secrets/ssh/openssh/ssh_certificate_test.py +++ /dev/null @@ -1,176 +0,0 @@ -# ssh_certificate_test.py - -import argparse -import base64 -import sys - -from hsm_secrets.ssh.openssh.signing import sign_ssh_cert, verify_ssh_cert -from hsm_secrets.ssh.openssh.ssh_certificate import cert_for_ssh_pub_id, OpenSSHCertificate, RSACertificate, ECDSACertificate, ED25519Certificate - -from cryptography.hazmat.primitives.serialization import ssh -from cryptography.hazmat.primitives.asymmetric import rsa, ed25519, ec - -def print_certificate_details(cert: OpenSSHCertificate) -> None: - print(f" Key Cipher: {cert.cert_cipher_string()}") - print(f" ID: {cert.cert_id}") - print(f" Serial: {cert.serial}") - print(f" Nonce: {base64.b64encode(cert.nonce).decode('ascii')}") - print(f" Type: {cert.cert_type}") - print(f" Valid Principals: {', '.join(cert.valid_principals)}") - print(f" Valid After: {cert.valid_after}") - print(f" Valid Before: {cert.valid_before}") - print(f" Critical Options: {cert.critical_options}") - print(f" Extensions: {cert.extensions}") - - if isinstance(cert, RSACertificate): - print(f" RSA Public Exponent: {cert.e}") - print(f" RSA Modulus: {cert.n}") - elif isinstance(cert, ECDSACertificate): - print(f" ECDSA Curve: {cert.curve}") - print(f" ECDSA Public Key: {base64.b64encode(cert.ec_point).decode('ascii')}") - elif isinstance(cert, ED25519Certificate): - print(f" ED25519 Public Key: {base64.b64encode(cert.pk).decode('ascii')}") - - print(f" Signature Key: {base64.b64encode(cert.signature_key).decode('ascii')}") - print(f" Signature: {base64.b64encode(cert.signature).decode('ascii')}") - - -def read_file_str(file_path: str) -> str: - try: - with open(file_path, 'r') as f: - return f.read().strip() - except IOError as e: - print(f"Error reading file {file_path}: {e}") - sys.exit(1) - - - -def parsecert(args: argparse.Namespace) -> None: - file_contents = read_file_str(args.cert_file) - - try: - cert = OpenSSHCertificate.from_string_fmt(file_contents) - except ValueError as e: - print(f"Error parsing certificate: {e}") - sys.exit(1) - - print("\nCertificate Details:") - print_certificate_details(cert) - - re_encoded_data = cert.to_string_fmt() - assert file_contents == re_encoded_data, "Re-encoded data does not match original data" - - re_parsed_cert = OpenSSHCertificate.from_string_fmt(re_encoded_data) - - # Perform all the assertions as in the original code - assert cert.cert_cipher_string() == re_parsed_cert.cert_cipher_string(), "Certificate type mismatch" - assert cert.nonce == re_parsed_cert.nonce, "Nonce mismatch" - assert cert.signature == re_parsed_cert.signature, "Signature mismatch" - assert cert.signature_key == re_parsed_cert.signature_key, "Signature key mismatch" - assert cert.valid_principals == re_parsed_cert.valid_principals, "Valid principals mismatch" - assert cert.valid_after == re_parsed_cert.valid_after, "Valid after mismatch" - assert cert.valid_before == re_parsed_cert.valid_before, "Valid before mismatch" - assert cert.critical_options == re_parsed_cert.critical_options, "Critical options mismatch" - assert cert.extensions == re_parsed_cert.extensions, "Extensions mismatch" - - print("OK - " + args.cert_file) - - -def parsepub(args: argparse.Namespace) -> None: - file_contents = read_file_str(args.pub_file) - - # Create a user certificate - user_cert = cert_for_ssh_pub_id( - file_contents, - cert_id = "user@example.com", - cert_type = ssh.SSHCertificateType.USER, - principals=["basic_users", "admins"]) - - print("Parsed public key into a user certificate:") - print_certificate_details(user_cert) - - # Create a host certificate - host_cert = cert_for_ssh_pub_id( - file_contents, - cert_id = "host.example.com", - cert_type = ssh.SSHCertificateType.HOST, - principals=["host.example.com", "*.example.com"]) - - print("\nParsed public key into a host certificate:") - print_certificate_details(host_cert) - - print("\nTesting signing & verification with different issuers:") - - issuers = [ - ed25519.Ed25519PrivateKey.generate(), - rsa.generate_private_key(65537, 2048), - ec.generate_private_key(ec.SECP256R1()), - ] - for ca in issuers: - assert isinstance(ca, (rsa.RSAPrivateKey, ed25519.Ed25519PrivateKey, ec.EllipticCurvePrivateKey)) - - # Sign and verify user certificate - sign_ssh_cert(user_cert, ca) - print(f" - Signed user certificate ok with {ca.__class__.__name__}") - if verify_ssh_cert(user_cert): - print(f" - User certificate verified OK") - else: - print(f" - User certificate verification FAILED!") - - # Sign and verify host certificate - sign_ssh_cert(host_cert, ca) - print(f" - Signed host certificate ok with {ca.__class__.__name__}") - if verify_ssh_cert(host_cert): - print(f" - Host certificate verified OK") - else: - print(f" - Host certificate verification FAILED!") - - -def checksig(args: argparse.Namespace) -> None: - cert_contents = read_file_str(args.cert_file) - - try: - cert = OpenSSHCertificate.from_string_fmt(cert_contents) - except ValueError as e: - print(f"Error parsing certificate: {e}") - sys.exit(1) - - is_valid = verify_ssh_cert(cert) - - if is_valid: - print(f"Signature OK: {args.cert_file}") - else: - print(f"Signature is INVALID: {args.cert_file}") - sys.exit(1) - - -def main() -> None: - parser = argparse.ArgumentParser(description="SSH Certificate Test Tool") - subparsers = parser.add_subparsers(dest="command", help="Available commands") - - # parsecert command - parsecert_parser = subparsers.add_parser("parsecert", help="Parse and validate a certificate file") - parsecert_parser.add_argument("cert_file", help="Path to the certificate file") - - # parsepub command - parsepub_parser = subparsers.add_parser("parsepub", help="Parse a public key file") - parsepub_parser.add_argument("pub_file", help="Path to the public key file") - - # checksig command - checksig_parser = subparsers.add_parser("checksig", help="Verify a certificate signature") - checksig_parser.add_argument("cert_file", help="Path to the certificate file") - - args = parser.parse_args() - - if args.command == "parsecert": - parsecert(args) - elif args.command == "parsepub": - parsepub(args) - elif args.command == "checksig": - checksig(args) - else: - parser.print_help() - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/hsm_secrets/ssh/ssh_utils.py b/hsm_secrets/ssh/yubihsm_ssh_structs.py similarity index 98% rename from hsm_secrets/ssh/ssh_utils.py rename to hsm_secrets/ssh/yubihsm_ssh_structs.py index 1b7b071..19b1507 100644 --- a/hsm_secrets/ssh/ssh_utils.py +++ b/hsm_secrets/ssh/yubihsm_ssh_structs.py @@ -1,3 +1,6 @@ +# Functions for OpenSSH certificates and templates for YubiHSM2 +# Not used in the current version of the project, but kept for reference +''' from __future__ import absolute_import, division from math import floor @@ -9,7 +12,6 @@ from cryptography.hazmat.primitives.asymmetric import (ed25519, rsa) - def create_template(ts_public_key: rsa.RSAPublicKey | ed25519.Ed25519PublicKey, key_whitelist: Sequence[int], not_before: int, @@ -175,3 +177,4 @@ def pack_options(options: Sequence[tuple[str, bytes]]) -> bytes: req += pack_bytes(packed_type_and_key) return req +''' diff --git a/run-tests.sh b/run-tests.sh index 6d0b85a..3d6d108 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -7,7 +7,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/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" # Helpers for `expect` calls: @@ -81,7 +82,7 @@ EOF test_pytest() { ./_venv/bin/pip install pytest - ./_venv/bin/pytest -v hsm_secrets + ./_venv/bin/pytest --cov=hsm_secrets --cov-append --cov-report='' -v hsm_secrets } test_fresh_device() { @@ -445,6 +446,10 @@ run_test() { rm -f $MOCKDB } +# Reset previous coverage files before accumulating new data +./_venv/bin/pip install coverage pytest-cov +rm -f .coverage .coverage.* + echo "Running tests:" run_test test_pytest run_test test_fresh_device @@ -460,4 +465,13 @@ run_test test_piv_user_certificate_csr run_test test_piv_dc_certificate run_test test_logging_commands -echo "All tests passed successfully!" +echo "---" + +echo "Running coverage report:" +./_venv/bin/coverage combine --append +./_venv/bin/coverage report +./_venv/bin/coverage html +./_venv/bin/coverage xml + +echo "---" +echo "OK. All tests passed successfully!"