In order to ensure the security of the Android system and application, it is necessary to verify the integrity of the package when installing APK, and at the same time, to verify whether the old and new are matched in the scenario of overwriting the installation. Both of these are guaranteed by the Android signature mechanism. In this paper, we will simply take a look at the principle of Android signature and verification. The analysis is divided into several parts:

  • What is an APK signature
  • How does APK signature ensure APK information integrity
  • How do I sign an APK
  • How do I verify APK signatures

What is Android’s APK signature

Abstract is like a fingerprint information of the content. Once the content is tampered, the abstract will change. Signature is the encryption result of the abstract, and the signature will be invalid if the abstract changes. The same is true for Android APK signature. If the APK signature does not correspond to the content, the Android system considers that the APK content has been tampered with and refuses to install it to ensure system security. At present, Android has three signature types: V1, V2 (N), and V3 (P). In this paper, we only look at the first two types of V1 and V2, and ignore the round secret of V3. Let’s look at the style of APK with only V1 signature:

Let’s look at the APK package style with only V2 signature:

Both have V1 V2 signature:

As you can see, if there are only V2 signatures, the contents of the APK package are almost unchanged, and no new files are added to META_INF. According to Google official documentation: When using V2 signature scheme for signing, an APK signature block is inserted into the APK file, which is located before and adjacent to the zip central directory part. In APK signature block, signature and signer identity information are stored in APK signature scheme V2 block to ensure that the entire APK file cannot be modified, as shown in the following figure:

V1 signature uses three files in meta-INF to ensure the integrity of signature and information:

How does APK signature ensure APK information integrity

How does V1 signature ensure the integrity of information? V1 signature mainly consists of three parts, if the narrow sense of signature and public key, only in the. Rsa file, V1 signature of the three files is actually a set of mechanisms, not just one thing.

Manifest.mf: summary file, storage file name and file SHA1 summary (Base64 format) key value pair, the format is as follows, its main function is to ensure the integrity of each file

If the resource file in the APK is replaced, then the summary of the resource must be changed. If the information in the manifest.mf is not modified, then V1 verification will fail during installation and cannot be installed. However, if the file is tampered with, the summary value in the manifest.mf will also be changed. The manifest.MF check can then be bypassed.

Cert. SF: secondary digest file that stores the SHA1 digest (Base64 format) key-value pairs of the file name and the manifest.mf digest entry in the following format

Cert.sf personally feels a bit like redundancy, more like a secondary guarantee of file integrity. Just like bypassing manifest.mf,.sf checksum can be easily bypassed.

Cert. RSA certificate (public key) and signature file that stores the keystore public key, release information, and signature information for the cert. SF file abstract (encrypted with the keystore private key)

Cert. RSA and cert. SF are corresponding to each other, the prefix of the two names must be the same, I don’t know if it is a boring standard. Take a look at the cert. RSA file:

Cert. RSA file stores the certificate public key, expiration date, issuer, encryption algorithm and other information. According to the public key and encryption algorithm, The Android system can calculate the summary information of cert. SF, which is in strict format as follows:

From cert. RSA, we can obtain the fingerprint information of the certificate, which is often used in wechat sharing and third-party SDK application. It is actually a signature of the public key + developer information:

In addition to cert. RSA file, the other two signature files actually have nothing to do with the keystore. They are mainly the abstract and secondary abstract of the file itself. When you sign with different keystore, the manifest. MF and cert. SF generated are the same, the only difference is the cert. RSA signature file. That is to say, the former two mainly guarantee the integrity of each file, while cert. RSA guarantees the source and integrity of APK on the whole. However, files in META_INF are not in the scope of verification, which is also a shortcoming of V1. How does V2 signature ensure information integrity?

How does V2 signature block ensure the integrity of APK

As mentioned above, the integrity of files in V1 signature is easy to be bypassed. It can be understood that the significance of integrity verification of a single file is not great, and the installation is time-consuming. Therefore, it is better to adopt a simpler and convenient verification method. V2 signature is not verified for a single file, but for APK. APK is divided into 1M blocks, and value summaries are calculated for each block. Then, all the summaries are summarized, and then the summaries are signed.

In other words, V2 digest signature is divided into two levels. The first level is to digest parts 1, 3 and 4 of APK file, and the second level is to digest the set of the first level, and then use the secret key to sign. Block digests can be processed in parallel at installation time, which improves validation speed.

Simple APK Signature Process (Signature Principle)

Message Digest: A Message Digest algorithm performs a one-way Hash on the Message data to generate a fixed length Hash value, which is the Message Digest. MD5 and SHA1 are two types of Digest algorithms. In theory, the digest must have collisions, but as long as the collision rate is low over a finite length, the digest can be used to guarantee the integrity of the message, and the digest will always change if the message is tampered with. However, if the message is modified at the same time as the digest, there is no way to know.

As for digital signature (public key digital signature), asymmetric encryption technology is used to encrypt the abstract through the private key to generate a string. The string + public key certificate can be regarded as the digital signature of the message. For example, RSA is a commonly used asymmetric encryption algorithm. In the absence of a private key, asymmetric encryption algorithm can ensure that others cannot forge a signature, so digital signature is also an effective proof of the authenticity of the sender’s information. However, the Android keystore certificate is self-signed without third-party authority authentication. Users can generate their own keystore. The Android signature scheme cannot ensure that APK is not re-signed.

Knowing the concept of digest and signature, let’s take a look at the Android signature file. How does it affect the original APK package? The command to sign an APK using apksign in the SDK is as follows:

 ./apksigner sign  --ks   keystore.jks  --ks-key-alias keystore  --ks-pass pass:XXX  --key-pass pass:XXX  --out output.apk input.apk
Copy the code

The main implementation in android/platform/tools/apksig folder, the body is ApkSigner. Java sign function, the function is longer, take steps

private void sign( DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn) throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { // Step 1. Find input APK's main ZIP sections ApkUtils.ZipSections inputZipSections; <! Object--> try {inputZipSections = apkutils.findzipsections (inputApk); .Copy the code

Let’s start with apkutils. findZipSections. This function parses the APK file to get some simple information in ZIP format and returns a ZipSections.

public static ZipSections findZipSections(DataSource apk) throws IOException, ZipFormatException { Pair<ByteBuffer, Long> eocdAndOffsetInFile = ZipUtils.findZipEndOfCentralDirectoryRecord(apk); ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); long eocdOffset = eocdAndOffsetInFile.getSecond(); eocdBuf.order(ByteOrder.LITTLE_ENDIAN); long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); . long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); long cdEndOffset = cdStartOffset + cdSizeBytes; int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); return new ZipSections( cdStartOffset, cdSizeBytes, cdRecordCount, eocdOffset, eocdBuf); }Copy the code

ZipSections contain information in the ZIP file format, such as central directory information and central directory end information. The ZIP file format is as follows:

After obtaining the ZipSections, you can further parse the APK ZIP package to continue the signing process.

long inputApkSigningBlockOffset = -1; DataSource inputApkSigningBlock = null; <! - check whether V2 signature exist -- > try {Pair < DataSource, Long > apkSigningBlockAndOffset = V2SchemeVerifier. FindApkSigningBlock (inputApk, inputZipSections); inputApkSigningBlock = apkSigningBlockAndOffset.getFirst(); inputApkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); } catch (V2SchemeVerifier.SignatureNotFoundException e) { <! --V2 signature does not exist, it is not necessary. - outside of V2 signature information area - - > the DataSource inputApkLfhSection = inputApk. Slice (0, (inputApkSigningBlockOffset! = 1)? inputApkSigningBlockOffset : inputZipSections.getZipCentralDirectoryOffset());Copy the code

You can see that there is a check for the V2 signature, here for the signature, why is there a check for the V2 signature? Signature for the first time will go directly to the exception logic branch, repeat signature when allowed to take before V2 signature, doubt here for V2 signature aim should be to eliminate V2 signature, and obtain the V2 signature block of data, because the signature itself can’t be figured into the signature, then parse the central directory, Build a DefaultApkSignerEngine for signing

<! Parse the central directory area, Parse the input APK's ZIP Central Directory ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections); List<CentralDirectoryRecord> inputCdRecords = parseZipCentralDirectory(inputCd, inputZipSections); // Step 3. Obtain a signer engine instance ApkSignerEngine signerEngine; if (mSignerEngine ! = null) { signerEngine = mSignerEngine; } else { // Construct a signer engine from the provided parameters ... List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs = new ArrayList<>(mSignerConfigs.size()); <! SignerConfig --> for (SignerConfig: mSignerConfigs) { engineSignerConfigs.add( new DefaultApkSignerEngine.SignerConfig.Builder( signerConfig.getName(), signerConfig.getPrivateKey(), signerConfig.getCertificates()) .build()); } <! - V1 V2 are enabled by default - > DefaultApkSignerEngine. Builder signerEngineBuilder = new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion) .setV1SigningEnabled(mV1SigningEnabled) .setV2SigningEnabled(mV2SigningEnabled) .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved); if (mCreatedBy ! = null) { signerEngineBuilder.setCreatedBy(mCreatedBy); } signerEngine = signerEngineBuilder.build(); }Copy the code

First parse the central directory area, get the AndroidManifest file, get the minSdkVersion(affecting the signature algorithm), and build DefaultApkSignerEngine. By default, V1 and V2 signatures are turned on.

// Step 4. Provide the signer engine with the input APK's APK Signing Block (if any) <! If (inputApkSigningBlock! = null) { signerEngine.inputApkSigningBlock(inputApkSigningBlock); } // Step 5. Iterate over input APK's entries and output the Local File Header + data of those // entries which need to be output. Entries are iterated in the order in which their Local // File Header records are stored in the file. This is  to achieve better data locality in // case Central Directory entries are in the wrong order. List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset = new ArrayList<>(inputCdRecords); Collections.sort( inputCdRecordsSortedByLfhOffset, CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); int lastModifiedDateForNewEntries = -1; int lastModifiedTimeForNewEntries = -1; long inputOffset = 0; long outputOffset = 0; Map<String, CentralDirectoryRecord> outputCdRecordsByName = new HashMap<>(inputCdRecords.size()); . // Step 6. Sort output APK's Central Directory records in the order in which they should // appear in the output List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10); for (CentralDirectoryRecord inputCdRecord : inputCdRecords) { String entryName = inputCdRecord.getName(); CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName); if (outputCdRecord ! = null) { outputCdRecords.add(outputCdRecord); }}Copy the code

The main work of the fifth and sixth steps is: preprocessing apK, including some sorting of the directory, should be in order to deal with the signature more efficiently, after the preprocessing, start the signature process, the first is V1 signature (default exists, unless actively closed) :

// Step 7. Generate and output JAR signatures, if necessary. This may output more Local File // Header + data entries and add to the list of output Central Directory records. ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest = signerEngine.outputJarEntries(); if (outputJarSignatureRequest ! = null) { if (lastModifiedDateForNewEntries == -1) { lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) lastModifiedTimeForNewEntries = 0; } for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : outputJarSignatureRequest.getAdditionalJarEntries()) { String entryName = entry.getName(); byte[] uncompressedData = entry.getData(); ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData)); byte[] compressedData = deflateResult.output; long uncompressedDataCrc32 = deflateResult.inputCrc32; ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = signerEngine.outputJarEntry(entryName); if (inspectEntryRequest ! = null) { inspectEntryRequest.getDataSink().consume( uncompressedData, 0, uncompressedData.length); inspectEntryRequest.done(); } long localFileHeaderOffset = outputOffset; outputOffset += LocalFileRecord.outputRecordWithDeflateCompressedData( entryName, lastModifiedTimeForNewEntries, lastModifiedDateForNewEntries, compressedData, uncompressedDataCrc32, uncompressedData.length, outputApkOut); outputCdRecords.add( CentralDirectoryRecord.createWithDeflateCompressedData( entryName, lastModifiedTimeForNewEntries, lastModifiedDateForNewEntries, uncompressedDataCrc32, compressedData.length, uncompressedData.length, localFileHeaderOffset)); } outputJarSignatureRequest.done(); } // Step 8. Construct output ZIP Central Directory in an in-memory buffer long outputCentralDirSizeBytes = 0; for (CentralDirectoryRecord record : outputCdRecords) { outputCentralDirSizeBytes += record.getSize(); } if (outputCentralDirSizeBytes > Integer.MAX_VALUE) { throw new IOException( "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes + " bytes"); } ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes); for (CentralDirectoryRecord record : outputCdRecords) { record.copyTo(outputCentralDir); } outputCentralDir.flip(); DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir); long outputCentralDirStartOffset = outputOffset; int outputCentralDirRecordCount = outputCdRecords.size(); // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer ByteBuffer outputEocd = EocdRecord.createWithModifiedCentralDirectoryInfo( inputZipSections.getZipEndOfCentralDirectory(), outputCentralDirRecordCount, outputCentralDirDataSource.size(), outputCentralDirStartOffset);Copy the code

Steps 7, 8, and 9 can be regarded as V1 signature processing logic, which is mainly handled in V1SchemeSigner, including creating some signature files in the meta-info folder, updating the central directory, and updating the end of the central directory. The simple process is not complicated and will not be described again.

There is no problem in signing an APK V1 again if it has already signed V1. The principle is that the previous signature file is excluded when it is signed again.

public static boolean isJarEntryDigestNeededInManifest(String entryName) { // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File // Entries which represent directories sould not be listed in the manifest. if (entryName.endsWith("/")) { return false; } // Entries outside of META-INF must be listed in the manifest. if (! entryName.startsWith("META-INF/")) { return true; } // Entries in subdirectories of META-INF must be listed in the manifest. if (entryName.indexOf('/', "META-INF/".length()) ! = -1) { return true; } // Ignored file names (case-insensitive) in META-INF directory: // MANIFEST.MF // *.SF // *.RSA // *.DSA // *.EC // SIG-* String fileNameLowerCase = entryName.substring("META-INF/".length()).toLowerCase(Locale.US); if (("manifest.mf".equals(fileNameLowerCase)) || (fileNameLowerCase.endsWith(".sf")) || (fileNameLowerCase.endsWith(".rsa")) || (fileNameLowerCase.endsWith(".dsa")) || (fileNameLowerCase.endsWith(".ec")) || (fileNameLowerCase.startsWith("sig-"))) { return false; } return true; }Copy the code

As you can see, directories, files in the meta-INF folder, sf, rsa, and other files at the end of the file are not processed by V1 signature, so there is no need to worry about multiple signatures. The next step is to process the V2 signature.

// Step 10. Generate and output APK Signature Scheme v2 signatures, if necessary. This may // insert an APK Signing Block just before the output's ZIP Central Directory ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest = signerEngine.outputZipSections( outputApkIn, outputCentralDirDataSource, DataSources.asDataSource(outputEocd)); if (outputApkSigingBlockRequest ! = null) { byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock(); outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); ZipUtils.setZipEocdCentralDirectoryOffset( outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length); outputApkSigingBlockRequest.done(); } // Step 11. Output ZIP Central Directory and ZIP End of Central Directory outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut); outputApkOut.consume(outputEocd); signerEngine.outputDone(); }Copy the code

V2SchemeSigner processes V2 signatures with clear logic. It blocks and summarizes the APK signed by V1 and collects signatures. V2 signatures do not change any information after V1 signatures. The offset of the central directory changes again:

How do I verify APK signatures

The process of signature verification can be regarded as the reverse of the signature, but the overwrite installation may need to verify that the public key and certificate information is consistent, otherwise the overwrite installation will fail. The entry of signature verification can be found in the PackageManagerService install. Mobile phones 7.0 or older preferentially check V2 signatures. If V2 signatures do not exist, V1 signatures are verified. If your App’s miniSdkVersion<24(N), then your signature mode must include V1 signature:

In addition to checking the integrity of the APK itself, overwrite installation also needs to check whether the certificate is consistent. Only when the certificate is consistent (the same keystore signature) can the upgrade be overwritten. An overwrite installation has several more checks than a clean installation

  • The package name is consistent
  • The certificate is consistent
  • Versioncode cannot be reduced

Only the certificate part is concerned here:

// Verify: if target already has an installer package, it must // be signed with the same cert as the caller. if (targetPackageSetting.installerPackageName ! = null) { PackageSetting setting = mSettings.mPackages.get( targetPackageSetting.installerPackageName); // If the currently set package isn't valid, then it's always // okay to change it. if (setting ! = null) { if (compareSignatures(callerSignature, setting.signatures.mSignatures) ! = PackageManager.SIGNATURE_MATCH) { throw new SecurityException( "Caller does not have same cert as old installer package " + targetPackageSetting.installerPackageName); }}}Copy the code

The entry point of multi-channel packaging of Meituan under V1 and V2 signatures

  • V1 signature: Adding files to the META_INFO folder has no impact on verification, and is the entry point of meituan V1 multi-channel packaging scheme
  • V2 signature: Some supplementary information can be added to the V2 signature block without affecting the signature. This is the entry point for V2 multi-channel packaging.

conclusion

  • V1 signature depends on the signature file in the META_INFO folder
  • V2 signature depends on the central directory before the V2 signature fast, ZIP directory structure will not change, of course, the end offset will be changed.
  • V1 V2 signature can exist at the same time (miniSdkVersion 7.0 is not possible without V1 signature)
  • The multiple go to package pointcut principle: Additional information does not affect signature validation

Author: reading the little snail

Android V1 and V2 signature Principle Analysis

For reference only, welcome correction