Last active
June 8, 2023 15:54
-
-
Save chludwig-haufe/52182ad1647a272a3bc1282a6e14ba7a to your computer and use it in GitHub Desktop.
xmlsec1 wrapper script mentioned in https://github.com/IdentityPython/pysaml2/issues/906
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Wrapper around the xmlsec1 binary used to work around | |
the issue described in `pysaml2 #906`_. | |
It's listed in ``pyproject.toml``'s ``[tool.poetry.scripts]`` section | |
as ``xmlsec1wrapper``; this script name is then passed into pysaml2's | |
config as ``xmlsec_binary``. | |
Provided under the terms of the `Apache License`_ (Version 2.0, January 2004). | |
.. _pysaml2 #906: https://github.com/IdentityPython/pysaml2/issues/906 | |
.. _Apache License: http://www.apache.org/licenses/ | |
""" | |
import sys | |
from collections.abc import Sequence | |
from os import execvp | |
from re import MULTILINE, Match, Pattern | |
from re import compile as re_compile | |
from subprocess import PIPE, STDOUT | |
from subprocess import run as run_subprocess | |
from typing import Final, NoReturn, TextIO | |
COMMAND_NAME: Final[str] = "xmlsec1" | |
VERSION_PATTERN: Final[Pattern[str]] = re_compile( | |
r"^\w*\s*(?P<version>(?P<major>\d+)\.(?P<minor>\d+)(?P<patch>\d+)?)", MULTILINE | |
) | |
VERIFICATION_RESULT_PATTERN: Final[Pattern[str]] = re_compile( | |
r"((?:^|\s+)(?:OK|FAIL)(?:\s+|$))" | |
) | |
def is_xmlsec1_before_1_3() -> bool: | |
""" | |
Determine whether the xmlsec1 executable found on the search path | |
has a version older than 1.3.0. | |
:return: ``True`` if and only if the xmlsec1 binary on the search path | |
reports a version < 1.3.0 | |
:raises FileNotFoundError: the xmlsec1 binary is not found on the search path | |
:raises ValueError: the version is not found in the output of ``xmlsec1 --version`` | |
""" | |
process_result = run_subprocess( | |
(COMMAND_NAME, "--version"), | |
encoding="ascii", | |
stdout=PIPE, | |
stderr=STDOUT, | |
) | |
if not (match := next(VERSION_PATTERN.finditer(process_result.stdout), None)): | |
raise ValueError( | |
f"version not found in output of {COMMAND_NAME}: {process_result.stdout}" | |
) | |
major = match.group("major") | |
minor = match.group("minor") | |
return int(major) <= 1 and int(minor) <= 2 | |
def _lax_key_search_position(args: Sequence[str]) -> int: | |
for cmd in ("--encrypt", "--decrypt", "--sign", "--verify"): | |
try: | |
return args.index(cmd) + 1 | |
except ValueError: | |
pass | |
return -1 | |
def adapt_args(args: Sequence[str]) -> list[str]: | |
""" | |
Construct a copy of the arguments (as a list) with the command | |
(i.e., the first argument) replaced with :py:const:`COMMAND_NAME` | |
and an extra option ``--lax-key-search`` if the arguments include | |
one of ``--encrypt``, ``--decrypt``, ``--sign``, or ``--verify``. | |
:param args: xmlsec1 (wrapper) arguments | |
:return: arguments adapted to xmlsec1 version 1.3.0 | |
""" | |
args = list(args) | |
if (pos := _lax_key_search_position(args)) > 0: | |
args.insert(pos, "--lax-key-search") | |
args[0] = COMMAND_NAME | |
return args | |
def run_verify(args: Sequence[str]) -> NoReturn: | |
""" | |
Run the command specified by args in a subprocess, | |
capture its output (both stdout and stderr), reprint this output with | |
any term "OK" or "FAIL" placed on a separate line, and then | |
terminate the current process with the sub-process's return code. | |
:param args: command line | |
""" | |
def replace_result(match: Match[str]) -> str: | |
if match.end(0) == match.endpos: | |
suffix = "" | |
else: | |
suffix = "\n" | |
return f"\n{match.group(0).strip()}{suffix}" | |
def patch_output(target: TextIO, captured: str) -> None: | |
for line in captured.splitlines(): | |
print(VERIFICATION_RESULT_PATTERN.sub(replace_result, line), file=target) | |
process_result = run_subprocess(args, capture_output=True, encoding="ascii") | |
patch_output(sys.stdout, process_result.stdout) | |
patch_output(sys.stderr, process_result.stderr) | |
exit(process_result.returncode) | |
def xmlsec1_wrapper() -> NoReturn: | |
""" | |
A wrapper around the xmlsec1 command line tool that works around the differences | |
between version 1.2.7 and 1.3.0 when called from the pysaml2 library. | |
If the xmlsec1 binary found on the path is version 1.2.7 or older, then | |
no adaption is necessary, and we execute the binary with the arguments received. | |
Otherwise, if the xmlsec1 command is one of "--encrypt", "--decrypt", "--sign", | |
or "--verify", then we add the option "--lax-key-search" to the argument list. | |
Finally, if the xmlsec1 command is "--verify", then we ensure the verification result | |
string "OK" or "FAIL" appears on a separate line in the output without whitespace or | |
other text around it (as expected by saml2.sigver.parse_xmlsec_output). | |
Otherwise, we simply execute the binary with the adapted arguments. | |
""" | |
if is_xmlsec1_before_1_3(): | |
# xmlsec1 is version 1.2.7 or older, no adaption necessary | |
execvp(COMMAND_NAME, [COMMAND_NAME] + sys.argv[1:]) | |
args = adapt_args(sys.argv) | |
if "--verify" in args: | |
run_verify(args) | |
else: | |
execvp(COMMAND_NAME, args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment