Skip to content

Instantly share code, notes, and snippets.

@chludwig-haufe
Last active June 8, 2023 15:54
Show Gist options
  • Save chludwig-haufe/52182ad1647a272a3bc1282a6e14ba7a to your computer and use it in GitHub Desktop.
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
"""
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