jarsigner: allow weak signatures

openjdk-11 11.0.17 in Debian unstable fails to verify weak signatures:

jarsigner -verbose -strict -verify tests/signindex/guardianproject.jar

         131 Fri Dec 02 20:10:00 CET 2016 META-INF/MANIFEST.MF
         252 Fri Dec 02 20:10:04 CET 2016 META-INF/1.SF
        2299 Fri Dec 02 20:10:04 CET 2016 META-INF/1.RSA
           0 Fri Dec 02 20:09:58 CET 2016 META-INF/
 m  ?  48743 Fri Dec 02 20:09:58 CET 2016 index.xml

  s = signature was verified
  m = entry is listed in manifest
  k = at least one certificate was found in keystore
  ? = unsigned entry

- Signed by "EMAILADDRESS=root@guardianproject.info, CN=guardianproject.info, O=Guardian Project, OU=FDroid Repo, L=New York, ST=New York, C=US"
    Digest algorithm: SHA1 (disabled)
    Signature algorithm: SHA1withRSA (disabled), 4096-bit key

WARNING: The jar will be treated as unsigned, because it is signed with a weak algorithm that is now disabled by the security property:

  jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, DSA keySize < 1024, SHA1 denyAfter 2019-01-01, include jdk.disabled.namedCurves
This commit is contained in:
Jochen Sprickerhof 2022-10-29 22:09:07 +02:00
parent d4b6e95c4e
commit 1bb963d768
No known key found for this signature in database
GPG key ID: 5BFFDCC258E69433
5 changed files with 64 additions and 84 deletions

View file

@ -3416,13 +3416,27 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir, v1_only=None):
return None return None
def verify_jar_signature(jar): def verify_deprecated_jar_signature(jar):
"""Verify the signature of a given JAR file. """Verify the signature of a given JAR file.
jarsigner is very shitty: unsigned JARs pass as "verified"! So jarsigner is very shitty: unsigned JARs pass as "verified"! So
this has to turn on -strict then check for result 4, since this this has to turn on -strict then check for result 4, since this
does not expect the signature to be from a CA-signed certificate. does not expect the signature to be from a CA-signed certificate.
Also used to verify the signature on an archived APK, supporting deprecated
algorithms.
F-Droid aims to keep every single binary that it ever published. Therefore,
it needs to be able to verify APK signatures that include deprecated/removed
algorithms. For example, jarsigner treats an MD5 signature as unsigned.
jarsigner passes unsigned APKs as "verified"! So this has to turn
on -strict then check for result 4.
Just to be safe, this never reuses the file, and locks down the
file permissions while in use. That should prevent a bad actor
from changing the settings during operation.
Raises Raises
------ ------
VerificationException VerificationException
@ -3430,15 +3444,30 @@ def verify_jar_signature(jar):
""" """
error = _('JAR signature failed to verify: {path}').format(path=jar) error = _('JAR signature failed to verify: {path}').format(path=jar)
_java_security = os.path.join(os.getcwd(), '.java.security')
if os.path.exists(_java_security):
os.remove(_java_security)
with open(_java_security, 'w') as fp:
fp.write('jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024')
os.chmod(_java_security, 0o400)
try: try:
output = subprocess.check_output([config['jarsigner'], '-strict', '-verify', jar], cmd = [
stderr=subprocess.STDOUT) config['jarsigner'],
'-J-Djava.security.properties=' + _java_security,
'-strict', '-verify', jar
]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
raise VerificationException(error + '\n' + output.decode('utf-8')) raise VerificationException(error + '\n' + output.decode('utf-8'))
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
if e.returncode == 4: if e.returncode == 4:
logging.debug(_('JAR signature verified: {path}').format(path=jar)) logging.debug(_('JAR signature verified: {path}').format(path=jar))
else: else:
raise VerificationException(error + '\n' + e.output.decode('utf-8')) from e raise VerificationException(error + '\n' + e.output.decode('utf-8')) from e
finally:
if os.path.exists(_java_security):
os.chmod(_java_security, 0o600)
os.remove(_java_security)
def verify_apk_signature(apk, min_sdk_version=None): def verify_apk_signature(apk, min_sdk_version=None):
@ -3471,63 +3500,13 @@ def verify_apk_signature(apk, min_sdk_version=None):
config['jarsigner_warning_displayed'] = True config['jarsigner_warning_displayed'] = True
logging.warning(_("Using Java's jarsigner, not recommended for verifying APKs! Use apksigner")) logging.warning(_("Using Java's jarsigner, not recommended for verifying APKs! Use apksigner"))
try: try:
verify_jar_signature(apk) verify_deprecated_jar_signature(apk)
return True return True
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
return False return False
def verify_old_apk_signature(apk):
"""Verify the signature on an archived APK, supporting deprecated algorithms.
F-Droid aims to keep every single binary that it ever published. Therefore,
it needs to be able to verify APK signatures that include deprecated/removed
algorithms. For example, jarsigner treats an MD5 signature as unsigned.
jarsigner passes unsigned APKs as "verified"! So this has to turn
on -strict then check for result 4.
Just to be safe, this never reuses the file, and locks down the
file permissions while in use. That should prevent a bad actor
from changing the settings during operation.
Returns
-------
Boolean
whether the APK was verified
"""
_java_security = os.path.join(os.getcwd(), '.java.security')
if os.path.exists(_java_security):
os.remove(_java_security)
with open(_java_security, 'w') as fp:
fp.write('jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024')
os.chmod(_java_security, 0o400)
try:
cmd = [
config['jarsigner'],
'-J-Djava.security.properties=' + _java_security,
'-strict', '-verify', apk
]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
if e.returncode != 4:
output = e.output
else:
logging.debug(_('JAR signature verified: {path}').format(path=apk))
return True
finally:
if os.path.exists(_java_security):
os.chmod(_java_security, 0o600)
os.remove(_java_security)
logging.error(_('Old APK signature failed to verify: {path}').format(path=apk)
+ '\n' + output.decode('utf-8'))
return False
apk_badchars = re.compile('''[/ :;'"]''') apk_badchars = re.compile('''[/ :;'"]''')
@ -3749,7 +3728,7 @@ def load_stats_fdroid_signing_key_fingerprints():
if not os.path.isfile(jar_file): if not os.path.isfile(jar_file):
return {} return {}
try: try:
verify_jar_signature(jar_file) verify_deprecated_jar_signature(jar_file)
except VerificationException as e: except VerificationException as e:
raise FDroidException("Signature validation of '{}' failed! " raise FDroidException("Signature validation of '{}' failed! "
"Please run publish again to rebuild this file.".format(jar_file)) from e "Please run publish again to rebuild this file.".format(jar_file)) from e

View file

@ -1518,7 +1518,7 @@ def get_index_from_jar(jarfile, fingerprint=None):
""" """
logging.debug(_('Verifying index signature:')) logging.debug(_('Verifying index signature:'))
common.verify_jar_signature(jarfile) common.verify_deprecated_jar_signature(jarfile)
with zipfile.ZipFile(jarfile) as jar: with zipfile.ZipFile(jarfile) as jar:
public_key, public_key_fingerprint = get_public_key_from_jar(jar) public_key, public_key_fingerprint = get_public_key_from_jar(jar)
if fingerprint is not None: if fingerprint is not None:

View file

@ -50,7 +50,7 @@ from . import _
from . import common from . import common
from . import index from . import index
from . import metadata from . import metadata
from .exception import BuildException, FDroidException from .exception import BuildException, FDroidException, VerificationException
from PIL import Image, PngImagePlugin from PIL import Image, PngImagePlugin
@ -1532,9 +1532,10 @@ def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=Fal
skipapk = False skipapk = False
if not common.verify_apk_signature(apkfile): if not common.verify_apk_signature(apkfile):
if repodir == 'archive' or allow_disabled_algorithms: if repodir == 'archive' or allow_disabled_algorithms:
if common.verify_old_apk_signature(apkfile): try:
common.verify_deprecated_jar_signature(apkfile)
apk['antiFeatures'].update(['KnownVuln', 'DisabledAlgorithm']) apk['antiFeatures'].update(['KnownVuln', 'DisabledAlgorithm'])
else: except VerificationException:
skipapk = True skipapk = True
else: else:
skipapk = True skipapk = True

View file

@ -39,7 +39,8 @@ import fdroidserver.signindex
import fdroidserver.common import fdroidserver.common
import fdroidserver.metadata import fdroidserver.metadata
from testcommon import TmpCwd from testcommon import TmpCwd
from fdroidserver.exception import FDroidException, VCSException, MetaDataException from fdroidserver.exception import FDroidException, VCSException,\
MetaDataException, VerificationException
class CommonTest(unittest.TestCase): class CommonTest(unittest.TestCase):
@ -484,34 +485,33 @@ class CommonTest(unittest.TestCase):
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
fdroidserver.common.config = config fdroidserver.common.config = config
self.assertTrue(fdroidserver.common.verify_old_apk_signature('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')) try:
self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_1.apk')) fdroidserver.common.verify_deprecated_jar_signature('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')
self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_2.apk')) fdroidserver.common.verify_deprecated_jar_signature('org.bitbucket.tickytacky.mirrormirror_1.apk')
self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_3.apk')) fdroidserver.common.verify_deprecated_jar_signature('org.bitbucket.tickytacky.mirrormirror_2.apk')
self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_4.apk')) fdroidserver.common.verify_deprecated_jar_signature('org.bitbucket.tickytacky.mirrormirror_3.apk')
self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.dyndns.fules.ck_20.apk')) fdroidserver.common.verify_deprecated_jar_signature('org.bitbucket.tickytacky.mirrormirror_4.apk')
self.assertTrue(fdroidserver.common.verify_old_apk_signature('urzip.apk')) fdroidserver.common.verify_deprecated_jar_signature('org.dyndns.fules.ck_20.apk')
self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-badcert.apk')) fdroidserver.common.verify_deprecated_jar_signature('urzip.apk')
self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-badsig.apk')) fdroidserver.common.verify_deprecated_jar_signature('urzip-release.apk')
self.assertTrue(fdroidserver.common.verify_old_apk_signature('urzip-release.apk')) except VerificationException:
self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-release-unsigned.apk')) self.fail("failed to jarsigner failed to verify an old apk")
self.assertRaises(VerificationException, fdroidserver.common.verify_deprecated_jar_signature, 'urzip-badcert.apk')
self.assertRaises(VerificationException, fdroidserver.common.verify_deprecated_jar_signature, 'urzip-badsig.apk')
self.assertRaises(VerificationException, fdroidserver.common.verify_deprecated_jar_signature, 'urzip-release-unsigned.apk')
def test_verify_jar_signature_succeeds(self): def test_verify_deprecated_jar_signature(self):
config = fdroidserver.common.read_config(fdroidserver.common.options)
fdroidserver.common.config = config
source_dir = os.path.join(self.basedir, 'signindex')
for f in ('testy.jar', 'guardianproject.jar'):
testfile = os.path.join(source_dir, f)
fdroidserver.common.verify_jar_signature(testfile)
def test_verify_jar_signature_fails(self):
config = fdroidserver.common.read_config(fdroidserver.common.options) config = fdroidserver.common.read_config(fdroidserver.common.options)
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
fdroidserver.common.config = config fdroidserver.common.config = config
source_dir = os.path.join(self.basedir, 'signindex') source_dir = os.path.join(self.basedir, 'signindex')
for f in ('testy.jar', 'guardianproject.jar'):
testfile = os.path.join(source_dir, f)
fdroidserver.common.verify_deprecated_jar_signature(testfile)
testfile = os.path.join(source_dir, 'unsigned.jar') testfile = os.path.join(source_dir, 'unsigned.jar')
with self.assertRaises(fdroidserver.index.VerificationException): with self.assertRaises(fdroidserver.index.VerificationException):
fdroidserver.common.verify_jar_signature(testfile) fdroidserver.common.verify_deprecated_jar_signature(testfile)
def test_verify_apks(self): def test_verify_apks(self):
config = fdroidserver.common.read_config(fdroidserver.common.options) config = fdroidserver.common.read_config(fdroidserver.common.options)

View file

@ -103,7 +103,7 @@ class SignindexTest(unittest.TestCase):
# index.jar aka v0 must by signed by SHA1withRSA # index.jar aka v0 must by signed by SHA1withRSA
f = 'repo/index.jar' f = 'repo/index.jar'
common.verify_jar_signature(f) common.verify_deprecated_jar_signature(f)
self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False)) self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False))
cp = subprocess.run( cp = subprocess.run(
['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE ['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE
@ -112,7 +112,7 @@ class SignindexTest(unittest.TestCase):
# index-v1.jar must by signed by SHA1withRSA # index-v1.jar must by signed by SHA1withRSA
f = 'repo/index-v1.jar' f = 'repo/index-v1.jar'
common.verify_jar_signature(f) common.verify_deprecated_jar_signature(f)
self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False)) self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False))
cp = subprocess.run( cp = subprocess.run(
['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE ['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE
@ -121,7 +121,7 @@ class SignindexTest(unittest.TestCase):
# entry.jar aka index v2 must by signed by a modern algorithm # entry.jar aka index v2 must by signed by a modern algorithm
f = 'repo/entry.jar' f = 'repo/entry.jar'
common.verify_jar_signature(f) common.verify_deprecated_jar_signature(f)
self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False)) self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False))
cp = subprocess.run( cp = subprocess.run(
['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE ['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE