diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b18bbca8..38c964fa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -668,17 +668,20 @@ jobs: matrix: container: - id: amazonlinux-2-aarch:base - # TODO: Python 3.7 on Amazon Linux 2 lacks `sha512_224` in hashlib; set to false to skip acvp. - quickcheck: false - acvptest: false + # Python 3.7 on Amazon Linux 2 lacks `sha512_224` in hashlib; skip the affected acvp test cases. + quickcheck: true + acvptest: true + acvp_options: --skip-unsupported - id: amazonlinux-2-aarch:gcc-7x - # TODO: Python 3.7 on Amazon Linux 2 lacks `sha512_224` in hashlib; set to false to skip acvp. - quickcheck: false - acvptest: false + # Python 3.7 on Amazon Linux 2 lacks `sha512_224` in hashlib; skip the affected acvp test cases. + quickcheck: true + acvptest: true + acvp_options: --skip-unsupported - id: amazonlinux-2-aarch:clang-7x - # TODO: Python 3.7 on Amazon Linux 2 lacks `sha512_224` in hashlib; set to false to skip acvp. - quickcheck: false - acvptest: false + # Python 3.7 on Amazon Linux 2 lacks `sha512_224` in hashlib; skip the affected acvp test cases. + quickcheck: true + acvptest: true + acvp_options: --skip-unsupported - id: amazonlinux-2023-aarch:base quickcheck: true acvptest: true @@ -743,6 +746,7 @@ jobs: kattest: true acvptest: ${{ matrix.container.acvptest }} quickcheck: ${{ matrix.container.quickcheck }} + acvp_options: ${{ matrix.container.acvp_options }} lint: false verbose: true cflags: "-O0" diff --git a/.github/workflows/ci_ec2_container.yml b/.github/workflows/ci_ec2_container.yml index 906bd1b6e..519fce250 100644 --- a/.github/workflows/ci_ec2_container.yml +++ b/.github/workflows/ci_ec2_container.yml @@ -52,6 +52,10 @@ on: quickcheck: type: boolean default: true + acvp_options: + type: string + description: Extra options for the ACVP client (e.g. --skip-unsupported) + default: "" lint: type: boolean default: true @@ -158,6 +162,8 @@ jobs: runs-on: ${{ needs.start-ec2-runner.outputs.label }} container: localhost:5000/${{ inputs.container }} + env: + ACVP_OPTIONS: ${{ inputs.acvp_options }} steps: # We're not using the checkout action here because on it's not supported # on all containers we want to test. Resort to a manual checkout. diff --git a/Makefile b/Makefile index 0323fbd10..350476c08 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ run_unit_87: unit_87 run_unit: run_unit_44 run_unit_65 run_unit_87 run_acvp: acvp - EXEC_WRAPPER="$(EXEC_WRAPPER)" python3 ./test/acvp/acvp_client.py $(if $(ACVP_VERSION),--version $(ACVP_VERSION)) + EXEC_WRAPPER="$(EXEC_WRAPPER)" python3 ./test/acvp/acvp_client.py $(if $(ACVP_VERSION),--version $(ACVP_VERSION)) $(ACVP_OPTIONS) func_44: $(MLDSA44_DIR)/bin/test_mldsa44 $(Q)echo " FUNC ML-DSA-44: $^" diff --git a/test/acvp/acvp_client.py b/test/acvp/acvp_client.py index a036356b1..352c11e87 100644 --- a/test/acvp/acvp_client.py +++ b/test/acvp/acvp_client.py @@ -177,35 +177,76 @@ def run_keyGen_test(tg, tc): return results +# ACVP hashAlg -> (hashlib name, XOF output length in bytes or None). +HASH_ALG_TO_HASHLIB = { + "SHA2-224": ("sha224", None), + "SHA2-256": ("sha256", None), + "SHA2-384": ("sha384", None), + "SHA2-512": ("sha512", None), + "SHA2-512/224": ("sha512_224", None), + "SHA2-512/256": ("sha512_256", None), + "SHA3-224": ("sha3_224", None), + "SHA3-256": ("sha3_256", None), + "SHA3-384": ("sha3_384", None), + "SHA3-512": ("sha3_512", None), + "SHAKE-128": ("shake_128", 32), + "SHAKE-256": ("shake_256", 64), +} + + def compute_hash(msg, alg): - msg_bytes = bytes.fromhex(msg) - - if alg == "SHA2-224": - return hashlib.sha224(msg_bytes).hexdigest() - elif alg == "SHA2-256": - return hashlib.sha256(msg_bytes).hexdigest() - elif alg == "SHA2-384": - return hashlib.sha384(msg_bytes).hexdigest() - elif alg == "SHA2-512": - return hashlib.sha512(msg_bytes).hexdigest() - elif alg == "SHA2-512/224": - return hashlib.new("sha512_224", msg_bytes).hexdigest() - elif alg == "SHA2-512/256": - return hashlib.new("sha512_256", msg_bytes).hexdigest() - elif alg == "SHA3-224": - return hashlib.sha3_224(msg_bytes).hexdigest() - elif alg == "SHA3-256": - return hashlib.sha3_256(msg_bytes).hexdigest() - elif alg == "SHA3-384": - return hashlib.sha3_384(msg_bytes).hexdigest() - elif alg == "SHA3-512": - return hashlib.sha3_512(msg_bytes).hexdigest() - elif alg == "SHAKE-128": - return hashlib.shake_128(msg_bytes).hexdigest(32) - elif alg == "SHAKE-256": - return hashlib.shake_256(msg_bytes).hexdigest(64) - else: + if alg not in HASH_ALG_TO_HASHLIB: raise ValueError(f"Unsupported hash algorithm: {alg}") + name, xof_len = HASH_ALG_TO_HASHLIB[alg] + h = hashlib.new(name, bytes.fromhex(msg)) + return h.hexdigest() if xof_len is None else h.hexdigest(xof_len) + + +def hashlib_can_compute(hashAlg): + # SHAKE-256 pre-hash is computed inside the ACVP binary, not via hashlib. + if hashAlg in (None, "none", "SHAKE-256"): + return True + name, _ = HASH_ALG_TO_HASHLIB.get(hashAlg, (None, None)) + if name is None: + return False + try: + hashlib.new(name) + return True + except (ValueError, TypeError): + return False + + +def unwrap_acvts(data): + # ACVTS files wrap the payload as [{"acvVersion": ...}, {...}]. + return data[1] if isinstance(data, list) else data + + +def unsupported_test_cases(acvp_data): + # Return {(tgId, tcId)} for pre-hash cases whose hashAlg this platform + # cannot compute, plus the set of offending hashAlgs. + cases, algs = set(), set() + for _, promptData, _, _ in acvp_data: + for tg in unwrap_acvts(promptData).get("testGroups", []): + if tg.get("preHash") != "preHash": + continue + for tc in tg.get("tests", []): + if not hashlib_can_compute(tc.get("hashAlg")): + cases.add((tg["tgId"], tc["tcId"])) + algs.add(tc["hashAlg"]) + return cases, algs + + +def filter_test_cases(acvp_data, drop): + # Remove the given (tgId, tcId) cases from both prompt and expected + # results so the result comparison stays consistent. + for _, promptData, _, expectedData in acvp_data: + for data in (promptData, expectedData): + if data is None: + continue + for tg in unwrap_acvts(data).get("testGroups", []): + tg["tests"] = [ + tc for tc in tg["tests"] if (tg["tgId"], tc["tcId"]) not in drop + ] def run_sigGen_test(tg, tc): @@ -449,7 +490,9 @@ def runTest(data, output): info("ALL GOOD!") -def test(prompt, expected, output, version, supported_modes=None): +def test( + prompt, expected, output, version, supported_modes=None, skip_unsupported=False +): assert prompt is not None or output is None, ( "cannot produce output if there is no input" ) @@ -469,6 +512,22 @@ def test(prompt, expected, output, version, supported_modes=None): info("No test data to run (all modes disabled in this build)") return + drop, algs = unsupported_test_cases(data) + if drop: + algs = ", ".join(sorted(algs)) + if not skip_unsupported: + err( + f"Error: test data uses hash algorithm(s) unavailable in this " + f"Python's hashlib: {algs}." + ) + err("Re-run with --skip-unsupported to skip the affected test cases.") + exit(1) + info( + f"Skipping {len(drop)} test case(s) using unsupported hash " + f"algorithm(s): {algs}" + ) + filter_test_cases(data, drop) + runTest(data, output) @@ -512,6 +571,12 @@ def test(prompt, expected, output, version, supported_modes=None): default=True, help="Auto-detect supported modes by running acvp_mldsa44 --info (default: True)", ) +parser.add_argument( + "--skip-unsupported", + action="store_true", + help="Skip test cases whose hash algorithm is unavailable in this Python's " + "hashlib (e.g. SHA2-512/224, SHA2-512/256) instead of failing", +) args = parser.parse_args() # Determine supported modes @@ -537,4 +602,11 @@ def test(prompt, expected, output, version, supported_modes=None): print("Failed to download ACVP test files", file=sys.stderr) sys.exit(1) -test(args.prompt, args.expected, args.output, args.version, supported_modes) +test( + args.prompt, + args.expected, + args.output, + args.version, + supported_modes, + args.skip_unsupported, +)