#!/bin/sh # vim: set noet sts=4 sw=4 ts=4: # File: cepceslib.sh # Location: https://bgstack15.ddns.net/cgit/cepceslib # Author: bgstack15, Sathvik Kolla # SPDX-License-Identifier: GPL-3.0-only # Startdate: 2024-08-23 08:21 # Title: CES username enrollment # Purpose: # History: # Usage: # Reference: # https://gist.github.com/leechristensen/28e4ddf89d77b70fe3e694684374c8a5 # https://www.server-world.info/en/note?os=Windows_Server_2022&p=iis&f=8 # https://stackoverflow.com/questions/3765212/an-error-occurred-when-verifying-security-for-the-message # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wstep/a22e793f-2c7e-4f5e-a22c-f05c49535855 # chatgpt for wsse:usernametoken fields and all xmlns entries # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xcep/3642fda9-8de2-417a-adad-9d368ffe8fc2 # https://medium.com/@fmcalbuquerque/python-elementtree-xml-api-with-dynamic-namespaces-171d9c9f391e # Improve: # use env vars for CN and SANs # Dependencies: # openssl, python3 # Documentation: README.md gen_csr() { # input env vars: KEYFILE, CSRFILE, TEMPLATE _cnf="$( mktemp )" cat >"${_cnf}" <"${CESFILE}" < http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep urn:uuid:$( uuidgen ) ${CESURL} http://www.w3.org/2005/08/addressing/anonymous ${CESUSER} ${CESPASSWORD} http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3 http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue ${_csr_contents} EOFCES } submit_ces_request() { # input env vars: CESURL, CESFILE # -k for irony curl --silent \ "${CESURL}" \ -H "Content-Type: application/soap+xml" \ -X POST \ --data @"${CESFILE}" \ -k } parse_ces_response() { # input env vars: CESRESPONSEFILE { printf '%s\r\n' '-----BEGIN PKCS7-----' python3 <<-EOFPYTHON import xml.etree.ElementTree as ET, sys rf = "${CESRESPONSEFILE}" tree = ET.parse(rf) print(tree.findall(".//*[@ValueType='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#PKCS7']")[0].text) EOFPYTHON printf '%s\r\n' '-----END PKCS7-----' } | grep -E '.' | openssl pkcs7 -in /dev/stdin -print_certs } use_ces() { # input env vars: KEYFILE, CSRFILE, CESURL, CESPASSWORD/CESPASSWODFILE, TEMPLATE, CERTFILE # optional: CESFILE, CESRESPONSEFILE unset _used_temp_crf _used_temp_cf test -z "${KEYFILE}" && { echo "Fatal! Need KEYFILE. Aborted." 1>&2 ; return 1 ; } test -z "${CSRFILE}" && { echo "Fatal! Need CSRFILE. Aborted." 1>&2 ; return 1 ; } test -z "${CESURL}" && { echo "Fatal! Need CESURL. Aborted." 1>&2 ; return 1 ; } test -z "${CESUSER}" && { echo "Fatal! Need CESUSER. Aborted." 1>&2 ; return 1 ; } test -z "${TEMPLATE}" && { echo "Fatal! Need TEMPLATE. How about WebServerV3? Aborted." 1>&2 ; return 1 ; } test -z "${CESPASSWORD}" && test -z "${CESPASSWORDFILE}" && { echo "Fatal! Need CESPASSWORD or CESPASSWORDFILE. Aborted." 1>&2 ; return 1 ; } test -n "$( cat "${CESPASSWORDFILE}" 2>/dev/null )" && CESPASSWORD="$( cat "${CESPASSWORDFILE}" )" test -z "${CESPASSWORD}" && { echo "Fatal! Need CESPASSWORD or CESPASSWORDFILE populated. Aborted." 1>&2 ; return 1 ; } test -z "${CESRESPONSEFILE}" && { CESRESPONSEFILE="$( mktemp )" ; _used_temp_crf=1 ; echo "Using CESRESPONSEFILE=${CESRESPONSEFILE}" 1>&2 ; } test -z "${CERTFILE}" && { echo "Fatal! Need CERTFILE. Aborted." 1>&2 ; return 1 ; } test -z "$( cat "${KEYFILE}" 2>/dev/null )" && { # need to generate a new key openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "${KEYFILE}" } # so now we have KEYFILE. Lets assume we need to make CSRFILE test -z "$( cat "${CSRFILE}" 2>/dev/null )" && { gen_csr # will produce CSRFILE } test -z "${CESFILE}" && { CESFILE="$( mktemp )" ; _used_temp_cf=1 ; echo "Using CESFILE=${CESFILE}" 1>&2 ; } test -z "$( cat "${CESFILE}" 2>/dev/null )" && { gen_ces # will populate CESFILE } test -z "${SKIP_SUBMIT}" && { submit_ces_request > "${CESRESPONSEFILE}" } test -n "$( cat "${CESRESPONSEFILE}" 2>/dev/null )" && { parse_ces_response > "${CERTFILE}" } # CLEANUP test -n "${_used_temp_crf}" && rm -f "${CESRESPONSEFILE:-NOTHINGTODEL}" 1>/dev/null 2>&1 test -n "${_used_temp_cf}" && rm -f "${CESFILE:-NOTHINGTODEL}" 1>/dev/null 2>&1 test -z "${NO_CLEAN}" && { rm -f "${CESRESPONSEFILE}" "${CESFILE}" 1>/dev/null 2>&1 } } gen_cep() { # input env vars: CEPFILE, CEPURL, CESUSER, CESPASSWORD cat >"${CEPFILE}" < http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPolicies urn:uuid:$( uuidgen ) ${CEPURL} http://www.w3.org/2005/08/addressing/anonymous ${CESUSER} ${CESPASSWORD} 0001-01-01T00:00:00 EOFCEP } submit_cep_request() { # input env vars: CEPURL, CEPFILE curl --silent \ "${CEPURL}" \ -H "Content-Type: application/soap+xml; charset=utf-8" \ -X POST \ --data @"${CEPFILE}" \ -k } parse_cep_response() { # input env vars: CEPRESPONSEFILE # You might be tempted to use ElementTree.register_namespace(), but the author was unable to make that work here, and findall(,namespaces={"ns1":ns}) is not shorter than what is used here and was not tested. python3 <&2 ; return 1 ; } test -z "${CESPASSWORD}" && test -z "${CESPASSWORDFILE}" && { echo "Fatal! Need CESPASSWORD or CESPASSWORDFILE. Aborted." 1>&2 ; return 1 ; } test -n "$( cat "${CESPASSWORDFILE}" 2>/dev/null )" && CESPASSWORD="$( cat "${CESPASSWORDFILE}" )" test -z "${CESPASSWORD}" && { echo "Fatal! Need CESPASSWORD or CESPASSWORDFILE populated. Aborted." 1>&2 ; return 1 ; } # process test -z "${CEPFILE}" && { CEPFILE="$( mktemp )" ; _used_temp_cf=1 ; echo "Using CEPFILE=${CEPFILE}" 1>&2 ; } test -z "$( cat "${CEPFILE}" 2>/dev/null )" && { gen_cep # will populate CEPFILE } test -z "${CEPRESPONSEFILE}" && { CEPRESPONSEFILE="$( mktemp )" ; _used_temp_crf=1 ; echo "Using CEPRESPONSEFILE=${CEPRESPONSEFILE}" 1>&2 ; } test -z "${SKIP_SUBMIT}" && { submit_cep_request > "${CEPRESPONSEFILE}" } test -n "$( cat "${CEPRESPONSEFILE}" 2>/dev/null )" && { parse_cep_response # will print available templates } # CLEANUP test -n "${_used_temp_crf}" && rm -f "${CEPRESPONSEFILE:-NOTHINGTODEL}" 1>/dev/null 2>&1 test -n "${_used_temp_cf}" && rm -f "${CEPFILE:-NOTHINGTODEL}" 1>/dev/null 2>&1 test -z "${NO_CLEAN}" && { rm -f "${CEPRESPONSEFILE}" "${CEPFILE}" 1>/dev/null 2>&1 } } # https://stackoverflow.com/questions/2683279/how-to-detect-if-a-script-is-being-sourced # BEGIN IS-SOURCED sourced=0 if [ -n "$ZSH_VERSION" ]; then case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac elif [ -n "$KSH_VERSION" ]; then [ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ] && sourced=1 elif [ -n "$BASH_VERSION" ]; then (return 0 2>/dev/null) && sourced=1 else # All other shells: examine $0 for known shell binary filenames. # Detects `sh` and `dash`; add additional shell filenames as needed. case ${0##*/} in sh|-sh|dash|-dash) sourced=1;; esac fi # END IS-SOURCED # MAIN if test "${sourced}" = "0" ; then action="${action:-${1:-NONE}}" case "${action}" in use_cep) use_cep ;; use_ces) use_ces ;; *) echo "Warning: action ${action} not defined yet. Skipping..." 1>&2 ; exit 1 ; esac fi