diff options
Diffstat (limited to 'cepceslib.sh')
-rwxr-xr-x | cepceslib.sh | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/cepceslib.sh b/cepceslib.sh new file mode 100755 index 0000000..b461c1a --- /dev/null +++ b/cepceslib.sh @@ -0,0 +1,278 @@ +#!/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}" <<EOFCONF +oid_section = new_oids +[ req ] +prompt = no +default_bits = 4096 +default_md = sha256 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +req_extensions = req_ext + +[ new_oids ] +certificateTemplateName = 1.3.6.1.4.1.311.20.2 + +[ req_distinguished_name ] +C = US +ST = New York +L = New York +O = Example Organization +# Important value +CN = $( hostname -f ) +#emailAddress = noreply@example.com + +[ req_ext ] +basicConstraints = CA:FALSE +keyUsage = digitalSignature, keyEncipherment +subjectAltName = @alt_names +certificateTemplateName = ASN1:UTF8STRING:${TEMPLATE} + +[ alt_names ] +# Important value +DNS.1 = $( hostname -f ) +DNS.2 = $( hostname -s ) +EOFCONF + # generate the csr + openssl req -config "${_cnf}" -new -key "${KEYFILE}" -out "${CSRFILE}" + # end of gen_csr + rm "${_cnf}" +} + +gen_ces() { + # input env vars: CSRFILE, CESFILE, CESURL, CESUSER, CESPASSWORD + # strip header/footer and make it on a single line + _csr_contents="$( sed -r -e '/^-----/d' "${CSRFILE}" | tr -d '\n' )" + cat >"${CESFILE}" <<EOFCES + <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" + xmlns:s="http://www.w3.org/2003/05/soap-envelope" + xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> + <s:Header> + <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action> + <a:MessageID>urn:uuid:$( uuidgen )</a:MessageID> + <a:To s:mustUnderstand="1">${CESURL}</a:To> + <a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo> + <wsse:Security s:mustUnderstand="1"> + <wsse:UsernameToken> + <wsse:Username>${CESUSER}</wsse:Username> + <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">${CESPASSWORD}</wsse:Password> + </wsse:UsernameToken> + </wsse:Security> + </s:Header> + <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <RequestSecurityToken PreferredLanguage="en-US" xmlns="http://docs.oasis-open.org/ws-sx/ws-trust/200512"> + <TokenType>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3</TokenType> + <RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</RequestType> + <BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary" a:Id="" + xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" + xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> + ${_csr_contents} + </BinarySecurityToken> + <RequestID xsi:nil="true" xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollment" /> + </RequestSecurityToken> + </s:Body> + </s:Envelope> +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}" <<EOFCEP + <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" + xmlns:s="http://www.w3.org/2003/05/soap-envelope" + xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" + xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> + <s:Header> + <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPolicies</a:Action> + <a:MessageID>urn:uuid:$( uuidgen )</a:MessageID> + <a:To s:mustUnderstand="1">${CEPURL}</a:To> + <a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo> + <wsse:Security s:mustUnderstand="1"> + <wsse:UsernameToken> + <wsse:Username>${CESUSER}</wsse:Username> + <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">${CESPASSWORD}</wsse:Password> + </wsse:UsernameToken> + </wsse:Security> + </s:Header> + <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <GetPolicies xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"> + <client> + <lastUpdate>0001-01-01T00:00:00</lastUpdate> + <preferredLanguage xsi:nil="true"></preferredLanguage> + </client> + <requestFilter xsi:nil="true"></requestFilter> + </GetPolicies> + </s:Body> + </s:Envelope> +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 <<EOFPYTHON +import xml.etree.ElementTree as ET, sys +rf = "${CEPRESPONSEFILE}" +ns = "{http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy}" +tree = ET.parse(rf) +print("endpoints:" + ','.join([i.text for i in tree.findall(f".//{ns}GetPoliciesResponse/{ns}cAs/{ns}cA/{ns}uris/{ns}cAURI/{ns}uri")])) +for p in tree.findall(f".//{ns}policies/{ns}policy"): + if p.find(f"./{ns}attributes/{ns}permission/{ns}enroll").text.lower() in ["true","t","1","yes","y"]: + print(p.find(f"./{ns}attributes/{ns}commonName").text) +EOFPYTHON +} + +use_cep() { + # input env vars: CEPURL, CESUSER, CESPASSWORD + # optional: CEPFILE, CEPRESPONSEFILE + unset _used_temp_cf _used_temp_crf + test -z "${CEPURL}" && { echo "Fatal! Need CEPURL. 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 ; } + # 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 |