chiark / gitweb /
support repo signing with a key on a smartcard
[fdroidserver.git] / fdroidserver / init.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # update.py - part of the FDroid server tools
5 # Copyright (C) 2010-2013, Ciaran Gultnieks, ciaran@ciarang.com
6 # Copyright (C) 2013-2014 Daniel Martí <mvdan@mvdan.cc>
7 # Copyright (C) 2013 Hans-Christoph Steiner <hans@eds.org>
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Affero General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU Affero General Public License for more details.
18 #
19 # You should have received a copy of the GNU Affero General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22 import glob
23 import hashlib
24 import os
25 import re
26 import shutil
27 import socket
28 import sys
29 from optparse import OptionParser
30 import logging
31
32 import common
33 from common import FDroidPopen, BuildException
34
35 config = {}
36 options = None
37
38 def write_to_config(key, value):
39     '''write a key/value to the local config.py'''
40     with open('config.py', 'r') as f:
41         data = f.read()
42     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
43     repl = '\n' + key + ' = "' + value + '"'
44     data = re.sub(pattern, repl, data)
45     with open('config.py', 'w') as f:
46         f.writelines(data)
47
48 def disable_in_config(key, value):
49     '''write a key/value to the local config.py, then comment it out'''
50     with open('config.py', 'r') as f:
51         data = f.read()
52     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
53     repl = '\n#' + key + ' = "' + value + '"'
54     data = re.sub(pattern, repl, data)
55     with open('config.py', 'w') as f:
56         f.writelines(data)
57
58
59 def genpassword():
60     '''generate a random password for when generating keys'''
61     h = hashlib.sha256()
62     h.update(os.urandom(16)) # salt
63     h.update(bytes(socket.getfqdn()))
64     return h.digest().encode('base64').strip()
65
66
67 def genkey(keystore, repo_keyalias, password, keydname):
68     '''generate a new keystore with a new key in it for signing repos'''
69     logging.info('Generating a new key in "' + keystore + '"...')
70     common.write_password_file("keystorepass", password)
71     common.write_password_file("keypass", password)
72     p = FDroidPopen(['keytool', '-genkey',
73                 '-keystore', keystore, '-alias', repo_keyalias,
74                 '-keyalg', 'RSA', '-keysize', '4096',
75                 '-sigalg', 'SHA256withRSA',
76                 '-validity', '10000',
77                 '-storepass:file', config['keystorepassfile'],
78                 '-keypass:file', config['keypassfile'],
79                 '-dname', keydname])
80     # TODO keypass should be sent via stdin
81     if p.returncode != 0:
82         raise BuildException("Failed to generate key", p.stdout)
83     # now show the lovely key that was just generated
84     p = FDroidPopen(['keytool', '-list', '-v',
85                      '-keystore', keystore, '-alias', repo_keyalias,
86                      '-storepass:file', config['keystorepassfile']])
87     logging.info(p.stdout.strip() + '\n\n')
88
89
90 def main():
91
92     global options, config
93
94     # Parse command line...
95     parser = OptionParser()
96     parser.add_option("-v", "--verbose", action="store_true", default=False,
97                       help="Spew out even more information than normal")
98     parser.add_option("-q", "--quiet", action="store_true", default=False,
99                       help="Restrict output to warnings and errors")
100     parser.add_option("-d", "--distinguished-name", default=None,
101                       help="X.509 'Distiguished Name' used when generating keys")
102     parser.add_option("--keystore", default=None,
103                       help="Path to the keystore for the repo signing key")
104     parser.add_option("--repo-keyalias", default=None,
105                       help="Alias of the repo signing key in the keystore")
106     (options, args) = parser.parse_args()
107
108     # find root install prefix
109     tmp = os.path.dirname(sys.argv[0])
110     if os.path.basename(tmp) == 'bin':
111         prefix = os.path.dirname(tmp)
112         examplesdir = prefix + '/share/doc/fdroidserver/examples'
113     else:
114         # we're running straight out of the git repo
115         prefix = tmp
116         examplesdir = prefix + '/examples'
117
118     fdroiddir = os.getcwd()
119
120     if not os.path.exists('config.py') and not os.path.exists('repo'):
121         # 'metadata' and 'tmp' are created in fdroid
122         os.mkdir('repo')
123         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
124         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
125         os.chmod('config.py', 0o0600)
126     else:
127         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
128         logging.info('Try running `fdroid init` in an empty directory.')
129         sys.exit()
130
131     # now that we have a local config.py, read configuration...
132     config = common.read_config(options)
133
134     # track down where the Android SDK is
135     if os.path.isdir(config['sdk_path']):
136         logging.info('Using "' + config['sdk_path'] + '" for the Android SDK')
137         sdk_path = config['sdk_path']
138     elif 'ANDROID_HOME' in os.environ.keys():
139         sdk_path = os.environ['ANDROID_HOME']
140     else:
141         default_sdk_path = '/opt/android-sdk'
142         while True:
143             s = raw_input('Enter the path to the Android SDK (' + default_sdk_path + '): ')
144             if re.match('^\s*$', s) != None:
145                 sdk_path = default_sdk_path
146             else:
147                 sdk_path = s
148             if os.path.isdir(os.path.join(sdk_path, 'build-tools')):
149                 break
150             else:
151                 logging.info('"' + s + '" does not contain the Android SDK! Try again...')
152     if os.path.isdir(sdk_path):
153         write_to_config('sdk_path', sdk_path)
154
155     # try to find a working aapt, in all the recent possible paths
156     build_tools = os.path.join(sdk_path, 'build-tools')
157     aaptdirs = []
158     aaptdirs.append(os.path.join(build_tools, config['build_tools']))
159     aaptdirs.append(build_tools)
160     for f in sorted(os.listdir(build_tools), reverse=True):
161         if os.path.isdir(os.path.join(build_tools, f)):
162             aaptdirs.append(os.path.join(build_tools, f))
163     for d in aaptdirs:
164         if os.path.isfile(os.path.join(d, 'aapt')):
165             aapt = os.path.join(d, 'aapt')
166             break
167     if os.path.isfile(aapt):
168         dirname = os.path.basename(os.path.dirname(aapt))
169         if dirname == 'build-tools':
170             # this is the old layout, before versioned build-tools
171             write_to_config('build_tools', '')
172         else:
173             write_to_config('build_tools', dirname)
174
175     # track down where the Android NDK is
176     ndk_path = '/opt/android-ndk'
177     if os.path.isdir(config['ndk_path']):
178         ndk_path = config['ndk_path']
179     elif 'ANDROID_NDK' in os.environ.keys():
180         logging.info('using ANDROID_NDK')
181         ndk_path = os.environ['ANDROID_NDK']
182     if os.path.isdir(ndk_path):
183         write_to_config('ndk_path', ndk_path)
184     # the NDK is optional so we don't prompt the user for it if its not found
185
186     # find or generate the keystore for the repo signing key. First try the
187     # path written in the default config.py.  Then check if the user has
188     # specified a path from the command line, which will trump all others.
189     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.  If
190     # keystore is set to NONE, that means that Java will look for keys in a
191     # Hardware Security Module aka Smartcard.
192     keystore = config['keystore']
193     if options.keystore:
194         keystore = os.path.abspath(options.keystore)
195         if options.keystore == 'NONE':
196             keystore = options.keystore
197         else:
198             keystore = os.path.abspath(options.keystore)
199             if not os.path.exists(keystore):
200                 logging.info('"' + keystore
201                              + '" does not exist, creating a new keystore there.')
202     write_to_config('keystore', keystore)
203     repo_keyalias = None
204     if options.repo_keyalias:
205         repo_keyalias = options.repo_keyalias
206         write_to_config('repo_keyalias', repo_keyalias)
207     if options.distinguished_name:
208         keydname = options.distinguished_name
209         write_to_config('keydname', keydname)
210     if keystore == 'NONE': # we're using a smartcard
211         write_to_config('repo_keyalias', '1')  # seems to be the default
212         disable_in_config('keypass', 'never used with smartcard')
213         write_to_config('smartcardoptions',
214                         ('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
215                          + '-providerClass sun.security.pkcs11.SunPKCS11 '
216                          + '-providerArg opensc-fdroid.cfg'))
217         # find opensc-pkcs11.so
218         if not os.path.exists('opensc-fdroid.cfg'):
219             if os.path.exists('/usr/lib/opensc-pkcs11.so'):
220                 opensc_so = '/usr/lib/opensc-pkcs11.so'
221             elif os.path.exists('/usr/lib64/opensc-pkcs11.so'):
222                 opensc_so = '/usr/lib64/opensc-pkcs11.so'
223             else:
224                 files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so')
225                 if len(files) > 0:
226                     opensc_so = files[0]
227             with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f:
228                 opensc_fdroid = f.read()
229             opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid,
230                                    flags=re.MULTILINE)
231             with open('opensc-fdroid.cfg', 'w') as f:
232                 f.write(opensc_fdroid)
233     elif not os.path.exists(keystore):
234         # no existing or specified keystore, generate the whole thing
235         keystoredir = os.path.dirname(keystore)
236         if not os.path.exists(keystoredir):
237             os.makedirs(keystoredir, mode=0o700)
238         password = genpassword()
239         write_to_config('keystorepass', password)
240         write_to_config('keypass', password)
241         if options.repo_keyalias == None:
242             repo_keyalias = socket.getfqdn()
243             write_to_config('repo_keyalias', repo_keyalias)
244         if not options.distinguished_name:
245             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
246             write_to_config('keydname', keydname)
247         genkey(keystore, repo_keyalias, password, keydname)
248
249     logging.info('Built repo based in "' + fdroiddir + '"')
250     logging.info('with this config:')
251     logging.info('  Android SDK:\t\t\t' + sdk_path)
252     logging.info('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
253     logging.info('  Android NDK (optional):\t' + ndk_path)
254     logging.info('  Keystore for signing key:\t' + keystore)
255     if repo_keyalias != None:
256         logging.info('  Alias for key in store:\t' + repo_keyalias)
257     logging.info('\nTo complete the setup, add your APKs to "' +
258           os.path.join(fdroiddir, 'repo') + '"' +
259 '''
260 then run "fdroid update -c; fdroid update".  You might also want to edit
261 "config.py" to set the URL, repo name, and more.  You should also set up
262 a signing key.
263
264 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
265 and https://f-droid.org/manual/fdroid.html#Signing
266 ''')