From bca07f794f5fbc4f61bc29465bf611aa68257301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Tue, 19 Sep 2017 16:03:11 +0200 Subject: [PATCH] added functions for storing/loading signer fingerprints to stats --- fdroidserver/common.py | 29 ++++++++ fdroidserver/publish.py | 91 +++++++++++++++++++++++- tests/dummy-keystore.jks | Bin 0 -> 10349 bytes tests/publish.TestCase | 147 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 tests/dummy-keystore.jks create mode 100755 tests/publish.TestCase diff --git a/fdroidserver/common.py b/fdroidserver/common.py index ae7f6936..5914109c 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -36,6 +36,7 @@ import socket import base64 import zipfile import tempfile +import json import xml.etree.ElementTree as XMLElementTree from binascii import hexlify @@ -2552,6 +2553,34 @@ def get_certificate(certificate_file): return encoder.encode(cert) +def load_stats_fdroid_signing_key_fingerprints(): + """Load list of signing-key fingerprints stored by fdroid publish from file. + + :returns: list of dictionanryies containing the singing-key fingerprints. + """ + jar_file = os.path.join('stats', 'publishsigkeys.jar') + if not os.path.isfile(jar_file): + return {} + cmd = [config['jarsigner'], '-strict', '-verify', jar_file] + p = FDroidPopen(cmd, output=False) + if p.returncode != 4: + raise FDroidException("Signature validation of '{}' failed! " + "Please run publish again to rebuild this file.".format(jar_file)) + + jar_sigkey = apk_signer_fingerprint(jar_file) + repo_key_sig = config.get('repo_key_sha256') + if repo_key_sig: + if jar_sigkey != repo_key_sig: + raise FDroidException("Signature key fingerprint of file '{}' does not match repo_key_sha256 in config.py (found fingerprint: '{}')".format(jar_file, jar_sigkey)) + else: + logging.warning("repo_key_sha256 not in config.py, setting it to the signature key fingerprint of '{}'".format(jar_file)) + config['repo_key_sha256'] = jar_sigkey + write_to_config(config, 'repo_key_sha256') + + with zipfile.ZipFile(jar_file, 'r') as f: + return json.loads(str(f.read('publishsigkeys.json'), 'utf-8')) + + def write_to_config(thisconfig, key, value=None, config_file=None): '''write a key/value to the local config.py diff --git a/fdroidserver/publish.py b/fdroidserver/publish.py index bd3000bf..c1c5b098 100644 --- a/fdroidserver/publish.py +++ b/fdroidserver/publish.py @@ -24,14 +24,17 @@ import shutil import glob import hashlib from argparse import ArgumentParser +from collections import OrderedDict import logging from gettext import ngettext +import json +import zipfile from . import _ from . import common from . import metadata from .common import FDroidPopen, SdkToolsPopen -from .exception import BuildException +from .exception import BuildException, FDroidException config = None options = None @@ -49,6 +52,92 @@ def publish_source_tarball(apkfilename, unsigned_dir, output_dir): logging.debug('...no source tarball for %s', apkfilename) +def key_alias(appid, resolve=False): + """Get the alias which which F-Droid uses to indentify the singing key + for this App in F-Droids keystore. + """ + if config and 'keyaliases' in config and appid in config['keyaliases']: + # For this particular app, the key alias is overridden... + keyalias = config['keyaliases'][appid] + if keyalias.startswith('@'): + m = hashlib.md5() + m.update(keyalias[1:].encode('utf-8')) + keyalias = m.hexdigest()[:8] + return keyalias + else: + m = hashlib.md5() + m.update(appid.encode('utf-8')) + return m.hexdigest()[:8] + + +def read_fingerprints_from_keystore(): + """Obtain a dictionary containing all singning-key fingerprints which + are managed by F-Droid, grouped by appid. + """ + env_vars = {'LC_ALL': 'C', + 'FDROID_KEY_STORE_PASS': config['keystorepass'], + 'FDROID_KEY_PASS': config['keypass']} + p = FDroidPopen([config['keytool'], '-list', + '-v', '-keystore', config['keystore'], + '-storepass:env', 'FDROID_KEY_STORE_PASS'], + envs=env_vars, output=False) + if p.returncode != 0: + raise FDroidException('could not read keysotre {}'.format(config['keystore'])) + + realias = re.compile('Alias name: (?P.+)\n') + resha256 = re.compile('\s+SHA256: (?P[:0-9A-F]{95})\n') + fps = {} + for block in p.output.split(('*' * 43) + '\n' + '*' * 43): + s_alias = realias.search(block) + s_sha256 = resha256.search(block) + if s_alias and s_sha256: + sigfp = s_sha256.group('sha256').replace(':', '').lower() + fps[s_alias.group('alias')] = sigfp + return fps + + +def sign_sig_key_fingerprint_list(jar_file): + """sign the list of app-signing key fingerprints which is + used primaryily by fdroid update to determine which APKs + where built and signed by F-Droid and which ones were + manually added by users. + """ + cmd = [config['jarsigner']] + cmd += '-keystore', config['keystore'] + cmd += '-storepass:env', 'FDROID_KEY_STORE_PASS' + cmd += '-digestalg', 'SHA1' + cmd += '-sigalg', 'SHA1withRSA' + cmd += jar_file, config['repo_keyalias'] + if config['keystore'] == 'NONE': + cmd += config['smartcardoptions'] + else: # smardcards never use -keypass + cmd += '-keypass:env', 'FDROID_KEY_PASS' + env_vars = {'FDROID_KEY_STORE_PASS': config['keystorepass'], + 'FDROID_KEY_PASS': config['keypass']} + p = common.FDroidPopen(cmd, envs=env_vars) + if p.returncode != 0: + raise FDroidException("Failed to sign '{}'!".format(jar_file)) + + +def store_stats_fdroid_signing_key_fingerprints(appids, indent=None): + """Store list of all signing-key fingerprints for given appids to HD. + This list will later on be needed by fdroid update. + """ + if not os.path.exists('stats'): + os.makedirs('stats') + data = OrderedDict() + fps = read_fingerprints_from_keystore() + for appid in sorted(appids): + alias = key_alias(appid) + if alias in fps: + data[appid] = {'signer': fps[key_alias(appid)]} + + jar_file = os.path.join('stats', 'publishsigkeys.jar') + with zipfile.ZipFile(jar_file, 'w', zipfile.ZIP_DEFLATED) as jar: + jar.writestr('publishsigkeys.json', json.dumps(data, indent=indent)) + sign_sig_key_fingerprint_list(jar_file) + + def main(): global config, options diff --git a/tests/dummy-keystore.jks b/tests/dummy-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..df6ec538c883cf350511e3151f466a871321a50a GIT binary patch literal 10349 zcma*tQ*b8h)&SsqF(wn+Hon--#P(!j+Y?SQv2EMV#I|kQw(-x|=UnW6*RFH+MPID$ zs?}ZZ>U!5xtNZ=z{R{*I1oZa-@%sRPKhz{rqQZAwf-wt5W zEA;A5s=DgzpvXgT7y#}=gmWlt z8|He9Se>I`i&rY6%=I=1hQ65GHt|h(sW5<>ie&E`&puX$Z|FT?FhgFPWSRv( zceXm5Iz!2r)P?y|$>Dl|H<6egk;y$cFEe}->J6G(o`>yn>F7(vIur{+1Dip6d1$nj zx&ywCJ^QKnbC@#3L+zgq&2LSIlT$yoShyk{%j?>+C1^=291glt9)>lu|nvK-MOFqAWgez}c_trSna$0=*xq@_yIO_|odm#&k$A zi#4gtRMl(e&LzJBRSy(b|t#VPnq7%NF{xeNsz+OcZ(Sz zzfgV0P9R>1yk|{K>vQYVQeO*w<#rhCm>K$+W7S$CmBk#33OBiL8zbw9WP4hkQ}MDi zD2*1_D?DP%d9G}ST-37rbQ^f1(1jzMHM@03s6w06sRv#4*_>~hbtMvtzEOq@F#sjokQ9xU3`QW9TWQD{= z#^goUwGLGmSw?w|g577qH9z*zcQ>>OU*{?b8+vOmBE@`WhSwYD=LyY^d`VlV4sdgQNKYHl z?Mg0v<#Y=rUk(r&W-jY_2Z+^tn;)V7$)efXq2=lvujjr$~#?g zFg^t59HH(Cs7)qc%5gdhKMaB$vnv-+r^NWuy{Lan)4*k zg0Jm{e8&z~7n3t^T1+)q7ovVIV1S522#%xN7EEX3lI-}rr#=%_ArDC+P7eI_V`D>C z&v=jHus#{7;1K%U_ml!%2ieGBY_OEY?0I~~yKb#9^-39MuD9r5+!r4!VwZx=ey4%~ zBx@t3$kJ>QS>}Of6Ovv**YQ|<_FI!6;&E+c|%@O&SkN$*;i=whFasp}zJV^FX zyj!heIddYl{S z00fXr#7Mx1_j~2>K7*r!qxR{C%Ta2l!KF7Xmmh*YV1e?6^D%uggpLc)|VFyy2WO-Y`KzS@e zlI{?Dri4Oj_KY|rT%4pl*^;oCRPHrR5#P;VU+1dj&G zLhaHgK|8A)vyz&xhBH#Xt^gLy+6`db9m8cXUs^EZ)8{bgs zyX=|S@>Y(d5+Jr-*?oXCQMpMys3r z7fz+e{2GN}bN$|)Mt+w2Jl2f47&Sl0WtfNpphr?PQHUzx*HXTqy-VuvNkH>@&l(dQ9qI`JZ~0 z(CIsU`852}LTuX#P3@jsGD%`H=bAv~x<#Ey+RA`G@H+ewWAVmJY8l5h? z3>8(FZ&Q_vJd+E7_PU`f2!0+D(6r;w7v)}2+xIK6EE3}mIH0qx>TkNJ8$tGf;2U&G zy)ynF87I#EsN~l#cTT6LxojoG62VU@1E-=o)0x`=o1iPcGWkBz48~7w1YAktEQPq5 zpEBesO7PrG;x9`}QHY0?#)9K)8?66Ddms%Iuie`|1_pQA_M3U@79CPy`)k%NBKINl zA);8um!4Q*;Ecal_ww2WzLU6Ij&vVGSMn7FOMkwK5Hl=pqv zQVDww2kW~^&)C10h<}6q%JV7YBnJMS!ce~11dEWqX?t%5LA}Z9IBVmMiK~oIexEQ% zSo+9$hA<+HLJJ8@D-ewV;RJOD(9#?J<)Vdixi!8)OXxO~4|M#j8CceKzyS|%ldx_7$<=Du3GwN9~9+fkrN z-a-S1Lda68rw@{V;(7cT?E8a2j=}K9fA;`ga~arQI?6w3=70782N3vY5Bw|qCn^4y z2MRzwBJya_*4G*ONBR&BCt@F9nTD#YaQqfv#hF*a@_n7idxF`HbtN#?ic-Bo4nPNN zo~FlRYwJ1FdG04`B8nr=ulc?Mk!Y-+R(!80>wDnf1NU*F*t~ za`LULc%Q=M!HA0NFKLRU#!)4*9&rMf)T~reiK;>|p*%iQ)8m<(RSm;q74SIZn00Av z-GdQk3dEOy(?efR6vDr~O^8Fm$hc+}6DypsEN-6IIL(#p^0s0wRzjg)Rxq2pk(bi0 zQTbum?E1@uMlz9Cd^0SAWY#_|D!+dWK9Gtz%I?z%twXBoApcVO9#t>E+9{yqjPQ3J zzzIBtr5o6I7-5F8!uK)%AcEZR|4a4$@Bo$^0+|YXPCtgU;bMGY(5@>P+O^z|EWDNh zR3rCMdy*;9N0c3hMp6G3IldmsXDlDg(?u2EORJqV9%?|&z|K<_lJ^~3m;@uFCUAgv zx%%YO=P$&R>7{;1Cb$TRj(dHbLT9Kz(v{2a=cIC!{tFzUt%($fP3jq6lW^;MI1?pcXvp7MW^E9zowsXAH_`L_FvBH%WXmM)2 zgLrOml@!4dQX*u#IyznXOZy_A6RSPgYd6DYwC7O!6c|Cy*`K@-$`;cTZMCwOor%Zrs4V@zc~Q*dpz**H{*d5 z!XG|vKSfXOPfa|f5o312H zF032Yhc6JXDl;Mj2BTHA2x`AdmvZplv4R*57@)=mhc0rKc#HS*SHz~gADc8_PCF(% zXsvYnGWy5KkavIXh$6N(;s+u^;hBhb!NybdP$(DB^uh)z z!N6Xma07l{PlwWO9nLgK?l{-iwboCl!uH19m%?GqRGrb>UIBpQtawNQlp_g|ZpcVu zn+|h`bQjzVq-d=1{^TG~14}MAXeiY(Q$*4(zCu;MXV=5;o7?s$q)xG;We7hrAT?ur z8iyw*>z_1?YI>*&qRDW|gELHFh_SFxSJ?DjC`kRemFB}UjsvR4`cY4}H56wy5ix}+ zywY{dT+lR91zG0$_3r8ul&TW)@#d^~;!K1(dA4a@pR9b2Iu^Ve;xNCXva0bOFH7Cy z2VQVvn{9-E@Cmjec8~g z84W}>w0+u13yMIz_`J*-r+6z>-*3?9P{&?wz1PUtMH1i8D$l+PwwbXZYLaeXhVvz_(S?2|)52tN(dc?L4f7l#UW(lRw57 z3SdXGjB8*1%vz*6W-|T0R|*O%$BjoaA>+yoV*c!6Iq!AWML^gx+ZuMjC86Af%OL;)tAy-AFDC;ADu1Anup0| z>j^9IZICflE2NIK^KrFI_giZVW8NUrvu0y&8d)SpB}0$B--f3i(<)YerLaFIw_B|! zwbvk(gh*!nLU&i6LfuiAO)hk4>nz(+dR|3Ol13Hb5Bo4vt$rlz2bwh% zyWPdCUcR`fT(fPQcHZ18;Ho1G*7YKH9eg7Ltx~soM}Ip0P|zQhQD|v3m&B<0oN+d#tDIA{K{S*SI*~*0&h!%%Crf<-Uru17)m! zDv>bk3$F`R2hOEaCrIJ&=Uh_ZAFbawN<-|TTM4sdW z$ow52@l#w@_g7#bsE6k--2{522x)R8=jcw{?6iyw?82RW<-WiL*Q^_f!#c$^8!#+GHsW~92;aCT@ z?n6FiHyTzsGfDdPwXV~}usz|bh3Rb7c!GD94L|Q*6;mLf3@lJHkL9$S0ly^ZMaQ$E z68$3rtL;qqa2`&7J?-FzQvW=&77bxUDHP$4w@o9nRSpHqloeA+1bR`}u0e|bq;y&W z+2in>UFR%6iPCK$PgC@E%vn{1rA-vIv!@-3t>)NwLCe<=P3%ppXgGHFAqaRhQDfD2!2#_)Xik^P(`|2s)dw-i_5B)?D^0^B4 zmS^`WN#EQKMe86Al}P{YZtkoE>fiGLLjz`gARDLAzvlx)jK4tu_zi-$zX8F~x{I`4 z82Nb;ip}w}FjMn`k*U{c!96us&&&O7`S4eJl7L*wPg1@uk)6C%Ms7b}?V}92_%HPw zbZp)nd4qIQ7O@8m>50syGrtU_EUQsh-r+%@mAbQ0S?>&g-+;}!a38y`i2hWq+W9(b z-x>Rp4GwW#?(E>s;##F6cuT)1O*)u{nWF?qtKubL-3v-`5W|bT+e4+O?x$RsmZZ^o zcq`H{ajk}FuXNbnrjMX(w95V1oX{u|_^!n5sSL8%UvCO3y9u3q9eLKR(VS=9#WZnu zv{c^r;LOmxkt{)RHxYQs1I@ubGPa}*YY@HW3DzeW&}57pMv}&|7_O3HBGlx>=S_XA z1-BudXEuyaxBF`m()Q>sswZj)vb!~_+0*{3V+3Y{KT97r#_JKhO}YkMe3G)x&vnDl z_{pQ$lmgHTSH9XtU-QhVjH(5#8Syqp%PG1X(A&6=J3Ky^9?HaZS?U;!!9sLi2aA%i z@pnd92p$j$>408hg%#C4%(2d1Qdnt`pC)szdq)2IXMtR4BCXNu+hD9UbF~7wQZyl3 zIETnm41nqE*hQ58n88bnbY(^15OZjH{+blKD~wW$ASZbx#6Sq zi>XHOOQJn-So#M5a+F2FQ4}5}ipDf(%lpiUEP>fP0PZUVY~jFUyYn6K+L?CdXqCRc z_&@-}rZDyfi%6j?bi(#EF`rpPS>-*u*5ap`R9hY28LMz5YoHKhk8CB!QCv~Bj|-th zrm^uFNRj0dyer=$VO#Z-_kH`7qZ_y0*&A!EgTIimzxxRN* z!7mv1taFAKbQ@c3hSn^KrC+tlrsYDK5RJzHN8UDB3rx|;9;o8GL1+;h9pJAp%8heU zjiGUEPpLMTqmPINTo2v=rviStfp5R=kCxP6E*iapjy^wxv=dV0y#iM;v(C`y4y5h( zg#7C12(owZgxIgoy#!#M>ekmPJaJ^&4evF-{2cnKA8eAHO?HZ0mU&4WPAD&k)EY_% zHlP&kT(6*`y~74UwRC=!_kOZa?n#-nqC3r9nV`NLyLb`v?L&=Mqq4N@`-m9#HWXR- z)rT7|0&yhJWG+|Iq;lmeZl5%I8xCiZBzq5~^Q$^WeHj^P=^*7Qu+wUn&56-GZ3Rci z&R<1@p#&D>CE(Zk%83Cz-_s+Kn>YSsVhykjk8{-@q{QBLG6b2fK{##SOO8O=I#+d zM{CsGbeC;`G>lq#P>c1Xl#6}o>Gr7zrdw;C8=!xj%&8 z#+rA|3|TQvjcK(_*P1gSmnl!Ng{H2G&)tTQ>`!bzd&Mj7e9d|A=1O zrz_u`ZW{5wb3n?HX8A7-<)8HO|2H0BWc+{Qf#2ajN%6lp&>Xa7dX`P>RW7>YsW1}s z;yn%~k`A&Mi*2T3yrsN}-Y)T;8J2Zz~~l@@-)JLZCmI&yc%KE z9NV3!ryd;4xs)Ts!Cm?>@cWZlY3)MVtU&HprDz3@&D&rmYfX6M`O4^N6I9qx_lcmL zj2UMq0Ac)JKQwkc+5lX+CF>Mz3V>d@(H(*(P6nyfWDJ zvlH3+)R)CkQsnnTuIllwQQ$f_T6(B8c12kmj^t8*xZ|eec1PhzkSP6I`ke%;O2({( zvw*X4sPBKbDfqha*A61^Q45nFFn3e0&H`#B= z5O=WG#=-#1Bi9|gxDF79h=C?{=cDUM}9EaL*)U^EMRGDEPpCR4)+Srm8jObARwzJ#y zhVKI`q3S;zU@~L{8XGhH!2!1O-#-`p{ubr~{`xl@I274~1HYtyL3ymUTM&bt9Kg!# z(`|yA5=RkoNh{Q0{y{Mc;K?+Y$(A6w~hUK4b~Qt3692 zTSsvh(C=V{C%H6#A=Exr339T&KW=B~2Ve%3?HH)E@k5{_Z8yR7dZJiom3K%uzY&kS zxx8A9prJFSHW{*dE(Pw!Z2RPEFN==JkgUQKtBN)M5F&xnBZ{!noM9pGsx22m!v=M0 z2hB;wW8$ne55$fd4mol%DiHJ-u;$O0LP@$L(!Q#&daiZyoEA*ZKN%E&Uc{F@y3cSR zlsU4%Z&J>CRBlt2(2;imkDYE@Urh=!;dzNzZ*Q$yO%m~oEIuEqBkqPPE)Mwt2MWp< z828F?C?k}z_g2P{=^UAAqS}rip4El?b|wkm7`qFRYbTZp-$!w*T_Hrp5kUa}=H7*_bfnndx3kf333F>uMdJVX8Oc!R=Dh4eh_{gsf$N3SX4OghDmL~V z`DY#+5Z+6LrOziC>HlI|* zCT5>8Vm1*)TXP2zZ~6mhFX~Z6O2hBKgc9>&jMSW?zDe)xF(u}0vAcq51#Q9rDQN5= z-|}1dzn&Ihj7BHtd)B6SIB*#)w(T|!s*9g{lU9mEx=Z;y|AfmfIw4e0OwzcQZ7=CS zB6%f5LM1u9o2T?GId?We?3c^Zu2<7!d_?Relns#(d*TgP9*mefM~iM;KHJrws`{E$ zh9>2djY$Y#O|ajWVAI)qr1Q&)ptZJz7wKGPiyx7u@N6dzGE%OJTdyRXaymThucN7I z=!yBb{sKR@vcLa$FW_E%>vgyARv1EaGuIWiC2#^Xyo+Rl(oW(B#e&diH#oE+C+yz` zTGyskig^}mS;ySU0U)(;Y`64Rx!FX*cbSnlT|ND+f}Tk9h3OHM4sCp9q^KIclFrs- z(;9M3O>?0VV`7yGnEIUtrA%trnaKuwOyAtK9@>bCMme7rgxyl;>dujl=-yh!7g`7i zCvF-gzK<+sEq_a5!$|fEc>JpE@5sO}!BAB45r0QQ?YrMOUtguc=I+hwSekB(#LvTn zdT%sknVlwppk0Z=$a}Za$9J!v6+$Hu?y8fA3wp7+Vg9~i-QozsT&EZW)UuRy8-$o? zcbH=Edw74B%vnPzG&e)xE!$e}(Pp=LY=a|3i>Tofb(dSFLYr8&{y72i7Sh_G)A1!y zl}us!6+TMWwz`j7r9v%ZBMk&p%ROuI9knh$9)+Gq!R+`tX1kDymX9s0ypGlGXds!m zc;lhkIqP!N#Nz>~7qOZZ2JuN7nsUQ&+iNeg+->)}BgD~yh=0m#6*iin^E-wY;i);8 zTOkGr1qVOlHdO%WGYI>MXunh|H|VH@K5t)CxZ>y41NPWW!hsqVd2ViS#HT4@U^ugX z&eW@NwEAXLLpVLdm#m0X3e{V%?^p>!cY(nRuV$vY2lcbRvM?=Yi$zfOXQnx`i{^Ze zO>LTQW*;KuuiTVZt2~~m-v{q6`nILSW{?$DBQz`w$OlHz}9AUBVlrxxfjuPt8u_a+IZ{P4zPJu(50w#ynKZ zwg`R!gmnHsn?_!-&Ou#;v!TGzx zKkpFrU>jwQBN6lA5{5D@1=0nfXOzA?2#v*)m0BW?>g7KZKuL(3m&gpwD_-sC2@-IN zTe!xcNGU1g4TemeaT94mRsNj@7E|50@=v$PBP)Kl1x#dXYcEU9zf|uJ4MZnsMyY;o zRfJhYcNhnK7KZT*5_g|-3Ku0VR>Qw36ZjfySxL})*HAxp*1-t_q!Ya}U35-Ho$oPj z%eP{R#97XkQUx2_v9Wa8n#5GcKg&EF^)>(W;yN@q8zoc|c#kR1oO+ji*_8-^F$JJJ zs>!LqV|qEC>z{9l=PpWOQ`aT1zq!K91C7u;15bYImOU5`NP`mkA<_Vj!7;X=hms;$ zDy;nBFMER!8fMQ*Wr*VFLVL}1hl#HcZ3U*yU5<+sA> O;?e7SrM9n(8~8t+`k0#l literal 0 HcmV?d00001 diff --git a/tests/publish.TestCase b/tests/publish.TestCase new file mode 100755 index 00000000..7a31d39e --- /dev/null +++ b/tests/publish.TestCase @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +# +# command which created the keystore used in this test case: +# +# $ for ALIAS in 'repokey a163ec9b d2d51ff2 dc3b169e 78688a0f'; \ +# do keytool -genkey -keystore dummy-keystore.jks \ +# -alias $ALIAS -keyalg 'RSA' -keysize '2048' \ +# -validity '10000' -storepass 123456 \ +# -keypass 123456 -dname 'CN=test, OU=F-Droid'; done +# + +import inspect +import optparse +import os +import sys +import unittest +import tempfile +import textwrap + +localmodule = os.path.realpath( + os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')) +print('localmodule: ' + localmodule) +if localmodule not in sys.path: + sys.path.insert(0, localmodule) + +from fdroidserver import publish +from fdroidserver import common +from fdroidserver.exception import FDroidException + + +class PublishTest(unittest.TestCase): + '''fdroidserver/publish.py''' + + def test_key_alias(self): + publish.config = {} + self.assertEqual('a163ec9b', publish.key_alias('com.example.app')) + self.assertEqual('d2d51ff2', publish.key_alias('com.example.anotherapp')) + self.assertEqual('dc3b169e', publish.key_alias('org.test.testy')) + self.assertEqual('78688a0f', publish.key_alias('org.org.org')) + + publish.config = {'keyaliases': {'yep.app': '@org.org.org', + 'com.example.app': '1a2b3c4d'}} + self.assertEqual('78688a0f', publish.key_alias('yep.app')) + self.assertEqual('1a2b3c4d', publish.key_alias('com.example.app')) + + def test_read_fingerprints_from_keystore(self): + common.config = {} + common.fill_config_defaults(common.config) + publish.config = common.config + publish.config['keystorepass'] = '123456' + publish.config['keypass'] = '123456' + publish.config['keystore'] = 'dummy-keystore.jks' + + expected = {'78688a0f': '277655a6235bc6b0ef2d824396c51ba947f5ebc738c293d887e7083ff338af82', + 'd2d51ff2': 'fa3f6a017541ee7fe797be084b1bcfbf92418a7589ef1f7fdeb46741b6d2e9c3', + 'dc3b169e': '6ae5355157a47ddcc3834a71f57f6fb5a8c2621c8e0dc739e9ddf59f865e497c', + 'a163ec9b': 'd34f678afbaa8f2fa6cc0edd6f0c2d1d2e2e9eb08bea521b24c740806016bff4', + 'repokey': 'c58460800c7b250a619c30c13b07b7359a43e5af71a4352d86c58ae18c9f6d41'} + result = publish.read_fingerprints_from_keystore() + self.maxDiff = None + self.assertEqual(expected, result) + + def test_store_and_load_fdroid_signing_key_fingerprints(self): + common.config = {} + common.fill_config_defaults(common.config) + publish.config = common.config + publish.config['keystorepass'] = '123456' + publish.config['keypass'] = '123456' + publish.config['keystore'] = os.path.join(os.getcwd(), + 'dummy-keystore.jks') + publish.config['repo_keyalias'] = 'repokey' + + appids = ['com.example.app', + 'net.unavailable', + 'org.test.testy', + 'com.example.anotherapp', + 'org.org.org'] + + with tempfile.TemporaryDirectory() as tmpdir: + orig_cwd = os.getcwd() + try: + os.chdir(tmpdir) + with open('config.py', 'w') as f: + pass + + publish.store_stats_fdroid_signing_key_fingerprints(appids, indent=2) + + self.maxDiff = None + expected = { + "com.example.anotherapp": { + "signer": "fa3f6a017541ee7fe797be084b1bcfbf92418a7589ef1f7fdeb46741b6d2e9c3" + }, + "com.example.app": { + "signer": "d34f678afbaa8f2fa6cc0edd6f0c2d1d2e2e9eb08bea521b24c740806016bff4" + }, + "org.org.org": { + "signer": "277655a6235bc6b0ef2d824396c51ba947f5ebc738c293d887e7083ff338af82" + }, + "org.test.testy": { + "signer": "6ae5355157a47ddcc3834a71f57f6fb5a8c2621c8e0dc739e9ddf59f865e497c" + } + } + self.assertEqual(expected, common.load_stats_fdroid_signing_key_fingerprints()) + + with open('config.py', 'r') as f: + self.assertEqual(textwrap.dedent('''\ + + repo_key_sha256 = "c58460800c7b250a619c30c13b07b7359a43e5af71a4352d86c58ae18c9f6d41" + '''), f.read()) + finally: + os.chdir(orig_cwd) + + def test_store_and_load_fdroid_signing_key_fingerprints_with_missmatch(self): + common.config = {} + common.fill_config_defaults(common.config) + publish.config = common.config + publish.config['keystorepass'] = '123456' + publish.config['keypass'] = '123456' + publish.config['keystore'] = os.path.join(os.getcwd(), + 'dummy-keystore.jks') + publish.config['repo_keyalias'] = 'repokey' + publish.config['repo_key_sha256'] = 'bad bad bad bad bad bad bad bad bad bad bad bad' + + with tempfile.TemporaryDirectory() as tmpdir: + orig_cwd = os.getcwd() + try: + os.chdir(tmpdir) + publish.store_stats_fdroid_signing_key_fingerprints({}, indent=2) + with self.assertRaises(FDroidException): + common.load_stats_fdroid_signing_key_fingerprints() + finally: + os.chdir(orig_cwd) + + +if __name__ == "__main__": + if os.path.basename(os.getcwd()) != 'tests' and os.path.isdir('tests'): + os.chdir('tests') + + parser = optparse.OptionParser() + parser.add_option("-v", "--verbose", action="store_true", default=False, + help="Spew out even more information than normal") + (common.options, args) = parser.parse_args(['--verbose']) + + newSuite = unittest.TestSuite() + newSuite.addTest(unittest.makeSuite(PublishTest)) + unittest.main()