Skip to content

Instantly share code, notes, and snippets.

@qrkourier
Last active December 19, 2024 19:48
Show Gist options
  • Save qrkourier/4f850053ab0c485937851490f2abc523 to your computer and use it in GitHub Desktop.
Save qrkourier/4f850053ab0c485937851490f2abc523 to your computer and use it in GitHub Desktop.
portable shell script verifies the server cert for a Ziti identity file
#!/bin/sh
#
## a POSIX-portable diagnostic for a Ziti identity's trust chain
#
# raise exceptions
set -e
set -u
# Default values
if [ -z "${ZITI_ALPN-}" ]; then
ZITI_ALPN="h2,http/1.1"
fi
if [ -z "${TMPDIR-}" ]; then
TMPDIR=$(mktemp -d)
fi
BASENAME=$(basename "$0")
if [ $# -gt 0 ] && [ -s "$1" ]
then
ZITI_IDENTITY=$1
elif [ -n "${ZITI_IDENTITY:-}" ] && [ -s "${ZITI_IDENTITY}" ]
then
:
else
printf '
Usage:
\t%s [ZITI_IDENTITY]
Example:
\t%s /opt/openziti/etc/identities/ziti_id.json
Options:
\tZITI_ALPN\tALPN protocols to use when connecting to the controller
\t\t\t[h2,http/1.1|ziti-ctrl] (default: %s)
' "$BASENAME" "$BASENAME" "$ZITI_ALPN"
exit 1
fi
get_ziti_url() {
if [ $# -eq 1 ] && [ -s "$1" ]
then
_ziti_id_file="$1"
else
echo "ERROR: get_ziti_url needs path to Ziti Identity JSON file as only param" >&2
return 1
fi
if command -v ${BIN_JQ-jq} >/dev/null 2>&1
then
_ztAPI=$(jq -r '
(if type == "string" then fromjson else . end) as $input |
($input.ztAPI | type) as $ztapi_type |
($input.ztAPI | if type == "string" then length else 0 end) as $ztapi_len |
($input.ztAPIs | type) as $ztapis_type |
($input.ztAPIs | if type == "array" then length else 0 end) as $ztapis_len |
($input.ztAPIs[0] | type) as $first_type |
if $ztapi_type == "string" and $ztapi_len > 0
then $input.ztAPI
elif $ztapis_type == "array" and $ztapis_len > 0 and $first_type == "string"
then $input.ztAPIs[0]
else empty
end' "$_ziti_id_file")
elif [ "$(echo '{}' | ${BIN_PY-python} -m json.tool 2>/dev/null)" = "{}" ]
then
_ztAPI=$(${BIN_PY-python} -c '
import sys, json
try:
data = json.load(sys.stdin)
ztAPI = data.get("ztAPI")
if ztAPI:
print(ztAPI)
elif "ztAPIs" in data and isinstance(data["ztAPIs"], list) and data["ztAPIs"]:
print(data["ztAPIs"][0])
else:
sys.exit(1)
except Exception as e:
sys.stderr.write("ERROR: " + str(e) + "\n")
sys.exit(1)
' < "$_ziti_id_file")
elif [ "$(${BIN_NODE-node} -e "console.log(JSON.parse(process.argv[1]))" '{}' 2>/dev/null)" = "{}" ]
then
_ztAPI=$(${BIN_NODE-node} -e '
try {
const data = require("fs").readFileSync(process.argv[1], "utf8");
const json = JSON.parse(data);
if (typeof json.ztAPI === "string") {
console.log(json.ztAPI);
} else if (Array.isArray(json.ztAPIs) && json.ztAPIs.length > 0 && typeof json.ztAPIs[0] === "string") {
console.log(json.ztAPIs[0]);
} else {
throw new Error("Missing or invalid ztAPI/ztAPIs properties");
}
} catch (e) {
console.error("ERROR:", e.message);
process.exit(1);
}' "$_ziti_id_file")
else
echo "ERROR: failed to find a JSON parser; need jq, python, or node" >&2
return 1
fi
if [ -z "$_ztAPI" ]
then
echo "ERROR: failed to find a Ziti API URL" >&2
return 1
else
echo "$_ztAPI"
unset _ztAPI
fi
}
get_ziti_edge() {
if [ $# -eq 1 ] && [ -n "$1" ] && [ "$(printf '%s' "$1" | cut -c1-8)" = "https://" ]
then
_ztAPI="$1"
else
echo "ERROR: parse_ziti_url needs Ziti API URL as only param" >&2
return 1
fi
_ziti_edge=$(echo "$_ztAPI" | awk -F[/:] '{print ($4!="" && $5!="")?$4":"($5==""?"443":$5):"ERROR"}')
if [ "$(echo "$_ziti_edge" | awk -F: '{print NF}')" -ne 2 ]
then
echo "ERROR: failed to extract a host and port from $_ztAPI" >&2
return 1
fi
echo "$_ziti_edge"
}
get_server_chain() {
if [ $# -eq 1 ] && [ -n "$1" ] && [ "$(echo "$1" | awk -F[:] '{print NF}')" -eq 2 ]
then
_ziti_edge="$1"
else
echo "ERROR: get_server_chain needs ziti edge as only param like host:port" >&2
return 1
fi
openssl s_client \
-alpn "${ZITI_ALPN}" \
-connect "${_ziti_edge}" \
-showcerts </dev/null 2>/dev/null \
| openssl storeutl -certs /dev/stdin
}
get_server_leaf() {
if [ $# -eq 1 ] && [ -s "$1" ]
then
_server_chain="$1"
else
echo "ERROR: get_server_leaf needs path to server chain PEM file as only param" >&2
return 1
fi
openssl storeutl -certs "$_server_chain" \
| openssl x509 -outform PEM
}
get_well_known() {
if [ $# -eq 1 ] && [ -n "$1" ] && [ "$(echo "$1" | awk -F[:] '{print NF}')" -eq 2 ]
then
_ziti_edge="$1"
else
echo "ERROR: get_well_known needs ziti edge as only param like host:port" >&2
return 1
fi
if command -v "${BIN_CURL-curl}" >/dev/null 2>&1
then
_encoded_pkcs7=$(curl --silent --show-error --insecure "https://$_ziti_edge/.well-known/est/cacerts")
elif command -v "${BIN_WGET-wget}" >/dev/null 2>&1
then
_encoded_pkcs7=$(wget --quiet --no-check-certificate --output-document=- "https://$_ziti_edge/.well-known/est/cacerts")
else
echo "ERROR: failed to find an HTTP client; need curl or wget" >&2
return 1
fi
if [ -n "$_encoded_pkcs7" ]
then
echo "$_encoded_pkcs7" \
| base64 -d \
| openssl pkcs7 -inform DER -outform PEM -print_certs
else
echo "ERROR: failed to fetch well-known .well-known/est/cacerts" >&2
return 1
fi
}
# shellcheck disable=SC2317
summary() {
_server_leaf_dn=$(
openssl storeutl -certs -noout -text ./server-leaf.pem \
| grep -E '(Subject|Issuer):'
) 2>/dev/null || true # ignore errors
if [ -n "$_server_leaf_dn" ]
then
echo "DEBUG: server leaf distinguished name:"
echo "$_server_leaf_dn"
fi
_server_chain_dn=$(
openssl storeutl -certs -noout -text ./server-chain.pem \
| grep -E '(Subject|Issuer):'
) 2>/dev/null || true # ignore errors
if [ -n "$_server_chain_dn" ]
then
echo "DEBUG: server chain distinguished names:"
echo "$_server_chain_dn"
fi
_well_known_dn=$(
openssl storeutl -certs -noout -text ./well-known.pem \
| grep -E '(Subject|Issuer):'
)
if [ -n "$_well_known_dn" ]
then
echo "DEBUG: well-known distinguished names:"
echo "$_well_known_dn"
fi
echo "DEBUG: temporary files:"
if command -v tree >/dev/null 2>&1
then
_tmp_files=$(tree "$(pwd)") 2>/dev/null || true # ignore errors
else
_tmp_files=$(ls -lARh "$(pwd)") 2>/dev/null || true # ignore errors
fi
if [ -n "$_tmp_files" ]
then
echo "$_tmp_files"
fi
}
# shellcheck disable=SC2317
cleanup() {
_es=$?
if [ "$_es" -ne 0 ]; then
summary
fi
exit "$_es"
}
_bins="openssl base64 awk grep"
for _bin in $_bins
do
if ! command -v "$_bin" >/dev/null 2>&1
then
echo "ERROR: failed to find $_bin (need $_bins)" >&2
exit 1
fi
done
cd "${TMPDIR}"
trap cleanup EXIT
ZITI_URL=$(get_ziti_url "${ZITI_IDENTITY-$1}")
ZITI_EDGE=$(get_ziti_edge "$ZITI_URL")
get_well_known "$ZITI_EDGE" > ./well-known.pem
get_server_chain "$ZITI_EDGE" > ./server-chain.pem
get_server_leaf ./server-chain.pem > ./server-leaf.pem
if openssl verify \
-CAfile ./well-known.pem \
-untrusted ./server-leaf.pem \
./server-leaf.pem >/dev/null 2>&1
then
echo 'INFO: verified by root CA'
exit 0
elif openssl verify \
-partial_chain \
-CAfile ./well-known.pem \
-untrusted ./server-leaf.pem \
./server-leaf.pem >/dev/null 2>&1
then
echo 'ERROR: partially verified by intermediate CA, missing root CA' >&2
exit 1
else
echo 'ERROR: not verified' >&2
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment