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

This commit is contained in:
Doug Zongker 2013-01-03 14:50:36 -08:00 committed by Gerrit Code Review
commit f09b7a0eaa

View file

@ -35,6 +35,7 @@ import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.encoders.Base64;
import java.io.BufferedReader;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
@ -96,7 +97,7 @@ class SignApk {
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
private static X509Certificate readPublicKey(File file)
throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
FileInputStream input = new FileInputStream(file);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
@ -133,7 +134,7 @@ class SignApk {
* @param keyFile The file containing the private key
*/
private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
throws GeneralSecurityException {
throws GeneralSecurityException {
EncryptedPrivateKeyInfo epkInfo;
try {
epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
@ -160,7 +161,7 @@ class SignApk {
/** Read a PKCS 8 format private key. */
private static PrivateKey readPrivateKey(File file)
throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
DataInputStream input = new DataInputStream(new FileInputStream(file));
try {
byte[] bytes = new byte[(int) file.length()];
@ -183,7 +184,7 @@ class SignApk {
/** Add the SHA1 of every file to the manifest, creating it if necessary. */
private static Manifest addDigestsToManifest(JarFile jar)
throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
@ -301,8 +302,8 @@ class SignApk {
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
@ -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'. */
private static void writeSignatureBlock(
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
@ -395,23 +371,170 @@ class SignApk {
dos.writeObject(asn1.readObject());
}
private static void signWholeOutputFile(byte[] zipData,
OutputStream outputStream,
X509Certificate publicKey,
PrivateKey privateKey)
throws IOException,
CertificateEncodingException,
OperatorCreationException,
CMSException {
// 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");
/**
* Copy all the files in a manifest from input to output. We set
* the modification times in the output to a fixed time, so as to
* 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,
CertificateEncodingException,
OperatorCreationException,
CMSException {
SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
}
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();
@ -423,8 +546,20 @@ class SignApk {
temp.write(message);
temp.write(0);
writeSignatureBlock(new CMSByteArraySlice(zipData, 0, zipData.length-2),
publicKey, privateKey, temp);
cmsOut.writeSignatureBlock(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;
if (total_size > 0xffff) {
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 >> 8) & 0xff);
temp.writeTo(outputStream);
}
/**
* Copy all the files in a manifest from input to output. We set
* the modification times in the output to a fixed time, so as to
* 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;
private static void signFile(Manifest manifest, JarFile inputJar,
X509Certificate[] publicKey, PrivateKey[] privateKey,
JarOutputStream outputJar)
throws Exception {
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
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);
JarEntry je;
InputStream data = in.getInputStream(inEntry);
while ((num = data.read(buffer)) > 0) {
out.write(buffer, 0, num);
}
out.flush();
// Everything else
copyFiles(manifest, inputJar, outputJar, timestamp);
// MANIFEST.MF
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];
JarFile inputJar = null;
JarOutputStream outputJar = null;
FileOutputStream outputFile = null;
try {
@ -555,76 +693,26 @@ class SignApk {
}
inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
OutputStream outputStream = null;
outputFile = new FileOutputStream(outputFilename);
if (signWholeFile) {
outputStream = new ByteArrayOutputStream();
SignApk.signWholeFile(inputJar, firstPublicKeyFile,
publicKey[0], privateKey[0], outputFile);
} else {
outputStream = outputFile = new FileOutputStream(outputFilename);
}
outputJar = new JarOutputStream(outputStream);
JarOutputStream outputJar = new JarOutputStream(outputFile);
// For signing .apks, use the maximum compression to make
// them as small as possible (since they live forever on
// the system partition). For OTA packages, use the
// default compression level, which is much much faster
// and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested).
if (!signWholeFile) {
// For signing .apks, use the maximum compression to make
// them as small as possible (since they live forever on
// the system partition). For OTA packages, use the
// default compression level, which is much much faster
// and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested).
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);
}
outputJar.close();
outputJar = null;
outputStream.flush();
if (signWholeFile) {
outputFile = new FileOutputStream(outputFilename);
signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),
outputFile, publicKey[0], privateKey[0]);
signFile(addDigestsToManifest(inputJar), inputJar,
publicKey, privateKey, outputJar);
outputJar.close();
}
} catch (Exception e) {
e.printStackTrace();