#! /bin/bash -e
#
# Signs or re-signs an appcast or release notes file with a Tunnelblick version 2 signature.
#
# The arguments are the path to the private key, the path to the file to be signed, and the
# number of days the signature should be valid. An optional fourth argument specifies the date
# and time that the signature should begin being valid; if not provided the signature will
# begin being valid immediately.
#
# A file with a Tunnelblick version 2 signature comes in three parts:
#
#		An HTML comment with the signature        terminated by an LF character
#		An HTML comment with validity information terminated by an LF character
#		The contents of the file itself
#
# The signature is calculated on the validity comment concatenated with the file, so the
# signature protects the validity information and the file. The validity information consists
# of the length of the file **without** the signature line, and the dates for
# which the file is valid. (The dates are used to detect replay attacks that cause
# Tunnelblick to report that no update is available. They will only detect such attacks that
# use versions of the appcast that are outside of the validity dates, but that will prevent
# an attacker from "permanently" misleading someone.)
#
# The length is expressed as a zero-padded, seven digit number of bytes.
# The dates are expressed as YYYY-MM-DDTHH:MM:SS
#
# Example signature and validity lines:
#
#		<!-- Tunnelblick_Signature v2 MC0CFHn3dYIiqbtE632sbloBdX1Qz1euAhUApaUq32CoyfgWGlAR66vgFyJ2TgI= -->
#		<!-- 00009972 2017-01-05T00:22:09 2017-05-06T00:22:09 -->
#
#
# Copyright © 2016, 2017, 2018 by Jonathan K. Bullard. All rights reserved.

export PATH="/bin:/sbin:/usr/sbin:/usr/bin"

readonly temp_file1="/tmp/net.tunnelblick.tunnelblick-tmp1.txt"
readonly temp_file2="/tmp/net.tunnelblick.tunnelblick-tmp2.txt"

readonly name="$(  basename "$0"  )"
readonly usage_message="Usage:
    $name   private-key-path   file-path   number-of-days-signature-should-be-valid  [begin-date   [begin-time] ]
Note: begin-date should be formatted as YYYY-MM-DD; begin-time as HH:MM:SS. Both are UTC (Universal Coordinated Time)"

if [ $# -ne 3 -a $# -ne 4 -a $# -ne 5 ] ; then
	echo "Wrong number of arguments"
	echo "$usage_message"
	exit 1
fi

readonly private_key_path="$1";
readonly private_key_extension="${private_key_path:(-4)}"
if [ "${private_key_extension}" != ".pem" ] ; then
	echo "$usage_message"
	echo "The private-key is a '$private_key_extension' file. It must be a \".pem\" file"
	exit 1
fi

readonly file_path="$2"

# Make sure the private key and the file are available
if [ ! -e "$private_key_path"  ] ; then
	echo "Private key does not exist at $private_key_path"
	echo "$usage_message"
	exit 1
fi

if [ ! -e "$file_path" ]; then
	echo "File to be signed does not exist: $file_path"
	echo "$usage_message"
	exit 1
fi

# Make sure $duration_arg is a number
readonly duration_arg="$3"
if [ "" != "$(echo "$3" | sed 's/[0-9]//g' )" ] ; then
	echo "The third argument must be an integer between 1 and 36500 (100 years)"
	echo "$usage_message"
	exit 1
fi

# Process optional start date and time
if [ $# -gt 4 ] ; then
	readonly start_time="$5"
else
	readonly start_time="00:00:00"
fi
if [ $# -gt 3 ] ; then
	readonly start_date="$4"
	readonly start_date_time_yyyy_format="${start_date}T$start_time"
	readonly start_date_time_utc="$( date -j -u -f "%Y-%m-%dT%H:%M:%S" +"%m%d%H%M%Y.%S" "$start_date_time_yyyy_format" 2>&1  )"
	if [ "$start_date_time_utc" == "" \
	  -o "${start_date_time_utc/usage//}" != "$start_date_time_utc" ] ; then
		echo "Invalid date and/or time. $usage_message"
		exit 1
	fi
else
	readonly start_date_time_utc="$( date -j -u +"%m%d%H%M%Y.%S" )"
fi

# Get the contents of the file after removing existing signature and validity lines (if any)
readonly first_line="$(head -n 1 "$file_path")"
readonly first_34_chars="${first_line:0:34}"
if [ "$first_34_chars" == "<!-- Tunnelblick DSA Signature v1 " ] ; then
	echo "$(tail -n +2 "$file_path")" > "$temp_file1"
else
	readonly first_30_chars="${first_line:0:30}"
	if [ "$first_30_chars" == "<!-- Tunnelblick_Signature v2 " ] ; then
		echo "$(tail -n +3 "$file_path")" > "$temp_file1"
	else
		cat "$file_path" > "$temp_file1"
	fi
fi

# Make signature valid starting now and ending $duration_arg days from now
readonly valid_start="$( date -j                      +"%Y-%m-%dT%H:%M:%S" "$start_date_time_utc"  )"
readonly valid_end="$(   date -j -v +${duration_arg}d +"%Y-%m-%dT%H:%M:%S" "$start_date_time_utc"  )"

# Set the length of the file (without the signature or validity lines)
readonly file_length="$(  stat -f %z "$temp_file1"  )"
if [ -n "$(  printf '%s' "$file_length" | sed 's/[0-9]//g'  )" ]; then
	echo "An error occurred; the length was not a number for file $temp_file1"
	exit 1
fi
if [ "$file_length" -gt 9999999 ] ; then
	echo "File length $file_length is too large! It must be smaller than 10,000,000 bytes: $temp_file1"
	exit 1
fi

readonly file_length_7_digits="$(  printf %07d "$file_length"  )"

readonly validity_comment="<!-- $file_length_7_digits $valid_start $valid_end -->"

echo "$validity_comment" > "$temp_file2"
cat  "$temp_file1"  >> "$temp_file2"

# Get the new signature of (the file prepended with the validity line)
# NOTE: The OpenSSL dss1 digest algorithm usually returns something which encodes into 64 base64 bytes, but about
#       1/256 of the time it returns something that encodes into 60 bytes. We always want 64 bytes digests so they
#       fit into the fixed size Tunnelblick_Signature v2 format, so we discard these short signatures.
new_sig=""
while [ ${#new_sig} -ne 64 ] ; do
    new_sig="$(  openssl dgst -sha1 -binary < "$temp_file2" | openssl dgst -dss1 -sign "$private_key_path" | openssl enc -base64 )"
done
readonly new_sig


# Replace the input file: First line of the new contents is the new signature comment
readonly new_line="<!-- Tunnelblick_Signature v2 $new_sig -->"
echo "$new_line" > "$file_path"

# Rest of new contents is the validity line followed by the original file (stripped of signature and validity lines if it had any)
cat "$temp_file2" >> "$file_path"

# Remove temporary files
rm -f "$temp_file1"
rm -f "$temp_file2"
