diff --git a/.gitignore b/.gitignore
index 6d35cbb04700cc3d03bac3070f7a280a10fdd513..b420a56885898d49cfd44e66b6eb755e1662368f 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 1a8ef7d1376f5f0afd5c6707ced2cfb5b9eec189..3074dcc9671bbca791acf4f04b68aff6938f421e 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 a32567b941812866d16176c132f9d6b29fb3e1ec..5bc4dab33b697d350bc1255b6ddfd8ba870952ea 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 0000000000000000000000000000000000000000..7132704747305fcb6ea49e548ef1f19fb6296746
--- /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 0000000000000000000000000000000000000000..144d55e17b2fa72a089690bdb0615f27fa7e5649
--- /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<String> 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<X509Certificate> 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 0000000000000000000000000000000000000000..3ea6941f8295c23b4548f29e962177abba68742e
--- /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
+