#!/bin/bash
#
# DKIM verification script
# Written by John Ryland
# Copyright 2015
#
# Usage: dkim-verify.sh raw-email.txt
#
# Returns:
# 0 if success and verifies OK
# 1 if has DKIM-signature and was processed but verification failed
# -1 for other errors
#
PROGRAM_NAME=`basename $0`
INPUT=$1
if [ "$#" != "1" ]
then
echo "$PROGRAM_NAME: Invalid number of parameters"
exit -1
fi
if [ ! -f "$INPUT" ]
then
echo "$PROGRAM_NAME: Invalid input file"
exit -1
fi
mkdir -p $HOME/tmp
TEMPDIR=`mktemp -d -p $HOME/tmp`
if [ ! -d "$TEMPDIR" ]
then
echo "$PROGRAM_NAME: Error creating temp directory"
exit -1
fi
echo "TEMPDIR=$TEMPDIR"
cat << EOF > $TEMPDIR/parameters.sh
DKIM_VERSION=0
DKIM_ALGORITHM=rsa-sha256
DKIM_CANONICALIZATION=relaxed/relaxed
DKIM_DOMAIN=domain
DKIM_SUBDOMAIN=sub
DKIM_TIMESTAMP=0
DKIM_BODY_HASH=0
DKIM_HEADERS=0
DKIM_BASE64_SIGNATURE=0
DKIM_SIGNATURE=0
EOF
# Split email text in to 2 parts, the email headers and the email body
cat $INPUT | tr -d '\r' | (
# cat $INPUT | (
echo -n "" > $TEMPDIR/headers.txt
while IFS= read -r line
do
echo "$line" >> $TEMPDIR/headers.txt
if [ "$line" == '' ]
then
break
fi
done
echo -n "" > $TEMPDIR/body.txt
while IFS= read -r line
do
echo "$line" >> $TEMPDIR/body.txt
done
)
# Handles a single trailing empty line
# Perhaps this needs to remove all trailing empty lines?
LAST_LINE=`tail -n 1 $TEMPDIR/body.txt`
if [ "$LAST_LINE" == '' ]
then
sed -i '$ d' $TEMPDIR/body.txt
fi
# Assuming dkim 'relaxed' canonicalization
cat $TEMPDIR/body.txt | sed 's/[ \t][ \t]*/ /g' | sed 's/[ \t]$//g' | unix2dos > $TEMPDIR/cbody.txt
cat $TEMPDIR/headers.txt | sed -e 's/\(.*\)[ \t]*:[ \t]\(.*\)/\L\1:\E\2/' | sed ':a;N;$!ba;s/[ \t]*\n[ \t][ \t]*/ /g' > $TEMPDIR/cheaders.txt
echo -n "dkim-signature:" > $TEMPDIR/signature.txt
cat $TEMPDIR/cheaders.txt | while read LINE
do
HEADER_FIELD=`echo "$LINE" | cut -d ':' -f 1`
HEADER_VALUE=`echo "$LINE" | cut -d ':' -f 2-`
# echo " field = $HEADER_FIELD"
if [ "dkim-signature" == "$HEADER_FIELD" ]
then
# echo " $HEADER_FIELD = $HEADER_VALUE"
echo "$HEADER_VALUE" | sed 's/\;/\;\n/g' | while read ATTRIB_LINE
do
ATTRIB=`echo "$ATTRIB_LINE" | tr -d ' '`
if [ "$ATTRIB" != "" ]
then
ATTRIB_NAME=`echo "$ATTRIB" | cut -d '=' -f 1`
ATTRIB_VALUE=`echo "$ATTRIB" | cut -d '=' -f 2-`
# echo " ATTRIB: $ATTRIB"
# echo " -$ATTRIB_NAME- = -$ATTRIB_VALUE-"
[ "$ATTRIB_NAME" != "b" ] && echo -n "$ATTRIB_LINE " >> $TEMPDIR/signature.txt
[ "$ATTRIB_NAME" == "b" ] && echo -n "$ATTRIB_LINE" | sed 's/b=[^;]*\(.*\)/b=\1/g' >> $TEMPDIR/signature.txt
[ "$ATTRIB_NAME" == "v" ] && echo "DKIM_VERSION=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "a" ] && echo "DKIM_ALGORITHM=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "c" ] && echo "DKIM_CANONICALIZATION=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "d" ] && echo "DKIM_DOMAIN=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "s" ] && echo "DKIM_SUBDOMAIN=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "t" ] && echo "DKIM_TIMESTAMP=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "bh" ] && echo "DKIM_BODY_HASH=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "h" ] && echo "DKIM_HEADERS=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
[ "$ATTRIB_NAME" == "b" ] && echo "DKIM_BASE64_SIGNATURE=$ATTRIB_VALUE" >> $TEMPDIR/parameters.sh
fi
done
fi
done
if [ "`cat $TEMPDIR/signature.txt`" == "dkim-signature:" ]
then
echo "$PROGRAM_NAME: No DKIM-Signature found"
exit -1
fi
# echo -n "b=" >> $TEMPDIR/signature.txt
. $TEMPDIR/parameters.sh
if [ "$DKIM_VERSION" != "1" ]
then
echo "$PROGRAM_NAME: Unsupported DKIM version: $DKIM_VERSION"
exit -1
fi
BODY_HASH=0
if [ "$DKIM_ALGORITHM" == "rsa-sha256" ]
then
BODY_HASH=`cat $TEMPDIR/cbody.txt | openssl dgst -sha256 -binary | openssl base64`
elif [ "$DKIM_ALGORITHM" == "rsa-sha1" ]
then
BODY_HASH=`cat $TEMPDIR/cbody.txt | openssl dgst -sha1 -binary | openssl base64`
else
echo "$PROGRAM_NAME: Unsupported DKIM signing algorithm: $DKIM_ALGORITHM"
exit -1
fi
if [ "$DKIM_BODY_HASH" != "$BODY_HASH" ]
then
echo "$PROGRAM_NAME: DKIM body hash incorrect ($DKIM_BODY_HASH != $BODY_HASH), possibly email body has been tampered with"
# echo " DKIM body hash field: $DKIM_BODY_HASH"
# echo " Calculated body hash: $BODY_HASH"
# echo "Continuing"
exit -1
fi
# rm $TEMPDIR/body.txt
# Lookup the public key for the domain
# PUBLIC_KEY=`dig ${DKIM_SUBDOMAIN}._domainkey.${DKIM_DOMAIN} TXT | grep rsa | cut -d '"' -f 2 | cut -d '=' -f 4`
DOMAIN_KEY_DNS_RECORD=`host -t txt ${DKIM_SUBDOMAIN}._domainkey.${DKIM_DOMAIN}`
RECORD_NOT_FOUND=`echo "$DOMAIN_KEY_DNS_RECORD" | grep -c -i "not found"`
if [ "$RECORD_NOT_FOUND" != "0" ]
then
echo "$PROGRAM_NAME: DKIM domain key not found in DNS"
# echo "Continuing"
# if TESTING
exit -1
fi
# echo "Copying over file"
# rm $TEMPDIR/key.pub
# cp misc/key.pub $TEMPDIR/key.pub
# echo "Copied over file"
#else # !TESTING
# echo "DOMAIN_KEY_DNS_RECORD= -$DOMAIN_KEY_DNS_RECORD-"
# | tr -d '["\\ ]' | sed 's/.*;p=\(.*\)/\1/g' | cut -d ';' -f 1
# echo "DOMAIN_KEY_DNS_RECORD= -`echo "$DOMAIN_KEY_DNS_RECORD" | sed 's/.*[\073 \t\$]p=\(.*\)[\073 \t\$].*/\1/g' | tr -d ' \"'`-"
PUBLIC_KEY=`echo "$DOMAIN_KEY_DNS_RECORD" | tr -d '["\\ ]' | sed 's/.*;p=\(.*\)/\1/g' | cut -d ';' -f 1`
# | cut -d '"' -f 2,4 | tr -d '[" ]' | tr ';' '\n' | grep "p=" | cut -d '=' -f 2`
# TODO: Need to see if the 'test' flag is set eg: 't=y' is in the TXT of the DNS record
# echo PUBLIC_KEY=$PUBLIC_KEY
# echo " DOMAIN_KEY_DNS_RECORD = $DOMAIN_KEY_DNS_RECORD "
LAST_CHAR=`echo $PUBLIC_KEY | tail -c 2`
if [ "$LAST_CHAR" == "\\" ]
then
# echo "fixing key"
PUBLIC_KEY=`echo $PUBLIC_KEY | head -c -2`
fi
# Convert to a format that openssl can understand
echo -----BEGIN PUBLIC KEY----- > $TEMPDIR/key.pub
echo $PUBLIC_KEY | fold -w 64 >> $TEMPDIR/key.pub
echo -----END PUBLIC KEY----- >> $TEMPDIR/key.pub
# fi # End TESTING
# Dump the contents of the public key
# echo -n "PUBLIC_MODULUS=" > secret.sh
# openssl asn1parse -in key.pub -strparse 19 -offset 4 | cut -d ':' -f 4 | head -n 1 >> secret.sh
# echo -n "PUBLIC_EXPONENT=" >> secret.sh
# openssl asn1parse -in key.pub -strparse 19 -offset 4 | cut -d ':' -f 4 | tail -n 1 >> secret.sh
echo -n "" > $TEMPDIR/dkim-headers.txt
HEADER_INDEX=1
# echo HEADERS = "$DKIM_HEADERS"
while true
do
CURRENT_HEADER=`echo "$DKIM_HEADERS :" | cut -d ':' -f $HEADER_INDEX | tr -d '[ \t]' | sed 's/\(.*\)/\L\1/g'`
if [ "$CURRENT_HEADER" == "" ]
then
break
fi
# echo " header[$HEADER_INDEX] = -$CURRENT_HEADER-"
cat $TEMPDIR/cheaders.txt | while read LINE
do
HEADER_FIELD=`echo "$LINE" | cut -d ':' -f 1`
# echo "$HEADER_FIELD"
if [ "$CURRENT_HEADER" == "$HEADER_FIELD" ]
then
echo "$LINE" >> $TEMPDIR/dkim-headers.txt
# echo "$LINE"
break
fi
done
HEADER_INDEX=$((HEADER_INDEX + 1))
done
cat $TEMPDIR/signature.txt >> $TEMPDIR/dkim-headers.txt
# cat $TEMPDIR/dkim-headers.txt
LAST_CHAR=`echo $DKIM_BASE64_SIGNATURE | tail -c 2`
if [ "$LAST_CHAR" == "
" ]
then
# echo "Fixing sig"
DKIM_BASE64_SIGNATURE=`echo $DKIM_BASE64_SIGNATURE | head -c -2`
fi
# echo " DKIM_BASE64_SIGNATURE=$DKIM_BASE64_SIGNATURE"
echo "$DKIM_BASE64_SIGNATURE" | fold -w 64 | base64 -d > $TEMPDIR/sign.bin
# cat $TEMPDIR/sign.txt | base64 -d > $TEMPDIR/sign.bin
# cat $TEMPDIR/dkim-headers.txt | head -c -1 | unix2dos > $TEMPDIR/dkim-headers.dos
uniq $TEMPDIR/dkim-headers.txt | head -c -1 | unix2dos > $TEMPDIR/dkim-headers.dos
if [ "$DKIM_ALGORITHM" == "rsa-sha256" ]
then
echo -n "$PROGRAM_NAME: "
cat $TEMPDIR/dkim-headers.dos | openssl dgst -keyform pem -sha256 -verify $TEMPDIR/key.pub -signature $TEMPDIR/sign.bin
exit $?
# echo "Ret: -$?-"
# if verified ok, returns 0, else returns 1
# echo "Hash DOS line endings"
# # Convert raw hash to DER encoding by pre-pending something
# (echo '3031300d060960864801650304020105000420' ; cat $TEMPDIR/dkim-headers.dos | sha256sum) | xxd -r -p | base64
# echo "Verified Hash Method 1"
# cat $TEMPDIR/sign.bin | openssl rsautl -verify -pkcs -pubin -inkey $TEMPDIR/key.pub | base64
#
# CALCULATED_HASH=`cat $TEMPDIR/dkim-headers.dos | sha256sum | tr [a-z] [A-Z] | tr -d ' -'`
# DECRYPTED_HASH=`cat $TEMPDIR/sign.bin | openssl rsautl -verify -pkcs -pubin -inkey $TEMPDIR/key.pub | openssl asn1parse -inform der -offset 17 | cut -d ':' -f 4`
# if [ "$CALCULATED_HASH" != "$DECRYPTED_HASH" ]
# then
# echo "$PROGRAM_NAME: Failure, mismatched hashes"
# echo " CALCULATED_HASH = -$CALCULATED_HASH-"
# echo " DECRYPTED_HASH = -$DECRYPTED_HASH-"
# fi
elif [ "$DKIM_ALGORITHM" == "rsa-sha1" ]
then
echo -n "$PROGRAM_NAME: "
cat $TEMPDIR/dkim-headers.dos | openssl dgst -keyform pem -sha1 -verify $TEMPDIR/key.pub -signature $TEMPDIR/sign.bin
# echo "Ret: -$?-"
# if verified ok, returns 0, else returns 1
exit $?
else
echo "$PROGRAM_NAME: Unsupported DKIM signing algorithm: $DKIM_ALGORITHM"
exit -1
fi