ECDSA

How to use the crypto_condor.primitives.ECDSA module to test implementations of ECDSA.

Test signing

crypto_condor.primitives.ECDSA.test_sign(
sign,
curve,
hash_function,
key_encoding,
*,
pre_hashed=False,
compliance=True,
resilience=False,
)

Tests a function that signs with ECDSA.

The sign function is called to sign messages. Since ECDSA is non-deterministic (except when using RFC 6979), crypto-condor cannot compare the generated signatures with a reference value. Instead, it verifies the signatures with a reference implementation. The test passes if the signature is considered valid.

Note that this method allows to test the RFC 6979 deterministic version of ECDSA, as the signatures remain compatible. However, it only tests that the signatures produced are valid ECDSA signatures: they may be invalid for RFC 6979. Test vectors for deterministic ECDSA will be added.

Available test vectors depend on the given curve and hash function, see here.

Parameters:
  • sign (Sign) – The function to test.

  • curve (Curve) – The elliptic curve to use.

  • hash_function (Hash) – The hash function used to generate the signatures.

  • key_encoding (KeyEncoding) – A private key encoding accepted by the function.

Keyword Arguments:
  • pre_hashed – If True, the messages are hashed before passing them to sign.

  • compliance – Whether to use compliance test vectors.

  • resilience – Whether to use resilience test vectors.

Returns:

A dictionary of Results, one for each test vectors file used. May be empty if there are no test vectors for the given curve and hash function.

Return type:

ResultsDict

Example

Let’s test crypto-condor’s internal signer. First import the ECDSA module.

>>> from crypto_condor.primitives import ECDSA

We define the parameters we want to test (curve, hash, encoding).

>>> curve = ECDSA.Curve.P256
>>> hash_function = ECDSA.Hash.SHA256
>>> encoding = ECDSA.KeyEncoding.DER

Then wrap the function to match the expected signature, defined by Sign.

>>> def my_sign(private_key: bytes, message: bytes) -> bytes:
...     return ECDSA._sign(private_key, hash_function, message)

And test the function.

>>> results = ECDSA.test_sign(my_sign, curve, hash_function, encoding)
[P-256][SHA-256][NIST CAVP] Test signing ...
>>> assert results.check()

Test verifying

crypto_condor.primitives.ECDSA.test_verify(
verify,
curve,
hash_function,
pubkey_encoding,
*,
pre_hashed=False,
compliance=True,
resilience=False,
)

Tests a function that verifies ECDSA signatures.

The verify function is called to verify messages. The test passes if valid signatures are correctly verified, and invalid signatures are rejected.

The available tests use valid keys and messages, invalid tests only modify the signature. Following Verify, implementations are expected to only return True or False, exceptions are treated as errors regardless of test vector and counted as failures.

This function can be used to test implementations of the RFC 6979 deterministic version of ECDSA.

Available test vectors depend on the given curve and hash function, see here.

Parameters:
  • verify (Verify) – The function to test.

  • curve (Curve) – The elliptic curve to use.

  • hash_function (Hash) – The hash function used to generate the signatures.

  • pubkey_encoding (PubKeyEncoding) – A public key encoding accepted by the function.

Keyword Arguments:
  • pre_hashed – If True, the messages are hashed before passing them to verify.

  • compliance – Whether to use compliance test vectors.

  • resilience – Whether to use resilience test vectors.

Returns:

A dictionary of Results, one for each test vectors file used. May be empty if there are no test vectors for the given curve and hash function.

Return type:

ResultsDict

Example

Let’s test ECDSA verification with cryptography.

>>> from cryptography.exceptions import InvalidSignature
>>> from cryptography.hazmat.primitives import serialization
>>> from cryptography.hazmat.primitives.asymmetric import ec

We also import crypto-condor’s ECSDA module to run the tests.

>>> from crypto_condor.primitives import ECDSA

We define the parameters we want to test (curve, hash, encoding).

>>> curve = ECDSA.Curve.P256
>>> hash_function = ECDSA.Hash.SHA256
>>> encoding = ECDSA.PubKeyEncoding.DER

Then wrap the call to the verifier to match the Verify protocol.

>>> def my_verify(pk: bytes, msg: bytes, sig: bytes) -> bool:
...     key = serialization.load_der_public_key(pk)
...     try:
...         key.verify(sig, msg, ec.ECDSA(hash_function.get_hash_instance()))
...     except InvalidSignature:
...         return False
...     else:
...         return True

And test this function.

>>> res = ECDSA.test_verify(my_verify, curve, hash_function, encoding)
[P-256][SHA-256][NIST CAVP] Test verifying ...
>>> assert res.check()

Test signing and verifying

crypto_condor.primitives.ECDSA.test_sign_verify_invariant(
sign,
verify,
curve,
hash_function,
key_encoding,
pubkey_encoding,
*,
prehash=False,
)

Tests signing and verifying.

A signature generated by an implementation should be valid for its own verifier. To test this, available signature generation test vectors are used. The sign function is called to compute the signature, then the verify function is called to verify this signature. The test passes if the signature is considered valid.

Note that this only verifies that the signer and verifier agree: for example, a verifier that returns True no matter the input will pass this test. Consider also testing both functions with test_sign() and test_verify().

Available test vectors depend on the given curve and hash function, see here.

Parameters:
  • sign (Sign) – The signing function to test.

  • verify (Verify) – The verifying function to test.

  • curve (Curve) – The elliptic curve to use.

  • hash_function (Hash) – This argument is used to select the test vectors to use. If the prehash argument is True, it also selects the hash function used to hash the messages before passing them to both functions.

  • key_encoding (KeyEncoding) – The private key encoding used by the signing function.

  • pubkey_encoding (PubKeyEncoding) – The public key encoding used by the verifying function.

Keyword Arguments:

prehash – If True, messages are hashed before passing them to sign and verify, otherwise messages are given as-is.

Returns:

A dictionary of results, containing one instance of Results per test vectors file used.

Return type:

ResultsDict

Example

Let’s test crypto-condor’s internal functions. First import the ECDSA module.

>>> from crypto_condor.primitives import ECDSA

Define the test parameters.

>>> curve = ECDSA.Curve.P256
>>> hash_function = ECDSA.Hash.SHA256
>>> key_encoding = ECDSA.KeyEncoding.DER
>>> pubkey_encoding = ECDSA.PubKeyEncoding.DER

Wrap both functions to match the corresponding protocols (Verify and Sign).

>>> def my_verify(pk: bytes, msg: bytes, sig: bytes) -> bool:
...     return ECDSA._verify(pk, hash_function, msg, sig)
>>> def my_sign(sk: bytes, msg: bytes) -> bytes:
...     return ECDSA._sign(sk, hash_function, msg)

Then test both functions.

>>> results = ECDSA.test_sign_verify_invariant(
...     my_sign, my_verify, curve, hash_function, key_encoding, pubkey_encoding
... )
[P-256][SHA-256][NIST CAVP] Test sign-verify ...
>>> assert results.check()

Test key pair generation

crypto_condor.primitives.ECDSA.test_key_pair_gen(keygen, curve, num_keys=5000)

Tests a function that generates ECDSA key pairs.

Calls keygen to generate num_keys keys in the format defined by KeyPair. There are three checks in this test. First, the private key is derived from the private value to verify it works for the given curve. Then, if the public key is included, it must match the private key. Finally, all private keys are concatenated and tested with crypto_condor.primitives.TestU01 to check for potential biases.

Parameters:
  • keygen (KeyGen) – The function that generates ECDSA key pairs. See KeyGen for the expected signature of this function.

  • curve (Curve) – The elliptic curve to use.

Keyword Arguments:

num_keys – The number of keys to generate. The more data is available for TestU01, the more chances of catching any bias present. A minimum of 5000 keys, which results in roughly 1 million bits on P-224, is enforced.

Returns:

A dictionary containing two Results, one for the results of generating key pairs and one for the results of the TestU01 battery.

Raises:

ValueError – If num_keys is less than 5000.

Return type:

ResultsDict

Example

Let’s test PyCryptodome’s key generation. We import the ECDSA module.

>>> from crypto_condor.primitives import ECDSA

We pick the curve P-224.

>>> curve = ECDSA.Curve.P224

Then wrap the implementation to match the signature defined by KeyGen.

>>> from Crypto.PublicKey import ECC
>>> def my_key_gen() -> tuple[int, int, int]:
...     key = ECC.generate(curve=str(curve))
...     return (int(key.d), int(key.pointQ.x), int(key.pointQ.y))

And test it.

>>> results_dict = ECDSA.test_key_pair_gen(my_key_gen, curve)
Generating keys ...
>>> assert results_dict.check()

Test signatures from a file

crypto_condor.primitives.ECDSA.test_output_sign(filename, curve, hash_function, pubkey_encoding)

Verifies signatures contained in a file.

Signatures are read from the file and then verified with a reference implementation. The file must have the following format:

Format:
  • One set of arguments per line.

  • Lines are separated by newlines (\n).

  • Lines that start with ‘#’ are counted as comments and ignored.

  • Arguments are written in hexadecimal and separated by slashes.

  • The keys may be different for each line but they must be encoded in the same format.

  • The order of the arguments is:

pub_key/message/signature
Parameters:
  • filename (str) – The name of the path containing the signatures to verify.

  • curve (Curve | None) – The elliptic curve of the keys. This argument is required when the public key encoding is UNCOMPRESSED, otherwise it is ignored.

  • hash_function (Hash) – The hash function used to generate the signatures.

  • pubkey_encoding (PubKeyEncoding) – The encoding of the public keys used.

Returns:

A dictionary of results. The results of verifying each signature with an internal implementation. Errors, including parsing ones, are counted as failures and do not raise exceptions, except for the IOError indicated below.

For parsing errors, the line numbering starts at 1.

Return type:

ResultsDict

Example

We start by importing the ECDSA module.

>>> from crypto_condor.primitives import ECDSA

For this example we already have a correctly formatted file. We print the first line to show the format, then split the arguments for clarity.

>>> filename = "/tmp/ecdsa-p256-sha256-signatures.txt"
>>> with open(filename, "r") as fd:
...     line = fd.readline().strip()
...     args = line.split("/")
...     print(line)
...     print(f"key = {args[0]}")
...     print(f"msg = {args[1]}")
...     print(f"sig = {args[2]}")
04e424dc61d4bb3cb7ef4344a7f8957a0c5134e16f7a67c074f82e6e12f49abf3c970eed7aa2bc48651545949de1dddaf0127e5965ac85d1243d6f60e7dfaee927/e1130af6a38ccb412a9c8d13e15dbfc9e69a16385af3c3f1e5da954fd5e7c45fd75e2b8c36699228e92840c0562fbf3772f07e17f1add56588dd45f7450e1217ad239922dd9c32695dc71ff2424ca0dec1321aa47064a044b7fe3c2b97d03ce470a592304c5ef21eed9f93da56bb232d1eeb0035f9bf0dfafdcc4606272b20a3/3045022100bf96b99aa49c705c910be33142017c642ff540c76349b9dab72f981fd9347f4f022017c55095819089c2e03b9cd415abdf12444e323075d98f31920b9e0f57ec871c
key = 04e424dc61d4bb3cb7ef4344a7f8957a0c5134e16f7a67c074f82e6e12f49abf3c970eed7aa2bc48651545949de1dddaf0127e5965ac85d1243d6f60e7dfaee927
msg = e1130af6a38ccb412a9c8d13e15dbfc9e69a16385af3c3f1e5da954fd5e7c45fd75e2b8c36699228e92840c0562fbf3772f07e17f1add56588dd45f7450e1217ad239922dd9c32695dc71ff2424ca0dec1321aa47064a044b7fe3c2b97d03ce470a592304c5ef21eed9f93da56bb232d1eeb0035f9bf0dfafdcc4606272b20a3
sig = 3045022100bf96b99aa49c705c910be33142017c642ff540c76349b9dab72f981fd9347f4f022017c55095819089c2e03b9cd415abdf12444e323075d98f31920b9e0f57ec871c

We use test_output_sign() to test this file. In this case the keys are DER-encoded and we used SHA-256 to hash the messages.

>>> result = ECDSA.test_output_sign(
...     filename,
...     ECDSA.Curve.P256,
...     ECDSA.Hash.SHA256,
...     ECDSA.PubKeyEncoding.UNCOMPRESSED,
... )
[P-256][SHA-256] Test signatures from file ...

Added in version TODO(version): Replaces verify_file().

crypto_condor.primitives.ECDSA.verify_file(filename, pubkey_encoding, hash_function, curve)

Verifies signatures contained in a file.

Signatures are read from the file and then verified with a reference implementation. The file must have the following format:

Format:
  • One set of arguments per line.

  • Lines are separated by newlines (\n).

  • Lines that start with ‘#’ are counted as comments and ignored.

  • Arguments are written in hexadecimal and separated by slashes.

  • The keys may be different for each line but they must be encoded in the same format.

  • The order of the arguments is:

pub_key/message/signature
Parameters:
  • filename (str) – The name of the path containing the signatures to verify.

  • pubkey_encoding (PubKeyEncoding) – The encoding of the public keys used.

  • hash_function (Hash) – The hash function used to generate the signatures.

  • curve (Curve | None) – The elliptic curve of the keys. This argument is required when the public key encoding is UNCOMPRESSED, otherwise it is ignored.

Returns:

A dictionary of results.

Return type:

ResultsDict

Deprecated since version TODO(version): Will be removed in a future version, use test_output_sign() instead.

Parameters

To test ECDSA implementations you must choose an elliptic curve and a hash function. We use enums to define the supported parameters: Curve and Hash.

Some functions require an indication of which key encoding to use: refer to KeyEncoding and PubKeyEncoding.

enum crypto_condor.primitives.ECDSA.Curve(value)

The supported elliptic curves.

Member Type:

str

Valid values are as follows:

P224 = <Curve.P224: 'P-224'>
P256 = <Curve.P256: 'P-256'>
P384 = <Curve.P384: 'P-384'>
P521 = <Curve.P521: 'P-521'>
B283 = <Curve.B283: 'B-283'>
B409 = <Curve.B409: 'B-409'>
B571 = <Curve.B571: 'B-571'>
SECP256K1 = <Curve.SECP256K1: 'secp256k1'>
BRAINPOOLP256R1 = <Curve.BRAINPOOLP256R1: 'brainpoolP256r1'>
BRAINPOOLP384R1 = <Curve.BRAINPOOLP384R1: 'brainpoolP384r1'>
BRAINPOOLP512R1 = <Curve.BRAINPOOLP512R1: 'brainpoolP512r1'>
enum crypto_condor.primitives.ECDSA.Hash(value)

The supported hash functions.

Member Type:

str

Valid values are as follows:

SHA224 = <Hash.SHA224: 'SHA-224'>
SHA256 = <Hash.SHA256: 'SHA-256'>
SHA384 = <Hash.SHA384: 'SHA-384'>
SHA512 = <Hash.SHA512: 'SHA-512'>
SHA512_224 = <Hash.SHA512_224: 'SHA-512/224'>
SHA512_256 = <Hash.SHA512_256: 'SHA-512/256'>
SHA3_224 = <Hash.SHA3_224: 'SHA3-224'>
SHA3_256 = <Hash.SHA3_256: 'SHA3-256'>
SHA3_384 = <Hash.SHA3_384: 'SHA3-384'>
SHA3_512 = <Hash.SHA3_512: 'SHA3-512'>
enum crypto_condor.primitives.ECDSA.KeyEncoding(value)

Supported key encodings.

Member Type:

str

Valid values are as follows:

PEM = <KeyEncoding.PEM: 'PEM'>
DER = <KeyEncoding.DER: 'DER'>
INT = <KeyEncoding.INT: 'INT'>
enum crypto_condor.primitives.ECDSA.PubKeyEncoding(value)

Supported public key encodings.

Member Type:

str

Valid values are as follows:

PEM = <PubKeyEncoding.PEM: 'PEM'>
DER = <PubKeyEncoding.DER: 'DER'>
UNCOMPRESSED = <PubKeyEncoding.UNCOMPRESSED: 'UNCOMPRESSED'>

Protocols

protocol crypto_condor.primitives.ECDSA.Sign

Represents a function that signs a message with ECDSA.

Classes that implement this protocol must have the following methods / attributes:

__call__(sk, msg)

Signs a message with ECDSA.

Parameters:
  • sk (bytes) – The private elliptic curve key. Can be encoded as PEM, DER, or an integer in bytes depending on KeyEncoding.

  • msg (bytes) – The message to sign.

Returns:

A DER-encoded signature.

Return type:

bytes

protocol crypto_condor.primitives.ECDSA.Verify

Represents a function that verifies ECDSA signatures.

Classes that implement this protocol must have the following methods / attributes:

__call__(pk, msg, sig)

Verifies an ECDSA signature.

Parameters:
  • pk (bytes) – The public elliptic curve key. Can be encoded as PEM, DER, or an integer in bytes depending on PubKeyEncoding.

  • msg (bytes) – The signed message.

  • sig (bytes) – The signature to verify.

Returns:

True if the signature is valid, False otherwise.

Return type:

bool

protocol crypto_condor.primitives.ECDSA.KeyGen

Represents a function that generates ECDSA key pairs.

Classes that implement this protocol must have the following methods / attributes:

__call__()

Generates an ECDSA key pair.

Returns:

A KeyPair.

Return type:

int | tuple[int, int, int] | bytes

Test vectors

The following tables describe the test vectors available for each combination of elliptic curve and hash function. C denotes compliance, R resilience.

Test vector types for signing

SHA-224

SHA-256

SHA-384

SHA-512

SHA-512/224

SHA-512/256

P-224

C

C

C

C

C

C

P-256

C

C

C

C

C

C

P-384

C

C

C

C

C

C

P-521

C

C

C

C

C

C

B-283

C

C

C

C

x

x

B-409

C

C

C

C

x

x

B-571

C

C

C

C

x

x

Test vector types for verifying

SHA-224

SHA-256

SHA-384

SHA-512

SHA-512/224

SHA-512/256

SHA3-224

SHA3-256

SHA3-384

SHA3-512

P-224

C + R

C + R

C

C + R

C

C

R

R

x

R

P-256

C

C + R

C

C + R

C

C

x

R

x

R

P-384

C

C

C + R

C + R

C

C

x

x

R

R

P-521

C

C

C

C + R

C

C

x

x

x

R

B-283

C

C

C

C

x

x

x

x

x

x

B-409

C

C

C

C

x

x

x

x

x

x

B-571

C

C

C

C

x

x

x

x

x

x

secp256k1

x

R

x

R

x

x

x

R

x

R

brainpoolP256r1

x

R

x

x

x

x

x

x

x

x

brainpoolP384r1

x

x

R

x

x

x

x

x

x

x