Commit f04d65692d for openssl.org
commit f04d65692dd8109be9f387ce2ddd9fb1ab612b6b
Author: Igor Ustinov <igus@openssl.foundation>
Date: Wed May 20 20:02:43 2026 +0200
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>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Mon Jun 8 18:57:53 2026
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 0000000000..5b3fc0f6ee
--- /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 0000000000..3db602e47c
Binary files /dev/null and b/test/cms-msg/missing-kdf.der differ
diff --git a/test/recipes/80-test_cms.t b/test/recipes/80-test_cms.t
index 152a1a55a0..160ad81ae3 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 => 37;
+plan tests => 38;
ok(run(test(["pkcs7_test"])), "test pkcs7");
@@ -1702,3 +1702,22 @@ subtest "ML-KEM KEMRecipientInfo tests for CMS" => sub {
"CMS decrypt with ML-KEM-768 and using UKM");
}
};
+
+# 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");
+ });
+};
+