blob: b602e5ab7fa3881faf3ea6f029604f6265c83347 [file] [log] [blame]
#!/bin/sh
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This script is temporary front-end to entd. It validates the policy's
# signature before starting the daemon. If the signing certificate or
# signature to not validate, then this script will log an error to syslog
# and exit without starting entd.
#
if [ -z "$HOME" ]; then
HOME=/home/chronos/user
fi
SCRIPT="$(basename $0)"
SIGNING_CERT="signing-cert.pem"
SIGNED_DIGEST="signed-digest.sha1"
ROOT_CA_FILE="root-ca.pem"
REQUIRED_TO_SIGN="isa-cros-policy manifest.json policy.js $SIGNING_CERT"
REQUIRED_TO_START="$REQUIRED_FOR_SIGN $SIGNED_DIGEST"
APPROVED_CA="approved-ca.pem"
CANDIDATE_CA="candidate-ca.pem"
ALLOW_CHAINING_FILE="chaining-allowed"
. "/usr/lib/shflags" || (echo "error loading shflags"; exit 1)
DEFINE_string command "" \
"Command to run; One of: sign, verify, or start" "c"
DEFINE_boolean verify_signature $FLAGS_TRUE \
"Verify the authenticity of the signature" "v"
DEFINE_boolean allow_self_signed $FLAGS_FALSE \
"Allow self signed certificates" "S"
DEFINE_string extension "" \
"Location of the enterprise extension" "x"
DEFINE_string extension_path "$HOME/Extensions" \
"Location to search for extensions when --extension is not provided" "p"
DEFINE_string enterprise_ca "" \
"URL of the enterprise root Certificate Authority." "C"
DEFINE_string signing_key "" \
"Location of the private key to use when signing." "K"
DEFINE_string crx_key "" \
"Location of the private key to use when creating the crx file." "k"
DEFINE_string entd "/usr/sbin/entd" \
"Path to the entd binary" "d"
DEFINE_string utility "/etc/entd/base-policy.js" \
"Path to policy utility file" "t"
DEFINE_string username "$CHROMEOS_USER" \
"ChromeOS Username (full email address)." "u"
DEFINE_string output "$(pwd)" \
"Destination directory or filename for crx files" "o"
DEFINE_string user_var "$HOME/var/entd/" \
"Directory to store user specific state" "V"
DEFINE_boolean allow_chaining $FLAGS_FALSE \
"Allow the extensions to be signed by a cert which chains to the approved \
cert (this option only matters when approving a cert)."
FLAGS "$@" || exit $?
eval set -- "${FLAGS_ARGV}"
main() {
local extension="$FLAGS_extension"
if [ -z "$FLAGS_command" ]; then
log "Missing option: --command"
exit 1
fi
if [ "$FLAGS_command" = "disapprove" ]; then
# Disable an approved enterprise ca.
cmd_disapprove
return $?
fi
if [ "$FLAGS_command" = "approve" ]; then
# Approve an enterprise ca.
cmd_approve "$FLAGS_enterprise_ca"
return $?
fi
log "Username: ${FLAGS_username}"
if [ -z "$extension" ]; then
extension="$(find_extension "$FLAGS_extension_path")"
fi
if [ -z "$extension" ]; then
return 1
fi
log "Enterprise extension: $extension"
local result
if [ "$FLAGS_command" = "makecrx" ]; then
# Sign an extension.
cmd_makecrx "$extension" "$FLAGS_crx_key" "$FLAGS_signing_key"
result=$?
elif [ "$FLAGS_command" = "verify" ]; then
# Verify a signature without starting entd.
cmd_verify "$extension"
result=$?
if [ "$result" = "0" ]; then
log "Extension verified: $extension"
fi
elif [ "$FLAGS_command" = "start" ]; then
# Verify the signature, then start entd.
cmd_start "$extension"
result=$?
else
log "Unknown command: $FLAGS_command"
result=1
fi
return $result
}
cmd_makecrx() {
local extension="$1"
local crx_key="$2"
local signing_key="$3"
if [ ! -d "$extension" ]; then
log "Missing or invalid extension: $extension"
return 1
fi
if [ ! -f "$crx_key" ]; then
log "Missing or invalid crx key: $crx_key"
return 1
fi
if [ ! -f "$signing_key" ]; then
log "Missing or invalid signing key: $signing_key"
return 1
fi
local chrome="$(which google-chrome)"
if [ ! -x "$chrome" ]; then
log "Unable to locate google-chrome executable"
return 1
fi
local workdir="$(mktemp -d)"
local signed="$workdir/signed"
mkdir -p "$signed"
cp -rL $extension/* "$signed"
rm $(find "$signed" -name '*~')
if ! cmd_sign "$signed" "$signing_key"; then
log "Error signing"
return 1
fi
local userdata="$workdir/userdata"
mkdir -p "$userdata"
"$chrome" --pack-extension="$signed" \
--pack-extension-key="$crx_key" \
--user-data-dir="$userdata" --no-message-box --no-first-run
if [ ! -f "$workdir/signed.crx" ]; then
log "Error creating crx"
return 1
fi
local output="$FLAGS_output"
if [ -d "$output" ]; then
local cn="$(get_cn "$extension")"
local version="$(get_version "$extension")"
output="$output/$cn-policy-$version.crx"
fi
mv "$workdir/signed.crx" "$output"
chmod a+r "$output"
log "CRX file written to: $output"
rm -rf "$workdir"
}
cmd_sign() {
local extension="$1"
local private_key="$2"
log "Signing: $extension"
if [ -z "$private_key" ]; then
log "Missing private key"
return 1
fi
if ! verify_files "$extension" "$REQUIRED_TO_SIGN"; then
log "Invalid extension: $extension"
return 1
fi
local catfile="$(make_catfile "$extension")"
if [ -z "$catfile" ]; then
return 1
fi
local output
output="$(openssl dgst -sha1 -sign "$private_key" \
-out "$extension/$SIGNED_DIGEST" "$catfile" 2>&1)"
local result=$?
rm "$catfile"
# If openssl exited with code 0 and the signature file is not zero length,
# then were probably successful.
if [ $result = 0 -a -s "$extension/$SIGNED_DIGEST" ]; then
cmd_verify "$extension"
return $?
fi
log "Error signing extension: $output"
return 1
}
cmd_verify() {
local extension="$1"
if ! (verify_files "$extension" "$REQUIRED_TO_START"); then
log "Invalid extension: $extension"
return 1
fi
if [ "$FLAGS_verify_signature" = "$FLAGS_TRUE" ]; then
if ! verify_cert "$extension"; then
return 1
fi
if ! verify_signature "$extension"; then
return 1
fi
else
log "WARNING: Not verifying extension certificate"
fi
}
cmd_start() {
local extension="$1"
if [ -z "$FLAGS_username" ]; then
log "Can't start enterprise daemon, no username provided."
return 1
fi
local domain="$(echo "$FLAGS_username" | cut -f2 -d'@')"
if [ "$domain" = "gmail.com" ]; then
log "Enterprise daemon disabled for gmail.com users."
return 1
fi
local session_path="$extension/session-id.json"
# Remove session-id file as it should be ignored when verifying
# the signature.
log "removing $session_path"
rm -f "$session_path"
if ! cmd_verify "$extension"; then
return 1
fi
# Remove session-id.json at exit to enable the extension to remain
# backward compatible with earlier versions of entd/entwife.
trap "rm -f $session_path" EXIT TERM
# Provide a way for a developer to disable session id to simplify
# iterating new extensions.
local local_session_id=""
if [ ! -r "/root/.disable-entd-session-id" ]; then
session_id=$(head -c 8 /dev/urandom | openssl md5)
fi
cat > "$session_path" <<EOF
{
"session_id": "$session_id"
}
EOF
local root_ca_option=""
if [ -f "$extension/$ROOT_CA_FILE" ]; then
root_ca_option="--root-ca-file=$extension/$ROOT_CA_FILE"
fi
local extid="$(basename $(dirname "$extension"))"
# Run entd in the background and wait on it - this allows the
# shell interpreter to catch TERM signal and clean up session_path.
"$FLAGS_entd" --utility="$FLAGS_utility" "$root_ca_option" \
--policy="$extension/policy.js" --manifest="$extension/manifest.json" \
--username="$FLAGS_username" --callback-origin=chrome-extension://"$extid" \
--session-id="$session_id" &
local pid=$!
wait $pid
}
cmd_disapprove() {
if [ -f "$FLAGS_user_var/$APPROVED_CA" ]; then
log "Removing enterprise certificate authority"
rm -f "$FLAGS_user_var/$APPROVED_CA"
else
log "No enterprise certificate authority has been approved."
fi
}
cmd_approve() {
local ca_url="$1"
if [ -z "$ca_url" ]; then
log "Missing CA url"
exit 1
fi
if [ "$(echo "$ca_url" | cut -c1-7)" != "http://" -a \
"$(echo "$ca_url" | cut -c1-8)" != "https://" ]; then
log "Invalid CA url, must begin with http:// or https://"
return 1
fi
mkdir -p "$FLAGS_user_var"
rm -f "$FLAGS_user_var/$CANDIDATE_CA"
curl -sS "$ca_url" -o "$FLAGS_user_var/$CANDIDATE_CA"
if [ ! -s "$FLAGS_user_var/$CANDIDATE_CA" ]; then
log "Error fetching candidate CA file."
return 1
fi
if ! grep -q "BEGIN CERTIFICATE" "$FLAGS_user_var/$CANDIDATE_CA"; then
log "Not a PEM encoded certificate."
return 1
fi
local output="$(openssl verify "$FLAGS_user_var/$CANDIDATE_CA")"
if [ "$FLAGS_allow_self_signed" = "$FLAGS_TRUE" ]; then
local stripped_output="$(echo $output | grep -v "self signed certificate")"
if [ "$output" != "$stripped_output" ]; then
log
log "WARNING: This is a self signed certificate."
log
fi
output="$stripped_output"
fi
if [ ! -z "$(echo "$output" | grep "error")" ]; then
# `openssl verify` exits with a success code even if verification fails,
# so we have to grep the output to determine the result of verification.
log "Unable to verify certificate authority: $output"
return 1
fi
log "This certificate authority has the subject:"
log "$(get_subject "$FLAGS_user_var/$CANDIDATE_CA")"
log
read -p ">>> If you trust this certificate authority, type 'I DO': " I_DO_
if [ "$I_DO_" != "I DO" ]; then
log "Canceling CA approval."
return 1
fi
rm -f "$FLAGS_user_var/$APPROVED_CA"
mv "$FLAGS_user_var/$CANDIDATE_CA" "$FLAGS_user_var/$APPROVED_CA"
chmod 440 "$FLAGS_user_var/$APPROVED_CA"
if [ "$FLAGS_allow_chaining" = "$FLAGS_TRUE" ]; then
log "Enabling chaining for this CA."
touch "$FLAGS_user_var/$ALLOW_CHAINING_FILE"
chmod 440 "$FLAGS_user_var/$ALLOW_CHAINING_FILE"
else
rm -f "$FLAGS_user_var/$ALLOW_CHAINING_FILE"
fi
log "CA approval complete."
}
log() {
if [ -t 2 ]; then
echo "---" "$@" >&2
else
if [ -z "$@" ]; then
# Don't log blank lines to the system log, they're only useful on ttys.
return
fi
logger -t "$SCRIPT" -- "$@"
fi
}
abspath() {
if [ -d "$1" ]; then
echo "$(cd $1; pwd)"
else
echo "$(cd $(dirname "$1"); pwd)/$(basename $1)"
fi
}
get_cn() {
local extension="$1"
# Extract the CN from the certificate.
openssl x509 -in "$extension/$SIGNING_CERT" -noout -subject | \
sed 's:.*/CN=\([^/]\+\).*:\1:'
}
get_subject() {
local cert="$1"
# Extract the CN from the certificate.
openssl x509 -in "$cert" -noout -subject | sed 's:^subject=\s*\(.*\):\1:'
}
get_version() {
local extension="$1"
cat "$extension/manifest.json" | sed -n 's/\s*"version":\s*"\([^"]\+\).*/\1/p'
}
# Search through the given path for one and only one enterprise extension.
find_extension() {
local path="$(readlink -f "$1")"
local depth="$2"
if [ -z "$depth" ]; then
depth=3
fi
if [ ! -d "$path" ]; then
log "Extension path is not a directory: $path"
return
fi
local candidates="$(find -L "$path" -mindepth $depth -maxdepth $depth \
-name isa-cros-policy)"
if [ -z "$candidates" ]; then
log "No enterprise policy extension found in $path";
return
fi
if [ "$(echo "$candidates" | wc -w)" != "1" ]; then
log "Multiple enterprise policies found in $path: $candidates";
return
fi
dirname "$candidates"
}
# Verifies that the extension contains a list of files. If the list is not
# specified, assume they mean the list of files require to start entd with
# this extension.
verify_files() {
local extension="$1"
local required_files="$2"
if [ -z "$required_files" ]; then
required_files="$REQUIRED_TO_START"
fi
local err=0
for file in $required_files; do
if [ ! -f "$extension/$file" ]; then
log "Missing $extension/$file";
err=1
fi
done
return $err
}
# The "catfile" is the concatenation of all of the files from the extension
# into a single file.
#
# We locate all regular files in the extension, excluding any previous
# signature file, sort them in ascending order. Then for each file, we
# append the filename and contents to the catfile.
#
# The hash of this file is what is actually signed.
#
make_catfile() {
local extension="$1"
local normalized_manifest="$(normalize_manifest "$extension")"
if [ -z "$normalized_manifest" ]; then
return 1
fi
cd "$extension" > /dev/null
local filenames="$(find . -type f -not -wholename "./$SIGNED_DIGEST" | sort)"
cd - > /dev/null
if [ -z "$filenames" ]; then
log "Error enumerating files"
return 1
fi
local catfile="$(mktemp)"
for filename in $filenames; do
echo $filename >> "$catfile"
if [ "$filename" = "./manifest.json" ]; then
echo "$normalized_manifest" >> "$catfile"
else
cat "$extension/$filename" >> "$catfile"
fi
done
echo "$catfile"
}
# Verify that a certificate is valid.
verify_cert() {
local extension="$1"
if [ ! -f "$FLAGS_user_var/$APPROVED_CA" ]; then
log "Cannot verify cert: No approved enterprise certificate authority."
return 1
fi
if [ -f "$FLAGS_user_var/$ALLOW_CHAINING_FILE" ]; then
log "Checking certificate chain."
local output="$(openssl verify -CAfile "$FLAGS_user_var/$APPROVED_CA" \
"$extension/$SIGNING_CERT" 2>&1)"
if [ ! -z "$(echo "$output" | grep -i "error")" ]; then
# `openssl verify` exits with a success code even if verification fails,
# so we have to grep the output to determine the result of verification.
log "Unable to verify certificate: $output"
return 1
fi
else
log "Checking for exact certificate match."
if [ "$(cat "$FLAGS_user_var/$APPROVED_CA")" != \
"$(cat "$extension/$SIGNING_CERT")" ]; then
log "Approved cert does not match signing cert."
return 1
fi
fi
}
# Verify the signature of a policy.
verify_signature() {
local extension="$1"
# This file contains the signature of the extension.
local sigfile="$extension/$SIGNED_DIGEST"
local catfile="$(make_catfile "$extension")"
if [ -z "$catfile" ]; then
return 1
fi
# Extract the public key from the certificate.
local keyfile="$(mktemp)"
openssl x509 -in "$extension/$SIGNING_CERT" -pubkey -noout > "$keyfile"
local result=$?
if [ "$result" != "0" ]; then
log "Error extracting public key from certificate"
rm "$catfile"
return $result
fi
if [ ! -f "$keyfile" ]; then
log "Unknown error extracting public key from certificate"
return 1
fi
# Declare $output before assigning, so we don't lose the return code of
# the openssl command.
local output
# Verify the signature.
output="$(openssl dgst -sha1 -verify "$keyfile" \
-signature "$extension/$SIGNED_DIGEST" "$catfile" 2>&1)"
result=$?
# Clean up the temporary files.
rm "$catfile" "$keyfile"
if [ "$result" != "0" ]; then
log "Signature did not validate: $output"
else
log "Signature is valid"
fi
return $result
}
# Chrome takes liberties with the manifest file when it installs an extension.
# It deserializes it, adds the "key" if it is missing, and then re-serializes
# it. There is no guaranteed ordering for the keys when the file is finally
# written to disk. This royally screws up our recalculation of the signature.
#
# So, instead of signing the actual manifest source file, we pass it through
# a normalization procedure where we impose a guaranteed ordering on the keys,
# and remove the extra key (called "key") that Chrome adds during installation.
#
# For example, given { "name": "Foo", "browser": { "hello": "world" } }, we
# return:
#
# manifest.browser.hello = "world"
# manifest.name = "Foo"
#
# If we can, we use a small Python script to do the normalization. If Python
# isn't available (say, on the device) then we use entd itself.
#
#
# TODO(rginda): Do better.
normalize_manifest() {
local python="$(which python)"
if [ -x "$python" ]; then
normalize_manifest_py "$@"
elif [ -x "$FLAGS_entd" ]; then
normalize_manifest_js "$@"
else
log "Can't find python or entd, unable to normalize extension manifest"
return 1
fi
}
normalize_manifest_py() {
local extension="$1"
local scriptfile="$(mktemp)"
cat <<EOF > "$scriptfile"
import sys
try:
import json
except ImportError:
json = None
if not json or not hasattr(json, 'loads'):
# The contemporary version of the json module wasn't included until
# Python 2.6. If you want to build a policy extension on an earlier version,
# you need to install the simplejson module.
import simplejson
json = simplejson
def to_str(name, value, ary = []):
if type(value) == list:
ary.append(name + ": []");
for i in xrange(len(value)):
to_str(name + "." + str(i), value[i], ary);
elif type(value) == dict:
ary.append(name + ": {}");
for key in value.keys():
to_str(name + "." + key, value[key], ary);
else:
ary.append(name + ": " + json.dumps(value));
return ary
if __name__ == '__main__':
fp = open(sys.argv[1])
d = json.load(fp)
# Remove the "key" possibly added by Chrome.
if 'key' in d:
del d['key']
ary = to_str('manifest', d)
ary.sort()
ary.reverse()
print '\n'.join(ary)
EOF
python "$scriptfile" "$extension/manifest.json"
rm "$scriptfile"
}
normalize_manifest_js() {
local extension="$1"
local scriptfile="$(mktemp)"
cat <<EOF > "$scriptfile"
entd.onLoad = function(m) {
// Remove the "key" possibly added by Chrome.
delete m.key;
var ary = str("manifest", m);
// Sort the results so we can determine an ordering.
println(ary.sort().reverse().join("\n"));
}
function str(name, value, ary) {
ary = ary || [];
if (value instanceof Array) {
ary.push(name + ": []");
for (var i = 0; i < value.length; ++i)
str(name + "." + i, value[i], ary);
} else if (value instanceof Object) {
ary.push(name + ": {}");
for (var key in value)
str(name + "." + key, value[key], ary);
} else {
ary.push(name + ": " + JSON.stringify(value));
}
return ary
}
EOF
"$FLAGS_entd" --policy="$scriptfile" --manifest="$extension/manifest.json" \
--username=user@example.com --allow-dirty-exit 2>/dev/null
rm "$scriptfile"
}
main "$@"