├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt └── verify_dkim.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | messages-organized 3 | messages-organized.zip 4 | messages-split 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, The Associated Press 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DKIM verification script # 2 | 3 | Reporters often need to verify the authenticity of leaked emails, and one 4 | increasingly popular technique is to check those emails' [DKIM signatures][], 5 | as [ProPublica documented so well in 2017][]. 6 | 7 | The ProPublica post explains how to do this for individual messages, but for 8 | [a recent story][], The Associated Press' investigative team needed to verify 9 | many emails contained in an [mbox][] archive. 10 | 11 | [DKIM signatures]: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail 12 | [ProPublica documented so well in 2017]: https://www.propublica.org/nerds/authenticating-email-using-dkim-and-arc-or-how-we-analyzed-the-kasowitz-emails 13 | [a recent story]: https://apnews.com/d093a02a3d8a4e1b8dc7f5d19475899b 14 | [mbox]: https://en.wikipedia.org/wiki/Mbox 15 | 16 | ## Usage ## 17 | 18 | ``` 19 | $ ./verify_dkim.sh MBOX_FILE 20 | ``` 21 | 22 | This script will create an output directory called `messages-organized`, with 23 | the following subdirectories: 24 | 25 | * `messages-organized/unsigned` will contain messages that had no DKIM 26 | signature at all. 27 | 28 | * `messages-organized/signed/unverified` will contain messages that had DKIM 29 | signatures, but for some reason those signatures could not be verified. 30 | (This does not necessarily imply forgery; configurations can change over 31 | time, and some emails servers just don't behave particularly well.) 32 | 33 | * `messages-organized/signed/verified` will contain messages that had DKIM 34 | signatures that were verified as authentic. 35 | 36 | The script also will produce two other outputs: 37 | 38 | * `messages-split` will be a directory containing all of the original emails, 39 | not organized in any particular way. 40 | 41 | * `messages-organized.zip` will be a zipped archive of the 42 | `messages-organized` directory, suitable for sending via any appropriate 43 | medium. 44 | 45 | ## Other potential formats ## 46 | 47 | * If you have just one message to verify, follow the instructions in 48 | [ProPublica's 2017 post][]. 49 | 50 | * If you have a directory of many individual messages, consider editing this 51 | script to skip the `git mailsplit` call in the `INITIALIZATION` section. 52 | 53 | [ProPublica's 2017 post]: https://www.propublica.org/nerds/authenticating-email-using-dkim-and-arc-or-how-we-analyzed-the-kasowitz-emails 54 | 55 | ## Dependencies ## 56 | 57 | * [Git][] 58 | 59 | * [dkimpy][] and [dnspython][] Python packages: 60 | 61 | ``` 62 | $ pip install -r requirements.txt 63 | ``` 64 | 65 | [Git]: https://git-scm.com/ 66 | [dkimpy]: https://launchpad.net/dkimpy 67 | [dnspython]: http://www.dnspython.org/ 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dkimpy==0.8.1 2 | dnspython==1.15.0 3 | -------------------------------------------------------------------------------- /verify_dkim.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 4 | # -=-=-=-=-=-=-=-=-=-=-=-=-=- BASH CONFIGURATION =-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 5 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 6 | 7 | # Unofficial bash strict mode: 8 | # http://redsymbol.net/articles/unofficial-bash-strict-mode/ 9 | set -euo pipefail 10 | IFS=$'\n\t' 11 | 12 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 13 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= PATHS =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 14 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 15 | 16 | SPLIT_DIR='messages-split' 17 | 18 | OUTPUT_DIR='messages-organized' 19 | UNSIGNED_DIR="${OUTPUT_DIR}/unsigned" 20 | UNVERIFIED_DIR="${OUTPUT_DIR}/signed/unverified" 21 | VERIFIED_DIR="${OUTPUT_DIR}/signed/verified" 22 | 23 | INPUT_PATH="${1:-}" 24 | if [[ -z "${INPUT_PATH}" ]]; then 25 | echo "Usage: ${0} input_file" 26 | exit 1 27 | fi 28 | 29 | ZIP_PATH="${OUTPUT_DIR}.zip" 30 | 31 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 32 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=- INITIALIZATION =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 33 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 34 | 35 | # Clean out our output. 36 | if [[ -e "${SPLIT_DIR}" ]]; then 37 | rm -rf "${SPLIT_DIR}" 38 | fi 39 | if [[ -e "${OUTPUT_DIR}" ]]; then 40 | rm -rf "${OUTPUT_DIR}" 41 | fi 42 | if [[ -e "${ZIP_PATH}" ]]; then 43 | rm -rf "${ZIP_PATH}" 44 | fi 45 | 46 | # Split the input file into individual message files. 47 | mkdir -p "${SPLIT_DIR}" 48 | message_count=$( git mailsplit "-o${SPLIT_DIR}" "${INPUT_PATH}" ) 49 | echo "${message_count} messages found" 50 | 51 | # Create our output directories. 52 | mkdir -p \ 53 | "${UNSIGNED_DIR}" \ 54 | "${UNVERIFIED_DIR}" \ 55 | "${VERIFIED_DIR}" 56 | 57 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 58 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-= DKIM VERIFICATION =-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 59 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 60 | 61 | input_message_names=$( ls -1 "${SPLIT_DIR}" ) 62 | for input_message_name in ${input_message_names}; do 63 | input_message_path="${SPLIT_DIR}/${input_message_name}" 64 | 65 | # Check whether there's a signature at all. 66 | set +e 67 | sig_header=$( 68 | grep \ 69 | --extended-regexp \ 70 | --max-count 1 \ 71 | '^DKIM-Signature: ' \ 72 | "${input_message_path}" 73 | ) 74 | sig_header_exit=$? # 0 means there were matching lines 75 | set -e 76 | 77 | # If signed: 78 | if [[ "${sig_header_exit}" -eq 0 ]]; then 79 | # Attempt to verify the signature. 80 | set +e 81 | verification_result=$(dkimverify < "${input_message_path}") 82 | verification_result_exit=$? # 0 means verification succeeded 83 | set -e 84 | 85 | # If verification succeeds: 86 | if [[ "${verification_result_exit}" -eq 0 ]]; then 87 | echo "${input_message_path} is signed and verified" 88 | cp "${input_message_path}" "${VERIFIED_DIR}/${input_message_name}.eml" 89 | # If verification fails: 90 | else 91 | echo "${input_message_path} is signed, but verification failed" 92 | cp "${input_message_path}" "${UNVERIFIED_DIR}/${input_message_name}.eml" 93 | fi 94 | # If unsigned: 95 | else 96 | echo "${input_message_path} is unsigned" 97 | cp "${input_message_path}" "${UNSIGNED_DIR}/${input_message_name}.eml" 98 | fi 99 | done 100 | 101 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 102 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ZIP CREATION -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 103 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 104 | 105 | zip --recurse-paths -9 "${ZIP_PATH}" "${OUTPUT_DIR}" > /dev/null 106 | echo "Output file created at ${ZIP_PATH}" 107 | --------------------------------------------------------------------------------