-
一个应用完整的安装过程如下图所示
入口函数有两处,一处由系统应用PackageInstaller调用installPackageWithVerificationAndEncryption()方法实现有界面的安装过程,一处由System级的APP调用installPackage()实现静默安装,本质其实是一样的:
@Override
public void installPackageWithVerificationAndEncryption(Uri packageURI,
IPackageInstallObserver observer, int flags, String installerPackageName,
VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) {
installCommon(packageURI, new LegacyPackageInstallObserver(observer), flags,
installerPackageName, verificationParams, encryptionParams);
}
@Override
public void installPackage(Uri packageURI, PackageInstallObserver observer,
int flags, String installerPackageName) {
final VerificationParams verificationParams = new VerificationParams(null, null,
null, VerificationParams.NO_UID, null);
installCommon(packageURI, observer, flags, installerPackageName, verificationParams, null);
}
我们重点看一下apk的签名校验过程,讲之前先说明一下APK文件解压后的META-INF目录,一般它有如下三个文件:
ANDROIDR.RSA、ANDROIDR.SF、MANIFEST.MF
APK要想安装成功,在校验的过程中要满足如下三个条件:
- MF文件中的SHA-1值与对应文件的真实SHA-1值要相等(不计META-INF目录)
- SF文件中的SHA-1值与MF文件本身与文件中的各子项作SHA-1和Base64编码后相等
- SF文件的签名信息与RSA文件的内容要一致
从函数collectCertificates()开始:
private static void collectCertificates(Package pkg, File apkFile, int flags)
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
StrictJarFile jarFile = null;
try {
/// TODO: [ALPS01655835][L.AOSP.EARLY.DEV] Bring-up build error on frameworks/base @{
jarFile = new StrictJarFile(apkPath);
/// @}
// Always verify manifest, regardless of source
final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
}
final List<ZipEntry> toVerify = new ArrayList<>();
toVerify.add(manifestEntry);
// If we're parsing an untrusted package, verify all contents
if ((flags & PARSE_IS_SYSTEM) == 0) {
final Iterator<ZipEntry> i = jarFile.iterator();
while (i.hasNext()) {
final ZipEntry entry = i.next();
if (entry.isDirectory()) continue;
if (entry.getName().startsWith("META-INF/")) continue;
if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;
toVerify.add(entry);
}
}
// 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) {
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);
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());
}
}
}
} 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);
}
}
首先是StrictJarFile对象的构造:
public StrictJarFile(String fileName) throws IOException, SecurityException {
this.nativeHandle = nativeOpenJarFile(fileName);
this.raf = new RandomAccessFile(fileName, "r");
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.
HashMap<String, byte[]> metaEntries = getMetaEntries();
this.manifest = new Manifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
this.verifier = new JarVerifier(fileName, manifest, metaEntries);
Set<String> files = this.manifest.getEntries().keySet();
for (String file : files) {
if (findEntry(file) == null) {
throw new SecurityException(fileName + ": File " + file + " in manifest does not exist");
}
}
isSigned = verifier.readCertificates() && verifier.isSignedJar();
} catch (IOException | SecurityException e) {
nativeClose(this.nativeHandle);
IoUtils.closeQuietly(this.raf);
throw e;
}
guard.open("close");
}
重点是isSigned 进行赋值时使用的两个方法:JarVerifier的readCertificates()和isSignedJar()方法:
/**
* 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;
}
/**
* Returns a <code>boolean</code> indication of whether or not the
* associated jar file is signed.
*
* @return {@code true} if the JAR is signed, {@code false}
* otherwise.
*/
boolean isSignedJar() {
return certificates.size() > 0;
}
SF文件校验:
/**
* @param certFile
*/
private void verifyCertificate(String certFile) {
// Found Digital Sig, .SF should already have been read
String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
byte[] sfBytes = metaEntries.get(signatureFile);
if (sfBytes == null) {
return;
}
byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
// Manifest entry is required for any verifications.
if (manifestBytes == null) {
return;
}
//获取证书信息,验证SF文件的签名信息是否与RSA文件的内部一致
byte[] sBlockBytes = metaEntries.get(certFile);
try {
Certificate[] signerCertChain = JarUtils.verifySignature(
new ByteArrayInputStream(sfBytes),
new ByteArrayInputStream(sBlockBytes));
if (signerCertChain != null) {
certificates.put(signatureFile, signerCertChain);
}
} catch (IOException e) {
return;
} catch (GeneralSecurityException e) {
throw failedVerification(jarName, signatureFile);
}
// Verify manifest hash in .sf file
Attributes attributes = new Attributes();
HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
try {
ManifestReader im = new ManifestReader(sfBytes, attributes);
im.readEntries(entries, null);
} catch (IOException e) {
return;
}
// Do we actually have any signatures to look at?
if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
return;
}
boolean createdBySigntool = false;
String createdBy = attributes.getValue("Created-By");
if (createdBy != null) {
createdBySigntool = createdBy.indexOf("signtool") != -1;
}
// Use .SF to verify the mainAttributes of the manifest
// If there is no -Digest-Manifest-Main-Attributes entry in .SF
// file, such as those created before java 1.5, then we ignore
// such verification.
//MF文件本身的签名及MF文件中各属性块的签名是否与SF文件中的信息匹配
if (mainAttributesEnd > 0 && !createdBySigntool) {
String digestAttribute = "-Digest-Manifest-Main-Attributes";
if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
throw failedVerification(jarName, signatureFile);
}
}
// Use .SF to verify the whole manifest.
String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Attributes> entry = it.next();
Manifest.Chunk chunk = manifest.getChunk(entry.getKey());
if (chunk == null) {
return;
}
if (!verify(entry.getValue(), "-Digest", manifestBytes,
chunk.start, chunk.end, createdBySigntool, false)) {
throw invalidDigest(signatureFile, entry.getKey(), jarName);
}
}
}
metaEntries.put(signatureFile, null);
signatures.put(signatureFile, entries);
}
使用libcore\luni\src\main\java\org\apache\harmony\security\utils\JarUtils.java的verifySignature()方法获取证书信息:
public static Certificate[] verifySignature(InputStream signature, InputStream
signatureBlock) throws IOException, GeneralSecurityException {
BerInputStream bis = new BerInputStream(signatureBlock);
ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
SignedData signedData = info.getSignedData();
if (signedData == null) {
throw new IOException("No SignedData found");
}
Collection<org.apache.harmony.security.x509.Certificate> encCerts
= signedData.getCertificates();
if (encCerts.isEmpty()) {
return null;
}
X509Certificate[] certs = new X509Certificate[encCerts.size()];
CertificateFactory cf = CertificateFactory.getInstance("X.509");
int i = 0;
for (org.apache.harmony.security.x509.Certificate encCert : encCerts) {
final byte[] encoded = encCert.getEncoded();
final InputStream is = new ByteArrayInputStream(encoded);
certs[i++] = new VerbatimX509Certificate((X509Certificate) cf.generateCertificate(is),
encoded);
}
List<SignerInfo> sigInfos = signedData.getSignerInfos();
SignerInfo sigInfo;
if (!sigInfos.isEmpty()) {
sigInfo = sigInfos.get(0);
} else {
return null;
}
// Issuer
X500Principal issuer = sigInfo.getIssuer();
// Certificate serial number
BigInteger snum = sigInfo.getSerialNumber();
// Locate the certificate
int issuerSertIndex = 0;
for (i = 0; i < certs.length; i++) {
if (issuer.equals(certs[i].getIssuerDN()) &&
snum.equals(certs[i].getSerialNumber())) {
issuerSertIndex = i;
break;
}
}
if (i == certs.length) { // No issuer certificate found
return null;
}
if (certs[issuerSertIndex].hasUnsupportedCriticalExtension()) {
throw new SecurityException("Can not recognize a critical extension");
}
// Get Signature instance
final String daOid = sigInfo.getDigestAlgorithm();
final String daName = sigInfo.getDigestAlgorithmName();
final String deaOid = sigInfo.getDigestEncryptionAlgorithm();
final String deaName = sigInfo.getDigestEncryptionAlgorithmName();
String alg = null;
Signature sig = null;
if (daOid != null && deaOid != null) {
alg = daOid + "with" + deaOid;
try {
sig = Signature.getInstance(alg);
} catch (NoSuchAlgorithmException e) {
}
// Try to convert to names instead of OID.
if (sig == null && daName != null && deaName != null) {
alg = daName + "with" + deaName;
try {
sig = Signature.getInstance(alg);
} catch (NoSuchAlgorithmException e) {
}
}
}
if (sig == null && deaOid != null) {
alg = deaOid;
try {
sig = Signature.getInstance(alg);
} catch (NoSuchAlgorithmException e) {
}
if (sig == null) {
alg = deaName;
try {
sig = Signature.getInstance(alg);
} catch (NoSuchAlgorithmException e) {
}
}
}
// We couldn't find a valid Signature type.
if (sig == null) {
return null;
}
sig.initVerify(certs[issuerSertIndex]);
// If the authenticatedAttributes field of SignerInfo contains more than zero attributes,
// compute the message digest on the ASN.1 DER encoding of the Attributes value.
// Otherwise, compute the message digest on the data.
List<AttributeTypeAndValue> atr = sigInfo.getAuthenticatedAttributes();
byte[] sfBytes = new byte[signature.available()];
signature.read(sfBytes);
if (atr == null) {
sig.update(sfBytes);
} else {
sig.update(sigInfo.getEncodedAuthenticatedAttributes());
// If the authenticatedAttributes field contains the message-digest attribute,
// verify that it equals the computed digest of the signature file
byte[] existingDigest = null;
for (AttributeTypeAndValue a : atr) {
if (Arrays.equals(a.getType().getOid(), MESSAGE_DIGEST_OID)) {
if (existingDigest != null) {
throw new SecurityException("Too many MessageDigest attributes");
}
Collection<?> entries = a.getValue().getValues(ASN1OctetString.getInstance());
if (entries.size() != 1) {
throw new SecurityException("Too many values for MessageDigest attribute");
}
existingDigest = (byte[]) entries.iterator().next();
}
}
// RFC 2315 section 9.1: if authenticatedAttributes is present, it
// must have a message-digest attribute.
if (existingDigest == null) {
throw new SecurityException("Missing MessageDigest in Authenticated Attributes");
}
MessageDigest md = null;
if (daOid != null) {
md = MessageDigest.getInstance(daOid);
}
if (md == null && daName != null) {
md = MessageDigest.getInstance(daName);
}
if (md == null) {
return null;
}
byte[] computedDigest = md.digest(sfBytes);
if (!Arrays.equals(existingDigest, computedDigest)) {
throw new SecurityException("Incorrect MD");
}
}
if (!sig.verify(sigInfo.getEncryptedDigest())) {
throw new SecurityException("Incorrect signature");
}
return createChain(certs[issuerSertIndex], certs);
}
然后看下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.
is = jarFile.getInputStream(entry);
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);
}
}
进入\libcore\luni\src\main\java\java\util\jar\StrictJarFile.java的getInputStream()方法
public InputStream getInputStream(ZipEntry ze){
final InputStream is=getZipInputStream(ze);
if(isSigned){
JarVerifier.VerifierEntry entry=verifier.initEntry(ze.getName());
if(entry==null){
return is;
}
return new JarFile.JarFileInputStream(is,ze.getSize(),entry);
}
return is;
}
可能使用的算法:
private static final String[] DIGEST_ALGORITHMS = new String[] {
"SHA-512",
"SHA-384",
"SHA-256",
"SHA1",
};
构造一个VerifierEntry对象:
VerifierEntry initEntry(String name) {
// If no manifest is present by the time an entry is found,
// verification cannot occur. If no signature files have
// been found, do not verify.
if (manifest == null || signatures.isEmpty()) {
return null;
}
Attributes attributes = manifest.getAttributes(name);
// entry has no digest
if (attributes == null) {
return null;
}
ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
HashMap<String, Attributes> hm = entry.getValue();
if (hm.get(name) != null) {
// Found an entry for entry name in .SF file
String signatureFile = entry.getKey();
Certificate[] certChain = certificates.get(signatureFile);
if (certChain != null) {
certChains.add(certChain);
}
}
}
// entry is not signed
if (certChains.isEmpty()) {
return null;
}
Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);
for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
final String algorithm = DIGEST_ALGORITHMS[i];
final String hash = attributes.getValue(algorithm + "-Digest");
if (hash == null) {
continue;
}
byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
try {
return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
certChainsArray, verifiedEntries);
} catch (NoSuchAlgorithmException ignored) {
}
}
return null;
}
得到一个InputStream对象(JarFileInputStream),回到loadCertificates()方法,进入readFullylgnoringContents()
public static long readFullyIgnoringContents(InputStream in) throws IOException {
byte[] buffer = sBuffer.getAndSet(null);
if (buffer == null) {
buffer = new byte[4096];
}
int n = 0;
int count = 0;
while ((n = in.read(buffer, 0, buffer.length)) != -1) {
count += n;
}
sBuffer.set(buffer);
return count;
}
主要是一个read方法,我们看下JarFileInputStream的read()方法:
@Override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
if (done) {
return -1;
}
if (count > 0) {
int r = super.read(buffer, byteOffset, byteCount);
if (r != -1) {
int size = r;
if (count < size) {
size = (int) count;
}
entry.write(buffer, byteOffset, size);
count -= size;
} else {
count = 0;
}
if (count == 0) {
done = true;
entry.verify();
}
return r;
} else {
done = true;
entry.verify();
return -1;
}
}
这个entry即我们之前构造的VerifierEntry,我们看下它的verify()方法:
/**
* Verifies that the digests stored in the manifest match the decrypted
* digests from the .SF file. This indicates the validity of the
* signing, not the integrity of the file, as its digest must be
* calculated and verified when its contents are read.
*
* @throws SecurityException
* if the digest value stored in the manifest does <i>not</i>
* agree with the decrypted digest as recovered from the
* <code>.SF</code> file.
*/
void verify() {
byte[] d = digest.digest();
//这里对META-INF/MANIFEST.MF文件作了摘要校验(与指定文件的真实摘要值做对比)
if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
}
verifiedEntries.put(name, certChains);
}
回到loadCertificates()方法,进入StrictJarFile的getCertificateChains()方法:
/**
* Return all certificate chains for a given {@link ZipEntry} belonging to this jar.
* This method MUST be called only after fully exhausting the InputStream belonging
* to this entry.
*
* Returns {@code null} if this jar file isn't signed or if this method is
* called before the stream is processed.
*/
public Certificate[][] getCertificateChains(ZipEntry ze) {
if (isSigned) {
return verifier.getCertificateChains(ze.getName());
}
return null;
}