appfs-cert at tip

File appfs-cert from the latest check-in


#! /usr/bin/env bash

#
# Copyright (c) 2014  Roy Keene
#
# 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.
#

PATH="${PATH}:$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

appfsd_options=()

CA_CERT_FILE='AppFS_CA.crt'
CA_KEY_FILE='AppFS_CA.key'
export CA_CERT_FILE CA_KEY_FILE

function call_appfsd() {
	appfsd "${appfsd_options[@]}" "$@"
}

function read_password() {
	local prompt variable

	prompt="$1"
	variable="$2"

	if [ -z "$(eval echo '$'${variable})" ]; then
		echo -n "${prompt}" >&2

		stty -echo
		IFS='' read -r $variable
		stty echo
		echo '' >&2
	fi
}

function read_text() {
	local prompt variable

	prompt="$1"
	variable="$2"

	if [ -z "$(eval echo '$'${variable})" ]; then
		echo -n "${prompt}" >&2

		IFS='' read -r $variable
	fi
}

function generate_ca_cert_and_key() {
	read_text 'Certificate Authority (CA) Company Name (O): ' CA_DN_S_O
	read_text 'Certificate Authority (CA) Responsible Party Name (CN): ' CA_DN_S_CN
	read_password 'Password for Certificate Authority Key: ' CA_PASSWORD

	export CA_DN_S_O CA_DN_S_CN CA_PASSWORD

	call_appfsd --tcl '
package require pki

set filename_cert $::env(CA_CERT_FILE)
set filename_key  $::env(CA_KEY_FILE)

if {[file exists $filename_key]} {
	set replace_key false

	set key [pki::pkcs::parse_key [read [open $filename_key]] $env(CA_PASSWORD)]
} else {
	set replace_key true

	puts -nonewline "Generating RSA Key..."
	flush stdout
	set key [pki::rsa::generate 2048]
	puts " Done."
}

lappend key subject "O=$::env(CA_DN_S_O),CN=$::env(CA_DN_S_CN)"

set ca [pki::x509::create_cert $key $key 1 [clock seconds] [clock add [clock seconds] 15 years] 1 [list] 1]

puts "Writing \"$filename_cert\""
set fd [open $filename_cert w 0644]
puts $fd $ca
close $fd

if {$replace_key} {
	puts "Writing \"$filename_key\""
	set fd [open $filename_key w 0400]
	puts $fd [pki::key $key $::env(CA_PASSWORD)]
	close $fd
}
'
}

function generate_key() {
	read_password 'Password for Site Key being generated: ' SITE_PASSWORD

	export SITE_PASSWORD

	call_appfsd --tcl '
package require pki

if {[info exists ::env(SITE_KEY_FILE)]} {
	set filename_key $::env(SITE_KEY_FILE)
} else {
	set filename_key "AppFS_Site.key"
}

puts -nonewline "Generating RSA Key..."
flush stdout
set key [pki::rsa::generate 2048]
puts " Done."

puts "Writing \"$filename_key\""
set fd [open $filename_key w 0400]
puts $fd [pki::key $key $::env(SITE_PASSWORD)]
close $fd
'
}

function generate_csr() {
	read_text 'Site hostname: ' SITE_HOSTNAME

	if [ -z "${SITE_KEY_FILE}" ]; then
		SITE_KEY_FILE="AppFS_Site_${SITE_HOSTNAME}.key"
	fi

	export SITE_HOSTNAME SITE_KEY_FILE

	if [ -f "${SITE_KEY_FILE}" ]; then
		echo 'Key file already exists.'
		if cat "${SITE_KEY_FILE}" | grep -i '^Proc-Type: .*,ENCRYPTED' >/dev/null; then
			read_password 'Password for (existing) Site Key: ' SITE_PASSWORD
		else
			SITE_PASSWORD=""
		fi

		export SITE_PASSWORD
	else
		generate_key
	fi

call_appfsd --tcl '
package require pki

if {[info exists ::env(SITE_KEY_FILE)]} {
        set filename_key $::env(SITE_KEY_FILE)
} else {
        set filename_key "AppFS_Site.key"
}
set filename_csr "[file rootname $filename_key].csr"

set key [read [open $filename_key]]

set key [::pki::pkcs::parse_key $key $::env(SITE_PASSWORD)]

set csr [::pki::pkcs::create_csr $key [list CN $::env(SITE_HOSTNAME)] 1]

puts "Writing \"$filename_csr\""
set fd [open $filename_csr w 0644]
puts $fd $csr
close $fd
'
}

function generate_cert() {
	SITE_CSR_FILE="$1"

	read_text 'Certificate Signing Request (CSR) file: ' SITE_CSR_FILE

	if [ -z "${SITE_CSR_FILE}" ]; then
		generate_csr || exit 1

		SITE_CSR_FILE="$(echo "${SITE_KEY_FILE}" | sed 's@.[^\.]*$@@').csr"
	fi

	if [ ! -e "${CA_CERT_FILE}" -o ! -e "${CA_KEY_FILE}" ]; then
		read_text 'Certificate Authority (CA) Certificate Filename: ' CA_CERT_FILE
		read_text 'Certificate Authority (CA) Key Filename: ' CA_KEY_FILE
	fi

	if cat "${CA_KEY_FILE}" | grep -i '^Proc-Type: .*,ENCRYPTED' >/dev/null; then
		read_password 'Certificate Authority (CA) Password: ' CA_PASSWORD
	fi

	SITE_SERIAL_NUMBER="$(uuidgen | dd conv=ucase 2>/dev/null | sed 's@-@@g;s@^@ibase=16; @' | bc -lq)"

	export SITE_CSR_FILE SITE_SERIAL_NUMBER CA_CERT_FILE CA_KEY_FILE CA_PASSWORD

	SITE_CERT="$(call_appfsd --tcl '
package require pki

set csr [read [open $::env(SITE_CSR_FILE)]]
set csr [::pki::pkcs::parse_csr $csr]

set ca_key [read [open $::env(CA_KEY_FILE)]]
set ca_cert [read [open $::env(CA_CERT_FILE)]]

set ca_key [::pki::pkcs::parse_key $ca_key $::env(CA_PASSWORD)]
set ca_cert [::pki::x509::parse_cert $ca_cert]
set ca_key [concat $ca_key $ca_cert]

set cert [::pki::x509::create_cert $csr $ca_key $::env(SITE_SERIAL_NUMBER) [clock seconds] [clock add [clock seconds] 1 year] 0 [list] 1]

puts $cert
')"

	SITE_SUBJECT="$(echo "${SITE_CERT}" | openssl x509 -subject -noout | sed 's@.*= @@')"

	echo "${USER}@${HOSTNAME} $(date): ${SITE_SERIAL_NUMBER} ${SITE_SUBJECT}" >> "${CA_KEY_FILE}.issued"

	echo "${SITE_CERT}" | (
		if [ -z "${SITE_HOSTNAME}" ]; then
			cat
		else
			tee "AppFS_Site_${SITE_HOSTNAME}.crt"
		fi
	)
}

function generate_selfsigned() {
	read_password 'Password for Key: ' SITE_PASSWORD
	read_text 'Site hostname: ' SITE_HOSTNAME

	SITE_SERIAL_NUMBER="$(uuidgen | dd conv=ucase 2>/dev/null | sed 's@-@@g;s@^@ibase=16; @' | bc -lq)"

	export SITE_PASSWORD SITE_HOSTNAME SITE_SERIAL_NUMBER

	call_appfsd --tcl '
package require pki

set filename_cert "AppFS_Site_$::env(SITE_HOSTNAME).crt"
set filename_key  "AppFS_Site_$::env(SITE_HOSTNAME).key"

puts -nonewline "Generating RSA Key..."
flush stdout
set key [pki::rsa::generate 2048]
puts " Done."

lappend key subject "CN=$::env(SITE_HOSTNAME)"

set cert [pki::x509::create_cert $key $key $::env(SITE_SERIAL_NUMBER) [clock seconds] [clock add [clock seconds] 1 years] 1 [list] 1]

puts "Writing \"$filename_cert\""
set fd [open $filename_cert w 0644]
puts $fd $cert
close $fd

puts "Writing \"$filename_key\""
set fd [open $filename_key w 0400]
puts $fd [pki::key $key $::env(SITE_PASSWORD)]
close $fd
'
}

function sign_site() {
	SITE_INDEX_FILE="$1"
	SITE_KEY_FILE="$2"
	SITE_CERT_FILE="$3"

	read_text 'AppFS Site Index file: ' SITE_INDEX_FILE
	read_text 'Site Key filename: ' SITE_KEY_FILE
	read_text 'Site Certificate filename: ' SITE_CERT_FILE

	if cat "${SITE_KEY_FILE}" | grep -i '^Proc-Type: .*,ENCRYPTED' >/dev/null; then
		read_password "Password for Key (${SITE_KEY_FILE}): " SITE_PASSWORD
	else
		SITE_PASSWORD=""
	fi

	export SITE_INDEX_FILE SITE_KEY_FILE SITE_CERT_FILE SITE_PASSWORD

	call_appfsd --tcl "$(cat <<\_EOF_
package require pki

set fd [open $::env(SITE_INDEX_FILE)]
gets $fd line
close $fd

set line [split $line ","]

# Data to be signed
set data [join [lrange $line 0 1] ","]

set key [read [open $::env(SITE_KEY_FILE)]]
set key [::pki::pkcs::parse_key $key $::env(SITE_PASSWORD)]

set cert [read [open $::env(SITE_CERT_FILE)]]
array set cert_arr [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]
binary scan $cert_arr(data) H* cert

set signature [::pki::sign $data $key]
binary scan $signature H* signature

set data [split $data ","]
lappend data $cert
lappend data $signature

set data [join $data ","]

if {![info exists ::env(APPFS_SIGN_IN_PLACE)]} {
	set fd [open "$::env(SITE_INDEX_FILE).new" "w" 0644]
	puts $fd $data
	close $fd

	file rename -force -- "$::env(SITE_INDEX_FILE).new" $::env(SITE_INDEX_FILE)
} else {
	set fd [open "$::env(SITE_INDEX_FILE)" "w" 0644]
	puts $fd $data
	close $fd
}

_EOF_
)"
}

cmd="$1"
shift
case "${cmd}" in
	generate-ca)
		generate_ca_cert_and_key "$@" || exit 1
		;;
	generate-key)
		# Hidden, users should use "generate-csr" instead
		generate_key "$@" || exit 1
		;;
	generate-csr)
		generate_csr "$@" || exit 1
		;;
	sign-csr|generate-cert)
		generate_cert "$@" || exit 1
		;;
	generate-selfsigned)
		generate_selfsigned "$@" || exit 1
		;;
	sign-site)
		sign_site "$@" || exit 1
		;;
	*)
		echo 'Usage: appfs-cert {generate-selfsigned|generate-ca|generate-csr|sign-csr|generate-cert|sign-site}' >&2

		exit 1
		;;
esac

exit 0