Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci_ec2_container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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: $^"
Expand Down
130 changes: 101 additions & 29 deletions test/acvp/acvp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
)
Expand All @@ -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)


Expand Down Expand Up @@ -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
Expand All @@ -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,
)
Loading