diff --git a/security/apps/Makefile.in b/security/apps/Makefile.in index a9e23da1931..9d65c6d7785 100644 --- a/security/apps/Makefile.in +++ b/security/apps/Makefile.in @@ -4,6 +4,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. GEN_CERT_HEADER = $(srcdir)/gen_cert_header.py +TEST_SSL_PATH = $(srcdir)/../manager/ssl/tests/unit/test_signed_manifest/ marketplace-prod-public.inc: marketplace-prod-public.crt $(GEN_CERT_HEADER) $(PYTHON) $(GEN_CERT_HEADER) marketplaceProdPublicRoot $< > $@ @@ -20,6 +21,18 @@ marketplace-dev-reviewers.inc: marketplace-dev-reviewers.crt $(GEN_CERT_HEADER) marketplace-stage.inc: marketplace-stage.crt $(GEN_CERT_HEADER) $(PYTHON) $(GEN_CERT_HEADER) marketplaceStageRoot $< > $@ +ifeq ($(shell test -s trusted-app-public.der; echo $$?),0) +TRUSTED_APP_PUBLIC=trusted-app-public.der +else +TRUSTED_APP_PUBLIC= +endif + +manifest-signing-root.inc: $(TRUSTED_APP_PUBLIC) $(GEN_CERT_HEADER) + $(PYTHON) $(GEN_CERT_HEADER) trustedAppPublicRoot $(TRUSTED_APP_PUBLIC) > $@ + +manifest-signing-test-root.inc: $(TEST_SSL_PATH)trusted_ca1.der $(GEN_CERT_HEADER) + $(PYTHON) $(GEN_CERT_HEADER) trustedAppTestRoot $< > $@ + xpcshell.inc: $(srcdir)/../manager/ssl/tests/unit/test_signed_apps/trusted_ca1.der $(GEN_CERT_HEADER) $(PYTHON) $(GEN_CERT_HEADER) xpcshellRoot $< > $@ @@ -29,5 +42,7 @@ export:: \ marketplace-dev-public.inc \ marketplace-dev-reviewers.inc \ marketplace-stage.inc \ + manifest-signing-root.inc \ + manifest-signing-test-root.inc \ xpcshell.inc \ $(NULL) diff --git a/security/apps/gen_cert_header.py b/security/apps/gen_cert_header.py index 58534736ed5..f77e5ccf1a3 100644 --- a/security/apps/gen_cert_header.py +++ b/security/apps/gen_cert_header.py @@ -22,8 +22,18 @@ def create_header(array_name, in_filename): print "};" return 0 +def create_empty_header(array_name): + # mfbt/ArrayUtils.h will not be able to pick up the + # correct specialization for ArrayLength(const array[0]) + # so add a value of 0 which will fail cert verification + # just the same as an empty array + print "const uint8_t " + array_name + "[] = { 0x0 };" + return 0 + if __name__ == '__main__': - if len(sys.argv) < 3: + if len(sys.argv) < 2: print 'ERROR: usage: gen_cert_header.py array_name in_filename' sys.exit(1); + if len(sys.argv) == 2: + sys.exit(create_empty_header(sys.argv[1])) sys.exit(create_header(sys.argv[1], sys.argv[2])) diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/README.md b/security/manager/ssl/tests/unit/test_signed_manifest/README.md new file mode 100644 index 00000000000..a442c705280 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/README.md @@ -0,0 +1,17 @@ +This folder contains the scripts needed to generate signed manifest files +to verify the Trusted Hosted Apps concept. + +Prerequisites: + +* NSS 3.4 or higher. +* Python 2.7 (should work with 2.6 also) +* Bash +* OpenSSL + +Usage: + +Run + I) For usage info execute ./create_test_files.sh --help + + II) Upload the signed manifest.webapp and manifest.sig to the + application hosting server. diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh b/security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh new file mode 100755 index 00000000000..cddbe17ec1e --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/create_test_files.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# +# Mode: shell-script; sh-indentation:2; +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +export BASE_PATH=`dirname $0` +echo $BASE_PATH + +# location of the 'sign_b2g_manifest.py' script +export SIGN_SCR_PATH=. + +DB_PATH=${BASE_PATH}/signingDB +PASSWORD_FILE=${DB_PATH}/passwordfile +VALID_MANIFEST_PATH=${BASE_PATH}/testValidSignedManifest +INVALID_MANIFEST_PATH=${BASE_PATH}/testInvalidSignedManifest + +TRUSTED_EE=trusted_ee1 +UNTRUSTED_EE=untrusted_ee1 +TRUSTED_CA=trusted_ca1 +UNTRUSTED_CA=untrusted_ca1 + +# Print usage info +usage() { + echo + echo + tput bold + echo "NAME" + tput sgr0 + echo " create_test_files.sh - Signing a manifest for Trusted Hosted Apps." + echo + tput bold + echo "SYNOPSIS" + tput sgr0 + echo " create_test_files.sh" + echo " create_test_files.sh [--regenerate-test-certs]" + echo " create_test_files.sh [--help]" + echo + tput bold + echo "DESCRIPTION" + tput sgr0 + echo " The script signs a manifest for Trusted Hosted Apps if no parameter" + echo " is given and if the manifest file and a certificate database directory" + echo " is present in the current directory." + echo " Two directories ./testValidSignedManifest and ./testInvalidSignedManifest" + echo " are generated containing a manifest signature file each, signed with valid" + echo " and invalid certificates respectively." + echo " If the --regenerate-test-certs parameter is given, a new certificate database" + echo " directory is generated before the signing takes place." + echo " If the certificate database is not present and the --regenerate-test-certs" + echo " parameter is not given the script exits whithout any operations." + echo + tput bold + echo "OPTIONS" + echo " --regenerate-test-certs," + tput sgr0 + echo " Generates a test certificate database and then signs the manifest.webapp" + echo " file in the current directory." + echo + tput bold + echo " --help," + tput sgr0 + echo " Show this usage information." + echo +} + +# Function to create a signing database +# Parameters: +# $1: Output directory (where the DB will be created) +# $2: Password file +createDB() { + local db_path=${1} + local password_file=${2} + + mkdir -p ${db_path} + echo insecurepassword > ${password_file} + certutil -d ${db_path} -N -f ${password_file} 2>&1 >/dev/null +} + +# Add a CA cert and a signing cert to the database +# Arguments: +# $1: DB directory +# $2: CA CN (don't include the CN=, just the value) +# $3: Signing Cert CN (don't include the CN=, just the value) +# $4: CA short name (don't use spaces!) +# $5: Signing Cert short name (don't use spaces!) +# $6: Password file +addCerts() { + local db_path=${1} + local password_file=${6} + + org="O=Example Trusted Corporation,L=Mountain View,ST=CA,C=US" + ca_subj="CN=${2},${org}" + ee_subj="CN=${3},${org}" + + noisefile=/tmp/noise.$$ + head -c 32 /dev/urandom > ${noisefile} + + ca_responses=/tmp/caresponses.$$ + ee_responses=/tmp/earesponses + + echo y > ${ca_responses} # Is this a CA? + echo >> ${ca_responses} # Accept default path length constraint (no constraint) + echo y >> ${ca_responses} # Is this a critical constraint? + echo n > ${ee_responses} # Is this a CA? + echo >> ${ee_responses} # Accept default path length constraint (no constraint) + echo y >> ${ee_responses} # Is this a critical constraint? + + make_cert="certutil -d ${db_path} -f ${password_file} -S -g 2048 -Z SHA256 \ + -z ${noisefile} -y 3 -2 --extKeyUsage critical,codeSigning" + ${make_cert} -v 480 -n ${4} -m 1 -s "${ca_subj}" --keyUsage critical,certSigning \ + -t ",,CTu" -x < ${ca_responses} 2>&1 >/dev/null + ${make_cert} -v 240 -n ${5} -c ${4} -m 2 -s "${ee_subj}" --keyUsage critical,digitalSignature \ + -t ",,," < ${ee_responses} 2>&1 >/dev/null + + certutil -d ${db_path} -L -n ${4} -r -o ${SIGN_SCR_PATH}/${4}.der + + rm -f ${noisefile} ${ee_responses} ${ca_responses} +} + +# Signs a manifest +# Parameters: +# $1: Database directory +# $2: Unsigned manifest file path +# $3: Signed manifest file path +# $4: Nickname of the signing certificate +# $5: Password file +signManifest() { + local db_path=${1} + local password_file=${5} + + python ${BASE_PATH}/${SIGN_SCR_PATH}/sign_b2g_manifest.py -d ${db_path} \ + -f ${password_file} -k ${4} -i ${2} -o ${3} +} + +# Generate the necessary files to be used for the signing +generate_files() { + # First create a new couple of signing DBs + rm -rf ${DB_PATH} ${VALID_MANIFEST_PATH} ${INVALID_MANIFEST_PATH} + createDB ${DB_PATH} ${PASSWORD_FILE} + addCerts ${DB_PATH} "Trusted Valid CA" "Trusted Corp Cert" ${TRUSTED_CA} ${TRUSTED_EE} ${PASSWORD_FILE} + addCerts ${DB_PATH} "Trusted Invalid CA" "Trusted Invalid Cert" ${UNTRUSTED_CA} ${UNTRUSTED_EE} ${PASSWORD_FILE} +} + +#Start of execution +if [ ${1} ] && [ "${1}" == '--regenerate-test-certs' ]; then + generate_files +elif [ "${1}" == '--help' ]; then + usage + exit 1 +else + if [ -d ${DB_PATH} ]; then + rm -rf ${VALID_MANIFEST_PATH} ${INVALID_MANIFEST_PATH} + else + echo "Error! The directory ${DB_PATH} does not exist!" + echo "New certificate database must be created!" + usage $0 + exit 1 + fi +fi + +# Create all the test manifests +mkdir -p ${VALID_MANIFEST_PATH} +mkdir -p ${INVALID_MANIFEST_PATH} + +CURDIR=`pwd` +cd $CURDIR + +# Sign a manifest file with a known issuer +signManifest ${DB_PATH} ${BASE_PATH}/manifest.webapp \ + ${VALID_MANIFEST_PATH}/manifest.sig \ + ${TRUSTED_EE} ${PASSWORD_FILE} + +# Sign a manifest file with a unknown issuer +signManifest ${DB_PATH} ${BASE_PATH}/manifest.webapp \ + ${INVALID_MANIFEST_PATH}/manifest.sig \ + ${UNTRUSTED_EE} ${PASSWORD_FILE} + +echo "Done!" diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp b/security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp new file mode 100644 index 00000000000..ad477719b48 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/manifest.webapp @@ -0,0 +1,10 @@ +{ "name": "Trusted App Example", + "description": "A Manifest for a Trusted Hosted Application", + "type": "trusted", + "launch_path": "/index.html", + "icons": { "128" : "icon-128.png" }, + "version": 1, + "csp" : "script-src https://www.example.com; style-src https://www.example.com", + "permissions": { "device-storage:videos":{ "access": "readonly" }, "device-storage:pictures":{ "access": "readwrite" } }, + "default_locale": "en-US" +} diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py b/security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py new file mode 100644 index 00000000000..393ca249f77 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/nss_ctypes.py @@ -0,0 +1,136 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from ctypes import * +import os +import sys + +if sys.platform == 'darwin': + libprefix = "lib" + libsuffix = ".dylib" +elif os.name == 'posix': + libprefix = "lib" + libsuffix = ".so" +else: # assume windows + libprefix = "" + libsuffix = ".dll" + +plc = cdll.LoadLibrary(libprefix + "plc4" + libsuffix) +nspr = cdll.LoadLibrary(libprefix + "nspr4" + libsuffix) +nss = cdll.LoadLibrary(libprefix + "nss3" + libsuffix) +smime = cdll.LoadLibrary(libprefix + "smime3" + libsuffix) + +nspr.PR_GetError.argtypes = [] +nspr.PR_GetError.restype = c_int32 +nspr.PR_ErrorToName.argtypes = [c_int32] +nspr.PR_ErrorToName.restype = c_char_p + +def raise_if_not_SECSuccess(rv): + SECSuccess = 0 + if (rv != SECSuccess): + raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError())) + +def raise_if_NULL(p): + if not p: + raise ValueError(nspr.PR_ErrorToName(nspr.PR_GetError())) + return p + +PRBool = c_int +SECStatus = c_int + +# from secoidt.h +SEC_OID_SHA1 = 4 + +# from certt.h +certUsageObjectSigner = 6 + +class SECItem(Structure): + _fields_ = [("type", c_int), + ("data", c_char_p), + ("len", c_uint)] + +nss.NSS_Init.argtypes = [c_char_p] +nss.NSS_Init.restype = SECStatus +def NSS_Init(db_dir): + nss.NSS_Init.argtypes = [c_char_p] + nss.NSS_Init.restype = SECStatus + raise_if_not_SECSuccess(nss.NSS_Init(db_dir)) + +nss.NSS_Shutdown.argtypes = [] +nss.NSS_Shutdown.restype = SECStatus +def NSS_Shutdown(): + raise_if_not_SECSuccess(nss.NSS_Shutdown()) + +PK11PasswordFunc = CFUNCTYPE(c_char_p, c_void_p, PRBool, c_char_p) + +# pass the result of this as the wincx parameter when a wincx is required +nss.PK11_SetPasswordFunc.argtypes = [PK11PasswordFunc] +nss.PK11_SetPasswordFunc.restype = None + +# Set the return type as *void so Python doesn't touch it +plc.PL_strdup.argtypes = [c_char_p] +plc.PL_strdup.restype = c_void_p +def SetPasswordContext(password): + def callback(slot, retry, arg): + return plc.PL_strdup(password) + wincx = PK11PasswordFunc(callback) + nss.PK11_SetPasswordFunc(wincx) + return wincx + +nss.CERT_GetDefaultCertDB.argtypes = [] +nss.CERT_GetDefaultCertDB.restype = c_void_p +def CERT_GetDefaultCertDB(): + return raise_if_NULL(nss.CERT_GetDefaultCertDB()) + +nss.PK11_FindCertFromNickname.argtypes = [c_char_p, c_void_p] +nss.PK11_FindCertFromNickname.restype = c_void_p +def PK11_FindCertFromNickname(nickname, wincx): + return raise_if_NULL(nss.PK11_FindCertFromNickname(nickname, wincx)) + +nss.CERT_DestroyCertificate.argtypes = [c_void_p] +nss.CERT_DestroyCertificate.restype = None +def CERT_DestroyCertificate(cert): + nss.CERT_DestroyCertificate(cert) + +smime.SEC_PKCS7CreateSignedData.argtypes = [c_void_p, c_int, c_void_p, + c_int, c_void_p, + c_void_p, c_void_p] +smime.SEC_PKCS7CreateSignedData.restype = c_void_p +def SEC_PKCS7CreateSignedData(cert, certusage, certdb, digestalg, digest, wincx): + item = SECItem(0, c_char_p(digest), len(digest)) + return raise_if_NULL(smime.SEC_PKCS7CreateSignedData(cert, certusage, certdb, + digestalg, + pointer(item), + None, wincx)) + +smime.SEC_PKCS7AddSigningTime.argtypes = [c_void_p] +smime.SEC_PKCS7AddSigningTime.restype = SECStatus +def SEC_PKCS7AddSigningTime(p7): + raise_if_not_SECSuccess(smime.SEC_PKCS7AddSigningTime(p7)) + +smime.SEC_PKCS7IncludeCertChain.argtypes = [c_void_p, c_void_p] +smime.SEC_PKCS7IncludeCertChain.restype = SECStatus +def SEC_PKCS7IncludeCertChain(p7, wincx): + raise_if_not_SECSuccess(smime.SEC_PKCS7IncludeCertChain(p7, wincx)) + +SEC_PKCS7EncoderOutputCallback = CFUNCTYPE(None, c_void_p, c_void_p, c_long) +smime.SEC_PKCS7Encode.argtypes = [c_void_p, SEC_PKCS7EncoderOutputCallback, + c_void_p, c_void_p, c_void_p, c_void_p] +smime.SEC_PKCS7Encode.restype = SECStatus +def SEC_PKCS7Encode(p7, bulkkey, wincx): + outputChunks = [] + def callback(chunks, data, len): + outputChunks.append(string_at(data, len)) + callbackWrapper = SEC_PKCS7EncoderOutputCallback(callback) + raise_if_not_SECSuccess(smime.SEC_PKCS7Encode(p7, callbackWrapper, + None, None, None, wincx)) + return "".join(outputChunks) + +smime.SEC_PKCS7DestroyContentInfo.argtypes = [c_void_p] +smime.SEC_PKCS7DestroyContentInfo.restype = None +def SEC_PKCS7DestroyContentInfo(p7): + smime.SEC_PKCS7DestroyContentInfo(p7) diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py b/security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py new file mode 100644 index 00000000000..acc5ac2f441 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_manifest/sign_b2g_manifest.py @@ -0,0 +1,76 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import argparse +from base64 import b64encode +from hashlib import sha1 +import sys +import ctypes +import nss_ctypes + +def nss_create_detached_signature(cert, dataToSign, wincx): + certdb = nss_ctypes.CERT_GetDefaultCertDB() + p7 = nss_ctypes.SEC_PKCS7CreateSignedData(cert, + nss_ctypes.certUsageObjectSigner, + certdb, + nss_ctypes.SEC_OID_SHA1, + sha1(dataToSign).digest(), + wincx) + try: + nss_ctypes.SEC_PKCS7AddSigningTime(p7) + nss_ctypes.SEC_PKCS7IncludeCertChain(p7, wincx) + return nss_ctypes.SEC_PKCS7Encode(p7, None, wincx) + finally: + nss_ctypes.SEC_PKCS7DestroyContentInfo(p7) + +# Sign a manifest file +def sign_file(in_file, out_file, cert, wincx): + contents = in_file.read() + in_file.close() + + # generate base64 encoded string of the sha1 digest of the input file + in_file_hash = b64encode(sha1(contents).digest()) + + # sign the base64 encoded string with the given certificate + in_file_signature = nss_create_detached_signature(cert, in_file_hash, wincx) + + # write the content of the output file + out_file.write(in_file_signature) + out_file.close() + +def main(): + parser = argparse.ArgumentParser(description='Sign a B2G app manifest.') + parser.add_argument('-d', action='store', + required=True, help='NSS database directory') + parser.add_argument('-f', action='store', + type=argparse.FileType('rb'), + required=True, help='password file') + parser.add_argument('-k', action='store', + required=True, help="nickname of signing cert.") + parser.add_argument('-i', action='store', type=argparse.FileType('rb'), + required=True, help="input manifest file (unsigned)") + parser.add_argument('-o', action='store', type=argparse.FileType('wb'), + required=True, help="output manifest file (signed)") + args = parser.parse_args() + + db_dir = args.d + password = args.f.readline().strip() + cert_nickname = args.k + cert = None + + try: + nss_ctypes.NSS_Init(db_dir) + wincx = nss_ctypes.SetPasswordContext(password) + cert = nss_ctypes.PK11_FindCertFromNickname(cert_nickname, wincx) + sign_file(args.i, args.o, cert, wincx) + return 0 + finally: + nss_ctypes.CERT_DestroyCertificate(cert) + nss_ctypes.NSS_Shutdown() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/testInvalidSignedManifest/manifest.sig b/security/manager/ssl/tests/unit/test_signed_manifest/testInvalidSignedManifest/manifest.sig new file mode 100644 index 00000000000..d5eee196c09 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_manifest/testInvalidSignedManifest/manifest.sig differ diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/testValidSignedManifest/manifest.sig b/security/manager/ssl/tests/unit/test_signed_manifest/testValidSignedManifest/manifest.sig new file mode 100644 index 00000000000..99622635115 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_manifest/testValidSignedManifest/manifest.sig differ diff --git a/security/manager/ssl/tests/unit/test_signed_manifest/trusted_ca1.der b/security/manager/ssl/tests/unit/test_signed_manifest/trusted_ca1.der new file mode 100644 index 00000000000..bf05308ccc5 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_signed_manifest/trusted_ca1.der differ