diff --git a/examples/config.py b/examples/config.py index 8eb1f266..4556233e 100644 --- a/examples/config.py +++ b/examples/config.py @@ -54,13 +54,24 @@ of applications from the main repository. """ -#The key (from the keystore defined below) to be used for signing the -#repository itself. Can be None for an unsigned repository. -repo_keyalias = None +# The key (from the keystore defined below) to be used for signing the +# repository itself. This is the same name you would give to keytool or +# 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 -#somewhere safe and secure, and backed up! -#keystore = "/home/me/.local/share/fdroidserver/keystore.jks" +# The keystore to use for release keys when building. This needs to be +# somewhere safe and secure, and backed up! The best way to manage these +# 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 # different than the keypass below, it can be OK to store the password in this diff --git a/examples/opensc-fdroid.cfg b/examples/opensc-fdroid.cfg new file mode 100644 index 00000000..bf3ef2fd --- /dev/null +++ b/examples/opensc-fdroid.cfg @@ -0,0 +1,4 @@ +name = OpenSC +description = SunPKCS11 w/ OpenSC Smart card Framework +library = /usr/lib/opensc-pkcs11.so +slotListIndex = 1 diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 205beccf..ef36deb6 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -54,6 +54,16 @@ def read_config(opts, config_file='config.py'): logging.debug("Reading %s" % config_file) 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 = { 'sdk_path': "$ANDROID_HOME", 'ndk_path': "$ANDROID_NDK", @@ -66,8 +76,8 @@ def read_config(opts, config_file='config.py'): 'stats_to_carbon': False, 'repo_maxage': 0, 'build_server_always': False, - 'keystore': os.path.join(os.getenv('HOME'), - '.local', 'share', 'fdroidserver', 'keystore.jks'), + 'keystore': '$HOME/.local/share/fdroidserver/keystore.jks', + 'smartcardoptions': [], 'char_limits': { 'Summary' : 50, 'Description' : 1500 @@ -86,10 +96,13 @@ def read_config(opts, config_file='config.py'): config[k] = os.path.expandvars(v) 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) 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) if any(k in config for k in ["keystore", "keystorepass", "keypass"]): diff --git a/fdroidserver/init.py b/fdroidserver/init.py index 5cd126c1..1b11bcd4 100644 --- a/fdroidserver/init.py +++ b/fdroidserver/init.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import glob import hashlib import os import re @@ -38,8 +39,18 @@ def write_to_config(key, value): '''write a key/value to the local config.py''' with open('config.py', 'r') as f: data = f.read() - pattern = key + '\s*=.*' - repl = key + ' = "' + value + '"' + pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"' + 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) with open('config.py', 'w') as f: f.writelines(data) @@ -71,8 +82,8 @@ def genkey(keystore, repo_keyalias, password, keydname): raise BuildException("Failed to generate key", p.stdout) # now show the lovely key that was just generated p = FDroidPopen(['keytool', '-list', '-v', - '-keystore', keystore, '-alias', repo_keyalias], - '-storepass:file', config['keystorepassfile']) + '-keystore', keystore, '-alias', repo_keyalias, + '-storepass:file', config['keystorepassfile']]) 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 # 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. - # 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'] if options.keystore: - if os.path.isfile(options.keystore): + keystore = os.path.abspath(options.keystore) + if options.keystore == 'NONE': keystore = options.keystore - write_to_config('keystore', keystore) else: - logging.info('"' + options.keystore + '" does not exist or is not a file!') - sys.exit(1) + keystore = os.path.abspath(options.keystore) + 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: repo_keyalias = options.repo_keyalias write_to_config('repo_keyalias', repo_keyalias) if options.distinguished_name: keydname = options.distinguished_name 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 - keystoredir = os.path.join(os.getenv('HOME'), - '.local', 'share', 'fdroidserver') + keystoredir = os.path.dirname(keystore) if not os.path.exists(keystoredir): os.makedirs(keystoredir, mode=0o700) - keystore = os.path.join(keystoredir, 'keystore.jks') - write_to_config('keystore', keystore) password = genpassword() write_to_config('keystorepass', password) write_to_config('keypass', password) - if not options.repo_keyalias: + if options.repo_keyalias == None: repo_keyalias = socket.getfqdn() write_to_config('repo_keyalias', repo_keyalias) 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 NDK (optional):\t' + ndk_path) 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 "' + os.path.join(fdroiddir, 'repo') + '"' + ''' 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 -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 and https://f-droid.org/manual/fdroid.html#Signing diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 6e54bbc0..f9d42486 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -627,7 +627,7 @@ def make_index(apps, apks, repodir, archive, categories): repoel.setAttribute("version", "12") 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 # (but with slightly different formatting) @@ -642,7 +642,8 @@ def make_index(apps, apks, repodir, archive, categories): p = FDroidPopen(['keytool', '-exportcert', '-alias', config['repo_keyalias'], '-keystore', config['keystore'], - '-storepass:file', config['keystorepassfile']]) + '-storepass:file', config['keystorepassfile']] + + config['smartcardoptions']) if p.returncode != 0: logging.critical("Failed to get repo pubkey") sys.exit(1) @@ -783,7 +784,7 @@ def make_index(apps, apks, repodir, archive, categories): of.write(output) of.close() - if config['repo_keyalias'] is not None: + if 'repo_keyalias' in config: logging.info("Creating signed index.") logging.info("Key fingerprint: %s" % repo_pubkey_fingerprint) @@ -795,11 +796,15 @@ def make_index(apps, apks, repodir, archive, categories): sys.exit(1) # Sign the index... - p = FDroidPopen(['jarsigner', '-keystore', config['keystore'], - '-storepass:file', config['keystorepassfile'], - '-keypass:file', config['keypassfile'], - '-digestalg', 'SHA1', '-sigalg', 'MD5withRSA', - os.path.join(repodir, 'index.jar') , config['repo_keyalias']]) + args = ['jarsigner', '-keystore', config['keystore'], + '-storepass:file', config['keystorepassfile'], + '-digestalg', 'SHA1', '-sigalg', 'MD5withRSA', + 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 if p.returncode != 0: logging.info("Failed to sign index") diff --git a/setup.py b/setup.py index 3bfea041..35ecc8e5 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ setup(name='fdroidserver', [ 'buildserver/config.buildserver.py', 'examples/config.py', 'examples/makebs.config.py', + 'examples/opensc-fdroid.cfg', 'examples/fdroid-icon.png']), ('fdroidserver/getsig', ['fdroidserver/getsig/getsig.class']) ], diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 5c5c31d0..c75a2f02 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -3,6 +3,18 @@ set -e 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 WORKSPACE=`dirname $(pwd)` echo "Setting Workspace to $WORKSPACE" @@ -19,16 +31,31 @@ fi REPOROOT=`mktemp --directory --tmpdir=$WORKSPACE` cd $REPOROOT $fdroid init -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 $REPOROOT/repo/$apk -done -# delete any 'unaligned' duplicates -rm -f $REPOROOT/repo/*unaligned*.apk - - +copy_apks_into_repo $REPOROOT $fdroid update -c $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