1087 lines
47 KiB
Java
1087 lines
47 KiB
Java
/*
|
|
* Copyright (C) 2018 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.apksig;
|
|
|
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
|
|
|
import com.android.apksig.apk.ApkFormatException;
|
|
import com.android.apksig.apk.ApkUtils;
|
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
|
import com.android.apksig.internal.apk.SignatureInfo;
|
|
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
|
|
import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
|
|
import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode;
|
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
|
import com.android.apksig.internal.util.Pair;
|
|
import com.android.apksig.internal.util.RandomAccessFileDataSink;
|
|
import com.android.apksig.util.DataSink;
|
|
import com.android.apksig.util.DataSource;
|
|
import com.android.apksig.util.DataSources;
|
|
import com.android.apksig.zip.ZipFormatException;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.RandomAccessFile;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.PrivateKey;
|
|
import java.security.PublicKey;
|
|
import java.security.SignatureException;
|
|
import java.security.cert.CertificateEncodingException;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* APK Signer Lineage.
|
|
*
|
|
* <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
|
|
* the validity of its descendant. Each additional descendant represents a new identity that can be
|
|
* used to sign an APK, and each generation has accompanying attributes which represent how the
|
|
* APK would like to view the older signing certificates, specifically how they should be trusted in
|
|
* certain situations.
|
|
*
|
|
* <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
|
|
* the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
|
|
* Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
|
|
* allow upgrades to the new certificate.
|
|
*
|
|
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
|
*/
|
|
public class SigningCertificateLineage {
|
|
|
|
public final static int MAGIC = 0x3eff39d1;
|
|
|
|
private final static int FIRST_VERSION = 1;
|
|
|
|
private static final int CURRENT_VERSION = FIRST_VERSION;
|
|
|
|
/** accept data from already installed pkg with this cert */
|
|
private static final int PAST_CERT_INSTALLED_DATA = 1;
|
|
|
|
/** accept sharedUserId with pkg with this cert */
|
|
private static final int PAST_CERT_SHARED_USER_ID = 2;
|
|
|
|
/** grant SIGNATURE permissions to pkgs with this cert */
|
|
private static final int PAST_CERT_PERMISSION = 4;
|
|
|
|
/**
|
|
* Enable updates back to this certificate. WARNING: this effectively removes any benefit of
|
|
* signing certificate changes, since a compromised key could retake control of an app even
|
|
* after change, and should only be used if there is a problem encountered when trying to ditch
|
|
* an older cert.
|
|
*/
|
|
private static final int PAST_CERT_ROLLBACK = 8;
|
|
|
|
/**
|
|
* Preserve authenticator module-based access in AccountManager gated by signing certificate.
|
|
*/
|
|
private static final int PAST_CERT_AUTH = 16;
|
|
|
|
private final int mMinSdkVersion;
|
|
|
|
/**
|
|
* The signing lineage is just a list of nodes, with the first being the original signing
|
|
* certificate and the most recent being the one with which the APK is to actually be signed.
|
|
*/
|
|
private final List<SigningCertificateNode> mSigningLineage;
|
|
|
|
private SigningCertificateLineage(int minSdkVersion, List<SigningCertificateNode> list) {
|
|
mMinSdkVersion = minSdkVersion;
|
|
mSigningLineage = list;
|
|
}
|
|
|
|
private static SigningCertificateLineage createSigningLineage(
|
|
int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities,
|
|
SignerConfig child, SignerCapabilities childCapabilities)
|
|
throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
|
|
SignatureException {
|
|
SigningCertificateLineage signingCertificateLineage =
|
|
new SigningCertificateLineage(minSdkVersion, new ArrayList<>());
|
|
signingCertificateLineage =
|
|
signingCertificateLineage.spawnFirstDescendant(parent, parentCapabilities);
|
|
return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities);
|
|
}
|
|
|
|
public static SigningCertificateLineage readFromFile(File file)
|
|
throws IOException {
|
|
if (file == null) {
|
|
throw new NullPointerException("file == null");
|
|
}
|
|
RandomAccessFile inputFile = new RandomAccessFile(file, "r");
|
|
return readFromDataSource(DataSources.asDataSource(inputFile));
|
|
}
|
|
|
|
public static SigningCertificateLineage readFromDataSource(DataSource dataSource)
|
|
throws IOException {
|
|
if (dataSource == null) {
|
|
throw new NullPointerException("dataSource == null");
|
|
}
|
|
ByteBuffer inBuff = dataSource.getByteBuffer(0, (int) dataSource.size());
|
|
inBuff.order(ByteOrder.LITTLE_ENDIAN);
|
|
return read(inBuff);
|
|
}
|
|
|
|
/**
|
|
* Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute.
|
|
*
|
|
* <note>
|
|
* this may not give a complete representation of an APK's signing certificate history,
|
|
* since the APK may have multiple signers corresponding to different platform versions.
|
|
* Use <code> readFromApkFile</code> to handle this case.
|
|
* </note>
|
|
* @param attrValue
|
|
*/
|
|
public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue)
|
|
throws IOException {
|
|
List<SigningCertificateNode> parsedLineage =
|
|
V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap(
|
|
attrValue).order(ByteOrder.LITTLE_ENDIAN));
|
|
int minSdkVersion = calculateMinSdkVersion(parsedLineage);
|
|
return new SigningCertificateLineage(minSdkVersion, parsedLineage);
|
|
}
|
|
|
|
/**
|
|
* Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
|
|
* signature block of the provided APK File.
|
|
*
|
|
* @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
|
|
* or if the V3 signature block does not contain a valid lineage.
|
|
*/
|
|
public static SigningCertificateLineage readFromApkFile(File apkFile)
|
|
throws IOException, ApkFormatException {
|
|
try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) {
|
|
DataSource apk = DataSources.asDataSource(f, 0, f.length());
|
|
return readFromApkDataSource(apk);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
|
|
* signature block of the provided APK DataSource.
|
|
*
|
|
* @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
|
|
* or if the V3 signature block does not contain a valid lineage.
|
|
*/
|
|
public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
|
|
throws IOException, ApkFormatException {
|
|
SignatureInfo signatureInfo;
|
|
try {
|
|
ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
|
|
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
|
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
|
|
signatureInfo =
|
|
ApkSigningBlockUtils.findSignature(apk, zipSections,
|
|
V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException(e.getMessage());
|
|
} catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
|
|
throw new IllegalArgumentException(
|
|
"The provided APK does not contain a valid V3 signature block.");
|
|
}
|
|
|
|
// FORMAT:
|
|
// * length-prefixed sequence of length-prefixed signers:
|
|
// * length-prefixed signed data
|
|
// * minSDK
|
|
// * maxSDK
|
|
// * length-prefixed sequence of length-prefixed signatures
|
|
// * length-prefixed public key
|
|
ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
|
|
List<SigningCertificateLineage> lineages = new ArrayList<>(1);
|
|
while (signers.hasRemaining()) {
|
|
ByteBuffer signer = getLengthPrefixedSlice(signers);
|
|
ByteBuffer signedData = getLengthPrefixedSlice(signer);
|
|
try {
|
|
SigningCertificateLineage lineage = readFromSignedData(signedData);
|
|
lineages.add(lineage);
|
|
} catch (IllegalArgumentException ignored) {
|
|
// The current signer block does not contain a valid lineage, but it is possible
|
|
// another block will.
|
|
}
|
|
}
|
|
SigningCertificateLineage result;
|
|
if (lineages.isEmpty()) {
|
|
throw new IllegalArgumentException(
|
|
"The provided APK does not contain a valid lineage.");
|
|
} else if (lineages.size() > 1) {
|
|
result = consolidateLineages(lineages);
|
|
} else {
|
|
result = lineages.get(0);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the provided
|
|
* signed data portion of a signer in a V3 signature block.
|
|
*
|
|
* @throws IllegalArgumentException if the provided signed data does not contain a valid
|
|
* lineage.
|
|
*/
|
|
public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData)
|
|
throws IOException, ApkFormatException {
|
|
// FORMAT:
|
|
// * length-prefixed sequence of length-prefixed digests:
|
|
// * length-prefixed sequence of certificates:
|
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
|
// * uint-32: minSdkVersion
|
|
// * uint-32: maxSdkVersion
|
|
// * length-prefixed sequence of length-prefixed additional attributes:
|
|
// * uint32: ID
|
|
// * (length - 4) bytes: value
|
|
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
|
|
// * length-prefixed proof-of-rotation structure
|
|
// consume the digests through the maxSdkVersion to reach the lineage in the attributes
|
|
getLengthPrefixedSlice(signedData);
|
|
getLengthPrefixedSlice(signedData);
|
|
signedData.getInt();
|
|
signedData.getInt();
|
|
// iterate over the additional attributes adding any lineages to the List
|
|
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
|
|
List<SigningCertificateLineage> lineages = new ArrayList<>(1);
|
|
while (additionalAttributes.hasRemaining()) {
|
|
ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
|
|
int id = attribute.getInt();
|
|
if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
|
|
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
|
SigningCertificateLineage lineage = readFromV3AttributeValue(value);
|
|
lineages.add(lineage);
|
|
}
|
|
}
|
|
SigningCertificateLineage result;
|
|
// There should only be a single attribute with the lineage, but if there are multiple then
|
|
// attempt to consolidate the lineages.
|
|
if (lineages.isEmpty()) {
|
|
throw new IllegalArgumentException("The signed data does not contain a valid lineage.");
|
|
} else if (lineages.size() > 1) {
|
|
result = consolidateLineages(lineages);
|
|
} else {
|
|
result = lineages.get(0);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public void writeToFile(File file) throws IOException {
|
|
if (file == null) {
|
|
throw new NullPointerException("file == null");
|
|
}
|
|
RandomAccessFile outputFile = new RandomAccessFile(file, "rw");
|
|
writeToDataSink(new RandomAccessFileDataSink(outputFile));
|
|
}
|
|
|
|
public void writeToDataSink(DataSink dataSink) throws IOException {
|
|
if (dataSink == null) {
|
|
throw new NullPointerException("dataSink == null");
|
|
}
|
|
dataSink.consume(write());
|
|
}
|
|
|
|
/**
|
|
* Add a new signing certificate to the lineage. This effectively creates a signing certificate
|
|
* rotation event, forcing APKs which include this lineage to be signed by the new signer. The
|
|
* flags associated with the new signer are set to a default value.
|
|
*
|
|
* @param parent current signing certificate of the containing APK
|
|
* @param child new signing certificate which will sign the APK contents
|
|
*/
|
|
public SigningCertificateLineage spawnDescendant(SignerConfig parent, SignerConfig child)
|
|
throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
|
|
SignatureException {
|
|
if (parent == null || child == null) {
|
|
throw new NullPointerException("can't add new descendant to lineage with null inputs");
|
|
}
|
|
SignerCapabilities signerCapabilities = new SignerCapabilities.Builder().build();
|
|
return spawnDescendant(parent, child, signerCapabilities);
|
|
}
|
|
|
|
/**
|
|
* Add a new signing certificate to the lineage. This effectively creates a signing certificate
|
|
* rotation event, forcing APKs which include this lineage to be signed by the new signer.
|
|
*
|
|
* @param parent current signing certificate of the containing APK
|
|
* @param child new signing certificate which will sign the APK contents
|
|
* @param childCapabilities flags
|
|
*/
|
|
public SigningCertificateLineage spawnDescendant(
|
|
SignerConfig parent, SignerConfig child, SignerCapabilities childCapabilities)
|
|
throws CertificateEncodingException, InvalidKeyException,
|
|
NoSuchAlgorithmException, SignatureException {
|
|
if (parent == null) {
|
|
throw new NullPointerException("parent == null");
|
|
}
|
|
if (child == null) {
|
|
throw new NullPointerException("child == null");
|
|
}
|
|
if (childCapabilities == null) {
|
|
throw new NullPointerException("childCapabilities == null");
|
|
}
|
|
if (mSigningLineage.isEmpty()) {
|
|
throw new IllegalArgumentException("Cannot spawn descendant signing certificate on an"
|
|
+ " empty SigningCertificateLineage: no parent node");
|
|
}
|
|
|
|
// make sure that the parent matches our newest generation (leaf node/sink)
|
|
SigningCertificateNode currentGeneration = mSigningLineage.get(mSigningLineage.size() - 1);
|
|
if (!Arrays.equals(currentGeneration.signingCert.getEncoded(),
|
|
parent.getCertificate().getEncoded())) {
|
|
throw new IllegalArgumentException("SignerConfig Certificate containing private key"
|
|
+ " to sign the new SigningCertificateLineage record does not match the"
|
|
+ " existing most recent record");
|
|
}
|
|
|
|
// create data to be signed, including the algorithm we're going to use
|
|
SignatureAlgorithm signatureAlgorithm = getSignatureAlgorithm(parent);
|
|
ByteBuffer prefixedSignedData = ByteBuffer.wrap(
|
|
V3SigningCertificateLineage.encodeSignedData(
|
|
child.getCertificate(), signatureAlgorithm.getId()));
|
|
prefixedSignedData.position(4);
|
|
ByteBuffer signedDataBuffer = ByteBuffer.allocate(prefixedSignedData.remaining());
|
|
signedDataBuffer.put(prefixedSignedData);
|
|
byte[] signedData = signedDataBuffer.array();
|
|
|
|
// create SignerConfig to do the signing
|
|
List<X509Certificate> certificates = new ArrayList<>(1);
|
|
certificates.add(parent.getCertificate());
|
|
ApkSigningBlockUtils.SignerConfig newSignerConfig =
|
|
new ApkSigningBlockUtils.SignerConfig();
|
|
newSignerConfig.privateKey = parent.getPrivateKey();
|
|
newSignerConfig.certificates = certificates;
|
|
newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm);
|
|
|
|
// sign it
|
|
List<Pair<Integer, byte[]>> signatures =
|
|
ApkSigningBlockUtils.generateSignaturesOverData(newSignerConfig, signedData);
|
|
|
|
// finally, add it to our lineage
|
|
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(signatures.get(0).getFirst());
|
|
byte[] signature = signatures.get(0).getSecond();
|
|
currentGeneration.sigAlgorithm = sigAlgorithm;
|
|
SigningCertificateNode childNode =
|
|
new SigningCertificateNode(
|
|
child.getCertificate(), sigAlgorithm, null,
|
|
signature, childCapabilities.getFlags());
|
|
List<SigningCertificateNode> lineageCopy = new ArrayList<>(mSigningLineage);
|
|
lineageCopy.add(childNode);
|
|
return new SigningCertificateLineage(mMinSdkVersion, lineageCopy);
|
|
}
|
|
|
|
/**
|
|
* The number of signing certificates in the lineage, including the current signer, which means
|
|
* this value can also be used to V2determine the number of signing certificate rotations by
|
|
* subtracting 1.
|
|
*/
|
|
public int size() {
|
|
return mSigningLineage.size();
|
|
}
|
|
|
|
private SignatureAlgorithm getSignatureAlgorithm(SignerConfig parent)
|
|
throws InvalidKeyException {
|
|
PublicKey publicKey = parent.getCertificate().getPublicKey();
|
|
|
|
// TODO switch to one signature algorithm selection, or add support for multiple algorithms
|
|
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
|
|
publicKey, mMinSdkVersion, false /* padding support */);
|
|
return algorithms.get(0);
|
|
}
|
|
|
|
private SigningCertificateLineage spawnFirstDescendant(
|
|
SignerConfig parent, SignerCapabilities signerCapabilities) {
|
|
if (!mSigningLineage.isEmpty()) {
|
|
throw new IllegalStateException("SigningCertificateLineage already has its first node");
|
|
}
|
|
|
|
// check to make sure that the public key for the first node is acceptable for our minSdk
|
|
try {
|
|
getSignatureAlgorithm(parent);
|
|
} catch (InvalidKeyException e) {
|
|
throw new IllegalArgumentException("Algorithm associated with first signing certificate"
|
|
+ " invalid on desired platform versions", e);
|
|
}
|
|
|
|
// create "fake" signed data (there will be no signature over it, since there is no parent
|
|
SigningCertificateNode firstNode = new SigningCertificateNode(
|
|
parent.getCertificate(), null, null, new byte[0], signerCapabilities.getFlags());
|
|
return new SigningCertificateLineage(mMinSdkVersion, Collections.singletonList(firstNode));
|
|
}
|
|
|
|
private static SigningCertificateLineage read(ByteBuffer inputByteBuffer)
|
|
throws IOException {
|
|
ApkSigningBlockUtils.checkByteOrderLittleEndian(inputByteBuffer);
|
|
if (inputByteBuffer.remaining() < 8) {
|
|
throw new IllegalArgumentException(
|
|
"Improper SigningCertificateLineage format: insufficient data for header.");
|
|
}
|
|
|
|
if (inputByteBuffer.getInt() != MAGIC) {
|
|
throw new IllegalArgumentException(
|
|
"Improper SigningCertificateLineage format: MAGIC header mismatch.");
|
|
}
|
|
return read(inputByteBuffer, inputByteBuffer.getInt());
|
|
}
|
|
|
|
private static SigningCertificateLineage read(ByteBuffer inputByteBuffer, int version)
|
|
throws IOException {
|
|
switch (version) {
|
|
case FIRST_VERSION:
|
|
try {
|
|
List<SigningCertificateNode> nodes =
|
|
V3SigningCertificateLineage.readSigningCertificateLineage(
|
|
getLengthPrefixedSlice(inputByteBuffer));
|
|
int minSdkVersion = calculateMinSdkVersion(nodes);
|
|
return new SigningCertificateLineage(minSdkVersion, nodes);
|
|
} catch (ApkFormatException e) {
|
|
// unable to get a proper length-prefixed lineage slice
|
|
throw new IOException("Unable to read list of signing certificate nodes in "
|
|
+ "SigningCertificateLineage", e);
|
|
}
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Improper SigningCertificateLineage format: unrecognized version.");
|
|
}
|
|
}
|
|
|
|
private static int calculateMinSdkVersion(List<SigningCertificateNode> nodes) {
|
|
if (nodes == null) {
|
|
throw new IllegalArgumentException("Can't calculate minimum SDK version of null nodes");
|
|
}
|
|
int minSdkVersion = AndroidSdkVersion.P; // lineage introduced in P
|
|
for (SigningCertificateNode node : nodes) {
|
|
if (node.sigAlgorithm != null) {
|
|
int nodeMinSdkVersion = node.sigAlgorithm.getMinSdkVersion();
|
|
if (nodeMinSdkVersion > minSdkVersion) {
|
|
minSdkVersion = nodeMinSdkVersion;
|
|
}
|
|
}
|
|
}
|
|
return minSdkVersion;
|
|
}
|
|
|
|
private ByteBuffer write() {
|
|
byte[] encodedLineage =
|
|
V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
|
|
int payloadSize = 4 + 4 + 4 + encodedLineage.length;
|
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
result.putInt(MAGIC);
|
|
result.putInt(CURRENT_VERSION);
|
|
result.putInt(encodedLineage.length);
|
|
result.put(encodedLineage);
|
|
result.flip();
|
|
return result;
|
|
}
|
|
|
|
public byte[] generateV3SignerAttribute() {
|
|
// FORMAT (little endian):
|
|
// * length-prefixed bytes: attribute pair
|
|
// * uint32: ID
|
|
// * bytes: value - encoded V3 SigningCertificateLineage
|
|
byte[] encodedLineage =
|
|
V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
|
|
int payloadSize = 4 + 4 + encodedLineage.length;
|
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
result.putInt(4 + encodedLineage.length);
|
|
result.putInt(V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID);
|
|
result.put(encodedLineage);
|
|
return result.array();
|
|
}
|
|
|
|
public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs(
|
|
List<DefaultApkSignerEngine.SignerConfig> signerConfigs) {
|
|
if (signerConfigs == null) {
|
|
throw new NullPointerException("signerConfigs == null");
|
|
}
|
|
|
|
// not the most elegant sort, but we expect signerConfigs to be quite small (1 or 2 signers
|
|
// in most cases) and likely already sorted, so not worth the overhead of doing anything
|
|
// fancier
|
|
List<DefaultApkSignerEngine.SignerConfig> sortedSignerConfigs =
|
|
new ArrayList<>(signerConfigs.size());
|
|
for (int i = 0; i < mSigningLineage.size(); i++) {
|
|
for (int j = 0; j < signerConfigs.size(); j++) {
|
|
DefaultApkSignerEngine.SignerConfig config = signerConfigs.get(j);
|
|
if (mSigningLineage.get(i).signingCert.equals(config.getCertificates().get(0))) {
|
|
sortedSignerConfigs.add(config);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (sortedSignerConfigs.size() != signerConfigs.size()) {
|
|
throw new IllegalArgumentException("SignerConfigs supplied which are not present in the"
|
|
+ " SigningCertificateLineage");
|
|
}
|
|
return sortedSignerConfigs;
|
|
}
|
|
|
|
/**
|
|
* Returns the SignerCapabilities for the signer in the lineage that matches the provided
|
|
* config.
|
|
*/
|
|
public SignerCapabilities getSignerCapabilities(SignerConfig config) {
|
|
if (config == null) {
|
|
throw new NullPointerException("config == null");
|
|
}
|
|
|
|
X509Certificate cert = config.getCertificate();
|
|
return getSignerCapabilities(cert);
|
|
}
|
|
|
|
/**
|
|
* Returns the SignerCapabilities for the signer in the lineage that matches the provided
|
|
* certificate.
|
|
*/
|
|
public SignerCapabilities getSignerCapabilities(X509Certificate cert) {
|
|
if (cert == null) {
|
|
throw new NullPointerException("cert == null");
|
|
}
|
|
|
|
for (int i = 0; i < mSigningLineage.size(); i++) {
|
|
SigningCertificateNode lineageNode = mSigningLineage.get(i);
|
|
if (lineageNode.signingCert.equals(cert)) {
|
|
int flags = lineageNode.flags;
|
|
return new SignerCapabilities.Builder(flags).build();
|
|
}
|
|
}
|
|
|
|
// the provided signer certificate was not found in the lineage
|
|
throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
|
|
+ ") not found in the SigningCertificateLineage");
|
|
}
|
|
|
|
/**
|
|
* Updates the SignerCapabilities for the signer in the lineage that matches the provided
|
|
* config. Only those capabilities that have been modified through the setXX methods will be
|
|
* updated for the signer to prevent unset default values from being applied.
|
|
*/
|
|
public void updateSignerCapabilities(SignerConfig config, SignerCapabilities capabilities) {
|
|
if (config == null) {
|
|
throw new NullPointerException("config == null");
|
|
}
|
|
|
|
X509Certificate cert = config.getCertificate();
|
|
for (int i = 0; i < mSigningLineage.size(); i++) {
|
|
SigningCertificateNode lineageNode = mSigningLineage.get(i);
|
|
if (lineageNode.signingCert.equals(cert)) {
|
|
int flags = lineageNode.flags;
|
|
SignerCapabilities newCapabilities = new SignerCapabilities.Builder(
|
|
flags).setCallerConfiguredCapabilities(capabilities).build();
|
|
lineageNode.flags = newCapabilities.getFlags();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// the provided signer config was not found in the lineage
|
|
throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
|
|
+ ") not found in the SigningCertificateLineage");
|
|
}
|
|
|
|
/**
|
|
* Returns a list containing all of the certificates in the lineage.
|
|
*/
|
|
public List<X509Certificate> getCertificatesInLineage() {
|
|
List<X509Certificate> certs = new ArrayList<>();
|
|
for (int i = 0; i < mSigningLineage.size(); i++) {
|
|
X509Certificate cert = mSigningLineage.get(i).signingCert;
|
|
certs.add(cert);
|
|
}
|
|
return certs;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if the specified config is in the lineage.
|
|
*/
|
|
public boolean isSignerInLineage(SignerConfig config) {
|
|
if (config == null) {
|
|
throw new NullPointerException("config == null");
|
|
}
|
|
|
|
X509Certificate cert = config.getCertificate();
|
|
return isCertificateInLineage(cert);
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if the specified certificate is in the lineage.
|
|
*/
|
|
public boolean isCertificateInLineage(X509Certificate cert) {
|
|
if (cert == null) {
|
|
throw new NullPointerException("cert == null");
|
|
}
|
|
|
|
for (int i = 0; i < mSigningLineage.size(); i++) {
|
|
if (mSigningLineage.get(i).signingCert.equals(cert)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static int calculateDefaultFlags() {
|
|
return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION
|
|
| PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH;
|
|
}
|
|
|
|
/**
|
|
* Returns a new SigingCertificateLineage which terminates at the node corresponding to the
|
|
* given certificate. This is useful in the event of rotating to a new signing algorithm that
|
|
* is only supported on some platform versions. It enables a v3 signature to be generated using
|
|
* this signing certificate and the shortened proof-of-rotation record from this sub lineage in
|
|
* conjunction with the appropriate SDK version values.
|
|
*
|
|
* @param x509Certificate the signing certificate for which to search
|
|
* @return A new SigningCertificateLineage if the given certificate is present.
|
|
*
|
|
* @throws IllegalArgumentException if the provided certificate is not in the lineage.
|
|
*/
|
|
public SigningCertificateLineage getSubLineage(X509Certificate x509Certificate) {
|
|
if (x509Certificate == null) {
|
|
throw new NullPointerException("x509Certificate == null");
|
|
}
|
|
for (int i = 0; i < mSigningLineage.size(); i++) {
|
|
if (mSigningLineage.get(i).signingCert.equals(x509Certificate)) {
|
|
return new SigningCertificateLineage(
|
|
mMinSdkVersion, new ArrayList<>(mSigningLineage.subList(0, i + 1)));
|
|
}
|
|
}
|
|
|
|
// looks like we didn't find the cert,
|
|
throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage");
|
|
}
|
|
|
|
/**
|
|
* Consolidates all of the lineages found in an APK into one lineage, which is the longest one.
|
|
* In so doing, it also checks that all of the smaller lineages are contained in the largest,
|
|
* and that they properly cover the desired platform ranges.
|
|
*
|
|
* An APK may contain multiple lineages, one for each signer, which correspond to different
|
|
* supported platform versions. In this event, the lineage(s) from the earlier platform
|
|
* version(s) need to be present in the most recent (longest) one to make sure that when a
|
|
* platform version changes.
|
|
*
|
|
* <note> This does not verify that the largest lineage corresponds to the most recent supported
|
|
* platform version. That check requires is performed during v3 verification. </note>
|
|
*/
|
|
public static SigningCertificateLineage consolidateLineages(
|
|
List<SigningCertificateLineage> lineages) {
|
|
if (lineages == null || lineages.isEmpty()) {
|
|
return null;
|
|
}
|
|
int largestIndex = 0;
|
|
int maxSize = 0;
|
|
|
|
// determine the longest chain
|
|
for (int i = 0; i < lineages.size(); i++) {
|
|
int curSize = lineages.get(i).size();
|
|
if (curSize > maxSize) {
|
|
largestIndex = i;
|
|
maxSize = curSize;
|
|
}
|
|
}
|
|
|
|
List<SigningCertificateNode> largestList = lineages.get(largestIndex).mSigningLineage;
|
|
// make sure all other lineages fit into this one, with the same capabilities
|
|
for (int i = 0; i < lineages.size(); i++) {
|
|
if (i == largestIndex) {
|
|
continue;
|
|
}
|
|
List<SigningCertificateNode> underTest = lineages.get(i).mSigningLineage;
|
|
if (!underTest.equals(largestList.subList(0, underTest.size()))) {
|
|
throw new IllegalArgumentException("Inconsistent SigningCertificateLineages. "
|
|
+ "Not all lineages are subsets of each other.");
|
|
}
|
|
}
|
|
|
|
// if we've made it this far, they all check out, so just return the largest
|
|
return lineages.get(largestIndex);
|
|
}
|
|
|
|
/**
|
|
* Representation of the capabilities the APK would like to grant to its old signing
|
|
* certificates. The {@code SigningCertificateLineage} provides two conceptual data structures.
|
|
* 1) proof of rotation - Evidence that other parties can trust an APK's current signing
|
|
* certificate if they trust an older one in this lineage
|
|
* 2) self-trust - certain capabilities may have been granted by an APK to other parties based
|
|
* on its own signing certificate. When it changes its signing certificate it may want to
|
|
* allow the other parties to retain those capabilities.
|
|
* {@code SignerCapabilties} provides a representation of the second structure.
|
|
*
|
|
* <p>Use {@link Builder} to obtain configuration instances.
|
|
*/
|
|
public static class SignerCapabilities {
|
|
private final int mFlags;
|
|
|
|
private final int mCallerConfiguredFlags;
|
|
|
|
private SignerCapabilities(int flags) {
|
|
this(flags, 0);
|
|
}
|
|
|
|
private SignerCapabilities(int flags, int callerConfiguredFlags) {
|
|
mFlags = flags;
|
|
mCallerConfiguredFlags = callerConfiguredFlags;
|
|
}
|
|
|
|
private int getFlags() {
|
|
return mFlags;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if the capabilities of this object match those of the provided
|
|
* object.
|
|
*/
|
|
public boolean equals(SignerCapabilities other) {
|
|
return this.mFlags == other.mFlags;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this object has the installed data capability.
|
|
*/
|
|
public boolean hasInstalledData() {
|
|
return (mFlags & PAST_CERT_INSTALLED_DATA) != 0;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this object has the shared UID capability.
|
|
*/
|
|
public boolean hasSharedUid() {
|
|
return (mFlags & PAST_CERT_SHARED_USER_ID) != 0;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this object has the permission capability.
|
|
*/
|
|
public boolean hasPermission() {
|
|
return (mFlags & PAST_CERT_PERMISSION) != 0;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this object has the rollback capability.
|
|
*/
|
|
public boolean hasRollback() {
|
|
return (mFlags & PAST_CERT_ROLLBACK) != 0;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this object has the auth capability.
|
|
*/
|
|
public boolean hasAuth() {
|
|
return (mFlags & PAST_CERT_AUTH) != 0;
|
|
}
|
|
|
|
/**
|
|
* Builder of {@link SignerCapabilities} instances.
|
|
*/
|
|
public static class Builder {
|
|
private int mFlags;
|
|
|
|
private int mCallerConfiguredFlags;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder}.
|
|
*/
|
|
public Builder() {
|
|
mFlags = calculateDefaultFlags();
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} with the initial capabilities set to the provided
|
|
* flags.
|
|
*/
|
|
public Builder(int flags) {
|
|
mFlags = flags;
|
|
}
|
|
|
|
/**
|
|
* Set the {@code PAST_CERT_INSTALLED_DATA} flag in this capabilities object. This flag
|
|
* is used by the platform to determine if installed data associated with previous
|
|
* signing certificate should be trusted. In particular, this capability is required to
|
|
* perform signing certificate rotation during an upgrade on-device. Without it, the
|
|
* platform will not permit the app data from the old signing certificate to
|
|
* propagate to the new version. Typically, this flag should be set to enable signing
|
|
* certificate rotation, and may be unset later when the app developer is satisfied that
|
|
* their install base is as migrated as it will be.
|
|
*/
|
|
public Builder setInstalledData(boolean enabled) {
|
|
mCallerConfiguredFlags |= PAST_CERT_INSTALLED_DATA;
|
|
if (enabled) {
|
|
mFlags |= PAST_CERT_INSTALLED_DATA;
|
|
} else {
|
|
mFlags &= ~PAST_CERT_INSTALLED_DATA;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the {@code PAST_CERT_SHARED_USER_ID} flag in this capabilities object. This flag
|
|
* is used by the platform to determine if this app is willing to be sharedUid with
|
|
* other apps which are still signed with the associated signing certificate. This is
|
|
* useful in situations where sharedUserId apps would like to change their signing
|
|
* certificate, but can't guarantee the order of updates to those apps.
|
|
*/
|
|
public Builder setSharedUid(boolean enabled) {
|
|
mCallerConfiguredFlags |= PAST_CERT_SHARED_USER_ID;
|
|
if (enabled) {
|
|
mFlags |= PAST_CERT_SHARED_USER_ID;
|
|
} else {
|
|
mFlags &= ~PAST_CERT_SHARED_USER_ID;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the {@code PAST_CERT_PERMISSION} flag in this capabilities object. This flag
|
|
* is used by the platform to determine if this app is willing to grant SIGNATURE
|
|
* permissions to apps signed with the associated signing certificate. Without this
|
|
* capability, an application signed with the older certificate will not be granted the
|
|
* SIGNATURE permissions defined by this app. In addition, if multiple apps define the
|
|
* same SIGNATURE permission, the second one the platform sees will not be installable
|
|
* if this capability is not set and the signing certificates differ.
|
|
*/
|
|
public Builder setPermission(boolean enabled) {
|
|
mCallerConfiguredFlags |= PAST_CERT_PERMISSION;
|
|
if (enabled) {
|
|
mFlags |= PAST_CERT_PERMISSION;
|
|
} else {
|
|
mFlags &= ~PAST_CERT_PERMISSION;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the {@code PAST_CERT_ROLLBACK} flag in this capabilities object. This flag
|
|
* is used by the platform to determine if this app is willing to upgrade to a new
|
|
* version that is signed by one of its past signing certificates.
|
|
*
|
|
* <note> WARNING: this effectively removes any benefit of signing certificate changes,
|
|
* since a compromised key could retake control of an app even after change, and should
|
|
* only be used if there is a problem encountered when trying to ditch an older cert
|
|
* </note>
|
|
*/
|
|
public Builder setRollback(boolean enabled) {
|
|
mCallerConfiguredFlags |= PAST_CERT_ROLLBACK;
|
|
if (enabled) {
|
|
mFlags |= PAST_CERT_ROLLBACK;
|
|
} else {
|
|
mFlags &= ~PAST_CERT_ROLLBACK;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the {@code PAST_CERT_AUTH} flag in this capabilities object. This flag
|
|
* is used by the platform to determine whether or not privileged access based on
|
|
* authenticator module signing certificates should be granted.
|
|
*/
|
|
public Builder setAuth(boolean enabled) {
|
|
mCallerConfiguredFlags |= PAST_CERT_AUTH;
|
|
if (enabled) {
|
|
mFlags |= PAST_CERT_AUTH;
|
|
} else {
|
|
mFlags &= ~PAST_CERT_AUTH;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Applies the capabilities that were explicitly set in the provided capabilities object
|
|
* to this builder. Any values that were not set will not be applied to this builder
|
|
* to prevent unintentinoally setting a capability back to a default value.
|
|
*/
|
|
public Builder setCallerConfiguredCapabilities(SignerCapabilities capabilities) {
|
|
// The mCallerConfiguredFlags should have a bit set for each capability that was
|
|
// set by a caller. If a capability was explicitly set then the corresponding bit
|
|
// in mCallerConfiguredFlags should be set. This allows the provided capabilities
|
|
// to take effect for those set by the caller while those that were not set will
|
|
// be cleared by the bitwise and and the initial value for the builder will remain.
|
|
mFlags = (mFlags & ~capabilities.mCallerConfiguredFlags) |
|
|
(capabilities.mFlags & capabilities.mCallerConfiguredFlags);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns a new {@code SignerConfig} instance configured based on the configuration of
|
|
* this builder.
|
|
*/
|
|
public SignerCapabilities build() {
|
|
return new SignerCapabilities(mFlags, mCallerConfiguredFlags);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage}
|
|
*
|
|
* <p>Use {@link Builder} to obtain configuration instances.
|
|
*/
|
|
public static class SignerConfig {
|
|
private final PrivateKey mPrivateKey;
|
|
private final X509Certificate mCertificate;
|
|
|
|
private SignerConfig(
|
|
PrivateKey privateKey,
|
|
X509Certificate certificate) {
|
|
mPrivateKey = privateKey;
|
|
mCertificate = certificate;
|
|
}
|
|
|
|
/**
|
|
* Returns the signing key of this signer.
|
|
*/
|
|
public PrivateKey getPrivateKey() {
|
|
return mPrivateKey;
|
|
}
|
|
|
|
/**
|
|
* Returns the certificate(s) of this signer. The first certificate's public key corresponds
|
|
* to this signer's private key.
|
|
*/
|
|
public X509Certificate getCertificate() {
|
|
return mCertificate;
|
|
}
|
|
|
|
/**
|
|
* Builder of {@link SignerConfig} instances.
|
|
*/
|
|
public static class Builder {
|
|
private final PrivateKey mPrivateKey;
|
|
private final X509Certificate mCertificate;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder}.
|
|
*
|
|
* @param privateKey signing key
|
|
* @param certificate the X.509 certificate with a subject public key of the
|
|
* {@code privateKey}.
|
|
*/
|
|
public Builder(
|
|
PrivateKey privateKey,
|
|
X509Certificate certificate) {
|
|
mPrivateKey = privateKey;
|
|
mCertificate = certificate;
|
|
}
|
|
|
|
/**
|
|
* Returns a new {@code SignerConfig} instance configured based on the configuration of
|
|
* this builder.
|
|
*/
|
|
public SignerConfig build() {
|
|
return new SignerConfig(
|
|
mPrivateKey,
|
|
mCertificate);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builder of {@link SigningCertificateLineage} instances.
|
|
*/
|
|
public static class Builder {
|
|
private final SignerConfig mOriginalSignerConfig;
|
|
private final SignerConfig mNewSignerConfig;
|
|
private SignerCapabilities mOriginalCapabilities;
|
|
private SignerCapabilities mNewCapabilities;
|
|
private int mMinSdkVersion;
|
|
/**
|
|
* Constructs a new {@code Builder}.
|
|
*
|
|
* @param originalSignerConfig first signer in this lineage, parent of the next
|
|
* @param newSignerConfig new signer in the lineage; the new signing key that the APK will
|
|
* use
|
|
*/
|
|
public Builder(
|
|
SignerConfig originalSignerConfig,
|
|
SignerConfig newSignerConfig) {
|
|
if (originalSignerConfig == null || newSignerConfig == null) {
|
|
throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
|
|
+ "new SigningCertificateLineage");
|
|
}
|
|
mOriginalSignerConfig = originalSignerConfig;
|
|
mNewSignerConfig = newSignerConfig;
|
|
}
|
|
|
|
/**
|
|
* Sets the minimum Android platform version (API Level) on which this lineage is expected
|
|
* to validate. It is possible that newer signers in the lineage may not be recognized on
|
|
* the given platform, but as long as an older signer is, the lineage can still be used to
|
|
* sign an APK for the given platform.
|
|
*
|
|
* <note> By default, this value is set to the value for the
|
|
* P release, since this structure was created for that release, and will also be set to
|
|
* that value if a smaller one is specified. </note>
|
|
*/
|
|
public Builder setMinSdkVersion(int minSdkVersion) {
|
|
mMinSdkVersion = minSdkVersion;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets capabilities to give {@code mOriginalSignerConfig}. These capabilities allow an
|
|
* older signing certificate to still be used in some situations on the platform even though
|
|
* the APK is now being signed by a newer signing certificate.
|
|
*/
|
|
public Builder setOriginalCapabilities(SignerCapabilities signerCapabilities) {
|
|
if (signerCapabilities == null) {
|
|
throw new NullPointerException("signerCapabilities == null");
|
|
}
|
|
mOriginalCapabilities = signerCapabilities;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets capabilities to give {@code mNewSignerConfig}. These capabilities allow an
|
|
* older signing certificate to still be used in some situations on the platform even though
|
|
* the APK is now being signed by a newer signing certificate. By default, the new signer
|
|
* will have all capabilities, so when first switching to a new signing certificate, these
|
|
* capabilities have no effect, but they will act as the default level of trust when moving
|
|
* to a new signing certificate.
|
|
*/
|
|
public Builder setNewCapabilities(SignerCapabilities signerCapabilities) {
|
|
if (signerCapabilities == null) {
|
|
throw new NullPointerException("signerCapabilities == null");
|
|
}
|
|
mNewCapabilities = signerCapabilities;
|
|
return this;
|
|
}
|
|
|
|
public SigningCertificateLineage build()
|
|
throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
|
|
SignatureException {
|
|
if (mMinSdkVersion < AndroidSdkVersion.P) {
|
|
mMinSdkVersion = AndroidSdkVersion.P;
|
|
}
|
|
|
|
if (mOriginalCapabilities == null) {
|
|
mOriginalCapabilities = new SignerCapabilities.Builder().build();
|
|
}
|
|
|
|
if (mNewCapabilities == null) {
|
|
mNewCapabilities = new SignerCapabilities.Builder().build();
|
|
}
|
|
|
|
return createSigningLineage(
|
|
mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities,
|
|
mNewSignerConfig, mNewCapabilities);
|
|
}
|
|
}
|
|
}
|