#!/bin/bash # Copyright (c) 2012, 2013 Peter Palfrader # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. set -e set -u shopt -s extglob usage() { echo "Usage: $0 [-v [-v]] [-d ] -c [] -C [] [:] [[:] [...]]" echo " o If torserver-fpr is -, a list of fingerprints is read from stdin" echo " o with -C set, this script will not launch a tor. instead it will use the" echo " specified control socket to manipulate an already running Tor as needed." } verbose=0 cachedir="" datadir="" controlsocket="" while getopts "vhc:d:C:" OPTION do case "$OPTION" in v) verbose=$((verbose + 1)) ;; h) usage exit 0 ;; c) cachedir="$OPTARG" ;; d) datadir="$OPTARG" ;; C) controlsocket="$OPTARG" ;; *) usage >&2 exit 1 esac done shift $(($OPTIND - 1)) if [ "${1:-}" = "--help" ]; then usage exit 0 elif [ "$#" -lt 2 ]; then usage >&2 exit 1 fi torserver="$1"; shift declare -a targets=("$@") declare -a used_cache mapaddr="192.0.2.1" logpid="" torpid="" tmpdir="" cleanup() { [ -z "$torpid" ] || kill "$torpid" || true [ -z "$logpid" ] || kill "$logpid" || true [ -z "$tmpdir" ] || rm -rf "$tmpdir" || true } tmpdir=$(mktemp -d "/tmp/cert-check-XXXXXX") trap 'cleanup' EXIT if [ -z "$controlsocket" ]; then pidfile="$tmpdir/pid" torlog="$tmpdir/log" if [ "$verbose" -gt 0 ]; then tail -F "$torlog" & logpid=$! fi if command -v tor > /dev/null; then tor="tor" elif [ -x /usr/sbin/tor ]; then tor="/usr/sbin/tor" else echo >&2 "Cannot find tor executable" exit 1 fi if [ "$verbose" -gt 1 ]; then loglevel="info" else loglevel="notice" fi datadir=${datadir:-$tmpdir/tor} controlsocket="$tmpdir/sock" cat > "$tmpdir/torrc" << EOF DataDirectory $datadir RunAsDaemon 1 SocksPort auto PidFile $pidfile Log $loglevel file $torlog SafeLogging 0 ControlSocket $controlsocket StrictNodes 1 EOF mkdir -p -m 0700 "$datadir" if [ "$verbose" -gt 0 ]; then hush=""; else hush="--hush"; fi "$tor" $hush -f "$tmpdir/torrc" torpid="$(cat $pidfile)" fi eatdata() { local fn="$1"; shift if [ "$verbose" -gt 0 ]; then tee "$fn" else cat > "$fn" fi } expect_ok() { local line read code line <&${COPROC[0]} line=$(echo "$line" | tr -d '\r') if ! [ "$code" = "250" ]; then echo >&2 "Tor said '$code $line' - no idea what that means" exit 1 fi } sanitize() { egrep -v '(Session-ID:|Master-Key:|Start Time:|SSL handshake has read)' } direct_fetch_one() { local i="$1" local host host="${targets[$i]}" [ "$verbose" = 0 ] || echo "Directly to $host:" openssl s_client -no_ticket -showcerts -connect "$host" < /dev/null 2>&1 | eatdata "$tmpdir/cert-direct-$i" [ -n "$cachedir" ] && cp "$tmpdir/cert-direct-$i" "$cachedir/tesc3-$host" used_cache[$i]="" } direct_fetch() { local i="$1" local host host="${targets[$i]}" if [ -n "$cachedir" ] && [ -e "$cachedir/tesc3-$host" ] ; then [ "$verbose" = 0 ] || echo "Using cached certificate for $host." cp "$cachedir/tesc3-$host" "$tmpdir/cert-direct-$i" used_cache[$i]="1" else direct_fetch_one "$i" fi sanitize < "$tmpdir/cert-direct-$i" > "$tmpdir/cert-direct-$i.filtered" [ "$verbose" = 0 ] || echo "====" } # set default port for ((i=0; i < ${#targets[@]}; i++)); do host="${targets[$i]}" [ "${host%%+([0-9])}" = "$host" ] && targets[$i]="$host:443" done [ -n "$cachedir" ] && find "$cachedir" -name "tesc3-*" -mmin +1400 -exec rm '{}' '+' for ((i=0; i < ${#targets[@]}; i++)); do direct_fetch "$i" done coproc socat UNIX-CONNECT:"$controlsocket" - echo 'AUTHENTICATE' >&${COPROC[1]} expect_ok echo 'GETINFO net/listeners/socks' >&${COPROC[1]} read line <&${COPROC[0]} line=$(echo "$line" | tr -d '\r') socksport=$( echo "$line" | sed -e 's#^250-net/listeners/socks="127.0.0.1:##; s#"$##' ) if [ "$socksport" = "$line" ] || echo "$socksport" | grep -q '[^0-9]'; then echo >&2 "Did not manage to learn listening port from tor." echo >&2 "Got '$line' and parsed it into '$socksport'." exit 1 fi expect_ok cat > "$tmpdir/torsocks.conf" << EOF server = 127.0.0.1 server_port = $socksport EOF errors=0 while : ; do if [ "$torserver" = "-" ]; then read server [ -n "$server" ] || break else server="$torserver" fi server="${server// /}" # de-base64 if needed if [ "$(echo "$server" | wc -c)" = 28 ] ; then server="$(echo "$server" | perl -MMIME::Base64 -e 'print unpack("H*", decode_base64(<>))')" fi [ "$verbose" = 0 ] || echo "Setting ExitNodes $server" echo "RESETCONF ExitNodes" >&${COPROC[1]} expect_ok echo "SETCONF ExitNodes=\$$server" >&${COPROC[1]} expect_ok for ((i=0; i < ${#targets[@]}; i++)); do host="${targets[$i]}" rm -f "$tmpdir/cert-tor-$i" "$tmpdir/cert-tor-$i.filtered" [ "$verbose" = 0 ] || echo "Via $server to $host:" TORSOCKS_CONF_FILE="$tmpdir/torsocks.conf" torify openssl s_client -no_ticket -showcerts -connect "$host" < /dev/null 2>&1 | eatdata "$tmpdir/cert-tor-$i" sanitize < "$tmpdir/cert-tor-$i" > "$tmpdir/cert-tor-$i.filtered" if [ -n "used_cache[$i]" ] && ! diff "$tmpdir/cert-direct-$i.filtered" "$tmpdir/cert-tor-$i.filtered" > /dev/null; then [ "$verbose" = 0 ] || echo "Fetching a non-cached copy of $host's cert." direct_fetch_one "$i" sanitize < "$tmpdir/cert-direct-$i" > "$tmpdir/cert-direct-$i.filtered" fi if diff "$tmpdir/cert-direct-$i.filtered" "$tmpdir/cert-tor-$i.filtered" > /dev/null; then echo "RESULT[$host]: $server: No real differences." [ "$verbose" = 0 ] || diff -U100 "$tmpdir/cert-direct-$i" "$tmpdir/cert-tor-$i" || true elif egrep '^connect:errno=' "$tmpdir/cert-tor-$i" > /dev/null; then [ "$verbose" -lt 1 ] || diff -U100 "$tmpdir/cert-direct-$i" "$tmpdir/cert-tor-$i" || true echo "RESULT[$host]: $server: Connect failed" errors=$((errors | 0x02)) elif egrep '^[0-9]*:error:.*:ssl handshake failure:' "$tmpdir/cert-tor-$i" > /dev/null; then [ "$verbose" -lt 1 ] || diff -U100 "$tmpdir/cert-direct-$i" "$tmpdir/cert-tor-$i" || true echo "RESULT[$host]: $server: SSL Handshake failed" errors=$((errors | 0x04)) else echo "RESULT[$host]: $server: differences!" [ "$verbose" = 0 ] || echo "====" [ "$verbose" = 0 ] || echo "Diff:" diff -U100 "$tmpdir/cert-direct-$i" "$tmpdir/cert-tor-$i" || true errors=$((errors | 0x08)) fi done [ "$torserver" = "-" ] || break done exit "$errors"