From 4063ef39a44070db9369d8cec1df9a95acb43eb7 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 26 Oct 2018 10:54:12 -0700 Subject: [PATCH] Update release config --- .gitignore | 1 + Dockerfile | 2 +- build.gradle | 115 +++++++------- buildSrc/build.gradle | 9 ++ .../org/signal/signing/ApkSignerUtil.java | 141 ++++++++++++++++++ pkcs11.config | 5 + 6 files changed, 210 insertions(+), 63 deletions(-) create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/java/org/signal/signing/ApkSignerUtil.java create mode 100644 pkcs11.config diff --git a/.gitignore b/.gitignore index 6d35cbb04..b420a5688 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ test/androidTestEspresso/res/values/arrays.xml obj/ jni/libspeex/.deps/ *.sh +pkcs11.password diff --git a/Dockerfile b/Dockerfile index 1a8ef7d13..3074dcc96 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN dpkg --add-architecture i386 && \ apt-get update -y && \ apt-get install -y software-properties-common && \ apt-get update -y && \ - apt-get install -y libc6:i386=2.26-0ubuntu2.1 libncurses5:i386=6.0+20160625-1ubuntu1 libstdc++6:i386=7.2.0-8ubuntu3.2 lib32z1=1:1.2.11.dfsg-0ubuntu2 wget openjdk-8-jdk=8u171-b11-0ubuntu0.17.10.1 git unzip && \ + apt-get install -y libc6:i386=2.26-0ubuntu2.1 libncurses5:i386=6.0+20160625-1ubuntu1 libstdc++6:i386=7.2.0-8ubuntu3.2 lib32z1=1:1.2.11.dfsg-0ubuntu2 wget openjdk-8-jdk=8u171-b11-0ubuntu0.17.10.1 git unzip opensc pcscd && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ apt-get clean diff --git a/build.gradle b/build.gradle index a32567b94..5bc4dab33 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import org.signal.signing.ApkSignerUtil + import java.security.MessageDigest buildscript { @@ -321,10 +323,6 @@ android { exclude 'META-INF/proguard/androidx-annotations.pro' } - signingConfigs { - release - } - buildTypes { debug { minifyEnabled true @@ -354,7 +352,6 @@ android { release { minifyEnabled true proguardFiles = buildTypes.debug.proguardFiles - signingConfig signingConfigs.release } } @@ -406,47 +403,66 @@ android { } } -task assembleWebsiteDescriptor << { - android.applicationVariants.all { variant -> - if (variant.name.equals("websiteDebug") || - variant.name.equals("websiteRelease")) - { - File file = new File(variant.outputs[0].outputFile.path) - - if (file.exists()) { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - file.eachByte 4096, {bytes, size -> - md.update(bytes, 0, size); - } - - String digest = md.digest().collect {String.format "%02x", it}.join(); - String url = variant.productFlavors.get(0).ext.websiteUpdateUrl - String apkName = variant.outputs[0].outputFile.name - - String descriptor = "{" + - "\"versionCode\" : $project.android.defaultConfig.versionCode," + - "\"versionName\" : \"$project.android.defaultConfig.versionName\"," + - "\"sha256sum\" : \"$digest\"," + - "\"url\" : \"$url/$apkName\"" + - "}" - - File descriptorFile = new File(variant.outputs[0].outputFile.parent, apkName.replace(".apk", ".json")) - - descriptorFile.write(descriptor) - } +def assembleWebsiteDescriptor = { variant, file -> + if (file.exists()) { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + file.eachByte 4096, {bytes, size -> + md.update(bytes, 0, size); } + + String digest = md.digest().collect {String.format "%02x", it}.join(); + String url = variant.productFlavors.get(0).ext.websiteUpdateUrl + String apkName = file.getName() + + String descriptor = "{" + + "\"versionCode\" : $project.android.defaultConfig.versionCode," + + "\"versionName\" : \"$project.android.defaultConfig.versionName\"," + + "\"sha256sum\" : \"$digest\"," + + "\"url\" : \"$url/$apkName\"" + + "}" + + File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json")) + + descriptorFile.write(descriptor) } } +def signProductionRelease = { variant -> + String apkName = variant.outputs[0].outputFile.name + File inputFile = new File(variant.outputs[0].outputFile.path); + File outputFile = new File(variant.outputs[0].outputFile.parent, apkName.replace("-unsigned", "")); + + new ApkSignerUtil("sun.security.pkcs11.SunPKCS11", + "pkcs11.config", + "PKCS11", + "file:pkcs11.password").calculateSignature(inputFile.getAbsolutePath(), + outputFile.getAbsolutePath()); + + inputFile.delete(); + return outputFile +} + +task signProductionPlayRelease << { + signProductionRelease(android.applicationVariants.find({ it.name.equals("playRelease") })) +} + +task signProductionWebsiteRelease << { + def variant = android.applicationVariants.find({ it.name.equals("websiteRelease") }) + File signedRelease = signProductionRelease(variant) + assembleWebsiteDescriptor(variant, signedRelease); +} + tasks.whenTaskAdded { task -> if (task.name.equals("lint")) { task.enabled = false } - if (task.name.equals("assembleWebsiteDebug") || - task.name.equals("assembleWebsiteRelease")) - { - task.finalizedBy assembleWebsiteDescriptor + if (task.name.equals("assemblePlayRelease")) { + task.finalizedBy signProductionPlayRelease + } + + if (task.name.equals("assembleWebsiteRelease")) { + task.finalizedBy signProductionWebsiteRelease } } @@ -462,28 +478,3 @@ def getLastCommitTimestamp() { } } -def Properties props = new Properties() -def propFile = new File('signing.properties') - -if (propFile.canRead()){ - props.load(new FileInputStream(propFile)) - - if (props !=null && - props.containsKey('STORE_FILE') && - props.containsKey('STORE_PASSWORD') && - props.containsKey('KEY_ALIAS') && - props.containsKey('KEY_PASSWORD')) - { - android.signingConfigs.release.storeFile = file(props['STORE_FILE']) - android.signingConfigs.release.storePassword = props['STORE_PASSWORD'] - android.signingConfigs.release.keyAlias = props['KEY_ALIAS'] - android.signingConfigs.release.keyPassword = props['KEY_PASSWORD'] - } else { - println 'signing.properties found but some entries are missing' - android.buildTypes.release.signingConfig = null - } -}else { - println 'signing.properties not found' - android.buildTypes.release.signingConfig = null -} - diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..713270474 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,9 @@ +apply plugin: 'java-gradle-plugin' + +repositories { + mavenCentral() +} + +dependencies { + compile group: 'com.android.tools.build', name: 'apksig', version: '2.3.0' +} diff --git a/buildSrc/src/main/java/org/signal/signing/ApkSignerUtil.java b/buildSrc/src/main/java/org/signal/signing/ApkSignerUtil.java new file mode 100644 index 000000000..144d55e17 --- /dev/null +++ b/buildSrc/src/main/java/org/signal/signing/ApkSignerUtil.java @@ -0,0 +1,141 @@ +package org.signal.signing; + +import com.android.apksig.ApkSigner; +import com.android.apksig.apk.ApkFormatException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.Security; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + +public class ApkSignerUtil { + + private final String providerClass; + + private final String providerArgument; + + private final String keyStoreType; + + private final String keyStorePassword; + + + public ApkSignerUtil(String providerClass, String providerArgument, String keyStoreType, String keyStorePassword) { + this.providerClass = providerClass; + this.providerArgument = providerArgument; + this.keyStoreType = keyStoreType; + this.keyStorePassword = keyStorePassword; + } + + public void calculateSignature(String inputApkFile, String outputApkFile) + throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, ApkFormatException, InvalidKeyException, SignatureException + { + System.out.println("Running calculateSignature()..."); + + if (providerClass != null) { + installProvider(providerClass, providerArgument); + } + + ApkSigner apkSigner = new ApkSigner.Builder(Collections.singletonList(loadKeyStore(keyStoreType, keyStorePassword))) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setInputApk(new File(inputApkFile)) + .setOutputApk(new File(outputApkFile)) + .setOtherSignersSignaturesPreserved(false) + .build(); + + apkSigner.sign(); + } + + private void installProvider(String providerName, String providerArgument) { + try { + Class providerClass = Class.forName(providerName); + + if (!Provider.class.isAssignableFrom(providerClass)) { + throw new IllegalArgumentException("JCA Provider class " + providerClass + " not subclass of " + Provider.class.getName()); + } + + Provider provider; + + if (providerArgument != null) { + provider = (Provider) providerClass.getConstructor(String.class).newInstance(providerArgument); + } else { + provider = (Provider) providerClass.getConstructor().newInstance(); + } + + Security.addProvider(provider); + } catch (ClassNotFoundException | InstantiationException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + throw new IllegalArgumentException(e); + } + } + + private ApkSigner.SignerConfig loadKeyStore(String keyStoreType, String keyStorePassword) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException { + KeyStore keyStoreEntity = KeyStore.getInstance(keyStoreType == null ? KeyStore.getDefaultType() : keyStoreType); + char[] password = getPassword(keyStorePassword); + keyStoreEntity.load(null, password); + + Enumeration aliases = keyStoreEntity.aliases(); + String keyAlias = null; + + while (aliases != null && aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (keyStoreEntity.isKeyEntry(alias)) { + keyAlias = alias; + break; + } + } + + if (keyAlias == null) { + throw new IllegalArgumentException("Keystore has no key entries!"); + } + + PrivateKey privateKey = (PrivateKey) keyStoreEntity.getKey(keyAlias, password); + Certificate[] certificates = keyStoreEntity.getCertificateChain(keyAlias); + + if (certificates == null || certificates.length == 0) { + throw new IllegalArgumentException("Unable to load certificates!"); + } + + List results = new LinkedList<>(); + + for (Certificate certificate : certificates) { + results.add((X509Certificate)certificate); + } + + + return new ApkSigner.SignerConfig.Builder("Signal Signer", privateKey, results).build(); + } + + private char[] getPassword(String encoded) throws IOException { + if (encoded.startsWith("file:")) { + String name = encoded.substring("file:".length()); + BufferedReader reader = new BufferedReader(new FileReader(new File(name))); + String password = reader.readLine(); + + if (password.length() == 0) { + throw new IOException("Failed to read password from file: " + name); + } + + return password.toCharArray(); + } else { + return encoded.toCharArray(); + } + } + +} diff --git a/pkcs11.config b/pkcs11.config new file mode 100644 index 000000000..3ea6941f8 --- /dev/null +++ b/pkcs11.config @@ -0,0 +1,5 @@ +name = OpenSC-PKCS11 +description = SunPKCS11 via OpenSC +library = /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so +slotListIndex = 0 +