AES#

Module to test AES implementations.

The crypto_condor.primitives.AES module can test implementations of AES </method/AES> encryption and decryption using several modes of operations with the test() function. Supported modes are defined by the Mode enum.

Test an implementation#

crypto_condor.primitives.AES.test(encrypt, decrypt, mode, key_length, *, iv_length=0, compliance=True, resilience=True)#

Tests implementations of AES encryption and decryption.

It runs the given functions on a set of test vectors determined by the mode of operation, key length, selection of compliance or resilience test vectors, and the IV length.

The functions to test must conform to the Encrypt and Decrypt protocols.

Parameters:
  • encrypt (Encrypt | None) – The encryption function to test.

  • decrypt (Decrypt | None) – The decryption function to test.

  • mode (Mode) – The mode of operation to test.

  • key_length (KeyLength) – The size of the key in bits. Use 0 to test all three values.

Keyword Arguments:
  • iv_length – The length of the IV. This options restrict the test vectors to only those that use IVs of the given length. Set to 0 to use all test vectors.

  • compliance – Whether to use compliance test vectors.

  • resilience – Whether to use resilience test vectors.

Returns:

A dictionary of results.

If the compliance option is True then NIST test vectors are used. They are separated in several files, each result corresponds to a single file and they are indexed by the file name, prefixed by NIST/encrypt/ and NIST/decrypt/.

If the resilience option is True then Wycheproof test vectors are used, if Wycheproof supports the given mode of operation. The dictionary key for results of testing the encrypt function (resp. the decrypt function) is Wycheproof/encrypt/ (resp. Wycheproof/decrypt/).

Return type:

ResultsDict

Example

Let’s test PyCryptodome’s implementation of AES-256-ECB.

We start by importing the AES module.

>>> from crypto_condor.primitives import AES

We need to wrap the functions to match the signature defined by Encrypt and Decrypt. In this case, we want to match the first overload, as it is the one that corresponds to ECB.

>>> from Crypto.Cipher import AES as pycAES
>>> def my_enc(key: bytes, plaintext: bytes) -> bytes:
...     cipher = pycAES.new(key, pycAES.MODE_ECB)
...     return cipher.encrypt(plaintext)
>>> def my_dec(key: bytes, ciphertext: bytes) -> bytes:
...     cipher = pycAES.new(key, pycAES.MODE_ECB)
...     return cipher.decrypt(ciphertext)

We define the parameters to test using the corresponding enums.

>>> mode = AES.Mode.ECB
>>> keylen = AES.KeyLength.AES256

And now we test the functions we defined.

>>> results_dict = AES.test(my_enc, my_dec, mode, keylen)
[NIST] ...
>>> assert results_dict.check()

Now let’s try a more specific example: testing AES-256-GCM decryption with only IVs of size 96.

>>> def my_gcm_dec(
...     key: bytes,
...     ciphertext: bytes,
...     *,
...     iv: bytes | None,
...     aad: bytes | None,
...     mac: bytes | None,
...     mac_len: int,
... ) -> tuple[bytes | None, bool]:
...     cipher = pycAES.new(key, pycAES.MODE_GCM, nonce=iv, mac_len=mac_len)
...     if aad is not None:
...         cipher.update(aad)
...     try:
...         plaintext = cipher.decrypt_and_verify(ciphertext, mac)
...         return (plaintext, True)
...     except ValueError:
...         return (None, False)
>>> mode = AES.Mode.GCM
>>> results_dict = AES.test(None, my_gcm_dec, mode, keylen, iv_length=96)
[NIST] ...
>>> assert results_dict.check()

Parameters#

enum crypto_condor.primitives.AES.Mode(value)#

Supported AES modes of operation.

The AES primitive is used with a variety of modes of operation. This enum defines those that are supported by crypto-condor.

Member Type:

str

Valid values are as follows:

ECB = <Mode.ECB: 'ECB'>#
CBC = <Mode.CBC: 'CBC'>#
CBC_PKCS7 = <Mode.CBC_PKCS7: 'CBC-PKCS7'>#
CFB = <Mode.CFB: 'CFB'>#
CFB8 = <Mode.CFB8: 'CFB8'>#
CFB128 = <Mode.CFB128: 'CFB128'>#
CTR = <Mode.CTR: 'CTR'>#
GCM = <Mode.GCM: 'GCM'>#
CCM = <Mode.CCM: 'CCM'>#
enum crypto_condor.primitives.AES.KeyLength(value)#

Supported key lengths.

AES has three different key lengths: 128, 192, and 256 bits. Since users may want to test a specific key length, this enum defines these three options alongside the KeyLength.ALL option to test all three.

Member Type:

int

Valid values are as follows:

ALL = <KeyLength.ALL: 0>#
AES128 = <KeyLength.AES128: 128>#
AES192 = <KeyLength.AES192: 192>#
AES256 = <KeyLength.AES256: 256>#
enum crypto_condor.primitives.AES.Operation(value)#

Operations supported for AES.

As a symmetric cipher, AES can encrypt and decrypt messages. This enum is used to choose between these operations for the verify_file() function.

Member Type:

str

Valid values are as follows:

ENCRYPT = <Operation.ENCRYPT: 'encrypt'>#
DECRYPT = <Operation.DECRYPT: 'decrypt'>#

Protocols#

protocol crypto_condor.primitives.AES.Encrypt#

Represents a function that encrypts with AES.

Encryption functions must behave like one of the __call__ functions to be tested with this module. Each correspond to one or more modes of operation. In order:

  • ECB

  • CBC or CTR or CFB8 or CFB128

  • CCM or GCM

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

__call__(key: bytes, plaintext: bytes) bytes#
__call__(key: bytes, plaintext: bytes, *, iv: bytes) bytes
__call__(key: bytes, plaintext: bytes, *, iv: bytes | None, aad: bytes | None, mac_len: int = 0) CiphertextAndTag

Encrypts with AES.

Parameters:
  • key – The symmetric key.

  • plaintext – The input to encrypt.

Keyword Arguments:
  • iv – (All modes except ECB) The IV or nonce.

  • aad – (CCM/GCM) The associated data.

  • mac_len – (CCM/GCM) The length of the authentication tag.

Returns:

(ECB/CBC/CTR/CFB) The resulting ciphertext.

(CCM/GCM) A (ciphertext, tag) tuple.

protocol crypto_condor.primitives.AES.Decrypt#

Represents a function that decrypts with AES.

Decryption functions must behave like one of the __call__ functions to be tested with this module. Each correspond to one or more modes of operation. In order:

  • ECB

  • CBC or CTR or CFB8 or CFB128

  • CCM or GCM

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

__call__(key: bytes, ciphertext: bytes) bytes#
__call__(key: bytes, ciphertext: bytes, *, iv: bytes | None) bytes
__call__(key: bytes, ciphertext: bytes, *, iv: bytes | None, aad: bytes | None, mac: bytes | None, mac_len: int = 0) PlaintextAndBool

Decrypts with AES.

Parameters:
  • key – The symmetric key.

  • ciphertext – The input to decrypt.

Keyword Arguments:
  • iv – (All modes except ECB) The IV or nonce.

  • aad – (CCM/GCM) The associated data.

  • mac – (CCM/GCM) The authentication tag.

  • mac_len – (CCM/GCM) The length of the authentication tag in bytes.

Returns:

(ECB/CBC/CTR/CFB) The resulting plaintext.

(CCM/GCM) If the MAC is valid it returns (plaintext, True). Otherwise the plaintext should not be release so it returns (None, False).

Notes

We decided to return None when the MAC verification fails to differentiate from the case where the message is empty, which is a valid case, and is tested by some test vectors. It serves as a clear sign that even in case we don’t test the verification status “this is not the plaintext you’re looking for”.

Test its output#

crypto_condor.primitives.AES.verify_file(filename, mode, operation)#

Tests the output of an implementation.

Tests an implementation from a set of inputs passed to it and the outputs it returned. These inputs are passed to the internal implementation and the results are compared to the outputs given.

Attention

This function uses the internal implementation of AES, which must be compiled and installed locally. This is done automatically when the function is called for the first time. If the installation fails, this function will not work.

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.

  • For ECB the order is:

key/input/output
  • For other classic modes of operation (CBC, CTR, CFB) the order is:

key/input/output/iv
  • For AEAD modes (CCM, GCM) the order is:

key/input/output/iv/[aad]/[mac]
  • Where:
    • key is the key used.

    • input is the plaintext when encrypting (resp. the ciphertext when decrypting).

    • output is the ciphertext when encrypting (resp. the plaintext when decrypting).

    • iv is the IV or nonce used for that operation.

    • aad is the associated data. It is optional and can be empty. Even if not used, the corresponding slashes must be present.

    • mac is the MAC tag generated when encrypting. When testing encryption, it is compared to the MAC generated internally. When decrypting, it is used for authenticating the ciphertext and associated data.

Parameters:
  • filename (str) – The name of the file to test.

  • mode (Mode) – The mode of operation to use.

  • operation (Operation) – The operation being tested, ‘encrypt’ or ‘decrypt’.

Returns:

The results of testing each line of the line.

Raises:

FileNotFoundError – If there is not file with that filename.

Return type:

Results

Example

Let’s generate 10 random tuples of (key, plaintext, IV), encrypt the plaintexts using PyCryptodome’s AES-128-GCM, and write everything to a file. We won’t use any associated data to illustrate how to skip it.

>>> import random
>>> from crypto_condor.primitives import AES
>>> from Crypto.Cipher import AES as pyAES
>>> filename = "/tmp/crypto-condor-test/aes-verify.txt"
>>> with open(filename, "w") as file:
...     for _ in range(10):
...         # Pick random values.
...         key = random.randbytes(16)
...         plaintext = random.randbytes(16)
...         iv = random.randbytes(12)
...         # Encrypt.
...         cipher = pyAES.new(key, pyAES.MODE_GCM, nonce=iv)
...         ciphertext, mac = cipher.encrypt_and_digest(plaintext)
...         # Convert to hex.
...         kh = bytes.hex(key)
...         ph = bytes.hex(plaintext)
...         ih = bytes.hex(iv)
...         ch = bytes.hex(ciphertext)
...         mh = bytes.hex(mac)
...         # Create the line to write.
...         # key/input/output/iv/[aad]/[mac]
...         line = f"{kh}/{ph}/{ch}/{ih}//{mh}\n"
...         _ = file.write(line)

Now we can test this file.

>>> mode = AES.Mode.GCM
>>> operation = AES.Operation.ENCRYPT
>>> results = AES.verify_file(filename, mode, operation)
Testing ...
>>> assert results.check()

Run a wrapper#

crypto_condor.primitives.AES.run_wrapper(wrapper, mode, key_length=KeyLength.ALL, *, compliance=True, resilience=True, encrypt=True, decrypt=True, iv_length=0)#

Runs a wrapper.

Parameters:
  • wrapper (Path) – The wrapper to test.

  • mode (Mode) – The mode of operation to test.

  • key_length (KeyLength) – The length of the keys to use, in bits.

Keyword Arguments:
  • compliance – Whether to run compliance test vectors.

  • resilience – Whether to run resilience test vectors.

  • encrypt – Whether to test the encryption.

  • decrypt – Whether to test the decryption.

  • iv_length – The length of the IV to test. If 0, use any test vector available.

Returns:

Returns the results from test().

enum crypto_condor.primitives.AES.Wrapper(value)#

Supported languages for wrappers.

Member Type:

str

Valid values are as follows:

PYTHON = <Wrapper.PYTHON: 'Python'>#
C = <Wrapper.C: 'C'>#