Skip to content

Commit

Permalink
Merge pull request #91 from iana-internal/review/feature2024_add_more…
Browse files Browse the repository at this point in the history
…_tests

add more tests to support assurance claims
  • Loading branch information
jschlyter authored Oct 10, 2024
2 parents fd603d0 + 040233e commit c24d07f
Show file tree
Hide file tree
Showing 15 changed files with 957 additions and 74 deletions.
6 changes: 3 additions & 3 deletions docs/assurance-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,15 @@ Test cases provides evidence that if a deprecated algorithm is listed in the Req

#### Argument (1.2.7.2)

KSR-POLICY-ALG.2: The signer software (in 'check\_zsk\_policy\_algorithm' of 'ksr/verify\_policy.py') checks the 'RequestPolicy' section of the KSR and denies the use of unspported algorithms ('common/data.py').
KSR-POLICY-ALG.2: The signer software (in 'check\_zsk\_policy\_algorithm' of 'ksr/verify\_policy.py') checks the 'RequestPolicy' section of the KSR and denies the use of unsupported algorithms ('common/data.py').

##### Evidence

Test cases provides evidence that if an unsupported algorithm is listed in the RequestPolicy section of the KSR the KSR is rejected.

#### Argument (1.2.7.3)

KSR-POLICY-ALG.3: The signer software (in 'check\_zsk\_policy\_algorithm' of 'ksr/verify\_policy.py') checks to ensure that each occurrence of an algorithm in the 'RequestPolicy' section of the KSR is acceptable according to the configured policy ('approved\_algoritms').
KSR-POLICY-ALG.3: The signer software (in 'check\_zsk\_policy\_algorithm' of 'ksr/verify\_policy.py') checks to ensure that each occurrence of an algorithm in the 'RequestPolicy' section of the KSR is acceptable according to the configured policy ('approved\_algorithms').

##### Evidence

Expand All @@ -241,7 +241,7 @@ Test cases provides evidence that if the algorithm listed in the 'RequestPolicy'

#### Argument (1.2.7.6)

KSR-POLICY-ALG.6: The signer software (in 'check\_keys\_match\_zsk\_policy' of 'ksr/verify\_budles.py') checks to ensure that the signature algorithms and key parameters of each key in each key bundle is in compliance with the 'RequestPolicy' section of the KSR.
KSR-POLICY-ALG.6: The signer software (in 'check\_keys\_match\_zsk\_policy' of 'ksr/verify\_bundles.py') checks to ensure that the signature algorithms and key parameters of each key in each key bundle is in compliance with the 'RequestPolicy' section of the KSR.

##### Evidence

Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ pytest-ruff = "^0.4.1"
online = ["fastapi", "pyopenssl"]

[tool.poetry.scripts]
kskm-keymaster = "kskm.tools.keymaster:main"
kskm-ksrsigner = "kskm.tools.ksrsigner:main"
kskm-sha2wordlist = "kskm.tools.sha2wordlist:main"
kskm-trustanchor = "kskm.tools.trustanchor:main"
kskm-wksr = "kskm.tools.wksr:main"
kskm-keymaster = "kskm.tools.keymaster:script_entrypoint"
kskm-ksrsigner = "kskm.tools.ksrsigner:script_entrypoint"
kskm-sha2wordlist = "kskm.tools.sha2wordlist:script_entrypoint"
kskm-trustanchor = "kskm.tools.trustanchor:script_entrypoint"
kskm-wksr = "kskm.tools.wksr:script_entrypoint"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
76 changes: 73 additions & 3 deletions src/kskm/skr/tests/test_output.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import os
import unittest
from pathlib import Path
from unittest.mock import MagicMock, mock_open, patch

from kskm.common.config import get_config
from kskm.common.integrity import sha2wordlist
from kskm.signer import output_skr_xml
from kskm.skr import load_skr, response_from_xml, skr_to_xml
from kskm.skr.data import Response
from kskm.skr.validate import validate_response


Expand All @@ -12,18 +16,25 @@ def setUp(self) -> None:
"""Prepare test instance"""
self.data_dir = Path(os.path.dirname(__file__), "data")

def test_recreate_2018_q1_0(self) -> None:
"""Test a parse->output->parse cycle with skr-root-2018-q1-0-d_to_e.xml"""
fn = self.data_dir.joinpath("skr-root-2018-q1-0-d_to_e.xml")
config = get_config(filename=None)
policy = config.response_policy

skr = load_skr(fn, policy)
self.assertEqual(skr.id, "4fe9bb10-6f6b-4503-8575-7824e2d66925")

self.skr_filename = fn
self.skr = skr
self.policy = policy

def test_recreate_2018_q1_0(self) -> None:
"""Test a parse->output->parse cycle with skr-root-2018-q1-0-d_to_e.xml"""

skr: Response = self.skr

new_xml = skr_to_xml(skr)
new_skr = response_from_xml(new_xml)
validate_response(new_skr, policy)
validate_response(new_skr, self.policy)

# compare larger and larger parts, to get better indications of the
# whereabouts of issues
Expand All @@ -39,3 +50,62 @@ def test_recreate_2018_q1_0(self) -> None:
self.assertEqual(skr.bundles, new_skr.bundles)

self.assertEqual(skr, new_skr)

@patch("builtins.print")
def test_output_skr_xml_print(self, mock: MagicMock) -> None:
"""Test the 'just print' mode of output_skr_xml"""
output_skr_xml(self.skr, output_filename=None)

expected = skr_to_xml(self.skr)
mock.assert_called_once_with(expected)

def test_output_skr_xml_write_to_file(self) -> None:
"""Test that the expected content is written to a (mocked) file"""
test_fn = Path("mocked_file.skr")
with patch("kskm.signer.open", mock_open()) as mock:
output_skr_xml(self.skr, output_filename=test_fn, log_contents=True)

expected = skr_to_xml(self.skr).encode()
mock.assert_called_once_with(test_fn, "wb")
mock().write.assert_called_once_with(expected)

def test_sha2wordlist(self) -> None:
"""Test generation of PGP word list (and SHA-256 checksum) of content"""
hexdigest, words = sha2wordlist(b"test")
assert hexdigest == "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"

expected = [
"quota",
"letterhead",
"stagnate",
"inventive",
"newborn",
"disbelief",
"klaxon",
"glossary",
"pupil",
"combustion",
"Trojan",
"Orlando",
"solo",
"existence",
"stagnate",
"bifocals",
"reform",
"rebellion",
"dropper",
"bravado",
"briefcase",
"armistice",
"miser",
"Chicago",
"stairway",
"filament",
"glucose",
"bifocals",
"ruffled",
"upcoming",
"allow",
"antenna",
]
assert words == expected
8 changes: 8 additions & 0 deletions src/kskm/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


class EXIT_CODES(Enum):
success = 0 # Success
interrupt = 1 # Ctrl+C pressed
config = 2 # Configuration error
fatal = 3 # Fatal error
86 changes: 55 additions & 31 deletions src/kskm/tools/keymaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import logging
import os
import sys
from argparse import Namespace as ArgsType
from typing import NoReturn

from PyKCS11 import PyKCS11Error

Expand All @@ -31,6 +33,7 @@
from kskm.keymaster.inventory import DNSRecords, key_inventory
from kskm.keymaster.keygen import generate_ec_key, generate_rsa_key
from kskm.misc.hsm import KSKM_P11, KeyType, init_pkcs11_modules
from kskm.tools import EXIT_CODES
from kskm.version import __verbose_version__

SUPPORTED_ALGORITHMS = [str(x.name) for x in KeyType]
Expand Down Expand Up @@ -131,10 +134,10 @@ def inventory(
return True


def main() -> bool:
"""Main function."""
progname = os.path.basename(sys.argv[0])

def parse_args(arguments: list[str]) -> ArgsType | None:
"""
Parse command line arguments.
"""
parser = argparse.ArgumentParser(
description=f"Keymaster {__verbose_version__}",
add_help=True,
Expand Down Expand Up @@ -238,51 +241,72 @@ def main() -> bool:
help="Don't ask for confirmation",
)

args = parser.parse_args()
logger = get_logger(progname=progname, debug=args.debug, syslog=False, filelog=True).getChild(__name__)

args = parser.parse_args(args=arguments)
try:
config = get_config(args.config)
except FileNotFoundError as exc:
logger.critical(str(exc))
return False
except ConfigurationError as exc:
logger = logging.getLogger("configuration")
for message in str(exc).splitlines():
logger.critical(message)
return False
_ = args.func
except AttributeError:
parser.print_help()
return None

return args


def keymaster(logger: logging.Logger, args: ArgsType, config: KSKMConfig) -> EXIT_CODES:
#
# Initialise PKCS#11 modules (HSMs)
#
try:
p11modules = init_pkcs11_modules(config, name=args.hsm, rw_session=True)
except Exception as e:
logger.critical("HSM initialisation error: %s", str(e))
return False
return EXIT_CODES.fatal

if len(p11modules) <= 0:
logger.critical("No HSM configured")
return False
return EXIT_CODES.fatal

try:
mode_function = args.func
except AttributeError:
parser.print_help()
return False
res = args.func(args, config, p11modules, logger)
return EXIT_CODES.success if res is True else EXIT_CODES.fatal
except PyKCS11Error as exc:
logger.critical(str(exc))

return EXIT_CODES.fatal


def main(progname: str, arguments: list[str]) -> EXIT_CODES:
"""Main function."""

args = parse_args(arguments)
if not args:
return EXIT_CODES.fatal

logger = get_logger(progname=progname, debug=args.debug, syslog=False, filelog=True).getChild(__name__)

try:
res = mode_function(args, config, p11modules, logger)
if res is True:
sys.exit(0)
except PyKCS11Error as exc:
config = get_config(args.config)
except FileNotFoundError as exc:
logger.critical(str(exc))
sys.exit(1)
except KeyboardInterrupt:
sys.exit(0)
return EXIT_CODES.fatal
except ConfigurationError as exc:
logger = logging.getLogger("configuration")
for message in str(exc).splitlines():
logger.critical(message)
return EXIT_CODES.config

sys.exit(1)
return keymaster(logger=logger, args=args, config=config)


def script_entrypoint() -> NoReturn:
"""Script entrypoint for this tool"""
try:
progname = os.path.basename(sys.argv[0])
res = main(progname=progname, arguments=sys.argv[1:])
sys.exit(res.value)
except KeyboardInterrupt:
sys.exit(EXIT_CODES.interrupt.value)


if __name__ == "__main__":
main()
# Run the script entrypoint if executed manually
script_entrypoint()
37 changes: 23 additions & 14 deletions src/kskm/tools/ksrsigner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import sys
from argparse import Namespace as ArgsType
from pathlib import Path
from typing import Any
from typing import Any, Mapping, NoReturn

import kskm.common
import kskm.ksr
Expand All @@ -25,11 +25,12 @@
from kskm.common.wordlist import pgp_wordlist
from kskm.signer import create_skr, output_skr_xml
from kskm.signer.policy import check_last_skr_and_new_skr, check_skr_and_ksr
from kskm.tools import EXIT_CODES
from kskm.version import __verbose_version__

__author__ = "ft"

_DEFAULTS = {
_DEFAULTS: Mapping[str, Any] = {
"debug": False,
"syslog": False,
"previous_skr": None,
Expand All @@ -42,10 +43,8 @@
"schema": "normal",
}

EXIT_CODES = {"success": 0, "interrupt": 1, "config": 2, "fatal": 3}


def parse_args(defaults: dict[str, Any]) -> ArgsType:
def parse_args(arguments: list[str], defaults: Mapping[str, Any]) -> ArgsType:
"""
Parse command line arguments.
Expand Down Expand Up @@ -151,7 +150,7 @@ def parse_args(defaults: dict[str, Any]) -> ArgsType:
default=None,
help="HSM to operate on",
)
return parser.parse_args()
return parser.parse_args(args=arguments)


def _previous_skr_filename(args: ArgsType, config: KSKMConfig) -> Path | None:
Expand Down Expand Up @@ -268,27 +267,37 @@ def ksrsigner(logger: logging.Logger, args: ArgsType, config: KSKMConfig | None
return True


def main() -> None:
def main(progname: str, arguments: list[str]) -> EXIT_CODES:
"""Main program function."""
try:
progname = os.path.basename(sys.argv[0])
args = parse_args(_DEFAULTS)
args = parse_args(arguments=arguments, defaults=_DEFAULTS)
logger = get_logger(progname=progname, debug=args.debug, syslog=args.syslog, filelog=True).getChild(__name__)
res = ksrsigner(logger, args)
if res is True:
sys.exit(EXIT_CODES["success"])
return EXIT_CODES.success
logging.critical("Fatal error, program stopped")
sys.exit(EXIT_CODES["fatal"])
return EXIT_CODES.fatal
except KeyboardInterrupt:
logging.warning("Keyboard interrupt, program stopped")
sys.exit(EXIT_CODES["interrupt"])
raise
except ConfigurationError as exc:
logger = logging.getLogger("configuration")
for message in str(exc).splitlines():
logger.critical(message)
logging.critical("Configuration error, program stopped")
sys.exit(EXIT_CODES["config"])
return EXIT_CODES.config


def script_entrypoint() -> NoReturn:
"""Script entrypoint for this tool"""
try:
progname = os.path.basename(sys.argv[0])
res = main(progname=progname, arguments=sys.argv[1:])
sys.exit(res.value)
except KeyboardInterrupt:
sys.exit(EXIT_CODES.interrupt.value)


if __name__ == "__main__":
main()
# Run the script entrypoint if executed manually
script_entrypoint()
Loading

0 comments on commit c24d07f

Please sign in to comment.