Bug 1038068: Check add-on signatures and refuse to install unsigned or broken add-ons (preffed off for now). r=dveditz
☠☠ backed out by cac8a0359113 ☠ ☠
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 31 Mar 2015 11:32:40 -0700
changeset 240637 f996215427273ee9a9a7c7c36230a4c6b2777e87
parent 240636 9b6b694dd82e1a3f7652c35d3e8b264d701ec253
child 240638 8d3db3f40c6bb9919e483a3ffcd71818222e7ab7
push id28639
push usercbook@mozilla.com
push dateThu, 23 Apr 2015 13:34:21 +0000
treeherdermozilla-central@3574449bd314 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdveditz
bugs1038068
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1038068: Check add-on signatures and refuse to install unsigned or broken add-ons (preffed off for now). r=dveditz
CLOBBER
browser/app/profile/firefox.js
browser/locales/en-US/chrome/browser/browser.properties
layout/tools/reftest/runreftest.py
modules/libpref/init/all.js
security/apps/AppTrustDomain.cpp
security/apps/addons-public.crt
security/apps/addons-stage.crt
security/apps/gen_cert_header.py
security/apps/moz.build
security/manager/ssl/public/nsIX509CertDB.idl
testing/profiles/prefs_general.js
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/internal/moz.build
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/install.rdf
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/test.txt
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/install.rdf
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/test.txt
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/install.rdf
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/test.txt
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/install.rdf
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/test.txt
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/preliminary_bootstrap_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_1.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_badid_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_badid_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_bootstrap_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_nonbootstrap_2.xpi
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js
toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,13 +17,12 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1155718: Merge Bluetooth v1/v2 files for all simple cases
+Bug 1038068: Check add-on signatures and refuse to install unsigned or broken add-ons
 
-This patch set renames source files. This requires updating the
-build system's dependency information from scratch. The issue has
-been reported in bug 1154232.
+Not sure why this needs a clobber but tests perma-failed when they don't on
+try.
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -64,16 +64,19 @@ pref("extensions.update.autoUpdateDefaul
 pref("extensions.hotfix.id", "firefox-hotfix@mozilla.org");
 pref("extensions.hotfix.cert.checkAttributes", true);
 pref("extensions.hotfix.certs.1.sha1Fingerprint", "91:53:98:0C:C1:86:DF:47:8F:35:22:9E:11:C9:A7:31:04:49:A1:AA");
 
 // Disable add-ons that are not installed by the user in all scopes by default.
 // See the SCOPE constants in AddonManager.jsm for values to use here.
 pref("extensions.autoDisableScopes", 15);
 
+// Don't require signed add-ons by default
+pref("xpinstall.signatures.required", false);
+
 // Dictionary download preference
 pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
 
 // At startup, should we check to see if the installation
 // date is older than some threshold
 pref("app.update.checkInstallTime", true);
 
 // The number of days a binary is permitted to be old without checking is defined in
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -64,30 +64,32 @@ addonwatch.restart.accesskey=R
 # Semicolon-separated list of plural forms. See:
 # http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 first add-on's name, #2 number of add-ons, #3 application name
 addonsInstalled=#1 has been installed successfully.;#2 add-ons have been installed successfully.
 addonsInstalledNeedsRestart=#1 will be installed after you restart #3.;#2 add-ons will be installed after you restart #3.
 addonInstallRestartButton=Restart Now
 addonInstallRestartButton.accesskey=R
 
-# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4):
+# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4, addonError-5):
 # #1 is the add-on name, #2 is the host name, #3 is the application name
 # #4 is the application version
 addonError-1=The add-on could not be downloaded because of a connection failure on #2.
 addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected.
 addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt.
 addonError-4=#1 could not be installed because #3 cannot modify the needed file.
+addonError-5=#3 has prevented this site from installing an unverified add-on.
 
-# LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonErrorIncompatible, addonErrorBlocklisted):
+# LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonLocalError-5, addonErrorIncompatible, addonErrorBlocklisted):
 # #1 is the add-on name, #3 is the application name, #4 is the application version
 addonLocalError-1=This add-on could not be installed because of a filesystem error.
 addonLocalError-2=This add-on could not be installed because it does not match the add-on #3 expected.
 addonLocalError-3=This add-on could not be installed because it appears to be corrupt.
 addonLocalError-4=#1 could not be installed because #3 cannot modify the needed file.
+addonLocalError-5=This add-on could not be installed because it has not been verified.
 addonErrorIncompatible=#1 could not be installed because it is not compatible with #3 #4.
 addonErrorBlocklisted=#1 could not be installed because it has a high risk of causing stability or security problems.
 
 # LOCALIZATION NOTE (deveditionTheme.name): This should be nearly the brand name for aurora.
 # See browser/branding/aurora/locales/*/brand.properties
 deveditionTheme.name=Developer Edition
 
 # LOCALIZATION NOTE (lwthemeInstallRequest.message): %S will be replaced with
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -218,16 +218,18 @@ class RefTest(object):
     prefs['browser.snippets.firstrunHomepage.enabled'] = False
     # And for useragent updates.
     prefs['general.useragent.updates.enabled'] = False
     # And for webapp updates.  Yes, it is supposed to be an integer.
     prefs['browser.webapps.checkForUpdates'] = 0
     # And for about:newtab content fetch and pings.
     prefs['browser.newtabpage.directory.source'] = 'data:application/json,{"reftest":1}'
     prefs['browser.newtabpage.directory.ping'] = ''
+    # Allow unsigned add-ons
+    prefs['xpinstall.signatures.required'] = False
 
     #Don't use auto-enabled e10s
     prefs['browser.tabs.remote.autostart.1'] = False
     prefs['browser.tabs.remote.autostart.2'] = False
     if options.e10s:
       prefs['browser.tabs.remote.autostart'] = True
 
     for v in options.extraPrefs:
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4103,16 +4103,18 @@ pref("html5.flushtimer.subsequentdelay",
 // Push/Pop/Replace State prefs
 pref("browser.history.allowPushState", true);
 pref("browser.history.allowReplaceState", true);
 pref("browser.history.allowPopState", true);
 pref("browser.history.maxStateObjectSize", 655360);
 
 // XPInstall prefs
 pref("xpinstall.whitelist.required", true);
+// Only Firefox requires add-on signatures
+pref("xpinstall.signatures.required", false);
 pref("extensions.alwaysUnpack", false);
 pref("extensions.minCompatiblePlatformVersion", "2.0");
 
 pref("network.buffer.cache.count", 24);
 pref("network.buffer.cache.size",  32768);
 
 // Desktop Notification
 pref("notification.feature.enabled", false);
--- a/security/apps/AppTrustDomain.cpp
+++ b/security/apps/AppTrustDomain.cpp
@@ -18,16 +18,19 @@
 #include "marketplace-prod-reviewers.inc"
 #include "marketplace-dev-public.inc"
 #include "marketplace-dev-reviewers.inc"
 #include "marketplace-stage.inc"
 #include "xpcshell.inc"
 // Trusted Hosted Apps Certificates
 #include "manifest-signing-root.inc"
 #include "manifest-signing-test-root.inc"
+// Add-on signing Certificates
+#include "addons-public.inc"
+#include "addons-stage.inc"
 
 using namespace mozilla::pkix;
 
 #ifdef PR_LOGGING
 extern PRLogModuleInfo* gPIPNSSLog;
 #endif
 
 static const unsigned int DEFAULT_MIN_RSA_BITS = 2048;
@@ -88,16 +91,26 @@ AppTrustDomain::SetTrustedRoot(AppTruste
       trustedDER.len = mozilla::ArrayLength(trustedAppPublicRoot);
       break;
 
     case nsIX509CertDB::TrustedHostedAppTestRoot:
       trustedDER.data = const_cast<uint8_t*>(trustedAppTestRoot);
       trustedDER.len = mozilla::ArrayLength(trustedAppTestRoot);
       break;
 
+    case nsIX509CertDB::AddonsPublicRoot:
+      trustedDER.data = const_cast<uint8_t*>(addonsPublicRoot);
+      trustedDER.len = mozilla::ArrayLength(addonsPublicRoot);
+      break;
+
+    case nsIX509CertDB::AddonsStageRoot:
+      trustedDER.data = const_cast<uint8_t*>(addonsStageRoot);
+      trustedDER.len = mozilla::ArrayLength(addonsStageRoot);
+      break;
+
     default:
       PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
       return SECFailure;
   }
 
   mTrustedRoot = CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
                                          &trustedDER, nullptr, false, true);
   if (!mTrustedRoot) {
new file mode 100644
index 0000000000000000000000000000000000000000..6ab711b996fc25b144995af21726b08bc399996d
GIT binary patch
literal 1637
zc$_n6VoNk=V)0zS%*4pV#K>sC%f_kI=F#?@mywZ&mBFCaklTQhjX9KsO_(V(*ignm
z3dG?O7WU1r%FM}0RB+BOD#$NNEXmBzGt@WG1<7&?tHER)ef<>zit<xRlR*j;f-}?e
zGV{_Ef>VpiGLusc<qc#(8kmK}it_VIbdwWx3*d@%6La$o<ivRmO%03<&5evqjZMv?
z#CeTCToWjFpq<ymq<|b?jI0dIO-#HDKyePHCMHIPExQx1$L{)Yx^ADr_pbUG4;HZW
z)Eq8;mlVeOoU`d?aD9in>ZZ>Izc~U|Z&tq8vSj=5(s>)s>a<$!V=#X3b+&+5+cVEQ
z2M(w_|IQwg^O8$jZfDJ^mA+d(uBw{((quaK;!QbTt6J_{RePVTu%hu|qe;$6Z@V{}
z_$A+cd!1@${^g6#H&NYp+UY6Ia&0qP+pM$i@A#H@PWgvfLO|VjwPfL^cbA2Hn;^mP
z;J<ACj@`=M9^GwU*qy6RUslwAFfnD$#**-D2V|D0)>i19h|@Z3nr-+v<z1D>;_T?7
z4yr~^mdZHIIx%^@zx<p(&01;m(j9U)y-+H=v9fSh_&O!FS;wN4V^=-b-q5SaUFLB0
zrQ71gng4EcTkfnnf4_J7`_M&E0{ctiV$TPi+GY?t)6+1Mna}?B!mWi|Y}+!fbuZ@?
zF}!B?&&$Pv#YdUFq-EQ_>%5751&>q~ObfGNn9s6dyUm2gD3b#xB=gE0cFr%@^lkeB
z2F`Ozi%hr2wFPHJxyjv|GVg({aDo#LckHtmaT%*ae-;UzZ0dO^@Zz&gAjjL5*Id)i
zoM{W%u+OGX!peA?*CclR_dmD|-#p#syz6P`r&(+Es=2tudK`aoy5@4zWl@f^`YkuB
z&Yr&iq}X@SES569qMcP{MPjAPgX&hFn|&d#NL5(QKYx*Bn3*8&?=4lY0$tJzbaw1!
zVq{+2_|~BDl>rYhA;}7}uo^HkGX6K<1M&EQEEX1EzGE{G1MyWsd>#WXHV$nzMpjmK
zW@dxNNgxSjmc}lF#`cAcjZ5kWIK?*sO9+q(Sw0pq7Lm<+UPY9wewbIGwQ2PnWw{l}
zI@7nK6(G!@Y>;tfVgg$W2YbIm%>2o*OQs*?*OOOdxiQy1@t3Au-=z;z*=*8`_Go|L
z+Wg|uKR<T=87cleM(6n^ob<iAxy<fK3&+QmX^OipF!{c*tW1<*wrN?rX~Oj9d0M}N
z+eFKjFRV%^D%<xu@4+IOj>lZ=$8)B1bvW%WQLLS^DdN>f>)4b3+gd$mom=#Ffh?QV
zLm{ChKX#S=ir;-n>TQN@?3V4jD=Re57&iQRJ0bmIlgQtb`+rSa{(6P`I<tMI4~~hS
zk;<79yJC?pr|!!;$)#r<*Qu;Jeaa^*=T-gfuulwG3h80X+U<H}-Q|T>Jn7!e^Je2?
zyKdd!X<?g-kJN@Hw62d&_3&HM@aANpCTD8Y{eDxu0IfS2Qw)|>@`faS|C~R&xnQmN
z!MmIS4ih%iR-gMeN8M(%hi9i`&<dATU!OcHNN~_(_So<^)~5VhStCo5sM3O5jnz~A
z!uD2GJ}6ta_qOPNF6LbhQ?n1vnk!{KyMOYZr=KHj7>a%09J}k8^E9gNk&;*lBlCo{
z+Y3Fu7k_*x{CLGXpZLJnJCnmg@2&cE{oUFW7O&pjTm7C1$o`ez{BVPUy>Z2Mvon)^
z2b;5t^9a1S9`)ss@1w^DcIY3}T%>tK>GbVo-yZ)C%*qo|vs&4;cFT0lhe10hZ@afJ
jC-=yes-n9)muyeCG4C<!0iUwQuV;(OE+4qPUTir4A9<cd
new file mode 100644
index 0000000000000000000000000000000000000000..73e48cadfe2da5373a20a88a54148dac695da7d4
GIT binary patch
literal 1895
zc$_n6Vox?`V)b6Y%*4pV#K>sC%f_kI=F#?@mywZ&mBFBKg(0^ACmVAp3!5-gXfTY!
zA<X3LXeeeN0^+a>^ZMqO=9MI7<|%|_rj{GZ7)XJ{xrBusQ&RHtiWNdqi%S%OGt=`j
z^U@7f43t2!%)-(oKw-T^uoAsuh^$^wetwBwa-yMufgao?oQz_M`31$P$=bywiRq~z
zB@kT>x%pL@IXQ`X$@#eka^k#(rUpia28O0a7N&+#;=D#cu7Np}JCwZA#H55A5sa)1
z%uP)E3_x)%rY0svhW(!>$;ssf9Olw`kodV$Y0{%Q);xI!#j9x_nA2oe$z1%iRINw&
zo0dVh|I-pv^I6=hqXQk@{bpGCud?|tBd5aetD!$nSM1(5qv*QGKkmn!54)V*-|sN|
z`jP2y+p--z70k0H%=*vYvf8}zL0N16_6=XeeoIb1zH|DMH`=wehpQ%3-V?GtRrTo6
zYLlrUDz4#If~Rej-FUV>oUwxW-bvqbbD8PWRNme?^U`(cq8f*aw8o;-GvsPzzR$ew
zGXE=&sN|wK^Y?V`=JsHI!`yaW=|Eao5$BS*dCZesoO8LX_22)0<m&aaYt@CaWhq?G
z_V^hJ+`fI_&Ao?ZDjaXmAL49Ubk$mW!uJhFMUFYoT6i;V#(Te&qI>pC?vyY4HA`f2
z*2@d-_m5f3do#U#!MfV{mSKr(pUu9-G;%D6uYIDhrCCO6dW+^dVRJckzpd-8%ung5
z%r|}NeJAG3nW-1;Vg<wZ#w4z^lU}&ox&6WYcaomUAN{_4aGAh&TWs+}mqJmsjE&{W
z3HeI4CaZbZJbtjb?Du)I?JZ{iHcKq?GB~ht_MTbo-Mnj??>}6!^Q(62)=%G=_Cy$)
zN?k5l*7;|};p^Kwr=PZ2@WI+oOS9$syz{zYXV@)f-VFC%VDogZnufuZ8~i`+b}rp!
zEi_B#;*Fj6D{h<Ko~Xl_6BM*TXRe!|^-E7{<?J_!T~f!HnAt+j7dJ6ZF=%3(V88=R
zgR;UbtOm@CjQ<VzKs<gRi-iT4r`Zg|KzvmYpT~fUjYFG_k(HI5nb|;xjWeOmgR$+0
z6C*E+yqK(iL26!bu#bX!YF=tlVo7R>f^%w7NoHDRGLU0nj8M(Oq9&%2QBqQ1rLPZ7
z@vyX;UzDz&oT#5%l%rRWnrk2nvYL-Yj76m7Rs7j}pGo?!E`IrXV)3R!JiC^CGibaD
zl2>MFJZsQ+ssXE-g^lZ$G_D>(S+@zJ2xSIk*dpJpt_x)R%`WfDT2-LFnf0DqmEGd-
zxyO~R-WT^<tF!08kwqfcqPXS%e!nbm*CJ<;>g_9x23O~w+V$)oSLWMU2Od|Qx;k_D
zwJ#gRbEoVUTzpW>swL-))*{37)N7o_43}0{=gc|(JMhOZE@{C-?Wb;AUEETo>Zz=f
z)U)C77N4*W9(KF!W=`Vuh^*?EXLoGX-t8i<n%}Q(sPQ*TG@W)Y=HrsT5A=7Xg-_hQ
zaQ|Cn1^G1}86WfVZcjWi{YJHkwU^DQ>@|<hPugT#!5ZTDyyLEvgI4J|QBEz}=IcrU
zQtGQVZf)DOHSyd^nMv1$mhabp7iFj$Tpgi4eTshkw*AJzr7Jm`+?-u6oNab3H|;Ub
zUQnQaR@7GgRdL`i_XAU|z4$lD<SB=@hR>&a(K{`-zx0v`R=!Z?ZLYb1$1eV>m&bdJ
zK8D7U^Q~PBr~lW-uAjsFVaDTkR+@q)ufNQ>-R;ulWj*&$`L?M~zB^u;9AtCj&dd0n
z3cv0xVLIz1)Un)(ow<wc$Ij4jJ;A%D`HSr2&a&7x-95{lJYBpazoPt9>V@U`4;-hr
z?7qq@k)SZS`XMvdA-3JB*Z4neYyEWiqw%$E!q=ZB=Xf9cFt2w`(aJj^TP$qn_$kPn
z^8~og;J&;=q9HMB&-<?Dm8P$h3O`k7?fV>``t97eCDyDN>Fa0Zifq1iW7qS`-ctb&
Cx%5H+
--- a/security/apps/gen_cert_header.py
+++ b/security/apps/gen_cert_header.py
@@ -30,13 +30,15 @@ array_names = [
   'marketplaceProdPublicRoot',
   'marketplaceProdReviewersRoot',
   'marketplaceDevPublicRoot',
   'marketplaceDevReviewersRoot',
   'marketplaceStageRoot',
   'trustedAppPublicRoot',
   'trustedAppTestRoot',
   'xpcshellRoot',
+  'addonsPublicRoot',
+  'addonsStageRoot',
 ]
 
 for n in array_names:
   # Make sure the lambda captures the right string.
   globals()[n] = lambda header, cert_filename, name=n: header.write(_create_header(name, _file_byte_generator(cert_filename)))
--- a/security/apps/moz.build
+++ b/security/apps/moz.build
@@ -29,15 +29,17 @@ headers_arrays_certs = [
     ('marketplace-prod-public.inc', 'marketplaceProdPublicRoot', 'marketplace-prod-public.crt'),
     ('marketplace-prod-reviewers.inc', 'marketplaceProdReviewersRoot', 'marketplace-prod-reviewers.crt'),
     ('marketplace-dev-public.inc', 'marketplaceDevPublicRoot', 'marketplace-dev-public.crt'),
     ('marketplace-dev-reviewers.inc', 'marketplaceDevReviewersRoot', 'marketplace-dev-reviewers.crt'),
     ('marketplace-stage.inc', 'marketplaceStageRoot', 'marketplace-stage.crt'),
     ('manifest-signing-root.inc', 'trustedAppPublicRoot', 'trusted-app-public.der'),
     ('manifest-signing-test-root.inc', 'trustedAppTestRoot', test_ssl_path + '/test_signed_manifest/trusted_ca1.der'),
     ('xpcshell.inc', 'xpcshellRoot', test_ssl_path + '/test_signed_apps/trusted_ca1.der'),
+    ('addons-public.inc', 'addonsPublicRoot', 'addons-public.crt'),
+    ('addons-stage.inc', 'addonsStageRoot', 'addons-stage.crt'),
 ]
 
 for header, array_name, cert in headers_arrays_certs:
     GENERATED_FILES += [header]
     h = GENERATED_FILES[header]
     h.script = 'gen_cert_header.py:' + array_name
     h.inputs = [cert]
--- a/security/manager/ssl/public/nsIX509CertDB.idl
+++ b/security/manager/ssl/public/nsIX509CertDB.idl
@@ -34,17 +34,17 @@ interface nsIVerifySignedManifestCallbac
   void verifySignedManifestFinished(in nsresult rv,
                                     in nsIX509Cert aSignerCert);
 };
 
 /**
  * This represents a service to access and manipulate
  * X.509 certificates stored in a database.
  */
-[scriptable, uuid(8b01c2af-3a44-44d3-8ea5-51c2455e6c4b)]
+[scriptable, uuid(560bc9ac-3e71-472e-9b08-2270d0c71878)]
 interface nsIX509CertDB : nsISupports {
 
   /**
    *  Constants that define which usages a certificate
    *  is trusted for.
    */
   const unsigned long UNTRUSTED       =      0;
   const unsigned long TRUSTED_SSL     = 1 << 0;
@@ -306,16 +306,18 @@ interface nsIX509CertDB : nsISupports {
   const AppTrustedRoot AppMarketplaceProdPublicRoot = 1;
   const AppTrustedRoot AppMarketplaceProdReviewersRoot = 2;
   const AppTrustedRoot AppMarketplaceDevPublicRoot = 3;
   const AppTrustedRoot AppMarketplaceDevReviewersRoot = 4;
   const AppTrustedRoot AppMarketplaceStageRoot = 5;
   const AppTrustedRoot AppXPCShellRoot = 6;
   const AppTrustedRoot TrustedHostedAppPublicRoot = 7;
   const AppTrustedRoot TrustedHostedAppTestRoot = 8;
+  const AppTrustedRoot AddonsPublicRoot = 9;
+  const AppTrustedRoot AddonsStageRoot = 10;
   void openSignedAppFileAsync(in AppTrustedRoot trustedRoot,
                               in nsIFile aJarFile,
                               in nsIOpenSignedAppFileCallback callback);
 
   /**
    * Given streams containing a signature and a manifest file, verifies
    * that the signature is valid for the manifest. The signature must
    * come from a certificate that is trusted for code signing and that
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -71,16 +71,18 @@ user_pref("experiments.manifest.uri", "h
 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
 user_pref("extensions.enabledScopes", 5);
 // Disable metadata caching for installed add-ons by default
 user_pref("extensions.getAddons.cache.enabled", false);
 // Disable intalling any distribution add-ons
 user_pref("extensions.installDistroAddons", false);
 // XPI extensions are required for test harnesses to load
 user_pref("extensions.defaultProviders.enabled", true);
+// Disable signature requirements where possible
+user_pref("xpinstall.signatures.required", false);
 
 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
 user_pref("geo.wifi.timeToWaitBeforeSending", 2000);
 user_pref("geo.wifi.scan", false);
 user_pref("geo.wifi.logging.enabled", true);
 
 // Make url-classifier updates so rare that they won't affect tests
 user_pref("urlclassifier.updateinterval", 172800);
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2667,16 +2667,18 @@ this.AddonManager = {
   // The download failed due to network problems.
   ERROR_NETWORK_FAILURE: -1,
   // The downloaded file did not match the provided hash.
   ERROR_INCORRECT_HASH: -2,
   // The downloaded file seems to be corrupted in some way.
   ERROR_CORRUPT_FILE: -3,
   // An error occured trying to write to the filesystem.
   ERROR_FILE_ACCESS: -4,
+  // The add-on must be signed and isn't.
+  ERROR_SIGNEDSTATE_REQUIRED: -5,
 
   // These must be kept in sync with AddonUpdateChecker.
   // No error was encountered.
   UPDATE_STATUS_NO_ERROR: 0,
   // The update check timed out
   UPDATE_STATUS_TIMEOUT: -1,
   // There was an error while downloading the update information.
   UPDATE_STATUS_DOWNLOAD_ERROR: -2,
@@ -2802,16 +2804,30 @@ this.AddonManager = {
   // an application change making an add-on incompatible. Doesn't include
   // add-ons that were pending being disabled the last time the application ran.
   STARTUP_CHANGE_DISABLED: "disabled",
   // Add-ons that were detected as enabled during startup, normally because of
   // an application change making an add-on compatible. Doesn't include
   // add-ons that were pending being enabled the last time the application ran.
   STARTUP_CHANGE_ENABLED: "enabled",
 
+  // Constants for Addon.signedState. Any states that should cause an add-on
+  // to be unusable in builds that require signing should have negative values.
+  // Add-on is signed but signature verification has failed.
+  SIGNEDSTATE_BROKEN: -2,
+  // Add-on may be signed but by an certificate that doesn't chain to our
+  // our trusted certificate.
+  SIGNEDSTATE_UNKNOWN: -1,
+  // Add-on is unsigned.
+  SIGNEDSTATE_MISSING: 0,
+  // Add-on is preliminarily reviewed.
+  SIGNEDSTATE_PRELIMINARY: 1,
+  // Add-on is fully reviewed.
+  SIGNEDSTATE_SIGNED: 2,
+
   // Constants for the Addon.userDisabled property
   // Indicates that the userDisabled state of this add-on is currently
   // ask-to-activate. That is, it can be conditionally enabled on a
   // case-by-case basis.
   STATE_ASK_TO_ACTIVATE: "askToActivate",
 
 #ifdef MOZ_EM_DEBUG
   get __AddonManagerInternal__() {
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -73,16 +73,19 @@ const PREF_EM_ENABLED_ADDONS          = 
 const PREF_EM_EXTENSION_FORMAT        = "extensions.";
 const PREF_EM_ENABLED_SCOPES          = "extensions.enabledScopes";
 const PREF_EM_AUTO_DISABLED_SCOPES    = "extensions.autoDisableScopes";
 const PREF_EM_SHOW_MISMATCH_UI        = "extensions.showMismatchUI";
 const PREF_XPI_ENABLED                = "xpinstall.enabled";
 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
 const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
 const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
+// xpinstall.signatures.required only supported in dev builds
+const PREF_XPI_SIGNATURES_REQUIRED    = "xpinstall.signatures.required";
+const PREF_XPI_SIGNATURES_DEV_ROOT    = "xpinstall.signatures.dev-root";
 const PREF_XPI_PERMISSIONS_BRANCH     = "xpinstall.";
 const PREF_XPI_UNPACK                 = "extensions.alwaysUnpack";
 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
 const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
 const PREF_INSTALL_DISTRO_ADDONS      = "extensions.installDistroAddons";
 const PREF_BRANCH_INSTALLED_ADDON     = "extensions.installedDistroAddon.";
 const PREF_SHOWN_SELECTION_UI         = "extensions.shownSelectionUI";
 const PREF_INTERPOSITION_ENABLED      = "extensions.interposition.enabled";
@@ -95,17 +98,16 @@ const PREF_CHECKCOMAT_THEMEOVERRIDE   = 
 const URI_EXTENSION_SELECT_DIALOG     = "chrome://mozapps/content/extensions/selectAddons.xul";
 const URI_EXTENSION_UPDATE_DIALOG     = "chrome://mozapps/content/extensions/update.xul";
 const URI_EXTENSION_STRINGS           = "chrome://mozapps/locale/extensions/extensions.properties";
 
 const STRING_TYPE_NAME                = "type.%ID%.name";
 
 const DIR_EXTENSIONS                  = "extensions";
 const DIR_STAGE                       = "staged";
-const DIR_XPI_STAGE                   = "staged-xpis";
 const DIR_TRASH                       = "trash";
 
 const FILE_DATABASE                   = "extensions.json";
 const FILE_OLD_CACHE                  = "extensions.cache";
 const FILE_INSTALL_MANIFEST           = "install.rdf";
 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
 
 const KEY_PROFILEDIR                  = "ProfD";
@@ -185,16 +187,29 @@ const TYPES = {
 };
 
 const RESTARTLESS_TYPES = new Set([
   "dictionary",
   "experiment",
   "locale",
 ]);
 
+const SIGNED_TYPES = new Set([
+  "extension",
+  "experiment",
+]);
+
+// Whether add-on signing is required.
+function mustSign(aType) {
+  if (!SIGNED_TYPES.has(aType))
+    return false;
+  return REQUIRE_SIGNING || Preferences.get(PREF_XPI_SIGNATURES_REQUIRED, false);
+}
+
+
 // Keep track of where we are in startup for telemetry
 // event happened during XPIDatabase.startup()
 const XPI_STARTING = "XPIStarting";
 // event happened after startup() but before the final-ui-startup event
 const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
 // event happened after final-ui-startup
 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
 
@@ -625,16 +640,19 @@ function applyBlocklistChanges(aOldAddon
  *         The add-on to check
  * @return true if the add-on should not be appDisabled
  */
 function isUsableAddon(aAddon) {
   // Hack to ensure the default theme is always usable
   if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
     return true;
 
+  if (mustSign(aAddon.type) && aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING)
+    return false;
+
   if (aAddon.blocklistState == Blocklist.STATE_BLOCKED)
     return false;
 
   if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
     return false;
 
   if (!aAddon.isPlatformCompatible)
     return false;
@@ -994,17 +1012,17 @@ function loadManifestFromRDF(aUri, aStre
 /**
  * Loads an AddonInternal object from an add-on extracted in a directory.
  *
  * @param  aDir
  *         The nsIFile directory holding the add-on
  * @return an AddonInternal object
  * @throws if the directory does not contain a valid install manifest
  */
-function loadManifestFromDir(aDir) {
+let loadManifestFromDir = Task.async(function* loadManifestFromDir(aDir) {
   function getFileSize(aFile) {
     if (aFile.isSymlink())
       return 0;
 
     if (!aFile.isDirectory())
       return aFile.fileSize;
 
     let size = 0;
@@ -1035,34 +1053,39 @@ function loadManifestFromDir(aDir) {
     addon.size = getFileSize(aDir);
 
     file = aDir.clone();
     file.append("chrome.manifest");
     let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
     addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
                                                              "binary-component");
 
+    if (SIGNED_TYPES.has(addon.type))
+      addon.signedState = yield verifyDirSignedState(aDir, addon.id);
+    else
+      addon.signedState = AddonManager.SIGNEDSTATE_MISSING;
+
     addon.appDisabled = !isUsableAddon(addon);
     return addon;
   }
   finally {
     bis.close();
     fis.close();
   }
-}
+});
 
 /**
  * Loads an AddonInternal object from an nsIZipReader for an add-on.
  *
  * @param  aZipReader
  *         An open nsIZipReader for the add-on's files
  * @return an AddonInternal object
  * @throws if the XPI file does not contain a valid install manifest
  */
-function loadManifestFromZipReader(aZipReader) {
+let loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(aZipReader) {
   let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
   let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
             createInstance(Ci.nsIBufferedInputStream);
   bis.init(zis, 4096);
 
   try {
     let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
     let addon = loadManifestFromRDF(uri, bis);
@@ -1078,54 +1101,89 @@ function loadManifestFromZipReader(aZipR
       uri = buildJarURI(aZipReader.file, "chrome.manifest");
       let chromeManifest = ChromeManifestParser.parseSync(uri);
       addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
                                                                "binary-component");
     } else {
       addon.hasBinaryComponents = false;
     }
 
+    if (SIGNED_TYPES.has(addon.type))
+      addon.signedState = yield verifyZipSignedState(aZipReader.file, addon.id, addon.version);
+    else
+      addon.signedState = AddonManager.SIGNEDSTATE_MISSING;
+
     addon.appDisabled = !isUsableAddon(addon);
     return addon;
   }
   finally {
     bis.close();
     zis.close();
   }
-}
+});
 
 /**
  * Loads an AddonInternal object from an add-on in an XPI file.
  *
  * @param  aXPIFile
  *         An nsIFile pointing to the add-on's XPI file
  * @return an AddonInternal object
  * @throws if the XPI file does not contain a valid install manifest
  */
-function loadManifestFromZipFile(aXPIFile) {
+let loadManifestFromZipFile = Task.async(function* loadManifestFromZipFile(aXPIFile) {
   let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
                   createInstance(Ci.nsIZipReader);
   try {
     zipReader.open(aXPIFile);
 
-    return loadManifestFromZipReader(zipReader);
+    // Can't return this promise because that will make us close the zip reader
+    // before it has finished loading the manifest. Wait for the result and then
+    // return.
+    let manifest = yield loadManifestFromZipReader(zipReader);
+    return manifest;
   }
   finally {
     zipReader.close();
   }
-}
+});
 
 function loadManifestFromFile(aFile) {
   if (aFile.isFile())
     return loadManifestFromZipFile(aFile);
   else
     return loadManifestFromDir(aFile);
 }
 
 /**
+ * A synchronous method for loading an add-on's manifest. This should only ever
+ * be used during startup or a sync load of the add-ons DB
+ */
+function syncLoadManifestFromFile(aFile) {
+  let success = undefined;
+  let result = null;
+
+  loadManifestFromFile(aFile).then(val => {
+    success = true;
+    result = val;
+  }, val => {
+    success = false;
+    result = val
+  });
+
+  let thread = Services.tm.currentThread;
+
+  while (success === undefined)
+    thread.processNextEvent(true);
+
+  if (!success)
+    throw result;
+  return result;
+}
+
+/**
  * Gets an nsIURI for a file within another file, either a directory or an XPI
  * file. If aFile is a directory then this will return a file: URI, if it is an
  * XPI file then it will return a jar: URI.
  *
  * @param  aFile
  *         The file containing the resources, must be either a directory or an
  *         XPI file
  * @param  aPath
@@ -1222,16 +1280,91 @@ function verifyZipSigning(aZip, aCertifi
     if (!entryCertificate || !aCertificate.equals(entryCertificate)) {
       return false;
     }
   }
   return aZip.manifestEntriesCount == count;
 }
 
 /**
+ * Returns the signedState for a given return code and certificate by verifying
+ * it against the expected ID.
+ */
+function getSignedStatus(aRv, aCert, aExpectedID) {
+  switch (aRv) {
+    case Cr.NS_OK:
+      if (aExpectedID != aCert.commonName)
+        return AddonManager.SIGNEDSTATE_BROKEN;
+
+      return /preliminary/i.test(aCert.organizationalUnit)
+               ? AddonManager.SIGNEDSTATE_PRELIMINARY
+               : AddonManager.SIGNEDSTATE_SIGNED;
+      break;
+    case Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED:
+      return AddonManager.SIGNEDSTATE_MISSING;
+      break;
+    case Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID:
+    case Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID:
+    case Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING:
+    case Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE:
+    case Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY:
+    case Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY:
+      return AddonManager.SIGNEDSTATE_BROKEN;
+      break;
+    default:
+      // Any other error indicates that either the add-on isn't signed or it
+      // is signed by a signature that doesn't chain to the trusted root.
+      return AddonManager.SIGNEDSTATE_UNKNOWN;
+      break;
+  }
+}
+
+/**
+ * Verifies that a zip file's contents are all correctly signed by an
+ * AMO-issued certificate
+ *
+ * @param  aFile
+ *         the xpi file to check
+ * @param  aExpectedID
+ *         the expected ID of the signature
+ * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
+ */
+function verifyZipSignedState(aFile, aExpectedID, aVersion) {
+  let certDB = Cc["@mozilla.org/security/x509certdb;1"]
+               .getService(Ci.nsIX509CertDB);
+
+  let root = Ci.nsIX509CertDB.AddonsPublicRoot;
+  if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false))
+    root = Ci.nsIX509CertDB.AddonsStageRoot;
+
+  return new Promise(resolve => {
+    certDB.openSignedAppFileAsync(root, aFile, (aRv, aZipReader, aCert) => {
+      if (aZipReader)
+        aZipReader.close();
+      resolve(getSignedStatus(aRv, aCert, aExpectedID));
+    });
+  });
+}
+
+/**
+ * Verifies that a directory's contents are all correctly signed by an
+ * AMO-issued certificate
+ *
+ * @param  aDir
+ *         the directory to check
+ * @param  aExpectedID
+ *         the expected ID of the signature
+ * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
+ */
+function verifyDirSignedState(aDir, aExpectedID) {
+  // TODO: Get the certificate for an unpacked add-on (bug 1038072)
+  return Promise.resolve(AddonManager.SIGNEDSTATE_MISSING);
+}
+
+/**
  * Replaces %...% strings in an addon url (update and updateInfo) with
  * appropriate values.
  *
  * @param  aAddon
  *         The AddonInternal representing the add-on
  * @param  aUri
  *         The uri to escape
  * @param  aUpdateType
@@ -2076,16 +2209,18 @@ this.XPIProvider = {
       this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
                                                      null);
       this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
                                                           null);
       this.enabledAddons = "";
 
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
+      if (!REQUIRE_SIGNING)
+        Services.prefs.addObserver(PREF_XPI_SIGNATURES_REQUIRED, this, false);
       Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
       if (Cu.isModuleLoaded("resource:///modules/devtools/ToolboxProcess.jsm")) {
         // If BrowserToolboxProcess is already loaded, set the boolean to true
         // and do whatever is needed
         this._toolboxProcessLoaded = true;
         BrowserToolboxProcess.on("connectionchange",
                                  this.onDebugConnectionChange.bind(this));
       }
@@ -2419,105 +2554,18 @@ this.XPIProvider = {
   processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) {
     let changed = false;
     this.installLocations.forEach(function(aLocation) {
       aManifests[aLocation.name] = {};
       // We can't install or uninstall anything in locked locations
       if (aLocation.locked)
         return;
 
-      let stagedXPIDir = aLocation.getXPIStagingDir();
       let stagingDir = aLocation.getStagingDir();
 
-      if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) {
-        let entries = stagedXPIDir.directoryEntries
-                                  .QueryInterface(Ci.nsIDirectoryEnumerator);
-        while (entries.hasMoreElements()) {
-          let stageDirEntry = entries.nextFile;
-
-          if (!stageDirEntry.isDirectory()) {
-            logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path);
-            continue;
-          }
-
-          // Find the last added XPI file in the directory
-          let stagedXPI = null;
-          var xpiEntries = stageDirEntry.directoryEntries
-                                        .QueryInterface(Ci.nsIDirectoryEnumerator);
-          while (xpiEntries.hasMoreElements()) {
-            let file = xpiEntries.nextFile;
-            if (file.isDirectory())
-              continue;
-
-            let extension = file.leafName;
-            extension = extension.substring(extension.length - 4);
-
-            if (extension != ".xpi" && extension != ".jar")
-              continue;
-
-            stagedXPI = file;
-          }
-          xpiEntries.close();
-
-          if (!stagedXPI)
-            continue;
-
-          let addon = null;
-          try {
-            addon = loadManifestFromZipFile(stagedXPI);
-          }
-          catch (e) {
-            logger.error("Unable to read add-on manifest from " + stagedXPI.path, e);
-            continue;
-          }
-
-          logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name);
-
-          if (addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
-            let targetDir = stagingDir.clone();
-            targetDir.append(addon.id);
-            try {
-              targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-            }
-            catch (e) {
-              logger.error("Failed to create staging directory for add-on " + addon.id, e);
-              continue;
-            }
-
-            try {
-              ZipUtils.extractFiles(stagedXPI, targetDir);
-            }
-            catch (e) {
-              logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " +
-                    aLocation.name, e);
-            }
-          }
-          else {
-            try {
-              stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
-            }
-            catch (e) {
-              logger.error("Failed to move staged XPI for add-on " + addon.id + " in " +
-                    aLocation.name, e);
-            }
-          }
-        }
-        entries.close();
-      }
-
-      if (stagedXPIDir.exists()) {
-        try {
-          recursiveRemove(stagedXPIDir);
-        }
-        catch (e) {
-          // Non-critical, just saves some perf on startup if we clean this up.
-          logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e);
-        }
-      }
-
       try {
         if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
           return;
       }
       catch (e) {
         logger.warn("Failed to find staging directory", e);
         return;
       }
@@ -2588,66 +2636,75 @@ this.XPIProvider = {
 
         aManifests[aLocation.name][id] = null;
         let existingAddonID = id;
 
         let jsonfile = stagingDir.clone();
         jsonfile.append(id + ".json");
 
         try {
-          aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry);
+          aManifests[aLocation.name][id] = syncLoadManifestFromFile(stageDirEntry);
         }
         catch (e) {
           logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e);
           // This add-on can't be installed so just remove it now
           seenFiles.push(stageDirEntry.leafName);
           seenFiles.push(jsonfile.leafName);
           continue;
         }
 
+        let addon = aManifests[aLocation.name][id];
+
+        if ((addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) && mustSign(addon.type)) {
+          logger.warn("Refusing to install staged add-on " + id + " with signed state " + addon.signedState);
+          seenFiles.push(stageDirEntry.leafName);
+          seenFiles.push(jsonfile.leafName);
+          continue;
+        }
+
         // Check for a cached metadata for this add-on, it may contain updated
         // compatibility information
         if (jsonfile.exists()) {
           logger.debug("Found updated metadata for " + id + " in " + aLocation.name);
           let fis = Cc["@mozilla.org/network/file-input-stream;1"].
                        createInstance(Ci.nsIFileInputStream);
           let json = Cc["@mozilla.org/dom/json;1"].
                      createInstance(Ci.nsIJSON);
 
           try {
             fis.init(jsonfile, -1, 0, 0);
             let metadata = json.decodeFromStream(fis, jsonfile.fileSize);
-            aManifests[aLocation.name][id].importMetadata(metadata);
+            addon.importMetadata(metadata);
           }
           catch (e) {
             // If some data can't be recovered from the cached metadata then it
             // is unlikely to be a problem big enough to justify throwing away
             // the install, just log and error and continue
             logger.error("Unable to read metadata from " + jsonfile.path, e);
           }
           finally {
             fis.close();
           }
         }
         seenFiles.push(jsonfile.leafName);
 
-        existingAddonID = aManifests[aLocation.name][id].existingAddonID || id;
+        existingAddonID = addon.existingAddonID || id;
 
         var oldBootstrap = null;
         logger.debug("Processing install of " + id + " in " + aLocation.name);
         if (existingAddonID in this.bootstrappedAddons) {
           try {
             var existingAddon = aLocation.getLocationForID(existingAddonID);
             if (this.bootstrappedAddons[existingAddonID].descriptor ==
                 existingAddon.persistentDescriptor) {
               oldBootstrap = this.bootstrappedAddons[existingAddonID];
 
               // We'll be replacing a currently active bootstrapped add-on so
               // call its uninstall method
-              let newVersion = aManifests[aLocation.name][id].version;
+              let newVersion = addon.version;
               let oldVersion = oldBootstrap.version;
               let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
                                     BOOTSTRAP_REASONS.ADDON_UPGRADE :
                                     BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
               this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
                                        existingAddon, "uninstall", uninstallReason,
                                        { newVersion: newVersion });
@@ -2655,27 +2712,25 @@ this.XPIProvider = {
               flushStartupCache();
             }
           }
           catch (e) {
           }
         }
 
         try {
-          var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
-                                                            existingAddonID);
-          if (aManifests[aLocation.name][id])
-            aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
+          addon._sourceBundle = aLocation.installAddon(id, stageDirEntry,
+                                                       existingAddonID);
         }
         catch (e) {
           logger.error("Failed to install staged add-on " + id + " in " + aLocation.name,
                 e);
           // Re-create the staged install
           AddonInstall.createStagedInstall(aLocation, stageDirEntry,
-                                           aManifests[aLocation.name][id]);
+                                           addon);
           // Make sure not to delete the cached manifest json file
           seenFiles.pop();
 
           delete aManifests[aLocation.name][id];
 
           if (oldBootstrap) {
             // Re-install the old add-on
             this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
@@ -2751,17 +2806,17 @@ this.XPIProvider = {
       if (!gIDTest.test(id)) {
         logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
             entry.path);
         continue;
       }
 
       let addon;
       try {
-        addon = loadManifestFromFile(entry);
+        addon = syncLoadManifestFromFile(entry);
       }
       catch (e) {
         logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
         continue;
       }
 
       if (addon.id != id) {
         logger.warn("File entry " + entry.path + " contains an add-on with an " +
@@ -2774,17 +2829,17 @@ this.XPIProvider = {
         existingEntry = profileLocation.getLocationForID(id);
       }
       catch (e) {
       }
 
       if (existingEntry) {
         let existingAddon;
         try {
-          existingAddon = loadManifestFromFile(existingEntry);
+          existingAddon = syncLoadManifestFromFile(existingEntry);
 
           if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
             continue;
         }
         catch (e) {
           // Bad add-on in the profile so just proceed and install over the top
           logger.warn("Profile contains an add-on with a bad or missing install " +
                "manifest at " + existingEntry.path + ", overwriting", e);
@@ -2869,17 +2924,17 @@ this.XPIProvider = {
 
       // Check if there is an updated install manifest for this add-on
       let newAddon = aManifests[aInstallLocation.name][aOldAddon.id];
 
       try {
         // If not load it
         if (!newAddon) {
           let file = aInstallLocation.getLocationForID(aOldAddon.id);
-          newAddon = loadManifestFromFile(file);
+          newAddon = syncLoadManifestFromFile(file);
           applyBlocklistChanges(aOldAddon, newAddon);
 
           // Carry over any pendingUninstall state to add-ons modified directly
           // in the profile. This is important when the attempt to remove the
           // add-on in processPendingFileChanges failed and caused an mtime
           // change to the add-ons files.
           newAddon.pendingUninstall = aOldAddon.pendingUninstall;
         }
@@ -2912,16 +2967,21 @@ this.XPIProvider = {
       // Update the database
       let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon,
                                                        aAddonState.descriptor);
       if (newDBAddon.visible) {
         visibleAddons[newDBAddon.id] = newDBAddon;
         // Remember add-ons that were changed during startup
         AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
                                              newDBAddon.id);
+        if (aOldAddon.active == newDBAddon.disabled) {
+          let change = aOldAddon.active ? AddonManager.STARTUP_CHANGE_DISABLED
+                                        : AddonManager.STARTUP_CHANGE_ENABLED;
+          AddonManagerPrivate.addStartupChange(change, newDBAddon.id);
+        }
 
         // If this was the active theme and it is now disabled then enable the
         // default theme
         if (aOldAddon.active && newDBAddon.disabled)
           XPIProvider.enableDefaultTheme();
 
         // If the new add-on is bootstrapped and active then call its install method
         if (newDBAddon.active && newDBAddon.bootstrap) {
@@ -3036,26 +3096,35 @@ this.XPIProvider = {
       }
 
       // App version changed, we may need to update the appDisabled property.
       if (aUpdateCompatibility) {
         let wasDisabled = aOldAddon.disabled;
         let wasAppDisabled = aOldAddon.appDisabled;
         let wasUserDisabled = aOldAddon.userDisabled;
         let wasSoftDisabled = aOldAddon.softDisabled;
-
+        let updateDB = false;
+
+        // If updating from a version of the app that didn't support signedState
+        // then fetch that property now
+        if (aOldAddon.signedState === undefined) {
+          let file = aInstallLocation.getLocationForID(aOldAddon.id);
+          let manifest = syncLoadManifestFromFile(file);
+          aOldAddon.signedState = manifest.signedState;
+          updateDB = true;
+        }
         // This updates the addon's JSON cached data in place
         applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
                               aOldPlatformVersion);
         aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
 
         let isDisabled = aOldAddon.disabled;
 
         // If either property has changed update the database.
-        if (wasAppDisabled != aOldAddon.appDisabled ||
+        if (updateDB || wasAppDisabled != aOldAddon.appDisabled ||
             wasUserDisabled != aOldAddon.userDisabled ||
             wasSoftDisabled != aOldAddon.softDisabled) {
           logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " +
               aOldAddon.appDisabled + ", userDisabled state to " +
               aOldAddon.userDisabled + " and softDisabled state to " +
               aOldAddon.softDisabled);
           XPIDatabase.saveChanges();
         }
@@ -3166,17 +3235,17 @@ this.XPIProvider = {
       // must be something dropped directly into the install location
       let isDetectedInstall = isNewInstall && !newAddon;
 
       // Load the manifest if necessary and sanity check the add-on ID
       try {
         if (!newAddon) {
           // Load the manifest from the add-on.
           let file = aInstallLocation.getLocationForID(aId);
-          newAddon = loadManifestFromFile(file);
+          newAddon = syncLoadManifestFromFile(file);
         }
         // The add-on in the manifest should match the add-on ID.
         if (newAddon.id != aId) {
           throw new Error("Invalid addon ID: expected addon ID " + aId +
                           ", found " + newAddon.id + " in manifest");
         }
       }
       catch (e) {
@@ -4089,23 +4158,28 @@ this.XPIProvider = {
       this._toolboxProcessLoaded = true;
       BrowserToolboxProcess.on("connectionchange",
                                this.onDebugConnectionChange.bind(this));
     }
 
     if (aTopic == "nsPref:changed") {
       switch (aData) {
       case PREF_EM_MIN_COMPAT_APP_VERSION:
-      case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
         this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
                                                        null);
+        this.updateAddonAppDisabledStates();
+        break;
+      case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
         this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
                                                             null);
         this.updateAddonAppDisabledStates();
         break;
+      case PREF_XPI_SIGNATURES_REQUIRED:
+        this.updateAddonAppDisabledStates();
+        break;
       }
     }
   },
 
   /**
    * Tests whether enabling an add-on will require a restart.
    *
    * @param  aAddon
@@ -4909,59 +4983,55 @@ AddonInstall.prototype = {
              this.hash.data + ")");
         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
         this.error = AddonManager.ERROR_INCORRECT_HASH;
         aCallback(this);
         return;
       }
     }
 
-    try {
-      let self = this;
-      this.loadManifest(function  initLocalInstall_loadManifest() {
-        XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
-          self.existingAddon = aAddon;
-          if (aAddon)
-            applyBlocklistChanges(aAddon, self.addon);
-          self.addon.updateDate = Date.now();
-          self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
-
-          if (!self.addon.isCompatible) {
-            // TODO Should we send some event here?
-            self.state = AddonManager.STATE_CHECKING;
-            new UpdateChecker(self.addon, {
-              onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) {
-                self.state = AddonManager.STATE_DOWNLOADED;
-                XPIProvider.installs.push(self);
-                AddonManagerPrivate.callInstallListeners("onNewInstall",
-                                                         self.listeners,
-                                                         self.wrapper);
-
-                aCallback(self);
-              }
-            }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
-          }
-          else {
-            XPIProvider.installs.push(self);
-            AddonManagerPrivate.callInstallListeners("onNewInstall",
-                                                     self.listeners,
-                                                     self.wrapper);
-
-            aCallback(self);
-          }
-        });
+    let self = this;
+    this.loadManifest().then(() => {
+      XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
+        self.existingAddon = aAddon;
+        if (aAddon)
+          applyBlocklistChanges(aAddon, self.addon);
+        self.addon.updateDate = Date.now();
+        self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
+
+        if (!self.addon.isCompatible) {
+          // TODO Should we send some event here?
+          self.state = AddonManager.STATE_CHECKING;
+          new UpdateChecker(self.addon, {
+            onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) {
+              self.state = AddonManager.STATE_DOWNLOADED;
+              XPIProvider.installs.push(self);
+              AddonManagerPrivate.callInstallListeners("onNewInstall",
+                                                       self.listeners,
+                                                       self.wrapper);
+
+              aCallback(self);
+            }
+          }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+        }
+        else {
+          XPIProvider.installs.push(self);
+          AddonManagerPrivate.callInstallListeners("onNewInstall",
+                                                   self.listeners,
+                                                   self.wrapper);
+
+          aCallback(self);
+        }
       });
-    }
-    catch (e) {
-      logger.warn("Invalid XPI", e);
+    }, ([error, message]) => {
+      logger.warn("Invalid XPI", message);
       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-      this.error = AddonManager.ERROR_CORRUPT_FILE;
+      this.error = error;
       aCallback(this);
-      return;
-    }
+    });
   },
 
   /**
    * Initialises this install to be a download from a remote url.
    *
    * @param  aCallback
    *         The callback to pass the initialised AddonInstall to
    * @param  aName
@@ -5140,18 +5210,17 @@ AddonInstall.prototype = {
    * @param  aZipReader
    *         An open nsIZipReader for the multi-package XPI's files. This will
    *         be closed before this method returns.
    * @param  aCallback
    *         A function to call when all of the add-on manifests have been
    *         loaded. Because this loadMultipackageManifests is an internal API
    *         we don't exception-wrap this callback
    */
-  _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
-                                                                   aCallback) {
+  _loadMultipackageManifests: Task.async(function* AI_loadMultipackageManifests(aZipReader) {
     let files = [];
     let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
     while (entries.hasMore()) {
       let entryName = entries.getNext();
       var target = getTemporaryFile();
       try {
         aZipReader.extract(entryName, target);
         files.push(target);
@@ -5174,28 +5243,27 @@ AddonInstall.prototype = {
 
     // Find the first file that has a valid install manifest and use it for
     // the add-on that this AddonInstall instance will install.
     while (files.length > 0) {
       this.removeTemporaryFile();
       this.file = files.shift();
       this.ownsTempFile = true;
       try {
-        addon = loadManifestFromZipFile(this.file);
+        addon = yield loadManifestFromZipFile(this.file);
         break;
       }
       catch (e) {
         logger.warn(this.file.leafName + " cannot be installed from multi-package " +
              "XPI", e);
       }
     }
 
     if (!addon) {
       // No valid add-on was found
-      aCallback();
       return;
     }
 
     this.addon = addon;
 
     this.updateAddonURIs();
 
     this.addon._install = this;
@@ -5207,144 +5275,138 @@ AddonInstall.prototype = {
     // makes it impossible to delete on Windows.
     //let newIcon = createWrapper(this.addon).iconURL;
     //if (newIcon)
     //  this.iconURL = newIcon;
 
     // Create new AddonInstall instances for every remaining file
     if (files.length > 0) {
       this.linkedInstalls = [];
-      let count = 0;
       let self = this;
-      files.forEach(function(file) {
-        AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) {
-          // Ignore bad add-ons (createInstall will have logged the error)
-          if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
-            // Manually remove the temporary file
-            file.remove(true);
-          }
-          else {
-            // Make the new install own its temporary file
-            aInstall.ownsTempFile = true;
-
-            self.linkedInstalls.push(aInstall)
-
-            aInstall.sourceURI = self.sourceURI;
-            aInstall.releaseNotesURI = self.releaseNotesURI;
-            aInstall.updateAddonURIs();
-          }
-
-          count++;
-          if (count == files.length)
-            aCallback();
-        }, file);
-      }, this);
-    }
-    else {
-      aCallback();
-    }
-  },
+      for (let file of files) {
+        let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
+
+        // Ignore bad add-ons (createInstall will have logged the error)
+        if (install.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+          // Manually remove the temporary file
+          file.remove(true);
+        }
+        else {
+          // Make the new install own its temporary file
+          install.ownsTempFile = true;
+
+          self.linkedInstalls.push(install)
+
+          install.sourceURI = self.sourceURI;
+          install.releaseNotesURI = self.releaseNotesURI;
+          install.updateAddonURIs();
+        }
+      }
+    }
+  }),
 
   /**
    * Called after the add-on is a local file and the signature and install
    * manifest can be read.
    *
    * @param  aCallback
    *         A function to call when the manifest has been loaded
    * @throws if the add-on does not contain a valid install manifest or the
    *         XPI is incorrectly signed
    */
-  loadManifest: function AI_loadManifest(aCallback) {
-    aCallback = makeSafe(aCallback);
-    let self = this;
-    function addRepositoryData(aAddon) {
-      // Try to load from the existing cache first
-      AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
-        if (aRepoAddon) {
-          aAddon._repositoryAddon = aRepoAddon;
-          self.name = self.name || aAddon._repositoryAddon.name;
-          aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
-          aAddon.appDisabled = !isUsableAddon(aAddon);
-          aCallback();
-          return;
-        }
-
-        // It wasn't there so try to re-download it
-        AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() {
-          AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
-            aAddon._repositoryAddon = aRepoAddon;
-            self.name = self.name || aAddon._repositoryAddon.name;
-            aAddon.compatibilityOverrides = aRepoAddon ?
-                                              aRepoAddon.compatibilityOverrides :
-                                              null;
-            aAddon.appDisabled = !isUsableAddon(aAddon);
-            aCallback();
-          });
-        });
-      });
-    }
-
+  loadManifest: Task.async(function* AI_loadManifest() {
     let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
                     createInstance(Ci.nsIZipReader);
     try {
       zipreader.open(this.file);
     }
     catch (e) {
       zipreader.close();
-      throw e;
-    }
-
-    let x509 = zipreader.getSigningCert(null);
-    if (x509) {
-      logger.debug("Verifying XPI signature");
-      if (verifyZipSigning(zipreader, x509)) {
-        this.certificate = x509;
-        if (this.certificate.commonName.length > 0) {
-          this.certName = this.certificate.commonName;
-        } else {
-          this.certName = this.certificate.organization;
-        }
-      } else {
-        zipreader.close();
-        throw new Error("XPI is incorrectly signed");
-      }
+      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
 
     try {
-      this.addon = loadManifestFromZipReader(zipreader);
+      // loadManifestFromZipReader performs the certificate verification for us
+      this.addon = yield loadManifestFromZipReader(zipreader);
     }
     catch (e) {
       zipreader.close();
-      throw e;
-    }
-
-    if (this.addon.type == "multipackage") {
-      this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
-        addRepositoryData(self.addon);
-      });
-      return;
-    }
+      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
+    }
+
+    if (mustSign(this.addon.type)) {
+      if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+        // This add-on isn't properly signed by a signature that chains to the
+        // trusted root.
+        let state = this.addon.signedState;
+        this.addon = null;
+        zipreader.close();
+
+        if (state == AddonManager.SIGNEDSTATE_MISSING)
+          return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
+                                 "signature is required but missing"])
+
+        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                               "signature verification failed"])
+      }
+    }
+    else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN) {
+      // Check object signing certificate, if any
+      let x509 = zipreader.getSigningCert(null);
+      if (x509) {
+        logger.debug("Verifying XPI signature");
+        if (verifyZipSigning(zipreader, x509)) {
+          this.certificate = x509;
+          if (this.certificate.commonName.length > 0) {
+            this.certName = this.certificate.commonName;
+          } else {
+            this.certName = this.certificate.organization;
+          }
+        } else {
+          zipreader.close();
+          return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                                 "XPI is incorrectly signed"]);
+        }
+      }
+    }
+
+    if (this.addon.type == "multipackage")
+      return this._loadMultipackageManifests(zipreader);
 
     zipreader.close();
 
     this.updateAddonURIs();
 
     this.addon._install = this;
     this.name = this.addon.selectedLocale.name;
     this.type = this.addon.type;
     this.version = this.addon.version;
 
     // Setting the iconURL to something inside the XPI locks the XPI and
     // makes it impossible to delete on Windows.
     //let newIcon = createWrapper(this.addon).iconURL;
     //if (newIcon)
     //  this.iconURL = newIcon;
 
-    addRepositoryData(this.addon);
-  },
+    // Try to load from the existing cache first
+    let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
+
+    // It wasn't there so try to re-download it
+    if (!repoAddon) {
+      yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve));
+      repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
+    }
+
+    this.addon._repositoryAddon = repoAddon;
+    this.name = this.name || this.addon._repositoryAddon.name;
+    this.addon.compatibilityOverrides = repoAddon ?
+                                    repoAddon.compatibilityOverrides :
+                                    null;
+    this.addon.appDisabled = !isUsableAddon(this.addon);
+  }),
 
   observe: function AI_observe(aSubject, aTopic, aData) {
     // Network is going offline
     this.cancel();
   },
 
   /**
    * Starts downloading the add-on's XPI file.
@@ -5570,36 +5632,34 @@ AddonInstall.prototype = {
         let calculatedHash = getHashStringForCrypto(this.crypto);
         this.crypto = null;
         if (this.hash && calculatedHash != this.hash.data) {
           this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
                               "Downloaded file hash (" + calculatedHash +
                               ") did not match provided hash (" + this.hash.data + ")");
           return;
         }
-        try {
-          let self = this;
-          this.loadManifest(function onStopRequest_loadManifest() {
-            if (self.addon.isCompatible) {
-              self.downloadCompleted();
-            }
-            else {
-              // TODO Should we send some event here (bug 557716)?
-              self.state = AddonManager.STATE_CHECKING;
-              new UpdateChecker(self.addon, {
-                onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
-                  self.downloadCompleted();
-                }
-              }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
-            }
-          });
-        }
-        catch (e) {
-          this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
-        }
+
+        let self = this;
+        this.loadManifest().then(() => {
+          if (self.addon.isCompatible) {
+            self.downloadCompleted();
+          }
+          else {
+            // TODO Should we send some event here (bug 557716)?
+            self.state = AddonManager.STATE_CHECKING;
+            new UpdateChecker(self.addon, {
+              onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
+                self.downloadCompleted();
+              }
+            }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+          }
+        }, ([error, message]) => {
+          this.downloadFailed(error, message);
+        });
       }
       else {
         if (aRequest instanceof Ci.nsIHttpChannel)
           this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
                               aRequest.responseStatus + " " +
                               aRequest.responseStatusText);
         else
           this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
@@ -6614,17 +6674,17 @@ function AddonWrapper(aAddon) {
 
     return [objValue, false];
   }
 
   ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible",
    "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
    "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
    "strictCompatibility", "compatibilityOverrides", "updateURL",
-   "getDataDirectory", "multiprocessCompatible"].forEach(function(aProp) {
+   "getDataDirectory", "multiprocessCompatible", "signedState"].forEach(function(aProp) {
      this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
   }, this);
 
   ["fullDescription", "developerComments", "eula", "supportURL",
    "contributionURL", "contributionAmount", "averageRating", "reviewCount",
    "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
    "repositoryStatus"].forEach(function(aProp) {
     this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() {
@@ -7186,17 +7246,17 @@ DirectoryInstallLocation.prototype = {
   _readAddons: function DirInstallLocation__readAddons() {
     // Use a snapshot of the directory contents to avoid possible issues with
     // iterating over a directory while removing files from it (the YAFFS2
     // embedded filesystem has this issue, see bug 772238).
     let entries = getDirectoryEntries(this._directory);
     for (let entry of entries) {
       let id = entry.leafName;
 
-      if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
+      if (id == DIR_STAGE || id == DIR_TRASH)
         continue;
 
       let directLoad = false;
       if (entry.isFile() &&
           id.substring(id.length - 4).toLowerCase() == ".xpi") {
         directLoad = true;
         id = id.substring(0, id.length - 4);
       }
@@ -7330,28 +7390,16 @@ DirectoryInstallLocation.prototype = {
     }
     catch (e) {
       logger.warn("Failed to remove staging dir", e);
       // Failing to remove the staging directory is ignorable
     }
   },
 
   /**
-   * Gets the directory used by old versions for staging XPI and JAR files ready
-   * to be installed.
-   *
-   * @return an nsIFile
-   */
-  getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
-    let dir = this._directory.clone();
-    dir.append(DIR_XPI_STAGE);
-    return dir;
-  },
-
-  /**
    * Returns a directory that is normally on the same filesystem as the rest of
    * the install location and can be used for temporarily storing files during
    * safe move operations. Calling this method will delete the existing trash
    * directory and its contents.
    *
    * @return an nsIFile
    */
   getTrashDir: function DirInstallLocation_getTrashDir() {
@@ -7717,16 +7765,29 @@ WinRegInstallLocation.prototype = {
    * @see DirectoryInstallLocation
    */
   isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) {
     return true;
   }
 };
 #endif
 
+// Make this a non-changable property so it can't be manipulated from other
+// code in the app.
+Object.defineProperty(this, "REQUIRE_SIGNING", {
+  configurable: false,
+  enumerable: false,
+  writable: false,
+#ifdef MOZ_REQUIRE_SIGNING
+  value: true,
+#else
+  value: false,
+#endif
+});
+
 let addonTypes = [
   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 4000),
   new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 5000),
   new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -65,17 +65,17 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "internalName", "updateURL", "updateKey", "optionsURL",
                           "optionsType", "aboutURL", "iconURL", "icon64URL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "descriptor", "installDate",
                           "updateDate", "applyBackgroundUpdates", "bootstrap",
                           "skinnable", "size", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall", "hasBinaryComponents",
                           "strictCompatibility", "locales", "targetApplications",
-                          "targetPlatforms", "multiprocessCompatible"];
+                          "targetPlatforms", "multiprocessCompatible", "signedState"];
 
 // Time to wait before async save of XPI JSON database, in milliseconds
 const ASYNC_SAVE_DELAY_MS = 20;
 
 const PREFIX_ITEM_URI                 = "urn:mozilla:item:";
 const RDFURI_ITEM_ROOT                = "urn:mozilla:item:root"
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
--- a/toolkit/mozapps/extensions/internal/moz.build
+++ b/toolkit/mozapps/extensions/internal/moz.build
@@ -23,13 +23,13 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'andr
 
 EXTRA_PP_JS_MODULES.addons += [
     'XPIProvider.jsm',
     'XPIProviderUtils.js',
 ]
 
 # This is used in multiple places, so is defined here to avoid it getting
 # out of sync.
-DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 16
+DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 17
 
 # Additional debugging info is exposed in debug builds
 if CONFIG['MOZ_EM_DEBUG']:
     DEFINES['MOZ_EM_DEBUG'] = 1
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/bootstrap.js
@@ -0,0 +1,29 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const VERSION = 1;
+
+// Test steps chain from pref observers on *_reason,
+// so always set that last
+function install(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
+  Services.prefs.setIntPref("bootstraptest.install_oldversion", data.oldVersion);
+  Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+}
+
+function startup(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
+  Services.prefs.setIntPref("bootstraptest.startup_oldversion", data.oldVersion);
+  Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+}
+
+function shutdown(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.active_version", 0);
+  Services.prefs.setIntPref("bootstraptest.shutdown_newversion", data.newVersion);
+  Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
+}
+
+function uninstall(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.installed_version", 0);
+  Services.prefs.setIntPref("bootstraptest.uninstall_newversion", data.newVersion);
+  Services.prefs.setIntPref("bootstraptest.uninstall_reason", reason);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>test@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Front End MetaData -->
+    <em:name>Test Add-on</em:name>
+    <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>2</em:minVersion>
+        <em:maxVersion>5</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/test.txt
@@ -0,0 +1,1 @@
+This test file can be altered to break signing checks.
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/bootstrap.js
@@ -0,0 +1,29 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const VERSION = 2;
+
+// Test steps chain from pref observers on *_reason,
+// so always set that last
+function install(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
+  Services.prefs.setIntPref("bootstraptest.install_oldversion", data.oldVersion);
+  Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+}
+
+function startup(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
+  Services.prefs.setIntPref("bootstraptest.startup_oldversion", data.oldVersion);
+  Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+}
+
+function shutdown(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.active_version", 0);
+  Services.prefs.setIntPref("bootstraptest.shutdown_newversion", data.newVersion);
+  Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
+}
+
+function uninstall(data, reason) {
+  Services.prefs.setIntPref("bootstraptest.installed_version", 0);
+  Services.prefs.setIntPref("bootstraptest.uninstall_newversion", data.newVersion);
+  Services.prefs.setIntPref("bootstraptest.uninstall_reason", reason);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>test@tests.mozilla.org</em:id>
+    <em:version>2.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Front End MetaData -->
+    <em:name>Test Add-on</em:name>
+    <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>4</em:minVersion>
+        <em:maxVersion>6</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/test.txt
@@ -0,0 +1,1 @@
+This test file can be altered to break signing checks.
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>test@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+
+    <!-- Front End MetaData -->
+    <em:name>Test Add-on</em:name>
+    <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>2</em:minVersion>
+        <em:maxVersion>5</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/test.txt
@@ -0,0 +1,1 @@
+This test file can be altered to break signing checks.
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>test@tests.mozilla.org</em:id>
+    <em:version>2.0</em:version>
+
+    <!-- Front End MetaData -->
+    <em:name>Test Add-on</em:name>
+    <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>4</em:minVersion>
+        <em:maxVersion>6</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/test.txt
@@ -0,0 +1,1 @@
+This test file can be altered to break signing checks.
new file mode 100644
index 0000000000000000000000000000000000000000..a62faf1c52b3b5aadb1383d16b4cb5419eaedd1f
GIT binary patch
literal 5158
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
new file mode 100644
index 0000000000000000000000000000000000000000..5444b951a48a7b9114d69cf44fbe7eb45e7280ab
GIT binary patch
literal 5146
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
new file mode 100644
index 0000000000000000000000000000000000000000..122c4d56b3d37bb98458ae9055630ca1f9aed670
GIT binary patch
literal 5150
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
new file mode 100644
index 0000000000000000000000000000000000000000..150918a8c157f6855b4d9ea6796965153af4fe17
GIT binary patch
literal 5156
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
new file mode 100644
index 0000000000000000000000000000000000000000..3c6cd647031e61ea5d9c2f60419956a629185451
GIT binary patch
literal 4628
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
TpWwFw{;z5O>)AZ=-_pMTmef^j
new file mode 100644
index 0000000000000000000000000000000000000000..cdf4325acde44b9ef829471ea730ba3c3ab90c72
GIT binary patch
literal 4634
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
ZKKVbAsSCry|LuVHtK0rMHi!JT^<PN0O_2Zq
new file mode 100644
index 0000000000000000000000000000000000000000..9d50f0825046db67b679ca8c62b95200b111b73b
GIT binary patch
literal 1156
zc$^FHW@Zs#U}E54NXl$-tLJuotHa2^u$qN|frmkcAt^t<q`0Igu|O}YI5dQlfjNAU
zY-lkMmsW5yFtU6DDh29hVAwj@-}|tGz|r?VMV$TCPU)@KQX#~@cSXcY)11kf!Z+Wv
z%)QAbxPQ0stZfR5zVvUud;CoP{pt<huWWg8k$bkr*5u&VwkI@rPlz}-Zjao?H^Vf<
zt$of}!<<)4H}@1y?py8|G$Fn5=;6~f-@b5^7c7pNc2IlOsU}_Lz?-)YYXvap9uU1S
zNwlf4(I)08qweJSU%Y)1V$<(SO0X{3#B^?x%IX)~D@%7>+rFlF@ujZ02g-_h<T(%A
z3;)j|eRqCk<+aZmu|k~Q8`iE+H&)nirkb0}SKa!%)bok}zjZbfs*>HN9u2eX-Ens6
zJ7&Mh=bv0?Jjc@c)>1Y?HTT0ECN^bpodowkiyO4OJm>DuV7~5dC2R9UZg>8}P)prE
zd_Svyz4-r9Smx$|d&gO|Eez^kJ>U+2hj=V7#6SK1q2mh-#tlpi4BWsF&&(?>NzBR7
zD@sWN2Y3+n0B@aiI`5Eyfa~|4T;^+J+eJc^xE6X$3El0O`(4d2;bi}bc_x|r_w{{d
za9LWFs9An)u4KGw^!A_y`e{1XvqaA=wupJ|IN|Mf>nB>L)*Ui-m@0J1#P^Dg+pRe#
zYIep<vQYGF@~u4bEt)ZR<EG694tGBun{rZm{T2Hdj#ttug?kiy%HBo3IHouyccw1S
z`U7hlUpQXhz?HrD(!r@ksf}EXs-b^Q=vaiCYbF~#P37~ee5Y4!tEOCAs??tT%v0p|
ztsl0G>}wA;Eez#bmNsvvhhx8OLQk2SwuAmEmwcWV)0kW(;%{kaMgDtt&2jdoBT+xy
z;^s`)rKP!{|8KUPi%N;((a2~I@plvU$?a^|^RtOPn0vvqBMHHwP2wWU%I~CEW(9}j
zhTbq1el&lFTl<q&#U7at8?_&bEqS}b{_6u*iSJ6~-IbsE+gZv>-0tYlWZw^p5iVei
zFfg<MW2AQL`fPI`8-zK4F;bFRT%uP}Q38&R*}$lnjTs#w8JWcjAcYEPnK`Kn$%%Oi
zNvR5nIVGt@sVNF2`3gxzsfpPN#hK}OnR)37$r-81*~NNX0p5&Ea?H50p9BL40J#iH
z8bK`7%*YDKjA#x)HW4#<A)8nPq)-wYD<rXDH4s<mAREZS%zzYDtdOw6FpZTBq=Oj<
LKLY7mW)Kemx+<e{
new file mode 100644
index 0000000000000000000000000000000000000000..6ba1efd727a26604778641ad78abbb0027c0bb5d
GIT binary patch
literal 691
zc$^FHW@Zs#U}E54cof_0*0-@S*nyFOp^J%uftx{wAv3SIBrzvPuP7xgG=!6ZS^a@r
zs45VbR&X;gvU~%o1ZrkrXbn2;ci2E+&u?v?s<Jt4Tq^@-1+DzTBF=Kh_tCa#e>&di
za@N@Pea={|^srg={JAsl*34NSt@=@XyPw3~u4jcpYq?uKU4P5>yQ^&V3yy&FDQ3RH
z=HEANWVedfn{MKnIZG(|ZRhW62cj0;)p6Qk?Kq>bbZO`_caM`zj<Zy_PlYa@=eke!
z*w1OZFLg=%k(g0COL)zq$yaYKIMq^iV2Mgs=EmoIdse-kQarVD6+^#G+4S8xlP~YH
zGHjpWDy6>AY+vB{9m*{4*IaCvU(y<Hu|b!)jnmRPV_C(=XVMRSLj@N%`!cAXTDeZ*
z!}sgYlQ{H!Pd>S5aYF3hg?)0rUpUq*TYQpfhHb%(D_ccWn++0gyuLckEhow;diuK)
zOaG(`E_UMlYxpFd<q&IuZ_3*a`!7d~4bHjdvvHrXZ(1N-=Jz*U`+|M|FcP?ck-)%k
z6&MM%Ti0is1KA+V0gQx_)Z!Aol8O>=3<v^4Ul20}LNYRo6+j9V(lT>W6_OM46p~UE
z5_3vYi&9e*O7azwic%A^6^b*{^D^_&6_PVjle3HUxB|QxndF#p#lHjt2mrYZOBz8e
rl+?fqNeyW3fEb9&FUSVA0x5*gSRp>cFpZTBq=N|v=L6|~Agv4lkSX$=
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -9,16 +9,17 @@ const XULAPPINFO_CONTRACTID = "@mozilla.
 const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
 
 const PREF_EM_CHECK_UPDATE_SECURITY   = "extensions.checkUpdateSecurity";
 const PREF_EM_STRICT_COMPATIBILITY    = "extensions.strictCompatibility";
 const PREF_EM_MIN_COMPAT_APP_VERSION      = "extensions.minCompatibleAppVersion";
 const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
 const PREF_GETADDONS_BYIDS               = "extensions.getAddons.get.url";
 const PREF_GETADDONS_BYIDS_PERFORMANCE   = "extensions.getAddons.getWithPerformance.url";
+const PREF_XPI_SIGNATURES_REQUIRED    = "xpinstall.signatures.required";
 
 // Forcibly end the test if it runs longer than 15 minutes
 const TIMEOUT_MS = 900000;
 
 Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
@@ -1448,16 +1449,19 @@ Services.prefs.setBoolPref("extensions.s
 
 // By default don't check for hotfixes
 Services.prefs.setCharPref("extensions.hotfix.id", "");
 
 // By default, set min compatible versions to 0
 Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0");
 Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0");
 
+// Disable signature checks for most tests
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false);
+
 // Register a temporary directory for the tests.
 const gTmpD = gProfD.clone();
 gTmpD.append("temp");
 gTmpD.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 registerDirectory("TmpD", gTmpD);
 
 // Write out an empty blocklist.xml file to the profile to ensure nothing
 // is blocklisted by default
@@ -1525,19 +1529,16 @@ do_register_cleanup(function addon_clean
   var testDir = gProfD.clone();
   testDir.append("extensions");
   testDir.append("trash");
   pathShouldntExist(testDir);
 
   testDir.leafName = "staged";
   pathShouldntExist(testDir);
 
-  testDir.leafName = "staged-xpis";
-  pathShouldntExist(testDir);
-
   shutdownManager();
 
   // Clear commonly set prefs.
   try {
     Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
   } catch (e) {}
   try {
     Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
@@ -192,45 +192,21 @@ function run_test() {
     // since, it should still be disabled but should be incompatible
     do_check_neq(a5, null);
     do_check_true(a5.userDisabled);
     do_check_true(a5.appDisabled);
     do_check_false(a5.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a5.id));
     do_check_false(a5.hasBinaryComponents);
 
-    // addon6 should be installed and compatible and packed unless unpacking is
-    // forced
-    do_check_neq(a6, null);
-    do_check_false(a6.userDisabled);
-    do_check_false(a6.appDisabled);
-    do_check_true(a6.isActive);
-    do_check_true(isExtensionInAddonsList(profileDir, a6.id));
-    if (Services.prefs.getBoolPref("extensions.alwaysUnpack"))
-      do_check_eq(a6.getResourceURI("install.rdf").scheme, "file");
-    else
-      do_check_eq(a6.getResourceURI("install.rdf").scheme, "jar");
-    do_check_false(a6.hasBinaryComponents);
-
-    // addon7 should be installed and compatible and unpacked
-    do_check_neq(a7, null);
-    do_check_false(a7.userDisabled);
-    do_check_false(a7.appDisabled);
-    do_check_true(a7.isActive);
-    do_check_true(isExtensionInAddonsList(profileDir, a7.id));
-    do_check_eq(a7.getResourceURI("install.rdf").scheme, "file");
-    do_check_false(a7.hasBinaryComponents);
-
-    // addon8 should be installed and compatible and have binary components
-    do_check_neq(a8, null);
-    do_check_false(a8.userDisabled);
-    do_check_false(a8.appDisabled);
-    do_check_true(a8.isActive);
-    do_check_true(isExtensionInAddonsList(profileDir, a8.id));
-    do_check_true(a8.hasBinaryComponents);
+    // addon6, addon7 and addon8 will have been lost as they were staged in the
+    // pre-Firefox 4.0 directory
+    do_check_eq(a6, null);
+    do_check_eq(a7, null);
+    do_check_eq(a8, null);
 
     // Theme 1 was previously enabled
     do_check_neq(t1, null);
     do_check_false(t1.userDisabled);
     do_check_false(t1.appDisabled);
     do_check_true(t1.isActive);
     do_check_true(isThemeInAddonsList(profileDir, t1.id));
     do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
@@ -238,13 +214,11 @@ function run_test() {
     // Theme 2 was previously disabled
     do_check_neq(t1, null);
     do_check_true(t2.userDisabled);
     do_check_false(t2.appDisabled);
     do_check_false(t2.isActive);
     do_check_false(isThemeInAddonsList(profileDir, t2.id));
     do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
 
-    do_check_false(stagedXPIs.exists());
-
     do_execute_soon(do_test_finished);
   });
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
@@ -191,35 +191,20 @@ function run_test() {
     // addon5 was disabled and compatible but a new version has been installed
     // since, it should still be disabled but should be incompatible
     do_check_neq(a5, null);
     do_check_true(a5.userDisabled);
     do_check_true(a5.appDisabled);
     do_check_false(a5.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a5.id));
 
-    // addon6 should be installed and compatible and packed unless unpacking is
-    // forced
-    do_check_neq(a6, null);
-    do_check_false(a6.userDisabled);
-    do_check_false(a6.appDisabled);
-    do_check_true(a6.isActive);
-    do_check_true(isExtensionInAddonsList(profileDir, a6.id));
-    if (Services.prefs.getBoolPref("extensions.alwaysUnpack"))
-      do_check_eq(a6.getResourceURI("install.rdf").scheme, "file");
-    else
-      do_check_eq(a6.getResourceURI("install.rdf").scheme, "jar");
-
-    // addon7 should be installed and compatible and unpacked
-    do_check_neq(a7, null);
-    do_check_false(a7.userDisabled);
-    do_check_false(a7.appDisabled);
-    do_check_true(a7.isActive);
-    do_check_true(isExtensionInAddonsList(profileDir, a7.id));
-    do_check_eq(a7.getResourceURI("install.rdf").scheme, "file");
+    // addon6 and addon7 will have been lost as they were staged in the
+    // pre-Firefox 4.0 directory
+    do_check_eq(a6, null);
+    do_check_eq(a7, null);
 
     // Theme 1 was previously disabled
     do_check_neq(t1, null);
     do_check_true(t1.userDisabled);
     do_check_false(t1.appDisabled);
     do_check_false(t1.isActive);
     do_check_true(isThemeInAddonsList(profileDir, t1.id));
     do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
@@ -227,13 +212,11 @@ function run_test() {
     // Theme 2 was previously disabled
     do_check_neq(t1, null);
     do_check_true(t2.userDisabled);
     do_check_false(t2.appDisabled);
     do_check_false(t2.isActive);
     do_check_false(isThemeInAddonsList(profileDir, t2.id));
     do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
 
-    do_check_false(stagedXPIs.exists());
-
     do_execute_soon(do_test_finished);
   });
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
@@ -0,0 +1,320 @@
+// Enable signature checks for these tests
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const ADDONS = {
+  bootstrap: {
+    unsigned: "unsigned_bootstrap_2.xpi",
+    badid: "signed_bootstrap_badid_2.xpi",
+    signed: "signed_bootstrap_2.xpi",
+  },
+  nonbootstrap: {
+    unsigned: "unsigned_nonbootstrap_2.xpi",
+    badid: "signed_nonbootstrap_badid_2.xpi",
+    signed: "signed_nonbootstrap_2.xpi",
+  }
+};
+const ID = "test@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Deletes a file from the test add-on in the profile
+function breakAddon(file) {
+  if (TEST_UNPACKED) {
+    file.append("test.txt");
+    file.remove(true);
+  }
+  else {
+    var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+               createInstance(AM_Ci.nsIZipWriter);
+    zipW.open(file, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+    zipW.removeEntry("test.txt", false);
+    zipW.close();
+  }
+}
+
+function resetPrefs() {
+  Services.prefs.setIntPref("bootstraptest.active_version", -1);
+  Services.prefs.setIntPref("bootstraptest.installed_version", -1);
+  Services.prefs.setIntPref("bootstraptest.startup_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.shutdown_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.install_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.uninstall_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.startup_oldversion", -1);
+  Services.prefs.setIntPref("bootstraptest.shutdown_newversion", -1);
+  Services.prefs.setIntPref("bootstraptest.install_oldversion", -1);
+  Services.prefs.setIntPref("bootstraptest.uninstall_newversion", -1);
+}
+
+function getActiveVersion() {
+  return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+  // Start and stop the manager to initialise everything in the profile before
+  // actual testing
+  startupManager();
+  shutdownManager();
+  resetPrefs();
+
+  run_next_test();
+}
+
+// Injecting into profile (bootstrap)
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.unsigned), profileDir, ID);
+
+  startupManager();
+
+  // Currently we leave the sideloaded add-on there but just don't run it
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+  do_check_eq(getActiveVersion(), -1);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+  resetPrefs();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID);
+  breakAddon(getFileForAddon(profileDir, ID));
+
+  startupManager();
+
+  // Currently we leave the sideloaded add-on there but just don't run it
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+  do_check_eq(getActiveVersion(), -1);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+  resetPrefs();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.badid), profileDir, ID);
+
+  startupManager();
+
+  // Currently we leave the sideloaded add-on there but just don't run it
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+  do_check_eq(getActiveVersion(), -1);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+  resetPrefs();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+// Installs a signed add-on then modifies it in place breaking its signing
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID);
+
+  // Make it appear to come from the past so when we modify it later it is
+  // detected during startup. Obviously malware can bypass this method of
+  // detection but the periodic scan will catch that
+  yield promiseSetExtensionModifiedTime(getFileForAddon(profileDir, ID).path, Date.now() - 600000);
+
+  startupManager();
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED);
+  do_check_eq(getActiveVersion(), 2);
+
+  yield promiseShutdownManager();
+  do_check_eq(getActiveVersion(), 0);
+
+  breakAddon(getFileForAddon(profileDir, ID));
+  resetPrefs();
+
+  startupManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+  do_check_eq(getActiveVersion(), -1);
+
+  let ids = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+  do_check_eq(ids.length, 1);
+  do_check_eq(ids[0], ID);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+  resetPrefs();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+// Injecting into profile (non-bootstrap)
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.unsigned), profileDir, ID);
+
+  startupManager();
+
+  // Currently we leave the sideloaded add-on there but just don't run it
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+  do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+  addon.uninstall();
+  yield promiseRestartManager();
+  yield promiseShutdownManager();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID);
+  breakAddon(getFileForAddon(profileDir, ID));
+
+  startupManager();
+
+  // Currently we leave the sideloaded add-on there but just don't run it
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+  do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+  addon.uninstall();
+  yield promiseRestartManager();
+  yield promiseShutdownManager();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.badid), profileDir, ID);
+
+  startupManager();
+
+  // Currently we leave the sideloaded add-on there but just don't run it
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+  do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+  addon.uninstall();
+  yield promiseRestartManager();
+  yield promiseShutdownManager();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+// Installs a signed add-on then modifies it in place breaking its signing
+add_task(function*() {
+  manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID);
+
+  // Make it appear to come from the past so when we modify it later it is
+  // detected during startup. Obviously malware can bypass this method of
+  // detection but the periodic scan will catch that
+  yield promiseSetExtensionModifiedTime(getFileForAddon(profileDir, ID).path, Date.now() - 60000);
+
+  startupManager();
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED);
+  do_check_true(isExtensionInAddonsList(profileDir, ID));
+
+  yield promiseShutdownManager();
+
+  breakAddon(getFileForAddon(profileDir, ID));
+
+  startupManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+  do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+  let ids = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+  do_check_eq(ids.length, 1);
+  do_check_eq(ids[0], ID);
+
+  addon.uninstall();
+  yield promiseRestartManager();
+  yield promiseShutdownManager();
+
+  do_check_false(getFileForAddon(profileDir, ID).exists());
+});
+
+// Stage install then modify before startup (non-bootstrap)
+add_task(function*() {
+  startupManager();
+  yield promiseInstallAllFiles([do_get_file(DATA + ADDONS.nonbootstrap.signed)]);
+  yield promiseShutdownManager();
+
+  let staged = profileDir.clone();
+  staged.append("staged");
+  staged.append(do_get_expected_addon_name(ID));
+  do_check_true(staged.exists());
+
+  breakAddon(staged);
+  startupManager();
+
+  // Should have refused to install the broken staged version
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon, null);
+
+  let install = getFileForAddon(profileDir, ID);
+  do_check_false(install.exists());
+
+  yield promiseShutdownManager();
+});
+
+// Manufacture staged install (bootstrap)
+add_task(function*() {
+  let stage = profileDir.clone();
+  stage.append("staged");
+
+  let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), stage, ID);
+  breakAddon(file);
+
+  startupManager();
+
+  // Should have refused to install the broken staged version
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon, null);
+  do_check_eq(getActiveVersion(), -1);
+
+  let install = getFileForAddon(profileDir, ID);
+  do_check_false(install.exists());
+
+  yield promiseShutdownManager();
+  resetPrefs();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js
@@ -0,0 +1,265 @@
+// Enable signature checks for these tests
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const ADDONS = {
+  bootstrap: {
+    unsigned: "unsigned_bootstrap_2.xpi",
+    badid: "signed_bootstrap_badid_2.xpi",
+    preliminary: "preliminary_bootstrap_2.xpi",
+    signed: "signed_bootstrap_2.xpi",
+  },
+};
+const WORKING = "signed_bootstrap_1.xpi";
+const ID = "test@tests.mozilla.org";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(4444);
+
+// Creates an add-on with a broken signature by changing an existing file
+function createBrokenAddonModify(file) {
+  let brokenFile = gTmpD.clone();
+  brokenFile.append("broken.xpi");
+  file.copyTo(brokenFile.parent, brokenFile.leafName);
+
+  var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+               createInstance(AM_Ci.nsIStringInputStream);
+  stream.setData("FOOBAR", -1);
+  var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+             createInstance(AM_Ci.nsIZipWriter);
+  zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+  zipW.removeEntry("test.txt", false);
+  zipW.addEntryStream("test.txt", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+                      stream, false);
+  zipW.close();
+
+  return brokenFile;
+}
+
+// Creates an add-on with a broken signature by adding a new file
+function createBrokenAddonAdd(file) {
+  let brokenFile = gTmpD.clone();
+  brokenFile.append("broken.xpi");
+  file.copyTo(brokenFile.parent, brokenFile.leafName);
+
+  var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+               createInstance(AM_Ci.nsIStringInputStream);
+  stream.setData("FOOBAR", -1);
+  var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+             createInstance(AM_Ci.nsIZipWriter);
+  zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+  zipW.addEntryStream("test2.txt", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+                      stream, false);
+  zipW.close();
+
+  return brokenFile;
+}
+
+// Creates an add-on with a broken signature by removing an existing file
+function createBrokenAddonRemove(file) {
+  let brokenFile = gTmpD.clone();
+  brokenFile.append("broken.xpi");
+  file.copyTo(brokenFile.parent, brokenFile.leafName);
+
+  var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+               createInstance(AM_Ci.nsIStringInputStream);
+  stream.setData("FOOBAR", -1);
+  var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+             createInstance(AM_Ci.nsIZipWriter);
+  zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+  zipW.removeEntry("test.txt", false);
+  zipW.close();
+
+  return brokenFile;
+}
+
+function createInstall(url) {
+  return new Promise(resolve => {
+    AddonManager.getInstallForURL(url, resolve, "application/x-xpinstall");
+  });
+}
+
+function serveUpdateRDF(leafName) {
+  gServer.registerPathHandler("/update.rdf", function(request, response) {
+    let updateData = {};
+    updateData[ID] = [{
+      version: "2.0",
+      targetApplications: [{
+        id: "xpcshell@tests.mozilla.org",
+        minVersion: "4",
+        maxVersion: "6",
+        updateLink: "http://localhost:4444/" + leafName
+      }]
+    }];
+
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.write(createUpdateRDF(updateData));
+  });
+}
+
+
+function* test_install_broken(file, expectedError) {
+  gServer.registerFile("/" + file.leafName, file);
+
+  let install = yield createInstall("http://localhost:4444/" + file.leafName);
+  yield promiseCompleteAllInstalls([install]);
+
+  do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+  do_check_eq(install.error, expectedError);
+  do_check_eq(install.addon, null);
+
+  gServer.registerFile("/" + file.leafName, null);
+}
+
+function* test_install_working(file, expectedSignedState) {
+  gServer.registerFile("/" + file.leafName, file);
+
+  let install = yield createInstall("http://localhost:4444/" + file.leafName);
+  yield promiseCompleteAllInstalls([install]);
+
+  do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+  do_check_neq(install.addon, null);
+  do_check_eq(install.addon.signedState, expectedSignedState);
+
+  gServer.registerFile("/" + file.leafName, null);
+
+  install.addon.uninstall();
+}
+
+function* test_update_broken(file, expectedError) {
+  // First install the older version
+  yield promiseInstallAllFiles([do_get_file(DATA + WORKING)]);
+
+  gServer.registerFile("/" + file.leafName, file);
+  serveUpdateRDF(file.leafName);
+
+  let addon = yield promiseAddonByID(ID);
+  let install = yield promiseFindAddonUpdates(addon);
+  yield promiseCompleteAllInstalls([install]);
+
+  do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+  do_check_eq(install.error, expectedError);
+  do_check_eq(install.addon, null);
+
+  gServer.registerFile("/" + file.leafName, null);
+  gServer.registerPathHandler("/update.rdf", null);
+
+  addon.uninstall();
+}
+
+function* test_update_working(file, expectedSignedState) {
+  // First install the older version
+  yield promiseInstallAllFiles([do_get_file(DATA + WORKING)]);
+
+  gServer.registerFile("/" + file.leafName, file);
+  serveUpdateRDF(file.leafName);
+
+  let addon = yield promiseAddonByID(ID);
+  let install = yield promiseFindAddonUpdates(addon);
+  yield promiseCompleteAllInstalls([install]);
+
+  do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+  do_check_neq(install.addon, null);
+  do_check_eq(install.addon.signedState, expectedSignedState);
+
+  gServer.registerFile("/" + file.leafName, null);
+  gServer.registerPathHandler("/update.rdf", null);
+
+  install.addon.uninstall();
+}
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+  startupManager();
+
+  run_next_test();
+}
+
+// Try to install a broken add-on
+add_task(function*() {
+  let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.bootstrap.signed));
+  yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+  file.remove(true);
+});
+
+add_task(function*() {
+  let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.bootstrap.signed));
+  yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+  file.remove(true);
+});
+
+add_task(function*() {
+  let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.bootstrap.signed));
+  yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+  file.remove(true);
+});
+
+// Try to install an add-on with an incorrect ID
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.badid);
+  yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+});
+
+// Try to install an unsigned add-on
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.unsigned);
+  yield test_install_broken(file, AddonManager.ERROR_SIGNEDSTATE_REQUIRED);
+});
+
+// Try to install a preliminarily reviewed add-on
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.preliminary);
+  yield test_install_working(file, AddonManager.SIGNEDSTATE_PRELIMINARY);
+});
+
+// Try to install a signed add-on
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.signed);
+  yield test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED);
+});
+
+// Try to update to a broken add-on
+add_task(function*() {
+  let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.bootstrap.signed));
+  yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+  file.remove(true);
+});
+
+add_task(function*() {
+  let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.bootstrap.signed));
+  yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+  file.remove(true);
+});
+
+add_task(function*() {
+  let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.bootstrap.signed));
+  yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+  file.remove(true);
+});
+
+// Try to update to an add-on with an incorrect ID
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.badid);
+  yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+});
+
+// Try to update to an unsigned add-on
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.unsigned);
+  yield test_update_broken(file, AddonManager.ERROR_SIGNEDSTATE_REQUIRED);
+});
+
+// Try to update to a preliminarily reviewed add-on
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.preliminary);
+  yield test_update_working(file, AddonManager.SIGNEDSTATE_PRELIMINARY);
+});
+
+// Try to update to a signed add-on
+add_task(function*() {
+  let file = do_get_file(DATA + ADDONS.bootstrap.signed);
+  yield test_update_working(file, AddonManager.SIGNEDSTATE_SIGNED);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
@@ -0,0 +1,164 @@
+// Enable signature checks for these tests
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const ADDONS = {
+  bootstrap: {
+    unsigned: "unsigned_bootstrap_2.xpi",
+    badid: "signed_bootstrap_badid_2.xpi",
+    signed: "signed_bootstrap_2.xpi",
+  },
+  nonbootstrap: {
+    unsigned: "unsigned_nonbootstrap_2.xpi",
+    badid: "signed_nonbootstrap_badid_2.xpi",
+    signed: "signed_nonbootstrap_2.xpi",
+  }
+};
+const ID = "test@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function resetPrefs() {
+  Services.prefs.setIntPref("bootstraptest.active_version", -1);
+  Services.prefs.setIntPref("bootstraptest.installed_version", -1);
+  Services.prefs.setIntPref("bootstraptest.startup_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.shutdown_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.install_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.uninstall_reason", -1);
+  Services.prefs.setIntPref("bootstraptest.startup_oldversion", -1);
+  Services.prefs.setIntPref("bootstraptest.shutdown_newversion", -1);
+  Services.prefs.setIntPref("bootstraptest.install_oldversion", -1);
+  Services.prefs.setIntPref("bootstraptest.uninstall_newversion", -1);
+}
+
+function getActiveVersion() {
+  return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+  // Start and stop the manager to initialise everything in the profile before
+  // actual testing
+  startupManager();
+  shutdownManager();
+  resetPrefs();
+
+  run_next_test();
+}
+
+// Removes the signedState field from add-ons in the json database to make it
+// look like the database was written with an older version of the application
+function stripDB() {
+  let jData = loadJSON(gExtensionsJSON);
+  jData.schemaVersion--;
+
+  for (let addon of jData.addons)
+    delete addon.signedState;
+
+  saveJSON(jData, gExtensionsJSON);
+}
+
+function* test_breaking_migrate(addons, test, expectedSignedState) {
+  // Startup as the old version
+  gAppInfo.version = "4";
+  startupManager(true);
+
+  // Install the signed add-on
+  yield promiseInstallAllFiles([do_get_file(DATA + addons.signed)]);
+  // Restart to let non-restartless add-ons install fully
+  yield promiseRestartManager();
+  yield promiseShutdownManager();
+  resetPrefs();
+  stripDB();
+
+  // Now replace it with the version to test. Doing this so quickly shouldn't
+  // trigger the file modification code to detect the change by itself.
+  manuallyUninstall(profileDir, ID);
+  manuallyInstall(do_get_file(DATA + addons[test]), profileDir, ID);
+
+  // Update the application
+  gAppInfo.version = "5";
+  startupManager(true);
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_true(addon.appDisabled);
+  do_check_false(addon.isActive);
+  do_check_eq(addon.signedState, expectedSignedState);
+
+  // Add-on shouldn't be active
+  if (addons == ADDONS.bootstrap)
+    do_check_eq(getActiveVersion(), -1);
+  else
+    do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+  // Should have flagged the change during startup
+  let changes = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+  do_check_eq(changes.length, 1);
+  do_check_eq(changes[0], ID);
+
+  addon.uninstall();
+  // Restart to let non-restartless add-ons uninstall fully
+  yield promiseRestartManager();
+  yield shutdownManager();
+  resetPrefs();
+}
+
+function* test_working_migrate(addons, test, expectedSignedState) {
+  // Startup as the old version
+  gAppInfo.version = "4";
+  startupManager(true);
+
+  // Install the signed add-on
+  yield promiseInstallAllFiles([do_get_file(DATA + addons.signed)]);
+  // Restart to let non-restartless add-ons install fully
+  yield promiseRestartManager();
+  yield promiseShutdownManager();
+  resetPrefs();
+  stripDB();
+
+  // Now replace it with the version to test. Doing this so quickly shouldn't
+  // trigger the file modification code to detect the change by itself.
+  manuallyUninstall(profileDir, ID);
+  manuallyInstall(do_get_file(DATA + addons[test]), profileDir, ID);
+
+  // Update the application
+  gAppInfo.version = "5";
+  startupManager(true);
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_eq(addon.signedState, expectedSignedState);
+
+  if (addons == ADDONS.bootstrap)
+    do_check_eq(getActiveVersion(), 2);
+  else
+    do_check_true(isExtensionInAddonsList(profileDir, ID));
+
+  addon.uninstall();
+  // Restart to let non-restartless add-ons uninstall fully
+  yield promiseRestartManager();
+  yield shutdownManager();
+  resetPrefs();
+}
+
+add_task(function*() {
+  yield test_breaking_migrate(ADDONS.bootstrap, "unsigned", AddonManager.SIGNEDSTATE_MISSING);
+  yield test_breaking_migrate(ADDONS.nonbootstrap, "unsigned", AddonManager.SIGNEDSTATE_MISSING);
+});
+
+add_task(function*() {
+  yield test_breaking_migrate(ADDONS.bootstrap, "badid", AddonManager.SIGNEDSTATE_BROKEN);
+  yield test_breaking_migrate(ADDONS.nonbootstrap, "badid", AddonManager.SIGNEDSTATE_BROKEN);
+});
+
+add_task(function*() {
+  yield test_working_migrate(ADDONS.bootstrap, "signed", AddonManager.SIGNEDSTATE_SIGNED);
+  yield test_working_migrate(ADDONS.nonbootstrap, "signed", AddonManager.SIGNEDSTATE_SIGNED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -17,12 +17,17 @@ skip-if = appname != "firefox"
 [test_isReady.js]
 [test_metadata_update.js]
 [test_pluginInfoURL.js]
 [test_provider_markSafe.js]
 [test_provider_shutdown.js]
 [test_provider_unsafe_access_shutdown.js]
 [test_provider_unsafe_access_startup.js]
 [test_shutdown.js]
+[test_signed_inject.js]
+skip-if = true
+[test_signed_install.js]
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_signed_migrate.js]
 [test_XPIcancel.js]
 [test_XPIStates.js]
 
 [include:xpcshell-shared.ini]