1.0 Basic Verification Using The java.util.jar.JarFile API
1.1 JARVerifierV1
A class that does basic verification of a signed JAR using the java.util.jar.JarFile
API.
final class JARVerifierV1
implements
JARVerifier
{
public boolean verify(File theFile)
{
Log.d(TAG, "verify: verifying " + theFile);
try
{
JarFile jf = new JarFile(theFile, true);
try
{
verify(jf);
Log.d(TAG, "verify: " + theFile + " verified");
return true;
}
finally
{
jf.close();
}
}
catch (Exception e)
{
Log.d(TAG, "verify: verification failed");
e.printStackTrace();
return false;
}
}
//
JARVerifierV1()
{
buffer = new byte[8192];
}
//
private void verify(JarFile theJARFile)
throws
Exception
{
Enumeration<JarEntry> entries = theJARFile.entries();
while (entries.hasMoreElements())
{
verify(theJARFile, entries.nextElement());
}
}
private void verify(JarFile theJARFile, JarEntry theEntry)
throws
Exception
{
Log.d(TAG, "verify: " + theEntry.getName());
InputStream is = theJARFile.getInputStream(theEntry);
while (is.read(buffer) != -1)
{
}
}
//
private byte[] buffer;
//
private static final String TAG = "JARVerifierV1";
}
1.1 Verification Of A Vanilla APK
If we create a vanilla APK which looks like this
Archive: GoodApp.apk
Length Date Time Name
-------- ---- ---- ----
624 07-10-13 21:11 res/layout/activity_main.xml
464 07-10-13 21:11 res/menu/activity_main.xml
1588 07-10-13 21:11 AndroidManifest.xml
2504 07-10-13 21:11 resources.arsc
409 07-10-13 21:11 res/drawable-hdpi/ic_action_search.png
4129 07-10-13 21:11 res/drawable-hdpi/ic_launcher.png
1756 07-10-13 21:11 res/drawable-ldpi/ic_launcher.png
311 07-10-13 21:11 res/drawable-mdpi/ic_action_search.png
2654 07-10-13 21:11 res/drawable-mdpi/ic_launcher.png
491 07-10-13 21:11 res/drawable-xhdpi/ic_action_search.png
5456 07-10-13 21:11 res/drawable-xhdpi/ic_launcher.png
366112 07-10-13 21:11 classes.dex
1034 07-10-13 21:11 META-INF/MANIFEST.MF
1087 07-10-13 21:11 META-INF/CERT.SF
776 07-10-13 21:11 META-INF/CERT.RSA
-------- -------
389395 15 files
and we attempt to verify it using JARVerifierV1
we succeed.
...
D/JARVerifierV1( 281): verify: verifying /data/data/xper.jv/files/GoodApp.apk
D/JARVerifierV1( 281): verify: res/layout/activity_main.xml
D/JARVerifierV1( 281): verify: res/menu/activity_main.xml
D/JARVerifierV1( 281): verify: AndroidManifest.xml
D/JARVerifierV1( 281): verify: resources.arsc
D/JARVerifierV1( 281): verify: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV1( 281): verify: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV1( 281): verify: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV1( 281): verify: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV1( 281): verify: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV1( 281): verify: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV1( 281): verify: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV1( 281): verify: classes.dex
D/JARVerifierV1( 281): verify: META-INF/MANIFEST.MF
D/JARVerifierV1( 281): verify: META-INF/CERT.SF
D/JARVerifierV1( 281): verify: META-INF/CERT.RSA
D/JARVerifierV1( 281): verify: /data/data/xper.jv/files/GoodApp.apk verified
...
This is what we would expect but it is not much of a test.
We need to know what happens when we modify a signed JAR in some way.
1.2 Verification After File Modification
If we modify our vanilla APK by replacing the original version of AndroidManifest.xml
with an empty one
Archive: GoodApp_modified.apk
Length Date Time Name
-------- ---- ---- ----
624 07-10-13 21:11 res/layout/activity_main.xml
464 07-10-13 21:11 res/menu/activity_main.xml
0 07-21-13 11:11 AndroidManifest.xml
2504 07-10-13 21:11 resources.arsc
409 07-10-13 21:11 res/drawable-hdpi/ic_action_search.png
4129 07-10-13 21:11 res/drawable-hdpi/ic_launcher.png
1756 07-10-13 21:11 res/drawable-ldpi/ic_launcher.png
311 07-10-13 21:11 res/drawable-mdpi/ic_action_search.png
2654 07-10-13 21:11 res/drawable-mdpi/ic_launcher.png
491 07-10-13 21:11 res/drawable-xhdpi/ic_action_search.png
5456 07-10-13 21:11 res/drawable-xhdpi/ic_launcher.png
366112 07-10-13 21:11 classes.dex
1034 07-10-13 21:11 META-INF/MANIFEST.MF
1087 07-10-13 21:11 META-INF/CERT.SF
776 07-10-13 21:11 META-INF/CERT.RSA
-------- -------
387807 15 files
and then attempt to verify it we get a SecurityException which is what we would expect.
...
D/JARVerifierV1( 280): verify: verifying /data/data/xper.jv/files/GoodApp_modified.apk
D/JARVerifierV1( 280): verify: res/layout/activity_main.xml
D/JARVerifierV1( 280): verify: res/menu/activity_main.xml
D/JARVerifierV1( 280): verify: AndroidManifest.xml
D/JARVerifierV1( 280): verify: verification failed
W/System.err( 280): java.lang.SecurityException: META-INF/MANIFEST.MF has \
invalid digest for AndroidManifest.xml in /data/data/xper.jv/files/GoodApp_modified.apk
W/System.err( 280): at java.util.jar.JarVerifier$VerifierEntry.verify(JarVerifier.java:129)
W/System.err( 280): at java.util.jar.JarFile$JarFileInputStream.read(JarFile.java:127)
W/System.err( 280): at java.io.FilterInputStream.read(FilterInputStream.java:130)
W/System.err( 280): at xper.jv.JARVerifierV1.verify(JARVerifierV1.java:71)
W/System.err( 280): at xper.jv.JARVerifierV1.verify(JARVerifierV1.java:58)
W/System.err( 280): at xper.jv.JARVerifierV1.verify(JARVerifierV1.java:24)
W/System.err( 280): at xper.jv.XperJVActivity.verify(XperJVActivity.java:127)
W/System.err( 280): at xper.jv.XperJVActivity.access$0(XperJVActivity.java:117)
W/System.err( 280): at xper.jv.XperJVActivity$1.run(XperJVActivity.java:111)
W/System.err( 280): at java.lang.Thread.run(Thread.java:1096)
...
1.3 Verification After File Addition
If we add the file assets/new_asset
to our vanilla APK
Archive: GoodApp_added.apk
Length Date Time Name
-------- ---- ---- ----
624 07-10-13 21:11 res/layout/activity_main.xml
464 07-10-13 21:11 res/menu/activity_main.xml
1588 07-10-13 21:11 AndroidManifest.xml
2504 07-10-13 21:11 resources.arsc
409 07-10-13 21:11 res/drawable-hdpi/ic_action_search.png
4129 07-10-13 21:11 res/drawable-hdpi/ic_launcher.png
1756 07-10-13 21:11 res/drawable-ldpi/ic_launcher.png
311 07-10-13 21:11 res/drawable-mdpi/ic_action_search.png
2654 07-10-13 21:11 res/drawable-mdpi/ic_launcher.png
491 07-10-13 21:11 res/drawable-xhdpi/ic_action_search.png
5456 07-10-13 21:11 res/drawable-xhdpi/ic_launcher.png
366112 07-10-13 21:11 classes.dex
1034 07-10-13 21:11 META-INF/MANIFEST.MF
1087 07-10-13 21:11 META-INF/CERT.SF
776 07-10-13 21:11 META-INF/CERT.RSA
48 07-24-13 21:56 assets/new_asset
-------- -------
389443 16 files
and then we attempt to verify it we succeed which is a bit disconcerting.
...
D/JARVerifierV1( 285): verify: verifying /data/data/xper.jv/files/GoodApp_added.apk
D/JARVerifierV1( 285): verify: res/layout/activity_main.xml
D/JARVerifierV1( 285): verify: res/menu/activity_main.xml
D/JARVerifierV1( 285): verify: AndroidManifest.xml
D/JARVerifierV1( 285): verify: resources.arsc
D/JARVerifierV1( 285): verify: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV1( 285): verify: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV1( 285): verify: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV1( 285): verify: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV1( 285): verify: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV1( 285): verify: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV1( 285): verify: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV1( 285): verify: classes.dex
D/JARVerifierV1( 285): verify: META-INF/MANIFEST.MF
D/JARVerifierV1( 285): verify: META-INF/CERT.SF
D/JARVerifierV1( 285): verify: META-INF/CERT.RSA
D/JARVerifierV1( 285): verify: assets/new_asset
D/JARVerifierV1( 285): verify: /data/data/xper.jv/files/GoodApp_added.apk verified
...
In fact the JarFile
implementation does not distinguish between a file whose computed digest matches the digest specified in the manifest, and a file which does not have a digest specified in the manifest.
This is a feature just not necessarily a good one.
1.4 Verification After File Removal
If we were to remove a file from our vanilla APK then we know a priori that verification would still succeed because all we are doing is iterating over the files that are in the JAR and verifying them.
To determine whether files had been removed from the JAR would require additional code to look for digests and for the digests of digests.
2.0 Fun With Digests
We can modify the code so that it checks for digest entries in the manifest.
2.1 JarVerifierV2
final class JARVerifierV2
implements
JARVerifier
{
public boolean verify(File theFile)
{
Log.d(TAG, "verify: verifying " + theFile);
try
{
JarFile jf = new JarFile(theFile, true);
try
{
verify(jf);
Log.d(TAG, "verify: " + theFile + " verified");
return true;
}
finally
{
jf.close();
}
}
catch (Exception e)
{
Log.d(TAG, "verify: verification failed");
e.printStackTrace();
return false;
}
}
//
JARVerifierV2()
{
buffer = new byte[8192];
}
//
private void verify(JarFile theJARFile)
throws
Exception
{
Enumeration<JarEntry> entries = theJARFile.entries();
while (entries.hasMoreElements())
{
verify(theJARFile, entries.nextElement());
}
}
private void verify(JarFile theJARFile, JarEntry theEntry)
throws
Exception
{
Log.d(TAG, "verify: " + theEntry.getName());
ensureDigest(theEntry);
InputStream is = theJARFile.getInputStream(theEntry);
while (is.read(buffer) != -1)
{
}
}
private void ensureDigest(JarEntry theEntry)
throws
Exception
{
if (!theEntry.getName().startsWith(META_INF))
{
Log.d(TAG, "ensureDigest: " + theEntry.getName());
Attributes attributes = theEntry.getAttributes();
if (attributes != null)
{
for (Object o : attributes.keySet())
{
if (o.toString().endsWith(DIGEST_SUFFIX))
{
return;
}
}
}
throw new SecurityException("No digest attribute: " + theEntry.getName());
}
}
//
private byte[] buffer;
//
private static final String DIGEST_SUFFIX = "-Digest";
private static final String META_INF = "META-INF/";
//
private static final String TAG = "JARVerifierV2";
}
2.2 Verification After File Addition
This time when we attempt we get a SecurityException, which is nice
...
D/JARVerifierV2( 316): verify: verifying /data/data/xper.jv/files/GoodApp_added.apk
D/JARVerifierV2( 316): verify: res/layout/activity_main.xml
D/JARVerifierV2( 316): ensureDigest: res/layout/activity_main.xml
D/JARVerifierV2( 316): verify: res/menu/activity_main.xml
D/JARVerifierV2( 316): ensureDigest: res/menu/activity_main.xml
D/JARVerifierV2( 316): verify: AndroidManifest.xml
D/JARVerifierV2( 316): ensureDigest: AndroidManifest.xml
D/JARVerifierV2( 316): verify: resources.arsc
D/JARVerifierV2( 316): ensureDigest: resources.arsc
D/JARVerifierV2( 316): verify: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV2( 316): verify: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV2( 316): verify: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV2( 316): verify: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV2( 316): verify: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV2( 316): verify: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV2( 316): verify: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV2( 316): ensureDigest: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV2( 316): verify: classes.dex
D/JARVerifierV2( 316): ensureDigest: classes.dex
D/JARVerifierV2( 316): verify: META-INF/MANIFEST.MF
D/JARVerifierV2( 316): verify: META-INF/CERT.SF
D/JARVerifierV2( 316): verify: META-INF/CERT.RSA
D/JARVerifierV2( 316): verify: assets/new_asset
D/JARVerifierV2( 316): ensureDigest: assets/new_asset
D/JARVerifierV2( 316): verify: verification failed
W/System.err( 316): java.lang.SecurityException: No digest attribute: assets/new_asset
W/System.err( 316): at xper.jv.JARVerifierV2.ensureDigest(JARVerifierV2.java:100)
W/System.err( 316): at xper.jv.JARVerifierV2.verify(JARVerifierV2.java:70)
W/System.err( 316): at xper.jv.JARVerifierV2.verify(JARVerifierV2.java:59)
W/System.err( 316): at xper.jv.JARVerifierV2.verify(JARVerifierV2.java:25)
W/System.err( 316): at xper.jv.XperJVActivity.verify(XperJVActivity.java:127)
W/System.err( 316): at xper.jv.XperJVActivity.access$0(XperJVActivity.java:117)
W/System.err( 316): at xper.jv.XperJVActivity$1.run(XperJVActivity.java:111)
W/System.err( 316): at java.lang.Thread.run(Thread.java:1096)
...
2.3 Manipulating The Manifest
There is a slight problem with this approach, namely that it is possible to add entries to the manifest of a signed JAR.
We can add an entry for our new file to the manifest like so
Manifest-Version: 1.0
Created-By: 1.0 (Android)
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: TJE1tg63y88xLdIALKGpuLmgh0s=
Name: res/drawable-mdpi/ic_action_search.png
SHA1-Digest: XjltJdEB3tvakTn9CN7KwdaNT68=
Name: AndroidManifest.xml
SHA1-Digest: iXwTXVvLX3gY+ByGhybEJvIxP08=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: aM73uFWfPOOvq5Kk9Ffd3cWm0OQ=
Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: ioH2V9g4FYKkqpDHk7jPryMKtcE=
Name: res/layout/activity_main.xml
SHA1-Digest: zd1eNopMfZ7CmMk8GjyPGPpYloc=
Name: resources.arsc
SHA1-Digest: 7nYq90PW0OZVg8ytyjBEcwQhYiA=
Name: classes.dex
SHA1-Digest: b80KFg5gdqlORweY5LrUS87tbVU=
Name: res/drawable-hdpi/ic_action_search.png
SHA1-Digest: rOWnNxWCWKwkicf+FpYGMg1fX2Y=
Name: res/drawable-xhdpi/ic_action_search.png
SHA1-Digest: WzSVDDIHZpn0cFeuxgKRJete4TU=
Name: res/menu/activity_main.xml
SHA1-Digest: TfxVPQP5IRVlJun7A7PdOqdkp+I=
Name: res/drawable-ldpi/ic_launcher.png
SHA1-Digest: r6Mdl54h2qEvVnqgfsgxU4CysiI=
Name: assets/new_asset
SHA1-Digest: PW9oQUbA9MQOVYfbtHutF0urHCg=
and then create a new APK containing the added file and the modified manifest
Archive: GoodApp_added_modified_manifest.apk
Length Date Time Name
-------- ---- ---- ----
624 07-10-13 21:11 res/layout/activity_main.xml
464 07-10-13 21:11 res/menu/activity_main.xml
1588 07-10-13 21:11 AndroidManifest.xml
2504 07-10-13 21:11 resources.arsc
409 07-10-13 21:11 res/drawable-hdpi/ic_action_search.png
4129 07-10-13 21:11 res/drawable-hdpi/ic_launcher.png
1756 07-10-13 21:11 res/drawable-ldpi/ic_launcher.png
311 07-10-13 21:11 res/drawable-mdpi/ic_action_search.png
2654 07-10-13 21:11 res/drawable-mdpi/ic_launcher.png
491 07-10-13 21:11 res/drawable-xhdpi/ic_action_search.png
5456 07-10-13 21:11 res/drawable-xhdpi/ic_launcher.png
366112 07-10-13 21:11 classes.dex
1103 07-26-13 14:51 META-INF/MANIFEST.MF
1087 07-10-13 21:11 META-INF/CERT.SF
776 07-10-13 21:11 META-INF/CERT.RSA
48 07-24-13 21:56 assets/new_asset
-------- -------
389512 16 files
If we attempt to verify it we succeed once more
...
D/JARVerifierV2( 280): verify: verifying /data/data/xper.jv/files/GoodApp_added_modified_manifest.apk
D/JARVerifierV2( 280): verify: res/layout/activity_main.xml
D/JARVerifierV2( 280): ensureDigest: res/layout/activity_main.xml
D/JARVerifierV2( 280): verify: res/menu/activity_main.xml
D/JARVerifierV2( 280): ensureDigest: res/menu/activity_main.xml
D/JARVerifierV2( 280): verify: AndroidManifest.xml
D/JARVerifierV2( 280): ensureDigest: AndroidManifest.xml
D/JARVerifierV2( 280): verify: resources.arsc
D/JARVerifierV2( 280): ensureDigest: resources.arsc
D/JARVerifierV2( 280): verify: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV2( 280): verify: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV2( 280): verify: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV2( 280): verify: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV2( 280): verify: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV2( 280): verify: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV2( 280): verify: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV2( 280): ensureDigest: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV2( 280): verify: classes.dex
D/JARVerifierV2( 280): ensureDigest: classes.dex
D/JARVerifierV2( 280): verify: META-INF/MANIFEST.MF
D/JARVerifierV2( 280): verify: META-INF/CERT.SF
D/JARVerifierV2( 280): verify: META-INF/CERT.RSA
D/JARVerifierV2( 280): verify: assets/new_asset
D/JARVerifierV2( 280): ensureDigest: assets/new_asset
D/JARVerifierV2( 280): verify: /data/data/xper.jv/files/GoodApp_added_modified_manifest.apk verified
...
Back to square one.
2.4 But What About The Digest Manifest ?
Given that the signature file contains a digest for the manifest and that has not been changed why is it possible to modify the manifest ?
The answer is because the digest manifest is considered to be an ‘optimization’ not a way of preventing the manifest from being changed.
If the computed digest of the manifest matches the one in the signature file then it is known to be unchanged and there is no need to check the individual digest entries in the manifest.
This is the ‘optimization’
If the computed digest of the manifest does not match then the individual digests in the signature file are used to check the corresponding digest entries in the manifest.
3.0 It Will Have To Be The Certificates Then
3.1 The JarEntry getCertificates Method
The Javadoc for the JerEntry
getCertificates
method for the implementation we are using says
Returns an array of Certificate
Objects associated with this entry or null
if none exists. Make sure that the everything is read from the input stream before calling this method, or else the method returns null
.
The Javadoc for a.n.other implementation says
Returns the Certificate
objects for this entry, or null
if none. This method can only be called once the JarEntry
has been completely verified by reading from the entry input stream until the end of the stream has been reached. Otherwise, this method will return null
.
The returned certificate array comprises all the signer certificates that were used to verify this entry. Each signer certificate is followed by its supporting certificate chain (which may be empty). Each signer certificate and its supporting certificate chain are ordered bottom-to-top (i.e., with the signer certificate first and the (root) certificate authority last).
which is a bit more useful.
If the getCertificates
method returns an array of Certificate
s we know that there is a digest entry in the manifest and a digest of the digest entry in a signature file which has been ‘verified’
3.2 JARVerifierV3
final class JARVerifierV3
implements
JARVerifier
{
public boolean verify(File theFile)
{
Log.d(TAG, "verify: verifying " + theFile);
try
{
JarFile jf = new JarFile(theFile, true);
try
{
verify(jf);
Log.d(TAG, "verify: " + theFile + " verified");
return true;
}
finally
{
jf.close();
}
}
catch (Exception e)
{
Log.d(TAG, "verify: verification failed");
e.printStackTrace();
return false;
}
}
//
JARVerifierV3()
{
buffer = new byte[8192];
}
//
private void verify(JarFile theJARFile)
throws
Exception
{
Enumeration<JarEntry> entries = theJARFile.entries();
while (entries.hasMoreElements())
{
verify(theJARFile, entries.nextElement());
}
}
private void verify(JarFile theJARFile, JarEntry theEntry)
throws
Exception
{
Log.d(TAG, "verify: " + theEntry.getName());
InputStream is = theJARFile.getInputStream(theEntry);
while (is.read(buffer) != -1)
{
}
ensureCertificates(theEntry);
}
private void ensureCertificates(JarEntry theEntry)
throws
Exception
{
if (!theEntry.getName().startsWith(META_INF))
{
Log.d(TAG, "ensureCertificates: " + theEntry.getName());
Certificate[] certificates = theEntry.getCertificates();
if (certificates == null)
{
throw new SecurityException("No certificates: " + theEntry.getName());
}
}
}
//
private byte[] buffer;
//
private static final String META_INF = "META-INF/";
//
private static final String TAG = "JARVerifierV3";
}
3.3 Verification After File Addition
If we attempt to verify our APK with added file and modified manifest using JARVerifierV3
we return to the status quo ante.
...
D/JARVerifierV3( 280): verify: verifying /data/data/xper.jv/files/GoodApp_added_modified_manifest.apk
D/JARVerifierV3( 280): verify: res/layout/activity_main.xml
D/JARVerifierV3( 280): ensureCertificates: res/layout/activity_main.xml
D/JARVerifierV3( 280): verify: res/menu/activity_main.xml
D/JARVerifierV3( 280): ensureCertificates: res/menu/activity_main.xml
D/JARVerifierV3( 280): verify: AndroidManifest.xml
D/JARVerifierV3( 280): ensureCertificates: AndroidManifest.xml
D/JARVerifierV3( 280): verify: resources.arsc
D/JARVerifierV3( 280): ensureCertificates: resources.arsc
D/JARVerifierV3( 280): verify: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV3( 280): verify: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV3( 280): verify: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV3( 280): verify: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV3( 280): verify: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV3( 280): verify: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV3( 280): verify: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV3( 280): ensureCertificates: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV3( 280): verify: classes.dex
D/JARVerifierV3( 280): ensureCertificates: classes.dex
D/JARVerifierV3( 280): verify: META-INF/MANIFEST.MF
D/JARVerifierV3( 280): verify: META-INF/CERT.SF
D/JARVerifierV3( 280): verify: META-INF/CERT.RSA
D/JARVerifierV3( 280): verify: assets/new_asset
D/JARVerifierV3( 280): ensureCertificates: assets/new_asset
D/JARVerifierV3( 280): verify: verification failed
W/System.err( 280): java.lang.SecurityException: No certificates: assets/new_asset
W/System.err( 280): at xper.jv.JARVerifierV3.ensureCertificates(JARVerifierV3.java:95)
W/System.err( 280): at xper.jv.JARVerifierV3.verify(JARVerifierV3.java:80)
W/System.err( 280): at xper.jv.JARVerifierV3.verify(JARVerifierV3.java:60)
W/System.err( 280): at xper.jv.JARVerifierV3.verify(JARVerifierV3.java:26)
W/System.err( 280): at xper.jv.XperJVActivity.verify(XperJVActivity.java:128)
W/System.err( 280): at xper.jv.XperJVActivity.access$0(XperJVActivity.java:118)
W/System.err( 280): at xper.jv.XperJVActivity$1.run(XperJVActivity.java:112)
W/System.err( 280): at java.lang.Thread.run(Thread.java:1096)
...
3.4 Signing Additional Manifest Entries
Needless to say there is a catch. Not only is it possible to add digest entries to the manifest of a signed JAR it is possible to sign them as well.
We can create a second signature file BERT.SF
which contains the digest of the added manifest entry
Signature-Version: 1.0
Name: assets/new_asset
SHA1-Digest: 6G+NGDseZOsVO904ZG1VLkFAr30=
and a signed signature file BERT.DSA
and then create a new APK
Archive: GoodApp_added_modified_manifest_ds.apk
Length Date Time Name
-------- ---- ---- ----
624 07-10-13 21:11 res/layout/activity_main.xml
464 07-10-13 21:11 res/menu/activity_main.xml
1588 07-10-13 21:11 AndroidManifest.xml
2504 07-10-13 21:11 resources.arsc
409 07-10-13 21:11 res/drawable-hdpi/ic_action_search.png
4129 07-10-13 21:11 res/drawable-hdpi/ic_launcher.png
1756 07-10-13 21:11 res/drawable-ldpi/ic_launcher.png
311 07-10-13 21:11 res/drawable-mdpi/ic_action_search.png
2654 07-10-13 21:11 res/drawable-mdpi/ic_launcher.png
491 07-10-13 21:11 res/drawable-xhdpi/ic_action_search.png
5456 07-10-13 21:11 res/drawable-xhdpi/ic_launcher.png
366112 07-10-13 21:11 classes.dex
1103 07-26-13 14:51 META-INF/MANIFEST.MF
1087 07-10-13 21:11 META-INF/CERT.SF
776 07-10-13 21:11 META-INF/CERT.RSA
48 07-24-13 21:56 assets/new_asset
2365 07-26-13 22:10 META-INF/BERT.DSA
95 07-26-13 22:10 META-INF/BERT.SF
-------- -------
391972 18 files
Now our new file assets/new_asset
has certificates associated with it and verification succeeds one more
...
D/JARVerifierV3( 281): verify: verifying /data/data/xper.jv/files/GoodApp_added_modified_manifest_ds.apk
D/JARVerifierV3( 281): verify: res/layout/activity_main.xml
D/dalvikvm( 281): GC_FOR_MALLOC freed 4701 objects / 294968 bytes in 59ms
D/JARVerifierV3( 281): ensureCertificates: res/layout/activity_main.xml
D/JARVerifierV3( 281): verify: res/menu/activity_main.xml
D/JARVerifierV3( 281): ensureCertificates: res/menu/activity_main.xml
D/JARVerifierV3( 281): verify: AndroidManifest.xml
D/JARVerifierV3( 281): ensureCertificates: AndroidManifest.xml
D/JARVerifierV3( 281): verify: resources.arsc
D/JARVerifierV3( 281): ensureCertificates: resources.arsc
D/JARVerifierV3( 281): verify: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-hdpi/ic_action_search.png
D/JARVerifierV3( 281): verify: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-hdpi/ic_launcher.png
D/JARVerifierV3( 281): verify: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-ldpi/ic_launcher.png
D/JARVerifierV3( 281): verify: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-mdpi/ic_action_search.png
D/JARVerifierV3( 281): verify: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-mdpi/ic_launcher.png
D/JARVerifierV3( 281): verify: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-xhdpi/ic_action_search.png
D/JARVerifierV3( 281): verify: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV3( 281): ensureCertificates: res/drawable-xhdpi/ic_launcher.png
D/JARVerifierV3( 281): verify: classes.dex
D/JARVerifierV3( 281): ensureCertificates: classes.dex
D/JARVerifierV3( 281): verify: META-INF/MANIFEST.MF
D/JARVerifierV3( 281): verify: META-INF/CERT.SF
D/JARVerifierV3( 281): verify: META-INF/CERT.RSA
D/JARVerifierV3( 281): verify: assets/new_asset
D/JARVerifierV3( 281): ensureCertificates: assets/new_asset
D/JARVerifierV3( 281): verify: META-INF/BERT.DSA
D/JARVerifierV3( 281): verify: META-INF/BERT.SF
D/JARVerifierV3( 281): verify: /data/data/xper.jv/files/GoodApp_added_modified_manifest_ds.apk verified
...
Back to square one again
4.0 PackageParser collectCertificates Again
This is why the PackageParser
collectCertificates
method is doing this
...
Enumeration<JarEntry> entries = jarFile.entries();
final Manifest manifest = jarFile.getManifest();
while (entries.hasMoreElements()) {
final JarEntry je = entries.nextElement();
if (je.isDirectory()) continue;
final String name = je.getName();
if (name.startsWith("META-INF/"))
continue;
if (ANDROID_MANIFEST_FILENAME.equals(name)) {
final Attributes attributes = manifest.getAttributes(name);
pkg.manifestDigest = ManifestDigest.fromAttributes(attributes);
}
final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);
if (DEBUG_JAR) {
Slog.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName()
+ ": certs=" + certs + " ("
+ (certs != null ? certs.length : 0) + ")");
}
if (localCerts == null) {
Slog.e(TAG, "Package " + pkg.packageName
+ " has no certificates at entry "
+ je.getName() + "; ignoring!");
jarFile.close();
mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
return false;
} else if (certs == null) {
certs = localCerts;
} else {
// Ensure all certificates match.
for (int i=0; i<certs.length; i++) {
boolean found = false;
for (int j=0; j<localCerts.length; j++) {
if (certs[i] != null &&
certs[i].equals(localCerts[j])) {
found = true;
break;
}
}
if (!found || certs.length != localCerts.length) {
Slog.e(TAG, "Package " + pkg.packageName
+ " has mismatched certificates at entry "
+ je.getName() + "; ignoring!");
jarFile.close();
mParseError = PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
return false;
}
}
}
}
...
not only checking for the existence of certificates associated with each file
...
if (localCerts == null) {
Slog.e(TAG, "Package " + pkg.packageName
+ " has no certificates at entry "
+ je.getName() + "; ignoring!");
jarFile.close();
mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
return false;
}
...
but also checking, albeit in a somewhat convoluted manner, that all the files have been signed with the same set of certificates .
...
// Ensure all certificates match.
for (int i=0; i<certs.length; i++) {
boolean found = false;
for (int j=0; j<localCerts.length; j++) {
if (certs[i] != null &&
certs[i].equals(localCerts[j])) {
found = true;
break;
}
}
if (!found || certs.length != localCerts.length) {
Slog.e(TAG, "Package " + pkg.packageName
+ " has mismatched certificates at entry "
+ je.getName() + "; ignoring!");
jarFile.close();
mParseError = PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
return false;
}
}
...
This is what verification of a signed JAR amounts to.
Establishing in the most complicated way possible whether all of the files present in the JAR have been signed with the ‘same’set of certificates.
Note that this does not address the question of whether files have been removed.
Note also that this relies on establishing the ‘equality’ of certficates which is an ‘interesting’ problem in its own right.
Copyright (c) 2013 By Simon Lewis. All Rights Reserved.
Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.
Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.