pyca / cryptography

cryptography is a package designed to expose cryptographic primitives and recipes to Python developers.
https://cryptography.io
Other
6.41k stars 1.47k forks source link

AES_GCM_SIV does not accept empty plaintexts. #10808

Open bleichenbacher-daniel opened 2 months ago

bleichenbacher-daniel commented 2 months ago

A test I have started failing recently when using AES-GCM-SIV with empty plaintexts.

The failing test is

from cryptography.hazmat.primitives.ciphers import aead
import cryptography
import sys

def test_empty_pt():
    key = bytes(range(16))
    nonce = bytes(range(12))
    pt = b""
    aad = b"associated data"
    crypter = aead.AESGCMSIV(key)
    ct = crypter.encrypt(nonce, pt, aad)

if __name__ == "__main__":
    print(f"{sys.version=}")
    print(f"{cryptography.__version__=}")
    test_empty_pt()

This gives the following output

sys.version='3.12.3 (tags/v3.12.3:f6650f9, Apr 9 2024, 14:05:25) [MSC v.1938 64 bit (AMD64)]' cryptography.version='42.0.5' Traceback (most recent call last): File "...pyca/aesgcmsiv.py", line 16, in test_empty_pt() File "...pyca/aesgcmsiv.py", line 11, in test_empty_pt ct = crypter.encrypt(nonce, pt, aad) ValueError: data must not be zero length

The exception is thrown for all supported key sizes of AES-GCM-SIV. No other AEAD mode seems to be affected. Other parameter sizes give the expected results. The test passed before updating to the Python version 3.12.3 and cryptography version 42.0.5.

reaperhulk commented 2 months ago

We only added AES-GCM-SIV support in cryptography 42.0.0, so you were seeing empty PT succeed in 42.0.x but failing in 42.0.5?

bleichenbacher-daniel commented 2 months ago

You are probably right. I can't find a test log where AES-GCM-SIV did run and all tests passed.

reaperhulk commented 2 months ago

I don't see a reason why we shouldn't support empty PT so I'll put in a PR to improve this 😄

reaperhulk commented 2 months ago

It looks like OpenSSL doesn't support 0 length PT (see reproducer below). I'll file an upstream issue soon.

#include <assert.h>
#include <stdio.h>
#include <openssl/evp.h>

int main() {
    EVP_CIPHER_CTX *ctx;
    unsigned char key[16] = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    unsigned char nonce[12] = {3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    unsigned char pt[12] = {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    int pt_len = 12;
    unsigned char aad[1] = {1};
    int aad_len = 1;
    unsigned char tag[16];
    unsigned char outbuf[16];
    int outlen;

    ctx = EVP_CIPHER_CTX_new();
    EVP_CIPHER *ciph = EVP_CIPHER_fetch(NULL, "aes-128-gcm-siv", NULL);
    assert(ciph != NULL);
    assert(EVP_EncryptInit_ex(ctx, ciph, NULL, key, nonce) == 1);
    assert(EVP_EncryptUpdate(ctx, NULL, &outlen, aad, aad_len) == 1);
    assert(EVP_EncryptUpdate(ctx, outbuf, &outlen, pt, pt_len) == 1);
//    assert(EVP_EncryptUpdate(ctx, outbuf, &outlen, NULL, 0) == 1); // uncomment this and it will not fail
    assert(EVP_EncryptFinal_ex(ctx, outbuf, &outlen) == 1);
    assert(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, tag) == 1);
    EVP_CIPHER_CTX_free(ctx);

    printf("Tag: ");
    for (int i = 0; i < 16; i++) {
        printf("%02x", tag[i]);
    }
    printf("\n");

    return 0;
}