diff options
| author | Enji Cooper <ngie@FreeBSD.org> | 2026-06-11 03:59:58 +0000 |
|---|---|---|
| committer | Enji Cooper <ngie@FreeBSD.org> | 2026-06-11 04:07:34 +0000 |
| commit | ca66129a2c8ace5af1b76b24f48e27ba5c9eac8b (patch) | |
| tree | 8a32f83d7363c7a8d87e23b869dde19cc59d3b0d /crypto/openssl/test/cms-msg/make_missing_kdf_der.py | |
| parent | 4df539cc3864f45e980774ac77238e0781b930d7 (diff) | |
openssl: update to 3.0.21
This change is a security release which resolves several issues with OpenSSL 3.0,
the highest severity issue being ranked "High". Users are strongly encouraged to
update to this release.
More information about the release (from a high level) can be found in
the release notes [1].
Tested with: (make tinderbox; spotchecks on dev hosts)
1. https://github.com/openssl/openssl/blob/openssl-3.0.21/NEWS.md
- :main and :stable/15 run OpenSSL 3.5 and :stable/14, et al runs
OpenSSL 3.0.
- This is not a merge commit because our pre-receive checkers still
don't allow direct MFVs without going through :main first.
- This is a direct commit to stable/14.
Diffstat (limited to 'crypto/openssl/test/cms-msg/make_missing_kdf_der.py')
| -rw-r--r-- | crypto/openssl/test/cms-msg/make_missing_kdf_der.py | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/crypto/openssl/test/cms-msg/make_missing_kdf_der.py b/crypto/openssl/test/cms-msg/make_missing_kdf_der.py new file mode 100644 index 000000000000..5b3fc0f6eeda --- /dev/null +++ b/crypto/openssl/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()) |
