Skip to content

_hashopenssl.c‍: Missing negative return check in EVP_get_block_size and EVP_get_digest_size #146287

@raminfp

Description

@raminfp

Bug report

Bug description:

EVP_get_block_size and EVP_get_digest_size pass the return value of
EVP_MD_CTX_block_size() / EVP_MD_CTX_size() directly to PyLong_FromLong()
without checking for a negative error return. When OpenSSL signals an error by
returning -1, Python silently exposes that value to the caller instead of
raising a ValueError.

Vulnerable Code

// Modules/_hashopenssl.c

static PyObject *
EVP_get_block_size(EVPobject *self, void *closure)
{
    long block_size;
    block_size = EVP_MD_CTX_block_size(self->ctx);
    return PyLong_FromLong(block_size);   // no check for negative value
}

static PyObject *
EVP_get_digest_size(EVPobject *self, void *closure)
{
    long size;
    size = EVP_MD_CTX_size(self->ctx);
    return PyLong_FromLong(size);         // no check for negative value
}

Root Cause

In OpenSSL 3.x, both macros are defined in <openssl/evp.h> as:

#define EVP_MD_CTX_get_size(e)        EVP_MD_get_size(EVP_MD_CTX_get0_md(e))
#define EVP_MD_CTX_size               EVP_MD_CTX_get_size

#define EVP_MD_CTX_get_block_size(e)  EVP_MD_get_block_size(EVP_MD_CTX_get0_md(e))
#define EVP_MD_CTX_block_size         EVP_MD_CTX_get_block_size

Both expand to EVP_MD_get_size(EVP_MD_CTX_get0_md(ctx)).
When the context has no digest set, EVP_MD_CTX_get0_md() returns NULL,
and EVP_MD_get_size(NULL) returns -1 an error signal that Python never checks.


Proof of Concept

import ctypes, ctypes.util, ssl

print(f"OpenSSL: {ssl.OPENSSL_VERSION}\n")

lib = ctypes.CDLL(ctypes.util.find_library('crypto'))
lib.EVP_MD_CTX_new.restype         = ctypes.c_void_p
lib.EVP_MD_CTX_free.restype        = None
lib.EVP_MD_CTX_free.argtypes       = [ctypes.c_void_p]
lib.EVP_MD_CTX_get0_md.restype     = ctypes.c_void_p
lib.EVP_MD_CTX_get0_md.argtypes    = [ctypes.c_void_p]
lib.EVP_MD_get_size.restype        = ctypes.c_int
lib.EVP_MD_get_size.argtypes       = [ctypes.c_void_p]
lib.EVP_MD_get_block_size.restype  = ctypes.c_int
lib.EVP_MD_get_block_size.argtypes = [ctypes.c_void_p]

# context without EVP_DigestInit_ex => get0_md() returns NULL
ctx = lib.EVP_MD_CTX_new()
md  = lib.EVP_MD_CTX_get0_md(ctx)

print(f"EVP_MD_CTX_get0_md()    = {md or 'NULL'}")
print(f"EVP_MD_get_size(NULL)   = {lib.EVP_MD_get_size(md)}")
print(f"EVP_MD_get_block_size() = {lib.EVP_MD_get_block_size(md)}\n")

digest_size = lib.EVP_MD_get_size(md)
block_size  = lib.EVP_MD_get_block_size(md)
print(f"hash.digest_size => {digest_size}  (no ValueError raised)")
print(f"hash.block_size  => {block_size}  (no ValueError raised)")

lib.EVP_MD_CTX_free(ctx)

Output:

OpenSSL: OpenSSL 3.3.1 4 Jun 2024

EVP_MD_CTX_get0_md()    = NULL
EVP_MD_get_size(NULL)   = -1
EVP_MD_get_block_size() = -1

hash.digest_size => -1  (no ValueError raised)
hash.block_size  => -1  (no ValueError raised)

Suggested Fix

static PyObject *
EVP_get_block_size(EVPobject *self, void *closure)
{
    long block_size = EVP_MD_CTX_block_size(self->ctx);
    if (block_size <= 0) {
        PyErr_SetString(PyExc_ValueError, "block size unavailable");
        return NULL;
    }
    return PyLong_FromLong(block_size);
}

static PyObject *
EVP_get_digest_size(EVPobject *self, void *closure)
{
    long size = EVP_MD_CTX_size(self->ctx);
    if (size <= 0) {
        PyErr_SetString(PyExc_ValueError, "digest size unavailable");
        return NULL;
    }
    return PyLong_FromLong(size);
}

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirpendingThe issue will be closed if no feedback is providedtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions