Files
Filip Skokan 9f9775e272 Bug 2040457 [wpt PR 59947] - [WebCrypto] add Hybrid KEMs, a=testonly
Automatic update from web-platform-tests
[WebCryptoAPI] Add tests for Hybrid KEMs (#59947)
--

wpt-commits: 55ce71bb9deaf4b8a30fe80f95558a10699fb433
wpt-pr: 59947
2026-09-11 14:45:34 +00:00

286 lines
8.9 KiB
Python

"""Generate the WebCryptoAPI generateKey registry and entrypoints."""
import argparse
import difflib
import json
from pathlib import Path
import sys
HERE = Path(__file__).resolve().parent
WEBCRYPTO_ROOT = HERE.parent
GENERATE_KEY_ROOT = WEBCRYPTO_ROOT / "generateKey"
ALGORITHMS = (
("AES-CTR", "CryptoKey", ("encrypt", "decrypt", "wrapKey", "unwrapKey"), ()),
("AES-CBC", "CryptoKey", ("encrypt", "decrypt", "wrapKey", "unwrapKey"), ()),
("AES-GCM", "CryptoKey", ("encrypt", "decrypt", "wrapKey", "unwrapKey"), ()),
("AES-OCB", "CryptoKey", ("encrypt", "decrypt", "wrapKey", "unwrapKey"), ()),
(
"ChaCha20-Poly1305",
"CryptoKey",
("encrypt", "decrypt", "wrapKey", "unwrapKey"),
(),
),
("AES-KW", "CryptoKey", ("wrapKey", "unwrapKey"), ()),
("HMAC", "CryptoKey", ("sign", "verify"), ()),
("RSASSA-PKCS1-v1_5", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
("RSA-PSS", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
(
"RSA-OAEP",
'"CryptoKeyPair"',
("encrypt", "decrypt", "wrapKey", "unwrapKey"),
("decrypt", "unwrapKey"),
),
("ECDSA", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
(
"ECDH",
'"CryptoKeyPair"',
("deriveKey", "deriveBits"),
("deriveKey", "deriveBits"),
),
("Ed25519", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
("Ed448", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
("ML-DSA-44", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
("ML-DSA-65", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
("ML-DSA-87", '"CryptoKeyPair"', ("sign", "verify"), ("sign",)),
(
"ML-KEM-512",
'"CryptoKeyPair"',
("decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"),
("decapsulateBits", "decapsulateKey"),
),
(
"ML-KEM-768",
'"CryptoKeyPair"',
("decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"),
("decapsulateBits", "decapsulateKey"),
),
(
"ML-KEM-1024",
'"CryptoKeyPair"',
("decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"),
("decapsulateBits", "decapsulateKey"),
),
(
"MLKEM768-P256",
'"CryptoKeyPair"',
("decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"),
("decapsulateBits", "decapsulateKey"),
),
(
"MLKEM768-X25519",
'"CryptoKeyPair"',
("decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"),
("decapsulateBits", "decapsulateKey"),
),
(
"MLKEM1024-P384",
'"CryptoKeyPair"',
("decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"),
("decapsulateBits", "decapsulateKey"),
),
(
"X25519",
'"CryptoKeyPair"',
("deriveKey", "deriveBits"),
("deriveKey", "deriveBits"),
),
(
"X448",
'"CryptoKeyPair"',
("deriveKey", "deriveBits"),
("deriveKey", "deriveBits"),
),
("KMAC128", "CryptoKey", ("sign", "verify"), ()),
("KMAC256", "CryptoKey", ("sign", "verify"), ()),
)
GROUPS = (
("AES-CTR", ("AES-CTR",), False),
("AES-CBC", ("AES-CBC",), False),
("AES-GCM", ("AES-GCM",), False),
("AES-OCB", ("AES-OCB",), True),
("chacha20_poly1305", ("ChaCha20-Poly1305",), True),
("AES-KW", ("AES-KW",), False),
("HMAC", ("HMAC",), False),
("RSASSA-PKCS1-v1_5", ("RSASSA-PKCS1-v1_5",), False),
("RSA-PSS", ("RSA-PSS",), False),
("RSA-OAEP", ("RSA-OAEP",), False),
("ECDSA", ("ECDSA",), False),
("ECDH", ("ECDH",), False),
("Ed25519", ("Ed25519",), False),
("Ed448", ("Ed448",), True),
("ML-DSA", ("ML-DSA-44", "ML-DSA-65", "ML-DSA-87"), True),
("ML-KEM", ("ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"), True),
(
"Hybrid-KEM",
("MLKEM768-P256", "MLKEM768-X25519", "MLKEM1024-P384"),
True,
),
("X25519", ("X25519",), False),
("X448", ("X448",), True),
("kmac", ("KMAC128", "KMAC256"), True),
)
def js_array(values):
return json.dumps(list(values))
# Algorithm normalization failures are independent of the algorithm under test,
# so they get a single wrapper instead of one per group.
BAD_ALGORITHM_WRAPPER = "\n".join(
[
"// META: title=WebCryptoAPI: generateKey() for Failures",
"// META: timeout=long",
"// META: script=../util/helpers.js",
"// META: script=failures.js",
"run_bad_algorithm_test();",
"",
]
)
def generate_registry():
lines = [
"// Generated by WebCryptoAPI/tools/generate.py. Do not edit directly.",
"const generateKeyTestVectors = [",
]
for name, result_type, usages, mandatory_usages in ALGORITHMS:
lines.append(
f" {{name: {json.dumps(name)}, resultType: {result_type}, "
f"usages: {js_array(usages)}, "
f"mandatoryUsages: {js_array(mandatory_usages)}}},"
)
lines.extend(
[
"];",
"",
"function getGenerateKeyTestVectors(algorithmNames) {",
" if (algorithmNames && !Array.isArray(algorithmNames)) {",
" algorithmNames = [algorithmNames];",
" }",
"",
" return generateKeyTestVectors.filter(",
" vector => !algorithmNames || algorithmNames.includes(vector.name));",
"}",
"",
]
)
return "\n".join(lines)
def success_variants(file_id):
if file_id == "RSA-OAEP":
return [f"?{start}-{start + 9}" for start in range(1, 151, 10)] + [
"?151-last"
]
if file_id in {"RSA-PSS", "RSASSA-PKCS1-v1_5"}:
return ["?1-10", "?11-20", "?21-30", "?31-last"]
return []
def generate_wrapper(kind, file_id, algorithms):
title = (
"WebCryptoAPI: generateKey() Successful Calls"
if kind == "successes"
else "WebCryptoAPI: generateKey() for Failures"
)
lines = [
f"// META: title={title}",
"// META: timeout=long",
]
if kind == "successes":
lines.extend(
f"// META: variant={variant}" for variant in success_variants(file_id)
)
lines.append("// META: script=../util/helpers.js")
if kind == "successes":
lines.append("// META: script=/common/subset-tests.js")
lines.extend(
[
"// META: script=algorithm_registry.js",
f"// META: script={kind}.js",
f"run_test({js_array(algorithms)});",
"",
]
)
return "\n".join(lines)
def expected_outputs():
outputs = {
GENERATE_KEY_ROOT / "algorithm_registry.js": generate_registry(),
GENERATE_KEY_ROOT
/ "failures_bad_algorithm.https.any.js": BAD_ALGORITHM_WRAPPER,
}
for file_id, algorithms, tentative in GROUPS:
marker = ".tentative" if tentative else ""
for kind in ("successes", "failures"):
path = GENERATE_KEY_ROOT / f"{kind}_{file_id}{marker}.https.any.js"
outputs[path] = generate_wrapper(kind, file_id, algorithms)
return outputs
def wrapper_paths():
return set(GENERATE_KEY_ROOT.glob("successes_*.https.any.js")) | set(
GENERATE_KEY_ROOT.glob("failures_*.https.any.js")
)
def show_diff(path, expected):
current = path.read_text(encoding="utf-8") if path.exists() else ""
relative = path.relative_to(WEBCRYPTO_ROOT)
sys.stdout.writelines(
difflib.unified_diff(
current.splitlines(keepends=True),
expected.splitlines(keepends=True),
fromfile=str(relative),
tofile=f"{relative} (generated)",
)
)
def check(outputs):
failed = False
for path, expected in outputs.items():
if not path.exists() or path.read_text(encoding="utf-8") != expected:
show_diff(path, expected)
failed = True
unexpected = wrapper_paths() - set(outputs)
for path in sorted(unexpected):
print(f"Unexpected generated wrapper: {path.relative_to(WEBCRYPTO_ROOT)}")
failed = True
return 1 if failed else 0
def write(outputs):
for path, expected in outputs.items():
if not path.exists() or path.read_text(encoding="utf-8") != expected:
path.write_text(expected, encoding="utf-8")
unexpected = wrapper_paths() - set(outputs)
for path in sorted(unexpected):
print(f"Unexpected generated wrapper: {path.relative_to(WEBCRYPTO_ROOT)}")
return 1 if unexpected else 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--check",
action="store_true",
help="check generated files without writing them",
)
args = parser.parse_args()
outputs = expected_outputs()
return check(outputs) if args.check else write(outputs)
if __name__ == "__main__":
raise SystemExit(main())