#!/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())