Merge branch 'master' into 'master'

implemeted support for using a smartcard for the repo signing key

This changeset implements using a smartcard (HSM) as the keystore for the signing key.  It also fixes lots of little bugs in the `fdroid init` process.
This commit is contained in:
Ciaran Gultnieks 2014-04-08 08:26:37 +00:00
commit e8c47765ae
7 changed files with 149 additions and 45 deletions

View file

@ -55,12 +55,23 @@ of applications from the main repository.
# The key (from the keystore defined below) to be used for signing the # The key (from the keystore defined below) to be used for signing the
#repository itself. Can be None for an unsigned repository. # repository itself. This is the same name you would give to keytool or
repo_keyalias = None # jarsigner using -alias. (Not needed in an unsigned repository).
#repo_keyalias = "fdroidrepo"
# The keystore to use for release keys when building. This needs to be # The keystore to use for release keys when building. This needs to be
#somewhere safe and secure, and backed up! # somewhere safe and secure, and backed up! The best way to manage these
#keystore = "/home/me/.local/share/fdroidserver/keystore.jks" # sensitive keys is to use a "smartcard" (aka Hardware Security Module). To
# configure FDroid to use a smartcard, set the keystore file using the keyword
# "NONE" (i.e. keystore = "NONE"). That makes Java find the keystore on the
# smartcard based on 'smartcardoptions' below.
#keystore = "~/.local/share/fdroidserver/keystore.jks"
# You should not need to change these at all, unless you have a very
# customized setup for using smartcards in Java with keytool/jarsigner
#smartcardoptions = "-storetype PKCS11 -providerName SunPKCS11-OpenSC \
# -providerClass sun.security.pkcs11.SunPKCS11 \
# -providerArg opensc-fdroid.cfg"
# The password for the keystore (at least 6 characters). If this password is # The password for the keystore (at least 6 characters). If this password is
# different than the keypass below, it can be OK to store the password in this # different than the keypass below, it can be OK to store the password in this

View file

@ -0,0 +1,4 @@
name = OpenSC
description = SunPKCS11 w/ OpenSC Smart card Framework
library = /usr/lib/opensc-pkcs11.so
slotListIndex = 1

View file

@ -54,6 +54,16 @@ def read_config(opts, config_file='config.py'):
logging.debug("Reading %s" % config_file) logging.debug("Reading %s" % config_file)
execfile(config_file, config) execfile(config_file, config)
# smartcardoptions must be a list since its command line args for Popen
if 'smartcardoptions' in config:
config['smartcardoptions'] = config['smartcardoptions'].split(' ')
elif 'keystore' in config and config['keystore'] == 'NONE':
# keystore='NONE' means use smartcard, these are required defaults
config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
'SunPKCS11-OpenSC', '-providerClass',
'sun.security.pkcs11.SunPKCS11',
'-providerArg', 'opensc-fdroid.cfg']
defconfig = { defconfig = {
'sdk_path': "$ANDROID_HOME", 'sdk_path': "$ANDROID_HOME",
'ndk_path': "$ANDROID_NDK", 'ndk_path': "$ANDROID_NDK",
@ -66,8 +76,8 @@ def read_config(opts, config_file='config.py'):
'stats_to_carbon': False, 'stats_to_carbon': False,
'repo_maxage': 0, 'repo_maxage': 0,
'build_server_always': False, 'build_server_always': False,
'keystore': os.path.join(os.getenv('HOME'), 'keystore': '$HOME/.local/share/fdroidserver/keystore.jks',
'.local', 'share', 'fdroidserver', 'keystore.jks'), 'smartcardoptions': [],
'char_limits': { 'char_limits': {
'Summary' : 50, 'Summary' : 50,
'Description' : 1500 'Description' : 1500
@ -86,10 +96,13 @@ def read_config(opts, config_file='config.py'):
config[k] = os.path.expandvars(v) config[k] = os.path.expandvars(v)
if not config['sdk_path']: if not config['sdk_path']:
logging.critical("$ANDROID_HOME is not set!") logging.critical("Neither $ANDROID_HOME nor sdk_path is set, no Android SDK found!")
sys.exit(3)
if not os.path.exists(config['sdk_path']):
logging.critical('Android SDK path "' + config['sdk_path'] + '" does not exist!')
sys.exit(3) sys.exit(3)
if not os.path.isdir(config['sdk_path']): if not os.path.isdir(config['sdk_path']):
logging.critical("$ANDROID_HOME points to a non-existing directory!") logging.critical('Android SDK path "' + config['sdk_path'] + '" is not a directory!')
sys.exit(3) sys.exit(3)
if any(k in config for k in ["keystore", "keystorepass", "keypass"]): if any(k in config for k in ["keystore", "keystorepass", "keypass"]):

View file

@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import glob
import hashlib import hashlib
import os import os
import re import re
@ -38,8 +39,18 @@ def write_to_config(key, value):
'''write a key/value to the local config.py''' '''write a key/value to the local config.py'''
with open('config.py', 'r') as f: with open('config.py', 'r') as f:
data = f.read() data = f.read()
pattern = key + '\s*=.*' pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
repl = key + ' = "' + value + '"' repl = '\n' + key + ' = "' + value + '"'
data = re.sub(pattern, repl, data)
with open('config.py', 'w') as f:
f.writelines(data)
def disable_in_config(key, value):
'''write a key/value to the local config.py, then comment it out'''
with open('config.py', 'r') as f:
data = f.read()
pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
repl = '\n#' + key + ' = "' + value + '"'
data = re.sub(pattern, repl, data) data = re.sub(pattern, repl, data)
with open('config.py', 'w') as f: with open('config.py', 'w') as f:
f.writelines(data) f.writelines(data)
@ -71,8 +82,8 @@ def genkey(keystore, repo_keyalias, password, keydname):
raise BuildException("Failed to generate key", p.stdout) raise BuildException("Failed to generate key", p.stdout)
# now show the lovely key that was just generated # now show the lovely key that was just generated
p = FDroidPopen(['keytool', '-list', '-v', p = FDroidPopen(['keytool', '-list', '-v',
'-keystore', keystore, '-alias', repo_keyalias], '-keystore', keystore, '-alias', repo_keyalias,
'-storepass:file', config['keystorepassfile']) '-storepass:file', config['keystorepassfile']])
logging.info(p.stdout.strip() + '\n\n') logging.info(p.stdout.strip() + '\n\n')
@ -175,33 +186,63 @@ def main():
# find or generate the keystore for the repo signing key. First try the # find or generate the keystore for the repo signing key. First try the
# path written in the default config.py. Then check if the user has # path written in the default config.py. Then check if the user has
# specified a path from the command line, which will trump all others. # specified a path from the command line, which will trump all others.
# Otherwise, create ~/.local/share/fdroidserver and stick it in there. # Otherwise, create ~/.local/share/fdroidserver and stick it in there. If
# keystore is set to NONE, that means that Java will look for keys in a
# Hardware Security Module aka Smartcard.
keystore = config['keystore'] keystore = config['keystore']
if options.keystore: if options.keystore:
if os.path.isfile(options.keystore): keystore = os.path.abspath(options.keystore)
if options.keystore == 'NONE':
keystore = options.keystore keystore = options.keystore
write_to_config('keystore', keystore)
else: else:
logging.info('"' + options.keystore + '" does not exist or is not a file!') keystore = os.path.abspath(options.keystore)
sys.exit(1) if not os.path.exists(keystore):
logging.info('"' + keystore
+ '" does not exist, creating a new keystore there.')
write_to_config('keystore', keystore)
repo_keyalias = None
if options.repo_keyalias: if options.repo_keyalias:
repo_keyalias = options.repo_keyalias repo_keyalias = options.repo_keyalias
write_to_config('repo_keyalias', repo_keyalias) write_to_config('repo_keyalias', repo_keyalias)
if options.distinguished_name: if options.distinguished_name:
keydname = options.distinguished_name keydname = options.distinguished_name
write_to_config('keydname', keydname) write_to_config('keydname', keydname)
if not os.path.isfile(keystore): if keystore == 'NONE': # we're using a smartcard
write_to_config('repo_keyalias', '1') # seems to be the default
disable_in_config('keypass', 'never used with smartcard')
write_to_config('smartcardoptions',
('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
+ '-providerClass sun.security.pkcs11.SunPKCS11 '
+ '-providerArg opensc-fdroid.cfg'))
# find opensc-pkcs11.so
if not os.path.exists('opensc-fdroid.cfg'):
if os.path.exists('/usr/lib/opensc-pkcs11.so'):
opensc_so = '/usr/lib/opensc-pkcs11.so'
elif os.path.exists('/usr/lib64/opensc-pkcs11.so'):
opensc_so = '/usr/lib64/opensc-pkcs11.so'
else:
files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so')
if len(files) > 0:
opensc_so = files[0]
else:
opensc_so = '/usr/lib/opensc-pkcs11.so'
logging.warn('No OpenSC PKCS#11 module found, ' +
'install OpenSC then edit "opensc-fdroid.cfg"!')
with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f:
opensc_fdroid = f.read()
opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid,
flags=re.MULTILINE)
with open('opensc-fdroid.cfg', 'w') as f:
f.write(opensc_fdroid)
elif not os.path.exists(keystore):
# no existing or specified keystore, generate the whole thing # no existing or specified keystore, generate the whole thing
keystoredir = os.path.join(os.getenv('HOME'), keystoredir = os.path.dirname(keystore)
'.local', 'share', 'fdroidserver')
if not os.path.exists(keystoredir): if not os.path.exists(keystoredir):
os.makedirs(keystoredir, mode=0o700) os.makedirs(keystoredir, mode=0o700)
keystore = os.path.join(keystoredir, 'keystore.jks')
write_to_config('keystore', keystore)
password = genpassword() password = genpassword()
write_to_config('keystorepass', password) write_to_config('keystorepass', password)
write_to_config('keypass', password) write_to_config('keypass', password)
if not options.repo_keyalias: if options.repo_keyalias == None:
repo_keyalias = socket.getfqdn() repo_keyalias = socket.getfqdn()
write_to_config('repo_keyalias', repo_keyalias) write_to_config('repo_keyalias', repo_keyalias)
if not options.distinguished_name: if not options.distinguished_name:
@ -215,12 +256,14 @@ def main():
logging.info(' Android SDK Build Tools:\t' + os.path.dirname(aapt)) logging.info(' Android SDK Build Tools:\t' + os.path.dirname(aapt))
logging.info(' Android NDK (optional):\t' + ndk_path) logging.info(' Android NDK (optional):\t' + ndk_path)
logging.info(' Keystore for signing key:\t' + keystore) logging.info(' Keystore for signing key:\t' + keystore)
if repo_keyalias != None:
logging.info(' Alias for key in store:\t' + repo_keyalias)
logging.info('\nTo complete the setup, add your APKs to "' + logging.info('\nTo complete the setup, add your APKs to "' +
os.path.join(fdroiddir, 'repo') + '"' + os.path.join(fdroiddir, 'repo') + '"' +
''' '''
then run "fdroid update -c; fdroid update". You might also want to edit then run "fdroid update -c; fdroid update". You might also want to edit
"config.py" to set the URL, repo name, and more. You should also set up "config.py" to set the URL, repo name, and more. You should also set up
a signing key. a signing key (a temporary one might have been automatically generated).
For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
and https://f-droid.org/manual/fdroid.html#Signing and https://f-droid.org/manual/fdroid.html#Signing

View file

@ -627,7 +627,7 @@ def make_index(apps, apks, repodir, archive, categories):
repoel.setAttribute("version", "12") repoel.setAttribute("version", "12")
repoel.setAttribute("timestamp", str(int(time.time()))) repoel.setAttribute("timestamp", str(int(time.time())))
if config['repo_keyalias']: if 'repo_keyalias' in config:
# Generate a certificate fingerprint the same way keytool does it # Generate a certificate fingerprint the same way keytool does it
# (but with slightly different formatting) # (but with slightly different formatting)
@ -642,7 +642,8 @@ def make_index(apps, apks, repodir, archive, categories):
p = FDroidPopen(['keytool', '-exportcert', p = FDroidPopen(['keytool', '-exportcert',
'-alias', config['repo_keyalias'], '-alias', config['repo_keyalias'],
'-keystore', config['keystore'], '-keystore', config['keystore'],
'-storepass:file', config['keystorepassfile']]) '-storepass:file', config['keystorepassfile']]
+ config['smartcardoptions'])
if p.returncode != 0: if p.returncode != 0:
logging.critical("Failed to get repo pubkey") logging.critical("Failed to get repo pubkey")
sys.exit(1) sys.exit(1)
@ -783,7 +784,7 @@ def make_index(apps, apks, repodir, archive, categories):
of.write(output) of.write(output)
of.close() of.close()
if config['repo_keyalias'] is not None: if 'repo_keyalias' in config:
logging.info("Creating signed index.") logging.info("Creating signed index.")
logging.info("Key fingerprint: %s" % repo_pubkey_fingerprint) logging.info("Key fingerprint: %s" % repo_pubkey_fingerprint)
@ -795,11 +796,15 @@ def make_index(apps, apks, repodir, archive, categories):
sys.exit(1) sys.exit(1)
# Sign the index... # Sign the index...
p = FDroidPopen(['jarsigner', '-keystore', config['keystore'], args = ['jarsigner', '-keystore', config['keystore'],
'-storepass:file', config['keystorepassfile'], '-storepass:file', config['keystorepassfile'],
'-keypass:file', config['keypassfile'],
'-digestalg', 'SHA1', '-sigalg', 'MD5withRSA', '-digestalg', 'SHA1', '-sigalg', 'MD5withRSA',
os.path.join(repodir, 'index.jar') , config['repo_keyalias']]) os.path.join(repodir, 'index.jar'), config['repo_keyalias']]
if config['keystore'] == 'NONE':
args += config['smartcardoptions']
else: # smardcards never use -keypass
args += ['-keypass:file', config['keypassfile']]
p = FDroidPopen(args)
# TODO keypass should be sent via stdin # TODO keypass should be sent via stdin
if p.returncode != 0: if p.returncode != 0:
logging.info("Failed to sign index") logging.info("Failed to sign index")

View file

@ -23,6 +23,7 @@ setup(name='fdroidserver',
[ 'buildserver/config.buildserver.py', [ 'buildserver/config.buildserver.py',
'examples/config.py', 'examples/config.py',
'examples/makebs.config.py', 'examples/makebs.config.py',
'examples/opensc-fdroid.cfg',
'examples/fdroid-icon.png']), 'examples/fdroid-icon.png']),
('fdroidserver/getsig', ['fdroidserver/getsig/getsig.class']) ('fdroidserver/getsig', ['fdroidserver/getsig/getsig.class'])
], ],

View file

@ -3,6 +3,18 @@
set -e set -e
set -x set -x
copy_apks_into_repo() {
for f in `ls -1 ../../*/bin/*.apk`; do
name=$(basename $(dirname `dirname $f`))
echo "name $name"
apk=${name}_`basename $f`
echo "apk $apk"
cp $f $1/repo/$apk
done
# delete any 'unaligned' duplicates
rm -f $1/repo/*unaligned*.apk
}
if [ -z $WORKSPACE ]; then if [ -z $WORKSPACE ]; then
WORKSPACE=`dirname $(pwd)` WORKSPACE=`dirname $(pwd)`
echo "Setting Workspace to $WORKSPACE" echo "Setting Workspace to $WORKSPACE"
@ -19,16 +31,31 @@ fi
REPOROOT=`mktemp --directory --tmpdir=$WORKSPACE` REPOROOT=`mktemp --directory --tmpdir=$WORKSPACE`
cd $REPOROOT cd $REPOROOT
$fdroid init $fdroid init
for f in `ls -1 ../../*/bin/*.apk`; do copy_apks_into_repo $REPOROOT
name=$(basename $(dirname `dirname $f`))
echo "name $name"
apk=${name}_`basename $f`
echo "apk $apk"
cp $f $REPOROOT/repo/$apk
done
# delete any 'unaligned' duplicates
rm -f $REPOROOT/repo/*unaligned*.apk
$fdroid update -c $fdroid update -c
$fdroid update $fdroid update
#------------------------------------------------------------------------------#
# setup a new repo from scratch and generate a keystore
REPOROOT=`mktemp --directory --tmpdir=$WORKSPACE`
KEYSTORE=$REPOROOT/keystore.jks
cd $REPOROOT
$fdroid init --keystore $KEYSTORE
test -e $KEYSTORE
copy_apks_into_repo $REPOROOT
$fdroid update -c
$fdroid update
test -e repo/index.xml
test -e repo/index.jar
#------------------------------------------------------------------------------#
# setup a new repo from scratch with a HSM/smartcard
REPOROOT=`mktemp --directory --tmpdir=$WORKSPACE`
cd $REPOROOT
$fdroid init --keystore NONE
test -e opensc-fdroid.cfg
test ! -e NONE