Android App(更新时)签名(证书)校验过程源码分析
还是看到知乎上的一个【问题】,然后翻了翻Android源代码,给出了一个答案,在这里好好整理一下代码流程。
对于非系统APP来说,更新包的时候,需要检查更新包和被更新目标是否是相同的签发者所签发,即更新包和被更新包是由相同的签名密钥签发,此时认为更新包和原应用是由相同实体(发布者)所生成,并建立更新包和已经存在应用的信任关系。这个规则叫做【同源策略TOFU Trust On First Use】——摘自《Android安全架构探究》
APP在升级的时候,会验证新的APK包的完整性,确保没被修改,验证通过,证书信息最终会保存到PackageParser.Package.mSignatures字段中。然后判断新旧证书是否是同一个,接下来进行升级或者拒绝。
接下来感兴趣的话看代码吧~ PackageManagerService
@Override
public int checkSignatures(String pkg1, String pkg2) {
synchronized (mPackages) {
final PackageParser.Package p1 = mPackages.get(pkg1);
final PackageParser.Package p2 = mPackages.get(pkg2);
if (p1 == null || p1.mExtras == null
|| p2 == null || p2.mExtras == null) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
final PackageSetting ps1 = (PackageSetting) p1.mExtras;
final PackageSetting ps2 = (PackageSetting) p2.mExtras;
if (filterAppAccessLPr(ps1, callingUid, callingUserId)
|| filterAppAccessLPr(ps2, callingUid, callingUserId)) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
return compareSignatures(p1.mSignatures, p2.mSignatures);
}
}
然后:
/**
* Compares two sets of signatures. Returns:
* <br />
* {@link PackageManager#SIGNATURE_NEITHER_SIGNED}: if both signature sets are null,
* <br />
* {@link PackageManager#SIGNATURE_FIRST_NOT_SIGNED}: if the first signature set is null,
* <br />
* {@link PackageManager#SIGNATURE_SECOND_NOT_SIGNED}: if the second signature set is null,
* <br />
* {@link PackageManager#SIGNATURE_MATCH}: if the two signature sets are identical,
* <br />
* {@link PackageManager#SIGNATURE_NO_MATCH}: if the two signature sets differ.
*/
static int compareSignatures(Signature[] s1, Signature[] s2) {
if (s1 == null) {
return s2 == null
? PackageManager.SIGNATURE_NEITHER_SIGNED
: PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
}
if (s2 == null) {
return PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
}
if (s1.length != s2.length) {
return PackageManager.SIGNATURE_NO_MATCH;
}
// Since both signature sets are of size 1, we can compare without HashSets.
if (s1.length == 1) {
return s1[0].equals(s2[0]) ?
PackageManager.SIGNATURE_MATCH :
PackageManager.SIGNATURE_NO_MATCH;
}
ArraySet<Signature> set1 = new ArraySet<Signature>();
for (Signature sig : s1) {
set1.add(sig);
}
ArraySet<Signature> set2 = new ArraySet<Signature>();
for (Signature sig : s2) {
set2.add(sig);
}
// Make sure s2 contains all signatures in s1.
if (set1.equals(set2)) {
return PackageManager.SIGNATURE_MATCH;
}
return PackageManager.SIGNATURE_NO_MATCH;
}
咱们来看看Signature类,看看什么时候认为两个包是相同实体发布的,重点关注equals、hashCode方法:
/*
* Copyright (C) 2008 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 android.content.pm;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.ArrayUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
/**
* Opaque, immutable representation of a signing certificate associated with an
* application package.
* <p>
* This class name is slightly misleading, since it's not actually a signature.
*/
public class Signature implements Parcelable {
private final byte[] mSignature;
private int mHashCode;
private boolean mHaveHashCode;
private SoftReference<String> mStringRef;
private Certificate[] mCertificateChain;
/**
* Create Signature from an existing raw byte array.
*/
public Signature(byte[] signature) {
mSignature = signature.clone();
mCertificateChain = null;
}
/**
* Create signature from a certificate chain. Used for backward
* compatibility.
*
* @throws CertificateEncodingException
* @hide
*/
public Signature(Certificate[] certificateChain) throws CertificateEncodingException {
mSignature = certificateChain[0].getEncoded();
if (certificateChain.length > 1) {
mCertificateChain = Arrays.copyOfRange(certificateChain, 1, certificateChain.length);
}
}
private static final int parseHexDigit(int nibble) {
if ('0' <= nibble && nibble <= '9') {
return nibble - '0';
} else if ('a' <= nibble && nibble <= 'f') {
return nibble - 'a' + 10;
} else if ('A' <= nibble && nibble <= 'F') {
return nibble - 'A' + 10;
} else {
throw new IllegalArgumentException("Invalid character " + nibble + " in hex string");
}
}
/**
* Create Signature from a text representation previously returned by
* {@link #toChars} or {@link #toCharsString()}. Signatures are expected to
* be a hex-encoded ASCII string.
*
* @param text hex-encoded string representing the signature
* @throws IllegalArgumentException when signature is odd-length
*/
public Signature(String text) {
final byte[] input = text.getBytes();
final int N = input.length;
if (N % 2 != 0) {
throw new IllegalArgumentException("text size " + N + " is not even");
}
final byte[] sig = new byte[N / 2];
int sigIndex = 0;
for (int i = 0; i < N;) {
final int hi = parseHexDigit(input[i++]);
final int lo = parseHexDigit(input[i++]);
sig[sigIndex++] = (byte) ((hi << 4) | lo);
}
mSignature = sig;
}
......
/**
* Returns the public key for this signature.
*
* @throws CertificateException when Signature isn't a valid X.509
* certificate; shouldn't happen.
* @hide
*/
public PublicKey getPublicKey() throws CertificateException {
final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); // DER 编码的X.509格式密钥
final ByteArrayInputStream bais = new ByteArrayInputStream(mSignature);
final Certificate cert = certFactory.generateCertificate(bais);
return cert.getPublicKey();
}
......
@Override
public boolean equals(Object obj) {
try {
if (obj != null) {
Signature other = (Signature)obj;
return this == other || Arrays.equals(mSignature, other.mSignature);
}
} catch (ClassCastException e) {
}
return false;
}
@Override
public int hashCode() {
if (mHaveHashCode) {
return mHashCode;
}
mHashCode = Arrays.hashCode(mSignature);
mHaveHashCode = true;
return mHashCode;
}
......
}
查看源代码可知,
Opaque, immutable representation of a signing certificate associated with an application package.
它是一个“不透明的、不可变的且与应用程序包关联的签名证书表示类(签名整数对象)”。 即,它是一个与某个APK文件关联的DER编码格式签名证书的封装。
查看APK安装过程源码,找到证书数据加载的位置,在PackageManagerService.java类中:
private PackageParser.Package scanPackageInternalLI(PackageParser.Package pkg, File scanFile, int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user) {
......
//注意变量是否APP更新
final boolean isUpdatedPkg = updatedPkg != null;
final boolean isUpdatedSystemPkg = isUpdatedPkg && (policyFlags & PackageParser.PARSE_IS_SYSTEM) != 0;
......
//如果APP更新,设置policyFlags
if (isUpdatedPkg) {
// An updated system app will not have the PARSE_IS_SYSTEM flag set
// initially
policyFlags |= PackageParser.PARSE_IS_SYSTEM;
// An updated privileged app will not have the PARSE_IS_PRIVILEGED
// flag set initially
if ((updatedPkg.pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
policyFlags |= PackageParser.PARSE_IS_PRIVILEGED;
}
}
// Verify certificates against what was last scanned
collectCertificatesLI(ps, pkg, scanFile, policyFlags);
}
经过 collectCertificatesLI,collectCertificatesInternal, 层层跳转,进入 PackageParse.java
private static void collectCertificates(Package pkg, File apkFile, int parseFlags)
throws PackageParserException {
......
//省略了V2签名,直接看V1
StrictJarFile jarFile = null;
try {
// Ignore signature stripping protections when verifying APKs from system partition.
// For those APKs we only care about extracting signer certificates, and don't care
// about verifying integrity.
boolean signatureSchemeRollbackProtectionsEnforced =
(parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
//初始化 jarFile
jarFile = new StrictJarFile(
apkPath,
!verified, // whether to verify JAR signature
signatureSchemeRollbackProtectionsEnforced);
......
// Verify that entries are signed consistently with the first entry
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.
for (ZipEntry entry : toVerify) {
//紧接着,调用loadCertificates方法:
final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
if (ArrayUtils.isEmpty(entryCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Package " + apkPath + " has no certificates at entry "
+ entry.getName());
}
final Signature[] entrySignatures = convertToSignatures(entryCerts);
//仅赋值一次,不为空则判断每次获取到的Certificate是否相同,不同则抛出PackageParserException异常
if (pkg.mCertificates == null) {
pkg.mCertificates = entryCerts;
pkg.mSignatures = entrySignatures;
pkg.mSigningKeys = new ArraySet<PublicKey>();
for (int i=0; i < entryCerts.length; i++) {
pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
+ " has mismatched certificates at entry "
+ entry.getName());
}
}
}
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath, e);
} finally {
closeQuietly(jarFile);
}
}
快到地方了,不要着急,咱们接着看loadCertificates方法:
private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
throws PackageParserException {
InputStream is = null;
try {
// We must read the stream for the JarEntry to retrieve
// its certificates.
//在getInputStream方法中初始化certificates数据
is = jarFile.getInputStream(entry);
//在其中调用is[JarFileInputStream类型]的read方法
//read方法调用verify方法,读取密钥到verifiedEntries中
readFullyIgnoringContents(is);
return jarFile.getCertificateChains(entry);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed reading " + entry.getName() + " in " + jarFile, e);
} finally {
IoUtils.closeQuietly(is);
}
}
OK,终于看到出处了,StrictJarFile类中:
public InputStream getInputStream(ZipEntry ze) {
final InputStream is = getZipInputStream(ze);
if (isSigned) {
//init 方法,创建VerifierEntry
StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
if (entry == null) {
return is;
}
return new JarFileInputStream(is, ze.getSize(), entry);
}
return is;
}
public Certificate[][] getCertificateChains(ZipEntry ze) {
if (isSigned) {
//verifier 从哪里来呢,接下来往下看。
return verifier.getCertificateChains(ze.getName());
}
return null;
}
verifier类型为StrictJarVerifier.java类查看源代码可知,verifiedEntries的赋值是在其内部类VerifierEntry的verify()方法中。
//存放密钥的变量
private final Hashtable<String, Certificate[][]> verifiedEntries =
new Hashtable<String, Certificate[][]>();
......
Certificate[][] getCertificateChains(String name) {
return verifiedEntries.get(name);
}
又因为verifiedEntries数据从外部传进来,所以还需要继续找。。。源头,在 jarFile = new StrictJarFile(apkPath, !verified, signatureSchemeRollbackProtectionsEnforced); 这句代码中,
private StrictJarFile(String name,
FileDescriptor fd,
boolean verify,
boolean signatureSchemeRollbackProtectionsEnforced)
throws IOException, SecurityException {
this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
this.fd = fd;
try {
// Read the MANIFEST and signature files up front and try to
// parse them. We never want to accept a JAR File with broken signatures
// or manifests, so it's best to throw as early as possible.
if (verify) {
HashMap<String, byte[]> metaEntries = getMetaEntries();
this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
this.verifier =
new StrictJarVerifier(
name,
manifest,
metaEntries,
signatureSchemeRollbackProtectionsEnforced);
Set<String> files = manifest.getEntries().keySet();
for (String file : files) {
if (findEntry(file) == null) {
throw new SecurityException("File " + file + " in manifest does not exist");
}
}
//此处readCertificates为最终数据初始化的地方
isSigned = verifier.readCertificates() && verifier.isSignedJar();
} else {
isSigned = false;
this.manifest = null;
this.verifier = null;
}
} catch (IOException | SecurityException e) {
nativeClose(this.nativeHandle);
IoUtils.closeQuietly(fd);
closed = true;
throw e;
}
guard.open("close");
}
还是要到StrictJarVerifier.java类中
/**
* If the associated JAR file is signed, check on the validity of all of the
* known signatures.
*
* @return {@code true} if the associated JAR is signed and an internal
* check verifies the validity of the signature(s). {@code false} if
* the associated JAR file has no entries at all in its {@code
* META-INF} directory. This situation is indicative of an invalid
* JAR file.
* <p>
* Will also return {@code true} if the JAR file is <i>not</i>
* signed.
* @throws SecurityException
* if the JAR file is signed and it is determined that a
* signature block file contains an invalid signature for the
* corresponding signature file.
*/
synchronized boolean readCertificates() {
if (metaEntries.isEmpty()) {
return false;
}
Iterator<String> it = metaEntries.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
//只解析这三种类型的密钥
if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
verifyCertificate(key);
it.remove();
}
}
return true;
}
/**
* @param certFile
*/
private void verifyCertificate(String certFile) {
...... //省略了使用证书(如RSA这行书)中的 .SF属性块对整个MF文件验证的过程
//验证通过,signature在StrictJarVerifier类中的initEntry方法中变成二维数组,
//放进StrictJarVerifier 的verifiedEntries变量中
signatures.put(signatureFile, entries);
}
至此分析结束
参考: