242 lines
8.1 KiB
Diff
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");
|
|
+ });
|
|
+};
|
|
+
|