Just An Application

August 10, 2014

And Another One: Part Five — All Done By Sleight Of Hand

There is in fact only one small sleight of hand involved in associating an arbitrary certificate with an Android application.

All the hard work is being done, or not being done as the case may be, by the android.content.pm.PackageParser method collectCertificates and associated Android system code.

File

    $(ANDROID_SRC)/frameworks/base/core/java/android/content/pm/PackageParser.java

Source

    ...
    
    public boolean collectCertificates(Package pkg, int flags) {
        pkg.mSignatures = null;
    
        WeakReference<byte[]> readBufferRef;
        byte[] readBuffer = null;
        synchronized (mSync) {
            readBufferRef = mReadBuffer;
            if (readBufferRef != null) {
                mReadBuffer = null;
                readBuffer = readBufferRef.get();
            }
            if (readBuffer == null) {
                readBuffer = new byte[8192];
                readBufferRef = new WeakReference(readBuffer);
            }
        }
            
        try {
            JarFile jarFile = new JarFile(mArchiveSourcePath);
            
            Certificate[] certs = null;
            
            if ((flags&PARSE_IS_SYSTEM) != 0) {
                // If this package comes from the system image, then we
                // can trust it...  we'll just use the AndroidManifest.xml
                // to retrieve its signatures, not validating all of the
                // files.
                JarEntry jarEntry = jarFile.getJarEntry(ANDROID_MANIFEST_FILENAME);
                certs = loadCertificates(jarFile, jarEntry, readBuffer);
                if (certs == null) {
                    Slog.e(TAG, "Package " + pkg.packageName
                            + " has no certificates at entry "
                            + jarEntry.getName() + "; ignoring!");
                    jarFile.close();
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
                    return false;
                }
                if (DEBUG_JAR) {
                    Slog.i(TAG, "File " + mArchiveSourcePath + ": entry=" + jarEntry
                            + " certs=" + (certs != null ? certs.length : 0));
                    if (certs != null) {
                        final int N = certs.length;
                        for (int i=0; i<N; i++) {
                            Slog.i(TAG, "  Public key: "
                                    + certs[i].getPublicKey().getEncoded()
                                    + " " + certs[i].getPublicKey());
                        }
                    }
                }
            } else {
                Enumeration<JarEntry> entries = jarFile.entries();
                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)) {
                        pkg.manifestDigest =
                            ManifestDigest.fromInputStream(jarFile.getInputStream(je));
                    }
                
                    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;
                            }
                        }
                    }
                }
            }
            jarFile.close();
                    
            synchronized (mSync) {
                mReadBuffer = readBufferRef;
            }
                    
            if (certs != null && certs.length > 0) {
                final int N = certs.length;
                pkg.mSignatures = new Signature[certs.length];
                for (int i=0; i<N; i++) {
                    pkg.mSignatures[i] = new Signature(
                            certs[i].getEncoded());
                }
            } else {
                Slog.e(TAG, "Package " + pkg.packageName
                        + " has no certificates; ignoring!");
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
                return false;
            }
        } catch (CertificateEncodingException e) {
            Slog.w(TAG, "Exception reading " + mArchiveSourcePath, e);
            mParseError = PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
            return false;
        } catch (IOException e) {
            Slog.w(TAG, "Exception reading " + mArchiveSourcePath, e);
            mParseError = PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
            return false;
        } catch (RuntimeException e) {
            Slog.w(TAG, "Exception reading " + mArchiveSourcePath, e);
            mParseError = PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
            return false;
        }
                    
        return true;
    }
                    
    ...

JarFile Construction

If we ignore the interesting read buffer memory optimization then the method starts by creating a JarFile instance.

            JarFile jarFile = new JarFile(mArchiveSourcePath);

It uses the single argument JarFile constructor

    public JarFile(File file) throws IOException

which simply calls the two argument constructor

    public JarFile(File file, boolean verify) throws IOException

with a second argument of true thereby ensuring the contents of the JAR will potentially be verified.

Certificate Collection

If the Android package is not a system package, which is the case we are interested in, the method iterates over all the members of the JAR containing the package.


        } else {
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                final JarEntry je = entries.nextElement();

    

ignoring any member which is a directory


                if (je.isDirectory()) continue;
                
    

or in the META-INF directory


                final String name = je.getName();
    
                if (name.startsWith("META-INF/"))
                    continue;
                

It calls the method loadCertificates assigning the return value to the local variable localCerts


                final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);
                

The method loadCertificates returns an array of java.security.cert.Certificates or null.

If the call to loadCertificates returns null then the method returns immediately and installation of the Android package fails.


                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;
                    

Otherwise if it is the first iteration the array of Certificates are cached in the local variable certs.


                } else if (certs == null) {
                    certs = localCerts;

On subsequent iterations the array of Certificates returned by the current call to loadCertificates are compared with those cached in the local variable certs.


                } 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;
                        }
                    }
                }

If any of the Certificates in certs are not found in localCerts or certs and localCerts are not of equal lengths the method returns immediately and installation of the Android package fails.

If the iteration over the members of the JAR completes successfully then the loadCertificates method has returned the same set of Certificates for each member of the JAR on which it was called.

Signature Construction

The signatures of the Android package are constructed from the array of Certificates cached in the local variable certs.


    
        if (certs != null && certs.length > 0) {
            final int N = certs.length;
            pkg.mSignatures = new Signature[certs.length];
            for (int i=0; i<N; i++) {
                pkg.mSignatures[i] = new Signature(
                        certs[i].getEncoded());
            }
        } ...

Each instance of android.content.pm.Signature is constructed with the encoded version of a Certificate.


Copyright (c) 2014 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.

Advertisements

1 Comment »

  1. […] know that verifier is not null since the JarFile instance was created by the PackageParser method collectCertificates using the single-argument JarFile constructor which will result in verifier being initialized with […]

    Pingback by And Another One: Part Seven — JarFile.getInputStream | Just An Application — August 11, 2014 @ 8:24 am


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: