openssl/0066-CVE-2026-42766.patch
2026-06-11 13:21:23 -04:00

242 lines
8.1 KiB
Diff

From df32fc58bd726eecf88ac67b22fef0c8ff5eeef8 Mon Sep 17 00:00:00 2001
From: Igor Ustinov <igus@openssl.foundation>
Date: Thu, 21 May 2026 08:36:54 +0200
Subject: [PATCH 1/2] Fix potential NULL dereference processing CMS
PasswordRecipientInfo
Avoid NULL dereferencing when keyDerivationAlgorithm is absent
in CMS PasswordRecipientInfo.
Fixes CVE-2026-42766
---
crypto/cms/cms_pwri.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/crypto/cms/cms_pwri.c b/crypto/cms/cms_pwri.c
index ac869a37f93..206ecd85e69 100644
--- a/crypto/cms/cms_pwri.c
+++ b/crypto/cms/cms_pwri.c
@@ -368,6 +368,11 @@ int ossl_cms_RecipientInfo_pwri_crypt(const CMS_ContentInfo *cms,
/* Finish password based key derivation to setup key in "ctx" */
+ if (algtmp == NULL) {
+ ERR_raise_data(ERR_LIB_CMS, CMS_R_INVALID_KEY_ENCRYPTION_PARAMETER,
+ "Missing KeyDerivationAlgorithm");
+ goto err;
+ }
if (!EVP_PBE_CipherInit_ex(algtmp->algorithm,
(char *)pwri->pass, (int)pwri->passlen,
algtmp->parameter, kekctx, en_de,
From 1eb593cce1e05599d22de6ab158a23c755146fae Mon Sep 17 00:00:00 2001
From: Igor Ustinov <igus@openssl.foundation>
Date: Wed, 20 May 2026 20:02:43 +0200
Subject: [PATCH 2/2] Test for CVE-2026-42766
The script make_missing_kdf_der.py was developed by Mayank Jangid
and Kushal Khemka.
Co-Authored-by: Mayank Jangid <mayank.jangid.moon@gmail.com>
Co-Authored-by: Kushal Khemka <kushalkhemka559@gmail.com>
---
test/cms-msg/make_missing_kdf_der.py | 137 +++++++++++++++++++++++++++
test/cms-msg/missing-kdf.der | Bin 0 -> 190 bytes
test/recipes/80-test_cms.t | 21 +++-
3 files changed, 157 insertions(+), 1 deletion(-)
create mode 100755 test/cms-msg/make_missing_kdf_der.py
create mode 100644 test/cms-msg/missing-kdf.der
diff --git a/test/cms-msg/make_missing_kdf_der.py b/test/cms-msg/make_missing_kdf_der.py
new file mode 100755
index 00000000000..5b3fc0f6eed
--- /dev/null
+++ b/test/cms-msg/make_missing_kdf_der.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+# This script generates missing-kdf.der - a password-encrypted CMS message
+# without the keyDerivationAlgorithm field, which is used in the
+# “PWRI missing keyDerivationAlgorithm regression” test.
+#
+# Usage: python3 make_missing_kdf_der.py valid.der missing-kdf.der
+
+from __future__ import annotations
+
+import argparse
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class Node:
+ off: int
+ tag: int
+ hdr_len: int
+ length: int
+ end: int
+ children: list["Node"]
+
+
+def read_len(data: bytes, off: int) -> tuple[int, int]:
+ first = data[off]
+ if first < 0x80:
+ return first, 1
+ n = first & 0x7F
+ if n == 0 or n > 4:
+ raise ValueError(f"unsupported DER length form at {off}")
+ val = 0
+ for b in data[off + 1 : off + 1 + n]:
+ val = (val << 8) | b
+ return val, 1 + n
+
+
+def parse_node(data: bytes, off: int) -> Node:
+ tag = data[off]
+ length, len_len = read_len(data, off + 1)
+ hdr_len = 1 + len_len
+ end = off + hdr_len + length
+ children: list[Node] = []
+ if tag & 0x20:
+ cur = off + hdr_len
+ while cur < end:
+ child = parse_node(data, cur)
+ children.append(child)
+ cur = child.end
+ if cur != end:
+ raise ValueError(f"child parse ended at {cur}, expected {end}")
+ return Node(off=off, tag=tag, hdr_len=hdr_len, length=length, end=end, children=children)
+
+
+def encode_len(length: int, existing_len_len: int) -> bytes:
+ if existing_len_len == 1:
+ if length >= 0x80:
+ raise ValueError("new length no longer fits in short-form DER")
+ return bytes([length])
+ payload_len = existing_len_len - 1
+ max_len = (1 << (payload_len * 8)) - 1
+ if length > max_len:
+ raise ValueError("new length no longer fits in existing long-form DER")
+ out = bytearray([0x80 | payload_len])
+ for shift in range((payload_len - 1) * 8, -8, -8):
+ out.append((length >> shift) & 0xFF)
+ return bytes(out)
+
+
+def patch_length_field(buf: bytearray, node: Node, delta: int) -> None:
+ new_len = node.length + delta
+ if new_len < 0:
+ raise ValueError("negative patched length")
+ len_bytes = encode_len(new_len, node.hdr_len - 1)
+ start = node.off + 1
+ end = start + len(node.hdr_len.to_bytes(1, "big")) - 1 # unused, kept for clarity
+ buf[start : start + len(len_bytes)] = len_bytes
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description="Remove PWRI keyDerivationAlgorithm from a CMS DER blob.")
+ ap.add_argument("input_der")
+ ap.add_argument("output_der")
+ args = ap.parse_args()
+
+ data = Path(args.input_der).read_bytes()
+ root = parse_node(data, 0)
+
+ # CMS structure we expect:
+ # SEQUENCE { OID envelopedData, [0] SEQUENCE { version, SET recipientInfos, ... } }
+ ed_wrapper = root.children[1]
+ env_seq = ed_wrapper.children[0]
+ recipient_set = env_seq.children[1]
+ pwri_choice = recipient_set.children[0] # [3]
+
+ if pwri_choice.tag != 0xA3:
+ raise ValueError(f"expected PWRI choice tag 0xA3, found 0x{pwri_choice.tag:02x}")
+ if len(pwri_choice.children) < 3:
+ raise ValueError("unexpected PWRI child count")
+
+ version = pwri_choice.children[0]
+ maybe_kdf = pwri_choice.children[1]
+ keyenc = pwri_choice.children[2]
+ if version.tag != 0x02:
+ raise ValueError("PWRI version is not INTEGER")
+ if maybe_kdf.tag != 0xA0:
+ raise ValueError(f"PWRI child after version is not [0] keyDerivationAlgorithm: 0x{maybe_kdf.tag:02x}")
+ if keyenc.tag != 0x30:
+ raise ValueError("PWRI keyEncryptionAlgorithm is not SEQUENCE")
+
+ remove_start = maybe_kdf.off
+ remove_end = maybe_kdf.end
+ remove_len = remove_end - remove_start
+
+ out = bytearray(data)
+ del out[remove_start:remove_end]
+
+ # Adjust ancestors whose length spans the removed field.
+ for node in [root, ed_wrapper, env_seq, recipient_set, pwri_choice]:
+ patch_length_field(out, node, -remove_len)
+
+ Path(args.output_der).write_bytes(out)
+ print(f"removed {remove_len} bytes at [{remove_start}, {remove_end})")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/test/cms-msg/missing-kdf.der b/test/cms-msg/missing-kdf.der
new file mode 100644
index 0000000000000000000000000000000000000000..3db602e47c23b76a9a55a707da18c5059ef38c9e
GIT binary patch
literal 190
zcmXqL+|9<R)#lOmotKfFc|qe^gT_@%jLe3OX^R_^nHU)iblA9|(wqX!oCdONoC$3n
zjH%2lj9M%LjeqC+*0CO$x3i6Lf!y{7Pgo2D?Y_1wJ@|E12X~D{Lpyt#=(Ru5SM)zF
zT>UakXma9=|F&g+-%70RE0WJSs>3LeamK&~VLc<7>0T^O8mGQjr>lr=GC8iQ@UuZ+
V#SA9-u!z04PD_OT{BCf}9snrZM1BAO
literal 0
HcmV?d00001
diff --git a/test/recipes/80-test_cms.t b/test/recipes/80-test_cms.t
index 152a1a55a0a..160ad81ae38 100644
--- a/test/recipes/80-test_cms.t
+++ b/test/recipes/80-test_cms.t
@@ -56,7 +56,7 @@ my ($no_des, $no_dh, $no_dsa, $no_ec, $no_ec2m, $no_rc2, $no_zlib)
$no_rc2 = 1 if disabled("legacy");
-plan tests => 31;
+plan tests => 32;
ok(run(test(["pkcs7_test"])), "test pkcs7");
@@ -1702,3 +1702,22 @@ subtest "ML-KEM KEMRecipientInfo tests for CMS" => sub {
"accept CMS verify with SLH-DSA-SHAKE-256s");
}
};
+
+# Regression test for NULL dereference in PWRI decrypt path
+# when optional keyDerivationAlgorithm is omitted.
+subtest "PWRI missing keyDerivationAlgorithm regression" => sub {
+ plan tests => 1;
+
+ with({ exit_checker => sub { return shift == 4; } }, sub {
+ ok(run(app([
+ "openssl", "cms", @prov,
+ "-decrypt",
+ "-inform", "DER",
+ "-in",
+ srctop_file('test', 'cms-msg', 'missing-kdf.der'),
+ "-out", "pwri-out.txt",
+ "-pwri_password", "secret"])),
+ "missing keyDerivationAlgorithm is rejected");
+ });
+};
+