blob: 4bc1c0816883eea8139db43816bced9ba4483c82 [file]
#!/usr/bin/env python3
from abc import abstractmethod
import argparse
import base64
from enum import Enum
import hashlib
import itertools
import json
from pathlib import Path
import textwrap
from typing import ClassVar
import weakref
import attrs
##############################################################################
#
# Attribute-based class helpers
#
attrclass = attrs.define(frozen=True, kw_only=True)
def auto_converter(typ):
"""Define an automatic converter"""
if attrs.has(typ):
has_parent = ("_parent" in attrs.fields_dict(typ))
def converter(value, self):
if has_parent:
value["_parent"] = self
return typ(**value)
else:
def converter(value, self):
return typ(value)
return converter
def scalar_field(typ, **kwargs):
"""Define an auto-converting field holding a single scalar value"""
converter = attrs.Converter(auto_converter(typ), takes_self=True)
if "default" in kwargs:
converter = attrs.converters.optional(converter)
return attrs.field(converter=converter, **kwargs)
def list_field(typ, **kwargs):
"""Define an auto-converting field holding an ordered list of values"""
subconverter = auto_converter(typ)
converter = attrs.Converter(
lambda l, self: [subconverter(x, self) for x in l],
takes_self=True,
)
return attrs.field(converter=converter, **kwargs)
def set_field(typ, **kwargs):
"""Define an auto-converting field holding an unordered set of values"""
subconverter = auto_converter(typ)
converter = attrs.Converter(
lambda l, self: {subconverter(x, self) for x in l},
takes_self=True,
)
return attrs.field(converter=converter, **kwargs)
def map_field(typ, key=str, **kwargs):
"""Define an auto-converting field holding a map of values"""
subconverter = auto_converter(typ)
keyconverter = auto_converter(key)
converter = attrs.Converter(
lambda d, self: {
keyconverter(k, self): subconverter(v, self)
for k, v in d.items()
},
takes_self=True,
)
return attrs.field(converter=converter, **kwargs)
##############################################################################
#
# iPXE algorithm mappings
#
class Algorithm(Enum):
"""An iPXE algorithm"""
@property
def basename(self):
"""Base algorithm name"""
return self.name.lower()
@property
def symbol(self):
"""Algorithm symbol"""
return "%s_algorithm" % self.basename
@property
def label(self):
"""Label for use in comments"""
return self.value
class CipherAlgorithm(Algorithm):
"""An iPXE cipher algorithm"""
AES_GCM = "AES-GCM"
class DigestAlgorithm(Algorithm):
"""An iPXE digest algorithm"""
SHA1 = "SHA-1"
SHA224 = "SHA-224"
SHA256 = "SHA-256"
SHA384 = "SHA-384"
SHA512 = "SHA-512"
SHA512_224 = "SHA-512/224"
SHA512_256 = "SHA-512/256"
class HmacDigestAlgorithm(Algorithm):
"""An iPXE digest algorithm used with HMAC"""
SHA1 = "HMACSHA1"
SHA224 = "HMACSHA224"
SHA256 = "HMACSHA256"
SHA384 = "HMACSHA384"
SHA512 = "HMACSHA512"
SHA512_224 = "HMACSHA512/224"
SHA512_256 = "HMACSHA512/256"
@property
def label(self):
"""Label for use in comments"""
return "HMAC-%s" % self.name.replace("_", "/")
class HkdfDigestAlgorithm(Algorithm):
"""An iPXE digest algorithm used with HKDF"""
SHA1 = "HKDF-SHA-1"
SHA256 = "HKDF-SHA-256"
SHA384 = "HKDF-SHA-384"
SHA512 = "HKDF-SHA-512"
@property
def label(self):
"""Label for use in comments"""
return "HKDF-%s" % self.name
class DiffieHellmanAlgorithm(Algorithm):
"""A Diffie-Hellman key exchange algorithm family"""
ECDH = "ECDH"
XDH = "XDH"
class ExchangeAlgorithm(Algorithm):
"""An iPXE key exchange algorithm"""
P256 = "secp256r1"
P384 = "secp384r1"
X25519 = "curve25519"
@property
def label(self):
"""Label for use in comments"""
return self.name
class PubkeyAlgorithm(Algorithm):
"""An iPXE public-key algorithm"""
RSA_ES_PKCS1 = "RSAES-PKCS1-v1_5"
RSA_SSA_PKCS1 = "RSASSA-PKCS1-v1_5"
@property
def basename(self):
"""Base algorithm name"""
return super().basename.split("_")[0]
##############################################################################
#
# Common types
#
class HexBytes(bytes):
"""A hex-encoded byte string"""
def __new__(cls, value):
if isinstance(value, str):
value = base64.b16decode(value, casefold=True)
return super().__new__(cls, value)
def __str__(self):
return base64.b16encode(self).decode().lower()
def source(self, prefix, size=0, width=80):
"""Generate source code fragment"""
pre = prefix.expandtabs() + " ( "
mid = " " * len(pre)
post = " )"
count = (width - len(pre + post)) // len("0x00, ")
value = self[-size:].rjust(size, b"\x00")
code = (
pre +
(",\n" + mid).join(
", ".join("0x%02x" % x for x in batch)
for batch in itertools.batched(value, count)
) +
post
).replace("\t".expandtabs(), "\t").replace(" ( )", "()")
return code
class TestResult(Enum):
"""A test result"""
ACCEPTABLE = "acceptable"
VALID = "valid"
INVALID = "invalid"
class TestBugType(Enum):
"""A test bug type"""
AUTH_BYPASS = "AUTH_BYPASS"
BASIC = "BASIC"
BER_ENCODING = "BER_ENCODING"
CAN_OF_WORMS = "CAN_OF_WORMS"
CONFIDENTIALITY = "CONFIDENTIALITY"
DEFINED = "DEFINED"
EDGE_CASE = "EDGE_CASE"
FUNCTIONALITY = "FUNCTIONALITY"
KNOWN_BUG = "KNOWN_BUG"
LEGACY = "LEGACY"
MALLEABILITY = "MALLEABILITY"
MISSING_PARAMETER = "MISSING_PARAMETER"
MISSING_STEP = "MISSING_STEP"
MODIFIED_PARAMETER = "MODIFIED_PARAMETER"
SIGNATURE_MALLEABILITY = "SIGNATURE_MALLEABILITY"
UNKNOWN = "UNKNOWN"
WEAK_PARAMS = "WEAK_PARAMS"
WRONG_PRIMITIVE = "WRONG_PRIMITIVE"
class TestFlag(Enum):
"""A test flag"""
ADDITION_CHAIN = "AdditionChain"
BER_ENCODED_PADDING = "BerEncodedPadding"
COMPRESSED_POINT = "CompressedPoint"
COMPRESSED_PUBLIC = "CompressedPublic"
COUNTER_WRAP = "CounterWrap"
CVS_2017_8932 = "CVE-2017-8932"
CVE_2020_14967 = "CVE-2020-14967"
CVE_2021_3580 = "CVE-2021-3580"
EDGE_CASE_DOUBLING = "EdgeCaseDoubling"
EDGE_CASE_EPHEMERAL_KEY = "EdgeCaseEphemeralKey"
EDGE_CASE_MULTIPLICATION = "EdgeCaseMultiplication"
EDGE_CASE_PRIVATE_KEY = "EdgeCasePrivateKey"
EDGE_CASE_SHARED = "EdgeCaseShared"
EDGE_CASE_SHARED_SECRET = "EdgeCaseSharedSecret"
EDGE_CASE_SIGNAGTURE = "EdgeCaseSignature"
EMPTY_SALT = "EmptySalt"
INVALID_ASN_IN_PADDING = "InvalidAsnInPadding"
INVALID_CIPHERTEXT_FORMAT = "InvalidCiphertextFormat"
INVALID_COMPRESSED_PUBLIC = "InvalidCompressedPublic"
INVALID_CURVE_ATTACK = "InvalidCurveAttack"
INVALID_ENCODING = "InvalidEncoding"
INVALID_PADDING = "InvalidPadding"
INVALID_PKCS1_PADDING = "InvalidPkcs1Padding"
INVALID_SIGNATURE = "InvalidSignature"
KTV = "Ktv"
LONG_IV = "LongIv"
LOW_ORDER_PUBLIC = "LowOrderPublic"
MAXIMAL_OUTPUT_SIZE = "MaximalOutputSize"
MISSING_NULL = "MissingNull"
MODIFIED_PADDING = "ModifiedPadding"
MODIFIED_TAG = "ModifiedTag"
NO_HASH = "NoHash"
NON_CANONICAL_PUBLIC = "NonCanonicalPublic"
NORMAL = "Normal"
OUTPUT_COLLISION = "OutputCollision"
PSEUDORANDOM = "Pseudorandom"
SHORT_PADDING = "ShortPadding"
SIGNATURE_MALLEABILITY = "SignatureMalleability"
SIZE_TOO_LARGE = "SizeTooLarge"
SMALL_IV = "SmallIv"
SMALL_MODULUS = "SmallModulus"
SMALL_PUBLIC_KEY = "SmallPublicKey"
SMALL_SIGNATURE = "SmallSignature"
SPECIAL_CASE_PADDING = "SpecialCasePadding"
SPECIAL_CASE = "SpecialCase"
SPECIAL_PUBLIC_KEY = "SpecialPublicKey"
SSLV23_PADDING = "Sslv23Padding"
TRUNCATED_HMAC = "TruncatedHmac"
TWIST = "Twist"
WEAK_HASH = "WeakHash"
WRONG_CURVE = "WrongCurve"
WRONG_HASH = "WrongHash"
WRONG_PRIMITIVE = "WrongPrimitive"
ZERO_LENGTH_IV = "ZeroLengthIv"
ZERO_SHARED_SECRET = "ZeroSharedSecret"
@attrclass
class TestNote:
"""A test note"""
bugType = scalar_field(TestBugType)
cves = list_field(str, factory=list)
description = scalar_field(str, default=None)
effect = scalar_field(str, default=None)
links = list_field(str, factory=list)
@attrclass
class TestSource:
"""A test source"""
name = scalar_field(str)
version = scalar_field(str)
@attrclass
class TestNamedObject:
"""A test named object"""
@property
@abstractmethod
def test_file(self):
"""Containing test file"""
@property
def stable_id(self):
"""Calculate a stable test ID
The tcId provides a stable ordering but not a stable
numbering. To avoid unnecessary churn when tests are
renumbered upstream, we construct a stable identifier built
from the relevant parameters of the test itself.
"""
props = {
field.name.encode(): str(getattr(self, field.name)).encode()
for field in attrs.fields(type(self))
if field.metadata.get("stable")
}
digest = hashlib.sha256()
digest.update(b":".join(itertools.chain(
(
b"algorithm:%s" % algorithm.basename.encode()
for algorithm in self.test_file.algorithms
),
(
b"%s:%d:%s" % (name, len(value), value)
for name, value in sorted(props.items())
),
)))
return digest.hexdigest()[:8]
@property
def test_name(self):
"""Test case name"""
return "wycheproof_%s_%s" % (self.test_file.basename, self.stable_id)
@attrclass
class TestCase(TestNamedObject):
"""A test case"""
_parent = scalar_field(weakref.proxy, alias="_parent")
comment = scalar_field(str)
flags = set_field(TestFlag)
result = scalar_field(TestResult)
tcId = scalar_field(int)
@property
def test_group(self):
"""Containing test group"""
return self._parent
@property
def test_file(self):
"""Containing test file"""
return self.test_group.test_file
def validate_fixed(self, attr, value, size):
"""Validate a fixed-size attribute"""
if self.skip:
return
if len(value) != size:
raise ValueError(
"%s: incorrect size %d (expected %d) for %s" %
(attr.name, len(value), size, value)
)
def validate_padded(self, attr, value, size):
"""Validate a potentially zero-padded attribute"""
if self.skip:
return
if any(x for x in value[:-size]):
raise ValueError(
"%s: non-zero leading padding in %s" %
(attr.name, value)
)
@property
def skip(self):
"""Reason for skipping test (if any)"""
return None
@property
def failure(self):
"""Check if test case is expected to fail"""
return self.result is TestResult.INVALID
def definition(self):
"""Generate source code for test definition
We include the tcId within the source code comment to enable
easy traceability of any failures back to the upstream test
case.
"""
skip = self.skip
code = (
"/* %s test case %d" % (self.test_file.label, self.tcId) +
(" (skipped: %s)" % skip if skip else "") +
" */\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
return ""
@attrclass
class TestGroup(TestNamedObject):
"""A test group"""
_parent = scalar_field(weakref.proxy, alias="_parent")
source = scalar_field(TestSource)
tests = list_field(TestCase)
type = scalar_field(str)
@property
def test_file(self):
"""Containing test file"""
return self._parent
def definition(self):
"""Generate source code for test group definition"""
code = "\n".join(x.definition() for x in self.tests)
return code
@attrclass
class TestFile:
"""A test file"""
SCHEMA: ClassVar = None
filename = scalar_field(str)
algorithm = scalar_field(Algorithm)
header = list_field(str, factory=list)
notes = map_field(TestNote, key=TestFlag, factory=dict)
numberOfTests = scalar_field(int)
schema = scalar_field(str)
testGroups = list_field(TestGroup)
@schema.validator
def validate_schema(self, attr, value):
"""Validate schema"""
if value != self.SCHEMA:
raise ValueError(
"%s: found schema %s (expected %s)" %
(self.filename, value, self.SCHEMA)
)
@numberOfTests.validator
def validate_number_of_tests(self, attr, value):
"""Validate number of tests"""
if value != len(self.tests):
raise ValueError(
"%s: found %d tests (expected %d)" %
(self.filename, len(self.tests), value)
)
@property
def algorithms(self):
"""All algorithms"""
return [self.algorithm]
@property
def basename(self):
"""Base name for test cases"""
return "_".join(x.basename for x in self.algorithms)
@property
def label(self):
"""Label for use in comments"""
return " / ".join(x.label for x in self.algorithms)
@property
def tests(self):
"""All test cases"""
return [test for group in self.testGroups for test in group.tests]
def source(self):
"""Generate source code"""
generator = Path(__file__).name
testname = "wycheproof_%s" % self.basename
execname = "%s_exec" % testname
label = self.label
definitions = "\n".join(x.definition() for x in self.testGroups)
invocations = "".join(x.invocation() for x in self.tests if not x.skip)
code = (
textwrap.dedent(f"""
/* This file is automatically generated by {generator}.
*
* DO NOT EDIT THIS FILE. Any changes will be lost.
*
*/
#include "../wycheproof_test.h"
""").lstrip() +
definitions +
textwrap.dedent(f"""
/** Perform Wycheproof {label} self-tests */
static void {execname} ( void ) {{
\t/* Perform tests in tcId order */
""") +
invocations +
"}\n" +
textwrap.dedent(f"""
/** Wycheproof {label} self-tests */
struct self_test {testname} __self_test = {{
\t.name = "{testname}",
\t.exec = {execname},
}};
REQUIRING_SYMBOL ( {testname} );
""")
)
return code
@classmethod
def load(cls, fh, **kwargs):
"""Load from JSON input file handle"""
data = json.load(fh)
return cls(**data, **kwargs)
@classmethod
def read(cls, filename, **kwargs):
"""Read from JSON input file"""
with open(filename, "rt") as fh:
return cls.load(fh, filename=filename, **kwargs)
def write(self, filename):
"""Write source output file"""
with open(filename, "wt") as fh:
fh.write(self.source())
##############################################################################
#
# Key exchange tests
#
@attrclass
class ExchangeTestCase(TestCase):
"""A key exchange test case"""
private = scalar_field(HexBytes, metadata={"stable": True})
public = scalar_field(HexBytes, metadata={"stable": True})
shared = scalar_field(HexBytes)
@private.validator
def validate_private(self, attr, value):
"""Validate private key size"""
self.validate_padded(attr, value, self.test_file.privsize)
@public.validator
def validate_public(self, attr, value):
"""Validate public key size"""
self.validate_fixed(attr, value, self.test_file.pubsize)
@shared.validator
def validate_shared(self, attr, value):
"""Validate shared key size"""
if not self.failure:
self.validate_fixed(attr, value, self.test_file.sharedsize)
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
if not self.skip:
algorithm = self.test_group.curve.symbol
privsize = self.test_file.privsize
pubsize = self.test_file.pubsize
sharedsize = self.test_file.sharedsize
code += (
"EXCHANGE_TEST ( %s, &%s,\n" % (self.test_name, algorithm) +
self.private.source("\tPRIVATE", privsize) + ",\n" +
self.public.source("\tPARTNER", pubsize) + ",\n" +
"\tPUBLIC_UNSPECIFIED,\n" +
("\tSHARED_FAIL" if self.failure else
self.shared.source("\tSHARED", sharedsize)) + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
code += "\texchange_ok ( &%s );\n" % self.test_name
return code
@attrclass
class ExchangeTestGroup(TestGroup):
"""A key exchange test group"""
curve = scalar_field(ExchangeAlgorithm)
tests = list_field(ExchangeTestCase)
@attrclass
class ExchangeTestFile(TestFile):
"""A key exchange test file"""
privsize = scalar_field(int)
pubsize = scalar_field(int)
sharedsize = scalar_field(int)
algorithm = scalar_field(DiffieHellmanAlgorithm)
testGroups = list_field(ExchangeTestGroup)
@property
def algorithms(self):
"""All algorithms"""
return sorted({x.curve for x in self.testGroups})
##############################################################################
#
# NIST key exchange tests
#
@attrclass
class NistExchangeTestCase(ExchangeTestCase):
"""A NIST elliptic curve key exchange test case"""
@property
def skip(self):
"""Reason for skipping test (if any)"""
if not self.public:
return "no public key"
if self.public[0] in (0x02, 0x03):
return "compressed"
@attrclass
class NistExchangeTestGroup(ExchangeTestGroup):
"""A NIST elliptic curve key exchange test group"""
encoding = scalar_field(str)
tests = list_field(NistExchangeTestCase)
@attrclass
class NistExchangeTestFile(ExchangeTestFile):
"""A NIST elliptic curve key exchange test file"""
SCHEMA: ClassVar = "ecdh_ecpoint_test_schema_v1.json"
testGroups = list_field(NistExchangeTestGroup)
##############################################################################
#
# XDH key exchange tests
#
@attrclass
class XdhTestCase(ExchangeTestCase):
"""An XDH key exchange test case"""
@property
def failure(self):
"""Check if test case is expected to fail"""
return TestFlag.ZERO_SHARED_SECRET in self.flags
@attrclass
class XdhTestGroup(ExchangeTestGroup):
"""An XDH key exchange test group"""
tests = list_field(XdhTestCase)
@attrclass
class XdhTestFile(ExchangeTestFile):
"""An XDH key exchange test file"""
SCHEMA: ClassVar = "xdh_comp_schema_v1.json"
testGroups = list_field(XdhTestGroup)
##############################################################################
#
# HMAC tests
#
@attrclass
class HmacTestCase(TestCase):
"""An HMAC test case"""
key = scalar_field(HexBytes, metadata={"stable": True})
msg = scalar_field(HexBytes, metadata={"stable": True})
tag = scalar_field(HexBytes)
@key.validator
def validate_key(self, attr, value):
"""Validate key size"""
self.validate_fixed(attr, value, (self.test_group.keySize // 8))
@tag.validator
def validate_tag(self, attr, value):
"""Validate tag size"""
self.validate_fixed(attr, value, (self.test_group.tagSize // 8))
@property
def skip(self):
"""Reason for skipping test (if any)"""
if TestFlag.MODIFIED_TAG in self.flags:
# Our HMAC abstraction covers only generating the digest,
# not comparing the output to check for a match
return "modified tag"
if self.failure:
# The test suite includes other failures such as using the
# wrong algorithm, which is not a meaningful test
return self.comment
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
if not self.skip:
algorithm = self.test_file.algorithm.symbol
code += (
"HMAC_TEST ( %s, &%s,\n" % (self.test_name, algorithm) +
self.key.source("\tKEY") + ",\n" +
self.msg.source("\tDATA") + ",\n" +
self.tag.source("\tEXPECTED") + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
code += "\thmac_ok ( &%s );\n" % self.test_name
return code
@attrclass
class HmacTestGroup(TestGroup):
"""An HMAC test group"""
keySize = scalar_field(int)
tagSize = scalar_field(int)
tests = list_field(HmacTestCase)
@attrclass
class HmacTestFile(TestFile):
"""An HMAC test file"""
SCHEMA: ClassVar = "mac_test_schema_v1.json"
algorithm = scalar_field(HmacDigestAlgorithm)
testGroups = list_field(HmacTestGroup)
@property
def basename(self):
"""Base name for test cases"""
return "hmac_%s" % super().basename
##############################################################################
#
# HKDF tests
#
@attrclass
class HkdfTestCase(TestCase):
"""An HKDF test case"""
ikm = scalar_field(HexBytes, metadata={"stable": True})
salt = scalar_field(HexBytes, metadata={"stable": True})
info = scalar_field(HexBytes, metadata={"stable": True})
size = scalar_field(int, metadata={"stable": True})
okm = scalar_field(HexBytes)
@ikm.validator
def validate_ikm(self, attr, value):
"""Validate key size"""
self.validate_fixed(attr, value, (self.test_group.keySize // 8))
@property
def skip(self):
"""Reason for skipping test (if any)"""
if TestFlag.SIZE_TOO_LARGE in self.flags:
# Our HKDF abstraction does not perform runtime checks for
# the output key material size
return "size too large"
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
if not self.skip:
algorithm = self.test_file.algorithm.symbol
salted = (len(self.salt) > 0)
code += (
"HKDF_TEST ( %s, &%s, %d,\n" % (
self.test_name, algorithm, salted
) +
self.ikm.source("\tIKM") + ",\n" +
self.salt.source("\tSALT") + ",\n" +
self.info.source("\tINFO") + ",\n" +
"\tPRK_UNSPECIFIED,\n" +
self.okm.source("\tOKM") + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
code += "\thkdf_ok ( &%s );\n" % self.test_name
return code
@attrclass
class HkdfTestGroup(TestGroup):
"""An HKDF test group"""
keySize = scalar_field(int)
tests = list_field(HkdfTestCase)
@attrclass
class HkdfTestFile(TestFile):
"""An HKDF test file"""
SCHEMA: ClassVar = "hkdf_test_schema_v1.json"
algorithm = scalar_field(HkdfDigestAlgorithm)
testGroups = list_field(HkdfTestGroup)
@property
def basename(self):
"""Base name for test cases"""
return "hkdf_%s" % super().basename
##############################################################################
#
# AEAD cipher tests
#
@attrclass
class AeadCipherTestCase(TestCase):
"""An AEAD cipher test case"""
key = scalar_field(HexBytes, metadata={"stable": True})
iv = scalar_field(HexBytes, metadata={"stable": True})
aad = scalar_field(HexBytes, metadata={"stable": True})
msg = scalar_field(HexBytes, metadata={"stable": True})
ct = scalar_field(HexBytes)
tag = scalar_field(HexBytes)
@key.validator
def validate_key(self, attr, value):
"""Validate key size"""
self.validate_fixed(attr, value, (self.test_group.keySize // 8))
@iv.validator
def validate_iv(self, attr, value):
"""Validate IV size"""
self.validate_fixed(attr, value, (self.test_group.ivSize // 8))
@tag.validator
def validate_tag(self, attr, value):
"""Validate tag size"""
self.validate_fixed(attr, value, (self.test_group.tagSize // 8))
@property
def skip(self):
"""Reason for skipping test (if any)"""
if TestFlag.MODIFIED_TAG in self.flags:
# Our cipher abstraction covers only generating the tag,
# not comparing the tag to check for a match
return "modified tag"
@property
def key_failure(self):
"""Check if test case is expected to fail due to invalid key"""
return False
@property
def iv_failure(self):
"""Check if test case is expected to fail due to invalid IV"""
return False
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
if not self.skip:
algorithm = self.test_file.algorithm.symbol
code += (
"CIPHER_TEST ( %s, &%s,\n" % (self.test_name, algorithm) +
self.key.source("\tKEY") + ",\n" +
self.iv.source("\tIV") + ",\n" +
self.aad.source("\tADDITIONAL") + ",\n" +
self.msg.source("\tPLAINTEXT") + ",\n" +
self.ct.source("\tCIPHERTEXT") + ",\n" +
self.tag.source("\tAUTH") + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
if self.key_failure:
code += "\tcipher_key_fail_ok ( &%s );\n" % self.test_name
elif self.iv_failure:
code += "\tcipher_iv_fail_ok ( &%s );\n" % self.test_name
elif self.failure:
raise ValueError("%d: unknown cipher failure reason" % self.tcId)
else:
code += "\tcipher_ok ( &%s );\n" % self.test_name
return code
@attrclass
class AeadCipherTestGroup(TestGroup):
"""An AEAD cipher test group"""
ivSize = scalar_field(int)
keySize = scalar_field(int)
tagSize = scalar_field(int)
tests = list_field(AeadCipherTestCase)
@attrclass
class AeadCipherTestFile(TestFile):
"""An AEAD cipher test file"""
SCHEMA: ClassVar = "aead_test_schema_v1.json"
algorithm = scalar_field(CipherAlgorithm)
testGroups = list_field(AeadCipherTestGroup)
##############################################################################
#
# GCM cipher tests
#
@attrclass
class GcmCipherTestCase(AeadCipherTestCase):
"""A GCM cipher test case"""
@property
def iv_failure(self):
"""Check if test case is expected to fail due to invalid IV"""
return TestFlag.ZERO_LENGTH_IV in self.flags
@attrclass
class GcmCipherTestGroup(AeadCipherTestGroup):
"""A GCM cipher test group"""
tests = list_field(GcmCipherTestCase)
@attrclass
class GcmCipherTestFile(AeadCipherTestFile):
"""A GCM cipher test file"""
testGroups = list_field(GcmCipherTestGroup)
##############################################################################
#
# RSA PKCS#1 tests
#
@attrclass
class RsaPkcs1TestGroup(TestGroup):
"""An RSA PKCS#1 test group"""
keySize = scalar_field(int)
@attrclass
class RsaPkcs1TestFile(TestFile):
"""An RSA PKCS#1 test file"""
algorithm = scalar_field(PubkeyAlgorithm)
testGroups = list_field(RsaPkcs1TestGroup)
@property
def keysizes(self):
"""All key sizes"""
return sorted({x.keySize for x in self.testGroups})
@property
def basename(self):
"""Base name for test cases"""
return "rsa_pkcs1_%s" % "_".join("%d" % x for x in self.keysizes)
@property
def label(self):
"""Label for use in comments"""
return "%s-bit" % "/".join("%d" % x for x in self.keysizes)
##############################################################################
#
# RSA PKCS#1 decryption tests
#
@attrclass
class RsaPkcs1DecryptTestCase(TestCase):
"""An RSA PKCS#1 decryption test case"""
msg = scalar_field(HexBytes, metadata={"stable": True})
ct = scalar_field(HexBytes, metadata={"stable": True})
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
code += (
"PUBKEY_ENCRYPTION_TEST ( %s,\n" % self.test_name +
"\t&%s, RANDOM(),\n" % self.test_group.test_name +
self.msg.source("\tPLAINTEXT") + ",\n" +
self.ct.source("\tCIPHERTEXT") + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
if self.failure:
code += "\tpubkey_decrypt_fail_ok ( &%s );\n" % self.test_name
else:
code += "\tpubkey_decrypt_ok ( &%s );\n" % self.test_name
return code
@attrclass
class RsaPkcs1DecryptTestGroup(RsaPkcs1TestGroup):
"""An RSA PKCS#1 decryption test group"""
privateKey = map_field(str) # ignored
privateKeyPkcs8 = scalar_field(HexBytes, metadata={"stable": True})
privateKeyPem = scalar_field(str) # ignored
privateKeyJwk = map_field(str) # ignored
tests = list_field(RsaPkcs1DecryptTestCase)
def definition(self):
"""Generate source code for test group definition"""
algorithm = self.test_file.algorithm.symbol
code = (
"/* Private key for following tests */\n" +
"PUBKEY_TEST ( %s, &%s,\n" % (self.test_name, algorithm) +
self.privateKeyPkcs8.source("\tPRIVATE") + ",\n" +
"\tPUBLIC() );\n" +
"\n" +
super().definition()
)
return code
@attrclass
class RsaPkcs1DecryptTestFile(RsaPkcs1TestFile):
"""An RSA PKCS#1 decryption test file"""
SCHEMA: ClassVar = "rsaes_pkcs1_decrypt_schema_v1.json"
testGroups = list_field(RsaPkcs1DecryptTestGroup)
@property
def basename(self):
"""Base name for test cases"""
return "%s_decrypt" % super().basename
@property
def label(self):
"""Label for use in comments"""
return "RSA-PKCS#1 (%s)" % super().label
##############################################################################
#
# RSA PKCS#1 signing tests
#
@attrclass
class RsaPkcs1SignTestCase(TestCase):
"""An RSA PKCS#1 signing test case"""
msg = scalar_field(HexBytes, metadata={"stable": True})
sig = scalar_field(HexBytes, metadata={"stable": True})
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
digest = self.test_group.sha.symbol
code += (
"PUBKEY_SIGNATURE_TEST ( %s,\n" % self.test_name +
"\t&%s, RANDOM(),\n" % self.test_group.test_name +
self.msg.source("\tPLAINTEXT") + ",\n" +
"\t&%s,\n" % digest +
self.sig.source("\tSIGNATURE") + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
code += "\tpubkey_sign_verify_ok ( &%s );\n" % self.test_name
return code
@attrclass
class RsaPkcs1SignTestGroup(RsaPkcs1TestGroup):
"""An RSA PKCS#1 signing test group"""
privateKey = map_field(str) # ignored
keyAsn = scalar_field(str) # ignored
keyDer = scalar_field(HexBytes, metadata={"stable": True})
keyJwk = map_field(str, factory=dict) # ignored
keyPem = scalar_field(str) # ignored
privateKeyJwk = map_field(str, factory=dict) # ignored
privateKeyPem = scalar_field(str) # ignored
privateKeyPkcs8 = scalar_field(HexBytes, metadata={"stable": True})
sha = scalar_field(DigestAlgorithm)
tests = list_field(RsaPkcs1SignTestCase)
def definition(self):
"""Generate source code for test group definition"""
algorithm = self.test_file.algorithm.symbol
code = (
"/* Key pair for following tests */\n" +
"PUBKEY_TEST ( %s, &%s,\n" % (self.test_name, algorithm) +
self.privateKeyPkcs8.source("\tPRIVATE") + ",\n" +
self.keyDer.source("\tPUBLIC") + " );\n" +
"\n" +
super().definition()
)
return code
@attrclass
class RsaPkcs1SignTestFile(RsaPkcs1TestFile):
"""An RSA PKCS#1 signing test file"""
SCHEMA: ClassVar = "rsassa_pkcs1_generate_schema_v1.json"
testGroups = list_field(RsaPkcs1SignTestGroup)
@property
def basename(self):
"""Base name for test cases"""
return "%s_sign" % super().basename
@property
def label(self):
"""Label for use in comments"""
return "RSA-PKCS#1 signing (%s)" % super().label
##############################################################################
#
# RSA PKCS#1 verification tests
#
@attrclass
class RsaPkcs1VerifyTestCase(TestCase):
"""An RSA PKCS#1 verification test case"""
msg = scalar_field(HexBytes, metadata={"stable": True})
sig = scalar_field(HexBytes, metadata={"stable": True})
@property
def failure(self):
"""Check if test case is expected to fail"""
failure = super().failure or TestFlag.MISSING_NULL in self.flags
return failure
def definition(self):
"""Generate source code for test definition"""
code = super().definition()
digest = self.test_group.sha.symbol
code += (
"PUBKEY_SIGNATURE_TEST ( %s,\n" % self.test_name +
"\t&%s, RANDOM(),\n" % self.test_group.test_name +
self.msg.source("\tPLAINTEXT") + ",\n" +
"\t&%s,\n" % digest +
self.sig.source("\tSIGNATURE") + " );\n"
)
return code
def invocation(self):
"""Generate source code for test invocation"""
code = super().invocation()
if self.failure:
code += "\tpubkey_verify_fail_ok ( &%s );\n" % self.test_name
else:
code += "\tpubkey_verify_ok ( &%s );\n" % self.test_name
return code
@attrclass
class RsaPkcs1VerifyTestGroup(RsaPkcs1TestGroup):
"""An RSA PKCS#1 verification test group"""
publicKey = map_field(str) # ignored
publicKeyAsn = scalar_field(str) # ignored
publicKeyDer = scalar_field(HexBytes, metadata={"stable": True})
publicKeyPem = scalar_field(str) # ignored
keyDer = scalar_field(HexBytes, default=None)
keyJwk = map_field(str, factory=dict) # ignored
sha = scalar_field(DigestAlgorithm)
tests = list_field(RsaPkcs1VerifyTestCase)
def definition(self):
"""Generate source code for test group definition"""
algorithm = self.test_file.algorithm.symbol
code = (
"/* Key pair for following tests */\n" +
"PUBKEY_TEST ( %s, &%s,\n" % (self.test_name, algorithm) +
"\tPRIVATE(),\n" +
self.publicKeyDer.source("\tPUBLIC") + " );\n" +
"\n" +
super().definition()
)
return code
@attrclass
class RsaPkcs1VerifyTestFile(RsaPkcs1TestFile):
"""An RSA PKCS#1 verification test file"""
SCHEMA: ClassVar = "rsassa_pkcs1_verify_schema_v1.json"
testGroups = list_field(RsaPkcs1VerifyTestGroup)
@property
def digests(self):
"""All digest algorithms"""
return sorted({x.sha for x in self.testGroups})
@property
def basename(self):
"""Base name for test cases"""
digests = "_".join(x.basename for x in self.digests)
return "%s_%s_verify" % (super().basename, digests)
@property
def label(self):
"""Label for use in comments"""
digests = " / ".join(x.label for x in self.digests)
return "RSA-PKCS#1 %s verification (%s)" % (digests, super().label)
def source(self):
"""Generate source code"""
code = super().source()
digests = sorted({x.sha.basename for x in self.testGroups})
code += "\n".join((
"REQUIRE_OBJECT ( rsa_%s );\n" % digest
for digest in digests
))
return code
##############################################################################
#
# Main program
#
def main():
"""Main program"""
# Parse command-line arguments
parser = argparse.ArgumentParser(
description="Import Project Wycheproof test cases"
)
parser.add_argument("dir", help="Project Wycheproof repository checkout")
args = parser.parse_args()
# Locate Project Wycheproof source files
srcdir = Path(args.dir) / "testvectors_v1"
if not srcdir.exists():
raise FileNotFoundError(srcdir)
# Locate iPXE output directory
dstdir = Path(__file__).parent.parent / "wycheproof"
if not dstdir.exists():
raise FileNotFoundError(dstdir)
# Read JSON inputs
tests = [
GcmCipherTestFile.read(srcdir / "aes_gcm_test.json"),
HkdfTestFile.read(srcdir / "hkdf_sha1_test.json"),
HkdfTestFile.read(srcdir / "hkdf_sha256_test.json"),
HkdfTestFile.read(srcdir / "hkdf_sha384_test.json"),
HkdfTestFile.read(srcdir / "hkdf_sha512_test.json"),
HmacTestFile.read(srcdir / "hmac_sha1_test.json"),
HmacTestFile.read(srcdir / "hmac_sha224_test.json"),
HmacTestFile.read(srcdir / "hmac_sha256_test.json"),
HmacTestFile.read(srcdir / "hmac_sha384_test.json"),
HmacTestFile.read(srcdir / "hmac_sha512_test.json"),
HmacTestFile.read(srcdir / "hmac_sha512_224_test.json"),
HmacTestFile.read(srcdir / "hmac_sha512_256_test.json"),
NistExchangeTestFile.read(
srcdir / "ecdh_secp256r1_ecpoint_test.json",
privsize=32, pubsize=65, sharedsize=32,
),
NistExchangeTestFile.read(
srcdir / "ecdh_secp384r1_ecpoint_test.json",
privsize=48, pubsize=97, sharedsize=48,
),
RsaPkcs1DecryptTestFile.read(srcdir / "rsa_pkcs1_2048_test.json"),
RsaPkcs1DecryptTestFile.read(srcdir / "rsa_pkcs1_3072_test.json"),
RsaPkcs1DecryptTestFile.read(srcdir / "rsa_pkcs1_4096_test.json"),
RsaPkcs1SignTestFile.read(srcdir / "rsa_pkcs1_1024_sig_gen_test.json"),
RsaPkcs1SignTestFile.read(srcdir / "rsa_pkcs1_1536_sig_gen_test.json"),
RsaPkcs1SignTestFile.read(srcdir / "rsa_pkcs1_2048_sig_gen_test.json"),
RsaPkcs1SignTestFile.read(srcdir / "rsa_pkcs1_3072_sig_gen_test.json"),
RsaPkcs1SignTestFile.read(srcdir / "rsa_pkcs1_4096_sig_gen_test.json"),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_2048_sha224_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_2048_sha256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_2048_sha384_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_2048_sha512_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_2048_sha512_224_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_2048_sha512_256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_3072_sha256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_3072_sha384_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_3072_sha512_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_3072_sha512_256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_4096_sha256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_4096_sha384_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_4096_sha512_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_4096_sha512_256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_8192_sha256_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_8192_sha384_test.json"
),
RsaPkcs1VerifyTestFile.read(
srcdir / "rsa_signature_8192_sha512_test.json"
),
XdhTestFile.read(
srcdir / "x25519_test.json",
privsize=32, pubsize=32, sharedsize=32,
),
]
# Write source code outputs
for test in tests:
test.write(dstdir / ("wycheproof_%s.c" % test.basename))
if __name__ == "__main__":
main()