Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f09b7a0e authored by Doug Zongker's avatar Doug Zongker Committed by Gerrit Code Review
Browse files

Merge "SignApk: perform the whole file signature in a single streaming pass."

parents 57a5e52d 29706d15
Loading
Loading
Loading
Loading
+235 −147
Original line number Original line Diff line number Diff line
@@ -35,6 +35,7 @@ import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Base64;


import java.io.BufferedReader;
import java.io.BufferedReader;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.File;
@@ -339,31 +340,6 @@ class SignApk {
        }
        }
    }
    }


    private static class CMSByteArraySlice implements CMSTypedData {
        private final ASN1ObjectIdentifier type;
        private final byte[] data;
        private final int offset;
        private final int length;
        public CMSByteArraySlice(byte[] data, int offset, int length) {
            this.data = data;
            this.offset = offset;
            this.length = length;
            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
        }

        public Object getContent() {
            throw new UnsupportedOperationException();
        }

        public ASN1ObjectIdentifier getContentType() {
            return type;
        }

        public void write(OutputStream out) throws IOException {
            out.write(data, offset, length);
        }
    }

    /** Sign data and write the digital signature to 'out'. */
    /** Sign data and write the digital signature to 'out'. */
    private static void writeSignatureBlock(
    private static void writeSignatureBlock(
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
@@ -395,24 +371,171 @@ class SignApk {
        dos.writeObject(asn1.readObject());
        dos.writeObject(asn1.readObject());
    }
    }


    private static void signWholeOutputFile(byte[] zipData,
    /**
                                            OutputStream outputStream,
     * Copy all the files in a manifest from input to output.  We set
                                            X509Certificate publicKey,
     * the modification times in the output to a fixed time, so as to
                                            PrivateKey privateKey)
     * reduce variation in the output file and make incremental OTAs
     * more efficient.
     */
    private static void copyFiles(Manifest manifest,
                                  JarFile in, JarOutputStream out, long timestamp) throws IOException {
        byte[] buffer = new byte[4096];
        int num;

        Map<String, Attributes> entries = manifest.getEntries();
        ArrayList<String> names = new ArrayList<String>(entries.keySet());
        Collections.sort(names);
        for (String name : names) {
            JarEntry inEntry = in.getJarEntry(name);
            JarEntry outEntry = null;
            if (inEntry.getMethod() == JarEntry.STORED) {
                // Preserve the STORED method of the input entry.
                outEntry = new JarEntry(inEntry);
            } else {
                // Create a new entry so that the compressed len is recomputed.
                outEntry = new JarEntry(name);
            }
            outEntry.setTime(timestamp);
            out.putNextEntry(outEntry);

            InputStream data = in.getInputStream(inEntry);
            while ((num = data.read(buffer)) > 0) {
                out.write(buffer, 0, num);
            }
            out.flush();
        }
    }

    private static class WholeFileSignerOutputStream extends FilterOutputStream {
        private boolean closing = false;
        private ByteArrayOutputStream footer = new ByteArrayOutputStream();
        private OutputStream tee;

        public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
            super(out);
            this.tee = tee;
        }

        public void notifyClosing() {
            closing = true;
        }

        public void finish() throws IOException {
            closing = false;

            byte[] data = footer.toByteArray();
            if (data.length < 2)
                throw new IOException("Less than two bytes written to footer");
            write(data, 0, data.length - 2);
        }

        public byte[] getTail() {
            return footer.toByteArray();
        }

        @Override
        public void write(byte[] b) throws IOException {
            write(b, 0, b.length);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (closing) {
                // if the jar is about to close, save the footer that will be written
                footer.write(b, off, len);
            }
            else {
                // write to both output streams. out is the CMSTypedData signer and tee is the file.
                out.write(b, off, len);
                tee.write(b, off, len);
            }
        }

        @Override
        public void write(int b) throws IOException {
            if (closing) {
                // if the jar is about to close, save the footer that will be written
                footer.write(b);
            }
            else {
                // write to both output streams. out is the CMSTypedData signer and tee is the file.
                out.write(b);
                tee.write(b);
            }
        }
    }

    private static class CMSSigner implements CMSTypedData {
        private JarFile inputJar;
        private File publicKeyFile;
        private X509Certificate publicKey;
        private PrivateKey privateKey;
        private String outputFile;
        private OutputStream outputStream;
        private final ASN1ObjectIdentifier type;
        private WholeFileSignerOutputStream signer;

        public CMSSigner(JarFile inputJar, File publicKeyFile,
                         X509Certificate publicKey, PrivateKey privateKey,
                         OutputStream outputStream) {
            this.inputJar = inputJar;
            this.publicKeyFile = publicKeyFile;
            this.publicKey = publicKey;
            this.privateKey = privateKey;
            this.outputStream = outputStream;
            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
        }

        public Object getContent() {
            throw new UnsupportedOperationException();
        }

        public ASN1ObjectIdentifier getContentType() {
            return type;
        }

        public void write(OutputStream out) throws IOException {
            try {
                signer = new WholeFileSignerOutputStream(out, outputStream);
                JarOutputStream outputJar = new JarOutputStream(signer);

                Manifest manifest = addDigestsToManifest(inputJar);
                signFile(manifest, inputJar,
                         new X509Certificate[]{ publicKey },
                         new PrivateKey[]{ privateKey },
                         outputJar);
                // Assume the certificate is valid for at least an hour.
                long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
                addOtacert(outputJar, publicKeyFile, timestamp, manifest);

                signer.notifyClosing();
                outputJar.close();
                signer.finish();
            }
            catch (Exception e) {
                throw new IOException(e);
            }
        }

        public void writeSignatureBlock(ByteArrayOutputStream temp)
            throws IOException,
            throws IOException,
                   CertificateEncodingException,
                   CertificateEncodingException,
                   OperatorCreationException,
                   OperatorCreationException,
                   CMSException {
                   CMSException {
        // For a zip with no archive comment, the
            SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
        // end-of-central-directory record will be 22 bytes long, so
        // we expect to find the EOCD marker 22 bytes from the end.
        if (zipData[zipData.length-22] != 0x50 ||
            zipData[zipData.length-21] != 0x4b ||
            zipData[zipData.length-20] != 0x05 ||
            zipData[zipData.length-19] != 0x06) {
            throw new IllegalArgumentException("zip data already has an archive comment");
        }
        }


        public WholeFileSignerOutputStream getSigner() {
            return signer;
        }
    }

    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
                                      X509Certificate publicKey, PrivateKey privateKey,
                                      OutputStream outputStream) throws Exception {
        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
                                         publicKey, privateKey, outputStream);

        ByteArrayOutputStream temp = new ByteArrayOutputStream();
        ByteArrayOutputStream temp = new ByteArrayOutputStream();


        // put a readable message and a null char at the start of the
        // put a readable message and a null char at the start of the
@@ -423,8 +546,20 @@ class SignApk {
        temp.write(message);
        temp.write(message);
        temp.write(0);
        temp.write(0);


        writeSignatureBlock(new CMSByteArraySlice(zipData, 0, zipData.length-2),
        cmsOut.writeSignatureBlock(temp);
                            publicKey, privateKey, temp);

        byte[] zipData = cmsOut.getSigner().getTail();

        // For a zip with no archive comment, the
        // end-of-central-directory record will be 22 bytes long, so
        // we expect to find the EOCD marker 22 bytes from the end.
        if (zipData[zipData.length-22] != 0x50 ||
            zipData[zipData.length-21] != 0x4b ||
            zipData[zipData.length-20] != 0x05 ||
            zipData[zipData.length-19] != 0x06) {
            throw new IllegalArgumentException("zip data already has an archive comment");
        }

        int total_size = temp.size() + 6;
        int total_size = temp.size() + 6;
        if (total_size > 0xffff) {
        if (total_size > 0xffff) {
            throw new IllegalArgumentException("signature is too big for ZIP file comment");
            throw new IllegalArgumentException("signature is too big for ZIP file comment");
@@ -458,44 +593,48 @@ class SignApk {
            }
            }
        }
        }


        outputStream.write(zipData, 0, zipData.length-2);
        outputStream.write(total_size & 0xff);
        outputStream.write(total_size & 0xff);
        outputStream.write((total_size >> 8) & 0xff);
        outputStream.write((total_size >> 8) & 0xff);
        temp.writeTo(outputStream);
        temp.writeTo(outputStream);
    }
    }


    /**
    private static void signFile(Manifest manifest, JarFile inputJar,
     * Copy all the files in a manifest from input to output.  We set
                                 X509Certificate[] publicKey, PrivateKey[] privateKey,
     * the modification times in the output to a fixed time, so as to
                                 JarOutputStream outputJar)
     * reduce variation in the output file and make incremental OTAs
        throws Exception {
     * more efficient.
        // Assume the certificate is valid for at least an hour.
     */
        long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
    private static void copyFiles(Manifest manifest,
        JarFile in, JarOutputStream out, long timestamp) throws IOException {
        byte[] buffer = new byte[4096];
        int num;


        Map<String, Attributes> entries = manifest.getEntries();
        JarEntry je;
        ArrayList<String> names = new ArrayList<String>(entries.keySet());
        Collections.sort(names);
        for (String name : names) {
            JarEntry inEntry = in.getJarEntry(name);
            JarEntry outEntry = null;
            if (inEntry.getMethod() == JarEntry.STORED) {
                // Preserve the STORED method of the input entry.
                outEntry = new JarEntry(inEntry);
            } else {
                // Create a new entry so that the compressed len is recomputed.
                outEntry = new JarEntry(name);
            }
            outEntry.setTime(timestamp);
            out.putNextEntry(outEntry);


            InputStream data = in.getInputStream(inEntry);
        // Everything else
            while ((num = data.read(buffer)) > 0) {
        copyFiles(manifest, inputJar, outputJar, timestamp);
                out.write(buffer, 0, num);

            }
        // MANIFEST.MF
            out.flush();
        je = new JarEntry(JarFile.MANIFEST_NAME);
        je.setTime(timestamp);
        outputJar.putNextEntry(je);
        manifest.write(outputJar);

        int numKeys = publicKey.length;
        for (int k = 0; k < numKeys; ++k) {
            // CERT.SF / CERT#.SF
            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
                              (String.format(CERT_SF_MULTI_NAME, k)));
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeSignatureFile(manifest, baos);
            byte[] signedData = baos.toByteArray();
            outputJar.write(signedData);

            // CERT.RSA / CERT#.RSA
            je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
                              (String.format(CERT_RSA_MULTI_NAME, k)));
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            writeSignatureBlock(new CMSProcessableByteArray(signedData),
                                publicKey[k], privateKey[k], outputJar);
        }
        }
    }
    }


@@ -531,7 +670,6 @@ class SignApk {
        String outputFilename = args[args.length-1];
        String outputFilename = args[args.length-1];


        JarFile inputJar = null;
        JarFile inputJar = null;
        JarOutputStream outputJar = null;
        FileOutputStream outputFile = null;
        FileOutputStream outputFile = null;


        try {
        try {
@@ -555,13 +693,14 @@ class SignApk {
            }
            }
            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.


            OutputStream outputStream = null;
            outputFile = new FileOutputStream(outputFilename);


            if (signWholeFile) {
            if (signWholeFile) {
                outputStream = new ByteArrayOutputStream();
                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
                                      publicKey[0], privateKey[0], outputFile);
            } else {
            } else {
                outputStream = outputFile = new FileOutputStream(outputFilename);
                JarOutputStream outputJar = new JarOutputStream(outputFile);
            }
            outputJar = new JarOutputStream(outputStream);


                // For signing .apks, use the maximum compression to make
                // For signing .apks, use the maximum compression to make
                // them as small as possible (since they live forever on
                // them as small as possible (since they live forever on
@@ -569,62 +708,11 @@ class SignApk {
                // default compression level, which is much much faster
                // default compression level, which is much much faster
                // and produces output that is only a tiny bit larger
                // and produces output that is only a tiny bit larger
                // (~0.1% on full OTA packages I tested).
                // (~0.1% on full OTA packages I tested).
            if (!signWholeFile) {
                outputJar.setLevel(9);
                outputJar.setLevel(9);
            }

            JarEntry je;

            Manifest manifest = addDigestsToManifest(inputJar);

            // Everything else
            copyFiles(manifest, inputJar, outputJar, timestamp);

            // otacert
            if (signWholeFile) {
                addOtacert(outputJar, firstPublicKeyFile, timestamp, manifest);
            }

            // MANIFEST.MF
            je = new JarEntry(JarFile.MANIFEST_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            manifest.write(outputJar);

            // In the case of multiple keys, all the .SF files will be
            // identical, but as far as I can tell the jarsigner docs
            // don't allow there to be just one copy in the zipfile;
            // there hase to be one per .RSA file.

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeSignatureFile(manifest, baos);
            byte[] signedData = baos.toByteArray();

            for (int k = 0; k < numKeys; ++k) {
                // CERT.SF / CERT#.SF
                je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
                                  (String.format(CERT_SF_MULTI_NAME, k)));
                je.setTime(timestamp);
                outputJar.putNextEntry(je);
                outputJar.write(signedData);

                // CERT.RSA / CERT#.RSA
                je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
                                  (String.format(CERT_RSA_MULTI_NAME, k)));
                je.setTime(timestamp);
                outputJar.putNextEntry(je);
                writeSignatureBlock(new CMSProcessableByteArray(signedData),
                                    publicKey[k], privateKey[k], outputJar);
            }


                signFile(addDigestsToManifest(inputJar), inputJar,
                         publicKey, privateKey, outputJar);
                outputJar.close();
                outputJar.close();
            outputJar = null;
            outputStream.flush();

            if (signWholeFile) {
                outputFile = new FileOutputStream(outputFilename);
                signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),
                                    outputFile, publicKey[0], privateKey[0]);
            }
            }
        } catch (Exception e) {
        } catch (Exception e) {
            e.printStackTrace();
            e.printStackTrace();