diff mbox series

[meta-networking,blacksail,3/28] openvpn: patch CVE-2026-84790

Message ID 20261013095903.3544906-3-ankur.tyagi85@gmail.com
State New
Headers show
Series [meta-networking,blacksail,1/28] openvpn: ignore CVE-2026-84256 | expand

Commit Message

Ankur Tyagi Oct. 13, 2026, 9:58 a.m. UTC
From: Ankur Tyagi <ankur.tyagi85@gmail.com>

Backport commit identified by Debian[1], also mentioned in the release note
https://github.com/OpenVPN/openvpn/releases/tag/v2.7.8

[1]https://security-tracker.debian.org/tracker/CVE-2026-84790

Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
---
 .../openvpn/openvpn/CVE-2026-84790.patch      | 361 ++++++++++++++++++
 .../recipes-support/openvpn/openvpn_2.7.7.bb  |   1 +
 2 files changed, 362 insertions(+)
 create mode 100644 meta-networking/recipes-support/openvpn/openvpn/CVE-2026-84790.patch
diff mbox series

Patch

diff --git a/meta-networking/recipes-support/openvpn/openvpn/CVE-2026-84790.patch b/meta-networking/recipes-support/openvpn/openvpn/CVE-2026-84790.patch
new file mode 100644
index 0000000000..b6f70d106d
--- /dev/null
+++ b/meta-networking/recipes-support/openvpn/openvpn/CVE-2026-84790.patch
@@ -0,0 +1,361 @@ 
+From 493c3b382a907ff2d5870cd0d21526bd2e866ebb Mon Sep 17 00:00:00 2001
+From: Max Fillinger <maximilian.fillinger@sentyron.com>
+Date: Thu, 17 Sep 2026 14:46:05 +0200
+Subject: [PATCH] Check for null-bytes in certificate subjects
+
+When using the OpenSSL library, we escaped embedded null-bytes in a
+certificate's subject in x509_get_subject(), but in
+extract_x509_field_ssl(), we copied any null-bytes that are contained in
+the field value. When using the option --verify-x509-name, this could
+lead to an incorrect name being accepted. For example, the common name
+"admin\0impersonator" would be accepted when running with
+--verify-x509-name admin name.
+
+To fix this, this commit makes x509_get_subject() return an error if the
+subject contains an embedded null-byte. This way, OpenVPN won't connect
+with peers that present such certificates.
+
+As a defense-in-depth measure, we also check for null-bytes in
+extract_x509_field_ssl() in case a future version of OpenVPN has a code
+path that avoids x509_get_subject().
+
+With Mbed TLS, the x509_get_subject() function already returned an error
+when there is an embedded null-byte. (As of Mbed TLS 3.5.) Here too, we
+added a check for null-bytes to the function where we extract individual
+fields from the subject.
+
+Also, backend_x509_get_username() is changed so that it returns the
+value of the *last* matching field, to be consistent with the behavior
+of the OpenSSL backend.
+
+This is a partial backport of the commit
+"Check for \0 and RFC 2253 chars in cert subjects" from the master
+branch but skipping the part about escaping RFC 2253 characters to avoid
+user-visible changes in a patch release. We do not consider the
+null-byte check "user-visible" because we don't expect users to put
+null-bytes into the common name.
+
+v2: The previous version of the commit unescaped control characters in
+the subject, so it had user-visible changes after all. This version
+escapes any control characters in the subject.
+
+Discovered and reported by BreachX Zero Day Labs, using Typhon AI Mil v2.
+Contributing Researcher: Vivek Parikh.
+
+CVE: 2026-84790
+Reported-by: Vivek Parikh <vivek.parikh@breachx.ai>
+Github: OpenVPN/openvpn-private-issues#163
+
+Change-Id: Iea1481e94ba1abe18c39d81cd797187c88427571
+Signed-off-by: Max Fillinger <maximilian.fillinger@sentyron.com>
+Acked-by: Steffan Karger <steffan@karger.me>
+Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1902
+Message-Id: <20260917124610.14291-1-gert@greenie.muc.de>
+URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg39304.html
+Signed-off-by: Gert Doering <gert@greenie.muc.de>
+(cherry picked from commit cf4384eef4676a01bd3ee98fa602f7b2af8079ea)
+
+CVE: CVE-2026-84790
+Upstream-Status: Backport [https://github.com/OpenVPN/openvpn/commit/cf4384eef4676a01bd3ee98fa602f7b2af8079ea]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ src/openvpn/ssl_verify_mbedtls.c    | 51 ++++++++++------
+ src/openvpn/ssl_verify_openssl.c    | 41 ++++++++++---
+ tests/unit_tests/openvpn/test_ssl.c | 92 ++++++++++++++++++++++++++++-
+ 3 files changed, 155 insertions(+), 29 deletions(-)
+
+diff --git a/src/openvpn/ssl_verify_mbedtls.c b/src/openvpn/ssl_verify_mbedtls.c
+index 66cc66e5..1319c079 100644
+--- a/src/openvpn/ssl_verify_mbedtls.c
++++ b/src/openvpn/ssl_verify_mbedtls.c
+@@ -214,6 +214,24 @@ fieldname_to_oid(const char *fieldname)
+     }
+ }
+ 
++static bool
++asn1_buf_is_cstr_compatible(const mbedtls_asn1_buf *asn1_buf)
++{
++    if (!(asn1_buf->tag == MBEDTLS_ASN1_UTF8_STRING || asn1_buf->tag == MBEDTLS_ASN1_PRINTABLE_STRING
++          || asn1_buf->tag == MBEDTLS_ASN1_IA5_STRING))
++    {
++        return false;
++    }
++    for (size_t i = 0; i < asn1_buf->len; i++)
++    {
++        if (asn1_buf->p[i] == '\0')
++        {
++            return false;
++        }
++    }
++    return true;
++}
++
+ result_t
+ backend_x509_get_username(char *cn, size_t cn_len, char *x509_username_field, mbedtls_x509_crt *cert)
+ {
+@@ -259,16 +277,17 @@ backend_x509_get_username(char *cn, size_t cn_len, char *x509_username_field, mb
+     }
+ 
+     /* Find field_oid in the subject name. */
+-    mbedtls_x509_name *name = &cert->subject;
+-    while (name != NULL)
++    mbedtls_x509_name *name = NULL;
++    mbedtls_x509_name *next = &cert->subject;
++    while (next != NULL)
+     {
+-        if (strlen(field_oid) == name->oid.len
+-            && 0 == memcmp(name->oid.p, field_oid, name->oid.len))
++        if (strlen(field_oid) == next->oid.len
++            && 0 == memcmp(next->oid.p, field_oid, next->oid.len))
+         {
+-            break;
++            name = next;
+         }
+ 
+-        name = name->next;
++        next = next->next;
+     }
+ 
+     /* Not found, return an error if this is the peer's certificate */
+@@ -277,6 +296,11 @@ backend_x509_get_username(char *cn, size_t cn_len, char *x509_username_field, mb
+         goto fail;
+     }
+ 
++    if (!asn1_buf_is_cstr_compatible(&name->val))
++    {
++        goto fail;
++    }
++
+     /* Check that we have room in the buffer, including the terminating '/0' byte. */
+     if (cn_len <= name->val.len)
+     {
+@@ -586,22 +610,11 @@ do_setenv_x509(struct env_set *es, const char *name, char *value, int depth)
+ static char *
+ asn1_buf_to_c_string(const mbedtls_asn1_buf *orig, struct gc_arena *gc)
+ {
+-    size_t i;
+     char *val;
+ 
+-    if (!(orig->tag == MBEDTLS_ASN1_UTF8_STRING || orig->tag == MBEDTLS_ASN1_PRINTABLE_STRING
+-          || orig->tag == MBEDTLS_ASN1_IA5_STRING))
++    if (!asn1_buf_is_cstr_compatible(orig))
+     {
+-        /* Only support C-string compatible types */
+-        return string_alloc("ERROR: unsupported ASN.1 string type", gc);
+-    }
+-
+-    for (i = 0; i < orig->len; ++i)
+-    {
+-        if (orig->p[i] == '\0')
+-        {
+-            return string_alloc("ERROR: embedded null value", gc);
+-        }
++        return string_alloc("ERROR: Unsupported string type or embedded null bytes.", gc);
+     }
+     val = gc_malloc(orig->len + 1, false, gc);
+     memcpy(val, orig->p, orig->len);
+diff --git a/src/openvpn/ssl_verify_openssl.c b/src/openvpn/ssl_verify_openssl.c
+index a30099d0..1f2804ff 100644
+--- a/src/openvpn/ssl_verify_openssl.c
++++ b/src/openvpn/ssl_verify_openssl.c
+@@ -196,12 +196,13 @@ extract_x509_field_ssl(const X509_NAME *x509, const char *field_name, char *out,
+     int lastpos = -1;
+     int tmp = -1;
+     unsigned char *buf = NULL;
++    result_t ret = FAILURE;
+ 
+     ASN1_OBJECT *field_name_obj = OBJ_txt2obj(field_name, 0);
+     if (field_name_obj == NULL)
+     {
+         msg(D_TLS_ERRORS, "Invalid X509 attribute name '%s'", field_name);
+-        return FAILURE;
++        goto exit;
+     }
+ 
+     ASSERT(size > 0);
+@@ -222,28 +223,31 @@ extract_x509_field_ssl(const X509_NAME *x509, const char *field_name, char *out,
+     /* Nothing found */
+     if (lastpos == -1)
+     {
+-        return FAILURE;
++        goto exit;
+     }
+ 
+     const X509_NAME_ENTRY *x509ne = X509_NAME_get_entry(x509, lastpos);
+     if (!x509ne)
+     {
+-        return FAILURE;
++        goto exit;
+     }
+ 
+     const ASN1_STRING *asn1 = X509_NAME_ENTRY_get_data(x509ne);
+     if (!asn1)
+     {
+-        return FAILURE;
++        goto exit;
+     }
+-    if (ASN1_STRING_to_UTF8(&buf, asn1) < 0)
++    int length = ASN1_STRING_to_UTF8(&buf, asn1);
++    if (length < 0 || (size_t)length != strlen((char *)buf))
+     {
+-        return FAILURE;
++        goto exit;
+     }
+ 
+     strncpynt(out, (char *)buf, size);
+ 
+-    const result_t ret = (strlen((char *)buf) < size) ? SUCCESS : FAILURE;
++    ret = (strlen((char *)buf) < size) ? SUCCESS : FAILURE;
++
++exit:
+     OPENSSL_free(buf);
+     return ret;
+ }
+@@ -384,15 +388,34 @@ x509_get_subject(X509 *cert, struct gc_arena *gc)
+         goto err;
+     }
+ 
++    /* Get the subject with unescaped control characters so that we can check for null bytes. */
+     X509_NAME_print_ex(subject_bio, X509_get_subject_name(cert), 0,
+-                       XN_FLAG_SEP_CPLUS_SPC | XN_FLAG_FN_SN | ASN1_STRFLGS_UTF8_CONVERT
+-                           | ASN1_STRFLGS_ESC_CTRL);
++                       XN_FLAG_SEP_CPLUS_SPC | XN_FLAG_FN_SN | ASN1_STRFLGS_UTF8_CONVERT);
+ 
+     if (BIO_eof(subject_bio))
+     {
+         goto err;
+     }
+ 
++    BIO_get_mem_ptr(subject_bio, &subject_mem);
++    if (memchr(subject_mem->data, 0, subject_mem->length) != NULL)
++    {
++        msg(M_WARN, "ERROR: Certificate subject contains a '\\0' byte.");
++        goto err;
++    }
++
++    /* Now output the subject with escaped control characters. */
++    if (BIO_reset(subject_bio) != 1)
++    {
++        goto err;
++    }
++    X509_NAME_print_ex(subject_bio, X509_get_subject_name(cert), 0,
++                       XN_FLAG_SEP_CPLUS_SPC | XN_FLAG_FN_SN | ASN1_STRFLGS_UTF8_CONVERT
++                           | ASN1_STRFLGS_ESC_CTRL);
++    if (BIO_eof(subject_bio))
++    {
++        goto err;
++    }
+     BIO_get_mem_ptr(subject_bio, &subject_mem);
+ 
+     subject = gc_malloc(subject_mem->length + 1, false, gc);
+diff --git a/tests/unit_tests/openvpn/test_ssl.c b/tests/unit_tests/openvpn/test_ssl.c
+index 7e7679c5..adfc07a8 100644
+--- a/tests/unit_tests/openvpn/test_ssl.c
++++ b/tests/unit_tests/openvpn/test_ssl.c
+@@ -884,6 +884,94 @@ crypto_test_print_cert_details(void **state)
+     free_certificate(cert);
+ }
+ 
++/* Certificate with two CNs, "foo" and "bar".
++ *
++ * Generated with:
++ * openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -subj "/CN=foo/CN=bar" \
++ * -noenc -out two_cns.crt -keyout two_cns.key
++ */
++static const char *cert_with_two_cns =
++    "-----BEGIN CERTIFICATE-----\n"
++    "MIIByTCCAVCgAwIBAgIUZuvd8Wfnq5jeRc3ohoJ/Vza54wQwCgYIKoZIzj0EAwIw\n"
++    "HDEMMAoGA1UEAwwDZm9vMQwwCgYDVQQDDANiYXIwHhcNMjYwODI4MTU1NjAwWhcN\n"
++    "MjYwOTI3MTU1NjAwWjAcMQwwCgYDVQQDDANmb28xDDAKBgNVBAMMA2JhcjB2MBAG\n"
++    "ByqGSM49AgEGBSuBBAAiA2IABERAfb210NOACy+QVYAu3EXrcGhTJpZDfhpDnE/h\n"
++    "PvPMWGWzqecTYdseArxZc0T/5Xma36IKCjGGsgN9ypZ5oQugQlB/NrVRCJIuHSGA\n"
++    "hGBWFvnUsqdNo765lGwVdBwhY6NTMFEwHQYDVR0OBBYEFPwnD+wK9R81Syk0qyTI\n"
++    "dFT/BNoJMB8GA1UdIwQYMBaAFPwnD+wK9R81Syk0qyTIdFT/BNoJMA8GA1UdEwEB\n"
++    "/wQFMAMBAf8wCgYIKoZIzj0EAwIDZwAwZAIwGxazGb6RRQtXzOWCPLRKId4e+E88\n"
++    "cwoCK3UwzEr8+Ddf54w5cEZC54f5J6AKFUJjAjBpT/JqF41uK57H+8i/16oZkBcm\n"
++    "OyQnlH8W/UzZo3/weTEBTcNW0iuCpIrS6im8pSk=\n"
++    "-----END CERTIFICATE-----\n";
++
++void
++ssl_test_extract_last_matching_field(void **state)
++{
++    /* When there are multiple fields of the same type in the certificate's subject, OpenVPN
++     * should extract the value of the last matching field. This is essentially arbitrary and there
++     * is no correct choice here, but this test exists to make sure that the behavior is consistent
++     * between different backends.
++     */
++    openvpn_x509_cert_t *cert = get_certificate(cert_with_two_cns);
++
++    char username[TLS_USERNAME_LEN + 1] = { 0 };
++    assert_int_equal(backend_x509_get_username(username, sizeof(username), "CN", cert), SUCCESS);
++    assert_string_equal(username, "bar");
++    free_certificate(cert);
++}
++
++/* Certificate with a null-byte embedded in the CN.
++ *
++ * Generated with the following Python script:
++ *
++ * from cryptography import x509
++ * from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
++ * from cryptography.hazmat.primitives import hashes, serialization
++ * from cryptography.hazmat.primitives.asymmetric import ec
++ * import datetime
++ *
++ * private_key = ec.generate_private_key(ec.SECP384R1())
++ * now = datetime.datetime.now(datetime.timezone.utc)
++ * name = "with\x00null"
++ *
++ * cert = (x509.CertificateBuilder()
++ *             .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]))
++ *             .issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]))
++ *             .public_key(private_key.public_key())
++ *             .serial_number(x509.random_serial_number())
++ *             .not_valid_before(now)
++ *             .not_valid_after(now + datetime.timedelta(days=10 * 365))
++ *             .sign(private_key, hashes.SHA256()))
++ *
++ * print(str(cert.public_bytes(serialization.Encoding.PEM), encoding='utf-8'))
++ **/
++static const char *cert_with_null =
++    "-----BEGIN CERTIFICATE-----\n"
++    "MIIBZTCB66ADAgECAhRLWdNRl+C20KKBG4QC9HFPWlOtBzAKBggqhkjOPQQDAjAU\n"
++    "MRIwEAYDVQQDDAl3aXRoAG51bGwwHhcNMjYwODI4MTQzMDA4WhcNMzYwODI1MTQz\n"
++    "MDA4WjAUMRIwEAYDVQQDDAl3aXRoAG51bGwwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n"
++    "AAT7PE+vJCBiB4CVHBcvJVF9n5n/dGQIn4C8HeYE8M2iXYCIRW5wg6/mlPaeiJY/\n"
++    "Ywh3pa8zhto1+aczbJKTvLgRwXn6N5vNpME1c5iWMzY0WHe1dJyVtoRBvkNvy5+k\n"
++    "GuEwCgYIKoZIzj0EAwIDaQAwZgIxAP2ekdQ8muG8Nv2o3PsBp0CqiaSNByGiP75i\n"
++    "k099bUFvHp/3LSp8Wf4JK2iWzc5h6gIxAINBGg2xjlmMgpXxROKA9qQqaM3d92Mp\n"
++    "EzZyRyPM3PGR83ZPbIaYDq8lCVxVlbYr/Q==\n"
++    "-----END CERTIFICATE-----\n";
++
++void
++ssl_test_reject_null_in_cert_subject(void **state)
++{
++    openvpn_x509_cert_t *cert = get_certificate(cert_with_null);
++    struct gc_arena gc = gc_new();
++
++    /* Trying to get the subject as a whole should fail. */
++    assert_ptr_equal(x509_get_subject(cert, &gc), NULL);
++
++    /* Trying to extract the common name should fail. */
++    char username[TLS_USERNAME_LEN + 1] = { 0 };
++    assert_int_equal(backend_x509_get_username(username, sizeof(username), "CN", cert), FAILURE);
++    gc_free(&gc);
++    free_certificate(cert);
++}
+ 
+ int
+ main(void)
+@@ -908,7 +996,9 @@ main(void)
+         cmocka_unit_test(test_data_channel_roundtrip_bf_cbc),
+         cmocka_unit_test(test_data_channel_known_vectors_epoch),
+         cmocka_unit_test(test_data_channel_known_vectors_shortpktid),
+-        cmocka_unit_test(crypto_test_print_cert_details)
++        cmocka_unit_test(crypto_test_print_cert_details),
++        cmocka_unit_test(ssl_test_extract_last_matching_field),
++        cmocka_unit_test(ssl_test_reject_null_in_cert_subject)
+ 
+     };
+ 
diff --git a/meta-networking/recipes-support/openvpn/openvpn_2.7.7.bb b/meta-networking/recipes-support/openvpn/openvpn_2.7.7.bb
index 6338a0c816..f4aa8fce9c 100644
--- a/meta-networking/recipes-support/openvpn/openvpn_2.7.7.bb
+++ b/meta-networking/recipes-support/openvpn/openvpn_2.7.7.bb
@@ -12,6 +12,7 @@  SRC_URI = "https://github.com/OpenVPN/openvpn/releases/download/v${PV}/${BP}.tar
            file://openvpn \
            file://run-ptest \
            file://CVE-2026-88964.patch \
+           file://CVE-2026-84790.patch \
           "
 
 UPSTREAM_CHECK_URI = "https://openvpn.net/community-downloads"