diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index dadaef71..561df72e 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -4,7 +4,8 @@ "released_date": "2025/01/02", "improvement": [ "精简打包应用模板 APK 文件大小", - "打包页面支持 Pinyin 库选项" + "打包页面支持 Pinyin 库选项", + "APK 文件类型增加文件大小与签名方案信息" ] }, "v6.6.1": { diff --git a/apksigner/src/main/java/com/android/apksig/ApkSigner.java b/apksigner/src/main/java/com/android/apksig/ApkSigner.java index 4f37aca0..9aba1e0e 100644 --- a/apksigner/src/main/java/com/android/apksig/ApkSigner.java +++ b/apksigner/src/main/java/com/android/apksig/ApkSigner.java @@ -16,12 +16,17 @@ package com.android.apksig; +import static com.android.apksig.Constants.LIBRARY_PAGE_ALIGNMENT_BYTES; import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkSigningBlockNotFoundException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.apk.MinSdkVersionException; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.ByteBufferDataSource; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.EocdRecord; @@ -79,25 +84,30 @@ public class ApkSigner { */ private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; - private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; + private static final short ANDROID_FILE_ALIGNMENT_BYTES = 4096; - /** - * Name of the Android manifest ZIP entry in APKs. - */ + /** Name of the Android manifest ZIP entry in APKs. */ private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; private final List mSignerConfigs; private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; private final boolean mForceSourceStampOverwrite; + private final boolean mSourceStampTimestampEnabled; private final Integer mMinSdkVersion; + private final int mRotationMinSdkVersion; + private final boolean mRotationTargetsDevRelease; private final boolean mV1SigningEnabled; private final boolean mV2SigningEnabled; private final boolean mV3SigningEnabled; private final boolean mV4SigningEnabled; + private final boolean mAlignFileSize; private final boolean mVerityEnabled; private final boolean mV4ErrorReportingEnabled; private final boolean mDebuggableApkPermitted; private final boolean mOtherSignersSignaturesPreserved; + private final boolean mAlignmentPreserved; + private final int mLibraryPageAlignmentBytes; private final String mCreatedBy; private final ApkSignerEngine mSignerEngine; @@ -116,16 +126,23 @@ public class ApkSigner { private ApkSigner( List signerConfigs, SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, boolean forceSourceStampOverwrite, + boolean sourceStampTimestampEnabled, Integer minSdkVersion, + int rotationMinSdkVersion, + boolean rotationTargetsDevRelease, boolean v1SigningEnabled, boolean v2SigningEnabled, boolean v3SigningEnabled, boolean v4SigningEnabled, + boolean alignFileSize, boolean verityEnabled, boolean v4ErrorReportingEnabled, boolean debuggableApkPermitted, boolean otherSignersSignaturesPreserved, + boolean alignmentPreserved, + int libraryPageAlignmentBytes, String createdBy, ApkSignerEngine signerEngine, File inputApkFile, @@ -138,16 +155,23 @@ public class ApkSigner { mSignerConfigs = signerConfigs; mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; mForceSourceStampOverwrite = forceSourceStampOverwrite; + mSourceStampTimestampEnabled = sourceStampTimestampEnabled; mMinSdkVersion = minSdkVersion; + mRotationMinSdkVersion = rotationMinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; mV1SigningEnabled = v1SigningEnabled; mV2SigningEnabled = v2SigningEnabled; mV3SigningEnabled = v3SigningEnabled; mV4SigningEnabled = v4SigningEnabled; + mAlignFileSize = alignFileSize; mVerityEnabled = verityEnabled; mV4ErrorReportingEnabled = v4ErrorReportingEnabled; mDebuggableApkPermitted = debuggableApkPermitted; mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; + mAlignmentPreserved = alignmentPreserved; + mLibraryPageAlignmentBytes = libraryPageAlignmentBytes; mCreatedBy = createdBy; mSignerEngine = signerEngine; @@ -164,6 +188,511 @@ public class ApkSigner { mSigningCertificateLineage = signingCertificateLineage; } + /** + * Signs the input APK and outputs the resulting signed APK. The input APK is not modified. + * + * @throws IOException if an I/O error is encountered while reading or writing the APKs + * @throws ApkFormatException if the input APK is malformed + * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because + * a required cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating or verifying a signature + * @throws IllegalStateException if this signer's configuration is missing required information + * or if the signing engine is in an invalid state. + */ + public void sign() + throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException, IllegalStateException { + Closeable in = null; + DataSource inputApk; + try { + if (mInputApkDataSource != null) { + inputApk = mInputApkDataSource; + } else if (mInputApkFile != null) { + RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r"); + in = inputFile; + inputApk = DataSources.asDataSource(inputFile); + } else { + throw new IllegalStateException("Input APK not specified"); + } + + Closeable out = null; + try { + DataSink outputApkOut; + DataSource outputApkIn; + if (mOutputApkDataSink != null) { + outputApkOut = mOutputApkDataSink; + outputApkIn = mOutputApkDataSource; + } else if (mOutputApkFile != null) { + RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw"); + out = outputFile; + outputFile.setLength(0); + outputApkOut = DataSinks.asDataSink(outputFile); + outputApkIn = DataSources.asDataSource(outputFile); + } else { + throw new IllegalStateException("Output APK not specified"); + } + + sign(inputApk, outputApkOut, outputApkIn); + } finally { + if (out != null) { + out.close(); + } + } + } finally { + if (in != null) { + in.close(); + } + } + } + + 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; + try { + inputZipSections = ApkUtils.findZipSections(inputApk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + long inputApkSigningBlockOffset = -1; + DataSource inputApkSigningBlock = null; + try { + ApkUtils.ApkSigningBlock apkSigningBlockInfo = + ApkUtils.findApkSigningBlock(inputApk, inputZipSections); + inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); + inputApkSigningBlock = apkSigningBlockInfo.getContents(); + } catch (ApkSigningBlockNotFoundException e) { + // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to + // contain this block. It's only needed if the APK is signed using APK Signature Scheme + // v2 and/or v3. + } + DataSource inputApkLfhSection = + inputApk.slice( + 0, + (inputApkSigningBlockOffset != -1) + ? inputApkSigningBlockOffset + : inputZipSections.getZipCentralDirectoryOffset()); + + // Step 2. Parse the input APK's ZIP Central Directory + ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections); + List inputCdRecords = + parseZipCentralDirectory(inputCd, inputZipSections); + + List pinPatterns = + extractPinPatterns(inputCdRecords, inputApkLfhSection); + List pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); + + // Step 3. Obtain a signer engine instance + ApkSignerEngine signerEngine; + if (mSignerEngine != null) { + // Use the provided signer engine + signerEngine = mSignerEngine; + } else { + // Construct a signer engine from the provided parameters + int minSdkVersion; + if (mMinSdkVersion != null) { + // No need to extract minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = mMinSdkVersion; + } else { + // Need to extract minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection); + } + List engineSignerConfigs = + new ArrayList<>(mSignerConfigs.size()); + for (SignerConfig signerConfig : mSignerConfigs) { + DefaultApkSignerEngine.SignerConfig.Builder signerConfigBuilder = + new DefaultApkSignerEngine.SignerConfig.Builder( + signerConfig.getName(), + signerConfig.getKeyConfig(), + signerConfig.getCertificates(), + signerConfig.getDeterministicDsaSigning()); + int signerMinSdkVersion = signerConfig.getMinSdkVersion(); + SigningCertificateLineage signerLineage = + signerConfig.getSigningCertificateLineage(); + if (signerMinSdkVersion > 0) { + signerConfigBuilder.setLineageForMinSdkVersion(signerLineage, + signerMinSdkVersion); + } + engineSignerConfigs.add(signerConfigBuilder.build()); + } + DefaultApkSignerEngine.Builder signerEngineBuilder = + new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion) + .setV1SigningEnabled(mV1SigningEnabled) + .setV2SigningEnabled(mV2SigningEnabled) + .setV3SigningEnabled(mV3SigningEnabled) + .setVerityEnabled(mVerityEnabled) + .setDebuggableApkPermitted(mDebuggableApkPermitted) + .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved) + .setSigningCertificateLineage(mSigningCertificateLineage) + .setMinSdkVersionForRotation(mRotationMinSdkVersion) + .setRotationTargetsDevRelease(mRotationTargetsDevRelease); + if (mCreatedBy != null) { + signerEngineBuilder.setCreatedBy(mCreatedBy); + } + if (mSourceStampSignerConfig != null) { + signerEngineBuilder.setStampSignerConfig( + new DefaultApkSignerEngine.SignerConfig.Builder( + mSourceStampSignerConfig.getName(), + mSourceStampSignerConfig.getKeyConfig(), + mSourceStampSignerConfig.getCertificates(), + mSourceStampSignerConfig.getDeterministicDsaSigning()) + .build()); + signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled); + } + if (mSourceStampSigningCertificateLineage != null) { + signerEngineBuilder.setSourceStampSigningCertificateLineage( + mSourceStampSigningCertificateLineage); + } + signerEngine = signerEngineBuilder.build(); + } + + // 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 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; + byte[] sourceStampCertificateDigest = null; + Map outputCdRecordsByName = + new HashMap<>(inputCdRecords.size()); + for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) { + String entryName = inputCdRecord.getName(); + if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { + continue; // We'll re-add below if needed. + } + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) { + try { + sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); + } catch (ZipFormatException ex) { + throw new ApkFormatException("Bad source stamp entry"); + } + continue; // Existing source stamp is handled below as needed. + } + ApkSignerEngine.InputJarEntryInstructions entryInstructions = + signerEngine.inputJarEntry(entryName); + boolean shouldOutput; + switch (entryInstructions.getOutputPolicy()) { + case OUTPUT: + shouldOutput = true; + break; + case OUTPUT_BY_ENGINE: + case SKIP: + shouldOutput = false; + break; + default: + throw new RuntimeException( + "Unknown output policy: " + entryInstructions.getOutputPolicy()); + } + + long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset(); + if (inputLocalFileHeaderStartOffset > inputOffset) { + // Unprocessed data in input starting at inputOffset and ending and the start of + // this record's LFH. We output this data verbatim because this signer is supposed + // to preserve as much of input as possible. + long chunkSize = inputLocalFileHeaderStartOffset - inputOffset; + inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); + outputOffset += chunkSize; + inputOffset = inputLocalFileHeaderStartOffset; + } + LocalFileRecord inputLocalFileRecord; + try { + inputLocalFileRecord = + LocalFileRecord.getRecord( + inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e); + } + inputOffset += inputLocalFileRecord.getSize(); + + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + entryInstructions.getInspectJarEntryRequest(); + if (inspectEntryRequest != null) { + fulfillInspectInputJarEntryRequest( + inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); + } + + if (shouldOutput) { + // Find the max value of last modified, to be used for new entries added by the + // signer. + int lastModifiedDate = inputCdRecord.getLastModificationDate(); + int lastModifiedTime = inputCdRecord.getLastModificationTime(); + if ((lastModifiedDateForNewEntries == -1) + || (lastModifiedDate > lastModifiedDateForNewEntries) + || ((lastModifiedDate == lastModifiedDateForNewEntries) + && (lastModifiedTime > lastModifiedTimeForNewEntries))) { + lastModifiedDateForNewEntries = lastModifiedDate; + lastModifiedTimeForNewEntries = lastModifiedTime; + } + + inspectEntryRequest = signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + fulfillInspectInputJarEntryRequest( + inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); + } + + // Output entry's Local File Header + data + long outputLocalFileHeaderOffset = outputOffset; + OutputSizeAndDataOffset outputLfrResult = + outputInputJarEntryLfhRecord( + inputApkLfhSection, + inputLocalFileRecord, + outputApkOut, + outputLocalFileHeaderOffset); + outputOffset += outputLfrResult.outputBytes; + long outputDataOffset = + outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes; + + if (pinPatterns != null) { + boolean pinFileHeader = false; + for (Hints.PatternWithRange pinPattern : pinPatterns) { + if (pinPattern.matcher(inputCdRecord.getName()).matches()) { + Hints.ByteRange dataRange = + new Hints.ByteRange(outputDataOffset, outputOffset); + Hints.ByteRange pinRange = + pinPattern.ClampToAbsoluteByteRange(dataRange); + if (pinRange != null) { + pinFileHeader = true; + pinByteRanges.add(pinRange); + } + } + } + if (pinFileHeader) { + pinByteRanges.add( + new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset)); + } + } + + // Enqueue entry's Central Directory record for output + CentralDirectoryRecord outputCdRecord; + if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) { + outputCdRecord = inputCdRecord; + } else { + outputCdRecord = + inputCdRecord.createWithModifiedLocalFileHeaderOffset( + outputLocalFileHeaderOffset); + } + outputCdRecordsByName.put(entryName, outputCdRecord); + } + } + long inputLfhSectionSize = inputApkLfhSection.size(); + if (inputOffset < inputLfhSectionSize) { + // Unprocessed data in input starting at inputOffset and ending and the end of the input + // APK's LFH section. We output this data verbatim because this signer is supposed + // to preserve as much of input as possible. + long chunkSize = inputLfhSectionSize - inputOffset; + inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); + outputOffset += chunkSize; + inputOffset = inputLfhSectionSize; + } + + // Step 6. Sort output APK's Central Directory records in the order in which they should + // appear in the output + List 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); + } + } + + if (lastModifiedDateForNewEntries == -1) { + lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) + lastModifiedTimeForNewEntries = 0; + } + + // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output + // more Local File Header + data entries and add to the list of output Central Directory + // records. + if (signerEngine.isEligibleForSourceStamp()) { + byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest(); + if (mForceSourceStampOverwrite + || sourceStampCertificateDigest == null + || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) { + outputOffset += + outputDataToOutputApk( + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } else { + throw new ApkFormatException( + String.format( + "Cannot generate SourceStamp. APK contains an existing entry with" + + " the name: %s, and it is different than the provided source" + + " stamp certificate", + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME)); + } + } + + // Step 7.5. Generate pinlist.meta file if necessary. + // This has to be before the step 8 so that the file is signed. + if (pinByteRanges != null) { + // Covers JAR signature and zip central dir entry. + // The signature files don't have to be pinned, but pinning them isn't that wasteful + // since the total size is small. + pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); + String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME; + byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges); + + requestOutputEntryInspection(signerEngine, entryName, uncompressedData); + outputOffset += + outputDataToOutputApk( + entryName, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } + + // Step 8. 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) { + for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : + outputJarSignatureRequest.getAdditionalJarEntries()) { + String entryName = entry.getName(); + byte[] uncompressedData = entry.getData(); + + requestOutputEntryInspection(signerEngine, entryName, uncompressedData); + outputOffset += + outputDataToOutputApk( + entryName, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } + outputJarSignatureRequest.done(); + } + + // Step 9. 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 10. Construct output ZIP End of Central Directory record in an in-memory buffer + // because it can be adjusted in Step 11 due to signing block. + // - CD offset (it's shifted by signing block) + // - Comments (when the output file needs to be sized 4k-aligned) + ByteBuffer outputEocd = + EocdRecord.createWithModifiedCentralDirectoryInfo( + inputZipSections.getZipEndOfCentralDirectory(), + outputCentralDirRecordCount, + outputCentralDirDataSource.size(), + outputCentralDirStartOffset); + + // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or + // SourceStamp signatures, if necessary. + // This may insert an APK Signing Block just before the output's ZIP Central Directory + ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest = + signerEngine.outputZipSections2( + outputApkIn, + outputCentralDirDataSource, + DataSources.asDataSource(outputEocd)); + + if (outputApkSigningBlockRequest != null) { + int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock(); + byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkSigningBlockRequest.done(); + + long fileSize = + outputCentralDirStartOffset + + outputCentralDirDataSource.size() + + padding + + outputApkSigningBlock.length + + outputEocd.remaining(); + if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) { + int eocdPadding = + (int) + (ANDROID_FILE_ALIGNMENT_BYTES + - fileSize % ANDROID_FILE_ALIGNMENT_BYTES); + // Replace EOCD with padding one so that output file size can be the multiples of + // alignment. + outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding); + + // Since EoCD has changed, we need to regenerate signing block as well. + outputApkSigningBlockRequest = + signerEngine.outputZipSections2( + outputApkIn, + new ByteBufferDataSource(outputCentralDir), + DataSources.asDataSource(outputEocd)); + outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkSigningBlockRequest.done(); + } + + outputApkOut.consume(ByteBuffer.allocate(padding)); + outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); + ZipUtils.setZipEocdCentralDirectoryOffset( + outputEocd, + outputCentralDirStartOffset + padding + outputApkSigningBlock.length); + } + + // Step 12. Output ZIP Central Directory and ZIP End of Central Directory + outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut); + outputApkOut.consume(outputEocd); + signerEngine.outputDone(); + + // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary. + if (mV4SigningEnabled) { + signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled); + } + } + + private static void requestOutputEntryInspection( + ApkSignerEngine signerEngine, + String entryName, + byte[] uncompressedData) + throws IOException { + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + inspectEntryRequest.getDataSink().consume( + uncompressedData, 0, uncompressedData.length); + inspectEntryRequest.done(); + } + } + private static long outputDataToOutputApk( String entryName, byte[] uncompressedData, @@ -210,14 +739,24 @@ public class ApkSigner { inspectEntryRequest.done(); } - private static OutputSizeAndDataOffset outputInputJarEntryLfhRecordPreservingDataAlignment( + private static class OutputSizeAndDataOffset { + public long outputBytes; + public long dataOffsetBytes; + + public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) { + this.outputBytes = outputBytes; + this.dataOffsetBytes = dataOffsetBytes; + } + } + + private OutputSizeAndDataOffset outputInputJarEntryLfhRecord( DataSource inputLfhSection, LocalFileRecord inputRecord, DataSink outputLfhSection, long outputOffset) throws IOException { long inputOffset = inputRecord.getStartOffsetInArchive(); - if (inputOffset == outputOffset) { + if (inputOffset == outputOffset && mAlignmentPreserved) { // This record's data will be aligned same as in the input APK. return new OutputSizeAndDataOffset( inputRecord.outputRecord(inputLfhSection, outputLfhSection), @@ -225,8 +764,8 @@ public class ApkSigner { } int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord); if ((dataAlignmentMultiple <= 1) - || ((inputOffset % dataAlignmentMultiple) - == (outputOffset % dataAlignmentMultiple))) { + || ((inputOffset % dataAlignmentMultiple) == (outputOffset % dataAlignmentMultiple) + && mAlignmentPreserved)) { // This record's data will be aligned same as in the input APK. return new OutputSizeAndDataOffset( inputRecord.outputRecord(inputLfhSection, outputLfhSection), @@ -234,7 +773,7 @@ public class ApkSigner { } long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord(); - if ((inputDataStartOffset % dataAlignmentMultiple) != 0) { + if ((inputDataStartOffset % dataAlignmentMultiple) != 0 && mAlignmentPreserved) { // This record's data is not aligned in the input APK. No need to align it in the // output. return new OutputSizeAndDataOffset( @@ -259,7 +798,7 @@ public class ApkSigner { dataOffset); } - private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) { + private int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) { if (entry.isDataCompressed()) { // Compressed entries don't need to be aligned return 1; @@ -299,7 +838,7 @@ public class ApkSigner { } // Fall back to filename-based defaults - return (entry.getName().endsWith(".so")) ? ANDROID_COMMON_PAGE_ALIGNMENT_BYTES : 4; + return (entry.getName().endsWith(".so")) ? mLibraryPageAlignmentBytes : 4; } private static ByteBuffer createExtraFieldToAlignData( @@ -483,463 +1022,6 @@ public class ApkSigner { return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest); } - /** - * Signs the input APK and outputs the resulting signed APK. The input APK is not modified. - * - * @throws IOException if an I/O error is encountered while reading or writing the APKs - * @throws ApkFormatException if the input APK is malformed - * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because - * a required cryptographic algorithm implementation is missing - * @throws InvalidKeyException if a signature could not be generated because a signing key is - * not suitable for generating the signature - * @throws SignatureException if an error occurred while generating or verifying a signature - * @throws IllegalStateException if this signer's configuration is missing required information - * or if the signing engine is in an invalid state. - */ - public void sign() - throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException, IllegalStateException { - Closeable in = null; - DataSource inputApk; - try { - if (mInputApkDataSource != null) { - inputApk = mInputApkDataSource; - } else if (mInputApkFile != null) { - RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r"); - in = inputFile; - inputApk = DataSources.asDataSource(inputFile); - } else { - throw new IllegalStateException("Input APK not specified"); - } - - Closeable out = null; - try { - DataSink outputApkOut; - DataSource outputApkIn; - if (mOutputApkDataSink != null) { - outputApkOut = mOutputApkDataSink; - outputApkIn = mOutputApkDataSource; - } else if (mOutputApkFile != null) { - RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw"); - out = outputFile; - outputFile.setLength(0); - outputApkOut = DataSinks.asDataSink(outputFile); - outputApkIn = DataSources.asDataSource(outputFile); - } else { - throw new IllegalStateException("Output APK not specified"); - } - - sign(inputApk, outputApkOut, outputApkIn); - } finally { - if (out != null) { - out.close(); - } - } - } finally { - if (in != null) { - in.close(); - } - } - } - - 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; - try { - inputZipSections = ApkUtils.findZipSections(inputApk); - } catch (ZipFormatException e) { - throw new ApkFormatException("Malformed APK: not a ZIP archive", e); - } - long inputApkSigningBlockOffset = -1; - DataSource inputApkSigningBlock = null; - try { - ApkUtils.ApkSigningBlock apkSigningBlockInfo = - ApkUtils.findApkSigningBlock(inputApk, inputZipSections); - inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); - inputApkSigningBlock = apkSigningBlockInfo.getContents(); - } catch (ApkSigningBlockNotFoundException e) { - // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to - // contain this block. It's only needed if the APK is signed using APK Signature Scheme - // v2 and/or v3. - } - DataSource inputApkLfhSection = - inputApk.slice( - 0, - (inputApkSigningBlockOffset != -1) - ? inputApkSigningBlockOffset - : inputZipSections.getZipCentralDirectoryOffset()); - - // Step 2. Parse the input APK's ZIP Central Directory - ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections); - List inputCdRecords = - parseZipCentralDirectory(inputCd, inputZipSections); - - List pinPatterns = - extractPinPatterns(inputCdRecords, inputApkLfhSection); - List pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); - - // Step 3. Obtain a signer engine instance - ApkSignerEngine signerEngine; - if (mSignerEngine != null) { - // Use the provided signer engine - signerEngine = mSignerEngine; - } else { - // Construct a signer engine from the provided parameters - int minSdkVersion; - if (mMinSdkVersion != null) { - // No need to extract minSdkVersion from the APK's AndroidManifest.xml - minSdkVersion = mMinSdkVersion; - } else { - // Need to extract minSdkVersion from the APK's AndroidManifest.xml - minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection); - } - List engineSignerConfigs = - new ArrayList<>(mSignerConfigs.size()); - for (SignerConfig signerConfig : mSignerConfigs) { - engineSignerConfigs.add( - new DefaultApkSignerEngine.SignerConfig.Builder( - signerConfig.getName(), - signerConfig.getPrivateKey(), - signerConfig.getCertificates()) - .build()); - } - DefaultApkSignerEngine.Builder signerEngineBuilder = - new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion) - .setV1SigningEnabled(mV1SigningEnabled) - .setV2SigningEnabled(mV2SigningEnabled) - .setV3SigningEnabled(mV3SigningEnabled) - .setVerityEnabled(mVerityEnabled) - .setDebuggableApkPermitted(mDebuggableApkPermitted) - .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved) - .setSigningCertificateLineage(mSigningCertificateLineage); - if (mCreatedBy != null) { - signerEngineBuilder.setCreatedBy(mCreatedBy); - } - if (mSourceStampSignerConfig != null) { - signerEngineBuilder.setStampSignerConfig( - new DefaultApkSignerEngine.SignerConfig.Builder( - mSourceStampSignerConfig.getName(), - mSourceStampSignerConfig.getPrivateKey(), - mSourceStampSignerConfig.getCertificates()) - .build()); - } - signerEngine = signerEngineBuilder.build(); - } - - // 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 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; - byte[] sourceStampCertificateDigest = null; - Map outputCdRecordsByName = - new HashMap<>(inputCdRecords.size()); - for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) { - String entryName = inputCdRecord.getName(); - if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { - continue; // We'll re-add below if needed. - } - if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) { - try { - sourceStampCertificateDigest = - LocalFileRecord.getUncompressedData( - inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); - } catch (ZipFormatException ex) { - throw new ApkFormatException("Bad source stamp entry"); - } - continue; // Existing source stamp is handled below as needed. - } - ApkSignerEngine.InputJarEntryInstructions entryInstructions = - signerEngine.inputJarEntry(entryName); - boolean shouldOutput; - switch (entryInstructions.getOutputPolicy()) { - case OUTPUT: - shouldOutput = true; - break; - case OUTPUT_BY_ENGINE: - case SKIP: - shouldOutput = false; - break; - default: - throw new RuntimeException( - "Unknown output policy: " + entryInstructions.getOutputPolicy()); - } - - long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset(); - if (inputLocalFileHeaderStartOffset > inputOffset) { - // Unprocessed data in input starting at inputOffset and ending and the start of - // this record's LFH. We output this data verbatim because this signer is supposed - // to preserve as much of input as possible. - long chunkSize = inputLocalFileHeaderStartOffset - inputOffset; - inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); - outputOffset += chunkSize; - inputOffset = inputLocalFileHeaderStartOffset; - } - LocalFileRecord inputLocalFileRecord; - try { - inputLocalFileRecord = - LocalFileRecord.getRecord( - inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); - } catch (ZipFormatException e) { - throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e); - } - inputOffset += inputLocalFileRecord.getSize(); - - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = - entryInstructions.getInspectJarEntryRequest(); - if (inspectEntryRequest != null) { - fulfillInspectInputJarEntryRequest( - inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); - } - - if (shouldOutput) { - // Find the max value of last modified, to be used for new entries added by the - // signer. - int lastModifiedDate = inputCdRecord.getLastModificationDate(); - int lastModifiedTime = inputCdRecord.getLastModificationTime(); - if ((lastModifiedDateForNewEntries == -1) - || (lastModifiedDate > lastModifiedDateForNewEntries) - || ((lastModifiedDate == lastModifiedDateForNewEntries) - && (lastModifiedTime > lastModifiedTimeForNewEntries))) { - lastModifiedDateForNewEntries = lastModifiedDate; - lastModifiedTimeForNewEntries = lastModifiedTime; - } - - inspectEntryRequest = signerEngine.outputJarEntry(entryName); - if (inspectEntryRequest != null) { - fulfillInspectInputJarEntryRequest( - inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); - } - - // Output entry's Local File Header + data - long outputLocalFileHeaderOffset = outputOffset; - OutputSizeAndDataOffset outputLfrResult = - outputInputJarEntryLfhRecordPreservingDataAlignment( - inputApkLfhSection, - inputLocalFileRecord, - outputApkOut, - outputLocalFileHeaderOffset); - outputOffset += outputLfrResult.outputBytes; - long outputDataOffset = - outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes; - - if (pinPatterns != null) { - boolean pinFileHeader = false; - for (Hints.PatternWithRange pinPattern : pinPatterns) { - if (pinPattern.matcher(inputCdRecord.getName()).matches()) { - Hints.ByteRange dataRange = - new Hints.ByteRange(outputDataOffset, outputOffset); - Hints.ByteRange pinRange = - pinPattern.ClampToAbsoluteByteRange(dataRange); - if (pinRange != null) { - pinFileHeader = true; - pinByteRanges.add(pinRange); - } - } - } - if (pinFileHeader) { - pinByteRanges.add( - new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset)); - } - } - - // Enqueue entry's Central Directory record for output - CentralDirectoryRecord outputCdRecord; - if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) { - outputCdRecord = inputCdRecord; - } else { - outputCdRecord = - inputCdRecord.createWithModifiedLocalFileHeaderOffset( - outputLocalFileHeaderOffset); - } - outputCdRecordsByName.put(entryName, outputCdRecord); - } - } - long inputLfhSectionSize = inputApkLfhSection.size(); - if (inputOffset < inputLfhSectionSize) { - // Unprocessed data in input starting at inputOffset and ending and the end of the input - // APK's LFH section. We output this data verbatim because this signer is supposed - // to preserve as much of input as possible. - long chunkSize = inputLfhSectionSize - inputOffset; - inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); - outputOffset += chunkSize; - inputOffset = inputLfhSectionSize; - } - - // Step 6. Sort output APK's Central Directory records in the order in which they should - // appear in the output - List 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); - } - } - - if (lastModifiedDateForNewEntries == -1) { - lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) - lastModifiedTimeForNewEntries = 0; - } - - // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output - // more Local File Header + data entries and add to the list of output Central Directory - // records. - if (signerEngine.isEligibleForSourceStamp()) { - byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest(); - if (mForceSourceStampOverwrite - || sourceStampCertificateDigest == null - || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) { - outputOffset += - outputDataToOutputApk( - SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, - uncompressedData, - outputOffset, - outputCdRecords, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - outputApkOut); - } else { - throw new ApkFormatException( - String.format( - "Cannot generate SourceStamp. APK contains an existing entry with" - + " the name: %s, and it is different than the provided source" - + " stamp certificate", - SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME)); - } - } - - // Step 8. 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) { - for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : - outputJarSignatureRequest.getAdditionalJarEntries()) { - String entryName = entry.getName(); - byte[] uncompressedData = entry.getData(); - - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = - signerEngine.outputJarEntry(entryName); - if (inspectEntryRequest != null) { - inspectEntryRequest - .getDataSink() - .consume(uncompressedData, 0, uncompressedData.length); - inspectEntryRequest.done(); - } - - outputOffset += - outputDataToOutputApk( - entryName, - uncompressedData, - outputOffset, - outputCdRecords, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - outputApkOut); - } - outputJarSignatureRequest.done(); - } - - if (pinByteRanges != null) { - pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir - String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME; - byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges); - outputOffset += - outputDataToOutputApk( - entryName, - uncompressedData, - outputOffset, - outputCdRecords, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - outputApkOut); - } - - // Step 9. 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 10. Construct output ZIP End of Central Directory record in an in-memory buffer - ByteBuffer outputEocd = - EocdRecord.createWithModifiedCentralDirectoryInfo( - inputZipSections.getZipEndOfCentralDirectory(), - outputCentralDirRecordCount, - outputCentralDirDataSource.size(), - outputCentralDirStartOffset); - - // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or - // SourceStamp signatures, if necessary. - // This may insert an APK Signing Block just before the output's ZIP Central Directory - ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest = - signerEngine.outputZipSections2( - outputApkIn, - outputCentralDirDataSource, - DataSources.asDataSource(outputEocd)); - - if (outputApkSigningBlockRequest != null) { - int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock(); - outputApkOut.consume(ByteBuffer.allocate(padding)); - byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); - outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); - ZipUtils.setZipEocdCentralDirectoryOffset( - outputEocd, - outputCentralDirStartOffset + padding + outputApkSigningBlock.length); - outputApkSigningBlockRequest.done(); - } - - // Step 12. Output ZIP Central Directory and ZIP End of Central Directory - outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut); - outputApkOut.consume(outputEocd); - signerEngine.outputDone(); - - // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary. - if (mV4SigningEnabled) { - signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled); - } - } - - private static class OutputSizeAndDataOffset { - public long outputBytes; - public long dataOffsetBytes; - - public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) { - this.outputBytes = outputBytes; - this.dataOffsetBytes = dataOffsetBytes; - } - } - /** * Configuration of a signer. * @@ -947,28 +1029,40 @@ public class ApkSigner { */ public static class SignerConfig { private final String mName; - private final PrivateKey mPrivateKey; + private final KeyConfig mKeyConfig; private final List mCertificates; + private final boolean mDeterministicDsaSigning; + private final int mMinSdkVersion; + private final SigningCertificateLineage mSigningCertificateLineage; - private SignerConfig( - String name, PrivateKey privateKey, List certificates) { - mName = name; - mPrivateKey = privateKey; - mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + private SignerConfig(Builder builder) { + mName = builder.mName; + mKeyConfig = builder.mKeyConfig; + mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates)); + mDeterministicDsaSigning = builder.mDeterministicDsaSigning; + mMinSdkVersion = builder.mMinSdkVersion; + mSigningCertificateLineage = builder.mSigningCertificateLineage; } - /** - * Returns the name of this signer. - */ + /** Returns the name of this signer. */ public String getName() { return mName; } /** * Returns the signing key of this signer. + * + * @deprecated Use {@link #getKeyConfig()} instead of accessing a {@link PrivateKey} + * directly. If the user of ApkSigner is signing with a KMS instead of JCA, this method + * will return null. */ + @Deprecated public PrivateKey getPrivateKey() { - return mPrivateKey; + return mKeyConfig.match(jca -> jca.privateKey, kms -> null); + } + + public KeyConfig getKeyConfig() { + return mKeyConfig; } /** @@ -980,29 +1074,169 @@ public class ApkSigner { } /** - * Builder of {@link SignerConfig} instances. + * If this signer is a DSA signer, whether or not the signing is done deterministically. */ + public boolean getDeterministicDsaSigning() { + return mDeterministicDsaSigning; + } + + /** Returns the minimum SDK version for which this signer should be used. */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** Returns the {@link SigningCertificateLineage} for this signer. */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + /** Builder of {@link SignerConfig} instances. */ public static class Builder { private final String mName; - private final PrivateKey mPrivateKey; + private final KeyConfig mKeyConfig; private final List mCertificates; + private final boolean mDeterministicDsaSigning; + + private int mMinSdkVersion; + private SigningCertificateLineage mSigningCertificateLineage; /** * Constructs a new {@code Builder}. * - * @param name signer's name. The name is reflected in the name of files comprising the - * JAR signature of the APK. - * @param privateKey signing key + * @deprecated use {@link #Builder(String, KeyConfig, List)} instead + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key * @param certificates list of one or more X.509 certificates. The subject public key of - * the first certificate must correspond to the {@code privateKey}. + * the first certificate must correspond to the {@code privateKey}. */ + @Deprecated public Builder(String name, PrivateKey privateKey, List certificates) { + this(name, privateKey, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @deprecated use {@link #Builder(String, KeyConfig, List, boolean)} instead + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic variant (RFC6979) should be used. + */ + @Deprecated + public Builder( + String name, + PrivateKey privateKey, + List certificates, + boolean deterministicDsaSigning) { if (name.isEmpty()) { throw new IllegalArgumentException("Empty name"); } mName = name; - mPrivateKey = privateKey; + mKeyConfig = new KeyConfig.Jca(privateKey); mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; + } + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param keyConfig signing key configuration + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder(String name, KeyConfig keyConfig, List certificates) { + this(name, keyConfig, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param keyConfig signing key configuration + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic variant (RFC6979) should be used. + */ + public Builder( + String name, + KeyConfig keyConfig, + List certificates, + boolean deterministicDsaSigning) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + mName = name; + mKeyConfig = keyConfig; + mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; + } + + /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */ + public Builder setMinSdkVersion(int minSdkVersion) { + return setLineageForMinSdkVersion(null, minSdkVersion); + } + + /** + * Sets the specified {@code minSdkVersion} as the minimum Android platform version + * (API level) for which the provided {@code lineage} (where applicable) should be used + * to produce the APK's signature. This method is useful if callers want to specify a + * particular rotated signer or lineage with restricted capabilities for later + * platform releases. + * + *

Note:>The V1 and V2 signature schemes do not support key rotation and + * signing lineages with capabilities; only an app's original signer(s) can be used for + * the V1 and V2 signature blocks. Because of this, only a value of {@code + * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was + * introduced can be specified. + * + *

Note:Due to limitations with platform targeting in the V3.0 signature + * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in + * the current {@code SignerConfig} being used in the V3.0 signing block and applied to + * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for + * subsequent {@code SignerConfig} instances). Because of this, only a single {@code + * SignerConfig} can be instantiated with a minimum SDK version <= 32. + * + * @param lineage the {@code SigningCertificateLineage} to target the specified {@code + * minSdkVersion} + * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig} + * should be used + * @return this {@code Builder} instance + * + * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the + * certificate provided in the constructor is not in the specified {@code lineage}. + */ + public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage, + int minSdkVersion) { + if (minSdkVersion < AndroidSdkVersion.P) { + throw new IllegalArgumentException( + "SDK targeted signing config is only supported with the V3 signature " + + "scheme on Android P (SDK version " + + AndroidSdkVersion.P + ") and later"); + } + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + minSdkVersion = AndroidSdkVersion.P; + } + mMinSdkVersion = minSdkVersion; + // If a lineage is provided, ensure the signing certificate for this signer is in + // the lineage; in the case of multiple signing certificates, the first is always + // used in the lineage. + if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) { + throw new IllegalArgumentException( + "The provided lineage does not contain the signing certificate, " + + mCertificates.get(0).getSubjectDN() + + ", for this SignerConfig"); + } + mSigningCertificateLineage = lineage; + return this; } /** @@ -1010,7 +1244,7 @@ public class ApkSigner { * this builder. */ public SignerConfig build() { - return new SignerConfig(mName, mPrivateKey, mCertificates); + return new SignerConfig(this); } } } @@ -1029,19 +1263,28 @@ public class ApkSigner { */ public static class Builder { private final List mSignerConfigs; - private final ApkSignerEngine mSignerEngine; private SignerConfig mSourceStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; private boolean mForceSourceStampOverwrite = false; + private boolean mSourceStampTimestampEnabled = true; private boolean mV1SigningEnabled = true; private boolean mV2SigningEnabled = true; private boolean mV3SigningEnabled = true; private boolean mV4SigningEnabled = true; + private boolean mAlignFileSize = false; private boolean mVerityEnabled = false; private boolean mV4ErrorReportingEnabled = false; private boolean mDebuggableApkPermitted = true; private boolean mOtherSignersSignaturesPreserved; + private boolean mAlignmentPreserved = false; + private int mLibraryPageAlignmentBytes = LIBRARY_PAGE_ALIGNMENT_BYTES; private String mCreatedBy; private Integer mMinSdkVersion; + private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + private boolean mRotationTargetsDevRelease = false; + + private final ApkSignerEngine mSignerEngine; + private File mInputApkFile; private DataSource mInputApkDataSource; @@ -1100,14 +1343,22 @@ public class ApkSigner { mSignerConfigs = null; } - /** - * Sets the signing configuration of the source stamp to be embedded in the APK. - */ + /** Sets the signing configuration of the source stamp to be embedded in the APK. */ public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) { mSourceStampSignerConfig = sourceStampSignerConfig; return this; } + /** + * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of + * signing certificate rotation for certificates previously used to sign source stamps. + */ + public Builder setSourceStampSigningCertificateLineage( + SigningCertificateLineage sourceStampSigningCertificateLineage) { + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + return this; + } + /** * Sets whether the APK should overwrite existing source stamp, if found. * @@ -1118,6 +1369,15 @@ public class ApkSigner { return this; } + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + /** * Sets the APK to be signed. * @@ -1230,7 +1490,7 @@ public class ApkSigner { * with an {@link ApkSignerEngine}. * * @throws IllegalStateException if this builder was initialized with an {@link - * ApkSignerEngine} + * ApkSignerEngine} */ public Builder setMinSdkVersion(int minSdkVersion) { checkInitializedWithoutEngine(); @@ -1238,6 +1498,58 @@ public class ApkSigner { return this; } + /** + * Sets the minimum Android platform version (API Level) for which an APK's rotated signing + * key should be used to produce the APK's signature. The original signing key for the APK + * will be used for all previous platform versions. If a rotated key with signing lineage is + * not provided then this method is a noop. This method is useful for overriding the + * default behavior where Android T is set as the minimum API level for rotation. + * + *

Note:Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result + * in the original V3 signing block being used without platform targeting. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setMinSdkVersionForRotation(int minSdkVersion) { + checkInitializedWithoutEngine(); + // If the provided SDK version does not support v3.1, then use the default SDK version + // with rotation support. + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT; + } else { + mRotationMinSdkVersion = minSdkVersion; + } + return this; + } + + /** + * Sets whether the rotation-min-sdk-version is intended to target a development release; + * this is primarily required after the T SDK is finalized, and an APK needs to target U + * during its development cycle for rotation. + * + *

This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + *

Note:If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + checkInitializedWithoutEngine(); + mRotationTargetsDevRelease = enabled; + return this; + } + /** * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). * @@ -1250,12 +1562,12 @@ public class ApkSigner { * with an {@link ApkSignerEngine}. * * @param enabled {@code true} to require the APK to be signed using JAR signing, {@code - * false} to require the APK to not be signed using JAR signing. + * false} to require the APK to not be signed using JAR signing. * @throws IllegalStateException if this builder was initialized with an {@link - * ApkSignerEngine} + * ApkSignerEngine} * @see JAR - * signing + * href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR + * signing */ public Builder setV1SigningEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1275,11 +1587,11 @@ public class ApkSigner { * with an {@link ApkSignerEngine}. * * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme - * v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2. + * v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2. * @throws IllegalStateException if this builder was initialized with an {@link - * ApkSignerEngine} + * ApkSignerEngine} * @see APK Signature - * Scheme v2 + * Scheme v2 */ public Builder setV2SigningEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1302,9 +1614,9 @@ public class ApkSigner { * may take multiple signers mapping to different targeted platform versions. * * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme - * v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3. + * v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3. * @throws IllegalStateException if this builder was initialized with an {@link - * ApkSignerEngine} + * ApkSignerEngine} */ public Builder setV3SigningEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1323,7 +1635,7 @@ public class ApkSigner { *

V4 signing requires that the APK be v2 or v3 signed. * * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2 - * or v3 and generate an v4 signature file + * or v3 and generate an v4 signature file */ public Builder setV4SigningEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1341,7 +1653,7 @@ public class ApkSigner { * the user did not explicitly request the v4 signing. * * @param enabled {@code false} to prevent errors encountered during the V4 signing from - * halting the signing process + * halting the signing process */ public Builder setV4ErrorReportingEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1349,12 +1661,27 @@ public class ApkSigner { return this; } + /** + * Sets whether the output APK files should be sized as multiples of 4K. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setAlignFileSize(boolean alignFileSize) { + checkInitializedWithoutEngine(); + mAlignFileSize = alignFileSize; + return this; + } + /** * Sets whether to enable the verity signature algorithm for the v2 and v3 signature * schemes. * * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the - * v2 and v3 signature blocks. + * v2 and v3 signature blocks. */ public Builder setVerityEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1390,7 +1717,7 @@ public class ApkSigner { * with an {@link ApkSignerEngine}. * * @throws IllegalStateException if this builder was initialized with an {@link - * ApkSignerEngine} + * ApkSignerEngine} */ public Builder setOtherSignersSignaturesPreserved(boolean preserved) { checkInitializedWithoutEngine(); @@ -1405,7 +1732,7 @@ public class ApkSigner { * with an {@link ApkSignerEngine}. * * @throws IllegalStateException if this builder was initialized with an {@link - * ApkSignerEngine} + * ApkSignerEngine} */ public Builder setCreatedBy(String createdBy) { checkInitializedWithoutEngine(); @@ -1437,6 +1764,26 @@ public class ApkSigner { return this; } + /** + * Sets whether the existing alignment within the APK should be preserved; the + * default for this setting is false. When this value is false, the value provided to + * {@link #setLibraryPageAlignmentBytes(int)} will be used to page align native library + * files and 4 bytes will be used to align all other uncompressed files. + */ + public Builder setAlignmentPreserved(boolean alignmentPreserved) { + mAlignmentPreserved = alignmentPreserved; + return this; + } + + /** + * Sets the number of bytes to be used to page align native library files in the APK; the + * default for this setting is {@link Constants#LIBRARY_PAGE_ALIGNMENT_BYTES}. + */ + public Builder setLibraryPageAlignmentBytes(int libraryPageAlignmentBytes) { + mLibraryPageAlignmentBytes = libraryPageAlignmentBytes; + return this; + } + /** * Returns a new {@code ApkSigner} instance initialized according to the configuration of * this builder. @@ -1473,16 +1820,23 @@ public class ApkSigner { return new ApkSigner( mSignerConfigs, mSourceStampSignerConfig, + mSourceStampSigningCertificateLineage, mForceSourceStampOverwrite, + mSourceStampTimestampEnabled, mMinSdkVersion, + mRotationMinSdkVersion, + mRotationTargetsDevRelease, mV1SigningEnabled, mV2SigningEnabled, mV3SigningEnabled, mV4SigningEnabled, + mAlignFileSize, mVerityEnabled, mV4ErrorReportingEnabled, mDebuggableApkPermitted, mOtherSignersSignaturesPreserved, + mAlignmentPreserved, + mLibraryPageAlignmentBytes, mCreatedBy, mSignerEngine, mInputApkFile, diff --git a/apksigner/src/main/java/com/android/apksig/ApkSignerEngine.java b/apksigner/src/main/java/com/android/apksig/ApkSignerEngine.java index ecd0572e..c79f2327 100644 --- a/apksigner/src/main/java/com/android/apksig/ApkSignerEngine.java +++ b/apksigner/src/main/java/com/android/apksig/ApkSignerEngine.java @@ -35,7 +35,7 @@ import java.util.Set; * generated. * *

Operating Model

- *

+ * * The abstract operating model is that there is an input APK which is being signed, thus producing * an output APK. In reality, there may be just an output APK being built from scratch, or the input * APK and the output APK may be the same file. Because this engine does not deal with reading and @@ -94,7 +94,7 @@ import java.util.Set; * method. * *

Incremental Operation

- *

+ * * The engine supports incremental operation where a signed APK is produced, then modified and * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes * by the developer. Re-signing may be more efficient than signing from scratch. @@ -107,7 +107,7 @@ import java.util.Set; * APK. * *

Output-only Operation

- *

+ * * The engine's abstract operating model consists of an input APK and an output APK. However, it is * possible to use the engine in output-only mode where the engine's {@code input...} methods are * not invoked. In this mode, the engine has less control over output because it cannot request that @@ -129,7 +129,7 @@ public interface ApkSignerEngine extends Closeable { * @param manifestBytes * @param entryNames * @return set of entry names which were processed by the engine during the initialization, a - * subset of entryNames + * subset of entryNames */ default Set initWith(byte[] manifestBytes, Set entryNames) { throw new UnsupportedOperationException("initWith method is not implemented"); @@ -140,9 +140,10 @@ public interface ApkSignerEngine extends Closeable { * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures. * * @param apkSigningBlock APK signing block of the input APK. The provided data source is - * guaranteed to not be used by the engine after this method terminates. - * @throws IOException if an I/O error occurs while reading the APK Signing Block - * @throws ApkFormatException if the APK Signing Block is malformed + * guaranteed to not be used by the engine after this method terminates. + * + * @throws IOException if an I/O error occurs while reading the APK Signing Block + * @throws ApkFormatException if the APK Signing Block is malformed * @throws IllegalStateException if this engine is closed */ void inputApkSigningBlock(DataSource apkSigningBlock) @@ -155,6 +156,7 @@ public interface ApkSignerEngine extends Closeable { * {@link #inputJarEntryRemoved(String)} before invoking this method. * * @return instructions about how to proceed with this entry + * * @throws IllegalStateException if this engine is closed */ InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException; @@ -170,8 +172,9 @@ public interface ApkSignerEngine extends Closeable { * {@link #outputJarEntryRemoved(String)} before invoking this method. * * @return request to inspect the entry or {@code null} if the engine does not need to inspect - * the entry. The request must be fulfilled before {@link #outputJarEntries()} is - * invoked. + * the entry. The request must be fulfilled before {@link #outputJarEntries()} is + * invoked. + * * @throws IllegalStateException if this engine is closed */ InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException; @@ -181,8 +184,9 @@ public interface ApkSignerEngine extends Closeable { * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked. * * @return output policy of this JAR entry. The policy indicates how this input entry affects - * the output APK. The client of this engine should use this information to determine - * how the removal of this input APK's JAR entry affects the output APK. + * the output APK. The client of this engine should use this information to determine + * how the removal of this input APK's JAR entry affects the output APK. + * * @throws IllegalStateException if this engine is closed */ InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) @@ -200,19 +204,20 @@ public interface ApkSignerEngine extends Closeable { * Indicates to this engine that all JAR entries have been output. * * @return request to add JAR signature to the output or {@code null} if there is no need to add - * a JAR signature. The request will contain additional JAR entries to be output. The - * request must be fulfilled before - * {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked. - * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine - * from producing a valid signature. For example, if the engine uses the provided - * {@code META-INF/MANIFEST.MF} as a template and the file is malformed. + * a JAR signature. The request will contain additional JAR entries to be output. The + * request must be fulfilled before + * {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked. + * + * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine + * from producing a valid signature. For example, if the engine uses the provided + * {@code META-INF/MANIFEST.MF} as a template and the file is malformed. * @throws NoSuchAlgorithmException if a signature could not be generated because a required - * cryptographic algorithm implementation is missing - * @throws InvalidKeyException if a signature could not be generated because a signing key is - * not suitable for generating the signature - * @throws SignatureException if an error occurred while generating a signature - * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR - * entries, or if the engine is closed + * cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating a signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries, or if the engine is closed */ OutputJarSignatureRequest outputJarEntries() throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, @@ -224,27 +229,30 @@ public interface ApkSignerEngine extends Closeable { *

The provided data sources are guaranteed to not be used by the engine after this method * terminates. * - * @param zipEntries the section of ZIP archive containing Local File Header records and data of - * the ZIP entries. In a well-formed archive, this section starts at the start of the - * archive and extends all the way to the ZIP Central Directory. - * @param zipCentralDirectory ZIP Central Directory section - * @param zipEocd ZIP End of Central Directory (EoCD) record - * @return request to add an APK Signing Block to the output or {@code null} if the output must - * not contain an APK Signing Block. The request must be fulfilled before - * {@link #outputDone()} is invoked. - * @throws IOException if an I/O error occurs while reading the provided ZIP sections - * @throws ApkFormatException if the provided APK is malformed in a way which prevents this - * engine from producing a valid signature. For example, if the APK Signing Block - * provided to the engine is malformed. - * @throws NoSuchAlgorithmException if a signature could not be generated because a required - * cryptographic algorithm implementation is missing - * @throws InvalidKeyException if a signature could not be generated because a signing key is - * not suitable for generating the signature - * @throws SignatureException if an error occurred while generating a signature - * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR - * entries or to output JAR signature, or if the engine is closed * @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource, * DataSource)}. + * + * @param zipEntries the section of ZIP archive containing Local File Header records and data of + * the ZIP entries. In a well-formed archive, this section starts at the start of the + * archive and extends all the way to the ZIP Central Directory. + * @param zipCentralDirectory ZIP Central Directory section + * @param zipEocd ZIP End of Central Directory (EoCD) record + * + * @return request to add an APK Signing Block to the output or {@code null} if the output must + * not contain an APK Signing Block. The request must be fulfilled before + * {@link #outputDone()} is invoked. + * + * @throws IOException if an I/O error occurs while reading the provided ZIP sections + * @throws ApkFormatException if the provided APK is malformed in a way which prevents this + * engine from producing a valid signature. For example, if the APK Signing Block + * provided to the engine is malformed. + * @throws NoSuchAlgorithmException if a signature could not be generated because a required + * cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating a signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries or to output JAR signature, or if the engine is closed */ @Deprecated OutputApkSigningBlockRequest outputZipSections( @@ -260,25 +268,27 @@ public interface ApkSignerEngine extends Closeable { *

The provided data sources are guaranteed to not be used by the engine after this method * terminates. * - * @param zipEntries the section of ZIP archive containing Local File Header records and data of - * the ZIP entries. In a well-formed archive, this section starts at the start of the - * archive and extends all the way to the ZIP Central Directory. + * @param zipEntries the section of ZIP archive containing Local File Header records and data of + * the ZIP entries. In a well-formed archive, this section starts at the start of the + * archive and extends all the way to the ZIP Central Directory. * @param zipCentralDirectory ZIP Central Directory section - * @param zipEocd ZIP End of Central Directory (EoCD) record + * @param zipEocd ZIP End of Central Directory (EoCD) record + * * @return request to add an APK Signing Block to the output or {@code null} if the output must - * not contain an APK Signing Block. The request must be fulfilled before - * {@link #outputDone()} is invoked. - * @throws IOException if an I/O error occurs while reading the provided ZIP sections - * @throws ApkFormatException if the provided APK is malformed in a way which prevents this - * engine from producing a valid signature. For example, if the APK Signing Block - * provided to the engine is malformed. + * not contain an APK Signing Block. The request must be fulfilled before + * {@link #outputDone()} is invoked. + * + * @throws IOException if an I/O error occurs while reading the provided ZIP sections + * @throws ApkFormatException if the provided APK is malformed in a way which prevents this + * engine from producing a valid signature. For example, if the APK Signing Block + * provided to the engine is malformed. * @throws NoSuchAlgorithmException if a signature could not be generated because a required - * cryptographic algorithm implementation is missing - * @throws InvalidKeyException if a signature could not be generated because a signing key is - * not suitable for generating the signature - * @throws SignatureException if an error occurred while generating a signature - * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR - * entries or to output JAR signature, or if the engine is closed + * cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating a signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries or to output JAR signature, or if the engine is closed */ OutputApkSigningBlockRequest2 outputZipSections2( DataSource zipEntries, @@ -294,22 +304,22 @@ public interface ApkSignerEngine extends Closeable { * output is signed. * * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR - * entries or to output signatures, or if the engine is closed + * entries or to output signatures, or if the engine is closed */ void outputDone() throws IllegalStateException; /** * Generates a V4 signature proto and write to output file. * - * @param data Input data to calculate a verity hash tree and hash root - * @param outputFile To store the serialized V4 Signature. + * @param data Input data to calculate a verity hash tree and hash root + * @param outputFile To store the serialized V4 Signature. * @param ignoreFailures Whether any failures will be silently ignored. - * @throws InvalidKeyException if a signature could not be generated because a signing key is - * not suitable for generating the signature + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature * @throws NoSuchAlgorithmException if a signature could not be generated because a required - * cryptographic algorithm implementation is missing - * @throws SignatureException if an error occurred while generating a signature - * @throws IOException if protobuf fails to be serialized and written to file + * cryptographic algorithm implementation is missing + * @throws SignatureException if an error occurred while generating a signature + * @throws IOException if protobuf fails to be serialized and written to file */ void signV4(DataSource data, File outputFile, boolean ignoreFailures) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException; @@ -322,9 +332,7 @@ public interface ApkSignerEngine extends Closeable { return false; } - /** - * Generates the digest of the certificate used to sign the source stamp. - */ + /** Generates the digest of the certificate used to sign the source stamp. */ default byte[] generateSourceStampCertificateDigest() throws SignatureException { return new byte[0]; } @@ -339,6 +347,70 @@ public interface ApkSignerEngine extends Closeable { @Override void close(); + /** + * Instructions about how to handle an input APK's JAR entry. + * + *

The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and + * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in + * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is + * invoked. + */ + public static class InputJarEntryInstructions { + private final OutputPolicy mOutputPolicy; + private final InspectJarEntryRequest mInspectJarEntryRequest; + + /** + * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry + * output policy and without a request to inspect the entry. + */ + public InputJarEntryInstructions(OutputPolicy outputPolicy) { + this(outputPolicy, null); + } + + /** + * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry + * output mode and with the provided request to inspect the entry. + * + * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no + * need to inspect the entry. + */ + public InputJarEntryInstructions( + OutputPolicy outputPolicy, + InspectJarEntryRequest inspectJarEntryRequest) { + mOutputPolicy = outputPolicy; + mInspectJarEntryRequest = inspectJarEntryRequest; + } + + /** + * Returns the output policy for this entry. + */ + public OutputPolicy getOutputPolicy() { + return mOutputPolicy; + } + + /** + * Returns the request to inspect the JAR entry or {@code null} if there is no need to + * inspect the entry. + */ + public InspectJarEntryRequest getInspectJarEntryRequest() { + return mInspectJarEntryRequest; + } + + /** + * Output policy for an input APK's JAR entry. + */ + public static enum OutputPolicy { + /** Entry must not be output. */ + SKIP, + + /** Entry should be output. */ + OUTPUT, + + /** Entry will be output by the engine. The client can thus ignore this input entry. */ + OUTPUT_BY_ENGINE, + } + } + /** * Request to inspect the specified JAR entry. * @@ -393,7 +465,7 @@ public interface ApkSignerEngine extends Closeable { * Constructs a new {@code JarEntry} with the provided name and data. * * @param data uncompressed data of the entry. Changes to this array will not be - * reflected in {@link #getData()}. + * reflected in {@link #getData()}. */ public JarEntry(String name, byte[] data) { mName = name; @@ -475,74 +547,4 @@ public interface ApkSignerEngine extends Closeable { */ int getPaddingSizeBeforeApkSigningBlock(); } - - /** - * Instructions about how to handle an input APK's JAR entry. - * - *

The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and - * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in - * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is - * invoked. - */ - public static class InputJarEntryInstructions { - private final OutputPolicy mOutputPolicy; - private final InspectJarEntryRequest mInspectJarEntryRequest; - - /** - * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry - * output policy and without a request to inspect the entry. - */ - public InputJarEntryInstructions(OutputPolicy outputPolicy) { - this(outputPolicy, null); - } - - /** - * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry - * output mode and with the provided request to inspect the entry. - * - * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no - * need to inspect the entry. - */ - public InputJarEntryInstructions( - OutputPolicy outputPolicy, - InspectJarEntryRequest inspectJarEntryRequest) { - mOutputPolicy = outputPolicy; - mInspectJarEntryRequest = inspectJarEntryRequest; - } - - /** - * Returns the output policy for this entry. - */ - public OutputPolicy getOutputPolicy() { - return mOutputPolicy; - } - - /** - * Returns the request to inspect the JAR entry or {@code null} if there is no need to - * inspect the entry. - */ - public InspectJarEntryRequest getInspectJarEntryRequest() { - return mInspectJarEntryRequest; - } - - /** - * Output policy for an input APK's JAR entry. - */ - public static enum OutputPolicy { - /** - * Entry must not be output. - */ - SKIP, - - /** - * Entry should be output. - */ - OUTPUT, - - /** - * Entry will be output by the engine. The client can thus ignore this input entry. - */ - OUTPUT_BY_ENGINE, - } - } } diff --git a/apksigner/src/main/java/com/android/apksig/ApkVerificationIssue.java b/apksigner/src/main/java/com/android/apksig/ApkVerificationIssue.java new file mode 100644 index 00000000..0a68588b --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/ApkVerificationIssue.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2020 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; + +/** + * This class is intended as a lightweight representation of an APK signature verification issue + * where the client does not require the additional textual details provided by a subclass. + */ +public class ApkVerificationIssue { + /* The V2 signer(s) could not be read from the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNERS = 1; + /* A V2 signature block exists without any V2 signers */ + public static final int V2_SIG_NO_SIGNERS = 2; + /* Failed to parse a signer's block in the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNER = 3; + /* Failed to parse the signer's signature record in the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNATURE = 4; + /* The V2 signer contained no signatures */ + public static final int V2_SIG_NO_SIGNATURES = 5; + /* The V2 signer's certificate could not be parsed */ + public static final int V2_SIG_MALFORMED_CERTIFICATE = 6; + /* No signing certificates exist for the V2 signer */ + public static final int V2_SIG_NO_CERTIFICATES = 7; + /* Failed to parse the V2 signer's digest record */ + public static final int V2_SIG_MALFORMED_DIGEST = 8; + /* The V3 signer(s) could not be read from the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNERS = 9; + /* A V3 signature block exists without any V3 signers */ + public static final int V3_SIG_NO_SIGNERS = 10; + /* Failed to parse a signer's block in the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNER = 11; + /* Failed to parse the signer's signature record in the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNATURE = 12; + /* The V3 signer contained no signatures */ + public static final int V3_SIG_NO_SIGNATURES = 13; + /* The V3 signer's certificate could not be parsed */ + public static final int V3_SIG_MALFORMED_CERTIFICATE = 14; + /* No signing certificates exist for the V3 signer */ + public static final int V3_SIG_NO_CERTIFICATES = 15; + /* Failed to parse the V3 signer's digest record */ + public static final int V3_SIG_MALFORMED_DIGEST = 16; + /* The source stamp signer contained no signatures */ + public static final int SOURCE_STAMP_NO_SIGNATURE = 17; + /* The source stamp signer's certificate could not be parsed */ + public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18; + /* The source stamp contains a signature produced using an unknown algorithm */ + public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19; + /* Failed to parse the signer's signature in the source stamp signature block */ + public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20; + /* The source stamp's signature block failed verification */ + public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21; + /* An exception was encountered when verifying the source stamp */ + public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22; + /* The certificate digest in the APK does not match the expected digest */ + public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23; + /* + * The APK contains a source stamp signature block without a corresponding stamp certificate + * digest in the APK contents. + */ + public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24; + /* + * The APK does not contain the source stamp certificate digest file nor the source stamp + * signature block. + */ + public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25; + /* + * None of the signatures provided by the source stamp were produced with a known signature + * algorithm. + */ + public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26; + /* + * The source stamp signer's certificate in the signing block does not match the certificate in + * the APK. + */ + public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27; + /* The APK could not be properly parsed due to a ZIP or APK format exception */ + public static final int MALFORMED_APK = 28; + /* An unexpected exception was caught when attempting to verify the APK's signatures */ + public static final int UNEXPECTED_EXCEPTION = 29; + /* The APK contains the certificate digest file but does not contain a stamp signature block */ + public static final int SOURCE_STAMP_SIG_MISSING = 30; + /* Source stamp block contains a malformed attribute. */ + public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31; + /* Source stamp block contains an unknown attribute. */ + public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32; + /** + * Failed to parse the SigningCertificateLineage structure in the source stamp + * attributes section. + */ + public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33; + /** + * The source stamp certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the stamp certificate history. + */ + public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34; + /** + * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record + * with signature(s) that did not verify. + */ + public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35; + /** No V1 / jar signing signature blocks were found in the APK. */ + public static final int JAR_SIG_NO_SIGNATURES = 36; + /** An exception was encountered when parsing the V1 / jar signer in the signature block. */ + public static final int JAR_SIG_PARSE_EXCEPTION = 37; + /** The source stamp timestamp attribute has an invalid value. */ + public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38; + /** + * One or more digests for a signature scheme that is not in the source stamp were provided to + * the source stamp verifier. + */ + public static final int SOURCE_STAMP_SIGNATURE_SCHEME_NOT_AVAILABLE = 39; + + private final int mIssueId; + private final String mFormat; + private final Object[] mParams; + + /** + * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and + * {@code params}. + */ + public ApkVerificationIssue(String format, Object... params) { + mIssueId = -1; + mFormat = format; + mParams = params; + } + + /** + * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code + * params}. + */ + public ApkVerificationIssue(int issueId, Object... params) { + mIssueId = issueId; + mFormat = null; + mParams = params; + } + + /** + * Returns the numeric ID for this issue. + */ + public int getIssueId() { + return mIssueId; + } + + /** + * Returns the optional parameters for this issue. + */ + public Object[] getParams() { + return mParams; + } + + @Override + public String toString() { + // If this instance was created by a subclass with a format string then return the same + // formatted String as the subclass. + if (mFormat != null) { + return String.format(mFormat, mParams); + } + StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId); + for (Object param : mParams) { + result.append(", ").append(param.toString()); + } + return result.toString(); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/ApkVerifier.java b/apksigner/src/main/java/com/android/apksig/ApkVerifier.java index 614869db..a97d1050 100644 --- a/apksigner/src/main/java/com/android/apksig/ApkVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/ApkVerifier.java @@ -18,20 +18,37 @@ package com.android.apksig; import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes; +import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest; +import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; -import static com.android.apksig.internal.apk.v1.V1SchemeSigner.MANIFEST_ENTRY_NAME; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_SOURCE_STAMP; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex; +import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import com.android.apksig.ApkVerifier.Result.V2SchemeSignerInfo; +import com.android.apksig.ApkVerifier.Result.V3SchemeSignerInfo; +import com.android.apksig.SigningCertificateLineage.SignerConfig; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; -import com.android.apksig.internal.apk.AndroidBinXmlParser; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.Result.SignerInfo.ContentDigest; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; import com.android.apksig.internal.apk.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeVerifier; import com.android.apksig.internal.apk.v4.V4SchemeVerifier; import com.android.apksig.internal.util.AndroidSdkVersion; @@ -47,12 +64,15 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; +import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +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.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -72,24 +92,26 @@ import java.util.Set; */ public class ApkVerifier { + private static final Set LINEAGE_RELATED_ISSUES = new HashSet<>(Arrays.asList( + Issue.V3_SIG_MALFORMED_LINEAGE, Issue.V3_INCONSISTENT_LINEAGES, + Issue.V3_SIG_POR_DID_NOT_VERIFY, Issue.V3_SIG_POR_CERT_MISMATCH)); + private static final Map SUPPORTED_APK_SIG_SCHEME_NAMES = loadSupportedApkSigSchemeNames(); - /** - * Android resource ID of the {@code android:targetSandboxVersion} attribute in - * AndroidManifest.xml. - */ - private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; - private static final String TARGET_SANDBOX_VERSION_ELEMENT_NAME = "manifest"; - /** - * Android resource ID of the {@code android:targetSdkVersion} attribute in - * AndroidManifest.xml. - */ - private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; - private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; - private static final String USES_SDK_ELEMENT_NAME = "uses-sdk"; + + private static Map loadSupportedApkSigSchemeNames() { + Map supportedMap = new HashMap<>(2); + supportedMap.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2"); + supportedMap.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3"); + return supportedMap; + } + private final File mApkFile; private final DataSource mApkDataSource; private final File mV4SignatureFile; + private final Integer mMinSdkVersion; private final int mMaxSdkVersion; @@ -106,206 +128,6 @@ public class ApkVerifier { mMaxSdkVersion = maxSdkVersion; } - private static Map loadSupportedApkSigSchemeNames() { - Map supportedMap = new HashMap<>(2); - supportedMap.put( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2"); - supportedMap.put( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3"); - return supportedMap; - } - - private static void checkV4Certificate(List v4Certs, List v2v3Certs, Result result) { - try { - byte[] v4Cert = v4Certs.get(0).getEncoded(); - byte[] cert = v2v3Certs.get(0).getEncoded(); - if (!Arrays.equals(cert, v4Cert)) { - result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH); - } - } catch (CertificateEncodingException e) { - throw new RuntimeException("Failed to encode APK signer cert", e); - } - } - - private static byte[] pickBestDigestForV4(List contentDigests) { - Map apkContentDigests = new HashMap<>(); - collectApkContentDigests(contentDigests, apkContentDigests); - return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests); - } - - private static Map getApkContentDigestsFromSigningSchemeResult( - ApkSigningBlockUtils.Result apkSigningSchemeResult) { - Map apkContentDigests = new HashMap<>(); - for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) { - collectApkContentDigests(signerInfo.contentDigests, apkContentDigests); - } - return apkContentDigests; - } - - private static Map getApkContentDigestFromV1SigningScheme( - List cdRecords, - DataSource apk, - ApkUtils.ZipSections zipSections) - throws IOException, ApkFormatException { - CentralDirectoryRecord manifestCdRecord = null; - Map v1ContentDigest = new HashMap<>(); - for (CentralDirectoryRecord cdRecord : cdRecords) { - if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) { - manifestCdRecord = cdRecord; - break; - } - } - if (manifestCdRecord == null) { - // No JAR signing manifest file found. For SourceStamp verification, returning an empty - // digest is enough since this would affect the final digest signed by the stamp, and - // thus an empty digest will invalidate that signature. - return v1ContentDigest; - } - try { - byte[] manifestBytes = - LocalFileRecord.getUncompressedData( - apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset()); - v1ContentDigest.put( - ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes)); - return v1ContentDigest; - } catch (ZipFormatException e) { - throw new ApkFormatException("Failed to read APK", e); - } - } - - private static void collectApkContentDigests(List contentDigests, Map apkContentDigests) { - for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) { - SignatureAlgorithm signatureAlgorithm = - SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId()); - if (signatureAlgorithm == null) { - continue; - } - ContentDigestAlgorithm contentDigestAlgorithm = - signatureAlgorithm.getContentDigestAlgorithm(); - apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue()); - } - - } - - private static ByteBuffer getAndroidManifestFromApk( - DataSource apk, ApkUtils.ZipSections zipSections) - throws IOException, ApkFormatException { - List cdRecords = - V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); - try { - return ApkSigner.getAndroidManifestFromApk( - cdRecords, - apk.slice(0, zipSections.getZipCentralDirectoryOffset())); - } catch (ZipFormatException e) { - throw new ApkFormatException("Failed to read AndroidManifest.xml", e); - } - } - - /** - * Returns the security sandbox version targeted by an APK with the provided - * {@code AndroidManifest.xml}. - * - * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format - * @throws ApkFormatException if an error occurred while determining the version - */ - private static int getTargetSandboxVersionFromBinaryAndroidManifest( - ByteBuffer androidManifestContents) throws ApkFormatException { - return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, - TARGET_SANDBOX_VERSION_ELEMENT_NAME, TARGET_SANDBOX_VERSION_ATTR_ID); - } - - /** - * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. - * - * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format - * @throws ApkFormatException if an error occurred while determining the version - */ - private static int getTargetSdkVersionFromBinaryAndroidManifest( - ByteBuffer androidManifestContents) { - // If the targetSdkVersion is not specified then the platform will use the value of the - // minSdkVersion; if neither is specified then the platform will use a value of 1. - int minSdkVersion = 1; - try { - return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, - USES_SDK_ELEMENT_NAME, TARGET_SDK_VERSION_ATTR_ID); - } catch (ApkFormatException e) { - // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk - // element is not specified at all. - } - androidManifestContents.rewind(); - try { - minSdkVersion = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, - USES_SDK_ELEMENT_NAME, MIN_SDK_VERSION_ATTR_ID); - } catch (ApkFormatException e) { - // Similar to above, expected if the APK does not contain a minSdkVersion attribute or - // the uses-sdk element is not specified at all. - } - return minSdkVersion; - } - - /** - * Returns the integer value of the requested {@code attributeId} in the specified {@code - * elementName} from the provided {@code androidManifestContents} in binary Android resource - * format. - * - * @throws ApkFormatException if an error occurred while attempting to obtain the attribute - */ - private static int getAttributeValueFromBinaryAndroidManifest( - ByteBuffer androidManifestContents, String elementName, int attributeId) - throws ApkFormatException { - // Return the value of the requested attribute from the specified element. - try { - AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); - int eventType = parser.getEventType(); - while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { - if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) - && (elementName.equals(parser.getName())) - && (parser.getNamespace().isEmpty())) { - int result = 1; - for (int i = 0; i < parser.getAttributeCount(); i++) { - if (parser.getAttributeNameResourceId(i) == attributeId) { - int valueType = parser.getAttributeValueType(i); - switch (valueType) { - case AndroidBinXmlParser.VALUE_TYPE_INT: - result = parser.getAttributeIntValue(i); - break; - default: - throw new ApkFormatException( - "Failed to determine APK's " - + elementName + " attribute" - + ": unsupported value type of" - + " AndroidManifest.xml " - + String.format("0x%08X", attributeId) - + ". Only integer values supported."); - } - break; - } - } - return result; - } - eventType = parser.next(); - } - throw new ApkFormatException( - "Failed to determine APK's " + elementName + " attribute " - + String.format("0x%08X", attributeId) - + " : no " + elementName + " element in AndroidManifest.xml"); - } catch (AndroidBinXmlParser.XmlParserException e) { - throw new ApkFormatException( - "Failed to determine APK's " + elementName + " attribute " - + String.format("0x%08X", attributeId) - + ": malformed AndroidManifest.xml", e); - } - } - - private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) { - if (targetSdkVersion >= AndroidSdkVersion.R) { - return VERSION_APK_SIGNATURE_SCHEME_V2; - } - return VERSION_JAR_SIGNATURE_SCHEME; - } - /** * Verifies the APK's signatures and returns the result of verification. The APK can be * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. @@ -360,17 +182,6 @@ public class ApkVerifier { */ private Result verify(DataSource apk) throws IOException, ApkFormatException, NoSuchAlgorithmException { - if (mMinSdkVersion != null) { - if (mMinSdkVersion < 0) { - throw new IllegalArgumentException( - "minSdkVersion must not be negative: " + mMinSdkVersion); - } - if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { - throw new IllegalArgumentException( - "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion - + ")"); - } - } int maxSdkVersion = mMaxSdkVersion; ApkUtils.ZipSections zipSections; @@ -382,23 +193,7 @@ public class ApkVerifier { ByteBuffer androidManifest = null; - int minSdkVersion; - if (mMinSdkVersion != null) { - // No need to obtain minSdkVersion from the APK's AndroidManifest.xml - minSdkVersion = mMinSdkVersion; - } else { - // Need to obtain minSdkVersion from the APK's AndroidManifest.xml - if (androidManifest == null) { - androidManifest = getAndroidManifestFromApk(apk, zipSections); - } - minSdkVersion = - ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); - if (minSdkVersion > mMaxSdkVersion) { - throw new IllegalArgumentException( - "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" - + mMaxSdkVersion + ")"); - } - } + int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); Result result = new Result(); Map> signatureSchemeApkContentDigests = @@ -413,40 +208,68 @@ public class ApkVerifier { // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2 // verification is performed it would see the stripping protection attribute, see that V3 // is in the list of supported signatures, and report a stripped signature. - Map supportedSchemeNames; - if (maxSdkVersion >= AndroidSdkVersion.P) { - supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; - } else if (maxSdkVersion >= AndroidSdkVersion.N) { - supportedSchemeNames = new HashMap<>(1); - supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, - SUPPORTED_APK_SIG_SCHEME_NAMES.get( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); - } else { - supportedSchemeNames = Collections.emptyMap(); - } + Map supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion); + // Android N and newer attempts to verify APKs using the APK Signing Block, which can // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature // verification. If the signature is found but does not verify, the APK is rejected. Set foundApkSigSchemeIds = new HashSet<>(2); if (maxSdkVersion >= AndroidSdkVersion.N) { RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; - // Android P and newer attempts to verify APKs using APK Signature Scheme v3 + // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0 + // also includes stripping protection for the minimum SDK version on which the rotated + // signing key should be used. + int rotationMinSdkVersion = 0; + if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) { + try { + ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk, + zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT), + maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) + .build() + .verify(); + foundApkSigSchemeIds.add(VERSION_APK_SIGNATURE_SCHEME_V31); + rotationMinSdkVersion = v31Result.signers.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(0); + result.mergeFrom(v31Result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V31, + getApkContentDigestsFromSigningSchemeResult(v31Result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v3.1 signature not required + } + if (result.containsErrors()) { + return result; + } + } + // Android P and newer attempts to verify APKs using APK Signature Scheme v3; since a + // V3.1 block should only be written with a V3.0 block, always perform the V3.0 check + // if the minSdkVersion supports V3.0. if (maxSdkVersion >= AndroidSdkVersion.P) { try { - ApkSigningBlockUtils.Result v3Result = - V3SchemeVerifier.verify( - executor, - apk, - zipSections, - Math.max(minSdkVersion, AndroidSdkVersion.P), - maxSdkVersion); + V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk, + zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P), + maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + if (rotationMinSdkVersion > 0) { + builder.setRotationMinSdkVersion(rotationMinSdkVersion); + } + ApkSigningBlockUtils.Result v3Result = builder.build().verify(); foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); result.mergeFrom(v3Result); signatureSchemeApkContentDigests.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, getApkContentDigestsFromSigningSchemeResult(v3Result)); } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { - // v3 signature not required + // v3 signature not required unless a v3.1 signature was found as a v3.1 + // signature is intended to support key rotation on T+ with the v3 signature + // containing the original signing key. + if (foundApkSigSchemeIds.contains( + VERSION_APK_SIGNATURE_SCHEME_V31)) { + result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK); + } } if (result.containsErrors()) { return result; @@ -554,7 +377,7 @@ public class ApkVerifier { apk, sourceStampCdRecord, zipSections.getZipCentralDirectoryOffset()); - ApkSigningBlockUtils.Result sourceStampResult = + ApkSigResult sourceStampResult = V2SourceStampVerifier.verify( apk, zipSections, @@ -564,7 +387,7 @@ public class ApkVerifier { maxSdkVersion); result.mergeFrom(sourceStampResult); } - } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + } catch (SignatureNotFoundException ignored) { result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING); } catch (ZipFormatException e) { throw new ApkFormatException("Failed to read APK", e); @@ -683,33 +506,46 @@ public class ApkVerifier { // The apkDigest field in the v4 signature should match the selected v2/v3. if (result.isVerifiedUsingV4Scheme()) { List v4Signers = result.getV4SchemeSigners(); - if (v4Signers.size() != 1) { - result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); - } List digestsFromV4 = v4Signers.get(0).getContentDigests(); if (digestsFromV4.size() != 1) { - result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); + result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV4.size()); + if (digestsFromV4.isEmpty()) { + return result; + } } final byte[] digestFromV4 = digestsFromV4.get(0).getValue(); if (result.isVerifiedUsingV3Scheme()) { - List v3Signers = result.getV3SchemeSigners(); - if (v3Signers.size() != 1) { + final boolean isV31 = result.isVerifiedUsingV31Scheme(); + final int expectedSize = isV31 ? 2 : 1; + if (v4Signers.size() != expectedSize) { + result.addError(isV31 ? Issue.V41_SIG_NEEDS_TWO_SIGNERS + : Issue.V4_SIG_MULTIPLE_SIGNERS); + return result; + } + + checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4, + result); + if (isV31) { + List digestsFromV41 = + v4Signers.get(1).getContentDigests(); + if (digestsFromV41.size() != 1) { + result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV41.size()); + if (digestsFromV41.isEmpty()) { + return result; + } + } + final byte[] digestFromV41 = digestsFromV41.get(0).getValue(); + checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts, + digestFromV41, result); + } + } else if (result.isVerifiedUsingV2Scheme()) { + if (v4Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); } - // Compare certificates. - checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result); - - // Compare digests. - final byte[] digestFromV3 = pickBestDigestForV4( - v3Signers.get(0).getContentDigests()); - if (!Arrays.equals(digestFromV4, digestFromV3)) { - result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); - } - } else if (result.isVerifiedUsingV2Scheme()) { List v2Signers = result.getV2SchemeSigners(); if (v2Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); @@ -722,7 +558,8 @@ public class ApkVerifier { final byte[] digestFromV2 = pickBestDigestForV4( v2Signers.get(0).getContentDigests()); if (!Arrays.equals(digestFromV4, digestFromV2)) { - result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); + result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 2, toHex(digestFromV2), + toHex(digestFromV4)); } } else { throw new RuntimeException("V4 signature must be also verified with V2/V3"); @@ -731,29 +568,38 @@ public class ApkVerifier { // If the targetSdkVersion has a minimum required signature scheme version then verify // that the APK was signed with at least that version. - if (androidManifest == null) { - androidManifest = getAndroidManifestFromApk(apk, zipSections); + try { + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + } catch (ApkFormatException e) { + // If the manifest is not available then skip the minimum signature scheme requirement + // to support bundle verification. } - int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest( - androidManifest.slice()); - int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion); - // The platform currently only enforces a single minimum signature scheme version, but when - // later platform versions support another minimum version this will need to be expanded to - // verify the minimum based on the target and maximum SDK version. - if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME && maxSdkVersion >= targetSdkVersion) { - switch (minSchemeVersion) { - case VERSION_APK_SIGNATURE_SCHEME_V2: - if (result.isVerifiedUsingV2Scheme()) { - break; - } - // Allow this case to fall through to the next as a signature satisfying a later - // scheme version will also satisfy this requirement. - case VERSION_APK_SIGNATURE_SCHEME_V3: - if (result.isVerifiedUsingV3Scheme()) { - break; - } - result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, targetSdkVersion, - minSchemeVersion); + if (androidManifest != null) { + int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest( + androidManifest.slice()); + int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion); + // The platform currently only enforces a single minimum signature scheme version, but + // when later platform versions support another minimum version this will need to be + // expanded to verify the minimum based on the target and maximum SDK version. + if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME + && maxSdkVersion >= targetSdkVersion) { + switch (minSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + if (result.isVerifiedUsingV2Scheme()) { + break; + } + // Allow this case to fall through to the next as a signature satisfying a + // later scheme version will also satisfy this requirement. + case VERSION_APK_SIGNATURE_SCHEME_V3: + if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) { + break; + } + result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, + targetSdkVersion, + minSchemeVersion); + } } } @@ -763,7 +609,10 @@ public class ApkVerifier { // Verified result.setVerified(); - if (result.isVerifiedUsingV3Scheme()) { + if (result.isVerifiedUsingV31Scheme()) { + List v31Signers = result.getV31SchemeSigners(); + result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate()); + } else if (result.isVerifiedUsingV3Scheme()) { List v3Signers = result.getV3SchemeSigners(); result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate()); } else if (result.isVerifiedUsingV2Scheme()) { @@ -782,6 +631,1487 @@ public class ApkVerifier { return result; } + /** + * Verifies and returns the minimum SDK version, either as provided to the builder or as read + * from the {@code apk}'s AndroidManifest.xml. + */ + private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections) + throws ApkFormatException, IOException { + if (mMinSdkVersion != null) { + if (mMinSdkVersion < 0) { + throw new IllegalArgumentException( + "minSdkVersion must not be negative: " + mMinSdkVersion); + } + if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { + throw new IllegalArgumentException( + "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion + + ")"); + } + return mMinSdkVersion; + } + + ByteBuffer androidManifest = null; + // Need to obtain minSdkVersion from the APK's AndroidManifest.xml + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + int minSdkVersion = + ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); + if (minSdkVersion > mMaxSdkVersion) { + throw new IllegalArgumentException( + "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" + + mMaxSdkVersion + ")"); + } + return minSdkVersion; + } + + /** + * Returns the mapping of signature scheme version to signature scheme name for all signature + * schemes starting from V2 supported by the {@code maxSdkVersion}. + */ + private static Map getSupportedSchemeNames(int maxSdkVersion) { + Map supportedSchemeNames; + if (maxSdkVersion >= AndroidSdkVersion.P) { + supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; + } else if (maxSdkVersion >= AndroidSdkVersion.N) { + supportedSchemeNames = new HashMap<>(1); + supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, + SUPPORTED_APK_SIG_SCHEME_NAMES.get( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); + } else { + supportedSchemeNames = Collections.emptyMap(); + } + return supportedSchemeNames; + } + + /** + * Verifies the APK's source stamp signature and returns the result of the verification. + * + *

The APK's source stamp can be considered verified if the result's {@link + * Result#isVerified} returns {@code true}. The details of the source stamp verification can + * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or + * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the + * verification fails additional details regarding the failure can be obtained from {@link + * Result#getAllErrors()}}. + */ + public Result verifySourceStamp() { + return verifySourceStamp(null); + } + + /** + * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of + * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result + * of the verification. + * + *

A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, + * if present, without verifying the actual source stamp certificate used to sign the source + * stamp. This can be used to verify an APK contains a properly signed source stamp without + * verifying a particular signer. + * + * @see #verifySourceStamp() + */ + public Result verifySourceStamp(String expectedCertDigest) { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verifySourceStamp(apk, expectedCertDigest); + } catch (IOException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.UNEXPECTED_EXCEPTION, e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Compares the digests coming from signature blocks. Returns {@code true} if at least one + * digest algorithm is present in both digests and actual digests for all common algorithms + * are the same. + */ + public static boolean compareDigests( + Map firstDigests, + Map secondDigests) throws NoSuchAlgorithmException { + + Set intersectKeys = new HashSet<>(firstDigests.keySet()); + intersectKeys.retainAll(secondDigests.keySet()); + if (intersectKeys.isEmpty()) { + return false; + } + + for (ContentDigestAlgorithm algorithm : intersectKeys) { + if (!Arrays.equals(firstDigests.get(algorithm), + secondDigests.get(algorithm))) { + return false; + } + } + return true; + } + + + /** + * Verifies the provided {@code apk}'s source stamp signature, including verification of the + * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and + * returns the result of the verification. + * + * @see #verifySourceStamp(String) + */ + private Result verifySourceStamp(DataSource apk, String expectedCertDigest) { + try { + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); + + // Attempt to obtain the source stamp's certificate digest from the APK. + List cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + + // If the source stamp's certificate digest is not available within the APK then the + // source stamp cannot be verified; check if a source stamp signing block is in the + // APK's signature block to determine the appropriate status to return. + if (sourceStampCdRecord == null) { + boolean stampSigningBlockFound; + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + VERSION_SOURCE_STAMP); + ApkSigningBlockUtils.findSignature(apk, zipSections, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result); + stampSigningBlockFound = true; + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + stampSigningBlockFound = false; + } + if (stampSigningBlockFound) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, + Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); + } else { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING, + Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + } + } + + // Verify that the contents of the source stamp certificate digest match the expected + // value, if provided. + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + if (expectedCertDigest != null) { + String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest); + if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus + .CERT_DIGEST_MISMATCH, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest, + expectedCertDigest); + } + } + + Map> signatureSchemeApkContentDigests = + new HashMap<>(); + Map supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion); + Set foundApkSigSchemeIds = new HashSet<>(2); + + Result result = new Result(); + ApkSigningBlockUtils.Result v3Result = null; + if (mMaxSdkVersion >= AndroidSdkVersion.P) { + v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, + supportedSchemeNames, signatureSchemeApkContentDigests, + VERSION_APK_SIGNATURE_SCHEME_V3, + Math.max(minSdkVersion, AndroidSdkVersion.P)); + if (v3Result != null && v3Result.containsErrors()) { + result.mergeFrom(v3Result); + return mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + result); + } + } + + ApkSigningBlockUtils.Result v2Result = null; + if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P + || foundApkSigSchemeIds.isEmpty())) { + v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, + supportedSchemeNames, signatureSchemeApkContentDigests, + VERSION_APK_SIGNATURE_SCHEME_V2, + Math.max(minSdkVersion, AndroidSdkVersion.N)); + if (v2Result != null && v2Result.containsErrors()) { + result.mergeFrom(v2Result); + return mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + result); + } + } + + if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) { + signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); + } + + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + minSdkVersion, + mMaxSdkVersion); + result.mergeFrom(sourceStampResult); + // Since the caller is only seeking to verify the source stamp the Result can be marked + // as verified if the source stamp verification was successful. + if (sourceStampResult.verified) { + result.setVerified(); + } else { + // To prevent APK signature verification with a failed / missing source stamp the + // source stamp verification will only log warnings; to allow the caller to capture + // the failure reason treat all warnings as errors. + result.setWarningsAsErrors(true); + } + return result; + } catch (ApkFormatException | IOException | ZipFormatException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.MALFORMED_APK, e); + } catch (NoSuchAlgorithmException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.UNEXPECTED_EXCEPTION, e); + } catch (SignatureNotFoundException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, + Issue.SOURCE_STAMP_SIG_MISSING); + } + } + + /** + * Creates and returns a {@code Result} that can be returned for source stamp verification + * with the provided source stamp {@code verificationStatus}, and logs an error for the + * specified {@code issue} and {@code params}. + */ + private static Result createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue, + Object... params) { + Result result = new Result(); + result.addError(issue, params); + return mergeSourceStampResult(verificationStatus, result); + } + + /** + * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the + * source stamp status to the provided {@code verificationStatus}. + */ + private static Result mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, + Result result) { + result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus); + return result; + } + + /** + * Gets content digests, signing lineage and certificates from the given {@code schemeId} block + * alongside encountered errors info and creates a new {@code Result} containing all this + * information. + */ + public static Result getSigningBlockResult( + DataSource apk, ApkUtils.ZipSections zipSections, int sdkVersion, int schemeId) + throws IOException, NoSuchAlgorithmException{ + Map> sigSchemeApkContentDigests = + new HashMap<>(); + Map supportedSchemeNames = getSupportedSchemeNames(sdkVersion); + Set foundApkSigSchemeIds = new HashSet<>(2); + + Result result = new Result(); + result.mergeFrom(getApkContentDigests(apk, zipSections, + foundApkSigSchemeIds, supportedSchemeNames, sigSchemeApkContentDigests, + schemeId, sdkVersion, sdkVersion)); + return result; + } + + /** + * Gets the content digest from the {@code result}'s signers. Ignores {@code ContentDigest}s + * for which {@code SignatureAlgorithm} is {@code null}. + */ + public static Map getContentDigestsFromResult( + Result result, int schemeId) { + Map apkContentDigests = new HashMap<>(); + if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V2 + || schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 + || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)) { + return apkContentDigests; + } + switch (schemeId) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + for (V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { + getContentDigests(signerInfo.getContentDigests(), apkContentDigests); + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + for (Result.V3SchemeSignerInfo signerInfo : result.getV3SchemeSigners()) { + getContentDigests(signerInfo.getContentDigests(), apkContentDigests); + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V31: + for (Result.V3SchemeSignerInfo signerInfo : result.getV31SchemeSigners()) { + getContentDigests(signerInfo.getContentDigests(), apkContentDigests); + } + break; + } + return apkContentDigests; + } + + private static void getContentDigests( + List digests, Map contentDigestsMap) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : + digests) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById( + contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + contentDigestsMap.put(signatureAlgorithm.getContentDigestAlgorithm(), + contentDigest.getValue()); + } + } + + /** + * Checks whether a given {@code result} contains errors indicating that a signing certificate + * lineage is incorrect. + */ + public static boolean containsLineageErrors( + Result result) { + if (!result.containsErrors()) { + return false; + } + + return (result.getAllErrors().stream().map(i -> i.getIssue()) + .anyMatch(error -> LINEAGE_RELATED_ISSUES.contains(error))); + } + + + /** + * Gets a lineage from the first signer from a given {@code result}. + * If the {@code result} contains errors related to the lineage incorrectness or there are no + * signers or certificates, it returns {@code null}. + * If the lineage is empty but there is a signer, it returns a 1-element lineage containing + * the signing key. + */ + public static SigningCertificateLineage getLineageFromResult( + Result result, int sdkVersion, int schemeId) + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 + || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31) + || containsLineageErrors(result)) { + return null; + } + List signersInfo = + schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 ? + result.getV3SchemeSigners() : result.getV31SchemeSigners(); + if (signersInfo.isEmpty()) { + return null; + } + V3SchemeSignerInfo firstSignerInfo = signersInfo.get(0); + SigningCertificateLineage lineage = firstSignerInfo.mSigningCertificateLineage; + if (lineage == null && firstSignerInfo.getCertificate() != null) { + try { + lineage = + new SigningCertificateLineage.Builder( + new SignerConfig.Builder( + /* keyConfig= */ (KeyConfig) null, + firstSignerInfo.getCertificate()) + .build()) + .build(); + } catch (Exception e) { + return null; + } + } + return lineage; + } + + /** + * Obtains the APK content digest(s) and adds them to the provided {@code + * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be + * merged with a {@code Result} to notify the client of any errors. + * + *

Note, this method currently only supports signature scheme V2 and V3; to obtain the + * content digests for V1 signatures use {@link + * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a + * signature scheme version other than V2 or V3 is provided a {@code null} value will be + * returned. + */ + private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk, + ApkUtils.ZipSections zipSections, Set foundApkSigSchemeIds, + Map supportedSchemeNames, + Map> sigSchemeApkContentDigests, + int apkSigSchemeVersion, int minSdkVersion) + throws IOException, NoSuchAlgorithmException { + return getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames, + sigSchemeApkContentDigests, apkSigSchemeVersion, minSdkVersion, mMaxSdkVersion); + } + + + /** + * Obtains the APK content digest(s) and adds them to the provided {@code + * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be + * merged with a {@code Result} to notify the client of any errors. + * + *

Note, this method currently only supports signature scheme V2 and V3; to obtain the + * content digests for V1 signatures use {@link + * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a + * signature scheme version other than V2 or V3 is provided a {@code null} value will be + * returned. + */ + private static ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk, + ApkUtils.ZipSections zipSections, Set foundApkSigSchemeIds, + Map supportedSchemeNames, + Map> sigSchemeApkContentDigests, + int apkSigSchemeVersion, int minSdkVersion, int maxSdkVersion) + throws IOException, NoSuchAlgorithmException { + if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2 + || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3 + || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V31)) { + return null; + } + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion); + SignatureInfo signatureInfo; + try { + int sigSchemeBlockId; + switch (apkSigSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V31: + sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID; + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + break; + default: + sigSchemeBlockId = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + } + signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, + sigSchemeBlockId, result); + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + return null; + } + foundApkSigSchemeIds.add(apkSigSchemeVersion); + + Set contentDigestsToVerify = new HashSet<>(1); + if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { + V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock, + contentDigestsToVerify, supportedSchemeNames, + foundApkSigSchemeIds, minSdkVersion, maxSdkVersion, result); + } else { + V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock, + contentDigestsToVerify, result); + } + Map apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : + signerInfo.contentDigests) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById( + contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), + contentDigest.getValue()); + } + } + sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests); + return result; + } + + private static void checkV4Signer(List v3Signers, + List v4Certs, byte[] digestFromV4, Result result) { + if (v3Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + + // Compare certificates. + checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result); + + // Compare digests. + final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests()); + if (!Arrays.equals(digestFromV4, digestFromV3)) { + result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 3, toHex(digestFromV3), + toHex(digestFromV4)); + } + } + + private static void checkV4Certificate(List v4Certs, + List v2v3Certs, Result result) { + try { + byte[] v4Cert = v4Certs.get(0).getEncoded(); + byte[] cert = v2v3Certs.get(0).getEncoded(); + if (!Arrays.equals(cert, v4Cert)) { + result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH); + } + } catch (CertificateEncodingException e) { + throw new RuntimeException("Failed to encode APK signer cert", e); + } + } + + private static byte[] pickBestDigestForV4( + List contentDigests) { + Map apkContentDigests = new HashMap<>(); + collectApkContentDigests(contentDigests, apkContentDigests); + return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests); + } + + private static Map getApkContentDigestsFromSigningSchemeResult( + ApkSigningBlockUtils.Result apkSigningSchemeResult) { + Map apkContentDigests = new HashMap<>(); + for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) { + collectApkContentDigests(signerInfo.contentDigests, apkContentDigests); + } + return apkContentDigests; + } + + private static Map getApkContentDigestFromV1SigningScheme( + List cdRecords, + DataSource apk, + ApkUtils.ZipSections zipSections) + throws IOException, ApkFormatException { + CentralDirectoryRecord manifestCdRecord = null; + Map v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) { + manifestCdRecord = cdRecord; + break; + } + } + if (manifestCdRecord == null) { + // No JAR signing manifest file found. For SourceStamp verification, returning an empty + // digest is enough since this would affect the final digest signed by the stamp, and + // thus an empty digest will invalidate that signature. + return v1ContentDigest; + } + try { + byte[] manifestBytes = + LocalFileRecord.getUncompressedData( + apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset()); + v1ContentDigest.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes)); + return v1ContentDigest; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + + private static void collectApkContentDigests( + List contentDigests, + Map apkContentDigests) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) { + SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue()); + } + + } + + private static ByteBuffer getAndroidManifestFromApk( + DataSource apk, ApkUtils.ZipSections zipSections) + throws IOException, ApkFormatException { + List cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + try { + return ApkSigner.getAndroidManifestFromApk( + cdRecords, + apk.slice(0, zipSections.getZipCentralDirectoryOffset())); + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read AndroidManifest.xml", e); + } + } + + private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) { + if (targetSdkVersion >= AndroidSdkVersion.R) { + return VERSION_APK_SIGNATURE_SCHEME_V2; + } + return VERSION_JAR_SIGNATURE_SCHEME; + } + + /** + * Result of verifying an APKs signatures. The APK can be considered verified iff + * {@link #isVerified()} returns {@code true}. + */ + public static class Result { + private final List mErrors = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mSignerCerts = new ArrayList<>(); + private final List mV1SchemeSigners = new ArrayList<>(); + private final List mV1SchemeIgnoredSigners = new ArrayList<>(); + private final List mV2SchemeSigners = new ArrayList<>(); + private final List mV3SchemeSigners = new ArrayList<>(); + private final List mV31SchemeSigners = new ArrayList<>(); + private final List mV4SchemeSigners = new ArrayList<>(); + private SourceStampInfo mSourceStampInfo; + + private boolean mVerified; + private boolean mVerifiedUsingV1Scheme; + private boolean mVerifiedUsingV2Scheme; + private boolean mVerifiedUsingV3Scheme; + private boolean mVerifiedUsingV31Scheme; + private boolean mVerifiedUsingV4Scheme; + private boolean mSourceStampVerified; + private boolean mWarningsAsErrors; + private SigningCertificateLineage mSigningCertificateLineage; + + /** + * Returns {@code true} if the APK's signatures verified. + */ + public boolean isVerified() { + return mVerified; + } + + private void setVerified() { + mVerified = true; + } + + /** + * Returns {@code true} if the APK's JAR signatures verified. + */ + public boolean isVerifiedUsingV1Scheme() { + return mVerifiedUsingV1Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. + */ + public boolean isVerifiedUsingV2Scheme() { + return mVerifiedUsingV2Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified. + */ + public boolean isVerifiedUsingV3Scheme() { + return mVerifiedUsingV3Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified. + */ + public boolean isVerifiedUsingV31Scheme() { + return mVerifiedUsingV31Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified. + */ + public boolean isVerifiedUsingV4Scheme() { + return mVerifiedUsingV4Scheme; + } + + /** + * Returns {@code true} if the APK's SourceStamp signature verified. + */ + public boolean isSourceStampVerified() { + return mSourceStampVerified; + } + + /** + * Returns the verified signers' certificates, one per signer. + */ + public List getSignerCertificates() { + return mSignerCerts; + } + + private void addSignerCertificate(X509Certificate cert) { + mSignerCerts.add(cert); + } + + /** + * Returns information about JAR signers associated with the APK's signature. These are the + * signers used by Android. + * + * @see #getV1SchemeIgnoredSigners() + */ + public List getV1SchemeSigners() { + return mV1SchemeSigners; + } + + /** + * Returns information about JAR signers ignored by the APK's signature verification + * process. These signers are ignored by Android. However, each signer's errors or warnings + * will contain information about why they are ignored. + * + * @see #getV1SchemeSigners() + */ + public List getV1SchemeIgnoredSigners() { + return mV1SchemeIgnoredSigners; + } + + /** + * Returns information about APK Signature Scheme v2 signers associated with the APK's + * signature. + */ + public List getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v3 signers associated with the APK's + * signature. + * + * Multiple signers represent different targeted platform versions, not + * a signing identity of multiple signers. APK Signature Scheme v3 only supports single + * signer identities. + */ + public List getV3SchemeSigners() { + return mV3SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v3.1 signers associated with the APK's + * signature. + * + * Multiple signers represent different targeted platform versions, not + * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single + * signer identities. + */ + public List getV31SchemeSigners() { + return mV31SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v4 signers associated with the APK's + * signature. + */ + public List getV4SchemeSigners() { + return mV4SchemeSigners; + } + + /** + * Returns information about SourceStamp associated with the APK's signature. + */ + public SourceStampInfo getSourceStampInfo() { + return mSourceStampInfo; + } + + /** + * Returns the combined SigningCertificateLineage associated with this APK's APK Signature + * Scheme v3 signing block. + */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + /** + * Sets whether warnings should be treated as errors. + */ + void setWarningsAsErrors(boolean value) { + mWarningsAsErrors = value; + } + + /** + * Returns errors encountered while verifying the APK's signatures. + */ + public List getErrors() { + if (!mWarningsAsErrors) { + return mErrors; + } else { + List allErrors = new ArrayList<>(); + allErrors.addAll(mErrors); + allErrors.addAll(mWarnings); + return allErrors; + } + } + + /** + * Returns warnings encountered while verifying the APK's signatures. + */ + public List getWarnings() { + return mWarnings; + } + + private void mergeFrom(V1SchemeVerifier.Result source) { + mVerifiedUsingV1Scheme = source.verified; + mErrors.addAll(source.getErrors()); + mWarnings.addAll(source.getWarnings()); + for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) { + mV1SchemeSigners.add(new V1SchemeSignerInfo(signer)); + } + for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) { + mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer)); + } + } + + private void mergeFrom(ApkSigResult source) { + switch (source.signatureSchemeVersion) { + case VERSION_SOURCE_STAMP: + mSourceStampVerified = source.verified; + if (!source.mSigners.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0)); + } + break; + default: + throw new IllegalArgumentException( + "Unknown ApkSigResult Signing Block Scheme Id " + + source.signatureSchemeVersion); + } + } + + private void mergeFrom(ApkSigningBlockUtils.Result source) { + if (source == null) { + return; + } + if (source.containsErrors()) { + mErrors.addAll(source.getErrors()); + } + if (source.containsWarnings()) { + mWarnings.addAll(source.getWarnings()); + } + switch (source.signatureSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + mVerifiedUsingV2Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + mVerifiedUsingV3Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV3SchemeSigners.add(new V3SchemeSignerInfo(signer)); + } + // Do not overwrite a previously set lineage from a v3.1 signing block. + if (mSigningCertificateLineage == null) { + mSigningCertificateLineage = source.signingCertificateLineage; + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V31: + mVerifiedUsingV31Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV31SchemeSigners.add(new V3SchemeSignerInfo(signer)); + } + mSigningCertificateLineage = source.signingCertificateLineage; + break; + case VERSION_APK_SIGNATURE_SCHEME_V4: + mVerifiedUsingV4Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV4SchemeSigners.add(new V4SchemeSignerInfo(signer)); + } + break; + case VERSION_SOURCE_STAMP: + mSourceStampVerified = source.verified; + if (!source.signers.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.signers.get(0)); + } + break; + default: + throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); + } + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. Any error + * prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (mWarningsAsErrors && !mWarnings.isEmpty()) { + return true; + } + if (!mV1SchemeSigners.isEmpty()) { + for (V1SchemeSignerInfo signer : mV1SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (!mV3SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV3SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (!mV31SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV31SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (mSourceStampInfo != null) { + if (mSourceStampInfo.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) { + return true; + } + } + + return false; + } + + /** + * Returns all errors for this result, including any errors from signature scheme signers + * and the source stamp. + */ + public List getAllErrors() { + List errors = new ArrayList<>(); + errors.addAll(mErrors); + if (mWarningsAsErrors) { + errors.addAll(mWarnings); + } + if (!mV1SchemeSigners.isEmpty()) { + for (V1SchemeSignerInfo signer : mV1SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV3SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV3SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV31SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV31SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (mSourceStampInfo != null) { + errors.addAll(mSourceStampInfo.getErrors()); + if (mWarningsAsErrors) { + errors.addAll(mSourceStampInfo.getWarnings()); + } + } + return errors; + } + + /** + * Information about a JAR signer associated with the APK's signature. + */ + public static class V1SchemeSignerInfo { + private final String mName; + private final List mCertChain; + private final String mSignatureBlockFileName; + private final String mSignatureFileName; + + private final List mErrors; + private final List mWarnings; + + private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) { + mName = result.name; + mCertChain = result.certChain; + mSignatureBlockFileName = result.signatureBlockFileName; + mSignatureFileName = result.signatureFileName; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + } + + /** + * Returns a user-friendly name of the signer. + */ + public String getName() { + return mName; + } + + /** + * Returns the name of the JAR entry containing this signer's JAR signature block file. + */ + public String getSignatureBlockFileName() { + return mSignatureBlockFileName; + } + + /** + * Returns the name of the JAR entry containing this signer's JAR signature file. + */ + public String getSignatureFileName() { + return mSignatureFileName; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCertChain.isEmpty() ? null : mCertChain.get(0); + } + + /** + * Returns the certificate chain for the signer's public key. The certificate containing + * the public key is first, followed by the certificate (if any) which issued the + * signing certificate, and so forth. An empty list may be returned if an error was + * encountered during verification (see {@link #containsErrors()}). + */ + public List getCertificateChain() { + return mCertChain; + } + + /** + * Returns {@code true} if an error was encountered while verifying this signer's JAR + * signature. Any error prevents the signer's signature from being considered verified. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns errors encountered while verifying this signer's JAR signature. Any error + * prevents the signer's signature from being considered verified. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns warnings encountered while verifying this signer's JAR signature. Warnings + * do not prevent the signer's signature from being considered verified. + */ + public List getWarnings() { + return mWarnings; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + } + + /** + * Information about an APK Signature Scheme v2 signer associated with the APK's signature. + */ + public static class V2SchemeSignerInfo { + private final int mIndex; + private final List mCerts; + + private final List mErrors; + private final List mWarnings; + private final List + mContentDigests; + + private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + mContentDigests = result.contentDigests; + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v2 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List getCertificates() { + return mCerts; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public List getContentDigests() { + return mContentDigests; + } + } + + /** + * Information about an APK Signature Scheme v3 signer associated with the APK's signature. + */ + public static class V3SchemeSignerInfo { + private final int mIndex; + private final List mCerts; + + private final List mErrors; + private final List mWarnings; + private final List + mContentDigests; + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + private final boolean mRotationTargetsDevRelease; + private final SigningCertificateLineage mSigningCertificateLineage; + + private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + mContentDigests = result.contentDigests; + mMinSdkVersion = result.minSdkVersion; + mMaxSdkVersion = result.maxSdkVersion; + mSigningCertificateLineage = result.signingCertificateLineage; + mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt( + attribute -> attribute.getId()).anyMatch( + attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v3 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List getCertificates() { + return mCerts; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public List getContentDigests() { + return mContentDigests; + } + + /** + * Returns the minimum SDK version on which this signer should be verified. + */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** + * Returns the maximum SDK version on which this signer should be verified. + */ + public int getMaxSdkVersion() { + return mMaxSdkVersion; + } + + /** + * Returns whether rotation is targeting a development release. + * + *

A development release uses the SDK version of the previously released platform + * until the SDK of the development release is finalized. To allow rotation to target + * a development release after T, this attribute must be set to ensure rotation is + * used on the development release but ignored on the released platform with the same + * API level. + */ + public boolean getRotationTargetsDevRelease() { + return mRotationTargetsDevRelease; + } + + /** + * Returns the {@link SigningCertificateLineage} for this signer; when an APK has + * SDK targeted signing configs, the lineage of each signer could potentially contain + * a subset of the full signing lineage and / or different capabilities for each signer + * in the lineage. + */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + } + + /** + * Information about an APK Signature Scheme V4 signer associated with the APK's + * signature. + */ + public static class V4SchemeSignerInfo { + private final int mIndex; + private final List mCerts; + + private final List mErrors; + private final List mWarnings; + private final List + mContentDigests; + + private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + mContentDigests = result.contentDigests; + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v3 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List getCertificates() { + return mCerts; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public List getContentDigests() { + return mContentDigests; + } + } + + /** + * Information about SourceStamp associated with the APK's signature. + */ + public static class SourceStampInfo { + public enum SourceStampVerificationStatus { + /** The stamp is present and was successfully verified. */ + STAMP_VERIFIED, + /** The stamp is present but failed verification. */ + STAMP_VERIFICATION_FAILED, + /** The expected cert digest did not match the digest in the APK. */ + CERT_DIGEST_MISMATCH, + /** The stamp is not present at all. */ + STAMP_MISSING, + /** The stamp is at least partially present, but was not able to be verified. */ + STAMP_NOT_VERIFIED, + /** The stamp was not able to be verified due to an unexpected error. */ + VERIFICATION_ERROR + } + + private final List mCertificates; + private final List mCertificateLineage; + + private final List mErrors; + private final List mWarnings; + private final List mInfoMessages; + + private final SourceStampVerificationStatus mSourceStampVerificationStatus; + + private final long mTimestamp; + + private SourceStampInfo(ApkSignerInfo result) { + mCertificates = result.certs; + mCertificateLineage = result.certificateLineage; + mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getErrors()); + mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getWarnings()); + mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getInfoMessages()); + if (mErrors.isEmpty() && mWarnings.isEmpty()) { + mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED; + } else { + mSourceStampVerificationStatus = + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED; + } + mTimestamp = result.timestamp; + } + + SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) { + mCertificates = Collections.emptyList(); + mCertificateLineage = Collections.emptyList(); + mErrors = Collections.emptyList(); + mWarnings = Collections.emptyList(); + mInfoMessages = Collections.emptyList(); + mSourceStampVerificationStatus = sourceStampVerificationStatus; + mTimestamp = 0; + } + + /** + * Returns the SourceStamp's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the SourceStamp's public key. + */ + public X509Certificate getCertificate() { + return mCertificates.isEmpty() ? null : mCertificates.get(0); + } + + /** + * Returns a list containing all of the certificates in the stamp certificate lineage. + */ + public List getCertificatesInLineage() { + return mCertificateLineage; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns {@code true} if any info messages were encountered during verification of + * this source stamp. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + /** + * Returns a {@code List} of {@link IssueWithParams} representing info messages + * that were encountered during verification of the source stamp. + */ + public List getInfoMessages() { + return mInfoMessages; + } + + /** + * Returns the reason for any source stamp verification failures, or {@code + * STAMP_VERIFIED} if the source stamp was successfully verified. + */ + public SourceStampVerificationStatus getSourceStampVerificationStatus() { + return mSourceStampVerificationStatus; + } + + /** + * Returns the epoch timestamp in seconds representing the time this source stamp block + * was signed, or 0 if the timestamp is not available. + */ + public long getTimestampEpochSeconds() { + return mTimestamp; + } + } + } + /** * Error or warning encountered while verifying an APK's signatures. */ @@ -792,6 +2122,16 @@ public class ApkVerifier { */ JAR_SIG_NO_SIGNATURES("No JAR signatures"), + /** + * APK signature scheme v1 has exceeded the maximum number of jar signers. + *

    + *
  • Parameter 1: maximum allowed signers ({@code Integer})
  • + *
  • Parameter 2: total number of signers ({@code Integer})
  • + *
+ */ + JAR_SIG_MAX_SIGNATURES_EXCEEDED( + "APK Signature Scheme v1 only supports a maximum of %1$d signers, found %2$d"), + /** * APK does not contain any entries covered by JAR signatures. */ @@ -836,9 +2176,7 @@ public class ApkVerifier { JAR_SIG_UNNNAMED_SIG_FILE_SECTION( "Malformed %1$s: invidual section #%2$d does not have a name"), - /** - * APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). - */ + /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */ JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"), /** @@ -1235,6 +2573,16 @@ public class ApkVerifier { "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but " + "no such signature was found. Signature stripped?"), + /** + * APK signature scheme v2 has exceeded the maximum number of signers. + *
    + *
  • Parameter 1: maximum allowed signers ({@code Integer})
  • + *
  • Parameter 2: total number of signers ({@code Integer})
  • + *
+ */ + V2_SIG_MAX_SIGNATURES_EXCEEDED( + "APK Signature Scheme V2 only supports a maximum of %1$d signers, found %2$d"), + /** * APK Signature Scheme v2 signature contains no signers. */ @@ -1603,6 +2951,61 @@ public class ApkVerifier { V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions" + " using APK Signature Scheme v3 are not all a part of the same overall lineage."), + /** + * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block + * was not found. + * + *
    + *
  • Parameter 1: min SDK version supporting rotation from attribute ({@code Integer}) + *
+ */ + V31_BLOCK_MISSING( + "The v3 signer indicates key rotation should be supported starting from SDK " + + "version %1$s, but a v3.1 block was not found"), + + /** + * The v3 stripping protection attribute for rotation does not match the minimum SDK version + * targeting rotation in the v3.1 signer block. + * + *
    + *
  • Parameter 1: min SDK version supporting rotation from attribute ({@code Integer}) + *
  • Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer}) + *
+ */ + V31_ROTATION_MIN_SDK_MISMATCH( + "The v3 signer indicates key rotation should be supported starting from SDK " + + "version %1$s, but the v3.1 block targets %2$s for rotation"), + + /** + * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min + * SDK version stripping protection attribute was not written to the v3 signer. + * + *
    + *
  • Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer}) + *
+ */ + V31_ROTATION_MIN_SDK_ATTR_MISSING( + "APK supports key rotation starting from SDK version %1$s, but the v3 signer does" + + " not contain the attribute to detect if this signature is stripped"), + + /** + * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only + * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the + * same as the SDK version for rotation then this should be written to a v3.0 block. + */ + V31_BLOCK_FOUND_WITHOUT_V3_BLOCK( + "The APK contains a v3.1 signing block without a v3.0 base block"), + + /** + * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in + * the signer; this attribute is only intended for v3.1 signers to indicate they should be + * targeting the next development release that is using the SDK version of the previously + * released platform SDK version. + */ + V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER( + "The rotation-targets-dev-release attribute is only supported on v3.1 signers; " + + "this attribute will be ignored by the platform in a v3.0 signer"), + /** * APK Signing Block contains an unknown entry. * @@ -1740,6 +3143,11 @@ public class ApkVerifier { V4_SIG_MULTIPLE_SIGNERS( "V4 signature only supports one signer"), + /** + * V4.1 signature requires two signers to match the v3 and the v3.1. + */ + V41_SIG_NEEDS_TWO_SIGNERS("V4.1 signature requires two signers"), + /** * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer * used to sign APK Signature Scheme V4 signature. @@ -1747,8 +3155,29 @@ public class ApkVerifier { V4_SIG_V2_V3_SIGNERS_MISMATCH( "V4 signature and V2/V3 signature have mismatched certificates"), + /** + * The v4 signature's digest does not match the digest from the corresponding v2 / v3 + * signature. + * + *
    + *
  • Parameter 1: Signature scheme of mismatched digest ({@code int}) + *
  • Parameter 2: v2/v3 digest ({@code String}) + *
  • Parameter 3: v4 digest ({@code String}) + *
+ */ V4_SIG_V2_V3_DIGESTS_MISMATCH( - "V4 signature and V2/V3 signature have mismatched digests"), + "V4 signature and V%1$d signature have mismatched digests, V%1$d digest: %2$s, V4" + + " digest: %3$s"), + + /** + * The v4 signature does not contain the expected number of digests. + * + *
    + *
  • Parameter 1: Number of digests found ({@code int}) + *
+ */ + V4_SIG_UNEXPECTED_DIGESTS( + "V4 signature does not have the expected number of digests, found %1$d"), /** * The v4 signature format version isn't the same as the tool's current version, something @@ -1759,8 +3188,14 @@ public class ApkVerifier { + "version %2$d"), /** - * APK contains SourceStamp file, but does not contain a SourceStamp signature. + * The APK does not contain the source stamp certificate digest file nor the signature block + * when verification expected a source stamp to be present. */ + SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING( + "Neither the source stamp certificate digest file nor the signature block are " + + "present in the APK"), + + /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */ SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"), /** @@ -1772,9 +3207,7 @@ public class ApkVerifier { */ SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"), - /** - * Failed to parse SourceStamp's signature. - */ + /** Failed to parse SourceStamp's signature. */ SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"), /** @@ -1805,15 +3238,19 @@ public class ApkVerifier { */ SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), - /** - * SourceStamp offers no signatures. - */ + /** SourceStamp offers no signatures. */ SOURCE_STAMP_NO_SIGNATURE("No signature"), /** * SourceStamp offers an unsupported signature. + *
    + *
  • Parameter 1: list of {@link SignatureAlgorithm}s in the source stamp + * signing block. + *
  • Parameter 2: {@code Exception} caught when attempting to obtain the list of + * supported signatures. + *
*/ - SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"), + SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"), /** * SourceStamp's certificate listed in the APK signing block does not match the certificate @@ -1828,7 +3265,106 @@ public class ApkVerifier { */ SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK( "Certificate mismatch between SourceStamp block in APK signing block and" - + " SourceStamp file in APK: <%1$s> vs <%2$s>"); + + " SourceStamp file in APK: <%1$s> vs <%2$s>"), + + /** + * The APK contains a source stamp signature block without the expected certificate digest + * in the APK contents. + */ + SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST( + "A source stamp signature block was found without a corresponding certificate " + + "digest in the APK"), + + /** + * When verifying just the source stamp, the certificate digest in the APK does not match + * the expected digest. + *
    + *
  • Parameter 1: SHA-256 digest of the source stamp certificate in the APK. + *
  • Parameter 2: SHA-256 digest of the expected source stamp certificate. + *
+ */ + SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH( + "The source stamp certificate digest in the APK, %1$s, does not match the " + + "expected digest, %2$s"), + + /** + * Source stamp block contains a malformed attribute. + * + *
    + *
  • Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
  • + *
+ */ + SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"), + + /** + * Source stamp block contains an unknown attribute. + * + *
    + *
  • Parameter 1: attribute ID ({@code Integer})
  • + *
+ */ + SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"), + + /** + * Failed to parse the SigningCertificateLineage structure in the source stamp + * attributes section. + */ + SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage " + + "structure in the source stamp attributes section."), + + /** + * The source stamp certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the stamp certificate history. + */ + SOURCE_STAMP_POR_CERT_MISMATCH( + "APK signing certificate differs from the associated certificate found in the " + + "signer's SigningCertificateLineage."), + + /** + * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record + * with signature(s) that did not verify. + */ + SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute " + + "contains a proof-of-rotation record with signature(s) that did not verify."), + + /** + * The source stamp timestamp attribute has an invalid value (<= 0). + *
    + *
  • Parameter 1: The invalid timestamp value. + *
+ */ + SOURCE_STAMP_INVALID_TIMESTAMP( + "The source stamp" + + " timestamp attribute has an invalid value: %1$d"), + + /** + * A signature scheme version that is not in the source stamp was provided to the verifier. + *
    + *
  • Parameter 1: An int value representing the signature scheme version. + *
+ */ + SOURCE_STAMP_SIGNATURE_SCHEME_NOT_AVAILABLE( + "No digests are available in the source stamp for signature scheme: %1$d"), + + /** + * The APK could not be properly parsed due to a ZIP or APK format exception. + *
    + *
  • Parameter 1: The {@code Exception} caught when attempting to parse the APK. + *
+ */ + MALFORMED_APK( + "Malformed APK; the following exception was caught when attempting to parse the " + + "APK: %1$s"), + + /** + * An unexpected exception was caught when attempting to verify the signature(s) within the + * APK. + *
    + *
  • Parameter 1: The {@code Exception} caught during verification. + *
+ */ + UNEXPECTED_EXCEPTION( + "An unexpected exception was caught when verifying the signature: %1$s"); private final String mFormat; @@ -1845,587 +3381,11 @@ public class ApkVerifier { } } - /** - * Result of verifying an APKs signatures. The APK can be considered verified iff - * {@link #isVerified()} returns {@code true}. - */ - public static class Result { - private final List mErrors = new ArrayList<>(); - private final List mWarnings = new ArrayList<>(); - private final List mSignerCerts = new ArrayList<>(); - private final List mV1SchemeSigners = new ArrayList<>(); - private final List mV1SchemeIgnoredSigners = new ArrayList<>(); - private final List mV2SchemeSigners = new ArrayList<>(); - private final List mV3SchemeSigners = new ArrayList<>(); - private final List mV4SchemeSigners = new ArrayList<>(); - private SourceStampInfo mSourceStampInfo; - - private boolean mVerified; - private boolean mVerifiedUsingV1Scheme; - private boolean mVerifiedUsingV2Scheme; - private boolean mVerifiedUsingV3Scheme; - private boolean mVerifiedUsingV4Scheme; - private boolean mSourceStampVerified; - private SigningCertificateLineage mSigningCertificateLineage; - - /** - * Returns {@code true} if the APK's signatures verified. - */ - public boolean isVerified() { - return mVerified; - } - - private void setVerified() { - mVerified = true; - } - - /** - * Returns {@code true} if the APK's JAR signatures verified. - */ - public boolean isVerifiedUsingV1Scheme() { - return mVerifiedUsingV1Scheme; - } - - /** - * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. - */ - public boolean isVerifiedUsingV2Scheme() { - return mVerifiedUsingV2Scheme; - } - - /** - * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified. - */ - public boolean isVerifiedUsingV3Scheme() { - return mVerifiedUsingV3Scheme; - } - - /** - * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified. - */ - public boolean isVerifiedUsingV4Scheme() { - return mVerifiedUsingV4Scheme; - } - - /** - * Returns {@code true} if the APK's SourceStamp signature verified. - */ - public boolean isSourceStampVerified() { - return mSourceStampVerified; - } - - /** - * Returns the verified signers' certificates, one per signer. - */ - public List getSignerCertificates() { - return mSignerCerts; - } - - private void addSignerCertificate(X509Certificate cert) { - mSignerCerts.add(cert); - } - - /** - * Returns information about JAR signers associated with the APK's signature. These are the - * signers used by Android. - * - * @see #getV1SchemeIgnoredSigners() - */ - public List getV1SchemeSigners() { - return mV1SchemeSigners; - } - - /** - * Returns information about JAR signers ignored by the APK's signature verification - * process. These signers are ignored by Android. However, each signer's errors or warnings - * will contain information about why they are ignored. - * - * @see #getV1SchemeSigners() - */ - public List getV1SchemeIgnoredSigners() { - return mV1SchemeIgnoredSigners; - } - - /** - * Returns information about APK Signature Scheme v2 signers associated with the APK's - * signature. - */ - public List getV2SchemeSigners() { - return mV2SchemeSigners; - } - - /** - * Returns information about APK Signature Scheme v3 signers associated with the APK's - * signature. - * - * Multiple signers represent different targeted platform versions, not - * a signing identity of multiple signers. APK Signature Scheme v3 only supports single - * signer identities. - */ - public List getV3SchemeSigners() { - return mV3SchemeSigners; - } - - private List getV4SchemeSigners() { - return mV4SchemeSigners; - } - - /** - * Returns information about SourceStamp associated with the APK's signature. - */ - public SourceStampInfo getSourceStampInfo() { - return mSourceStampInfo; - } - - /** - * Returns the combined SigningCertificateLineage associated with this APK's APK Signature - * Scheme v3 signing block. - */ - public SigningCertificateLineage getSigningCertificateLineage() { - return mSigningCertificateLineage; - } - - void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - - void addWarning(Issue msg, Object... parameters) { - mWarnings.add(new IssueWithParams(msg, parameters)); - } - - /** - * Returns errors encountered while verifying the APK's signatures. - */ - public List getErrors() { - return mErrors; - } - - /** - * Returns warnings encountered while verifying the APK's signatures. - */ - public List getWarnings() { - return mWarnings; - } - - private void mergeFrom(V1SchemeVerifier.Result source) { - mVerifiedUsingV1Scheme = source.verified; - mErrors.addAll(source.getErrors()); - mWarnings.addAll(source.getWarnings()); - for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) { - mV1SchemeSigners.add(new V1SchemeSignerInfo(signer)); - } - for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) { - mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer)); - } - } - - private void mergeFrom(ApkSigningBlockUtils.Result source) { - switch (source.signatureSchemeVersion) { - case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: - mVerifiedUsingV2Scheme = source.verified; - for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { - mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); - } - break; - case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: - mVerifiedUsingV3Scheme = source.verified; - for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { - mV3SchemeSigners.add(new V3SchemeSignerInfo(signer)); - } - mSigningCertificateLineage = source.signingCertificateLineage; - break; - case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4: - mVerifiedUsingV4Scheme = source.verified; - for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { - mV4SchemeSigners.add(new V4SchemeSignerInfo(signer)); - } - break; - case ApkSigningBlockUtils.VERSION_SOURCE_STAMP: - mSourceStampVerified = source.verified; - if (!source.signers.isEmpty()) { - mSourceStampInfo = new SourceStampInfo(source.signers.get(0)); - } - break; - default: - throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); - } - mErrors.addAll(source.getErrors()); - mWarnings.addAll(source.getWarnings()); - } - - /** - * Returns {@code true} if an error was encountered while verifying the APK. Any error - * prevents the APK from being considered verified. - */ - public boolean containsErrors() { - if (!mErrors.isEmpty()) { - return true; - } - if (!mV1SchemeSigners.isEmpty()) { - for (V1SchemeSignerInfo signer : mV1SchemeSigners) { - if (signer.containsErrors()) { - return true; - } - } - } - if (!mV2SchemeSigners.isEmpty()) { - for (V2SchemeSignerInfo signer : mV2SchemeSigners) { - if (signer.containsErrors()) { - return true; - } - } - } - if (!mV3SchemeSigners.isEmpty()) { - for (V3SchemeSignerInfo signer : mV3SchemeSigners) { - if (signer.containsErrors()) { - return true; - } - } - } - if (mSourceStampInfo != null && mSourceStampInfo.containsErrors()) { - return true; - } - - return false; - } - - /** - * Information about a JAR signer associated with the APK's signature. - */ - public static class V1SchemeSignerInfo { - private final String mName; - private final List mCertChain; - private final String mSignatureBlockFileName; - private final String mSignatureFileName; - - private final List mErrors; - private final List mWarnings; - - private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) { - mName = result.name; - mCertChain = result.certChain; - mSignatureBlockFileName = result.signatureBlockFileName; - mSignatureFileName = result.signatureFileName; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - } - - /** - * Returns a user-friendly name of the signer. - */ - public String getName() { - return mName; - } - - /** - * Returns the name of the JAR entry containing this signer's JAR signature block file. - */ - public String getSignatureBlockFileName() { - return mSignatureBlockFileName; - } - - /** - * Returns the name of the JAR entry containing this signer's JAR signature file. - */ - public String getSignatureFileName() { - return mSignatureFileName; - } - - /** - * Returns this signer's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the signer's public key. - */ - public X509Certificate getCertificate() { - return mCertChain.isEmpty() ? null : mCertChain.get(0); - } - - /** - * Returns the certificate chain for the signer's public key. The certificate containing - * the public key is first, followed by the certificate (if any) which issued the - * signing certificate, and so forth. An empty list may be returned if an error was - * encountered during verification (see {@link #containsErrors()}). - */ - public List getCertificateChain() { - return mCertChain; - } - - /** - * Returns {@code true} if an error was encountered while verifying this signer's JAR - * signature. Any error prevents the signer's signature from being considered verified. - */ - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - /** - * Returns errors encountered while verifying this signer's JAR signature. Any error - * prevents the signer's signature from being considered verified. - */ - public List getErrors() { - return mErrors; - } - - /** - * Returns warnings encountered while verifying this signer's JAR signature. Warnings - * do not prevent the signer's signature from being considered verified. - */ - public List getWarnings() { - return mWarnings; - } - - private void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - } - - /** - * Information about an APK Signature Scheme v2 signer associated with the APK's signature. - */ - public static class V2SchemeSignerInfo { - private final int mIndex; - private final List mCerts; - - private final List mErrors; - private final List mWarnings; - private final List - mContentDigests; - - private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { - mIndex = result.index; - mCerts = result.certs; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - mContentDigests = result.contentDigests; - } - - /** - * Returns this signer's {@code 0}-based index in the list of signers contained in the - * APK's APK Signature Scheme v2 signature. - */ - public int getIndex() { - return mIndex; - } - - /** - * Returns this signer's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the signer's public key. - */ - public X509Certificate getCertificate() { - return mCerts.isEmpty() ? null : mCerts.get(0); - } - - /** - * Returns this signer's certificates. The first certificate is for the signer's public - * key. An empty list may be returned if an error was encountered during verification - * (see {@link #containsErrors()}). - */ - public List getCertificates() { - return mCerts; - } - - private void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - - public List getContentDigests() { - return mContentDigests; - } - } - - /** - * Information about an APK Signature Scheme v3 signer associated with the APK's signature. - */ - public static class V3SchemeSignerInfo { - private final int mIndex; - private final List mCerts; - - private final List mErrors; - private final List mWarnings; - private final List - mContentDigests; - - private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { - mIndex = result.index; - mCerts = result.certs; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - mContentDigests = result.contentDigests; - } - - /** - * Returns this signer's {@code 0}-based index in the list of signers contained in the - * APK's APK Signature Scheme v3 signature. - */ - public int getIndex() { - return mIndex; - } - - /** - * Returns this signer's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the signer's public key. - */ - public X509Certificate getCertificate() { - return mCerts.isEmpty() ? null : mCerts.get(0); - } - - /** - * Returns this signer's certificates. The first certificate is for the signer's public - * key. An empty list may be returned if an error was encountered during verification - * (see {@link #containsErrors()}). - */ - public List getCertificates() { - return mCerts; - } - - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - - public List getContentDigests() { - return mContentDigests; - } - } - - /** - * Information about an APK Signature Scheme V4 signer associated with the APK's - * signature. - */ - public static class V4SchemeSignerInfo { - private final int mIndex; - private final List mCerts; - - private final List mErrors; - private final List mWarnings; - private final List - mContentDigests; - - private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { - mIndex = result.index; - mCerts = result.certs; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - mContentDigests = result.contentDigests; - } - - /** - * Returns this signer's {@code 0}-based index in the list of signers contained in the - * APK's APK Signature Scheme v3 signature. - */ - public int getIndex() { - return mIndex; - } - - /** - * Returns this signer's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the signer's public key. - */ - public X509Certificate getCertificate() { - return mCerts.isEmpty() ? null : mCerts.get(0); - } - - /** - * Returns this signer's certificates. The first certificate is for the signer's public - * key. An empty list may be returned if an error was encountered during verification - * (see {@link #containsErrors()}). - */ - public List getCertificates() { - return mCerts; - } - - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - - public List getContentDigests() { - return mContentDigests; - } - } - - /** - * Information about SourceStamp associated with the APK's signature. - */ - public static class SourceStampInfo { - private final List mCertificates; - - private final List mErrors; - private final List mWarnings; - - private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) { - mCertificates = result.certs; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - } - - /** - * Returns the SourceStamp's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the SourceStamp's public key. - */ - public X509Certificate getCertificate() { - return mCertificates.isEmpty() ? null : mCertificates.get(0); - } - - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - } - } - /** * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted * form. */ - public static class IssueWithParams { + public static class IssueWithParams extends ApkVerificationIssue { private final Issue mIssue; private final Object[] mParams; @@ -2434,6 +3394,7 @@ public class ApkVerifier { * parameters. */ public IssueWithParams(Issue issue, Object[] params) { + super(issue.mFormat, params); mIssue = issue; mParams = params; } @@ -2588,4 +3549,123 @@ public class ApkVerifier { mMaxSdkVersion); } } + + /** + * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link + * IssueWithParams} equivalent. + */ + public static class ApkVerificationIssueAdapter { + private ApkVerificationIssueAdapter() { + } + + // This field is visible for testing + static final Map sVerificationIssueIdToIssue = new HashMap<>(); + + static { + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS, + Issue.V2_SIG_MALFORMED_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS, + Issue.V2_SIG_NO_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER, + Issue.V2_SIG_MALFORMED_SIGNER); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE, + Issue.V2_SIG_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES, + Issue.V2_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE, + Issue.V2_SIG_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES, + Issue.V2_SIG_NO_CERTIFICATES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST, + Issue.V2_SIG_MALFORMED_DIGEST); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS, + Issue.V3_SIG_MALFORMED_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS, + Issue.V3_SIG_NO_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER, + Issue.V3_SIG_MALFORMED_SIGNER); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE, + Issue.V3_SIG_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES, + Issue.V3_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE, + Issue.V3_SIG_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES, + Issue.V3_SIG_NO_CERTIFICATES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST, + Issue.V3_SIG_MALFORMED_DIGEST); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE, + Issue.SOURCE_STAMP_NO_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, + Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, + Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, + Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, + Issue.SOURCE_STAMP_DID_NOT_VERIFY); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, + Issue.SOURCE_STAMP_VERIFY_EXCEPTION); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST, + Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING, + Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, + Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue + .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, + Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK, + Issue.MALFORMED_APK); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION, + Issue.UNEXPECTED_EXCEPTION); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING, + Issue.SOURCE_STAMP_SIG_MISSING); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, + Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, + Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE, + Issue.SOURCE_STAMP_MALFORMED_LINEAGE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH, + Issue.SOURCE_STAMP_POR_CERT_MISMATCH); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY, + Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES, + Issue.JAR_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, + Issue.JAR_SIG_PARSE_EXCEPTION); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP, + Issue.SOURCE_STAMP_INVALID_TIMESTAMP); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_SCHEME_NOT_AVAILABLE, + Issue.SOURCE_STAMP_SIGNATURE_SCHEME_NOT_AVAILABLE); + } + + /** + * Converts the provided {@code verificationIssues} to a {@code List} of corresponding + * {@link IssueWithParams} instances. + */ + public static List getIssuesFromVerificationIssues( + List verificationIssues) { + List result = new ArrayList<>(verificationIssues.size()); + for (ApkVerificationIssue issue : verificationIssues) { + if (issue instanceof IssueWithParams) { + result.add((IssueWithParams) issue); + } else { + result.add( + new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()), + issue.getParams())); + } + } + return result; + } + } } diff --git a/apksigner/src/main/java/com/android/apksig/Constants.java b/apksigner/src/main/java/com/android/apksig/Constants.java new file mode 100644 index 00000000..dd33028c --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/Constants.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 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 com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.v1.V1SchemeConstants; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; + +/** + * Exports internally defined constants to allow clients to reference these values without relying + * on internal code. + */ +public class Constants { + private Constants() {} + + public static final int VERSION_SOURCE_STAMP = 0; + public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; + public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; + public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; + public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31; + public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; + + /** + * The maximum number of signers supported by the v1 and v2 APK Signature Schemes. + */ + public static final int MAX_APK_SIGNERS = 10; + + /** + * The default page alignment for native library files in bytes. + */ + public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384; + + public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME; + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID; + public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID; + + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; + public static final int V2_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; + + public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1"; +} diff --git a/apksigner/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/apksigner/src/main/java/com/android/apksig/DefaultApkSignerEngine.java index 95564987..1b797112 100644 --- a/apksigner/src/main/java/com/android/apksig/DefaultApkSignerEngine.java +++ b/apksigner/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -18,9 +18,12 @@ package com.android.apksig; import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; @@ -29,9 +32,11 @@ import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.stamp.V2SourceStampSigner; import com.android.apksig.internal.apk.v1.DigestAlgorithm; +import com.android.apksig.internal.apk.v1.V1SchemeConstants; import com.android.apksig.internal.apk.v1.V1SchemeSigner; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.apk.v2.V2SchemeSigner; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeSigner; import com.android.apksig.internal.apk.v4.V4SchemeSigner; import com.android.apksig.internal.apk.v4.V4Signature; @@ -63,6 +68,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -97,35 +104,39 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private final boolean mOtherSignersSignaturesPreserved; private final String mCreatedBy; private final List mSignerConfigs; + private final List mTargetedSignerConfigs; private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; + private final boolean mSourceStampTimestampEnabled; private final int mMinSdkVersion; private final SigningCertificateLineage mSigningCertificateLineage; - /** - * Requests for digests of output JAR entries. - */ - private final Map mOutputJarEntryDigestRequests = - new HashMap<>(); - /** - * Digests of output JAR entries. - */ - private final Map mOutputJarEntryDigests = new HashMap<>(); - /** - * Data of JAR entries emitted by this engine as v1 signature. - */ - private final Map mEmittedSignatureJarEntryData = new HashMap<>(); - /** - * Requests for data of output JAR entries which comprise the v1 signature. - */ - private final Map mOutputSignatureJarEntryDataRequests = - new HashMap<>(); + + private List mPreservedV2Signers = Collections.emptyList(); + private List> mPreservedSignatureBlocks = Collections.emptyList(); + private List mV1SignerConfigs = Collections.emptyList(); private DigestAlgorithm mV1ContentDigestAlgorithm; + private boolean mClosed; + private boolean mV1SignaturePending; - /** - * Names of JAR entries which this engine is expected to output as part of v1 signing. - */ + + /** Names of JAR entries which this engine is expected to output as part of v1 signing. */ private Set mSignatureExpectedOutputJarEntryNames = Collections.emptySet(); + + /** Requests for digests of output JAR entries. */ + private final Map mOutputJarEntryDigestRequests = + new HashMap<>(); + + /** Digests of output JAR entries. */ + private final Map mOutputJarEntryDigests = new HashMap<>(); + + /** Data of JAR entries emitted by this engine as v1 signature. */ + private final Map mEmittedSignatureJarEntryData = new HashMap<>(); + + /** Requests for data of output JAR entries which comprise the v1 signature. */ + private final Map mOutputSignatureJarEntryDataRequests = + new HashMap<>(); /** * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued. */ @@ -159,9 +170,27 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED; + /** + * A Set of block IDs to be discarded when requesting to preserve the original signatures. + */ + private static final Set DISCARDED_SIGNATURE_BLOCK_IDS; + static { + DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3); + // The verity padding block is recomputed on an + // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary. + DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID); + // The source stamp block is not currently preserved; appending a new signature scheme + // block will invalidate the previous source stamp. + DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID); + DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID); + } + private DefaultApkSignerEngine( List signerConfigs, + List targetedSignerConfigs, SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, + boolean sourceStampTimestampEnabled, int minSdkVersion, boolean v1SigningEnabled, boolean v2SigningEnabled, @@ -172,13 +201,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { String createdBy, SigningCertificateLineage signingCertificateLineage) throws InvalidKeyException { - if (signerConfigs.isEmpty()) { + if (signerConfigs.isEmpty() && targetedSignerConfigs.isEmpty()) { throw new IllegalArgumentException("At least one signer config must be provided"); } - if (otherSignersSignaturesPreserved) { - throw new UnsupportedOperationException( - "Preserving other signer's signatures is not yet implemented"); - } mV1SigningEnabled = v1SigningEnabled; mV2SigningEnabled = v2SigningEnabled; @@ -191,7 +216,10 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; mCreatedBy = createdBy; mSignerConfigs = signerConfigs; + mTargetedSignerConfigs = targetedSignerConfigs; mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + mSourceStampTimestampEnabled = sourceStampTimestampEnabled; mMinSdkVersion = minSdkVersion; mSigningCertificateLineage = signingCertificateLineage; @@ -200,7 +228,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { // v3 signing only supports single signers, of which the oldest (first) will be the // one to use for v1 and v2 signing - SignerConfig oldestConfig = signerConfigs.get(0); + SignerConfig oldestConfig = !signerConfigs.isEmpty() ? signerConfigs.get(0) + : targetedSignerConfigs.get(0); // in the event of signing certificate changes, make sure we have the oldest in the // signing history to sign with v1 @@ -211,8 +240,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { if (subLineage.size() != 1) { throw new IllegalArgumentException( "v1 signing enabled but the oldest signer in the" - + " SigningCertificateLineage is missing. Please provide the" - + " oldest signer to enable v1 signing"); + + " SigningCertificateLineage is missing. Please provide the" + + " oldest signer to enable v1 signing"); } } createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion); @@ -250,9 +279,10 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(publicKey, minSdkVersion); V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig(); v1SignerConfig.name = v1SignerName; - v1SignerConfig.privateKey = signerConfig.getPrivateKey(); + v1SignerConfig.keyConfig = signerConfig.getKeyConfig(); v1SignerConfig.certificates = certificates; v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm; + v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning(); // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm // of comparable strength to the digest algorithm used for computing the signature. // When there are multiple signers, pick the strongest digest algorithm out of their @@ -262,7 +292,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm; } else { if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare( - v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) + v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) > 0) { v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm; } @@ -282,7 +312,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { // to use for v1 and v2 signing List signerConfig = new ArrayList<>(); - SignerConfig oldestConfig = mSignerConfigs.get(0); + SignerConfig oldestConfig = !mSignerConfigs.isEmpty() ? mSignerConfigs.get(0) + : mTargetedSignerConfigs.get(0); // first make sure that if we have signing certificate history that the oldest signer // corresponds to the oldest ancestor @@ -298,7 +329,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } signerConfig.add( createSigningBlockSignerConfig( - mSignerConfigs.get(0), + oldestConfig, apkSigningBlockPaddingSupported, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); return signerConfig; @@ -309,12 +340,18 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - private List createV3SignerConfigs( - boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { - List rawConfigs = - createSigningBlockSignerConfigs( - apkSigningBlockPaddingSupported, - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + private List processV3Configs( + List rawConfigs) throws InvalidKeyException { + // If the caller only specified targeted signing configs, ensure those configs cover the + // full range for V3 support (or the APK's minSdkVersion if > P). + int minRequiredV3SdkVersion = Math.max(AndroidSdkVersion.P, mMinSdkVersion); + if (mSignerConfigs.isEmpty() && + mTargetedSignerConfigs.get(0).getMinSdkVersion() > minRequiredV3SdkVersion) { + throw new IllegalArgumentException( + "The provided targeted signer configs do not cover the SDK range for V3 " + + "support; either provide the original signer or ensure a signer " + + "targets SDK version " + minRequiredV3SdkVersion); + } List processedConfigs = new ArrayList<>(); @@ -339,20 +376,40 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { // this needs to change config.maxSdkVersion = Integer.MAX_VALUE; } else { - // otherwise, we only want to use this signer up to the minimum platform version - // on which a newer one is acceptable - config.maxSdkVersion = currentMinSdk - 1; + // If the previous signer was targeting a development release, then the current + // signer's maxSdkVersion should overlap with the previous signer's minSdkVersion + // to ensure the current signer applies to the production release. + ApkSigningBlockUtils.SignerConfig prevSigner = processedConfigs.get( + processedConfigs.size() - 1); + if (prevSigner.signerTargetsDevRelease) { + config.maxSdkVersion = prevSigner.minSdkVersion; + } else { + config.maxSdkVersion = currentMinSdk - 1; + } } - config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms); - if (mSigningCertificateLineage != null) { - config.mSigningCertificateLineage = - mSigningCertificateLineage.getSubLineage(config.certificates.get(0)); + if (config.minSdkVersion == V3SchemeConstants.DEV_RELEASE) { + // If the current signer is targeting the current development release, then set + // the signer's minSdkVersion to the last production release and the flag indicating + // this signer is targeting a dev release. + config.minSdkVersion = V3SchemeConstants.PROD_RELEASE; + config.signerTargetsDevRelease = true; + } else if (config.minSdkVersion == 0) { + config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms( + config.signatureAlgorithms); + } + // Truncate the lineage to the current signer if it is not the latest signer. + X509Certificate signerCert = config.certificates.get(0); + if (config.signingCertificateLineage != null + && !config.signingCertificateLineage.isCertificateLatestInLineage(signerCert)) { + config.signingCertificateLineage = config.signingCertificateLineage.getSubLineage( + signerCert); } // we know that this config will be used, so add it to our result, order doesn't matter - // at this point (and likely only one will be needed + // at this point processedConfigs.add(config); currentMinSdk = config.minSdkVersion; - if (currentMinSdk <= mMinSdkVersion || currentMinSdk <= AndroidSdkVersion.P) { + if (config.signerTargetsDevRelease ? currentMinSdk < minRequiredV3SdkVersion + : currentMinSdk <= minRequiredV3SdkVersion) { // this satisfies all we need, stop here break; } @@ -363,26 +420,70 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { "Provided key algorithms not supported on all desired " + "Android SDK versions"); } + return processedConfigs; } - private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() - throws InvalidKeyException, IllegalStateException { - List configs = - createSigningBlockSignerConfigs( - true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); - if (configs.size() != 1) { - throw new IllegalStateException("Only accepting one signer config for V4 Signature."); + private List createV3SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3)); + } + + private List processV31SignerConfigs( + List v3SignerConfigs) { + // The V3.1 signature scheme supports SDK targeted signing config, but this scheme should + // only be used when a separate signing config exists for the V3.0 block. + if (v3SignerConfigs.size() == 1) { + return null; } - return configs.get(0); + + // When there are multiple signing configs, the signer with the minimum SDK version should + // be used for the V3.0 block, and all other signers should be used for the V3.1 block. + int signerMinSdkVersion = v3SignerConfigs.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(AndroidSdkVersion.P); + List v31SignerConfigs = new ArrayList<>(); + Iterator v3SignerIterator = v3SignerConfigs.iterator(); + while (v3SignerIterator.hasNext()) { + ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next(); + // If the signer config's minSdkVersion supports V3.1 and is not the min signer in the + // list, then add it to the V3.1 signer configs and remove it from the V3.0 list. If + // the signer is targeting the minSdkVersion as a development release, then it should + // be included in V3.1 to allow the V3.0 block to target the production release of the + // same SDK version. + if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT + && (signerConfig.minSdkVersion > signerMinSdkVersion + || (signerConfig.minSdkVersion >= signerMinSdkVersion + && signerConfig.signerTargetsDevRelease))) { + v31SignerConfigs.add(signerConfig); + v3SignerIterator.remove(); + } + } + return v31SignerConfigs; + } + + private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException { + List v4Configs = createSigningBlockSignerConfigs(true, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); + if (v4Configs.size() != 1) { + // V4 uses signer config to connect back to v3. Use the same filtering logic. + v4Configs = processV3Configs(v4Configs); + } + List v41configs = processV31SignerConfigs(v4Configs); + return new V4SchemeSigner.SignerConfig(v4Configs, v41configs); } private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig() throws InvalidKeyException { - return createSigningBlockSignerConfig( + ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig( mSourceStampSignerConfig, /* apkSigningBlockPaddingSupported= */ false, ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + if (mSourceStampSigningCertificateLineage != null) { + config.signingCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage( + config.certificates.get(0)); + } + return config; } private int getMinSdkFromV3SignatureAlgorithms(List algorithms) { @@ -404,13 +505,21 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private List createSigningBlockSignerConfigs( boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException { List signerConfigs = - new ArrayList<>(mSignerConfigs.size()); + new ArrayList<>(mSignerConfigs.size() + mTargetedSignerConfigs.size()); for (int i = 0; i < mSignerConfigs.size(); i++) { SignerConfig signerConfig = mSignerConfigs.get(i); signerConfigs.add( createSigningBlockSignerConfig( signerConfig, apkSigningBlockPaddingSupported, schemeId)); } + if (schemeId >= VERSION_APK_SIGNATURE_SCHEME_V3) { + for (int i = 0; i < mTargetedSignerConfigs.size(); i++) { + SignerConfig signerConfig = mTargetedSignerConfigs.get(i); + signerConfigs.add( + createSigningBlockSignerConfig( + signerConfig, apkSigningBlockPaddingSupported, schemeId)); + } + } return signerConfigs; } @@ -421,8 +530,11 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { PublicKey publicKey = certificates.get(0).getPublicKey(); ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig(); - newSignerConfig.privateKey = signerConfig.getPrivateKey(); + newSignerConfig.keyConfig = signerConfig.getKeyConfig(); newSignerConfig.certificates = certificates; + newSignerConfig.minSdkVersion = signerConfig.getMinSdkVersion(); + newSignerConfig.signerTargetsDevRelease = signerConfig.getSignerTargetsDevRelease(); + newSignerConfig.signingCertificateLineage = signerConfig.getSigningCertificateLineage(); switch (schemeId) { case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: @@ -430,7 +542,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { V2SchemeSigner.getSuggestedSignatureAlgorithms( publicKey, mMinSdkVersion, - apkSigningBlockPaddingSupported && mVerityEnabled); + apkSigningBlockPaddingSupported && mVerityEnabled, + signerConfig.getDeterministicDsaSigning()); break; case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: try { @@ -438,7 +551,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { V3SchemeSigner.getSuggestedSignatureAlgorithms( publicKey, mMinSdkVersion, - apkSigningBlockPaddingSupported && mVerityEnabled); + apkSigningBlockPaddingSupported && mVerityEnabled, + signerConfig.getDeterministicDsaSigning()); } catch (InvalidKeyException e) { // It is possible for a signer used for v1/v2 signing to not be allowed for use @@ -452,7 +566,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { try { newSignerConfig.signatureAlgorithms = V4SchemeSigner.getSuggestedSignatureAlgorithms( - publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported); + publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported, + signerConfig.getDeterministicDsaSigning()); } catch (InvalidKeyException e) { // V4 is an optional signing schema, ok to proceed without. newSignerConfig.signatureAlgorithms = null; @@ -484,16 +599,16 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { * for in the {@link #outputJarEntry(String)} method * * @param manifestBytes raw representation of MANIFEST.MF file - * @param entryNames a set of expected entries names + * @param entryNames a set of expected entries names * @return set of entry names which were processed by the engine during the initialization, a - * subset of entryNames + * subset of entryNames */ @Override @SuppressWarnings("AndroidJdkLibsChecker") public Set initWith(byte[] manifestBytes, Set entryNames) { - V1SchemeVerifier.Result dummyResult = new V1SchemeVerifier.Result(); + V1SchemeVerifier.Result result = new V1SchemeVerifier.Result(); Pair> sections = - V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult); + V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result); String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm); for (Map.Entry entry : sections.getSecond().entrySet()) { String entryName = entry.getKey(); @@ -532,11 +647,92 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } if (mOtherSignersSignaturesPreserved) { - // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured - // in this engine. + boolean schemeSignatureBlockPreserved = false; + mPreservedSignatureBlocks = new ArrayList<>(); + try { + List> signatureBlocks = + ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock); + for (Pair signatureBlock : signatureBlocks) { + if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { + // If a V2 signature block is found and the engine is configured to use V2 + // then save any of the previous signers that are not part of the current + // signing request. + if (mV2SigningEnabled) { + List, byte[]>> v2Signers = + ApkSigningBlockUtils.getApkSignatureBlockSigners( + signatureBlock.getFirst()); + mPreservedV2Signers = new ArrayList<>(v2Signers.size()); + for (Pair, byte[]> v2Signer : v2Signers) { + if (!isConfiguredWithSigner(v2Signer.getFirst())) { + mPreservedV2Signers.add(v2Signer.getSecond()); + schemeSignatureBlockPreserved = true; + } + } + } else { + // else V2 signing is not enabled; save the entire signature block to be + // added to the final APK signing block. + mPreservedSignatureBlocks.add(signatureBlock); + schemeSignatureBlockPreserved = true; + } + } else if (signatureBlock.getSecond() + == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) { + // Preserving other signers in the presence of a V3 signature block is only + // supported if the engine is configured to resign the APK with the V3 + // signature scheme, and the V3 signer in the signature block is the same + // as the engine is configured to use. + if (!mV3SigningEnabled) { + throw new IllegalStateException( + "Preserving an existing V3 signature is not supported"); + } + List, byte[]>> v3Signers = + ApkSigningBlockUtils.getApkSignatureBlockSigners( + signatureBlock.getFirst()); + if (v3Signers.size() > 1) { + throw new IllegalArgumentException( + "The provided APK signing block contains " + v3Signers.size() + + " V3 signers; the V3 signature scheme only supports" + + " one signer"); + } + // If there is only a single V3 signer then ensure it is the signer + // configured to sign the APK. + if (v3Signers.size() == 1 + && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) { + throw new IllegalStateException( + "The V3 signature scheme only supports one signer; a request " + + "was made to preserve the existing V3 signature, " + + "but the engine is configured to sign with a " + + "different signer"); + } + } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains( + signatureBlock.getSecond())) { + mPreservedSignatureBlocks.add(signatureBlock); + } + } + } catch (ApkFormatException | CertificateException | IOException e) { + throw new IllegalArgumentException("Unable to parse the provided signing block", e); + } + // Signature scheme V3+ only support a single signer; if the engine is configured to + // sign with V3+ then ensure no scheme signature blocks have been preserved. + if (mV3SigningEnabled && schemeSignatureBlockPreserved) { + throw new IllegalStateException( + "Signature scheme V3+ only supports a single signer and cannot be " + + "appended to the existing signature scheme blocks"); + } return; } - // TODO: Preserve blocks other than APK Signature Scheme v2 blocks. + } + + /** + * Returns whether the engine is configured to sign the APK with a signer using the specified + * {@code signerCerts}. + */ + private boolean isConfiguredWithSigner(List signerCerts) { + for (SignerConfig signerConfig : mSignerConfigs) { + if (signerCerts.containsAll(signerConfig.getCertificates())) { + return true; + } + } + return false; } @Override @@ -551,7 +747,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { case OUTPUT: return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT); case OUTPUT_BY_ENGINE: - if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) { // We copy the main section of the JAR manifest from input to output. Thus, this // invalidates v1 signature and we need to see the entry's data. mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); @@ -619,7 +815,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { // the entry's data is as output by the engine. invalidateV1Signature(); GetJarEntryDataRequest dataRequest; - if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) { dataRequest = new GetJarEntryDataRequest(entryName); mInputJarManifestEntryDataRequest = dataRequest; } else { @@ -674,7 +870,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { @Override public OutputJarSignatureRequest outputJarEntries() throws ApkFormatException, InvalidKeyException, SignatureException, - NoSuchAlgorithmException { + NoSuchAlgorithmException { checkNotClosed(); if (!mV1SignaturePending) { @@ -727,9 +923,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { if (isEligibleForSourceStamp()) { inputJarManifest = V1SchemeSigner.generateManifestFile( - mV1ContentDigestAlgorithm, - mOutputJarEntryDigests, - inputJarManifest) + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + inputJarManifest) .contents; } @@ -754,7 +950,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { V1SchemeSigner.generateManifestFile( mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest); byte[] emittedSignatureManifest = - mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME); + mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME); if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) { // Emitted v1 signature is no longer valid. try { @@ -853,6 +1049,13 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { List> signingSchemeBlocks = new ArrayList<>(); ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null; ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null; + // If the engine is configured to preserve previous signature blocks and any were found in + // the existing APK signing block then add them to the list to be used to generate the + // new APK signing block. + if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null + && !mPreservedSignatureBlocks.isEmpty()) { + signingSchemeBlocks.addAll(mPreservedSignatureBlocks); + } // create APK Signature Scheme V2 Signature if requested if (mV2SigningEnabled) { @@ -866,20 +1069,40 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { zipCentralDirectory, eocd, v2SignerConfigs, - mV3SigningEnabled); + mV3SigningEnabled, + mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null); signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock); } if (mV3SigningEnabled) { invalidateV3Signature(); List v3SignerConfigs = createV3SignerConfigs(apkSigningBlockPaddingSupported); + List v31SignerConfigs = processV31SignerConfigs( + v3SignerConfigs); + if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) { + ApkSigningBlockUtils.SigningSchemeBlockAndDigests + v31SigningSchemeBlockAndDigests = + new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd, + v31SignerConfigs) + .setRunnablesExecutor(mExecutor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) + .build() + .generateApkSignatureSchemeV3BlockAndDigests(); + signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock); + } + V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir, + zipCentralDirectory, eocd, v3SignerConfigs) + .setRunnablesExecutor(mExecutor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + if (v31SignerConfigs != null && !v31SignerConfigs.isEmpty()) { + // The V3.1 stripping protection writes the minimum SDK version from the targeted + // signers as an additional attribute in the V3.0 signing block. + int minSdkVersionForV31 = v31SignerConfigs.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(MIN_SDK_WITH_V31_SUPPORT); + builder.setMinSdkVersionForV31(minSdkVersionForV31); + } v3SigningSchemeBlockAndDigests = - V3SchemeSigner.generateApkSignatureSchemeV3Block( - mExecutor, - beforeCentralDir, - zipCentralDirectory, - eocd, - v3SignerConfigs); + builder.build().generateApkSignatureSchemeV3BlockAndDigests(); signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock); } if (isEligibleForSourceStamp()) { @@ -907,9 +1130,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { : null; byte[] jarManifest = V1SchemeSigner.generateManifestFile( - mV1ContentDigestAlgorithm, - mOutputJarEntryDigests, - inputJarManifest) + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + inputJarManifest) .contents; // The digest of the jar manifest does not need to be computed in chunks due to // the small size of the manifest. @@ -921,9 +1144,12 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { signatureSchemeDigestInfos.put( VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests); } - signingSchemeBlocks.add( - V2SourceStampSigner.generateSourceStampBlock( - sourceStampSignerConfig, signatureSchemeDigestInfos)); + V2SourceStampSigner v2SourceStampSigner = + new V2SourceStampSigner.Builder(sourceStampSignerConfig, + signatureSchemeDigestInfos) + .setSourceStampTimestampEnabled(mSourceStampTimestampEnabled) + .build(); + signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock()); } // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks @@ -951,7 +1177,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { throw new SignatureException("Missing V4 output file."); } try { - ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig(); + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile); } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) { if (ignoreFailures) { @@ -961,16 +1187,14 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - /** - * For external use only to generate V4 & tree separately. - */ + /** For external use only to generate V4 & tree separately. */ public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput) throws SignatureException { if (sigOutput == null) { throw new SignatureException("Missing V4 output streams."); } try { - ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig(); + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); Pair pair = V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig); pair.getFirst().writeTo(sigOutput); @@ -1146,9 +1370,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { mDebuggable = null; } - /** - * Returns the output policy for the provided input JAR entry. - */ + /** Returns the output policy for the provided input JAR entry. */ private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) { if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE; @@ -1216,9 +1438,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - /** - * JAR entry inspection request which obtain the entry's uncompressed data. - */ + /** JAR entry inspection request which obtain the entry's uncompressed data. */ private static class GetJarEntryDataRequest implements InspectJarEntryRequest { private final String mEntryName; private final Object mLock = new Object(); @@ -1284,9 +1504,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - /** - * JAR entry inspection request which obtains the digest of the entry's uncompressed data. - */ + /** JAR entry inspection request which obtains the digest of the entry's uncompressed data. */ private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest { private final String mEntryName; private final String mJcaDigestAlgorithm; @@ -1369,9 +1587,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - /** - * JAR entry inspection request which transparently satisfies multiple such requests. - */ + /** JAR entry inspection request which transparently satisfies multiple such requests. */ private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest { private final String mEntryName; private final InspectJarEntryRequest[] mRequests; @@ -1419,28 +1635,42 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { */ public static class SignerConfig { private final String mName; - private final PrivateKey mPrivateKey; + private final KeyConfig mKeyConfig; private final List mCertificates; + private final boolean mDeterministicDsaSigning; + private final int mMinSdkVersion; + private final boolean mSignerTargetsDevRelease; + private final SigningCertificateLineage mSigningCertificateLineage; - private SignerConfig( - String name, PrivateKey privateKey, List certificates) { - mName = name; - mPrivateKey = privateKey; - mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + private SignerConfig(Builder builder) { + mName = builder.mName; + mKeyConfig = builder.mKeyConfig; + mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates)); + mDeterministicDsaSigning = builder.mDeterministicDsaSigning; + mMinSdkVersion = builder.mMinSdkVersion; + mSignerTargetsDevRelease = builder.mSignerTargetsDevRelease; + mSigningCertificateLineage = builder.mSigningCertificateLineage; } - /** - * Returns the name of this signer. - */ + /** Returns the name of this signer. */ public String getName() { return mName; } /** * Returns the signing key of this signer. + * + * @deprecated Use {@link #getKeyConfig()} instead of accessing a {@link PrivateKey} + * directly. If the user of ApkSigner is signing with a KMS instead of JCA, this method + * will return null. */ + @Deprecated public PrivateKey getPrivateKey() { - return mPrivateKey; + return mKeyConfig.match(jca -> jca.privateKey, kms -> null); + } + + public KeyConfig getKeyConfig() { + return mKeyConfig; } /** @@ -1452,51 +1682,221 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } /** - * Builder of {@link SignerConfig} instances. + * If this signer is a DSA signer, whether or not the signing is done deterministically. */ + public boolean getDeterministicDsaSigning() { + return mDeterministicDsaSigning; + } + + /** Returns the minimum SDK version for which this signer should be used. */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** Returns whether this signer targets a development release. */ + public boolean getSignerTargetsDevRelease() { + return mSignerTargetsDevRelease; + } + + /** Returns the {@link SigningCertificateLineage} for this signer. */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + /** Builder of {@link SignerConfig} instances. */ public static class Builder { private final String mName; - private final PrivateKey mPrivateKey; + private final KeyConfig mKeyConfig; private final List mCertificates; + private final boolean mDeterministicDsaSigning; + private int mMinSdkVersion; + private boolean mSignerTargetsDevRelease; + private SigningCertificateLineage mSigningCertificateLineage; /** * Constructs a new {@code Builder}. * - * @param name signer's name. The name is reflected in the name of files comprising the - * JAR signature of the APK. - * @param privateKey signing key + * @deprecated use {@link #Builder(String, KeyConfig, List)} instead + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key * @param certificates list of one or more X.509 certificates. The subject public key of - * the first certificate must correspond to the {@code privateKey}. + * the first certificate must correspond to the {@code privateKey}. */ + @Deprecated public Builder(String name, PrivateKey privateKey, List certificates) { + this(name, privateKey, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @deprecated use {@link #Builder(String, KeyConfig, List, boolean)} instead + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic signing algorithm variant (RFC6979) should be used. + */ + @Deprecated + public Builder( + String name, + PrivateKey privateKey, + List certificates, + boolean deterministicDsaSigning) { if (name.isEmpty()) { throw new IllegalArgumentException("Empty name"); } mName = name; - mPrivateKey = privateKey; + mKeyConfig = new KeyConfig.Jca(privateKey); mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; } + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param keyConfig signing key configuration. + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder(String name, KeyConfig keyConfig, List certificates) { + this(name, keyConfig, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param keyConfig signing key configuration + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic signing algorithm variant (RFC6979) should be used. + */ + public Builder( + String name, + KeyConfig keyConfig, + List certificates, + boolean deterministicDsaSigning) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + mName = name; + mKeyConfig = keyConfig; + mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; + } + + /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */ + public Builder setMinSdkVersion(int minSdkVersion) { + return setLineageForMinSdkVersion(null, minSdkVersion); + } + + /** + * Sets the specified {@code minSdkVersion} as the minimum Android platform version + * (API level) for which the provided {@code lineage} (where applicable) should be used + * to produce the APK's signature. This method is useful if callers want to specify a + * particular rotated signer or lineage with restricted capabilities for later + * platform releases. + * + *

Note:>The V1 and V2 signature schemes do not support key rotation and + * signing lineages with capabilities; only an app's original signer(s) can be used for + * the V1 and V2 signature blocks. Because of this, only a value of {@code + * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was + * introduced can be specified. + * + *

Note:Due to limitations with platform targeting in the V3.0 signature + * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in + * the current {@code SignerConfig} being used in the V3.0 signing block and applied to + * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for + * subsequent {@code SignerConfig} instances). Because of this, only a single {@code + * SignerConfig} can be instantiated with a minimum SDK version <= 32. + * + * @param lineage the {@code SigningCertificateLineage} to target the specified {@code + * minSdkVersion} + * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig} + * should be used + * @return this {@code Builder} instance + * + * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the + * certificate provided in the constructor is not in the specified {@code lineage}. + */ + public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage, + int minSdkVersion) { + if (minSdkVersion < AndroidSdkVersion.P) { + throw new IllegalArgumentException( + "SDK targeted signing config is only supported with the V3 signature " + + "scheme on Android P (SDK version " + + AndroidSdkVersion.P + ") and later"); + } + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + minSdkVersion = AndroidSdkVersion.P; + } + mMinSdkVersion = minSdkVersion; + // If a lineage is provided, ensure the signing certificate for this signer is in + // the lineage; in the case of multiple signing certificates, the first is always + // used in the lineage. + if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) { + throw new IllegalArgumentException( + "The provided lineage does not contain the signing certificate, " + + mCertificates.get(0).getSubjectDN() + + ", for this SignerConfig"); + } + mSigningCertificateLineage = lineage; + return this; + } + + /** + * Sets whether this signer's min SDK version is intended to target a development + * release. + * + *

This is primarily required for a signer testing on a platform's development + * release; however, it is recommended that signer's use the latest development SDK + * version instead of explicitly specifying this boolean. This class will properly + * handle an SDK that is currently targeting a development release and will use the + * finalized SDK version on release. + */ + private Builder setSignerTargetsDevRelease(boolean signerTargetsDevRelease) { + if (signerTargetsDevRelease && mMinSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + throw new IllegalArgumentException( + "Rotation can only target a development release for signers targeting " + + MIN_SDK_WITH_V31_SUPPORT + " or later"); + } + mSignerTargetsDevRelease = signerTargetsDevRelease; + return this; + } + + /** * Returns a new {@code SignerConfig} instance configured based on the configuration of * this builder. */ public SignerConfig build() { - return new SignerConfig(mName, mPrivateKey, mCertificates); + return new SignerConfig(this); } } } - /** - * Builder of {@link DefaultApkSignerEngine} instances. - */ + /** Builder of {@link DefaultApkSignerEngine} instances. */ public static class Builder { - private final int mMinSdkVersion; private List mSignerConfigs; + private List mTargetedSignerConfigs; private SignerConfig mStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; + private boolean mSourceStampTimestampEnabled = true; + private final int mMinSdkVersion; + private boolean mV1SigningEnabled = true; private boolean mV2SigningEnabled = true; private boolean mV3SigningEnabled = true; + private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + private boolean mRotationTargetsDevRelease = false; private boolean mVerityEnabled = false; private boolean mDebuggableApkPermitted = true; private boolean mOtherSignersSignaturesPreserved; @@ -1517,11 +1917,11 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { * Constructs a new {@code Builder}. * * @param signerConfigs information about signers with which the APK will be signed. At - * least one signer configuration must be provided. + * least one signer configuration must be provided. * @param minSdkVersion API Level of the oldest Android platform on which the APK is - * supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code - * AndroidManifest.xml}. The higher the version, the stronger signing features will be - * enabled. + * supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code + * AndroidManifest.xml}. The higher the version, the stronger signing features will be + * enabled. */ public Builder(List signerConfigs, int minSdkVersion) { if (signerConfigs.isEmpty()) { @@ -1538,11 +1938,10 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } /** - * Returns a new {@code DefaultApkSignerEngine} instance configured based on the - * configuration of this builder. + * Sets the APK signature schemes that should be enabled based on the options provided by + * the caller. */ - public DefaultApkSignerEngine build() throws InvalidKeyException { - + private void setEnabledSignatureSchemes() { if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) { throw new IllegalStateException( "Builder configured to both enable and disable APK " @@ -1553,27 +1952,163 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } else if (mV3SigningExplicitlyEnabled) { mV3SigningEnabled = true; } + } - // make sure our signers are appropriately setup + /** + * Sets the SDK targeted signer configs based on the signing config and rotation options + * provided by the caller. + * + * @throws InvalidKeyException if a {@link SigningCertificateLineage} cannot be created + * from the provided options + */ + private void setTargetedSignerConfigs() throws InvalidKeyException { + // If the caller specified any SDK targeted signer configs, then the min SDK version + // should be set for those configs, all others should have a default 0 min SDK version. + mSignerConfigs.sort(((signerConfig1, signerConfig2) -> signerConfig1.getMinSdkVersion() + - signerConfig2.getMinSdkVersion())); + // With the signer configs sorted, find the first targeted signer config with a min + // SDK version > 0 to create the separate targeted signer configs. + mTargetedSignerConfigs = new ArrayList<>(); + for (int i = 0; i < mSignerConfigs.size(); i++) { + if (mSignerConfigs.get(i).getMinSdkVersion() > 0) { + mTargetedSignerConfigs = mSignerConfigs.subList(i, mSignerConfigs.size()); + mSignerConfigs = mSignerConfigs.subList(0, i); + break; + } + } + + // A lineage provided outside a targeted signing config is intended for the original + // rotation; sort the untargeted signing configs based on this lineage and create a new + // targeted signing config for the initial rotation. if (mSigningCertificateLineage != null) { + if (!mTargetedSignerConfigs.isEmpty()) { + // Only the initial rotation can use the rotation-min-sdk-version; all + // subsequent targeted rotations must use targeted signing configs. + int firstTargetedSdkVersion = mTargetedSignerConfigs.get(0).getMinSdkVersion(); + if (mRotationMinSdkVersion >= firstTargetedSdkVersion) { + throw new IllegalStateException( + "The rotation-min-sdk-version, " + mRotationMinSdkVersion + + ", must be less than the first targeted SDK version, " + + firstTargetedSdkVersion); + } + } try { mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs); - if (!mV3SigningEnabled && mSignerConfigs.size() > 1) { - - // this is a strange situation: we've provided a valid rotation history, but - // are only signing with v1/v2. blow up, since we don't know for sure with - // which signer the user intended to sign - throw new IllegalStateException( - "Provided multiple signers which are part of the" - + " SigningCertificateLineage, but not signing with APK" - + " Signature Scheme v3"); - } } catch (IllegalArgumentException e) { throw new IllegalStateException( "Provided signer configs do not match the " + "provided SigningCertificateLineage", e); } + // Get the last signer in the lineage, create a new targeted signer from it, + // and add it as a targeted signer config. + SignerConfig rotatedSignerConfig = mSignerConfigs.remove(mSignerConfigs.size() - 1); + SignerConfig.Builder rotatedConfigBuilder = + new SignerConfig.Builder( + rotatedSignerConfig.getName(), + rotatedSignerConfig.getKeyConfig(), + rotatedSignerConfig.getCertificates(), + rotatedSignerConfig.getDeterministicDsaSigning()); + rotatedConfigBuilder.setLineageForMinSdkVersion(mSigningCertificateLineage, + mRotationMinSdkVersion); + rotatedConfigBuilder.setSignerTargetsDevRelease(mRotationTargetsDevRelease); + mTargetedSignerConfigs.add(0, rotatedConfigBuilder.build()); + } + mSigningCertificateLineage = mergeTargetedSigningConfigLineages(); + } + + /** + * Merges and returns the lineages from any caller provided SDK targeted {@link + * SignerConfig} instances with an optional {@code lineage} specified as part of the general + * signing config. + * + *

If multiple signing configs target the same SDK version, or if any of the lineages + * cannot be merged, then an {@code IllegalStateException} is thrown. + */ + private SigningCertificateLineage mergeTargetedSigningConfigLineages() + throws InvalidKeyException { + SigningCertificateLineage mergedLineage = null; + int prevSdkVersion = 0; + for (SignerConfig signerConfig : mTargetedSignerConfigs) { + int signerMinSdkVersion = signerConfig.getMinSdkVersion(); + if (signerMinSdkVersion < AndroidSdkVersion.P) { + throw new IllegalStateException( + "Targeted signing config is not supported prior to SDK version " + + AndroidSdkVersion.P + "; received value " + + signerMinSdkVersion); + } + SigningCertificateLineage signerLineage = + signerConfig.getSigningCertificateLineage(); + // It is possible for a lineage to be null if the user is using one of the + // signers from the lineage as the only signer to target an SDK version; create + // a single element lineage to verify the signer is part of the merged lineage. + if (signerLineage == null) { + try { + signerLineage = + new SigningCertificateLineage.Builder( + new SigningCertificateLineage.SignerConfig.Builder( + signerConfig.mKeyConfig, + signerConfig.mCertificates.get(0)) + .build()) + .build(); + } catch (CertificateEncodingException + | NoSuchAlgorithmException + | SignatureException e) { + throw new IllegalStateException( + "Unable to create a SignerConfig for signer from certificate " + + signerConfig.mCertificates.get(0).getSubjectDN()); + } + } + // The V3.0 signature scheme does not support verified targeted SDK signing + // configs; if a signer is targeting any SDK version < T, then it will + // target P with the V3.0 signature scheme. + if (signerMinSdkVersion < AndroidSdkVersion.T) { + signerMinSdkVersion = AndroidSdkVersion.P; + } + // Ensure there are no SignerConfigs targeting the same SDK version. + if (signerMinSdkVersion == prevSdkVersion) { + throw new IllegalStateException( + "Multiple SignerConfigs were found targeting SDK version " + + signerMinSdkVersion); + } + // If multiple lineages have been provided, then verify each subsequent lineage + // is a valid descendant or ancestor of the previously merged lineages. + if (mergedLineage == null) { + mergedLineage = signerLineage; + } else { + try { + mergedLineage = mergedLineage.mergeLineageWith(signerLineage); + } catch (IllegalArgumentException e) { + throw new IllegalStateException( + "The provided lineage targeting SDK " + signerMinSdkVersion + + " is not in the signing history of the other targeted " + + "signing configs", e); + } + } + prevSdkVersion = signerMinSdkVersion; + } + return mergedLineage; + } + + /** + * Returns a new {@code DefaultApkSignerEngine} instance configured based on the + * configuration of this builder. + */ + public DefaultApkSignerEngine build() throws InvalidKeyException { + setEnabledSignatureSchemes(); + setTargetedSignerConfigs(); + + // make sure our signers are appropriately setup + if (mSigningCertificateLineage != null) { + if (!mV3SigningEnabled && mSignerConfigs.size() > 1) { + // this is a strange situation: we've provided a valid rotation history, but + // are only signing with v1/v2. blow up, since we don't know for sure with + // which signer the user intended to sign + throw new IllegalStateException( + "Provided multiple signers which are part of the" + + " SigningCertificateLineage, but not signing with APK" + + " Signature Scheme v3"); + } } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) { throw new IllegalStateException( "Multiple signing certificates provided for use with APK Signature Scheme" @@ -1582,7 +2117,10 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { return new DefaultApkSignerEngine( mSignerConfigs, + mTargetedSignerConfigs, mStampSignerConfig, + mSourceStampSigningCertificateLineage, + mSourceStampTimestampEnabled, mMinSdkVersion, mV1SigningEnabled, mV2SigningEnabled, @@ -1594,14 +2132,31 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { mSigningCertificateLineage); } - /** - * Sets the signer configuration for the SourceStamp to be embedded in the APK. - */ + /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */ public Builder setStampSignerConfig(SignerConfig stampSignerConfig) { mStampSignerConfig = stampSignerConfig; return this; } + /** + * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of + * signing certificate rotation for certificates previously used to sign source stamps. + */ + public Builder setSourceStampSigningCertificateLineage( + SigningCertificateLineage sourceStampSigningCertificateLineage) { + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + return this; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + /** * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). * @@ -1676,9 +2231,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { return this; } - /** - * Sets the value of the {@code Created-By} field in JAR signature files. - */ + /** Sets the value of the {@code Created-By} field in JAR signature files. */ public Builder setCreatedBy(String createdBy) { if (createdBy == null) { throw new NullPointerException(); @@ -1700,5 +2253,49 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } return this; } + + /** + * Sets the minimum Android platform version (API Level) for which an APK's rotated signing + * key should be used to produce the APK's signature. The original signing key for the APK + * will be used for all previous platform versions. If a rotated key with signing lineage is + * not provided then this method is a noop. + * + *

By default, if a signing lineage is specified with {@link + * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme + * V3.1 will be used to only apply the rotation on devices running Android T+. + * + *

Note:Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result + * in the original V3 signing block being used without platform targeting. + */ + public Builder setMinSdkVersionForRotation(int minSdkVersion) { + // If the provided SDK version does not support v3.1, then use the default SDK version + // with rotation support. + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT; + } else { + mRotationMinSdkVersion = minSdkVersion; + } + return this; + } + + /** + * Sets whether the rotation-min-sdk-version is intended to target a development release; + * this is primarily required after the T SDK is finalized, and an APK needs to target U + * during its development cycle for rotation. + * + *

This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + *

Note:If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + mRotationTargetsDevRelease = enabled; + return this; + } } } diff --git a/apksigner/src/main/java/com/android/apksig/Hints.java b/apksigner/src/main/java/com/android/apksig/Hints.java index 6adcfb5d..4070fa23 100644 --- a/apksigner/src/main/java/com/android/apksig/Hints.java +++ b/apksigner/src/main/java/com/android/apksig/Hints.java @@ -14,10 +14,9 @@ * limitations under the License. */ package com.android.apksig; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; import java.io.IOException; +import java.io.DataOutputStream; +import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; @@ -39,6 +38,49 @@ public final class Hints { return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE)); } + public static final class ByteRange { + final long start; + final long end; + + public ByteRange(long start, long end) { + this.start = start; + this.end = end; + } + } + + public static final class PatternWithRange { + final Pattern pattern; + final long offset; + final long size; + + public PatternWithRange(String pattern) { + this.pattern = Pattern.compile(pattern); + this.offset= 0; + this.size = Long.MAX_VALUE; + } + + public PatternWithRange(String pattern, long offset, long size) { + this.pattern = Pattern.compile(pattern); + this.offset = offset; + this.size = size; + } + + public Matcher matcher(CharSequence input) { + return this.pattern.matcher(input); + } + + public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) { + if (rangeIn.end - rangeIn.start < this.offset) { + return null; + } + long rangeOutStart = rangeIn.start + this.offset; + long rangeOutSize = Math.min(rangeIn.end - rangeOutStart, + this.size); + return new ByteRange(rangeOutStart, + rangeOutStart + rangeOutSize); + } + } + /** * Create a blob of bytes that PinnerService understands as a * sequence of byte ranges to pin. @@ -78,47 +120,4 @@ public final class Hints { } return pinPatterns; } - - public static final class ByteRange { - final long start; - final long end; - - public ByteRange(long start, long end) { - this.start = start; - this.end = end; - } - } - - public static final class PatternWithRange { - final Pattern pattern; - final long offset; - final long size; - - public PatternWithRange(String pattern) { - this.pattern = Pattern.compile(pattern); - this.offset = 0; - this.size = Long.MAX_VALUE; - } - - public PatternWithRange(String pattern, long offset, long size) { - this.pattern = Pattern.compile(pattern); - this.offset = offset; - this.size = size; - } - - public Matcher matcher(CharSequence input) { - return this.pattern.matcher(input); - } - - public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) { - if (rangeIn.end - rangeIn.start < this.offset) { - return null; - } - long rangeOutStart = rangeIn.start + this.offset; - long rangeOutSize = Math.min(rangeIn.end - rangeOutStart, - this.size); - return new ByteRange(rangeOutStart, - rangeOutStart + rangeOutSize); - } - } } diff --git a/apksigner/src/main/java/com/android/apksig/JcaSignerEngine.java b/apksigner/src/main/java/com/android/apksig/JcaSignerEngine.java new file mode 100644 index 00000000..ad6f1b51 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/JcaSignerEngine.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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 java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.AlgorithmParameterSpec; + +/** Cryptographic signing using standard Java Crypto Architecture (JCA) */ +public class JcaSignerEngine implements SignerEngine { + private final PrivateKey mPrivateKey; + private final String mSignatureAlgorithm; + private final AlgorithmParameterSpec mAlgorithmParameterSpec; + + public JcaSignerEngine( + PrivateKey privateKey, + String signatureAlgorithm, + AlgorithmParameterSpec algorithmParameterSpec) { + if (privateKey == null) { + throw new IllegalArgumentException("privateKey cannot be null"); + } + if (signatureAlgorithm == null) { + throw new IllegalArgumentException("signatureAlgorithm cannot be null"); + } + mPrivateKey = privateKey; + mSignatureAlgorithm = signatureAlgorithm; + mAlgorithmParameterSpec = algorithmParameterSpec; + } + + @Override + public byte[] sign(byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, SignatureException { + Signature signature = Signature.getInstance(mSignatureAlgorithm); + signature.initSign(mPrivateKey); + if (mAlgorithmParameterSpec != null) { + signature.setParameter(mAlgorithmParameterSpec); + } + signature.update(data); + return signature.sign(); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/KeyConfig.java b/apksigner/src/main/java/com/android/apksig/KeyConfig.java new file mode 100644 index 00000000..71179b82 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/KeyConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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 java.security.PrivateKey; +import java.util.function.Function; + +/** + * Represents the private key that will be used for signing, where that key could either be locally + * accessible or exposed only via a Key Management Service (KMS). + */ +public abstract class KeyConfig { + private KeyConfig() {} + + /** Helper function to perform some operation on a {@link KeyConfig} regardless of subtype. */ + public abstract T match(Function local, Function kms); + + /** + * For signing via Java Crypto Architecture (JCA). Simply wraps a {@link PrivateKey} that is + * accessible locally. + */ + public static class Jca extends KeyConfig { + public final PrivateKey privateKey; + + @Override + public T match(Function jca, Function kms) { + return jca.apply(this); + } + + public Jca(PrivateKey privateKey) { + this.privateKey = privateKey; + } + } + + /** For signing via a Key Management Service (KMS). */ + public static class Kms extends KeyConfig { + public final String kmsType; + public final String keyAlias; + + @Override + public T match(Function jca, Function kms) { + return kms.apply(this); + } + + public Kms(String kmsType, String keyAlias) { + this.kmsType = kmsType; + this.keyAlias = keyAlias; + } + } +} diff --git a/apksigner/src/main/java/com/android/apksig/SignerEngine.java b/apksigner/src/main/java/com/android/apksig/SignerEngine.java new file mode 100644 index 00000000..c69141c3 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/SignerEngine.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 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 java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +/** A basic abstraction for cryptographic signing. */ +public interface SignerEngine { + /** + * Cryptographically sign the input. + * + * @param data a blob of bytes to sign. + * @return the signed bytes. + */ + byte[] sign(byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, SignatureException; +} diff --git a/apksigner/src/main/java/com/android/apksig/SignerEngineFactory.java b/apksigner/src/main/java/com/android/apksig/SignerEngineFactory.java new file mode 100644 index 00000000..4301ac7f --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/SignerEngineFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 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 com.android.apksig.kms.KmsException; +import com.android.apksig.kms.KmsSignerEngineProvider; + +import java.security.spec.AlgorithmParameterSpec; +import java.util.Objects; +import java.util.ServiceLoader; + +/** Simple util to fetch a signer engine based on provided config values. */ +public class SignerEngineFactory { + private SignerEngineFactory() {} + + /** + * Retrieves an implementation based on the provided config. If keyConfig is a {@link + * KeyConfig.Kms}, signatureAlgorithm and signatureAlgorithmParameterSpec are ignored. + * + * @param keyConfig kms key type and alias, or a local private key. + * @param jcaSignatureAlgorithm which signature algorithm to use for signing. + * @param algorithmParameterSpec optional, any parameters needed by the signature alogrithm. + * @return a concrete {@link SignerEngine} implementation. + */ + public static SignerEngine getImplementation( + KeyConfig keyConfig, + String jcaSignatureAlgorithm, + AlgorithmParameterSpec algorithmParameterSpec) { + return keyConfig.match( + jca -> + new JcaSignerEngine( + jca.privateKey, jcaSignatureAlgorithm, algorithmParameterSpec), + kms -> getKmsImplementation(kms, jcaSignatureAlgorithm, algorithmParameterSpec)); + } + + private static SignerEngine getKmsImplementation( + KeyConfig.Kms keyConfig, + String jcaSignatureAlgorithm, + AlgorithmParameterSpec algorithmParameterSpec) { + ServiceLoader providers = + ServiceLoader.load(KmsSignerEngineProvider.class); + for (KmsSignerEngineProvider provider : providers) { + if (Objects.equals(provider.getKmsType(), keyConfig.kmsType)) { + return provider.getInstance( + keyConfig, jcaSignatureAlgorithm, algorithmParameterSpec); + } + } + + throw new KmsException( + keyConfig.kmsType, "No SignerEngine implementation found on the classpath"); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/SigningCertificateLineage.java b/apksigner/src/main/java/com/android/apksig/SigningCertificateLineage.java index 871538b5..1af8fd4b 100644 --- a/apksigner/src/main/java/com/android/apksig/SigningCertificateLineage.java +++ b/apksigner/src/main/java/com/android/apksig/SigningCertificateLineage.java @@ -23,6 +23,7 @@ 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.V3SchemeConstants; 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; @@ -76,19 +77,13 @@ public class SigningCertificateLineage { private static final int CURRENT_VERSION = FIRST_VERSION; - /** - * accept data from already installed pkg with this cert - */ + /** 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 - */ + /** 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 - */ + /** grant SIGNATURE permissions to pkgs with this cert */ private static final int PAST_CERT_PERMISSION = 4; /** @@ -117,6 +112,16 @@ public class SigningCertificateLineage { mSigningLineage = list; } + /** + * Creates a {@code SigningCertificateLineage} with a single signer in the lineage. + */ + private static SigningCertificateLineage createSigningLineage(int minSdkVersion, + SignerConfig signer, SignerCapabilities capabilities) { + SigningCertificateLineage signingCertificateLineage = new SigningCertificateLineage( + minSdkVersion, new ArrayList<>()); + return signingCertificateLineage.spawnFirstDescendant(signer, capabilities); + } + private static SigningCertificateLineage createSigningLineage( int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities, SignerConfig child, SignerCapabilities childCapabilities) @@ -129,6 +134,11 @@ public class SigningCertificateLineage { return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities); } + public static SigningCertificateLineage readFromBytes(byte[] lineageBytes) + throws IOException { + return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes))); + } + public static SigningCertificateLineage readFromFile(File file) throws IOException { if (file == null) { @@ -152,11 +162,10 @@ public class SigningCertificateLineage { * Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute. * * - * 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 readFromApkFile to handle this case. + * 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 readFromApkFile to handle this case. * - * * @param attrValue */ public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue) @@ -165,7 +174,7 @@ public class SigningCertificateLineage { V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap( attrValue).order(ByteOrder.LITTLE_ENDIAN)); int minSdkVersion = calculateMinSdkVersion(parsedLineage); - return new SigningCertificateLineage(minSdkVersion, parsedLineage); + return new SigningCertificateLineage(minSdkVersion, parsedLineage); } /** @@ -173,7 +182,7 @@ public class SigningCertificateLineage { * 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. + * or if the V3 signature block does not contain a valid lineage. */ public static SigningCertificateLineage readFromApkFile(File apkFile) throws IOException, ApkFormatException { @@ -184,49 +193,105 @@ public class SigningCertificateLineage { } /** - * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 - * signature block of the provided APK DataSource. + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 and + * V3.1 signature blocks 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. + * @throws IllegalArgumentException if the provided APK does not contain a V3 nor V3.1 + * signature block, or if the V3 and V3.1 signature blocks do not contain a valid lineage. */ + public static SigningCertificateLineage readFromApkDataSource(DataSource apk) throws IOException, ApkFormatException { - SignatureInfo signatureInfo; + return readFromApkDataSource(apk, /* readV31Lineage= */ true, /* readV3Lineage= */true); + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3.1 + * signature blocks of the provided APK DataSource. + * + * @throws IllegalArgumentException if the provided APK does not contain a V3.1 signature block, + * or if the V3.1 signature block does not contain a valid lineage. + */ + + public static SigningCertificateLineage readV31FromApkDataSource(DataSource apk) + throws IOException, ApkFormatException { + return readFromApkDataSource(apk, /* readV31Lineage= */ true, + /* readV3Lineage= */ false); + } + + private static SigningCertificateLineage readFromApkDataSource( + DataSource apk, + boolean readV31Lineage, + boolean readV3Lineage) + throws IOException, ApkFormatException { + ApkUtils.ZipSections zipSections; 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); + zipSections = ApkUtils.findZipSections(apk); } 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 lineages = new ArrayList<>(1); - while (signers.hasRemaining()) { - ByteBuffer signer = getLengthPrefixedSlice(signers); - ByteBuffer signedData = getLengthPrefixedSlice(signer); + List signatureInfoList = new ArrayList<>(); + if (readV31Lineage) { 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. + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31); + signatureInfoList.add( + ApkSigningBlockUtils.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // This could be expected if there's only a V3 signature block. } } + if (readV3Lineage) { + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + signatureInfoList.add( + ApkSigningBlockUtils.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // This could be expected if the provided APK is not signed with the V3 signature + // scheme + } + } + if (signatureInfoList.isEmpty()) { + String message; + if (readV31Lineage && readV3Lineage) { + message = "The provided APK does not contain a valid V3 nor V3.1 signature block."; + } else if (readV31Lineage) { + message = "The provided APK does not contain a valid V3.1 signature block."; + } else if (readV3Lineage) { + message = "The provided APK does not contain a valid V3 signature block."; + } else { + message = "No signature blocks were requested."; + } + throw new IllegalArgumentException(message); + } + + List lineages = new ArrayList<>(1); + for (SignatureInfo signatureInfo : signatureInfoList) { + // 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); + 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( @@ -244,7 +309,7 @@ public class SigningCertificateLineage { * signed data portion of a signer in a V3 signature block. * * @throws IllegalArgumentException if the provided signed data does not contain a valid - * lineage. + * lineage. */ public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData) throws IOException, ApkFormatException { @@ -270,7 +335,7 @@ public class SigningCertificateLineage { while (additionalAttributes.hasRemaining()) { ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes); int id = attribute.getInt(); - if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) { + if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) { byte[] value = ByteBufferUtils.toByteArray(attribute); SigningCertificateLineage lineage = readFromV3AttributeValue(value); lineages.add(lineage); @@ -289,6 +354,154 @@ public class SigningCertificateLineage { return result; } + public byte[] getBytes() { + return write().array(); + } + + 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 certificates = new ArrayList<>(1); + certificates.add(parent.getCertificate()); + ApkSigningBlockUtils.SignerConfig newSignerConfig = + new ApkSigningBlockUtils.SignerConfig(); + newSignerConfig.keyConfig = parent.getKeyConfig(); + newSignerConfig.certificates = certificates; + newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm); + + // sign it + List> 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 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 algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, mMinSdkVersion, false /* verityEnabled */, + false /* deterministicDsaSigning */); + 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); @@ -341,201 +554,6 @@ public class SigningCertificateLineage { return minSdkVersion; } - private static int calculateDefaultFlags() { - return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION - | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH; - } - - /** - * 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. - * - * This does not verify that the largest lineage corresponds to the most recent supported - * platform version. That check requires is performed during v3 verification. - */ - public static SigningCertificateLineage consolidateLineages( - List 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 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 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); - } - - 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 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> 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 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 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 ByteBuffer write() { byte[] encodedLineage = V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); @@ -550,20 +568,8 @@ public class SigningCertificateLineage { 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 byte[] encodeSigningCertificateLineage() { + return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); } public List sortSignerConfigs( @@ -637,11 +643,23 @@ public class SigningCertificateLineage { if (config == null) { throw new NullPointerException("config == null"); } + updateSignerCapabilities(config.getCertificate(), capabilities); + } + + /** + * Updates the {@code capabilities} for the signer with the provided {@code certificate} in the + * lineage. 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(X509Certificate certificate, + SignerCapabilities capabilities) { + if (certificate == 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)) { + if (lineageNode.signingCert.equals(certificate)) { int flags = lineageNode.flags; SignerCapabilities newCapabilities = new SignerCapabilities.Builder( flags).setCallerConfiguredCapabilities(capabilities).build(); @@ -651,7 +669,7 @@ public class SigningCertificateLineage { } // the provided signer config was not found in the lineage - throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN() + throw new IllegalArgumentException("Certificate (" + certificate.getSubjectDN() + ") not found in the SigningCertificateLineage"); } @@ -696,7 +714,27 @@ public class SigningCertificateLineage { } /** - * Returns a new SigingCertificateLineage which terminates at the node corresponding to the + * Returns whether the provided {@code cert} is the latest signing certificate in the lineage. + * + *

This method will only compare the provided {@code cert} against the latest signing + * certificate in the lineage; if a certificate that is not in the lineage is provided, this + * method will return false. + */ + public boolean isCertificateLatestInLineage(X509Certificate cert) { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + return mSigningLineage.get(mSigningLineage.size() - 1).signingCert.equals(cert); + } + + private static int calculateDefaultFlags() { + return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION + | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH; + } + + /** + * Returns a new SigningCertificateLineage 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 @@ -704,6 +742,7 @@ public class SigningCertificateLineage { * * @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) { @@ -721,14 +760,179 @@ public class SigningCertificateLineage { throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage"); } + /** + * Consolidates all of the lineages found in an APK into one lineage. In so doing, it also + * checks that all of the lineages are contained in one common lineage. + * + * 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) should be present in the most recent, either directly or via a sublineage + * that would allow the earlier lineages to merge with the most recent. + * + * This does not verify that the largest lineage corresponds to the most recent supported + * platform version. That check is performed during v3 verification. + */ + public static SigningCertificateLineage consolidateLineages( + List lineages) { + if (lineages == null || lineages.isEmpty()) { + return null; + } + SigningCertificateLineage consolidatedLineage = lineages.get(0); + for (int i = 1; i < lineages.size(); i++) { + consolidatedLineage = consolidatedLineage.mergeLineageWith(lineages.get(i)); + } + return consolidatedLineage; + } + + /** + * Merges this lineage with the provided {@code otherLineage}. + * + *

The merged lineage does not currently handle merging capabilities of common signers and + * should only be used to determine the full signing history of a collection of lineages. + */ + public SigningCertificateLineage mergeLineageWith(SigningCertificateLineage otherLineage) { + // Determine the ancestor and descendant lineages; if the original signer is in the other + // lineage, then it is considered a descendant. + SigningCertificateLineage ancestorLineage; + SigningCertificateLineage descendantLineage; + X509Certificate signerCert = mSigningLineage.get(0).signingCert; + if (otherLineage.isCertificateInLineage(signerCert)) { + descendantLineage = this; + ancestorLineage = otherLineage; + } else { + descendantLineage = otherLineage; + ancestorLineage = this; + } + + int ancestorIndex = 0; + int descendantIndex = 0; + SigningCertificateNode ancestorNode; + SigningCertificateNode descendantNode = descendantLineage.mSigningLineage.get( + descendantIndex++); + List mergedLineage = new ArrayList<>(); + // Iterate through the ancestor lineage and add the current node to the resulting lineage + // until the first node of the descendant is found. + while (ancestorIndex < ancestorLineage.size()) { + ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++); + if (ancestorNode.signingCert.equals(descendantNode.signingCert)) { + break; + } + mergedLineage.add(ancestorNode); + } + // If all of the nodes in the ancestor lineage have been added to the merged lineage, then + // there is no overlap between this and the provided lineage. + if (ancestorIndex == mergedLineage.size()) { + throw new IllegalArgumentException( + "The provided lineage is not a descendant or an ancestor of this lineage"); + } + // The descendant lineage's first node was in the ancestor's lineage above; add it to the + // merged lineage. + mergedLineage.add(descendantNode); + while (ancestorIndex < ancestorLineage.size() + && descendantIndex < descendantLineage.size()) { + ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++); + descendantNode = descendantLineage.mSigningLineage.get(descendantIndex++); + if (!ancestorNode.signingCert.equals(descendantNode.signingCert)) { + throw new IllegalArgumentException( + "The provided lineage diverges from this lineage"); + } + mergedLineage.add(descendantNode); + } + // At this point, one or both of the lineages have been exhausted and all signers to this + // point were a match between the two lineages; add any remaining elements from either + // lineage to the merged lineage. + while (ancestorIndex < ancestorLineage.size()) { + mergedLineage.add(ancestorLineage.mSigningLineage.get(ancestorIndex++)); + } + while (descendantIndex < descendantLineage.size()) { + mergedLineage.add(descendantLineage.mSigningLineage.get(descendantIndex++)); + } + return new SigningCertificateLineage(Math.min(mMinSdkVersion, otherLineage.mMinSdkVersion), + mergedLineage); + } + + /** + * Checks whether given lineages are compatible. Returns {@code true} if an installed APK with + * the oldLineage could be updated with an APK with the newLineage. + */ + public static boolean checkLineagesCompatibility( + SigningCertificateLineage oldLineage, SigningCertificateLineage newLineage) { + + final ArrayList oldCertificates = oldLineage == null ? + new ArrayList() + : new ArrayList(oldLineage.getCertificatesInLineage()); + final ArrayList newCertificates = newLineage == null ? + new ArrayList() + : new ArrayList(newLineage.getCertificatesInLineage()); + + if (oldCertificates.isEmpty()) { + return true; + } + if (newCertificates.isEmpty()) { + return false; + } + + // Both lineages contain exactly the same certificates or the new lineage extends + // the old one. The capabilities of particular certificates may have changed though but it + // does not matter in terms of current compatibility. + if (newCertificates.size() >= oldCertificates.size() + && newCertificates.subList(0, oldCertificates.size()).equals(oldCertificates)) { + return true; + } + + ArrayList newCertificatesArray = new ArrayList(newCertificates); + ArrayList oldCertificatesArray = new ArrayList(oldCertificates); + + int lastOldCertIndexInNew = newCertificatesArray.lastIndexOf( + oldCertificatesArray.get(oldCertificatesArray.size()-1)); + + // The new lineage trims some nodes from the beginning of the old lineage and possibly + // extends it at the end. The new lineage must contain the old signing certificate and + // the nodes up until the node with signing certificate must be in the same order. + // Good example 1: + // old: A -> B -> C + // new: B -> C -> D + // Good example 2: + // old: A -> B -> C + // new: C + // Bad example 1: + // old: A -> B -> C + // new: A -> C + // Bad example 1: + // old: A -> B + // new: C -> B + if (lastOldCertIndexInNew >= 0) { + return newCertificatesArray.subList(0, lastOldCertIndexInNew+1).equals( + oldCertificatesArray.subList( + oldCertificates.size()-1-lastOldCertIndexInNew, + oldCertificatesArray.size())); + } + + + // The new lineage can be shorter than the old one only if the last certificate of the new + // lineage exists in the old lineage and has a rollback capability there. + // Good example: + // old: A -> B_withRollbackCapability -> C + // new: A -> B + // Bad example 1: + // old: A -> B -> C + // new: A -> B + // Bad example 2: + // old: A -> B_withRollbackCapability -> C + // new: A -> B -> D + return oldCertificates.subList(0, newCertificates.size()).equals(newCertificates) + && oldLineage.getSignerCapabilities( + oldCertificates.get(newCertificates.size()-1)).hasRollback(); + } + /** * 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. + * 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. * *

Use {@link Builder} to obtain configuration instances. @@ -738,10 +942,6 @@ public class SigningCertificateLineage { private final int mCallerConfiguredFlags; - private SignerCapabilities(int flags) { - this(flags, 0); - } - private SignerCapabilities(int flags, int callerConfiguredFlags) { mFlags = flags; mCallerConfiguredFlags = callerConfiguredFlags; @@ -755,8 +955,17 @@ public class SigningCertificateLineage { * 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; + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof SignerCapabilities)) return false; + + return this.mFlags == ((SignerCapabilities) other).mFlags; + } + + @Override + public int hashCode() { + return 31 * mFlags; } /** @@ -935,26 +1144,33 @@ public class SigningCertificateLineage { } /** - * Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage} + * Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage} * *

Use {@link Builder} to obtain configuration instances. */ public static class SignerConfig { - private final PrivateKey mPrivateKey; + private final KeyConfig mKeyConfig; private final X509Certificate mCertificate; - private SignerConfig( - PrivateKey privateKey, - X509Certificate certificate) { - mPrivateKey = privateKey; + private SignerConfig(KeyConfig keyConfig, X509Certificate certificate) { + mKeyConfig = keyConfig; mCertificate = certificate; } /** * Returns the signing key of this signer. + * + * @deprecated Use {@link #getKeyConfig()} instead of accessing a {@link PrivateKey} + * directly. If the user of ApkSigner is signing with a KMS instead of JCA, this method + * will return null. */ + @Deprecated public PrivateKey getPrivateKey() { - return mPrivateKey; + return mKeyConfig.match(jca -> jca.privateKey, kms -> null); + } + + public KeyConfig getKeyConfig() { + return mKeyConfig; } /** @@ -969,20 +1185,32 @@ public class SigningCertificateLineage { * Builder of {@link SignerConfig} instances. */ public static class Builder { - private final PrivateKey mPrivateKey; + private final KeyConfig mKeyConfig; 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}. + * @deprecated use {@link #Builder(KeyConfig, X509Certificate)} instead + * @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; + @Deprecated + public Builder(PrivateKey privateKey, X509Certificate certificate) { + mKeyConfig = new KeyConfig.Jca(privateKey); + mCertificate = certificate; + } + + /** + * Constructs a new {@code Builder}. + * + * @param keyConfig signing key configuration + * @param certificate the X.509 certificate with a subject public key of the {@code + * privateKey}. + */ + public Builder(KeyConfig keyConfig, X509Certificate certificate) { + mKeyConfig = keyConfig; mCertificate = certificate; } @@ -991,9 +1219,7 @@ public class SigningCertificateLineage { * this builder. */ public SignerConfig build() { - return new SignerConfig( - mPrivateKey, - mCertificate); + return new SignerConfig(mKeyConfig, mCertificate); } } } @@ -1007,13 +1233,12 @@ public class SigningCertificateLineage { 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 + * @param newSignerConfig new signer in the lineage; the new signing key that the APK will + * use */ public Builder( SignerConfig originalSignerConfig, @@ -1026,6 +1251,21 @@ public class SigningCertificateLineage { mNewSignerConfig = newSignerConfig; } + /** + * Constructs a new {@code Builder} that is intended to create a {@code + * SigningCertificateLineage} with a single signer in the signing history. + * + * @param originalSignerConfig first signer in this lineage + */ + public Builder(SignerConfig originalSignerConfig) { + if (originalSignerConfig == null) { + throw new NullPointerException("Can't pass null SignerConfigs when constructing a " + + "new SigningCertificateLineage"); + } + mOriginalSignerConfig = originalSignerConfig; + mNewSignerConfig = null; + } + /** * 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 @@ -1081,6 +1321,11 @@ public class SigningCertificateLineage { mOriginalCapabilities = new SignerCapabilities.Builder().build(); } + if (mNewSignerConfig == null) { + return createSigningLineage(mMinSdkVersion, mOriginalSignerConfig, + mOriginalCapabilities); + } + if (mNewCapabilities == null) { mNewCapabilities = new SignerCapabilities.Builder().build(); } diff --git a/apksigner/src/main/java/com/android/apksig/SourceStampVerifier.java b/apksigner/src/main/java/com/android/apksig/SourceStampVerifier.java new file mode 100644 index 00000000..39e1f48e --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/SourceStampVerifier.java @@ -0,0 +1,1012 @@ +/* + * Copyright (C) 2020 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.Constants.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V31; +import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtilsLite; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * APK source stamp verifier intended only to verify the validity of the stamp signature. + * + *

Note, this verifier does not validate the signatures of the jar signing / APK signature blocks + * when obtaining the digests for verification. This verifier should only be used in cases where + * another mechanism has already been used to verify the APK signatures. + */ +public class SourceStampVerifier { + private final File mApkFile; + private final DataSource mApkDataSource; + + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + + private SourceStampVerifier( + File apkFile, + DataSource apkDataSource, + int minSdkVersion, + int maxSdkVersion) { + mApkFile = apkFile; + mApkDataSource = apkDataSource; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Verifies the APK's source stamp signature and returns the result of the verification. + * + *

The APK's source stamp can be considered verified if the result's {@link + * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the + * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors + * can be obtained as follows: + *

    + *
  • Obtain the generic errors via {@link Result#getErrors()} + *
  • Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer + * query for any errors with {@link Result.SignerInfo#getErrors()} + *
  • Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer + * query for any errors with {@link Result.SignerInfo#getErrors()} + *
  • Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query + * for any stamp errors with {@link Result.SourceStampInfo#getErrors()} + *
+ */ + public SourceStampVerifier.Result verifySourceStamp() { + return verifySourceStamp(null); + } + + /** + * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of + * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result + * of the verification. + * + *

A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, + * if present, without verifying the actual source stamp certificate used to sign the source + * stamp. This can be used to verify an APK contains a properly signed source stamp without + * verifying a particular signer. + * + * @see #verifySourceStamp() + */ + public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verifySourceStamp(apk, expectedCertDigest); + } catch (IOException e) { + Result result = new Result(); + result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e); + return result; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Verifies the provided {@code apk}'s source stamp signature, including verification of the + * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and + * returns the result of the verification. + * + * @see #verifySourceStamp(String) + */ + private SourceStampVerifier.Result verifySourceStamp(DataSource apk, + String expectedCertDigest) { + Result result = new Result(); + try { + ZipSections zipSections = ApkUtilsLite.findZipSections(apk); + // Attempt to obtain the source stamp's certificate digest from the APK. + List cdRecords = + ZipUtils.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + + // If the source stamp's certificate digest is not available within the APK then the + // source stamp cannot be verified; check if a source stamp signing block is in the + // APK's signature block to determine the appropriate status to return. + if (sourceStampCdRecord == null) { + boolean stampSigningBlockFound; + try { + ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); + stampSigningBlockFound = true; + } catch (SignatureNotFoundException e) { + stampSigningBlockFound = false; + } + result.addVerificationError(stampSigningBlockFound + ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST + : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + return result; + } + + // Verify that the contents of the source stamp certificate digest match the expected + // value, if provided. + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + if (expectedCertDigest != null) { + String actualCertDigest = ApkSigningBlockUtilsLite.toHex( + sourceStampCertificateDigest); + if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { + result.addVerificationError( + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, + actualCertDigest, expectedCertDigest); + return result; + } + } + + Map> signatureSchemeApkContentDigests = + new HashMap<>(); + if (mMaxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V31, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V31, apkContentDigests); + } + } + + // Even though the specified SDK version range may only require the V3.1 signature + // scheme, the V3.0 signer should also be included since it's possible the source + // stamp does not include the V3.1 signer. + if (mMaxSdkVersion >= AndroidSdkVersion.P) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests); + } + } + + if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P || + signatureSchemeApkContentDigests.isEmpty())) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests); + } + } + + if (mMinSdkVersion < AndroidSdkVersion.N + || signatureSchemeApkContentDigests.isEmpty()) { + Map apkContentDigests = + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result); + signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, + apkContentDigests); + } + + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + mMinSdkVersion, + mMaxSdkVersion); + result.mergeFrom(sourceStampResult); + return result; + } catch (ApkFormatException | IOException | ZipFormatException e) { + result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e); + } catch (NoSuchAlgorithmException e) { + result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e); + } catch (SignatureNotFoundException e) { + result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING); + } + return result; + } + + /** + * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding + * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}. + * + *

This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + public void parseSigners( + ByteBuffer apkSignatureSchemeBlock, + int apkSigSchemeVersion, + Map apkContentDigests, + Result result) { + boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2; + // Both the V2 and V3 signature blocks contain the following: + // * length-prefixed sequence of length-prefixed signers + ByteBuffer signers; + try { + signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock); + } catch (ApkFormatException e) { + result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS + : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS); + return; + } + if (!signers.hasRemaining()) { + result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS + : ApkVerificationIssue.V3_SIG_NO_SIGNERS); + return; + } + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + while (signers.hasRemaining()) { + Result.SignerInfo signerInfo = new Result.SignerInfo(); + try { + ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers); + parseSigner( + signer, + apkSigSchemeVersion, + certFactory, + apkContentDigests, + signerInfo); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addVerificationWarning( + isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER + : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER); + return; + } finally { + // Signers are added here to ensure that only V3.1 signers that target the specified + // range are included. This block will execute after either block above, so even + // in the case of a return, the error will be added to the SignerInfo and that + // signer can then be returned to the caller to see the cause of the failure. + switch (apkSigSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + result.addV2Signer(signerInfo); + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + result.addV3Signer(signerInfo); + break; + case VERSION_APK_SIGNATURE_SCHEME_V31: + // The V3.1 scheme supports SDK targeted signing configs; only add this + // signer if it's within the verifier's SDK range. + if (signerInfo.getMaxSdkVersion() >= mMinSdkVersion + && signerInfo.getMinSdkVersion() <= mMaxSdkVersion) { + result.addV31Signer(signerInfo); + } + break; + } + } + } + } + + /** + * Parses the provided signer block and populates the {@code result}. + * + *

This verifies signatures over {@code signed-data} contained in this block but does not + * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this + * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the + * integrity of the APK. + * + *

This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + private void parseSigner( + ByteBuffer signerBlock, + int apkSigSchemeVersion, + CertificateFactory certFactory, + Map apkContentDigests, + Result.SignerInfo signerInfo) + throws ApkFormatException { + boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2; + // Both the V2 and V3 signer blocks contain the following: + // * length-prefixed signed data + // * length-prefixed sequence of length-prefixed digests: + // * uint32: signature algorithm ID + // * length-prefixed bytes: digest of contents + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock); + ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData); + ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData); + if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V31) { + // A V3+ signer block contains the following additional fields; while these were + // never verified with the V3.0 signature scheme, they are used for SDK targeted + // signing configs with the V3.1 scheme: + // * uint32: minSdkVersion + // * uint32: maxSdkVersion + int minSdkVersion = signedData.getInt(); + int maxSdkVersion = signedData.getInt(); + signerInfo.setMinSdkVersion(minSdkVersion); + signerInfo.setMaxSdkVersion(maxSdkVersion); + // If the current signer falls outside the SDK range for the verifier, return now before + // adding any content digests to the resulting Map. + if (maxSdkVersion < mMinSdkVersion || signerInfo.getMinSdkVersion() > mMaxSdkVersion) { + return; + } + } + + // Parse the digests block + while (digests.hasRemaining()) { + try { + ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests); + int sigAlgorithmId = digest.getInt(); + byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + continue; + } + apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST + : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST); + return; + } + } + + // Parse the certificates block + if (certificates.hasRemaining()) { + byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + } catch (CertificateException e) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE + : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE); + return; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert); + signerInfo.setSigningCertificate(certificate); + } + + if (signerInfo.getSigningCertificate() == null) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES + : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES); + return; + } + } + + /** + * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the + * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is + * returned. + * + *

If any errors are encountered while parsing the V1 signers the provided {@code result} + * will be updated to include a warning, but the source stamp verification can still proceed. + */ + private static Map getApkContentDigestFromV1SigningScheme( + List cdRecords, + DataSource apk, + ZipSections zipSections, + Result result) + throws IOException, ApkFormatException { + CentralDirectoryRecord manifestCdRecord = null; + List signatureBlockRecords = new ArrayList<>(1); + Map v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); + for (CentralDirectoryRecord cdRecord : cdRecords) { + String cdRecordName = cdRecord.getName(); + if (cdRecordName == null) { + continue; + } + if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) { + manifestCdRecord = cdRecord; + continue; + } + if (cdRecordName.startsWith("META-INF/") + && (cdRecordName.endsWith(".RSA") + || cdRecordName.endsWith(".DSA") + || cdRecordName.endsWith(".EC"))) { + signatureBlockRecords.add(cdRecord); + } + } + if (manifestCdRecord == null) { + // No JAR signing manifest file found. For SourceStamp verification, returning an empty + // digest is enough since this would affect the final digest signed by the stamp, and + // thus an empty digest will invalidate that signature. + return v1ContentDigest; + } + if (signatureBlockRecords.isEmpty()) { + result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES); + } else { + for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk, + signatureBlockRecord, zipSections.getZipCentralDirectoryOffset()); + for (Certificate certificate : certFactory.generateCertificates( + new ByteArrayInputStream(signatureBlockBytes))) { + // If multiple certificates are found within the signature block only the + // first is used as the signer of this block. + if (certificate instanceof X509Certificate) { + Result.SignerInfo signerInfo = new Result.SignerInfo(); + signerInfo.setSigningCertificate((X509Certificate) certificate); + result.addV1Signer(signerInfo); + break; + } + } + } catch (CertificateException e) { + // Log a warning for the parsing exception but still proceed with the stamp + // verification. + result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, + signatureBlockRecord.getName(), e); + break; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + } + try { + byte[] manifestBytes = + LocalFileRecord.getUncompressedData( + apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset()); + v1ContentDigest.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes)); + return v1ContentDigest; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + + /** + * Result of verifying the APK's source stamp signature; this signature can only be considered + * verified if {@link #isVerified()} returns true. + */ + public static class Result { + private final List mV1SchemeSigners = new ArrayList<>(); + private final List mV2SchemeSigners = new ArrayList<>(); + private final List mV3SchemeSigners = new ArrayList<>(); + private final List mV31SchemeSigners = new ArrayList<>(); + private final List> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners, + mV2SchemeSigners, mV3SchemeSigners, mV31SchemeSigners); + private SourceStampInfo mSourceStampInfo; + + private final List mErrors = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + + private boolean mVerified; + + void addVerificationError(int errorId, Object... params) { + mErrors.add(new ApkVerificationIssue(errorId, params)); + } + + void addVerificationWarning(int warningId, Object... params) { + mWarnings.add(new ApkVerificationIssue(warningId, params)); + } + + private void addV1Signer(SignerInfo signerInfo) { + mV1SchemeSigners.add(signerInfo); + } + + private void addV2Signer(SignerInfo signerInfo) { + mV2SchemeSigners.add(signerInfo); + } + + private void addV3Signer(SignerInfo signerInfo) { + mV3SchemeSigners.add(signerInfo); + } + + private void addV31Signer(SignerInfo signerInfo) { + mV31SchemeSigners.add(signerInfo); + } + + /** + * Returns {@code true} if the APK's source stamp signature + */ + public boolean isVerified() { + return mVerified; + } + + private void mergeFrom(ApkSigResult source) { + switch (source.signatureSchemeVersion) { + case Constants.VERSION_SOURCE_STAMP: + mVerified = source.verified; + if (!source.mSigners.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0)); + } + break; + default: + throw new IllegalArgumentException( + "Unknown ApkSigResult Signing Block Scheme Id " + + source.signatureSchemeVersion); + } + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the + * provided APK. + */ + public List getV1SchemeSigners() { + return mV1SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the + * provided APK. + */ + public List getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the + * provided APK. + */ + public List getV3SchemeSigners() { + return mV3SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V3.1 signers of + * the provided APK. + */ + public List getV31SchemeSigners() { + return mV31SchemeSigners; + } + + /** + * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the + * APK, or null if the source stamp signature verification failed before the stamp signature + * block could be fully parsed. + */ + public SourceStampInfo getSourceStampInfo() { + return mSourceStampInfo; + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. + * + *

Any error prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + for (List signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + } + if (mSourceStampInfo != null) { + if (mSourceStampInfo.containsErrors()) { + return true; + } + } + return false; + } + + /** + * Returns the errors encountered while verifying the APK's source stamp. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered while verifying the APK's source stamp. + */ + public List getWarnings() { + return mWarnings; + } + + /** + * Returns all errors for this result, including any errors from signature scheme signers + * and the source stamp. + */ + public List getAllErrors() { + List errors = new ArrayList<>(); + errors.addAll(mErrors); + + for (List signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + errors.addAll(signer.getErrors()); + } + } + if (mSourceStampInfo != null) { + errors.addAll(mSourceStampInfo.getErrors()); + } + return errors; + } + + /** + * Returns all warnings for this result, including any warnings from signature scheme + * signers and the source stamp. + */ + public List getAllWarnings() { + List warnings = new ArrayList<>(); + warnings.addAll(mWarnings); + + for (List signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + warnings.addAll(signer.getWarnings()); + } + } + if (mSourceStampInfo != null) { + warnings.addAll(mSourceStampInfo.getWarnings()); + } + return warnings; + } + + /** + * Contains information about an APK's signer and any errors encountered while parsing the + * corresponding signature block. + */ + public static class SignerInfo { + /** + * Value for the min and max SDK versions when the value could not be parsed or is not + * applicable; only the V3.1 signature scheme includes these fields. + */ + public static final int INVALID_SDK_VERSION = -1; + private X509Certificate mSigningCertificate; + private final List mErrors = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private int mMinSdkVersion = INVALID_SDK_VERSION; + private int mMaxSdkVersion = INVALID_SDK_VERSION; + + void setSigningCertificate(X509Certificate signingCertificate) { + mSigningCertificate = signingCertificate; + } + + void addVerificationError(int errorId, Object... params) { + mErrors.add(new ApkVerificationIssue(errorId, params)); + } + + void addVerificationWarning(int warningId, Object... params) { + mWarnings.add(new ApkVerificationIssue(warningId, params)); + } + + void setMinSdkVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + } + + void setMaxSdkVersion(int maxSdkVersion) { + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Returns the current signing certificate used by this signer. + */ + public X509Certificate getSigningCertificate() { + return mSigningCertificate; + } + + /** + * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors + * encountered during processing of this signer's signature block. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings + * encountered during processing of this signer's signature block. + */ + public List getWarnings() { + return mWarnings; + } + + /** + * Returns {@code true} if any errors were encountered while parsing this signer's + * signature block. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns the minSdkVersion for this signer or {@link #INVALID_SDK_VERSION} if the + * field is not available. This value is only applicable to V3.1 signers as this is the + * only signature scheme that supports SDK targeted signing configs. + */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** + * Returns the maxSdkVersion for this signer or {@link #INVALID_SDK_VERSION} if the + * field is not available. This value is only applicable to V3.1 signers as this is the + * only signature scheme that supports SDK targeted signing configs. + */ + public int getMaxSdkVersion() { + return mMaxSdkVersion; + } + } + + /** + * Contains information about an APK's source stamp and any errors encountered while + * parsing the stamp signature block. + */ + public static class SourceStampInfo { + private final List mCertificates; + private final List mCertificateLineage; + + private final List mErrors = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mInfoMessages = new ArrayList<>(); + + private final long mTimestamp; + + /* + * Since this utility is intended just to verify the source stamp, and the source stamp + * currently only logs warnings to prevent failing the APK signature verification, treat + * all warnings as errors. If the stamp verification is updated to log errors this + * should be set to false to ensure only errors trigger a failure verifying the source + * stamp. + */ + private static final boolean mWarningsAsErrors = true; + + private SourceStampInfo(ApkSignerInfo result) { + mCertificates = result.certs; + mCertificateLineage = result.certificateLineage; + mErrors.addAll(result.getErrors()); + mWarnings.addAll(result.getWarnings()); + mInfoMessages.addAll(result.getInfoMessages()); + mTimestamp = result.timestamp; + } + + /** + * Returns the SourceStamp's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the SourceStamp's public key. + */ + public X509Certificate getCertificate() { + return mCertificates.isEmpty() ? null : mCertificates.get(0); + } + + /** + * Returns a {@code List} of {@link X509Certificate} instances representing the source + * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List} + * if the stamp's signing certificate has not been rotated. + */ + public List getCertificatesInLineage() { + return mCertificateLineage; + } + + /** + * Returns whether any errors were encountered during the source stamp verification. + */ + public boolean containsErrors() { + return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty()); + } + + /** + * Returns {@code true} if any info messages were encountered during verification of + * this source stamp. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were + * encountered during source stamp verification. + */ + public List getErrors() { + if (!mWarningsAsErrors) { + return mErrors; + } + List result = new ArrayList<>(); + result.addAll(mErrors); + result.addAll(mWarnings); + return result; + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that + * were encountered during source stamp verification. + */ + public List getWarnings() { + return mWarnings; + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing info messages + * that were encountered during source stamp verification. + */ + public List getInfoMessages() { + return mInfoMessages; + } + + /** + * Returns the epoch timestamp in seconds representing the time this source stamp block + * was signed, or 0 if the timestamp is not available. + */ + public long getTimestampEpochSeconds() { + return mTimestamp; + } + } + } + + /** + * Builder of {@link SourceStampVerifier} instances. + * + *

The resulting verifier, by default, checks whether the APK's source stamp signature will + * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not + * queried to determine the APK's minimum supported level, so the caller should specify a lower + * bound with {@link #setMinCheckedPlatformVersion(int)}. + */ + public static class Builder { + private final File mApkFile; + private final DataSource mApkDataSource; + + private int mMinSdkVersion = 1; + private int mMaxSdkVersion = Integer.MAX_VALUE; + + /** + * Constructs a new {@code Builder} for source stamp verification of the provided {@code + * apk}. + */ + public Builder(File apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkFile = apk; + mApkDataSource = null; + } + + /** + * Constructs a new {@code Builder} for source stamp verification of the provided {@code + * apk}. + */ + public Builder(DataSource apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkDataSource = apk; + mApkFile = null; + } + + /** + * Sets the oldest Android platform version for which the APK's source stamp is verified. + * + *

APK source stamp verification will confirm that the APK's stamp is expected to verify + * on all Android platforms starting from the platform version with the provided {@code + * minSdkVersion}. The upper end of the platform versions range can be modified via + * {@link #setMaxCheckedPlatformVersion(int)}. + * + * @param minSdkVersion API Level of the oldest platform for which to verify the APK + */ + public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets the newest Android platform version for which the APK's source stamp is verified. + * + *

APK source stamp verification will confirm that the APK's stamp is expected to verify + * on all platform versions up to and including the proviced {@code maxSdkVersion}. The + * lower end of the platform versions range can be modified via {@link + * #setMinCheckedPlatformVersion(int)}. + * + * @param maxSdkVersion API Level of the newest platform for which to verify the APK + * @see #setMinCheckedPlatformVersion(int) + */ + public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { + mMaxSdkVersion = maxSdkVersion; + return this; + } + + /** + * Returns a {@link SourceStampVerifier} initialized according to the configuration of this + * builder. + */ + public SourceStampVerifier build() { + return new SourceStampVerifier( + mApkFile, + mApkDataSource, + mMinSdkVersion, + mMaxSdkVersion); + } + } +} diff --git a/apksigner/src/main/java/com/android/apksig/apk/ApkUtils.java b/apksigner/src/main/java/com/android/apksig/apk/ApkUtils.java index c5a315d6..1a0db198 100644 --- a/apksigner/src/main/java/com/android/apksig/apk/ApkUtils.java +++ b/apksigner/src/main/java/com/android/apksig/apk/ApkUtils.java @@ -17,6 +17,7 @@ package com.android.apksig.apk; import com.android.apksig.internal.apk.AndroidBinXmlParser; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.CentralDirectoryRecord; @@ -28,8 +29,6 @@ import com.android.apksig.zip.ZipFormatException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -44,67 +43,42 @@ public abstract class ApkUtils { */ public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; - /** - * Name of the SourceStamp certificate hash ZIP entry in APKs. - */ - public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; - // See https://source.android.com/security/apksigning/v2.html - private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; - private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; - private static final int APK_SIG_BLOCK_MIN_SIZE = 32; - /** - * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. - */ - private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; - /** - * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml. - */ - private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; + /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ + public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = + SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; - private ApkUtils() { - } + private ApkUtils() {} /** * Finds the main ZIP sections of the provided APK. * - * @throws IOException if an I/O error occurred while reading the APK + * @throws IOException if an I/O error occurred while reading the APK * @throws ZipFormatException if the APK is malformed */ public static ZipSections findZipSections(DataSource apk) throws IOException, ZipFormatException { - Pair eocdAndOffsetInFile = - ZipUtils.findZipEndOfCentralDirectoryRecord(apk); - if (eocdAndOffsetInFile == null) { - throw new ZipFormatException("ZIP End of Central Directory record not found"); - } - - ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); - long eocdOffset = eocdAndOffsetInFile.getSecond(); - eocdBuf.order(ByteOrder.LITTLE_ENDIAN); - long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); - if (cdStartOffset > eocdOffset) { - throw new ZipFormatException( - "ZIP Central Directory start offset out of range: " + cdStartOffset - + ". ZIP End of Central Directory offset: " + eocdOffset); - } - - long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); - long cdEndOffset = cdStartOffset + cdSizeBytes; - if (cdEndOffset > eocdOffset) { - throw new ZipFormatException( - "ZIP Central Directory overlaps with End of Central Directory" - + ". CD end: " + cdEndOffset - + ", EoCD start: " + eocdOffset); - } - - int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); - + com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk); return new ZipSections( - cdStartOffset, - cdSizeBytes, - cdRecordCount, - eocdOffset, - eocdBuf); + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipCentralDirectorySizeBytes(), + zipSections.getZipCentralDirectoryRecordCount(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); + } + + /** + * Information about the ZIP sections of an APK. + */ + public static class ZipSections extends com.android.apksig.zip.ZipSections { + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount, + eocdOffset, eocd); + } } /** @@ -112,8 +86,8 @@ public abstract class ApkUtils { * Directory record. * * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record - * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must - * be between {@code 0} and {@code 2^32 - 1} inclusive. + * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must + * be between {@code 0} and {@code 2^32 - 1} inclusive. */ public static void setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset) { @@ -122,76 +96,74 @@ public abstract class ApkUtils { ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); } + /** + * Updates the length of EOCD comment. + * + * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record + */ + public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { + ByteBuffer eocd = zipEndOfCentralDirectory.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.updateZipEocdCommentLen(eocd); + } + + /** + * Returns the APK Signing Block of the provided {@code apk}. + * + * @throws ApkFormatException if the APK is not a valid ZIP archive + * @throws IOException if an I/O error occurs + * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK + * + * @see APK Signature Scheme v2 + * + */ + public static ApkSigningBlock findApkSigningBlock(DataSource apk) + throws ApkFormatException, IOException, ApkSigningBlockNotFoundException { + ApkUtils.ZipSections inputZipSections; + try { + inputZipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + return findApkSigningBlock(apk, inputZipSections); + } + /** * Returns the APK Signing Block of the provided APK. * - * @throws IOException if an I/O error occurs + * @throws IOException if an I/O error occurs * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK - * @see APK Signature Scheme v2 + * + * @see APK Signature Scheme v2 + * */ public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) throws IOException, ApkSigningBlockNotFoundException { - // FORMAT (see https://source.android.com/security/apksigning/v2.html): - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes payload - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic + ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk, + zipSections); + return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents()); + } - long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); - long centralDirEndOffset = - centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); - long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); - if (centralDirEndOffset != eocdStartOffset) { - throw new ApkSigningBlockNotFoundException( - "ZIP Central Directory is not immediately followed by End of Central Directory" - + ". CD end: " + centralDirEndOffset - + ", EoCD start: " + eocdStartOffset); + /** + * Information about the location of the APK Signing Block inside an APK. + */ + public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock { + /** + * Constructs a new {@code ApkSigningBlock}. + * + * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK + * Signing Block inside the APK file + * @param contents contents of the APK Signing Block + */ + public ApkSigningBlock(long startOffsetInApk, DataSource contents) { + super(startOffsetInApk, contents); } - - if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) { - throw new ApkSigningBlockNotFoundException( - "APK too small for APK Signing Block. ZIP Central Directory offset: " - + centralDirStartOffset); - } - // Read the magic and offset in file from the footer section of the block: - // * uint64: size of block - // * 16 bytes: magic - ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24); - footer.order(ByteOrder.LITTLE_ENDIAN); - if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) - || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { - throw new ApkSigningBlockNotFoundException( - "No APK Signing Block before ZIP Central Directory"); - } - // Read and compare size fields - long apkSigBlockSizeInFooter = footer.getLong(0); - if ((apkSigBlockSizeInFooter < footer.capacity()) - || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { - throw new ApkSigningBlockNotFoundException( - "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); - } - int totalSize = (int) (apkSigBlockSizeInFooter + 8); - long apkSigBlockOffset = centralDirStartOffset - totalSize; - if (apkSigBlockOffset < 0) { - throw new ApkSigningBlockNotFoundException( - "APK Signing Block offset out of range: " + apkSigBlockOffset); - } - ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8); - apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); - long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); - if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { - throw new ApkSigningBlockNotFoundException( - "APK Signing Block sizes in header and footer do not match: " - + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); - } - return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize)); } /** * Returns the contents of the APK's {@code AndroidManifest.xml}. * - * @throws IOException if an I/O error occurs while reading the APK + * @throws IOException if an I/O error occurs while reading the APK * @throws ApkFormatException if the APK is malformed */ public static ByteBuffer getAndroidManifest(DataSource apk) @@ -225,12 +197,47 @@ public abstract class ApkUtils { } } + /** + * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. + */ + private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; + + /** + * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml. + */ + private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; + + /** + * Android resource ID of the {@code android:targetSandboxVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; + + /** + * Android resource ID of the {@code android:targetSdkVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; + private static final String USES_SDK_ELEMENT_TAG = "uses-sdk"; + + /** + * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml. + */ + private static final int VERSION_CODE_ATTR_ID = 0x0101021b; + private static final String MANIFEST_ELEMENT_TAG = "manifest"; + + /** + * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml. + */ + private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576; + /** * Returns the lowest Android platform version (API Level) supported by an APK with the * provided {@code AndroidManifest.xml}. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format + * resource format + * * @throws MinSdkVersionException if an error occurred while determining the API Level */ public static int getMinSdkVersionFromBinaryAndroidManifest( @@ -294,6 +301,44 @@ public abstract class ApkUtils { } } + private static class CodenamesLazyInitializer { + + /** + * List of platform codename (first letter of) to API Level mappings. The list must be + * sorted by the first letter. For codenames not in the list, the assumption is that the API + * Level is incremented by one for every increase in the codename's first letter. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private static final Pair[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL = + new Pair[] { + Pair.of('C', 2), + Pair.of('D', 3), + Pair.of('E', 4), + Pair.of('F', 7), + Pair.of('G', 8), + Pair.of('H', 10), + Pair.of('I', 13), + Pair.of('J', 15), + Pair.of('K', 18), + Pair.of('L', 20), + Pair.of('M', 22), + Pair.of('N', 23), + Pair.of('O', 25), + }; + + private static final Comparator> CODENAME_FIRST_CHAR_COMPARATOR = + new ByFirstComparator(); + + private static class ByFirstComparator implements Comparator> { + @Override + public int compare(Pair o1, Pair o2) { + char c1 = o1.getFirst(); + char c2 = o2.getFirst(); + return c1 - c2; + } + } + } + /** * Returns the API Level corresponding to the provided platform codename. * @@ -308,6 +353,10 @@ public abstract class ApkUtils { * @throws CodenameMinSdkVersionException if the {@code codename} is not supported */ static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException { + if ("Baklava".equals(codename)) { + return 34; // VIC (35) was the version before Baklava, return VIC version minus one + } + char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0); // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now. // We only look at the first letter of the codename as this is the most important letter. @@ -328,7 +377,7 @@ public abstract class ApkUtils { // element at insertionIndex (if present) is greater than firstChar. int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length] if (insertionIndex == 0) { - // 'A' or 'B' -- never released to public + // 'A' or 'B' (not Baklava) -- never released to public return 1; } else { // The element at insertionIndex - 1 is the newest older codename. @@ -353,7 +402,8 @@ public abstract class ApkUtils { * See the {@code android:debuggable} attribute of the {@code application} element. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format + * resource format + * * @throws ApkFormatException if the manifest is malformed */ public static boolean getDebuggableFromBinaryAndroidManifest( @@ -429,7 +479,8 @@ public abstract class ApkUtils { * {@code manifest} element. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format + * resource format + * * @throws ApkFormatException if the manifest is malformed */ public static String getPackageNameFromBinaryAndroidManifest( @@ -468,150 +519,156 @@ public abstract class ApkUtils { } } - public static byte[] computeSha256DigestBytes(byte[] data) { - MessageDigest messageDigest; + /** + * Returns the security sandbox version targeted by an APK with the provided + * {@code AndroidManifest.xml}. + * + *

If the security sandbox version is not specified in the manifest a default value of 1 is + * returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + */ + public static int getTargetSandboxVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) { try { - messageDigest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-256 is not found", e); - } - messageDigest.update(data); - return messageDigest.digest(); - } - - /** - * Information about the ZIP sections of an APK. - */ - public static class ZipSections { - private final long mCentralDirectoryOffset; - private final long mCentralDirectorySizeBytes; - private final int mCentralDirectoryRecordCount; - private final long mEocdOffset; - private final ByteBuffer mEocd; - - public ZipSections( - long centralDirectoryOffset, - long centralDirectorySizeBytes, - int centralDirectoryRecordCount, - long eocdOffset, - ByteBuffer eocd) { - mCentralDirectoryOffset = centralDirectoryOffset; - mCentralDirectorySizeBytes = centralDirectorySizeBytes; - mCentralDirectoryRecordCount = centralDirectoryRecordCount; - mEocdOffset = eocdOffset; - mEocd = eocd; - } - - /** - * Returns the start offset of the ZIP Central Directory. This value is taken from the - * ZIP End of Central Directory record. - */ - public long getZipCentralDirectoryOffset() { - return mCentralDirectoryOffset; - } - - /** - * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the - * ZIP End of Central Directory record. - */ - public long getZipCentralDirectorySizeBytes() { - return mCentralDirectorySizeBytes; - } - - /** - * Returns the number of records in the ZIP Central Directory. This value is taken from the - * ZIP End of Central Directory record. - */ - public int getZipCentralDirectoryRecordCount() { - return mCentralDirectoryRecordCount; - } - - /** - * Returns the start offset of the ZIP End of Central Directory record. The record extends - * until the very end of the APK. - */ - public long getZipEndOfCentralDirectoryOffset() { - return mEocdOffset; - } - - /** - * Returns the contents of the ZIP End of Central Directory. - */ - public ByteBuffer getZipEndOfCentralDirectory() { - return mEocd; + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID); + } catch (ApkFormatException e) { + // An ApkFormatException indicates the target sandbox is not specified in the manifest; + // return a default value of 1. + return 1; } } /** - * Information about the location of the APK Signing Block inside an APK. + * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. + * + *

If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither + * value is specified then a value of 1 is returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format */ - public static class ApkSigningBlock { - private final long mStartOffsetInApk; - private final DataSource mContents; - - /** - * Constructs a new {@code ApkSigningBlock}. - * - * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK - * Signing Block inside the APK file - * @param contents contents of the APK Signing Block - */ - public ApkSigningBlock(long startOffsetInApk, DataSource contents) { - mStartOffsetInApk = startOffsetInApk; - mContents = contents; + public static int getTargetSdkVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) { + // If the targetSdkVersion is not specified then the platform will use the value of the + // minSdkVersion; if neither is specified then the platform will use a value of 1. + int minSdkVersion = 1; + try { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID); + } catch (ApkFormatException e) { + // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk + // element is not specified at all. } - - /** - * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block. - */ - public long getStartOffset() { - return mStartOffsetInApk; - } - - /** - * Returns the data source which provides the full contents of the APK Signing Block, - * including its footer. - */ - public DataSource getContents() { - return mContents; + androidManifestContents.rewind(); + try { + minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents); + } catch (ApkFormatException e) { + // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or + // the uses-sdk element is not specified at all. } + return minSdkVersion; } - private static class CodenamesLazyInitializer { + /** + * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}. + * + *

If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid + * integer an ApkFormatException is thrown. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * @throws ApkFormatException if an error occurred while determining the versionCode, or if the + * versionCode attribute value is not available. + */ + public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents) + throws ApkFormatException { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID); + } - /** - * List of platform codename (first letter of) to API Level mappings. The list must be - * sorted by the first letter. For codenames not in the list, the assumption is that the API - * Level is incremented by one for every increase in the codename's first letter. - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - private static final Pair[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL = - new Pair[]{ - Pair.of('C', 2), - Pair.of('D', 3), - Pair.of('E', 4), - Pair.of('F', 7), - Pair.of('G', 8), - Pair.of('H', 10), - Pair.of('I', 13), - Pair.of('J', 15), - Pair.of('K', 18), - Pair.of('L', 20), - Pair.of('M', 22), - Pair.of('N', 23), - Pair.of('O', 25), - }; + /** + * Returns the versionCode and versionCodeMajor of the APK according to its {@code + * AndroidManifest.xml} combined together as a single long value. + * + *

The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower + * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * @throws ApkFormatException if an error occurred while determining the version, or if the + * versionCode attribute value is not available. + */ + public static long getLongVersionCodeFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws ApkFormatException { + // If the versionCode is not found then allow the ApkFormatException to be thrown to notify + // the caller that the versionCode is not available. + int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents); + long versionCodeMajor = 0; + try { + androidManifestContents.rewind(); + versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID); + } catch (ApkFormatException e) { + // This is expected if the versionCodeMajor has not been defined for the APK; in this + // case the return value is just the versionCode. + } + return (versionCodeMajor << 32) | versionCode; + } - private static final Comparator> CODENAME_FIRST_CHAR_COMPARATOR = - new ByFirstComparator(); + /** + * Returns the integer value of the requested {@code attributeId} in the specified {@code + * elementName} from the provided {@code androidManifestContents} in binary Android resource + * format. + * + * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or + * if the requested attribute is not found. + */ + private static int getAttributeValueFromBinaryAndroidManifest( + ByteBuffer androidManifestContents, String elementName, int attributeId) + throws ApkFormatException { + if (elementName == null) { + throw new NullPointerException("elementName cannot be null"); + } + try { + AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); + int eventType = parser.getEventType(); + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) + && (elementName.equals(parser.getName()))) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeNameResourceId(i) == attributeId) { + int valueType = parser.getAttributeValueType(i); + switch (valueType) { + case AndroidBinXmlParser.VALUE_TYPE_INT: + case AndroidBinXmlParser.VALUE_TYPE_STRING: + return parser.getAttributeIntValue(i); + default: + throw new ApkFormatException( + "Unsupported value type, " + valueType + + ", for attribute " + String.format("0x%08X", + attributeId) + " under element " + elementName); - private static class ByFirstComparator implements Comparator> { - @Override - public int compare(Pair o1, Pair o2) { - char c1 = o1.getFirst(); - char c2 = o2.getFirst(); - return c1 - c2; + } + } + } + } + eventType = parser.next(); } + throw new ApkFormatException( + "Failed to determine APK's " + elementName + " attribute " + + String.format("0x%08X", attributeId) + " value"); + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new ApkFormatException( + "Unable to determine value for attribute " + String.format("0x%08X", + attributeId) + " under element " + elementName + + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } } + + public static byte[] computeSha256DigestBytes(byte[] data) { + return ApkUtilsLite.computeSha256DigestBytes(data); + } } diff --git a/apksigner/src/main/java/com/android/apksig/apk/ApkUtilsLite.java b/apksigner/src/main/java/com/android/apksig/apk/ApkUtilsLite.java new file mode 100644 index 00000000..13f23011 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/apk/ApkUtilsLite.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2020 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.apk; + +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Lightweight version of the ApkUtils for clients that only require a subset of the utility + * functionality. + */ +public class ApkUtilsLite { + private ApkUtilsLite() {} + + /** + * Finds the main ZIP sections of the provided APK. + * + * @throws IOException if an I/O error occurred while reading the APK + * @throws ZipFormatException if the APK is malformed + */ + public static ZipSections findZipSections(DataSource apk) + throws IOException, ZipFormatException { + Pair eocdAndOffsetInFile = + ZipUtils.findZipEndOfCentralDirectoryRecord(apk); + if (eocdAndOffsetInFile == null) { + throw new ZipFormatException("ZIP End of Central Directory record not found"); + } + + ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); + long eocdOffset = eocdAndOffsetInFile.getSecond(); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); + if (cdStartOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory start offset out of range: " + cdStartOffset + + ". ZIP End of Central Directory offset: " + eocdOffset); + } + + long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); + long cdEndOffset = cdStartOffset + cdSizeBytes; + if (cdEndOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + + ". CD end: " + cdEndOffset + + ", EoCD start: " + eocdOffset); + } + + int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); + + return new ZipSections( + cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocdBuf); + } + + // See https://source.android.com/security/apksigning/v2.html + private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; + private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; + private static final int APK_SIG_BLOCK_MIN_SIZE = 32; + + /** + * Returns the APK Signing Block of the provided APK. + * + * @throws IOException if an I/O error occurs + * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK + * + * @see APK Signature Scheme v2 + * + */ + public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) + throws IOException, ApkSigningBlockNotFoundException { + // FORMAT (see https://source.android.com/security/apksigning/v2.html): + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); + long centralDirEndOffset = + centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); + long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); + if (centralDirEndOffset != eocdStartOffset) { + throw new ApkSigningBlockNotFoundException( + "ZIP Central Directory is not immediately followed by End of Central Directory" + + ". CD end: " + centralDirEndOffset + + ", EoCD start: " + eocdStartOffset); + } + + if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) { + throw new ApkSigningBlockNotFoundException( + "APK too small for APK Signing Block. ZIP Central Directory offset: " + + centralDirStartOffset); + } + // Read the magic and offset in file from the footer section of the block: + // * uint64: size of block + // * 16 bytes: magic + ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24); + footer.order(ByteOrder.LITTLE_ENDIAN); + if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) + || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { + throw new ApkSigningBlockNotFoundException( + "No APK Signing Block before ZIP Central Directory"); + } + // Read and compare size fields + long apkSigBlockSizeInFooter = footer.getLong(0); + if ((apkSigBlockSizeInFooter < footer.capacity()) + || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + int totalSize = (int) (apkSigBlockSizeInFooter + 8); + long apkSigBlockOffset = centralDirStartOffset - totalSize; + if (apkSigBlockOffset < 0) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block offset out of range: " + apkSigBlockOffset); + } + ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } + return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize)); + } + + /** + * Information about the location of the APK Signing Block inside an APK. + */ + public static class ApkSigningBlock { + private final long mStartOffsetInApk; + private final DataSource mContents; + + /** + * Constructs a new {@code ApkSigningBlock}. + * + * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK + * Signing Block inside the APK file + * @param contents contents of the APK Signing Block + */ + public ApkSigningBlock(long startOffsetInApk, DataSource contents) { + mStartOffsetInApk = startOffsetInApk; + mContents = contents; + } + + /** + * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block. + */ + public long getStartOffset() { + return mStartOffsetInApk; + } + + /** + * Returns the data source which provides the full contents of the APK Signing Block, + * including its footer. + */ + public DataSource getContents() { + return mContents; + } + } + + public static byte[] computeSha256DigestBytes(byte[] data) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not found", e); + } + messageDigest.update(data); + return messageDigest.digest(); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java b/apksigner/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java index 2351350a..e30bc359 100644 --- a/apksigner/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java +++ b/apksigner/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java @@ -25,9 +25,7 @@ public class CodenameMinSdkVersionException extends MinSdkVersionException { private static final long serialVersionUID = 1L; - /** - * Encountered codename. - */ + /** Encountered codename. */ private final String mCodename; /** diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/apksigner/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java index aa36e244..bc5a4573 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java @@ -34,39 +34,25 @@ import java.util.Map; */ public class AndroidBinXmlParser { - /** - * Event: start of document. - */ + /** Event: start of document. */ public static final int EVENT_START_DOCUMENT = 1; - /** - * Event: end of document. - */ + /** Event: end of document. */ public static final int EVENT_END_DOCUMENT = 2; - /** - * Event: start of an element. - */ + /** Event: start of an element. */ public static final int EVENT_START_ELEMENT = 3; - /** - * Event: end of an document. - */ + /** Event: end of an document. */ public static final int EVENT_END_ELEMENT = 4; - /** - * Attribute value type is not supported by this parser. - */ + /** Attribute value type is not supported by this parser. */ public static final int VALUE_TYPE_UNSUPPORTED = 0; - /** - * Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. - */ + /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */ public static final int VALUE_TYPE_STRING = 1; - /** - * Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. - */ + /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */ public static final int VALUE_TYPE_INT = 2; /** @@ -74,9 +60,7 @@ public class AndroidBinXmlParser { */ public static final int VALUE_TYPE_REFERENCE = 3; - /** - * Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. - */ + /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */ public static final int VALUE_TYPE_BOOLEAN = 4; private static final long NO_NAMESPACE = 0xffffffffL; @@ -119,75 +103,6 @@ public class AndroidBinXmlParser { mXml = resXmlChunk.getContents(); } - /** - * Returns new byte buffer whose content is a shared subsequence of this buffer's content - * between the specified start (inclusive) and end (exclusive) positions. As opposed to - * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source - * buffer's byte order. - */ - private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) { - if (start < 0) { - throw new IllegalArgumentException("start: " + start); - } - if (end < start) { - throw new IllegalArgumentException("end < start: " + end + " < " + start); - } - int capacity = source.capacity(); - if (end > source.capacity()) { - throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); - } - return sliceFromTo(source, (int) start, (int) end); - } - - /** - * Returns new byte buffer whose content is a shared subsequence of this buffer's content - * between the specified start (inclusive) and end (exclusive) positions. As opposed to - * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source - * buffer's byte order. - */ - private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { - if (start < 0) { - throw new IllegalArgumentException("start: " + start); - } - if (end < start) { - throw new IllegalArgumentException("end < start: " + end + " < " + start); - } - int capacity = source.capacity(); - if (end > source.capacity()) { - throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); - } - int originalLimit = source.limit(); - int originalPosition = source.position(); - try { - source.position(0); - source.limit(end); - source.position(start); - ByteBuffer result = source.slice(); - result.order(source.order()); - return result; - } finally { - source.position(0); - source.limit(originalLimit); - source.position(originalPosition); - } - } - - private static int getUnsignedInt8(ByteBuffer buffer) { - return buffer.get() & 0xff; - } - - private static int getUnsignedInt16(ByteBuffer buffer) { - return buffer.getShort() & 0xffff; - } - - private static long getUnsignedInt32(ByteBuffer buffer) { - return buffer.getInt() & 0xffffffffL; - } - - private static long getUnsignedInt32(ByteBuffer buffer, int position) { - return buffer.getInt(position) & 0xffffffffL; - } - /** * Returns the depth of the current element. Outside of the root of the document the depth is * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and @@ -244,8 +159,8 @@ public class AndroidBinXmlParser { * element or {@code 0} if the name is not associated with a resource ID. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred */ public int getAttributeNameResourceId(int index) throws XmlParserException { return getAttribute(index).getNameResourceId(); @@ -255,8 +170,8 @@ public class AndroidBinXmlParser { * Returns the name of the specified attribute of the current element. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred */ public String getAttributeName(int index) throws XmlParserException { return getAttribute(index).getName(); @@ -267,8 +182,8 @@ public class AndroidBinXmlParser { * the attribute is not associated with a namespace. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred */ public String getAttributeNamespace(int index) throws XmlParserException { return getAttribute(index).getNamespace(); @@ -279,8 +194,8 @@ public class AndroidBinXmlParser { * {@code VALUE_TYPE_...} constants. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred */ public int getAttributeValueType(int index) throws XmlParserException { int type = getAttribute(index).getValueType(); @@ -304,8 +219,8 @@ public class AndroidBinXmlParser { * {@code VALUE_TYPE_...} constants. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event. - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event. + * @throws XmlParserException if a parsing error is occurred */ public int getAttributeIntValue(int index) throws XmlParserException { return getAttribute(index).getIntValue(); @@ -316,8 +231,8 @@ public class AndroidBinXmlParser { * {@code VALUE_TYPE_...} constants. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event. - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event. + * @throws XmlParserException if a parsing error is occurred */ public boolean getAttributeBooleanValue(int index) throws XmlParserException { return getAttribute(index).getBooleanValue(); @@ -328,8 +243,8 @@ public class AndroidBinXmlParser { * {@code VALUE_TYPE_...} constants. * * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a - * {@code start element} event. - * @throws XmlParserException if a parsing error is occurred + * {@code start element} event. + * @throws XmlParserException if a parsing error is occurred */ public String getAttributeStringValue(int index) throws XmlParserException { return getAttribute(index).getStringValue(); @@ -374,7 +289,8 @@ public class AndroidBinXmlParser { mStringPool = new StringPool(chunk); break; - case Chunk.RES_XML_TYPE_START_ELEMENT: { + case Chunk.RES_XML_TYPE_START_ELEMENT: + { if (mStringPool == null) { throw new XmlParserException( "Named element encountered before string pool"); @@ -395,12 +311,12 @@ public class AndroidBinXmlParser { if (attrStartOffset > contents.remaining()) { throw new XmlParserException( "Attributes start offset out of bounds: " + attrStartOffset - + ", max: " + contents.remaining()); + + ", max: " + contents.remaining()); } if (attrEndOffset > contents.remaining()) { throw new XmlParserException( "Attributes end offset out of bounds: " + attrEndOffset - + ", max: " + contents.remaining()); + + ", max: " + contents.remaining()); } mCurrentElementName = mStringPool.getString(nameId); @@ -417,7 +333,8 @@ public class AndroidBinXmlParser { return mCurrentEvent; } - case Chunk.RES_XML_TYPE_END_ELEMENT: { + case Chunk.RES_XML_TYPE_END_ELEMENT: + { if (mStringPool == null) { throw new XmlParserException( "Named element encountered before string pool"); @@ -591,6 +508,22 @@ public class AndroidBinXmlParser { mContents = contents; } + public ByteBuffer getContents() { + ByteBuffer result = mContents.slice(); + result.order(mContents.order()); + return result; + } + + public ByteBuffer getHeader() { + ByteBuffer result = mHeader.slice(); + result.order(mHeader.order()); + return result; + } + + public int getType() { + return mType; + } + /** * Consumes the chunk located at the current position of the input and returns the chunk * or {@code null} if there is no chunk left in the input. @@ -632,22 +565,6 @@ public class AndroidBinXmlParser { input.position((int) chunkEndPosition); return chunk; } - - public ByteBuffer getContents() { - ByteBuffer result = mContents.slice(); - result.order(mContents.order()); - return result; - } - - public ByteBuffer getHeader() { - ByteBuffer result = mHeader.slice(); - result.order(mHeader.order()); - return result; - } - - public int getType() { - return mType; - } } /** @@ -718,6 +635,40 @@ public class AndroidBinXmlParser { mChunkContents = contents; } + /** + * Returns the string located at the specified {@code 0}-based index in this pool. + * + * @throws XmlParserException if the string does not exist or cannot be decoded + */ + public String getString(long index) throws XmlParserException { + if (index < 0) { + throw new XmlParserException("Unsuported string index: " + index); + } else if (index >= mStringCount) { + throw new XmlParserException( + "Unsuported string index: " + index + ", max: " + (mStringCount - 1)); + } + + int idx = (int) index; + String result = mCachedStrings.get(idx); + if (result != null) { + return result; + } + + long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4); + if (offsetInStringsSection >= mStringsSection.capacity()) { + throw new XmlParserException( + "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection + + ", max: " + (mStringsSection.capacity() - 1)); + } + mStringsSection.position((int) offsetInStringsSection); + result = + (mUtf8Encoded) + ? getLengthPrefixedUtf8EncodedString(mStringsSection) + : getLengthPrefixedUtf16EncodedString(mStringsSection); + mCachedStrings.put(idx, result); + return result; + } + private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded) throws XmlParserException { // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16. @@ -796,40 +747,6 @@ public class AndroidBinXmlParser { throw new RuntimeException("UTF-8 character encoding not supported", e); } } - - /** - * Returns the string located at the specified {@code 0}-based index in this pool. - * - * @throws XmlParserException if the string does not exist or cannot be decoded - */ - public String getString(long index) throws XmlParserException { - if (index < 0) { - throw new XmlParserException("Unsuported string index: " + index); - } else if (index >= mStringCount) { - throw new XmlParserException( - "Unsuported string index: " + index + ", max: " + (mStringCount - 1)); - } - - int idx = (int) index; - String result = mCachedStrings.get(idx); - if (result != null) { - return result; - } - - long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4); - if (offsetInStringsSection >= mStringsSection.capacity()) { - throw new XmlParserException( - "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection - + ", max: " + (mStringsSection.capacity() - 1)); - } - mStringsSection.position((int) offsetInStringsSection); - result = - (mUtf8Encoded) - ? getLengthPrefixedUtf8EncodedString(mStringsSection) - : getLengthPrefixedUtf16EncodedString(mStringsSection); - mCachedStrings.put(idx, result); - return result; - } } /** @@ -849,7 +766,7 @@ public class AndroidBinXmlParser { mChunkContents = chunk.getContents().slice(); mChunkContents.order(chunk.getContents().order()); // Each entry of the map is four bytes long, containing the int32 resource ID. - mEntryCount = mChunkContents.remaining() / 4; + mEntryCount = mChunkContents.remaining() / 4; } /** @@ -866,6 +783,75 @@ public class AndroidBinXmlParser { } } + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + return sliceFromTo(source, (int) start, (int) end); + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + private static int getUnsignedInt8(ByteBuffer buffer) { + return buffer.get() & 0xff; + } + + private static int getUnsignedInt16(ByteBuffer buffer) { + return buffer.getShort() & 0xffff; + } + + private static long getUnsignedInt32(ByteBuffer buffer) { + return buffer.getInt() & 0xffffffffL; + } + + private static long getUnsignedInt32(ByteBuffer buffer, int position) { + return buffer.getInt(position) & 0xffffffffL; + } + /** * Indicates that an error occurred while parsing a document. */ diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java new file mode 100644 index 00000000..6151351b --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 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.internal.apk; + +import com.android.apksig.ApkVerificationIssue; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signature verification result. + */ +public class ApkSigResult { + public final int signatureSchemeVersion; + + /** Whether the APK's Signature Scheme signature verifies. */ + public boolean verified; + + public final List mSigners = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + public ApkSigResult(int signatureSchemeVersion) { + this.signatureSchemeVersion = signatureSchemeVersion; + } + + /** + * Returns {@code true} if this result encountered errors during verification. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!mSigners.isEmpty()) { + for (ApkSignerInfo signer : mSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + return false; + } + + /** + * Returns {@code true} if this result encountered warnings during verification. + */ + public boolean containsWarnings() { + if (!mWarnings.isEmpty()) { + return true; + } + if (!mSigners.isEmpty()) { + for (ApkSignerInfo signer : mSigners) { + if (signer.containsWarnings()) { + return true; + } + } + } + return false; + } + + /** + * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code + * issueId} and {@code params}. + */ + public void addError(int issueId, Object... parameters) { + mErrors.add(new ApkVerificationIssue(issueId, parameters)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code + * issueId} and {@code params}. + */ + public void addWarning(int issueId, Object... parameters) { + mWarnings.add(new ApkVerificationIssue(issueId, parameters)); + } + + /** + * Returns the errors encountered during verification. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered during verification. + */ + public List getWarnings() { + return mWarnings; + } +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java new file mode 100644 index 00000000..3e793419 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 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.internal.apk; + +import com.android.apksig.ApkVerificationIssue; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signer. + */ +public class ApkSignerInfo { + public int index; + public long timestamp; + public List certs = new ArrayList<>(); + public List certificateLineage = new ArrayList<>(); + + private final List mInfoMessages = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + /** + * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code + * issueId} and {@code params}. + */ + public void addError(int issueId, Object... params) { + mErrors.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code + * issueId} and {@code params}. + */ + public void addWarning(int issueId, Object... params) { + mWarnings.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the + * provided {@code issueId} and {@code params}. + */ + public void addInfoMessage(int issueId, Object... params) { + mInfoMessages.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Returns {@code true} if any errors were encountered during verification for this signer. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns {@code true} if any warnings were encountered during verification for this signer. + */ + public boolean containsWarnings() { + return !mWarnings.isEmpty(); + } + + /** + * Returns {@code true} if any info messages were encountered during verification of this + * signer. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + + /** + * Returns the errors encountered during verification for this signer. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered during verification for this signer. + */ + public List getWarnings() { + return mWarnings; + } + + /** + * Returns the info messages encountered during verification of this signer. + */ + public List getInfoMessages() { + return mInfoMessages; + } +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java index 92b9f908..bc9831d1 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,14 +16,16 @@ package com.android.apksig.internal.apk; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256; import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512; import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256; import com.android.apksig.ApkVerifier; +import com.android.apksig.KeyConfig; +import com.android.apksig.SignerEngineFactory; import com.android.apksig.SigningCertificateLineage; import com.android.apksig.apk.ApkFormatException; -import com.android.apksig.apk.ApkSigningBlockNotFoundException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.asn1.Asn1BerParser; import com.android.apksig.internal.asn1.Asn1DecodingException; @@ -41,9 +42,10 @@ import com.android.apksig.internal.pkcs7.SignerIdentifier; import com.android.apksig.internal.pkcs7.SignerInfo; import com.android.apksig.internal.util.ByteBufferDataSource; import com.android.apksig.internal.util.ChainedDataSource; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; import com.android.apksig.internal.util.Pair; -import com.android.apksig.internal.util.SupplierCompat; import com.android.apksig.internal.util.VerityTreeBuilder; +import com.android.apksig.internal.util.X509CertificateUtils; import com.android.apksig.internal.x509.RSAPublicKey; import com.android.apksig.internal.x509.SubjectPublicKeyInfo; import com.android.apksig.internal.zip.ZipUtils; @@ -55,7 +57,6 @@ import com.android.apksig.util.RunnablesExecutor; import java.io.IOException; import java.math.BigInteger; -import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.DigestException; @@ -69,6 +70,7 @@ import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; @@ -82,83 +84,39 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import javax.security.auth.x500.X500Principal; public class ApkSigningBlockUtils { + private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; + private static final byte[] APK_SIGNING_BLOCK_MAGIC = + new byte[] { + 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, + }; + public static final int VERITY_PADDING_BLOCK_ID = 0x42726577; + + private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS = + {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256}; + public static final int VERSION_SOURCE_STAMP = 0; public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; + public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31; public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; - private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); - private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; - private static final byte[] APK_SIGNING_BLOCK_MAGIC = - new byte[]{ - 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, - 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, - }; - private static final int VERITY_PADDING_BLOCK_ID = 0x42726577; - private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS = - {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256}; /** * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. */ public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { - ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); - ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); - return compareContentDigestAlgorithm(digestAlg1, digestAlg2); + return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2); } - /** - * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number - * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference. - */ - private static int compareContentDigestAlgorithm( - ContentDigestAlgorithm alg1, - ContentDigestAlgorithm alg2) { - switch (alg1) { - case CHUNKED_SHA256: - switch (alg2) { - case CHUNKED_SHA256: - return 0; - case CHUNKED_SHA512: - case VERITY_CHUNKED_SHA256: - return -1; - default: - throw new IllegalArgumentException("Unknown alg2: " + alg2); - } - case CHUNKED_SHA512: - switch (alg2) { - case CHUNKED_SHA256: - case VERITY_CHUNKED_SHA256: - return 1; - case CHUNKED_SHA512: - return 0; - default: - throw new IllegalArgumentException("Unknown alg2: " + alg2); - } - case VERITY_CHUNKED_SHA256: - switch (alg2) { - case CHUNKED_SHA256: - return 1; - case VERITY_CHUNKED_SHA256: - return 0; - case CHUNKED_SHA512: - return -1; - default: - throw new IllegalArgumentException("Unknown alg2: " + alg2); - } - default: - throw new IllegalArgumentException("Unknown alg1: " + alg1); - } - } - - /** * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the * APK and comparing them against the digests listed in APK Signing Block. The expected digests @@ -212,7 +170,7 @@ public class ApkSigningBlockUtils { if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { throw new RuntimeException( "APK Signing Block is not aligned on 4k boundary: " + - beforeApkSigningBlock.size()); + beforeApkSigningBlock.size()); } long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); @@ -220,7 +178,7 @@ public class ApkSigningBlockUtils { if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { throw new RuntimeException( "APK Signing Block size is not multiple of page size: " + - signingBlockSize); + signingBlockSize); } } } catch (DigestException e) { @@ -277,155 +235,27 @@ public class ApkSigningBlockUtils { ByteBuffer apkSigningBlock, int blockId, Result result) throws SignatureNotFoundException { - checkByteOrderLittleEndian(apkSigningBlock); - // FORMAT: - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes pairs - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); - - int entryCount = 0; - while (pairs.hasRemaining()) { - entryCount++; - if (pairs.remaining() < 8) { - throw new SignatureNotFoundException( - "Insufficient data to read size of APK Signing Block entry #" + entryCount); - } - long lenLong = pairs.getLong(); - if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { - throw new SignatureNotFoundException( - "APK Signing Block entry #" + entryCount - + " size out of range: " + lenLong); - } - int len = (int) lenLong; - int nextEntryPos = pairs.position() + len; - if (len > pairs.remaining()) { - throw new SignatureNotFoundException( - "APK Signing Block entry #" + entryCount + " size out of range: " + len - + ", available: " + pairs.remaining()); - } - int id = pairs.getInt(); - if (id == blockId) { - return getByteBuffer(pairs, len - 4); - } - pairs.position(nextEntryPos); + try { + return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId); + } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage()); } - - throw new SignatureNotFoundException( - "No APK Signature Scheme block in APK Signing Block with ID: " + blockId); } public static void checkByteOrderLittleEndian(ByteBuffer buffer) { - if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { - throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); - } - } - - /** - * Returns new byte buffer whose content is a shared subsequence of this buffer's content - * between the specified start (inclusive) and end (exclusive) positions. As opposed to - * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source - * buffer's byte order. - */ - private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { - if (start < 0) { - throw new IllegalArgumentException("start: " + start); - } - if (end < start) { - throw new IllegalArgumentException("end < start: " + end + " < " + start); - } - int capacity = source.capacity(); - if (end > source.capacity()) { - throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); - } - int originalLimit = source.limit(); - int originalPosition = source.position(); - try { - source.position(0); - source.limit(end); - source.position(start); - ByteBuffer result = source.slice(); - result.order(source.order()); - return result; - } finally { - source.position(0); - source.limit(originalLimit); - source.position(originalPosition); - } - } - - /** - * Relative get method for reading {@code size} number of bytes from the current - * position of this buffer. - * - *

This method reads the next {@code size} bytes at this buffer's current position, - * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to - * {@code size}, byte order set to this buffer's byte order; and then increments the position by - * {@code size}. - */ - private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { - if (size < 0) { - throw new IllegalArgumentException("size: " + size); - } - int originalLimit = source.limit(); - int position = source.position(); - int limit = position + size; - if ((limit < position) || (limit > originalLimit)) { - throw new BufferUnderflowException(); - } - source.limit(limit); - try { - ByteBuffer result = source.slice(); - result.order(source.order()); - source.position(limit); - return result; - } finally { - source.limit(originalLimit); - } + ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer); } public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { - if (source.remaining() < 4) { - throw new ApkFormatException( - "Remaining buffer too short to contain length of length-prefixed field" - + ". Remaining: " + source.remaining()); - } - int len = source.getInt(); - if (len < 0) { - throw new IllegalArgumentException("Negative length"); - } else if (len > source.remaining()) { - throw new ApkFormatException( - "Length-prefixed field longer than remaining buffer" - + ". Field length: " + len + ", remaining: " + source.remaining()); - } - return getByteBuffer(source, len); + return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source); } public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { - int len = buf.getInt(); - if (len < 0) { - throw new ApkFormatException("Negative length"); - } else if (len > buf.remaining()) { - throw new ApkFormatException( - "Underflow while reading length-prefixed value. Length: " + len - + ", available: " + buf.remaining()); - } - byte[] result = new byte[len]; - buf.get(result); - return result; + return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf); } public static String toHex(byte[] value) { - StringBuilder sb = new StringBuilder(value.length * 2); - int len = value.length; - for (int i = 0; i < len; i++) { - int hi = (value[i] & 0xff) >>> 4; - int lo = value[i] & 0x0f; - sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); - } - return sb.toString(); + return ApkSigningBlockUtilsLite.toHex(value); } public static Map computeContentDigests( @@ -445,7 +275,7 @@ public class ApkSigningBlockUtils { computeOneMbChunkContentDigests( executor, oneMbChunkBasedAlgorithm, - new DataSource[]{beforeCentralDir, centralDir, eocd}, + new DataSource[] { beforeCentralDir, centralDir, eocd }, contentDigests); if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) { @@ -590,9 +420,184 @@ public class ApkSigningBlockUtils { } } + private static class ChunkDigests { + private final ContentDigestAlgorithm algorithm; + private final int digestOutputSize; + private final byte[] concatOfDigestsOfChunks; + + private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) { + this.algorithm = algorithm; + digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes(); + concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize]; + + // Fill the initial values of the concatenated digests of chunks, which is + // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}. + concatOfDigestsOfChunks[0] = 0x5a; + setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1); + } + + private MessageDigest createMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm()); + } + + private int getOffset(int chunkIndex) { + return 1 + 4 + chunkIndex * digestOutputSize; + } + } + + /** + * A per-thread digest worker. + */ + private static class ChunkDigester implements Runnable { + private final ChunkSupplier dataSupplier; + private final List chunkDigests; + private final List messageDigests; + private final DataSink mdSink; + + private ChunkDigester(ChunkSupplier dataSupplier, List chunkDigests) { + this.dataSupplier = dataSupplier; + this.chunkDigests = chunkDigests; + messageDigests = new ArrayList<>(chunkDigests.size()); + for (ChunkDigests chunkDigest : chunkDigests) { + try { + messageDigests.add(chunkDigest.createMessageDigest()); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0])); + } + + @Override + public void run() { + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + + try { + for (ChunkSupplier.Chunk chunk = dataSupplier.get(); + chunk != null; + chunk = dataSupplier.get()) { + int size = chunk.size; + if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) { + throw new RuntimeException("Chunk size greater than expected: " + size); + } + + // First update with the chunk prefix. + setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1); + mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length); + + // Then update with the chunk data. + mdSink.consume(chunk.data); + + // Now finalize chunk for all algorithms. + for (int i = 0; i < chunkDigests.size(); i++) { + ChunkDigests chunkDigest = chunkDigests.get(i); + int actualDigestSize = messageDigests.get(i).digest( + chunkDigest.concatOfDigestsOfChunks, + chunkDigest.getOffset(chunk.chunkIndex), + chunkDigest.digestOutputSize); + if (actualDigestSize != chunkDigest.digestOutputSize) { + throw new RuntimeException( + "Unexpected output size of " + chunkDigest.algorithm + + " digest: " + actualDigestSize); + } + } + } + } catch (IOException | DigestException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a + * supplied {@link DataSource}, the data from the next {@link DataSource} + * are NOT concatenated. Only the next call to get() will fetch from the + * next {@link DataSource} in the input {@link DataSource} array. + */ + private static class ChunkSupplier implements Supplier { + private final DataSource[] dataSources; + private final int[] chunkCounts; + private final int totalChunkCount; + private final AtomicInteger nextIndex; + + private ChunkSupplier(DataSource[] dataSources) { + this.dataSources = dataSources; + chunkCounts = new int[dataSources.length]; + int totalChunkCount = 0; + for (int i = 0; i < dataSources.length; i++) { + long chunkCount = getChunkCount(dataSources[i].size(), + CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + if (chunkCount > Integer.MAX_VALUE) { + throw new RuntimeException( + String.format( + "Number of chunks in dataSource[%d] is greater than max int.", + i)); + } + chunkCounts[i] = (int)chunkCount; + totalChunkCount = (int) (totalChunkCount + chunkCount); + } + this.totalChunkCount = totalChunkCount; + nextIndex = new AtomicInteger(0); + } + + /** + * We map an integer index to the termination-adjusted dataSources 1MB chunks. + * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned + * blocks in each input {@link DataSource} (unless the DataSource itself is + * 1MB-aligned). + */ + @Override + public ChunkSupplier.Chunk get() { + int index = nextIndex.getAndIncrement(); + if (index < 0 || index >= totalChunkCount) { + return null; + } + + int dataSourceIndex = 0; + long dataSourceChunkOffset = index; + for (; dataSourceIndex < dataSources.length; dataSourceIndex++) { + if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) { + break; + } + dataSourceChunkOffset -= chunkCounts[dataSourceIndex]; + } + + long remainingSize = Math.min( + dataSources[dataSourceIndex].size() - + dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, + CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + + final int size = (int)remainingSize; + final ByteBuffer buffer = ByteBuffer.allocate(size); + try { + dataSources[dataSourceIndex].copyTo( + dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size, + buffer); + } catch (IOException e) { + throw new IllegalStateException("Failed to read chunk", e); + } + buffer.rewind(); + + return new Chunk(index, buffer, size); + } + + static class Chunk { + private final int chunkIndex; + private final ByteBuffer data; + private final int size; + + private Chunk(int chunkIndex, ByteBuffer data, int size) { + this.chunkIndex = chunkIndex; + this.data = data; + this.size = size; + } + } + } + @SuppressWarnings("ByteBufferBackingArray") private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir, - DataSource eocd, Map outputContentDigests) + DataSource eocd, Map outputContentDigests) throws IOException, NoSuchAlgorithmException { ByteBuffer encoded = createVerityDigestBuffer(true); // Use 0s as salt for now. This also needs to be consistent in the fsverify header for @@ -621,6 +626,19 @@ public class ApkSigningBlockUtils { return encoded; } + public static class VerityTreeAndDigest { + public final ContentDigestAlgorithm contentDigestAlgorithm; + public final byte[] rootHash; + public final byte[] tree; + + VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash, + byte[] tree) { + this.contentDigestAlgorithm = contentDigestAlgorithm; + this.rootHash = rootHash; + this.tree = tree; + } + } + @SuppressWarnings("ByteBufferBackingArray") public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource) throws IOException, NoSuchAlgorithmException { @@ -652,7 +670,8 @@ public class ApkSigningBlockUtils { if ("X.509".equals(publicKey.getFormat())) { encodedPublicKey = publicKey.getEncoded(); // if the key is an RSA key check for a negative modulus - if ("RSA".equals(publicKey.getAlgorithm())) { + String keyAlgorithm = publicKey.getAlgorithm(); + if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) { try { // Parse the encoded public key into the separate elements of the // SubjectPublicKeyInfo to obtain the SubjectPublicKey. @@ -752,25 +771,13 @@ public class ApkSigningBlockUtils { result.put(element); } return result.array(); - } + } public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( List> sequence) { - int resultSize = 0; - for (Pair element : sequence) { - resultSize += 12 + element.getSecond().length; - } - ByteBuffer result = ByteBuffer.allocate(resultSize); - result.order(ByteOrder.LITTLE_ENDIAN); - for (Pair element : sequence) { - byte[] second = element.getSecond(); - result.putInt(8 + second.length); - result.putInt(element.getFirst()); - result.putInt(second.length); - result.put(second); - } - return result.array(); - } + return ApkSigningBlockUtilsLite + .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence); + } /** * Returns the APK Signature Scheme block contained in the provided APK file for the given ID @@ -779,36 +786,18 @@ public class ApkSigningBlockUtils { * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 * block ID. + * * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme - * @throws IOException if an I/O error occurs while reading the APK + * @throws IOException if an I/O error occurs while reading the APK */ public static SignatureInfo findSignature( DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result) - throws IOException, SignatureNotFoundException { - // Find the APK Signing Block. - DataSource apkSigningBlock; - long apkSigningBlockOffset; + throws IOException, SignatureNotFoundException { try { - ApkUtils.ApkSigningBlock apkSigningBlockInfo = - ApkUtils.findApkSigningBlock(apk, zipSections); - apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); - apkSigningBlock = apkSigningBlockInfo.getContents(); - } catch (ApkSigningBlockNotFoundException e) { - throw new SignatureNotFoundException(e.getMessage(), e); + return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId); + } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage()); } - ByteBuffer apkSigningBlockBuf = - apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); - apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); - - // Find the APK Signature Scheme Block inside the APK Signing Block. - ByteBuffer apkSignatureSchemeBlock = - findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId, result); - return new SignatureInfo( - apkSignatureSchemeBlock, - apkSigningBlockOffset, - zipSections.getZipCentralDirectoryOffset(), - zipSections.getZipEndOfCentralDirectoryOffset(), - zipSections.getZipEndOfCentralDirectory()); } /** @@ -818,7 +807,7 @@ public class ApkSigningBlockUtils { * padding is used to allow for verity-based APK verification. * * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of - * padding used. + * padding used. */ public static Pair generateApkSigningBlockPadding( DataSource beforeCentralDir, @@ -862,7 +851,7 @@ public class ApkSigningBlockUtils { // uint64: size (excluding this field) // uint32: ID // (size - 4) bytes: value - // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes) + // (extra verity ID-value for padding to make block size a multiple of 4096 bytes) // uint64: size (same as the one above) // uint128: magic @@ -873,9 +862,9 @@ public class ApkSigningBlockUtils { int resultSize = 8 // size - + blocksSize - + 8 // size - + 16 // magic + + blocksSize + + 8 // size + + 16 // magic ; ByteBuffer paddingPair = null; if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { @@ -896,7 +885,6 @@ public class ApkSigningBlockUtils { long blockSizeFieldValue = resultSize - 8L; result.putLong(blockSizeFieldValue); - for (Pair schemeBlockPair : apkSignatureSchemeBlockPairs) { byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst(); int apkSignatureSchemeId = schemeBlockPair.getSecond(); @@ -916,26 +904,137 @@ public class ApkSigningBlockUtils { return result.array(); } + /** + * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a + * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the + * contents / value of the signature block and the second element is the ID of the block. + * + * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock} + */ + public static List> getApkSignatureBlocks( + DataSource apkSigningBlock) throws IOException { + // FORMAT: + // uint64: size (excluding this field) + // repeated ID-value pairs: + // uint64: size (excluding this field) + // uint32: ID + // (size - 4) bytes: value + // (extra verity ID-value for padding to make block size a multiple of 4096 bytes) + // uint64: size (same as the one above) + // uint128: magic + long apkSigningBlockSize = apkSigningBlock.size(); + if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) { + throw new IllegalArgumentException( + "APK signing block size out of range: " + apkSigningBlockSize); + } + // Remove the header and footer from the signing block to iterate over only the repeated + // ID-value pairs. + ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8, + (int) apkSigningBlock.size() - 32); + apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN); + List> signatureBlocks = new ArrayList<>(); + while (apkSigningBlockBuffer.hasRemaining()) { + long blockLength = apkSigningBlockBuffer.getLong(); + if (blockLength > Integer.MAX_VALUE || blockLength < 4) { + throw new IllegalArgumentException( + "Block index " + (signatureBlocks.size() + 1) + " size out of range: " + + blockLength); + } + int blockId = apkSigningBlockBuffer.getInt(); + // Since the block ID has already been read from the signature block read the next + // blockLength - 4 bytes as the value. + byte[] blockValue = new byte[(int) blockLength - 4]; + apkSigningBlockBuffer.get(blockValue); + signatureBlocks.add(Pair.of(blockValue, blockId)); + } + return signatureBlocks; + } + + /** + * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code + * List} of {@code Pair} instances where the first element is a {@code List} of {@link + * X509Certificate}s and the second element is a byte array of the individual signer's block. + * + *

This method supports any signature block that adheres to the following format up to the + * signing certificate(s): + *

+     * * length-prefixed sequence of length-prefixed signers
+     *   * length-prefixed signed data
+     *     * length-prefixed sequence of length-prefixed digests:
+     *       * uint32: signature algorithm ID
+     *       * length-prefixed bytes: digest of contents
+     *     * length-prefixed sequence of certificates:
+     *       * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+     * 
+ * + *

Note, this is a convenience method to obtain any signers from an existing signature block; + * the signature of each signer will not be verified. + * + * @throws ApkFormatException if an error is encountered while parsing the provided {@code + * signatureBlock} + * @throws CertificateException if the signing certificate(s) within an individual signer block + * cannot be parsed + */ + public static List, byte[]>> getApkSignatureBlockSigners( + byte[] signatureBlock) throws ApkFormatException, CertificateException { + ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock); + signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer); + List, byte[]>> signers = new ArrayList<>(); + while (signersBuffer.hasRemaining()) { + // Parse the next signer block, save all of its bytes for the resulting List, and + // rewind the buffer to allow the signing certificate(s) to be parsed. + ByteBuffer signer = getLengthPrefixedSlice(signersBuffer); + byte[] signerBytes = new byte[signer.remaining()]; + signer.get(signerBytes); + signer.rewind(); + + ByteBuffer signedData = getLengthPrefixedSlice(signer); + // The first length prefixed slice is the sequence of digests which are not required + // when obtaining the signing certificate(s). + getLengthPrefixedSlice(signedData); + ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData); + List certificates = new ArrayList<>(); + while (certificatesBuffer.hasRemaining()) { + int certLength = certificatesBuffer.getInt(); + byte[] certBytes = new byte[certLength]; + if (certLength > certificatesBuffer.remaining()) { + throw new IllegalArgumentException( + "Cert index " + (certificates.size() + 1) + " under signer index " + + (signers.size() + 1) + " size out of range: " + certLength); + } + certificatesBuffer.get(certBytes); + GuaranteedEncodedFormX509Certificate signerCert = + new GuaranteedEncodedFormX509Certificate( + X509CertificateUtils.generateCertificate(certBytes), certBytes); + certificates.add(signerCert); + } + signers.add(Pair.of(certificates, signerBytes)); + } + return signers; + } + /** * Computes the digests of the given APK components according to the algorithms specified in the * given SignerConfigs. * * @param signerConfigs signer configurations, one for each signer At least one signer config - * must be provided. - * @throws IOException if an I/O error occurs + * must be provided. + * + * @throws IOException if an I/O error occurs * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is - * missing - * @throws SignatureException if an error occurs when computing digests of generating - * signatures + * missing + * @throws SignatureException if an error occurs when computing digests of generating + * signatures */ public static Pair, Map> - computeContentDigests( - RunnablesExecutor executor, - DataSource beforeCentralDir, - DataSource centralDir, - DataSource eocd, - List signerConfigs) - throws IOException, NoSuchAlgorithmException, SignatureException { + computeContentDigests( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs) + throws IOException, NoSuchAlgorithmException, SignatureException { if (signerConfigs.isEmpty()) { throw new IllegalArgumentException( "No signer configs provided. At least one is required"); @@ -979,54 +1078,56 @@ public class ApkSigningBlockUtils { * requested platform versions. As a result, the result may contain more than one signature. * * @throws NoSupportedSignaturesException if no supported signatures were - * found for an Android platform version in the range. + * found for an Android platform version in the range. */ - public static List getSignaturesToVerify( - List signatures, int minSdkVersion, int maxSdkVersion) + public static List getSignaturesToVerify( + List signatures, int minSdkVersion, int maxSdkVersion) throws NoSupportedSignaturesException { - // Pick the signature with the strongest algorithm at all required SDK versions, to mimic - // Android's behavior on those versions. - // - // Here we assume that, once introduced, a signature algorithm continues to be supported in - // all future Android versions. We also assume that the better-than relationship between - // algorithms is exactly the same on all Android platform versions (except that older - // platforms might support fewer algorithms). If these assumption are no longer true, the - // logic here will need to change accordingly. - Map bestSigAlgorithmOnSdkVersion = new HashMap<>(); - int minProvidedSignaturesVersion = Integer.MAX_VALUE; - for (SupportedSignature sig : signatures) { - SignatureAlgorithm sigAlgorithm = sig.algorithm; - int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion(); - if (sigMinSdkVersion > maxSdkVersion) { - continue; - } - if (sigMinSdkVersion < minProvidedSignaturesVersion) { - minProvidedSignaturesVersion = sigMinSdkVersion; - } + return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false); + } - SupportedSignature candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion); - if ((candidate == null) - || (compareSignatureAlgorithm( - sigAlgorithm, candidate.algorithm) > 0)) { - bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig); - } + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + *

{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a + * signature within the signing block using the standard JCA. + * + *

Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static List getSignaturesToVerify( + List signatures, int minSdkVersion, int maxSdkVersion, + boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException { + try { + return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion, + maxSdkVersion, onlyRequireJcaSupport); + } catch (NoApkSupportedSignaturesException e) { + throw new NoSupportedSignaturesException(e.getMessage()); + } + } + + public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException { + public NoSupportedSignaturesException(String message) { + super(message); + } + } + + public static class SignatureNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public SignatureNotFoundException(String message) { + super(message); } - // Must have some supported signature algorithms for minSdkVersion. - if (minSdkVersion < minProvidedSignaturesVersion) { - throw new NoSupportedSignaturesException( - "Minimum provided signature version " + minProvidedSignaturesVersion + - " > minSdkVersion " + minSdkVersion); + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); } - if (bestSigAlgorithmOnSdkVersion.isEmpty()) { - throw new NoSupportedSignaturesException("No supported signature"); - } - List signaturesToVerify = - new ArrayList<>(bestSigAlgorithmOnSdkVersion.values()); - Collections.sort( - signaturesToVerify, - (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId())); - return signaturesToVerify; } /** @@ -1036,7 +1137,7 @@ public class ApkSigningBlockUtils { */ public static List> generateSignaturesOverData( SignerConfig signerConfig, byte[] data) - throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { + throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { List> signatures = new ArrayList<>(signerConfig.signatureAlgorithms.size()); PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); @@ -1045,15 +1146,15 @@ public class ApkSigningBlockUtils { signatureAlgorithm.getJcaSignatureAlgorithmAndParams(); String jcaSignatureAlgorithm = sigAlgAndParams.getFirst(); AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond(); + byte[] signatureBytes; try { - Signature signature = Signature.getInstance(jcaSignatureAlgorithm); - signature.initSign(signerConfig.privateKey); - if (jcaSignatureAlgorithmParams != null) { - signature.setParameter(jcaSignatureAlgorithmParams); - } - signature.update(data); - signatureBytes = signature.sign(); + signatureBytes = + SignerEngineFactory.getImplementation( + signerConfig.keyConfig, + jcaSignatureAlgorithm, + jcaSignatureAlgorithmParams) + .sign(data); } catch (InvalidKeyException e) { throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); } catch (InvalidAlgorithmParameterException | SignatureException e) { @@ -1091,14 +1192,14 @@ public class ApkSigningBlockUtils { * Wrap the signature according to CMS PKCS #7 RFC 5652. * The high-level simplified structure is as follows: * // ContentInfo - * // digestAlgorithm - * // SignedData - * // bag of certificates - * // SignerInfo - * // signing cert issuer and serial number (for locating the cert in the above bag) - * // digestAlgorithm - * // signatureAlgorithm - * // signature + * // digestAlgorithm + * // SignedData + * // bag of certificates + * // SignerInfo + * // signing cert issuer and serial number (for locating the cert in the above bag) + * // digestAlgorithm + * // signatureAlgorithm + * // signature * * @throws Asn1EncodingException if the ASN.1 structure could not be encoded */ @@ -1139,7 +1240,7 @@ public class ApkSigningBlockUtils { /** * Picks the correct v2/v3 digest for v4 signature verification. - *

+ * * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils. */ public static byte[] pickBestDigestForV4(Map contentDigests) { @@ -1151,226 +1252,23 @@ public class ApkSigningBlockUtils { return null; } - private static class ChunkDigests { - private final ContentDigestAlgorithm algorithm; - private final int digestOutputSize; - private final byte[] concatOfDigestsOfChunks; - - private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) { - this.algorithm = algorithm; - digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes(); - concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize]; - - // Fill the initial values of the concatenated digests of chunks, which is - // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}. - concatOfDigestsOfChunks[0] = 0x5a; - setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1); - } - - private MessageDigest createMessageDigest() throws NoSuchAlgorithmException { - return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm()); - } - - private int getOffset(int chunkIndex) { - return 1 + 4 + chunkIndex * digestOutputSize; - } - } - - /** - * A per-thread digest worker. - */ - private static class ChunkDigester implements Runnable { - private final ChunkSupplier dataSupplier; - private final List chunkDigests; - private final List messageDigests; - private final DataSink mdSink; - - private ChunkDigester(ChunkSupplier dataSupplier, List chunkDigests) { - this.dataSupplier = dataSupplier; - this.chunkDigests = chunkDigests; - messageDigests = new ArrayList<>(chunkDigests.size()); - for (ChunkDigests chunkDigest : chunkDigests) { - try { - messageDigests.add(chunkDigest.createMessageDigest()); - } catch (NoSuchAlgorithmException ex) { - throw new RuntimeException(ex); - } - } - mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0])); - } - - @Override - public void run() { - byte[] chunkContentPrefix = new byte[5]; - chunkContentPrefix[0] = (byte) 0xa5; - - try { - for (ChunkSupplier.Chunk chunk = dataSupplier.get(); - chunk != null; - chunk = dataSupplier.get()) { - int size = chunk.size; - if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) { - throw new RuntimeException("Chunk size greater than expected: " + size); - } - - // First update with the chunk prefix. - setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1); - mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length); - - // Then update with the chunk data. - mdSink.consume(chunk.data); - - // Now finalize chunk for all algorithms. - for (int i = 0; i < chunkDigests.size(); i++) { - ChunkDigests chunkDigest = chunkDigests.get(i); - int actualDigestSize = messageDigests.get(i).digest( - chunkDigest.concatOfDigestsOfChunks, - chunkDigest.getOffset(chunk.chunkIndex), - chunkDigest.digestOutputSize); - if (actualDigestSize != chunkDigest.digestOutputSize) { - throw new RuntimeException( - "Unexpected output size of " + chunkDigest.algorithm - + " digest: " + actualDigestSize); - } - } - } - } catch (IOException | DigestException e) { - throw new RuntimeException(e); - } - } - } - - /** - * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a - * supplied {@link DataSource}, the data from the next {@link DataSource} - * are NOT concatenated. Only the next call to get() will fetch from the - * next {@link DataSource} in the input {@link DataSource} array. - */ - private static class ChunkSupplier implements SupplierCompat { - private final DataSource[] dataSources; - private final int[] chunkCounts; - private final int totalChunkCount; - private final AtomicInteger nextIndex; - - private ChunkSupplier(DataSource[] dataSources) { - this.dataSources = dataSources; - chunkCounts = new int[dataSources.length]; - int totalChunkCount = 0; - for (int i = 0; i < dataSources.length; i++) { - long chunkCount = getChunkCount(dataSources[i].size(), - CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); - if (chunkCount > Integer.MAX_VALUE) { - throw new RuntimeException( - String.format( - "Number of chunks in dataSource[%d] is greater than max int.", - i)); - } - chunkCounts[i] = (int) chunkCount; - totalChunkCount = (int) (totalChunkCount + chunkCount); - } - this.totalChunkCount = totalChunkCount; - nextIndex = new AtomicInteger(0); - } - - /** - * We map an integer index to the termination-adjusted dataSources 1MB chunks. - * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned - * blocks in each input {@link DataSource} (unless the DataSource itself is - * 1MB-aligned). - */ - @Override - public ChunkSupplier.Chunk get() { - int index = nextIndex.getAndIncrement(); - if (index < 0 || index >= totalChunkCount) { - return null; - } - - int dataSourceIndex = 0; - long dataSourceChunkOffset = index; - for (; dataSourceIndex < dataSources.length; dataSourceIndex++) { - if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) { - break; - } - dataSourceChunkOffset -= chunkCounts[dataSourceIndex]; - } - - long remainingSize = Math.min( - dataSources[dataSourceIndex].size() - - dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, - CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); - - final int size = (int) remainingSize; - final ByteBuffer buffer = ByteBuffer.allocate(size); - try { - dataSources[dataSourceIndex].copyTo( - dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size, - buffer); - } catch (IOException e) { - throw new IllegalStateException("Failed to read chunk", e); - } - buffer.rewind(); - - return new Chunk(index, buffer, size); - } - - static class Chunk { - private final int chunkIndex; - private final ByteBuffer data; - private final int size; - - private Chunk(int chunkIndex, ByteBuffer data, int size) { - this.chunkIndex = chunkIndex; - this.data = data; - this.size = size; - } - } - } - - public static class VerityTreeAndDigest { - public final ContentDigestAlgorithm contentDigestAlgorithm; - public final byte[] rootHash; - public final byte[] tree; - - VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash, - byte[] tree) { - this.contentDigestAlgorithm = contentDigestAlgorithm; - this.rootHash = rootHash; - this.tree = tree; - } - } - - public static class NoSupportedSignaturesException extends Exception { - private static final long serialVersionUID = 1L; - - public NoSupportedSignaturesException(String message) { - super(message); - } - } - - public static class SignatureNotFoundException extends Exception { - private static final long serialVersionUID = 1L; - - public SignatureNotFoundException(String message) { - super(message); - } - - public SignatureNotFoundException(String message, Throwable cause) { - super(message, cause); - } - } - - /** - * Signer configuration. - */ + /** Signer configuration. */ public static class SignerConfig { /** * Private key. + * + * @deprecated all internal usage has migrated to use {@link #keyConfig}. This field is not + * removed so that compilation is not broken for clients referencing it, but using this + * field may lead to unexpected errors. */ - public PrivateKey privateKey; + @Deprecated public PrivateKey privateKey; + + /** Signing key config. */ + public KeyConfig keyConfig; /** * Certificates, with the first certificate containing the public key corresponding to - * {@link #privateKey}. + * {@link #keyConfig}. */ public List certificates; @@ -1381,22 +1279,18 @@ public class ApkSigningBlockUtils { public int minSdkVersion; public int maxSdkVersion; - public SigningCertificateLineage mSigningCertificateLineage; + public boolean signerTargetsDevRelease; + public SigningCertificateLineage signingCertificateLineage; } - public static class Result { - public final int signatureSchemeVersion; + public static class Result extends ApkSigResult { + public SigningCertificateLineage signingCertificateLineage = null; public final List signers = new ArrayList<>(); private final List mWarnings = new ArrayList<>(); private final List mErrors = new ArrayList<>(); - /** - * Whether the APK's APK Signature Scheme signature verifies. - */ - public boolean verified; - public SigningCertificateLineage signingCertificateLineage = null; public Result(int signatureSchemeVersion) { - this.signatureSchemeVersion = signatureSchemeVersion; + super(signatureSchemeVersion); } public boolean containsErrors() { @@ -1435,19 +1329,17 @@ public class ApkSigningBlockUtils { mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); } + @Override public List getErrors() { return mErrors; } + @Override public List getWarnings() { return mWarnings; } - public static class SignerInfo { - private final List mWarnings = new ArrayList<>(); - private final List mErrors = new ArrayList<>(); - public int index; - public List certs = new ArrayList<>(); + public static class SignerInfo extends ApkSignerInfo { public List contentDigests = new ArrayList<>(); public Map verifiedContentDigests = new HashMap<>(); public List signatures = new ArrayList<>(); @@ -1458,6 +1350,9 @@ public class ApkSigningBlockUtils { public int maxSdkVersion; public SigningCertificateLineage signingCertificateLineage; + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + public void addError(ApkVerifier.Issue msg, Object... parameters) { mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); } @@ -1487,7 +1382,7 @@ public class ApkSigningBlockUtils { private final byte[] mValue; public ContentDigest(int signatureAlgorithmId, byte[] value) { - mSignatureAlgorithmId = signatureAlgorithmId; + mSignatureAlgorithmId = signatureAlgorithmId; mValue = value; } @@ -1505,7 +1400,7 @@ public class ApkSigningBlockUtils { private final byte[] mValue; public Signature(int algorithmId, byte[] value) { - mAlgorithmId = algorithmId; + mAlgorithmId = algorithmId; mValue = value; } @@ -1523,7 +1418,7 @@ public class ApkSigningBlockUtils { private final byte[] mValue; public AdditionalAttribute(int id, byte[] value) { - mId = id; + mId = id; mValue = value.clone(); } @@ -1538,13 +1433,9 @@ public class ApkSigningBlockUtils { } } - public static class SupportedSignature { - public final SignatureAlgorithm algorithm; - public final byte[] signature; - + public static class SupportedSignature extends ApkSupportedSignature { public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { - this.algorithm = algorithm; - this.signature = signature; + super(algorithm, signature); } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java new file mode 100644 index 00000000..40ae9479 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2020 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.internal.apk; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkSigningBlockNotFoundException; +import com.android.apksig.apk.ApkUtilsLite; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the + * utility functionality. + */ +public class ApkSigningBlockUtilsLite { + private ApkSigningBlockUtilsLite() {} + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + /** + * Returns the APK Signature Scheme block contained in the provided APK file for the given ID + * and the additional information relevant for verifying the block against the file. + * + * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs + * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 + * block ID. + * + * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme + * @throws IOException if an I/O error occurs while reading the APK + */ + public static SignatureInfo findSignature( + DataSource apk, ZipSections zipSections, int blockId) + throws IOException, SignatureNotFoundException { + // Find the APK Signing Block. + DataSource apkSigningBlock; + long apkSigningBlockOffset; + try { + ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo = + ApkUtilsLite.findApkSigningBlock(apk, zipSections); + apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); + apkSigningBlock = apkSigningBlockInfo.getContents(); + } catch (ApkSigningBlockNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage(), e); + } + ByteBuffer apkSigningBlockBuf = + apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); + apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); + + // Find the APK Signature Scheme Block inside the APK Signing Block. + ByteBuffer apkSignatureSchemeBlock = + findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId); + return new SignatureInfo( + apkSignatureSchemeBlock, + apkSigningBlockOffset, + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); + } + + public static ByteBuffer findApkSignatureSchemeBlock( + ByteBuffer apkSigningBlock, + int blockId) throws SignatureNotFoundException { + checkByteOrderLittleEndian(apkSigningBlock); + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes pairs + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + + int entryCount = 0; + while (pairs.hasRemaining()) { + entryCount++; + if (pairs.remaining() < 8) { + throw new SignatureNotFoundException( + "Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + long lenLong = pairs.getLong(); + if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong); + } + int len = (int) lenLong; + int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + pairs.remaining()); + } + int id = pairs.getInt(); + if (id == blockId) { + return getByteBuffer(pairs, len - 4); + } + pairs.position(nextEntryPos); + } + + throw new SignatureNotFoundException( + "No APK Signature Scheme block in APK Signing Block with ID: " + blockId); + } + + public static void checkByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + *

Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoApkSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static List getSignaturesToVerify( + List signatures, int minSdkVersion, int maxSdkVersion) + throws NoApkSupportedSignaturesException { + return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false); + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + *

{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a + * signature within the signing block using the standard JCA. + * + *

Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoApkSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static List getSignaturesToVerify( + List signatures, int minSdkVersion, int maxSdkVersion, + boolean onlyRequireJcaSupport) throws + NoApkSupportedSignaturesException { + // Pick the signature with the strongest algorithm at all required SDK versions, to mimic + // Android's behavior on those versions. + // + // Here we assume that, once introduced, a signature algorithm continues to be supported in + // all future Android versions. We also assume that the better-than relationship between + // algorithms is exactly the same on all Android platform versions (except that older + // platforms might support fewer algorithms). If these assumption are no longer true, the + // logic here will need to change accordingly. + Map + bestSigAlgorithmOnSdkVersion = new HashMap<>(); + int minProvidedSignaturesVersion = Integer.MAX_VALUE; + for (T sig : signatures) { + SignatureAlgorithm sigAlgorithm = sig.algorithm; + int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion() + : sigAlgorithm.getMinSdkVersion(); + if (sigMinSdkVersion > maxSdkVersion) { + continue; + } + if (sigMinSdkVersion < minProvidedSignaturesVersion) { + minProvidedSignaturesVersion = sigMinSdkVersion; + } + + T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion); + if ((candidate == null) + || (compareSignatureAlgorithm( + sigAlgorithm, candidate.algorithm) > 0)) { + bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig); + } + } + + // Must have some supported signature algorithms for minSdkVersion. + if (minSdkVersion < minProvidedSignaturesVersion) { + throw new NoApkSupportedSignaturesException( + "Minimum provided signature version " + minProvidedSignaturesVersion + + " > minSdkVersion " + minSdkVersion); + } + if (bestSigAlgorithmOnSdkVersion.isEmpty()) { + throw new NoApkSupportedSignaturesException("No supported signature"); + } + List signaturesToVerify = + new ArrayList<>(bestSigAlgorithmOnSdkVersion.values()); + Collections.sort( + signaturesToVerify, + (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId())); + return signaturesToVerify; + } + + /** + * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if + * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. + */ + public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { + ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); + ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); + return compareContentDigestAlgorithm(digestAlg1, digestAlg2); + } + + /** + * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number + * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference. + */ + private static int compareContentDigestAlgorithm( + ContentDigestAlgorithm alg1, + ContentDigestAlgorithm alg2) { + switch (alg1) { + case CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + case VERITY_CHUNKED_SHA256: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case CHUNKED_SHA512: + switch (alg2) { + case CHUNKED_SHA256: + case VERITY_CHUNKED_SHA256: + return 1; + case CHUNKED_SHA512: + return 0; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case VERITY_CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 1; + case VERITY_CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + default: + throw new IllegalArgumentException("Unknown alg1: " + alg1); + } + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + /** + * Relative get method for reading {@code size} number of bytes from the current + * position of this buffer. + * + *

This method reads the next {@code size} bytes at this buffer's current position, + * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to + * {@code size}, byte order set to this buffer's byte order; and then increments the position by + * {@code size}. + */ + private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + int originalLimit = source.limit(); + int position = source.position(); + int limit = position + size; + if ((limit < position) || (limit > originalLimit)) { + throw new BufferUnderflowException(); + } + source.limit(limit); + try { + ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + return result; + } finally { + source.limit(originalLimit); + } + } + + public static String toHex(byte[] value) { + StringBuilder sb = new StringBuilder(value.length * 2); + int len = value.length; + for (int i = 0; i < len; i++) { + int hi = (value[i] & 0xff) >>> 4; + int lo = value[i] & 0x0f; + sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); + } + return sb.toString(); + } + + public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { + if (source.remaining() < 4) { + throw new ApkFormatException( + "Remaining buffer too short to contain length of length-prefixed field" + + ". Remaining: " + source.remaining()); + } + int len = source.getInt(); + if (len < 0) { + throw new IllegalArgumentException("Negative length"); + } else if (len > source.remaining()) { + throw new ApkFormatException( + "Length-prefixed field longer than remaining buffer" + + ". Field length: " + len + ", remaining: " + source.remaining()); + } + return getByteBuffer(source, len); + } + + public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { + int len = buf.getInt(); + if (len < 0) { + throw new ApkFormatException("Negative length"); + } else if (len > buf.remaining()) { + throw new ApkFormatException( + "Underflow while reading length-prefixed value. Length: " + len + + ", available: " + buf.remaining()); + } + byte[] result = new byte[len]; + buf.get(result); + return result; + } + + public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + List> sequence) { + int resultSize = 0; + for (Pair element : sequence) { + resultSize += 12 + element.getSecond().length; + } + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (Pair element : sequence) { + byte[] second = element.getSecond(); + result.putInt(8 + second.length); + result.putInt(element.getFirst()); + result.putInt(second.length); + result.put(second); + } + return result.array(); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/MathCompat.java b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java similarity index 50% rename from apksigner/src/main/java/com/android/apksig/internal/util/MathCompat.java rename to apksigner/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java index 705c65e6..61652a43 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/MathCompat.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam + * Copyright (C) 2020 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. @@ -14,21 +14,22 @@ * limitations under the License. */ -package com.android.apksig.internal.util; +package com.android.apksig.internal.apk; + +/** + * Base implementation of a supported signature for an APK. + */ +public class ApkSupportedSignature { + public final SignatureAlgorithm algorithm; + public final byte[] signature; -public class MathCompat { /** - * Returns the value of the {@code long} argument; - * throwing an exception if the value overflows an {@code int}. - * - * @param value the long value - * @return the argument as an int - * @throws ArithmeticException if the {@code argument} overflows an int + * Constructs a new supported signature using the provided {@code algorithm} and {@code + * signature} bytes. */ - public static int toIntExact(long value) { - if ((int) value != value) { - throw new ArithmeticException("integer overflow"); - } - return (int) value; + public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { + this.algorithm = algorithm; + this.signature = signature; } + } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/apksigner/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java index 4cd067e5..b806d1e4 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java @@ -16,28 +16,18 @@ package com.android.apksig.internal.apk; -/** - * APK Signature Scheme v2 content digest algorithm. - */ +/** APK Signature Scheme v2 content digest algorithm. */ public enum ContentDigestAlgorithm { - /** - * SHA2-256 over 1 MB chunks. - */ + /** SHA2-256 over 1 MB chunks. */ CHUNKED_SHA256(1, "SHA-256", 256 / 8), - /** - * SHA2-512 over 1 MB chunks. - */ + /** SHA2-512 over 1 MB chunks. */ CHUNKED_SHA512(2, "SHA-512", 512 / 8), - /** - * SHA2-256 over 4 KB chunks for APK verity. - */ + /** SHA2-256 over 4 KB chunks for APK verity. */ VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8), - /** - * Non-chunk SHA2-256. - */ + /** Non-chunk SHA2-256. */ SHA256(4, "SHA-256", 256 / 8); private final int mId; @@ -51,9 +41,7 @@ public enum ContentDigestAlgorithm { mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes; } - /** - * Returns the ID of the digest algorithm used on the APK. - */ + /** Returns the ID of the digest algorithm used on the APK. */ public int getId() { return mId; } @@ -66,9 +54,7 @@ public enum ContentDigestAlgorithm { return mJcaMessageDigestAlgorithm; } - /** - * Returns the size (in bytes) of the digest of a chunk of content. - */ + /** Returns the size (in bytes) of the digest of a chunk of content. */ int getChunkDigestOutputSizeBytes() { return mChunkDigestOutputSizeBytes; } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java b/apksigner/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java new file mode 100644 index 00000000..52c6085c --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 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.internal.apk; + +/** + * Base exception that is thrown when there are no signatures that support the full range of + * requested platform versions. + */ +public class NoApkSupportedSignaturesException extends Exception { + public NoApkSupportedSignaturesException(String message) { + super(message); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java index 9a8e5835..804eb37b 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java @@ -18,7 +18,6 @@ package com.android.apksig.internal.apk; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.Pair; - import java.security.spec.AlgorithmParameterSpec; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; @@ -39,7 +38,8 @@ public enum SignatureAlgorithm { Pair.of("SHA256withRSA/PSS", new PSSParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.M), /** * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content @@ -53,57 +53,65 @@ public enum SignatureAlgorithm { "SHA512withRSA/PSS", new PSSParameterSpec( "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.M), - /** - * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. - */ + /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ RSA_PKCS1_V1_5_WITH_SHA256( 0x0103, ContentDigestAlgorithm.CHUNKED_SHA256, "RSA", Pair.of("SHA256withRSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), - /** - * RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. - */ + /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ RSA_PKCS1_V1_5_WITH_SHA512( 0x0104, ContentDigestAlgorithm.CHUNKED_SHA512, "RSA", Pair.of("SHA512withRSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), - /** - * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. - */ + /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ ECDSA_WITH_SHA256( 0x0201, ContentDigestAlgorithm.CHUNKED_SHA256, "EC", Pair.of("SHA256withECDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.HONEYCOMB), - /** - * ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. - */ + /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ ECDSA_WITH_SHA512( 0x0202, ContentDigestAlgorithm.CHUNKED_SHA512, "EC", Pair.of("SHA512withECDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.HONEYCOMB), - /** - * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. - */ + /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ DSA_WITH_SHA256( 0x0301, ContentDigestAlgorithm.CHUNKED_SHA256, "DSA", Pair.of("SHA256withDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), + + /** + * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done + * deterministically according to RFC 6979. + */ + DETDSA_WITH_SHA256( + 0x0301, + ContentDigestAlgorithm.CHUNKED_SHA256, + "DSA", + Pair.of("SHA256withDetDSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), /** * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in @@ -115,7 +123,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, "RSA", Pair.of("SHA256withRSA", null), - AndroidSdkVersion.P), + AndroidSdkVersion.P, + AndroidSdkVersion.INITIAL_RELEASE), /** * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way @@ -127,7 +136,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, "EC", Pair.of("SHA256withECDSA", null), - AndroidSdkVersion.P), + AndroidSdkVersion.P, + AndroidSdkVersion.HONEYCOMB), /** * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way @@ -139,34 +149,28 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, "DSA", Pair.of("SHA256withDSA", null), - AndroidSdkVersion.P); + AndroidSdkVersion.P, + AndroidSdkVersion.INITIAL_RELEASE); private final int mId; private final String mJcaKeyAlgorithm; private final ContentDigestAlgorithm mContentDigestAlgorithm; private final Pair mJcaSignatureAlgAndParams; private final int mMinSdkVersion; + private final int mJcaSigAlgMinSdkVersion; SignatureAlgorithm(int id, - ContentDigestAlgorithm contentDigestAlgorithm, - String jcaKeyAlgorithm, - Pair jcaSignatureAlgAndParams, - int minSdkVersion) { + ContentDigestAlgorithm contentDigestAlgorithm, + String jcaKeyAlgorithm, + Pair jcaSignatureAlgAndParams, + int minSdkVersion, + int jcaSigAlgMinSdkVersion) { mId = id; mContentDigestAlgorithm = contentDigestAlgorithm; mJcaKeyAlgorithm = jcaKeyAlgorithm; mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; mMinSdkVersion = minSdkVersion; - } - - public static SignatureAlgorithm findById(int id) { - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { - if (alg.getId() == id) { - return alg; - } - } - - return null; + mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion; } /** @@ -201,4 +205,21 @@ public enum SignatureAlgorithm { public int getMinSdkVersion() { return mMinSdkVersion; } + + /** + * Returns the minimum SDK version that supports the JCA signature algorithm. + */ + public int getJcaSigAlgMinSdkVersion() { + return mJcaSigAlgMinSdkVersion; + } + + public static SignatureAlgorithm findById(int id) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { + if (alg.getId() == id) { + return alg; + } + } + + return null; + } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java b/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java index b060dd45..5e26327b 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java @@ -23,29 +23,19 @@ import java.nio.ByteBuffer; * contained in the block against the file. */ public class SignatureInfo { - /** - * Contents of APK Signature Scheme block. - */ + /** Contents of APK Signature Scheme block. */ public final ByteBuffer signatureBlock; - /** - * Position of the APK Signing Block in the file. - */ + /** Position of the APK Signing Block in the file. */ public final long apkSigningBlockOffset; - /** - * Position of the ZIP Central Directory in the file. - */ + /** Position of the ZIP Central Directory in the file. */ public final long centralDirOffset; - /** - * Position of the ZIP End of Central Directory (EoCD) in the file. - */ + /** Position of the ZIP End of Central Directory (EoCD) in the file. */ public final long eocdOffset; - /** - * Contents of ZIP End of Central Directory (EoCD) of the file. - */ + /** Contents of ZIP End of Central Directory (EoCD) of the file. */ public final ByteBuffer eocd; public SignatureInfo( diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java b/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java new file mode 100644 index 00000000..95f06eff --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 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.internal.apk; + +/** + * Base exception that is thrown when the APK is not signed with the requested signature scheme. + */ +public class SignatureNotFoundException extends Exception { + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java new file mode 100644 index 00000000..93627ff0 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2020 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.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */ +public class SourceStampCertificateLineage { + + private final static int FIRST_VERSION = 1; + private final static int CURRENT_VERSION = FIRST_VERSION; + + /** + * Deserializes the binary representation of a SourceStampCertificateLineage. Also + * verifies that the structure is well-formed, e.g. that the signature for each node is from its + * parent. + */ + public static List readSigningCertificateLineage(ByteBuffer inputBytes) + throws IOException { + List result = new ArrayList<>(); + int nodeCount = 0; + if (inputBytes == null || !inputBytes.hasRemaining()) { + return null; + } + + ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes); + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); + } + + // FORMAT (little endian): + // * uint32: version code + // * sequence of length-prefixed (uint32): nodes + // * length-prefixed bytes: signed data + // * length-prefixed bytes: certificate + // * uint32: signature algorithm id + // * uint32: flags + // * uint32: signature algorithm id (used by to sign next cert in lineage) + // * length-prefixed bytes: signature over above signed data + + X509Certificate lastCert = null; + int lastSigAlgorithmId = 0; + + try { + int version = inputBytes.getInt(); + if (version != CURRENT_VERSION) { + // we only have one version to worry about right now, so just check it + throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version" + + " different than any of which we are aware"); + } + HashSet certHistorySet = new HashSet<>(); + while (inputBytes.hasRemaining()) { + nodeCount++; + ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes); + ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes); + int flags = nodeBytes.getInt(); + int sigAlgorithmId = nodeBytes.getInt(); + SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId); + byte[] signature = readLengthPrefixedByteArray(nodeBytes); + + if (lastCert != null) { + // Use previous level cert to verify current level + String jcaSignatureAlgorithm = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + PublicKey publicKey = lastCert.getPublicKey(); + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(signedData); + if (!sig.verify(signature)) { + throw new SecurityException("Unable to verify signature of certificate #" + + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying" + + " SourceStampCertificateLineage object"); + } + } + + signedData.rewind(); + byte[] encodedCert = readLengthPrefixedByteArray(signedData); + int signedSigAlgorithm = signedData.getInt(); + if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) { + throw new SecurityException("Signing algorithm ID mismatch for certificate #" + + nodeBytes + " when verifying SourceStampCertificateLineage object"); + } + lastCert = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert); + if (certHistorySet.contains(lastCert)) { + throw new SecurityException("Encountered duplicate entries in " + + "SigningCertificateLineage at certificate #" + nodeCount + ". All " + + "signing certificates should be unique"); + } + certHistorySet.add(lastCert); + lastSigAlgorithmId = sigAlgorithmId; + result.add(new SigningCertificateNode( + lastCert, SignatureAlgorithm.findById(signedSigAlgorithm), + SignatureAlgorithm.findById(sigAlgorithmId), signature, flags)); + } + } catch(ApkFormatException | BufferUnderflowException e){ + throw new IOException("Failed to parse SourceStampCertificateLineage object", e); + } catch(NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e){ + throw new SecurityException( + "Failed to verify signature over signed data for certificate #" + nodeCount + + " when parsing SourceStampCertificateLineage object", e); + } catch(CertificateException e){ + throw new SecurityException("Failed to decode certificate #" + nodeCount + + " when parsing SourceStampCertificateLineage object", e); + } + return result; + } + + /** + * Represents one signing certificate in the SourceStampCertificateLineage, which + * generally means it is/was used at some point to sign source stamps. + */ + public static class SigningCertificateNode { + + public SigningCertificateNode( + X509Certificate signingCert, + SignatureAlgorithm parentSigAlgorithm, + SignatureAlgorithm sigAlgorithm, + byte[] signature, + int flags) { + this.signingCert = signingCert; + this.parentSigAlgorithm = parentSigAlgorithm; + this.sigAlgorithm = sigAlgorithm; + this.signature = signature; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SigningCertificateNode)) return false; + + SigningCertificateNode that = (SigningCertificateNode) o; + if (!signingCert.equals(that.signingCert)) return false; + if (parentSigAlgorithm != that.parentSigAlgorithm) return false; + if (sigAlgorithm != that.sigAlgorithm) return false; + if (!Arrays.equals(signature, that.signature)) return false; + if (flags != that.flags) return false; + + // we made it + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode()); + result = prime * result + + ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode()); + result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode()); + result = prime * result + Arrays.hashCode(signature); + result = prime * result + flags; + return result; + } + + /** + * the signing cert for this node. This is part of the data signed by the parent node. + */ + public final X509Certificate signingCert; + + /** + * the algorithm used by this node's parent to bless this data. Its ID value is part of + * the data signed by the parent node. {@code null} for first node. + */ + public final SignatureAlgorithm parentSigAlgorithm; + + /** + * the algorithm used by this node to bless the next node's data. Its ID value is part + * of the signed data of the next node. {@code null} for the last node. + */ + public SignatureAlgorithm sigAlgorithm; + + /** + * signature over the signed data (above). The signature is from this node's parent + * signing certificate, which should correspond to the signing certificate used to sign an + * APK before rotating to this one, and is formed using {@code signatureAlgorithm}. + */ + public final byte[] signature; + + /** + * the flags detailing how the platform should treat this signing cert + */ + public int flags; + } +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java new file mode 100644 index 00000000..2a949adb --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 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.internal.apk.stamp; + +/** Constants used for source stamp signing and verification. */ +public class SourceStampConstants { + private SourceStampConstants() {} + + public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e; + public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d; + public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7; + /** + * The source stamp timestamp attribute value is an 8-byte little-endian encoded long + * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes + * of the attribute value buffer will be used to read the timestamp, and any additional buffer + * space will be ignored. + */ + public static final int STAMP_TIME_ATTR_ID = 0xe43c5946; +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java index ad068773..6195e37d 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java @@ -15,15 +15,25 @@ */ package com.android.apksig.internal.apk.stamp; -import com.android.apksig.ApkVerifier; -import com.android.apksig.apk.ApkFormatException; -import com.android.apksig.internal.apk.ApkSigningBlockUtils; -import com.android.apksig.internal.apk.SignatureAlgorithm; -import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; -import com.android.apksig.internal.util.X509CertificateUtils; +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V31; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex; +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSupportedSignature; +import com.android.apksig.internal.apk.NoApkSupportedSignaturesException; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; + +import java.io.ByteArrayInputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; @@ -52,9 +62,7 @@ import java.util.Map; * block. */ class SourceStampVerifier { - /** - * Hidden constructor to prevent instantiation. - */ + /** Hidden constructor to prevent instantiation. */ private SourceStampVerifier() { } @@ -70,7 +78,7 @@ class SourceStampVerifier { public static void verifyV1SourceStamp( ByteBuffer sourceStampBlockData, CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, + ApkSignerInfo result, byte[] apkDigest, byte[] sourceStampCertificateDigest, int minSdkVersion, @@ -83,12 +91,13 @@ class SourceStampVerifier { return; } + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData); verifySourceStampSignature( apkDigest, minSdkVersion, maxSdkVersion, sourceStampCertificate, - sourceStampBlockData, + apkDigestSignatures, result); } @@ -104,7 +113,7 @@ class SourceStampVerifier { public static void verifyV2SourceStamp( ByteBuffer sourceStampBlockData, CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, + ApkSignerInfo result, Map signatureSchemeApkDigests, byte[] sourceStampCertificateDigest, int minSdkVersion, @@ -118,20 +127,27 @@ class SourceStampVerifier { } // Parse signed signature schemes block. - ByteBuffer signedSignatureSchemes = - ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData); Map signedSignatureSchemeData = new HashMap<>(); while (signedSignatureSchemes.hasRemaining()) { - ByteBuffer signedSignatureScheme = - ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes); + ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes); int signatureSchemeId = signedSignatureScheme.getInt(); - signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme); + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme); + signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures); } for (Map.Entry signatureSchemeApkDigest : signatureSchemeApkDigests.entrySet()) { + // TODO(b/329101755): Once the source stamp is updated to include support for V3.1 + // signatures, add verification of the content digests for this scheme. + if (signatureSchemeApkDigest.getKey() == VERSION_APK_SIGNATURE_SCHEME_V31) { + result.addInfoMessage( + ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_SCHEME_NOT_AVAILABLE, + VERSION_APK_SIGNATURE_SCHEME_V31); + continue; + } if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE); + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); return; } verifySourceStampSignature( @@ -141,28 +157,43 @@ class SourceStampVerifier { sourceStampCertificate, signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()), result); - if (result.containsWarnings() || result.containsWarnings()) { + if (result.containsWarnings() || result.containsErrors()) { return; } } + + if (sourceStampBlockData.hasRemaining()) { + // The stamp block contains some additional attributes. + ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData); + + byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()]; + stampAttributeData.get(stampAttributeBytes); + stampAttributeData.flip(); + + verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion, + sourceStampCertificate, stampAttributeDataSignatures, result); + if (result.containsErrors() || result.containsWarnings()) { + return; + } + parseStampAttributes(stampAttributeData, sourceStampCertificate, result); + } } private static X509Certificate verifySourceStampCertificate( ByteBuffer sourceStampBlockData, CertificateFactory certFactory, byte[] sourceStampCertificateDigest, - ApkSigningBlockUtils.Result.SignerInfo result) + ApkSignerInfo result) throws NoSuchAlgorithmException, ApkFormatException { // Parse the SourceStamp certificate. - byte[] sourceStampEncodedCertificate = - ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData); + byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData); X509Certificate sourceStampCertificate; try { - sourceStampCertificate = - X509CertificateUtils.generateCertificate( - sourceStampEncodedCertificate, certFactory); + sourceStampCertificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(sourceStampEncodedCertificate)); } catch (CertificateException e) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e); + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e); return null; } // Wrap the cert so that the result's getEncoded returns exactly the original encoded @@ -180,62 +211,71 @@ class SourceStampVerifier { byte[] sourceStampBlockCertificateDigest = messageDigest.digest(); if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { result.addWarning( - ApkVerifier.Issue + ApkVerificationIssue .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, - ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest), - ApkSigningBlockUtils.toHex(sourceStampCertificateDigest)); + toHex(sourceStampBlockCertificateDigest), + toHex(sourceStampCertificateDigest)); return null; } return sourceStampCertificate; } private static void verifySourceStampSignature( - byte[] apkDigest, + byte[] data, int minSdkVersion, int maxSdkVersion, X509Certificate sourceStampCertificate, - ByteBuffer signedData, - ApkSigningBlockUtils.Result.SignerInfo result) - throws ApkFormatException { + ByteBuffer signatures, + ApkSignerInfo result) { // Parse the signatures block and identify supported signatures - ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData); int signatureCount = 0; - List supportedSignatures = new ArrayList<>(1); + List supportedSignatures = new ArrayList<>(1); while (signatures.hasRemaining()) { signatureCount++; try { - ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures); + ByteBuffer signature = getLengthPrefixedSlice(signatures); int sigAlgorithmId = signature.getInt(); - byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature); + byte[] sigBytes = readLengthPrefixedByteArray(signature); SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); if (signatureAlgorithm == null) { - result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + result.addInfoMessage( + ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, + sigAlgorithmId); continue; } supportedSignatures.add( - new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes)); + new ApkSupportedSignature(signatureAlgorithm, sigBytes)); } catch (ApkFormatException | BufferUnderflowException e) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount); + ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount); return; } } if (supportedSignatures.isEmpty()) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE); + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); return; } // Verify signatures over digests using the SourceStamp's certificate. - List signaturesToVerify; + List signaturesToVerify; try { signaturesToVerify = - ApkSigningBlockUtils.getSignaturesToVerify( - supportedSignatures, minSdkVersion, maxSdkVersion); - } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE); + getSignaturesToVerify( + supportedSignatures, minSdkVersion, maxSdkVersion, true); + } catch (NoApkSupportedSignaturesException e) { + // To facilitate debugging capture the signature algorithms and resulting exception in + // the warning. + StringBuilder signatureAlgorithms = new StringBuilder(); + for (ApkSupportedSignature supportedSignature : supportedSignatures) { + if (signatureAlgorithms.length() > 0) { + signatureAlgorithms.append(", "); + } + signatureAlgorithms.append(supportedSignature.algorithm); + } + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, + signatureAlgorithms.toString(), e); return; } - for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) { + for (ApkSupportedSignature signature : signaturesToVerify) { SignatureAlgorithm signatureAlgorithm = signature.algorithm; String jcaSignatureAlgorithm = signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); @@ -248,11 +288,11 @@ class SourceStampVerifier { if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } - sig.update(apkDigest); + sig.update(data); byte[] sigBytes = signature.signature; if (!sig.verify(sigBytes)) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm); + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm); return; } } catch (InvalidKeyException @@ -260,9 +300,66 @@ class SourceStampVerifier { | SignatureException | NoSuchAlgorithmException e) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e); + ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e); return; } } } + + private static void parseStampAttributes(ByteBuffer stampAttributeData, + X509Certificate sourceStampCertificate, ApkSignerInfo result) + throws ApkFormatException { + ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData); + int stampAttributeCount = 0; + while (stampAttributes.hasRemaining()) { + stampAttributeCount++; + try { + ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes); + int id = attribute.getInt(); + byte[] value = ByteBufferUtils.toByteArray(attribute); + if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) { + readStampCertificateLineage(value, sourceStampCertificate, result); + } else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) { + long timestamp = ByteBuffer.wrap(value).order( + ByteOrder.LITTLE_ENDIAN).getLong(); + if (timestamp > 0) { + result.timestamp = timestamp; + } else { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP, + timestamp); + } + } else { + result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id); + } + } catch (ApkFormatException | BufferUnderflowException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, + stampAttributeCount); + return; + } + } + } + + private static void readStampCertificateLineage(byte[] lineageBytes, + X509Certificate sourceStampCertificate, ApkSignerInfo result) { + try { + // SourceStampCertificateLineage is verified when built + List nodes = + SourceStampCertificateLineage.readSigningCertificateLineage( + ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN)); + for (int i = 0; i < nodes.size(); i++) { + result.certificateLineage.add(nodes.get(i).signingCert); + } + // Make sure that the last cert in the chain matches this signer cert + if (!sourceStampCertificate.equals( + result.certificateLineage.get(result.certificateLineage.size() - 1))) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } + } catch (SecurityException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY); + } catch (IllegalArgumentException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } catch (Exception e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE); + } + } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java index 62e011e9..dee24bd1 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,6 +31,7 @@ import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -48,14 +48,11 @@ import java.util.Map; *

V1 of the source stamp allows signing the digest of at most one signature scheme only. */ public abstract class V1SourceStampSigner { + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; - public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e; - - /** - * Hidden constructor to prevent instantiation. - */ - private V1SourceStampSigner() { - } + /** Hidden constructor to prevent instantiation. */ + private V1SourceStampSigner() {} public static Pair generateSourceStampBlock( SignerConfig sourceStampSignerConfig, Map digestInfo) @@ -68,7 +65,7 @@ public abstract class V1SourceStampSigner { for (Map.Entry digest : digestInfo.entrySet()) { digests.add(Pair.of(digest.getKey().getId(), digest.getValue())); } - Collections.sort(digests, (o1, o2) -> o1.getFirst().compareTo(o2.getFirst())); + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); SourceStampBlock sourceStampBlock = new SourceStampBlock(); @@ -93,16 +90,16 @@ public abstract class V1SourceStampSigner { // * length-prefixed bytes: signature of signed data byte[] sourceStampSignerBlock = encodeAsSequenceOfLengthPrefixedElements( - new byte[][]{ - sourceStampBlock.stampCertificate, - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( - sourceStampBlock.signedDigests), + new byte[][] { + sourceStampBlock.stampCertificate, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedDigests), }); // FORMAT: // * length-prefixed stamp block. - return Pair.of( - encodeAsLengthPrefixedElement(sourceStampSignerBlock), V1_SOURCE_STAMP_BLOCK_ID); + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID); } private static final class SourceStampBlock { diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java index 98aa80b6..c3fdeecc 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +16,7 @@ package com.android.apksig.internal.apk.stamp; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; -import static com.android.apksig.internal.apk.stamp.V1SourceStampSigner.V1_SOURCE_STAMP_BLOCK_ID; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; import com.android.apksig.ApkVerifier; import com.android.apksig.apk.ApkFormatException; @@ -36,6 +35,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -46,11 +46,8 @@ import java.util.Map; */ public abstract class V1SourceStampVerifier { - /** - * Hidden constructor to prevent instantiation. - */ - private V1SourceStampVerifier() { - } + /** Hidden constructor to prevent instantiation. */ + private V1SourceStampVerifier() {} /** * Verifies the provided APK's SourceStamp signatures and returns the result of verification. @@ -58,11 +55,11 @@ public abstract class V1SourceStampVerifier { * {@code true}. If verification fails, the result will contain errors -- see {@link * ApkSigningBlockUtils.Result#getErrors()}. * - * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are - * found - * @throws IOException if an I/O error occurs when reading the APK + * found + * @throws IOException if an I/O error occurs when reading the APK */ public static ApkSigningBlockUtils.Result verify( DataSource apk, @@ -72,7 +69,7 @@ public abstract class V1SourceStampVerifier { int minSdkVersion, int maxSdkVersion) throws IOException, NoSuchAlgorithmException, - ApkSigningBlockUtils.SignatureNotFoundException { + ApkSigningBlockUtils.SignatureNotFoundException { ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); SignatureInfo signatureInfo = @@ -136,7 +133,7 @@ public abstract class V1SourceStampVerifier { apkContentDigests.entrySet()) { digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); } - Collections.sort(digests, (o1, o2) -> o1.getFirst().compareTo(o2.getFirst())); + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); return digests; } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java index 7c7156ea..0060ea5a 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +16,26 @@ package com.android.apksig.internal.apk.stamp; +import android.os.Build; +import com.android.apksig.SigningCertificateLineage; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.util.Pair; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; @@ -24,20 +43,6 @@ import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengt import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; -import com.android.apksig.internal.apk.ApkSigningBlockUtils; -import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; -import com.android.apksig.internal.apk.ContentDigestAlgorithm; -import com.android.apksig.internal.util.Pair; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - /** * SourceStamp signer. * @@ -50,21 +55,31 @@ import java.util.Map; * *

V2 of the source stamp allows signing the digests of more than one signature schemes. */ -public abstract class V2SourceStampSigner { +public class V2SourceStampSigner { + public static final int V2_SOURCE_STAMP_BLOCK_ID = SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; - public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d; + private final SignerConfig mSourceStampSignerConfig; + private final Map> mSignatureSchemeDigestInfos; + private final boolean mSourceStampTimestampEnabled; - /** - * Hidden constructor to prevent instantiation. - */ - private V2SourceStampSigner() { + /** Hidden constructor to prevent instantiation. */ + private V2SourceStampSigner(Builder builder) { + mSourceStampSignerConfig = builder.mSourceStampSignerConfig; + mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos; + mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled; } public static Pair generateSourceStampBlock( SignerConfig sourceStampSignerConfig, Map> signatureSchemeDigestInfos) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { - if (sourceStampSignerConfig.certificates.isEmpty()) { + return new Builder(sourceStampSignerConfig, + signatureSchemeDigestInfos).build().generateSourceStampBlock(); + } + + public Pair generateSourceStampBlock() + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + if (mSourceStampSignerConfig.certificates.isEmpty()) { throw new SignatureException("No certificates configured for signer"); } @@ -72,26 +87,26 @@ public abstract class V2SourceStampSigner { List> signatureSchemeDigests = new ArrayList<>(); getSignedDigestsFor( VERSION_APK_SIGNATURE_SCHEME_V3, - signatureSchemeDigestInfos, - sourceStampSignerConfig, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, signatureSchemeDigests); getSignedDigestsFor( VERSION_APK_SIGNATURE_SCHEME_V2, - signatureSchemeDigestInfos, - sourceStampSignerConfig, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, signatureSchemeDigests); getSignedDigestsFor( VERSION_JAR_SIGNATURE_SCHEME, - signatureSchemeDigestInfos, - sourceStampSignerConfig, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, signatureSchemeDigests); - Collections.sort(signatureSchemeDigests, (o1, o2) -> o1.getFirst().compareTo(o2.getFirst())); + signatureSchemeDigests.sort(Comparator.comparing(Pair::getFirst)); SourceStampBlock sourceStampBlock = new SourceStampBlock(); try { sourceStampBlock.stampCertificate = - sourceStampSignerConfig.certificates.get(0).getEncoded(); + mSourceStampSignerConfig.certificates.get(0).getEncoded(); } catch (CertificateEncodingException e) { throw new SignatureException( "Retrieving the encoded form of the stamp certificate failed", e); @@ -99,42 +114,55 @@ public abstract class V2SourceStampSigner { sourceStampBlock.signedDigests = signatureSchemeDigests; + sourceStampBlock.stampAttributes = encodeStampAttributes( + generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage)); + sourceStampBlock.signedStampAttributes = + ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig, + sourceStampBlock.stampAttributes); + // FORMAT: // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded) // * length-prefixed sequence of length-prefixed signed signature scheme digests: // * uint32: signature scheme id // * length-prefixed bytes: signed digests for the respective signature scheme + // * length-prefixed bytes: encoded stamp attributes + // * length-prefixed sequence of length-prefixed signed stamp attributes: + // * uint32: signature algorithm id + // * length-prefixed bytes: signed stamp attributes for the respective signature algorithm byte[] sourceStampSignerBlock = encodeAsSequenceOfLengthPrefixedElements( new byte[][]{ sourceStampBlock.stampCertificate, encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( sourceStampBlock.signedDigests), + sourceStampBlock.stampAttributes, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedStampAttributes), }); // FORMAT: // * length-prefixed stamp block. - return Pair.of( - encodeAsLengthPrefixedElement(sourceStampSignerBlock), V2_SOURCE_STAMP_BLOCK_ID); + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); } private static void getSignedDigestsFor( int signatureSchemeVersion, - Map> signatureSchemeDigestInfos, - SignerConfig sourceStampSignerConfig, + Map> mSignatureSchemeDigestInfos, + SignerConfig mSourceStampSignerConfig, List> signatureSchemeDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - if (!signatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) { + if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) { return; } Map digestInfo = - signatureSchemeDigestInfos.get(signatureSchemeVersion); + mSignatureSchemeDigestInfos.get(signatureSchemeVersion); List> digests = new ArrayList<>(); for (Map.Entry digest : digestInfo.entrySet()) { digests.add(Pair.of(digest.getKey().getId(), digest.getValue())); } - Collections.sort(digests, (o1, o2) -> o1.getFirst().compareTo(o2.getFirst())); + digests.sort(Comparator.comparing(Pair::getFirst)); // FORMAT: // * length-prefixed sequence of length-prefixed digests: @@ -149,7 +177,7 @@ public abstract class V2SourceStampSigner { // * length-prefixed bytes: signed digest for the respective signature algorithm List> signedDigest = ApkSigningBlockUtils.generateSignaturesOverData( - sourceStampSignerConfig, digestBytes); + mSourceStampSignerConfig, digestBytes); // FORMAT: // * length-prefixed sequence of length-prefixed signed signature scheme digests: @@ -162,8 +190,98 @@ public abstract class V2SourceStampSigner { signedDigest))); } + private static byte[] encodeStampAttributes(Map stampAttributes) { + int payloadSize = 0; + for (byte[] attributeValue : stampAttributes.values()) { + // Pair size + Attribute ID + Attribute value + payloadSize += 4 + 4 + attributeValue.length; + } + + // FORMAT (little endian): + // * length-prefixed bytes: pair + // * uint32: ID + // * bytes: value + ByteBuffer result = ByteBuffer.allocate(4 + payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize); + for (Map.Entry stampAttribute : stampAttributes.entrySet()) { + // Pair size + result.putInt(4 + stampAttribute.getValue().length); + result.putInt(stampAttribute.getKey()); + result.put(stampAttribute.getValue()); + } + return result.array(); + } + + private Map generateStampAttributes(SigningCertificateLineage lineage) { + HashMap stampAttributes = new HashMap<>(); + + if (mSourceStampTimestampEnabled) { + // Write the current epoch time as the timestamp for the source stamp. + long timestamp = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? Instant.now().getEpochSecond() + : System.currentTimeMillis() / 1000; + if (timestamp > 0) { + ByteBuffer attributeBuffer = ByteBuffer.allocate(8); + attributeBuffer.order(ByteOrder.LITTLE_ENDIAN); + attributeBuffer.putLong(timestamp); + stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID, + attributeBuffer.array()); + } else { + // The epoch time should never be <= 0, and since security decisions can potentially + // be made based on the value in the timestamp, throw an Exception to ensure the + // issues with the environment are resolved before allowing the signing. + throw new IllegalStateException( + "Received an invalid value from Instant#getTimestamp: " + timestamp); + } + } + + if (lineage != null) { + stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID, + lineage.encodeSigningCertificateLineage()); + } + return stampAttributes; + } + private static final class SourceStampBlock { public byte[] stampCertificate; public List> signedDigests; + // Optional stamp attributes that are not required for verification. + public byte[] stampAttributes; + public List> signedStampAttributes; + } + + /** Builder of {@link V2SourceStampSigner} instances. */ + public static class Builder { + private final SignerConfig mSourceStampSignerConfig; + private final Map> mSignatureSchemeDigestInfos; + private boolean mSourceStampTimestampEnabled = true; + + /** + * Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig} + * and the {@code signatureSchemeDigestInfos}. + */ + public Builder(SignerConfig sourceStampSignerConfig, + Map> signatureSchemeDigestInfos) { + mSourceStampSignerConfig = sourceStampSignerConfig; + mSignatureSchemeDigestInfos = signatureSchemeDigestInfos; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + + /** + * Builds a new V2SourceStampSigner that can be used to generate a new source stamp + * block signed with the specified signing config. + */ + public V2SourceStampSigner build() { + return new V2SourceStampSigner(this); + } } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java index d3b50135..a215b986 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +16,21 @@ package com.android.apksig.internal.apk.stamp; -import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; -import static com.android.apksig.internal.apk.stamp.V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; -import com.android.apksig.ApkVerifier; +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.Constants; 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.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; import com.android.apksig.internal.util.Pair; import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipSections; import java.io.IOException; import java.nio.BufferUnderflowException; @@ -37,6 +40,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,38 +52,34 @@ import java.util.Map; */ public abstract class V2SourceStampVerifier { - /** - * Hidden constructor to prevent instantiation. - */ - private V2SourceStampVerifier() { - } + /** Hidden constructor to prevent instantiation. */ + private V2SourceStampVerifier() {} /** * Verifies the provided APK's SourceStamp signatures and returns the result of verification. - * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is + * The APK must be considered verified only if {@link ApkSigResult#verified} is * {@code true}. If verification fails, the result will contain errors -- see {@link - * ApkSigningBlockUtils.Result#getErrors()}. + * ApkSigResult#getErrors()}. * - * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing - * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are - * found - * @throws IOException if an I/O error occurs when reading the APK + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws SignatureNotFoundException if no SourceStamp signatures are + * found + * @throws IOException if an I/O error occurs when reading the APK */ - public static ApkSigningBlockUtils.Result verify( + public static ApkSigResult verify( DataSource apk, - ApkUtils.ZipSections zipSections, + ZipSections zipSections, byte[] sourceStampCertificateDigest, Map> signatureSchemeApkContentDigests, int minSdkVersion, int maxSdkVersion) - throws IOException, NoSuchAlgorithmException, - ApkSigningBlockUtils.SignatureNotFoundException { - ApkSigningBlockUtils.Result result = - new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + ApkSigResult result = + new ApkSigResult(Constants.VERSION_SOURCE_STAMP); SignatureInfo signatureInfo = - ApkSigningBlockUtils.findSignature( - apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID, result); + ApkSigningBlockUtilsLite.findSignature( + apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID); verify( signatureInfo.signatureBlock, @@ -94,7 +94,7 @@ public abstract class V2SourceStampVerifier { /** * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided * {@code result}. APK is considered verified only if there are no errors reported in the {@code - * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for + * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for * more information about the contract of this method. */ private static void verify( @@ -103,15 +103,14 @@ public abstract class V2SourceStampVerifier { Map> signatureSchemeApkContentDigests, int minSdkVersion, int maxSdkVersion, - ApkSigningBlockUtils.Result result) + ApkSigResult result) throws NoSuchAlgorithmException { - ApkSigningBlockUtils.Result.SignerInfo signerInfo = - new ApkSigningBlockUtils.Result.SignerInfo(); - result.signers.add(signerInfo); + ApkSignerInfo signerInfo = new ApkSignerInfo(); + result.mSigners.add(signerInfo); try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); ByteBuffer sourceStampBlockData = - ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); + ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock); SourceStampVerifier.verifyV2SourceStamp( sourceStampBlockData, certFactory, @@ -124,7 +123,7 @@ public abstract class V2SourceStampVerifier { } catch (CertificateException e) { throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); } catch (ApkFormatException | BufferUnderflowException e) { - signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE); } } @@ -149,7 +148,12 @@ public abstract class V2SourceStampVerifier { apkContentDigests.entrySet()) { digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); } - Collections.sort(digests, (o1, o2) -> o1.getFirst().compareTo(o2.getFirst())); + Collections.sort(digests, new Comparator>() { + @Override + public int compare(Pair pair1, Pair pair2) { + return pair1.getFirst() - pair2.getFirst(); + } + }); return digests; } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java index 51487540..51b9810f 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java @@ -22,17 +22,12 @@ import java.util.Comparator; * Digest algorithm used with JAR signing (aka v1 signing scheme). */ public enum DigestAlgorithm { - /** - * SHA-1 - */ + /** SHA-1 */ SHA1("SHA-1"), - /** - * SHA2-256 - */ + /** SHA2-256 */ SHA256("SHA-256"); - public static Comparator BY_STRENGTH_COMPARATOR = new StrengthComparator(); private final String mJcaMessageDigestAlgorithm; private DigestAlgorithm(String jcaMessageDigestAlgoritm) { @@ -47,6 +42,8 @@ public enum DigestAlgorithm { return mJcaMessageDigestAlgorithm; } + public static Comparator BY_STRENGTH_COMPARATOR = new StrengthComparator(); + private static class StrengthComparator implements Comparator { @Override public int compare(DigestAlgorithm a1, DigestAlgorithm a2) { diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java new file mode 100644 index 00000000..db1d15f6 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 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.internal.apk.v1; + +/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */ +public class V1SchemeConstants { + private V1SchemeConstants() {} + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = + "X-Android-APK-Signed"; +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java index 5499994b..35f051cd 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,9 +16,9 @@ package com.android.apksig.internal.apk.v1; -import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid; -import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm; - +import android.os.Build; +import com.android.apksig.KeyConfig; +import com.android.apksig.SignerEngineFactory; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.asn1.Asn1EncodingException; @@ -27,11 +26,11 @@ import com.android.apksig.internal.jar.ManifestWriter; import com.android.apksig.internal.jar.SignatureFileWriter; import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; import com.android.apksig.internal.util.Pair; -import com.mcal.apksigner.utils.Base64; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -43,6 +42,7 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -54,25 +54,65 @@ import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.Manifest; +import static com.android.apksig.Constants.MAX_APK_SIGNERS; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; +import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid; +import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm; + /** * APK signer which uses JAR signing (aka v1 signing scheme). * * @see Signed JAR File */ public abstract class V1SchemeSigner { + public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME; - public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; - static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed"; private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = new Attributes.Name("Created-By"); private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; + private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = - new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); /** - * Hidden constructor to prevent instantiation. + * Signer configuration. */ + public static class SignerConfig { + /** Name. */ + public String name; + + /** + * Private key. + * + * @deprecated all internal usage has migrated to use {@link #keyConfig}. This field is not + * removed so that compilation is not broken for clients referencing it, but using this + * field may lead to unexpected errors. + */ + @Deprecated + public PrivateKey privateKey; + + /** Signing key configuration */ + public KeyConfig keyConfig; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #keyConfig}. + */ + public List certificates; + + /** + * Digest algorithm used for the signature. + */ + public DigestAlgorithm signatureDigestAlgorithm; + + /** + * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used. + */ + public boolean deterministicDsaSigning; + } + + /** Hidden constructor to prevent instantiation. */ private V1SchemeSigner() { } @@ -87,7 +127,7 @@ public abstract class V1SchemeSigner { public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) { // Prior to API Level 18, only SHA-1 can be used with RSA. if (minSdkVersion < 18) { return DigestAlgorithm.SHA1; @@ -126,9 +166,9 @@ public abstract class V1SchemeSigner { for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) { char c = nameCharsUpperCase[i]; if (((c >= 'A') && (c <= 'Z')) - || ((c >= '0') && (c <= '9')) - || (c == '-') - || (c == '_')) { + || ((c >= '0') && (c <= '9')) + || (c == '-') + || (c == '_')) { result.append(c); } else { result.append('_'); @@ -184,15 +224,12 @@ public abstract class V1SchemeSigner { // 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; + return (!"manifest.mf".equals(fileNameLowerCase)) + && (!fileNameLowerCase.endsWith(".sf")) + && (!fileNameLowerCase.endsWith(".rsa")) + && (!fileNameLowerCase.endsWith(".dsa")) + && (!fileNameLowerCase.endsWith(".ec")) + && (!fileNameLowerCase.startsWith("sig-")); } /** @@ -221,6 +258,11 @@ public abstract class V1SchemeSigner { if (signerConfigs.isEmpty()) { throw new IllegalArgumentException("At least one signer config must be provided"); } + if (signerConfigs.size() > MAX_APK_SIGNERS) { + throw new IllegalArgumentException( + "APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", " + + signerConfigs.size() + " provided"); + } OutputManifestFile manifest = generateManifestFile( jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes); @@ -276,11 +318,11 @@ public abstract class V1SchemeSigner { PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); String signatureBlockFileName = "META-INF/" + signerName + "." - + publicKey.getAlgorithm().toUpperCase(Locale.US); + + publicKey.getAlgorithm().toUpperCase(Locale.US); signatureJarEntries.add( Pair.of(signatureBlockFileName, signatureBlock)); } - signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents)); + signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents)); return signatureJarEntries; } @@ -295,10 +337,10 @@ public abstract class V1SchemeSigner { PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); String signatureBlockFileName = "META-INF/" + signerName + "." - + publicKey.getAlgorithm().toUpperCase(Locale.US); + + publicKey.getAlgorithm().toUpperCase(Locale.US); result.add(signatureBlockFileName); } - result.add(MANIFEST_ENTRY_NAME); + result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME); return result; } @@ -344,9 +386,10 @@ public abstract class V1SchemeSigner { checkEntryNameValid(entryName); byte[] entryDigest = jarEntryDigests.get(entryName); Attributes entryAttrs = new Attributes(); - entryAttrs.putValue( - entryDigestAttributeName, - Base64.encode(entryDigest)); + String encodedEntryDigest = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? Base64.getEncoder().encodeToString(entryDigest) + : com.mcal.apksigner.utils.Base64.encode(entryDigest); + entryAttrs.putValue(entryDigestAttributeName, encodedEntryDigest); ByteArrayOutputStream sectionOut = new ByteArrayOutputStream(); byte[] sectionBytes; try { @@ -382,6 +425,12 @@ public abstract class V1SchemeSigner { } } + public static class OutputManifestFile { + public byte[] contents; + public SortedMap individualSectionsContents; + public Attributes mainSectionAttributes; + } + private static byte[] generateSignatureFile( List apkSignatureSchemeIds, DigestAlgorithm manifestDigestAlgorithm, @@ -402,7 +451,7 @@ public abstract class V1SchemeSigner { if (attrValue.length() > 0) { attrValue.append(", "); } - attrValue.append(String.valueOf(id)); + attrValue.append(id); } mainAttrs.put( SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME, @@ -411,9 +460,10 @@ public abstract class V1SchemeSigner { // Add main attribute containing the digest of MANIFEST.MF. MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm); - mainAttrs.putValue( - getManifestDigestAttributeName(manifestDigestAlgorithm), - Base64.encode(md.digest(manifest.contents))); + String encodedManifestDigest = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? Base64.getEncoder().encodeToString(md.digest(manifest.contents)) + : com.mcal.apksigner.utils.Base64.encode(md.digest(manifest.contents)); + mainAttrs.putValue(getManifestDigestAttributeName(manifestDigestAlgorithm), encodedManifestDigest); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { SignatureFileWriter.writeMainSection(out, mainAttrs); @@ -427,9 +477,10 @@ public abstract class V1SchemeSigner { byte[] sectionContents = manifestSection.getValue(); byte[] sectionDigest = md.digest(sectionContents); Attributes attrs = new Attributes(); - attrs.putValue( - entryDigestAttributeName, - Base64.encode(sectionDigest)); + String encodedSectionDigest = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? Base64.getEncoder().encodeToString(sectionDigest) + : com.mcal.apksigner.utils.Base64.encode(sectionDigest); + attrs.putValue(entryDigestAttributeName, encodedSectionDigest); try { SignatureFileWriter.writeIndividualSection(out, sectionName, attrs); @@ -452,6 +503,7 @@ public abstract class V1SchemeSigner { return out.toByteArray(); } + /** * Generates the CMS PKCS #7 signature block corresponding to the provided signature file and * signing configuration. @@ -466,19 +518,20 @@ public abstract class V1SchemeSigner { PublicKey publicKey = signingCert.getPublicKey(); DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm; Pair signatureAlgs = - getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm); + getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm, + signerConfig.deterministicDsaSigning); String jcaSignatureAlgorithm = signatureAlgs.getFirst(); // Generate the cryptographic signature of the signature file byte[] signatureBytes; try { - Signature signature = Signature.getInstance(jcaSignatureAlgorithm); - signature.initSign(signerConfig.privateKey); - signature.update(signatureFileBytes); - signatureBytes = signature.sign(); + signatureBytes = + SignerEngineFactory.getImplementation( + signerConfig.keyConfig, jcaSignatureAlgorithm, null) + .sign(signatureFileBytes); } catch (InvalidKeyException e) { throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); - } catch (SignatureException e) { + } catch (InvalidAlgorithmParameterException | SignatureException e) { throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); } @@ -493,12 +546,12 @@ public abstract class V1SchemeSigner { } catch (InvalidKeyException e) { throw new InvalidKeyException( "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" - + " public key from certificate", + + " public key from certificate", e); } catch (SignatureException e) { throw new SignatureException( "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" - + " public key from certificate", + + " public key from certificate", e); } @@ -516,6 +569,7 @@ public abstract class V1SchemeSigner { } } + private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) { switch (digestAlgorithm) { case SHA1: @@ -539,36 +593,4 @@ public abstract class V1SchemeSigner { "Unexpected content digest algorithm: " + digestAlgorithm); } } - - /** - * Signer configuration. - */ - public static class SignerConfig { - /** - * Name. - */ - public String name; - - /** - * Private key. - */ - public PrivateKey privateKey; - - /** - * Certificates, with the first certificate containing the public key corresponding to - * {@link #privateKey}. - */ - public List certificates; - - /** - * Digest algorithm used for the signature. - */ - public DigestAlgorithm signatureDigestAlgorithm; - } - - public static class OutputManifestFile { - public byte[] contents; - public SortedMap individualSectionsContents; - public Attributes mainSectionAttributes; - } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java index 9cff412f..f3fd6415 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +16,7 @@ package com.android.apksig.internal.apk.v1; +import static com.android.apksig.Constants.MAX_APK_SIGNERS; import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels; import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm; import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm; @@ -27,6 +27,7 @@ import com.android.apksig.ApkVerifier.Issue; import com.android.apksig.ApkVerifier.IssueWithParams; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.asn1.Asn1BerParser; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1DecodingException; @@ -47,24 +48,29 @@ import com.android.apksig.internal.util.InclusiveIntRange; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSinks; import com.android.apksig.util.DataSource; import com.android.apksig.zip.ZipFormatException; -import com.mcal.apksigner.utils.Base64; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.security.InvalidKeyException; +import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; +import java.util.Base64.Decoder; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -82,42 +88,7 @@ import java.util.jar.Attributes; * @see Signed JAR File */ public abstract class V1SchemeVerifier { - - private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME; - private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = { - "SHA-512", - "SHA-384", - "SHA-256", - "SHA-1", - }; - private static final Map UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL; - private static final Map - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST; - - static { - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512"); - } - - static { - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( - "SHA-384", AndroidSdkVersion.GINGERBREAD); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( - "SHA-512", AndroidSdkVersion.GINGERBREAD); - } - - private V1SchemeVerifier() { - } + private V1SchemeVerifier() {} /** * Verifies the provided APK's JAR signatures and returns the result of verification. APK is @@ -130,10 +101,10 @@ public abstract class V1SchemeVerifier { * result with one or more errors and whose {@code Result.verified == false}, or this method * throws an exception. * - * @throws ApkFormatException if the APK is malformed - * @throws IOException if an I/O error occurs when reading the APK + * @throws ApkFormatException if the APK is malformed + * @throws IOException if an I/O error occurs when reading the APK * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a - * required cryptographic algorithm implementation is missing + * required cryptographic algorithm implementation is missing */ public static Result verify( DataSource apk, @@ -196,15 +167,15 @@ public abstract class V1SchemeVerifier { } /** - * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section - * representation and a mapping between entry name and its manifest section representation. - * - * @param manifestBytes raw representation of Manifest.MF - * @param cdEntryNames expected set of entry names - * @param result object to keep track of errors that happened during the parsing - * @return a pair of main entry manifest section representation and a mapping between entry name - * and its manifest section representation - */ + * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section + * representation and a mapping between entry name and its manifest section representation. + * + * @param manifestBytes raw representation of Manifest.MF + * @param cdEntryNames expected set of entry names + * @param result object to keep track of errors that happened during the parsing + * @return a pair of main entry manifest section representation and a mapping between entry name + * and its manifest section representation + */ public static Pair> parseManifest( byte[] manifestBytes, Set cdEntryNames, Result result) { ManifestParser manifest = new ManifestParser(manifestBytes); @@ -233,283 +204,6 @@ public abstract class V1SchemeVerifier { return Pair.of(manifestMainSection, entryNameToManifestSection); } - public static Collection getDigestsToVerify( - ManifestParser.Section section, - String digestAttrSuffix, - int minSdkVersion, - int maxSdkVersion) { - List result = new ArrayList<>(1); - if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { - // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is - // to rely on the ancient Digest-Algorithms attribute which contains - // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The - // first digest attribute (with supported digest algorithm) found using the list is - // used. - String algs = section.getAttributeValue("Digest-Algorithms"); - if (algs == null) { - algs = "SHA SHA1"; - } - StringTokenizer tokens = new StringTokenizer(algs); - while (tokens.hasMoreTokens()) { - String alg = tokens.nextToken(); - String attrName = alg + digestAttrSuffix; - String digestBase64 = section.getAttributeValue(attrName); - if (digestBase64 == null) { - // Attribute not found - continue; - } - alg = getCanonicalJcaMessageDigestAlgorithm(alg); - if ((alg == null) - || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg) - > minSdkVersion)) { - // Unsupported digest algorithm - continue; - } - // Supported digest algorithm - result.add(new NamedDigest(alg, Base64.decode(digestBase64))); - break; - } - // No supported digests found -- this will fail to verify on pre-JB MR2 Androids. - if (result.isEmpty()) { - return result; - } - } - - if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) { - // On JB MR2 and newer, Android platform picks the strongest algorithm out of: - // SHA-512, SHA-384, SHA-256, SHA-1. - for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { - String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); - String digestBase64 = section.getAttributeValue(attrName); - if (digestBase64 == null) { - // Attribute not found - continue; - } - byte[] digest = Base64.decode(digestBase64); - byte[] digestInResult = getDigest(result, alg); - if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) { - result.add(new NamedDigest(alg, digest)); - } - break; - } - } - - return result; - } - - private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) { - return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US)); - } - - public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( - String jcaAlgorithmName) { - Integer result = - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get( - jcaAlgorithmName.toUpperCase(Locale.US)); - return (result != null) ? result : Integer.MAX_VALUE; - } - - private static String getJarDigestAttributeName( - String jcaDigestAlgorithm, String attrNameSuffix) { - if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) { - return "SHA1" + attrNameSuffix; - } else { - return jcaDigestAlgorithm + attrNameSuffix; - } - } - - private static byte[] getDigest(Collection digests, String jcaDigestAlgorithm) { - for (NamedDigest digest : digests) { - if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) { - return digest.digest; - } - } - return null; - } - - public static List parseZipCentralDirectory( - DataSource apk, - ApkUtils.ZipSections apkSections) - throws IOException, ApkFormatException { - // Read the ZIP Central Directory - long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); - if (cdSizeBytes > Integer.MAX_VALUE) { - throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); - } - long cdOffset = apkSections.getZipCentralDirectoryOffset(); - ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); - cd.order(ByteOrder.LITTLE_ENDIAN); - - // Parse the ZIP Central Directory - int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); - List cdRecords = new ArrayList<>(expectedCdRecordCount); - for (int i = 0; i < expectedCdRecordCount; i++) { - CentralDirectoryRecord cdRecord; - int offsetInsideCd = cd.position(); - try { - cdRecord = CentralDirectoryRecord.getRecord(cd); - } catch (ZipFormatException e) { - throw new ApkFormatException( - "Malformed ZIP Central Directory record #" + (i + 1) - + " at file offset " + (cdOffset + offsetInsideCd), - e); - } - String entryName = cdRecord.getName(); - if (entryName.endsWith("/")) { - // Ignore directory entries - continue; - } - cdRecords.add(cdRecord); - } - // There may be more data in Central Directory, but we don't warn or throw because Android - // ignores unused CD data. - - return cdRecords; - } - - /** - * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's - * manifest for the APK to verify on Android. - */ - private static boolean isJarEntryDigestNeededInManifest(String entryName) { - // NOTE: This logic is different from what's required by the JAR signing scheme. This is - // because Android's APK verification logic differs from that spec. In particular, JAR - // signing spec includes into JAR manifest all files in subdirectories of META-INF and - // any files inside META-INF not related to signatures. - if (entryName.startsWith("META-INF/")) { - return false; - } - return !entryName.endsWith("/"); - } - - private static Set verifyJarEntriesAgainstManifestAndSigners( - DataSource apk, - long cdOffsetInApk, - Collection cdRecords, - Map entryNameToManifestSection, - List signers, - int minSdkVersion, - int maxSdkVersion, - Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { - // Iterate over APK contents as sequentially as possible to improve performance. - List cdRecordsSortedByLocalFileHeaderOffset = - new ArrayList<>(cdRecords); - Collections.sort( - cdRecordsSortedByLocalFileHeaderOffset, - CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); - List firstSignedEntrySigners = null; - String firstSignedEntryName = null; - for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { - String entryName = cdRecord.getName(); - if (!isJarEntryDigestNeededInManifest(entryName)) { - continue; - } - - ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); - if (manifestSection == null) { - result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); - continue; - } - - List entrySigners = new ArrayList<>(signers.size()); - for (Signer signer : signers) { - if (signer.getSigFileEntryNames().contains(entryName)) { - entrySigners.add(signer); - } - } - if (entrySigners.isEmpty()) { - result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName); - continue; - } - if (firstSignedEntrySigners == null) { - firstSignedEntrySigners = entrySigners; - firstSignedEntryName = entryName; - } else if (!entrySigners.equals(firstSignedEntrySigners)) { - result.addError( - Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH, - firstSignedEntryName, - getSignerNames(firstSignedEntrySigners), - entryName, - getSignerNames(entrySigners)); - continue; - } - - List expectedDigests = - new ArrayList<>( - getDigestsToVerify( - manifestSection, "-Digest", minSdkVersion, maxSdkVersion)); - if (expectedDigests.isEmpty()) { - result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); - continue; - } - - MessageDigest[] mds = new MessageDigest[expectedDigests.size()]; - for (int i = 0; i < expectedDigests.size(); i++) { - mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm); - } - - try { - LocalFileRecord.outputUncompressedData( - apk, - cdRecord, - cdOffsetInApk, - DataSinks.asDataSink(mds)); - } catch (ZipFormatException e) { - throw new ApkFormatException("Malformed ZIP entry: " + entryName, e); - } catch (IOException e) { - throw new IOException("Failed to read entry: " + entryName, e); - } - - for (int i = 0; i < expectedDigests.size(); i++) { - NamedDigest expectedDigest = expectedDigests.get(i); - byte[] actualDigest = mds[i].digest(); - if (!Arrays.equals(expectedDigest.digest, actualDigest)) { - result.addError( - Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, - entryName, - expectedDigest.jcaDigestAlgorithm, - V1SchemeSigner.MANIFEST_ENTRY_NAME, - Base64.encode(actualDigest), - Base64.encode(expectedDigest.digest)); - } - } - } - - if (firstSignedEntrySigners == null) { - result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES); - return Collections.emptySet(); - } else { - return new HashSet<>(firstSignedEntrySigners); - } - } - - private static List getSignerNames(List signers) { - if (signers.isEmpty()) { - return Collections.emptyList(); - } - List result = new ArrayList<>(signers.size()); - for (Signer signer : signers) { - result.add(signer.getName()); - } - return result; - } - - private static MessageDigest getMessageDigest(String algorithm) - throws NoSuchAlgorithmException { - return MessageDigest.getInstance(algorithm); - } - - private static byte[] digest(String algorithm, byte[] data, int offset, int length) - throws NoSuchAlgorithmException { - MessageDigest md = getMessageDigest(algorithm); - md.update(data, offset, length); - return md.digest(); - } - - private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { - return getMessageDigest(algorithm).digest(data); - } - /** * All JAR signers of an APK. */ @@ -540,7 +234,8 @@ public abstract class V1SchemeVerifier { if (!entryName.startsWith("META-INF/")) { continue; } - if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) { + if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals( + entryName))) { manifestEntry = cdRecord; continue; } @@ -612,6 +307,11 @@ public abstract class V1SchemeVerifier { result.addError(Issue.JAR_SIG_NO_SIGNATURES); return; } + if (signers.size() > MAX_APK_SIGNERS) { + result.addError(Issue.JAR_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, + signers.size()); + return; + } // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding // signature file .SF. Any error encountered for any signer terminates verification, to @@ -741,33 +441,6 @@ public abstract class V1SchemeVerifier { mSignatureFileEntry = sigFileEntry; } - public static List getCertificateChain( - List certs, X509Certificate leaf) { - List unusedCerts = new ArrayList<>(certs); - List result = new ArrayList<>(1); - result.add(leaf); - unusedCerts.remove(leaf); - X509Certificate root = leaf; - while (!root.getSubjectDN().equals(root.getIssuerDN())) { - Principal targetDn = root.getIssuerDN(); - boolean issuerFound = false; - for (int i = 0; i < unusedCerts.size(); i++) { - X509Certificate unusedCert = unusedCerts.get(i); - if (targetDn.equals(unusedCert.getSubjectDN())) { - issuerFound = true; - unusedCerts.remove(i); - result.add(unusedCert); - root = unusedCert; - break; - } - } - if (!issuerFound) { - break; - } - } - return result; - } - public String getName() { return mName; } @@ -798,7 +471,7 @@ public abstract class V1SchemeVerifier { public void verifySigBlockAgainstSigFile( DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion) - throws IOException, ApkFormatException, NoSuchAlgorithmException { + throws IOException, ApkFormatException, NoSuchAlgorithmException { // Obtain the signature block from the APK byte[] sigBlockBytes; try { @@ -826,7 +499,7 @@ public abstract class V1SchemeVerifier { Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class); if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) { throw new Asn1DecodingException( - "Unsupported ContentInfo.contentType: " + contentInfo.contentType); + "Unsupported ContentInfo.contentType: " + contentInfo.contentType); } signedData = Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class); @@ -930,8 +603,8 @@ public abstract class V1SchemeVerifier { byte[] signatureFile, int minSdkVersion, int maxSdkVersion) - throws Pkcs7DecodingException, NoSuchAlgorithmException, - InvalidKeyException, SignatureException { + throws Pkcs7DecodingException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException { String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm; String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm; InclusiveIntRange desiredApiLevels = @@ -1015,7 +688,27 @@ public abstract class V1SchemeVerifier { String jcaSignatureAlgorithm = getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid); Signature s = Signature.getInstance(jcaSignatureAlgorithm); - s.initVerify(signingCertificate.getPublicKey()); + PublicKey publicKey = signingCertificate.getPublicKey(); + try { + s.initVerify(publicKey); + } catch (InvalidKeyException e) { + // An InvalidKeyException could be caught if the PublicKey in the certificate is not + // properly encoded; attempt to resolve any encoding errors, generate a new public + // key, and reattempt the initVerify with the newly encoded key. + try { + byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey); + publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic( + new X509EncodedKeySpec(encodedPublicKey)); + } catch (InvalidKeySpecException ikse) { + // If an InvalidKeySpecException is caught then throw the original Exception + // since the key couldn't be properly re-encoded, and the original Exception + // will have more useful debugging info. + throw e; + } + s = Signature.getInstance(jcaSignatureAlgorithm); + s.initVerify(publicKey); + } + if (signerInfo.signedAttrs != null) { // Signed attributes present -- verify signature against the ASN.1 DER encoded form // of signed attributes. This verifies integrity of the signature file because @@ -1067,7 +760,7 @@ public abstract class V1SchemeVerifier { } byte[] actualSignatureFileDigest = MessageDigest.getInstance( - getJcaDigestAlgorithm(digestAlgorithmOid)) + getJcaDigestAlgorithm(digestAlgorithmOid)) .digest(signatureFile); if (!Arrays.equals( expectedSignatureFileDigest, actualSignatureFileDigest)) { @@ -1104,6 +797,38 @@ public abstract class V1SchemeVerifier { return signingCertificate; } + + + public static List getCertificateChain( + List certs, X509Certificate leaf) { + List unusedCerts = new ArrayList<>(certs); + List result = new ArrayList<>(1); + result.add(leaf); + unusedCerts.remove(leaf); + X509Certificate root = leaf; + while (!root.getSubjectDN().equals(root.getIssuerDN())) { + Principal targetDn = root.getIssuerDN(); + boolean issuerFound = false; + for (int i = 0; i < unusedCerts.size(); i++) { + X509Certificate unusedCert = unusedCerts.get(i); + if (targetDn.equals(unusedCert.getSubjectDN())) { + issuerFound = true; + unusedCerts.remove(i); + result.add(unusedCert); + root = unusedCert; + break; + } + } + if (!issuerFound) { + break; + } + } + return result; + } + + + + public void verifySigFileAgainstManifest( byte[] manifestBytes, ManifestParser.Section manifestMainSection, @@ -1243,11 +968,11 @@ public abstract class V1SchemeVerifier { if (!Arrays.equals(expected, actual)) { mResult.addWarning( Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, - V1SchemeSigner.MANIFEST_ENTRY_NAME, + V1SchemeConstants.MANIFEST_ENTRY_NAME, jcaDigestAlgorithm, mSignatureFileEntry.getName(), - Base64.encode(actual), - Base64.encode(expected)); + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); verified = false; } } @@ -1288,8 +1013,8 @@ public abstract class V1SchemeVerifier { Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY, jcaDigestAlgorithm, mSignatureFileEntry.getName(), - Base64.encode(actual), - Base64.encode(expected)); + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); } } } @@ -1341,8 +1066,8 @@ public abstract class V1SchemeVerifier { entryName, jcaDigestAlgorithm, mSignatureFileEntry.getName(), - Base64.encode(actual), - Base64.encode(expected)); + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); } } } @@ -1353,7 +1078,7 @@ public abstract class V1SchemeVerifier { Set foundApkSigSchemeIds) { String signedWithApkSchemes = sfMainSection.getAttributeValue( - V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); // This field contains a comma-separated list of APK signature scheme IDs which were // used to sign this APK. Android rejects APKs where an ID is known to the platform but // the APK didn't verify using that scheme. @@ -1412,6 +1137,283 @@ public abstract class V1SchemeVerifier { } } + public static Collection getDigestsToVerify( + ManifestParser.Section section, + String digestAttrSuffix, + int minSdkVersion, + int maxSdkVersion) { + Decoder base64Decoder = Base64.getDecoder(); + List result = new ArrayList<>(1); + if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { + // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is + // to rely on the ancient Digest-Algorithms attribute which contains + // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The + // first digest attribute (with supported digest algorithm) found using the list is + // used. + String algs = section.getAttributeValue("Digest-Algorithms"); + if (algs == null) { + algs = "SHA SHA1"; + } + StringTokenizer tokens = new StringTokenizer(algs); + while (tokens.hasMoreTokens()) { + String alg = tokens.nextToken(); + String attrName = alg + digestAttrSuffix; + String digestBase64 = section.getAttributeValue(attrName); + if (digestBase64 == null) { + // Attribute not found + continue; + } + alg = getCanonicalJcaMessageDigestAlgorithm(alg); + if ((alg == null) + || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg) + > minSdkVersion)) { + // Unsupported digest algorithm + continue; + } + // Supported digest algorithm + result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64))); + break; + } + // No supported digests found -- this will fail to verify on pre-JB MR2 Androids. + if (result.isEmpty()) { + return result; + } + } + + if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) { + // On JB MR2 and newer, Android platform picks the strongest algorithm out of: + // SHA-512, SHA-384, SHA-256, SHA-1. + for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { + String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); + String digestBase64 = section.getAttributeValue(attrName); + if (digestBase64 == null) { + // Attribute not found + continue; + } + byte[] digest = base64Decoder.decode(digestBase64); + byte[] digestInResult = getDigest(result, alg); + if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) { + result.add(new NamedDigest(alg, digest)); + } + break; + } + } + + return result; + } + + private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = { + "SHA-512", + "SHA-384", + "SHA-256", + "SHA-1", + }; + + private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) { + return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US)); + } + + public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( + String jcaAlgorithmName) { + Integer result = + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get( + jcaAlgorithmName.toUpperCase(Locale.US)); + return (result != null) ? result : Integer.MAX_VALUE; + } + + private static String getJarDigestAttributeName( + String jcaDigestAlgorithm, String attrNameSuffix) { + if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) { + return "SHA1" + attrNameSuffix; + } else { + return jcaDigestAlgorithm + attrNameSuffix; + } + } + + private static final Map UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL; + static { + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512"); + } + + private static final Map + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST; + static { + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( + "SHA-384", AndroidSdkVersion.GINGERBREAD); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( + "SHA-512", AndroidSdkVersion.GINGERBREAD); + } + + private static byte[] getDigest(Collection digests, String jcaDigestAlgorithm) { + for (NamedDigest digest : digests) { + if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) { + return digest.digest; + } + } + return null; + } + + public static List parseZipCentralDirectory( + DataSource apk, + ApkUtils.ZipSections apkSections) + throws IOException, ApkFormatException { + return ZipUtils.parseZipCentralDirectory(apk, apkSections); + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest for the APK to verify on Android. + */ + private static boolean isJarEntryDigestNeededInManifest(String entryName) { + // NOTE: This logic is different from what's required by the JAR signing scheme. This is + // because Android's APK verification logic differs from that spec. In particular, JAR + // signing spec includes into JAR manifest all files in subdirectories of META-INF and + // any files inside META-INF not related to signatures. + if (entryName.startsWith("META-INF/")) { + return false; + } + return !entryName.endsWith("/"); + } + + private static Set verifyJarEntriesAgainstManifestAndSigners( + DataSource apk, + long cdOffsetInApk, + Collection cdRecords, + Map entryNameToManifestSection, + List signers, + int minSdkVersion, + int maxSdkVersion, + Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { + // Iterate over APK contents as sequentially as possible to improve performance. + List cdRecordsSortedByLocalFileHeaderOffset = + new ArrayList<>(cdRecords); + Collections.sort( + cdRecordsSortedByLocalFileHeaderOffset, + CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); + List firstSignedEntrySigners = null; + String firstSignedEntryName = null; + for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { + String entryName = cdRecord.getName(); + if (!isJarEntryDigestNeededInManifest(entryName)) { + continue; + } + + ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); + if (manifestSection == null) { + result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); + continue; + } + + List entrySigners = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + if (signer.getSigFileEntryNames().contains(entryName)) { + entrySigners.add(signer); + } + } + if (entrySigners.isEmpty()) { + result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName); + continue; + } + if (firstSignedEntrySigners == null) { + firstSignedEntrySigners = entrySigners; + firstSignedEntryName = entryName; + } else if (!entrySigners.equals(firstSignedEntrySigners)) { + result.addError( + Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH, + firstSignedEntryName, + getSignerNames(firstSignedEntrySigners), + entryName, + getSignerNames(entrySigners)); + continue; + } + + List expectedDigests = + new ArrayList<>( + getDigestsToVerify( + manifestSection, "-Digest", minSdkVersion, maxSdkVersion)); + if (expectedDigests.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); + continue; + } + + MessageDigest[] mds = new MessageDigest[expectedDigests.size()]; + for (int i = 0; i < expectedDigests.size(); i++) { + mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm); + } + + try { + LocalFileRecord.outputUncompressedData( + apk, + cdRecord, + cdOffsetInApk, + DataSinks.asDataSink(mds)); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + entryName, e); + } catch (IOException e) { + throw new IOException("Failed to read entry: " + entryName, e); + } + + for (int i = 0; i < expectedDigests.size(); i++) { + NamedDigest expectedDigest = expectedDigests.get(i); + byte[] actualDigest = mds[i].digest(); + if (!Arrays.equals(expectedDigest.digest, actualDigest)) { + result.addError( + Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, + entryName, + expectedDigest.jcaDigestAlgorithm, + V1SchemeConstants.MANIFEST_ENTRY_NAME, + Base64.getEncoder().encodeToString(actualDigest), + Base64.getEncoder().encodeToString(expectedDigest.digest)); + } + } + } + + if (firstSignedEntrySigners == null) { + result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES); + return Collections.emptySet(); + } else { + return new HashSet<>(firstSignedEntrySigners); + } + } + + private static List getSignerNames(List signers) { + if (signers.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + result.add(signer.getName()); + } + return result; + } + + private static MessageDigest getMessageDigest(String algorithm) + throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm); + } + + private static byte[] digest(String algorithm, byte[] data, int offset, int length) + throws NoSuchAlgorithmException { + MessageDigest md = getMessageDigest(algorithm); + md.update(data, offset, length); + return md.digest(); + } + + private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { + return getMessageDigest(algorithm).digest(data); + } + public static class NamedDigest { public final String jcaDigestAlgorithm; public final byte[] digest; @@ -1424,21 +1426,20 @@ public abstract class V1SchemeVerifier { public static class Result { - /** - * List of APK's signers. These signers are used by Android. - */ + /** Whether the APK's JAR signature verifies. */ + public boolean verified; + + /** List of APK's signers. These signers are used by Android. */ public final List signers = new ArrayList<>(); + /** * Signers encountered in the APK but not included in the set of the APK's signers. These * signers are ignored by Android. */ public final List ignoredSigners = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); private final List mErrors = new ArrayList<>(); - /** - * Whether the APK's JAR signature verifies. - */ - public boolean verified; private boolean containsErrors() { if (!mErrors.isEmpty()) { diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java new file mode 100644 index 00000000..0e244c83 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 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.internal.apk.v2; + +/** Constants used by the V2 Signature Scheme signing and verification. */ +public class V2SchemeConstants { + private V2SchemeConstants() {} + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java index 34aeeae9..06da96cf 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java @@ -16,6 +16,7 @@ package com.android.apksig.internal.apk.v2; +import static com.android.apksig.Constants.MAX_APK_SIGNERS; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates; @@ -71,27 +72,24 @@ public abstract class V2SchemeSigner { * protected by signatures inside the block. */ - public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; - // Attribute to check whether a newer APK Signature Scheme signature was stripped - protected static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; - /** - * Hidden constructor to prevent instantiation. - */ - private V2SchemeSigner() { - } + /** Hidden constructor to prevent instantiation. */ + private V2SchemeSigner() {} /** * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the * provided key. * * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see - * AndroidManifest.xml minSdkVersion attribute). + * AndroidManifest.xml minSdkVersion attribute). * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK - * Signature Scheme v2 + * Signature Scheme v2 */ public static List getSuggestedSignatureAlgorithms(PublicKey signingKey, - int minSdkVersion, boolean verityEnabled) throws InvalidKeyException { + int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning) + throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); if ("RSA".equalsIgnoreCase(keyAlgorithm)) { // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee @@ -116,7 +114,10 @@ public abstract class V2SchemeSigner { } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { // DSA is supported only with SHA-256. List algorithms = new ArrayList<>(); - algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256); + algorithms.add( + deterministicDsaSigning ? + SignatureAlgorithm.DETDSA_WITH_SHA256 : + SignatureAlgorithm.DSA_WITH_SHA256); if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256); } @@ -143,33 +144,58 @@ public abstract class V2SchemeSigner { } public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests - generateApkSignatureSchemeV2Block( - RunnablesExecutor executor, - DataSource beforeCentralDir, - DataSource centralDir, - DataSource eocd, - List signerConfigs, - boolean v3SigningEnabled) - throws IOException, InvalidKeyException, NoSuchAlgorithmException, - SignatureException { + generateApkSignatureSchemeV2Block(RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs, + boolean v3SigningEnabled) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd, + signerConfigs, v3SigningEnabled, null); + } + + public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests + generateApkSignatureSchemeV2Block( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs, + boolean v3SigningEnabled, + List preservedV2SignerBlocks) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { Pair, Map> digestInfo = ApkSigningBlockUtils.computeContentDigests( executor, beforeCentralDir, centralDir, eocd, signerConfigs); return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests( generateApkSignatureSchemeV2Block( - digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled), + digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled, + preservedV2SignerBlocks), digestInfo.getSecond()); } private static Pair generateApkSignatureSchemeV2Block( List signerConfigs, Map contentDigests, - boolean v3SigningEnabled) + boolean v3SigningEnabled, + List preservedV2SignerBlocks) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { // FORMAT: // * length-prefixed sequence of length-prefixed signer blocks. + if (signerConfigs.size() > MAX_APK_SIGNERS) { + throw new IllegalArgumentException( + "APK Signature Scheme v2 only supports a maximum of " + MAX_APK_SIGNERS + ", " + + signerConfigs.size() + " provided"); + } + List signerBlocks = new ArrayList<>(signerConfigs.size()); + if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) { + signerBlocks.addAll(preservedV2SignerBlocks); + } int signerNumber = 0; for (SignerConfig signerConfig : signerConfigs) { signerNumber++; @@ -186,10 +212,10 @@ public abstract class V2SchemeSigner { return Pair.of( encodeAsSequenceOfLengthPrefixedElements( - new byte[][]{ - encodeAsSequenceOfLengthPrefixedElements(signerBlocks), + new byte[][] { + encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }), - APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); } private static byte[] generateSignerBlock( @@ -242,12 +268,12 @@ public abstract class V2SchemeSigner { signer.signedData = encodeAsSequenceOfLengthPrefixedElements( - new byte[][]{ - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( - signedData.digests), - encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), - signedData.additionalAttributes, - new byte[0], + new byte[][] { + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signedData.digests), + encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), + signedData.additionalAttributes, + new byte[0], }); signer.publicKey = encodedPublicKey; signer.signatures = new ArrayList<>(); @@ -261,11 +287,11 @@ public abstract class V2SchemeSigner { // * length-prefixed bytes: signature of signed data // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) return encodeAsSequenceOfLengthPrefixedElements( - new byte[][]{ - signer.signedData, - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( - signer.signatures), - signer.publicKey, + new byte[][] { + signer.signedData, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signer.signatures), + signer.publicKey, }); } @@ -279,7 +305,7 @@ public abstract class V2SchemeSigner { ByteBuffer result = ByteBuffer.allocate(payloadSize); result.order(ByteOrder.LITTLE_ENDIAN); result.putInt(payloadSize - 4); - result.putInt(STRIPPING_PROTECTION_ATTR_ID); + result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID); result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); return result.array(); } else { diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java index c07796fa..4d6e3e1a 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java @@ -16,6 +16,8 @@ package com.android.apksig.internal.apk.v2; +import static com.android.apksig.Constants.MAX_APK_SIGNERS; + import com.android.apksig.ApkVerifier.Issue; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; @@ -24,11 +26,10 @@ import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; import com.android.apksig.internal.util.ByteBufferUtils; -import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; import com.android.apksig.internal.util.X509CertificateUtils; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; import com.android.apksig.util.DataSource; import com.android.apksig.util.RunnablesExecutor; - import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; @@ -62,14 +63,8 @@ import java.util.Set; * @see APK Signature Scheme v2 */ public abstract class V2SchemeVerifier { - - private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; - - /** - * Hidden constructor to prevent instantiation. - */ - private V2SchemeVerifier() { - } + /** Hidden constructor to prevent instantiation. */ + private V2SchemeVerifier() {} /** * Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of @@ -84,12 +79,12 @@ public abstract class V2SchemeVerifier { * this method returns a result with one or more errors and whose * {@code Result.verified == false}, or this method throws an exception. * - * @throws ApkFormatException if the APK is malformed - * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing + * @throws ApkFormatException if the APK is malformed + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing * @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2 - * signatures are found - * @throws IOException if an I/O error occurs when reading the APK + * signatures are found + * @throws IOException if an I/O error occurs when reading the APK */ public static ApkSigningBlockUtils.Result verify( RunnablesExecutor executor, @@ -105,7 +100,7 @@ public abstract class V2SchemeVerifier { ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, - APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result); + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result); DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); DataSource centralDir = @@ -134,7 +129,7 @@ public abstract class V2SchemeVerifier { * Set, int, int)} for more information about the contract of this method. * * @param result result populated by this method with interesting information about the APK, - * such as information about signers, and verification errors and warnings. + * such as information about signers, and verification errors and warnings. */ private static void verify( RunnablesExecutor executor, @@ -229,6 +224,9 @@ public abstract class V2SchemeVerifier { return; } } + if (signerCount > MAX_APK_SIGNERS) { + result.addError(Issue.V2_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, signerCount); + } } /** @@ -251,8 +249,7 @@ public abstract class V2SchemeVerifier { Map supportedApkSigSchemeNames, Set foundApkSigSchemeIds, int minSdkVersion, - int maxSdkVersion) - throws ApkFormatException, NoSuchAlgorithmException { + int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException { ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock); byte[] signedDataBytes = new byte[signedData.remaining()]; signedData.get(signedDataBytes); @@ -439,7 +436,7 @@ public abstract class V2SchemeVerifier { result.additionalAttributes.add( new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value)); switch (id) { - case V2SchemeSigner.STRIPPING_PROTECTION_ATTR_ID: + case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID: // stripping protection added when signing with a newer scheme int foundId = ByteBuffer.wrap(value).order( ByteOrder.LITTLE_ENDIAN).getInt(); diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java new file mode 100644 index 00000000..dd92da34 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 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.internal.apk.v3; + +import com.android.apksig.internal.util.AndroidSdkVersion; + +/** Constants used by the V3 Signature Scheme signing and verification. */ +public class V3SchemeConstants { + private V3SchemeConstants() {} + + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; + + public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P; + public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T; + /** + * By default, APK signing key rotation will target T, but packages that have previously + * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the + * --rotation-min-sdk-version parameter when using apksigner or when invoking + * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}. + */ + public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T; + + /** + * This attribute is intended to be written to the V3.0 signer block as an additional attribute + * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If + * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version + * for rotation in the v3.1 signing block is not X, then the APK should be rejected. + */ + public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02; + + /** + * This attribute is written to the V3.1 signer block as an additional attribute to signify that + * the rotation-min-sdk-version is targeting a development release. This is required to support + * testing rotation on new development releases as the previous platform release SDK version + * is used as the development release SDK version until the development release SDK is + * finalized. + */ + public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba; + + /** + * The current development release; rotation / signing configs targeting this release should + * be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute. + */ + public static final int DEV_RELEASE = AndroidSdkVersion.U; + + /** + * The current production release. + */ + public static final int PROD_RELEASE = AndroidSdkVersion.T; +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java index 7fc9416a..28f65897 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java @@ -24,6 +24,7 @@ import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicK import com.android.apksig.SigningCertificateLineage; import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests; import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; @@ -45,6 +46,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.OptionalInt; /** * APK Signature Scheme v3 signer. @@ -53,19 +55,40 @@ import java.util.Map; * Signature Scheme v2 goals. * * @see APK Signature Scheme v2 - *

The main contribution of APK Signature Scheme v3 is the introduction of the {@link - * SigningCertificateLineage}, which enables an APK to change its signing certificate as long as - * it can prove the new siging certificate was signed by the old. + *

The main contribution of APK Signature Scheme v3 is the introduction of the {@link + * SigningCertificateLineage}, which enables an APK to change its signing certificate as long as + * it can prove the new siging certificate was signed by the old. */ -public abstract class V3SchemeSigner { +public class V3SchemeSigner { + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID; - public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; - public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; + private final RunnablesExecutor mExecutor; + private final DataSource mBeforeCentralDir; + private final DataSource mCentralDir; + private final DataSource mEocd; + private final List mSignerConfigs; + private final int mBlockId; + private final OptionalInt mOptionalV31MinSdkVersion; + private final boolean mRotationTargetsDevRelease; - /** - * Hidden constructor to prevent instantiation. - */ - private V3SchemeSigner() { + private V3SchemeSigner(DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs, + RunnablesExecutor executor, + int blockId, + OptionalInt optionalV31MinSdkVersion, + boolean rotationTargetsDevRelease) { + mBeforeCentralDir = beforeCentralDir; + mCentralDir = centralDir; + mEocd = eocd; + mSignerConfigs = signerConfigs; + mExecutor = executor; + mBlockId = blockId; + mOptionalV31MinSdkVersion = optionalV31MinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; } /** @@ -73,12 +96,13 @@ public abstract class V3SchemeSigner { * provided key. * * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see - * AndroidManifest.xml minSdkVersion attribute). + * AndroidManifest.xml minSdkVersion attribute). * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK - * Signature Scheme v3 + * Signature Scheme v3 */ public static List getSuggestedSignatureAlgorithms(PublicKey signingKey, - int minSdkVersion, boolean verityEnabled) throws InvalidKeyException { + int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning) + throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); if ("RSA".equalsIgnoreCase(keyAlgorithm)) { // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee @@ -103,7 +127,10 @@ public abstract class V3SchemeSigner { } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { // DSA is supported only with SHA-256. List algorithms = new ArrayList<>(); - algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256); + algorithms.add( + deterministicDsaSigning ? + SignatureAlgorithm.DETDSA_WITH_SHA256 : + SignatureAlgorithm.DSA_WITH_SHA256); if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256); } @@ -129,31 +156,92 @@ public abstract class V3SchemeSigner { } } - public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests - generateApkSignatureSchemeV3Block( + public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block( RunnablesExecutor executor, DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, List signerConfigs) - throws IOException, InvalidKeyException, NoSuchAlgorithmException, - SignatureException { - Pair, Map> digestInfo = - ApkSigningBlockUtils.computeContentDigests( - executor, beforeCentralDir, centralDir, eocd, signerConfigs); - return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests( - generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()), - digestInfo.getSecond()); + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { + return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) + .build() + .generateApkSignatureSchemeV3BlockAndDigests(); } - private static Pair generateApkSignatureSchemeV3Block( - List signerConfigs, Map contentDigests) + public static byte[] generateV3SignerAttribute( + SigningCertificateLineage signingCertificateLineage) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - encoded V3 SigningCertificateLineage + byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage(); + int payloadSize = 4 + 4 + encodedLineage.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(4 + encodedLineage.length); + result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID); + result.put(encodedLineage); + return result.array(); + } + + private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute( + int rotationMinSdkVersion) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - int32 representing minimum SDK version for rotation + int payloadSize = 4 + 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID); + result.putInt(rotationMinSdkVersion); + return result.array(); + } + + private static byte[] generateV31RotationTargetsDevReleaseAttribute() { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - No value is used for this attribute + int payloadSize = 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + return result.array(); + } + + /** + * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x + * signing scheme block and digests based on the parameters provided to the {@link Builder}. + * + * @throws IOException if an I/O error occurs + * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is + * missing + * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained + * @throws SignatureException if an error occurs when computing digests or generating + * signatures + */ + public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests() + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { + Pair, Map> digestInfo = + ApkSigningBlockUtils.computeContentDigests( + mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs); + return new SigningSchemeBlockAndDigests( + generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond()); + } + + private Pair generateApkSignatureSchemeV3Block( + Map contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { // FORMAT: // * length-prefixed sequence of length-prefixed signer blocks. - List signerBlocks = new ArrayList<>(signerConfigs.size()); + List signerBlocks = new ArrayList<>(mSignerConfigs.size()); int signerNumber = 0; - for (SignerConfig signerConfig : signerConfigs) { + for (SignerConfig signerConfig : mSignerConfigs) { signerNumber++; byte[] signerBlock; try { @@ -168,13 +256,13 @@ public abstract class V3SchemeSigner { return Pair.of( encodeAsSequenceOfLengthPrefixedElements( - new byte[][]{ - encodeAsSequenceOfLengthPrefixedElements(signerBlocks), + new byte[][] { + encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }), - APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + mBlockId); } - private static byte[] generateSignerBlock( + private byte[] generateSignerBlock( SignerConfig signerConfig, Map contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { if (signerConfig.certificates.isEmpty()) { @@ -224,7 +312,7 @@ public abstract class V3SchemeSigner { return encodeSigner(signer); } - private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) { + private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) { byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData); byte[] signatures = encodeAsLengthPrefixedElement( @@ -253,7 +341,7 @@ public abstract class V3SchemeSigner { return result.array(); } - private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) { + private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) { byte[] digests = encodeAsLengthPrefixedElement( encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( @@ -289,11 +377,31 @@ public abstract class V3SchemeSigner { return result.array(); } - private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) { - if (signerConfig.mSigningCertificateLineage == null) { + private byte[] generateAdditionalAttributes(SignerConfig signerConfig) { + List attributes = new ArrayList<>(); + if (signerConfig.signingCertificateLineage != null) { + attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage)); + } + if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease) + && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) { + attributes.add(generateV31RotationTargetsDevReleaseAttribute()); + } + if (mOptionalV31MinSdkVersion.isPresent() + && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) { + attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute( + mOptionalV31MinSdkVersion.getAsInt())); + } + int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum(); + byte[] attributesBuffer = new byte[attributesSize]; + if (attributesSize == 0) { return new byte[0]; } - return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute(); + int index = 0; + for (byte[] attribute : attributes) { + System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length); + index += attribute.length; + } + return attributesBuffer; } private static final class V3SignatureSchemeBlock { @@ -313,4 +421,111 @@ public abstract class V3SchemeSigner { public byte[] additionalAttributes; } } + + /** Builder of {@link V3SchemeSigner} instances. */ + public static class Builder { + private final DataSource mBeforeCentralDir; + private final DataSource mCentralDir; + private final DataSource mEocd; + private final List mSignerConfigs; + + private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED; + private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty(); + private boolean mRotationTargetsDevRelease = false; + + /** + * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code + * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to + * be used to sign the APK. + */ + public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, + List signerConfigs) { + mBeforeCentralDir = beforeCentralDir; + mCentralDir = centralDir; + mEocd = eocd; + mSignerConfigs = signerConfigs; + } + + /** + * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests. + */ + public Builder setRunnablesExecutor(RunnablesExecutor executor) { + mExecutor = executor; + return this; + } + + /** + * Sets the {@code blockId} to be used for the V3 signature block. + * + *

This {@code V3SchemeSigner} currently supports the block IDs for the {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes. + */ + public Builder setBlockId(int blockId) { + mBlockId = blockId; + return this; + } + + /** + * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each + * signer's block. + * + *

This value provides stripping protection to ensure a v3.1 signing block with rotation + * is not modified or removed from the APK's signature block. + */ + public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) { + return setMinSdkVersionForV31(rotationMinSdkVersion); + } + + /** + * Sets the {@code minSdkVersion} to be written as an additional attribute in each + * signer's block. + * + *

This value provides the stripping protection to ensure a v3.1 signing block is not + * modified or removed from the APK's signature block. + */ + public Builder setMinSdkVersionForV31(int minSdkVersion) { + if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) { + minSdkVersion = V3SchemeConstants.PROD_RELEASE; + } + mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion); + return this; + } + + /** + * Sets whether the minimum SDK version of a signer is intended to target a development + * release; this is primarily required after the T SDK is finalized, and an APK needs to + * target U during its development cycle for rotation. + * + *

This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. A signer with a minimum SDK version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + *

Note:If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + mRotationTargetsDevRelease = enabled; + return this; + } + + /** + * Returns a new {@link V3SchemeSigner} built with the configuration provided to this + * {@code Builder}. + */ + public V3SchemeSigner build() { + return new V3SchemeSigner(mBeforeCentralDir, + mCentralDir, + mEocd, + mSignerConfigs, + mExecutor, + mBlockId, + mOptionalV31MinSdkVersion, + mRotationTargetsDevRelease); + } + } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java index a7a33650..bd808f0e 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +19,7 @@ package com.android.apksig.internal.apk.v3; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; +import com.android.apksig.ApkVerificationIssue; import com.android.apksig.ApkVerifier.Issue; import com.android.apksig.SigningCertificateLineage; import com.android.apksig.apk.ApkFormatException; @@ -29,7 +29,6 @@ import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundExc import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; -import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.ByteBufferUtils; import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; import com.android.apksig.internal.util.X509CertificateUtils; @@ -39,6 +38,7 @@ import com.android.apksig.util.RunnablesExecutor; import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; @@ -55,6 +55,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.OptionalInt; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; @@ -68,14 +69,41 @@ import java.util.TreeMap; * * @see APK Signature Scheme v2 */ -public abstract class V3SchemeVerifier { +public class V3SchemeVerifier { + private final RunnablesExecutor mExecutor; + private final DataSource mApk; + private final ApkUtils.ZipSections mZipSections; + private final ApkSigningBlockUtils.Result mResult; + private final Set mContentDigestsToVerify; + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + private final int mBlockId; + private final OptionalInt mOptionalRotationMinSdkVersion; + private final boolean mFullVerification; - private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + private ByteBuffer mApkSignatureSchemeV3Block; - /** - * Hidden constructor to prevent instantiation. - */ - private V3SchemeVerifier() { + private V3SchemeVerifier( + RunnablesExecutor executor, + DataSource apk, + ApkUtils.ZipSections zipSections, + Set contentDigestsToVerify, + ApkSigningBlockUtils.Result result, + int minSdkVersion, + int maxSdkVersion, + int blockId, + OptionalInt optionalRotationMinSdkVersion, + boolean fullVerification) { + mExecutor = executor; + mApk = apk; + mZipSections = zipSections; + mContentDigestsToVerify = contentDigestsToVerify; + mResult = result; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + mBlockId = blockId; + mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion; + mFullVerification = fullVerification; } /** @@ -91,12 +119,15 @@ public abstract class V3SchemeVerifier { * this method returns a result with one or more errors and whose * {@code Result.verified == false}, or this method throws an exception. * - * @throws ApkFormatException if the APK is malformed - * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing + *

This method only verifies the v3.0 signing block without platform targeted rotation from + * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence + * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}. + * + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing * @throws SignatureNotFoundException if no APK Signature Scheme v3 - * signatures are found - * @throws IOException if an I/O error occurs when reading the APK + * signatures are found + * @throws IOException if an I/O error occurs when reading the APK */ public static ApkSigningBlockUtils.Result verify( RunnablesExecutor executor, @@ -105,34 +136,11 @@ public abstract class V3SchemeVerifier { int minSdkVersion, int maxSdkVersion) throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { - ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); - SignatureInfo signatureInfo = - ApkSigningBlockUtils.findSignature(apk, zipSections, - APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); - - DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); - DataSource centralDir = - apk.slice( - signatureInfo.centralDirOffset, - signatureInfo.eocdOffset - signatureInfo.centralDirOffset); - ByteBuffer eocd = signatureInfo.eocd; - - // v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported - // platforms - if (minSdkVersion < AndroidSdkVersion.P) { - minSdkVersion = AndroidSdkVersion.P; - } - - verify(executor, - beforeApkSigningBlock, - signatureInfo.signatureBlock, - centralDir, - eocd, - minSdkVersion, - maxSdkVersion, - result); - return result; + return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) + .build() + .verify(); } /** @@ -141,34 +149,41 @@ public abstract class V3SchemeVerifier { * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int, * int)} for more information about the contract of this method. * - * @param result result populated by this method with interesting information about the APK, - * such as information about signers, and verification errors and warnings. + * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the + * APK, such as information about signers, and verification errors and warnings */ - private static void verify( - RunnablesExecutor executor, - DataSource beforeApkSigningBlock, - ByteBuffer apkSignatureSchemeV3Block, - DataSource centralDir, - ByteBuffer eocd, - int minSdkVersion, - int maxSdkVersion, - ApkSigningBlockUtils.Result result) - throws IOException, NoSuchAlgorithmException { - Set contentDigestsToVerify = new HashSet<>(1); - parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result); - - if (result.containsErrors()) { - return; + public ApkSigningBlockUtils.Result verify() + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + if (mApk == null || mZipSections == null) { + throw new IllegalStateException( + "A non-null apk and zip sections must be specified to verify an APK's v3 " + + "signatures"); } - ApkSigningBlockUtils.verifyIntegrity( - executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result); + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult); + mApkSignatureSchemeV3Block = signatureInfo.signatureBlock; + + DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset); + DataSource centralDir = + mApk.slice( + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset - signatureInfo.centralDirOffset); + ByteBuffer eocd = signatureInfo.eocd; + + parseSigners(); + + if (mResult.containsErrors()) { + return mResult; + } + ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd, + mContentDigestsToVerify, mResult); // make sure that the v3 signers cover the entire targeted sdk version ranges and that the // longest SigningCertificateHistory, if present, corresponds to the newest platform // versions SortedMap sortedSigners = new TreeMap<>(); - for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) { - sortedSigners.put(signer.minSdkVersion, signer); + for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) { + sortedSigners.put(signer.maxSdkVersion, signer); } // first make sure there is neither overlap nor holes @@ -177,7 +192,7 @@ public abstract class V3SchemeVerifier { int lastLineageSize = 0; // while we're iterating through the signers, build up the list of lineages - List lineages = new ArrayList<>(result.signers.size()); + List lineages = new ArrayList<>(mResult.signers.size()); for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) { int currentMin = signer.minSdkVersion; @@ -186,8 +201,11 @@ public abstract class V3SchemeVerifier { // first round sets up our basis firstMin = currentMin; } else { - if (currentMin != lastMax + 1) { - result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS); + // A signer's minimum SDK can equal the previous signer's maximum SDK if this signer + // is targeting a development release. + if (currentMin != (lastMax + 1) + && !(currentMin == lastMax && signerTargetsDevRelease(signer))) { + mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS); break; } } @@ -197,7 +215,7 @@ public abstract class V3SchemeVerifier { if (signer.signingCertificateLineage != null) { int currLineageSize = signer.signingCertificateLineage.size(); if (currLineageSize < lastLineageSize) { - result.addError(Issue.V3_INCONSISTENT_LINEAGES); + mResult.addError(Issue.V3_INCONSISTENT_LINEAGES); break; } lastLineageSize = currLineageSize; @@ -205,20 +223,24 @@ public abstract class V3SchemeVerifier { } } - // make sure we support our desired sdk ranges - if (firstMin > minSdkVersion || lastMax < maxSdkVersion) { - result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax); + // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block + // then the max level only needs to support up to that sdk version for rotation. + if (firstMin > mMinSdkVersion + || lastMax < (mOptionalRotationMinSdkVersion.isPresent() + ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) { + mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax); } try { - result.signingCertificateLineage = + mResult.signingCertificateLineage = SigningCertificateLineage.consolidateLineages(lineages); } catch (IllegalArgumentException e) { - result.addError(Issue.V3_INCONSISTENT_LINEAGES); + mResult.addError(Issue.V3_INCONSISTENT_LINEAGES); } - if (!result.containsErrors()) { - result.verified = true; + if (!mResult.containsErrors()) { + mResult.verified = true; } + return mResult; } /** @@ -237,16 +259,49 @@ public abstract class V3SchemeVerifier { ByteBuffer apkSignatureSchemeV3Block, Set contentDigestsToVerify, ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException { + try { + new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block) + .setResult(result) + .setContentDigestsToVerify(contentDigestsToVerify) + .setFullVerification(false) + .build() + .parseSigners(); + } catch (IOException | SignatureNotFoundException e) { + // This should never occur since the apkSignatureSchemeV3Block was already provided. + throw new IllegalStateException("An exception was encountered when attempting to parse" + + " the signers from the provided APK Signature Scheme v3 block", e); + } + } + + /** + * Parses each signer in the APK Signature Scheme v3 block and populates corresponding + * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the + * returned {@link ApkSigningBlockUtils.Result}. + * + *

This verifies signatures over {@code signed-data} block contained in each signer block. + * However, this does not verify the integrity of the rest of the APK but rather simply reports + * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}). + * + *

This method adds one or more errors to the returned {@code Result} if a verification error + * is encountered when parsing the signers. + */ + public ApkSigningBlockUtils.Result parseSigners() + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { ByteBuffer signers; try { - signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block); + if (mApkSignatureSchemeV3Block == null) { + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult); + mApkSignatureSchemeV3Block = signatureInfo.signatureBlock; + } + signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block); } catch (ApkFormatException e) { - result.addError(Issue.V3_SIG_MALFORMED_SIGNERS); - return; + mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS); + return mResult; } if (!signers.hasRemaining()) { - result.addError(Issue.V3_SIG_NO_SIGNERS); - return; + mResult.addError(Issue.V3_SIG_NO_SIGNERS); + return mResult; } CertificateFactory certFactory; @@ -262,15 +317,16 @@ public abstract class V3SchemeVerifier { ApkSigningBlockUtils.Result.SignerInfo signerInfo = new ApkSigningBlockUtils.Result.SignerInfo(); signerInfo.index = signerIndex; - result.signers.add(signerInfo); + mResult.signers.add(signerInfo); try { ByteBuffer signer = getLengthPrefixedSlice(signers); - parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify); + parseSigner(signer, certFactory, signerInfo); } catch (ApkFormatException | BufferUnderflowException e) { signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER); - return; + return mResult; } } + return mResult; } /** @@ -285,11 +341,8 @@ public abstract class V3SchemeVerifier { * expected to be encountered on an Android platform version in the * {@code [minSdkVersion, maxSdkVersion]} range. */ - private static void parseSigner( - ByteBuffer signerBlock, - CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, - Set contentDigestsToVerify) + private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory, + ApkSigningBlockUtils.Result.SignerInfo result) throws ApkFormatException, NoSuchAlgorithmException { ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); byte[] signedDataBytes = new byte[signedData.remaining()]; @@ -379,7 +432,7 @@ public abstract class V3SchemeVerifier { return; } result.verifiedSignatures.put(signatureAlgorithm, sigBytes); - contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); + mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); } catch (InvalidKeyException | InvalidAlgorithmParameterException | SignatureException e) { result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e); @@ -439,7 +492,8 @@ public abstract class V3SchemeVerifier { X509Certificate mainCertificate = result.certs.get(0); byte[] certificatePublicKeyBytes; try { - certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(mainCertificate.getPublicKey()); + certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey( + mainCertificate.getPublicKey()); } catch (InvalidKeyException e) { System.out.println("Caught an exception encoding the public key: " + e); e.printStackTrace(); @@ -489,6 +543,7 @@ public abstract class V3SchemeVerifier { // Parse the additional attributes block. int additionalAttributeCount = 0; + boolean rotationAttrFound = false; while (additionalAttributes.hasRemaining()) { additionalAttributeCount++; try { @@ -498,7 +553,7 @@ public abstract class V3SchemeVerifier { byte[] value = ByteBufferUtils.toByteArray(attribute); result.additionalAttributes.add( new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value)); - if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) { + if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) { try { // SigningCertificateLineage is verified when built result.signingCertificateLineage = @@ -516,6 +571,31 @@ public abstract class V3SchemeVerifier { } catch (Exception e) { result.addError(Issue.V3_SIG_MALFORMED_LINEAGE); } + } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) { + rotationAttrFound = true; + // API targeting for rotation was added with V3.1; if the maxSdkVersion + // does not support v3.1 then ignore this attribute. + if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + && mFullVerification) { + int attrRotationMinSdkVersion = ByteBuffer.wrap(value) + .order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (mOptionalRotationMinSdkVersion.isPresent()) { + int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt(); + if (attrRotationMinSdkVersion != rotationMinSdkVersion) { + result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH, + attrRotationMinSdkVersion, rotationMinSdkVersion); + } + } else { + result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion); + } + } + } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) { + // This attribute should only be used by a v3.1 signer to indicate rotation + // is targeting the development release that is using the SDK version of the + // previously released platform version. + if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) { + result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER); + } } else { result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id); } @@ -525,5 +605,179 @@ public abstract class V3SchemeVerifier { return; } } + if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) { + result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING, + mOptionalRotationMinSdkVersion.getAsInt()); + } + } + + /** + * Returns whether the specified {@code signerInfo} is targeting a development release. + */ + public static boolean signerTargetsDevRelease( + ApkSigningBlockUtils.Result.SignerInfo signerInfo) { + boolean result = signerInfo.additionalAttributes.stream() + .mapToInt(attribute -> attribute.getId()) + .anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + return result; + } + + /** Builder of {@link V3SchemeVerifier} instances. */ + public static class Builder { + private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED; + private DataSource mApk; + private ApkUtils.ZipSections mZipSections; + private ByteBuffer mApkSignatureSchemeV3Block; + private Set mContentDigestsToVerify; + private ApkSigningBlockUtils.Result mResult; + private int mMinSdkVersion; + private int mMaxSdkVersion; + private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + private boolean mFullVerification = true; + private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty(); + + /** + * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to + * verify the V3 signing block of the provided {@code apk} with the specified {@code + * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}. + */ + public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion, + int maxSdkVersion) { + mApk = apk; + mZipSections = zipSections; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to + * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code + * apkSignatureSchemeV3Block}. + * + * Full verification of the v3 signature is not possible when instantiating a new + * {@code V3SchemeVerifier} with this method. + */ + public Builder(ByteBuffer apkSignatureSchemeV3Block) { + mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block; + } + + /** + * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests. + */ + public Builder setRunnablesExecutor(RunnablesExecutor executor) { + mExecutor = executor; + return this; + } + + /** + * Sets the V3 {code blockId} to be verified in the provided APK. + * + *

This {@code V3SchemeVerifier} currently supports the block IDs for the {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes. + */ + public Builder setBlockId(int blockId) { + mBlockId = blockId; + return this; + } + + /** + * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional + * attribute. + * + *

This value can be obtained from the signers returned when verifying the v3.1 signing + * block of an APK; in the case of multiple signers targeting different SDK versions in the + * v3.1 signing block, the minimum SDK version from all the signers should be used. + */ + public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) { + mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion); + return this; + } + + /** + * Sets the {@code result} instance to be used when returning verification results. + * + *

This method can be used when the caller already has a {@link + * ApkSigningBlockUtils.Result} and wants to store the verification results in this + * instance. + */ + public Builder setResult(ApkSigningBlockUtils.Result result) { + mResult = result; + return this; + } + + /** + * Sets the instance to be used to store the {@code contentDigestsToVerify}. + * + *

This method can be used when the caller needs access to the {@code + * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}. + */ + public Builder setContentDigestsToVerify( + Set contentDigestsToVerify) { + mContentDigestsToVerify = contentDigestsToVerify; + return this; + } + + /** + * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built + * from this instance. + * + * {@link #verify()} will always verify the content digests for the APK, but this + * allows verification of the rotation minimum SDK version stripping attribute to be skipped + * for scenarios where this value may not have been parsed from a V3.1 signing block (such + * as when only {@link #parseSigners()} will be invoked. + */ + public Builder setFullVerification(boolean fullVerification) { + mFullVerification = fullVerification; + return this; + } + + /** + * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this + * {@code Builder}. + */ + public V3SchemeVerifier build() { + int sigSchemeVersion; + switch (mBlockId) { + case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID: + sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; + mMinSdkVersion = Math.max(mMinSdkVersion, + V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT); + break; + case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID: + sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; + // V3.1 supports targeting an SDK version later than that of the initial release + // in which it is supported; allow any range for V3.1 as long as V3.0 covers the + // rest of the range. + mMinSdkVersion = mMaxSdkVersion; + break; + default: + throw new IllegalArgumentException( + String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x", + mBlockId)); + } + if (mResult == null) { + mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion); + } + if (mContentDigestsToVerify == null) { + mContentDigestsToVerify = new HashSet<>(1); + } + + V3SchemeVerifier verifier = new V3SchemeVerifier( + mExecutor, + mApk, + mZipSections, + mContentDigestsToVerify, + mResult, + mMinSdkVersion, + mMaxSdkVersion, + mBlockId, + mOptionalRotationMinSdkVersion, + mFullVerification); + if (mApkSignatureSchemeV3Block != null) { + verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block; + } + return verifier; + } } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java index 0b117d47..4ae7a536 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java @@ -45,6 +45,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Objects; /** * APK Signer Lineage. @@ -152,14 +153,14 @@ public class V3SigningCertificateLineage { lastCert, SignatureAlgorithm.findById(signedSigAlgorithm), SignatureAlgorithm.findById(sigAlgorithmId), signature, flags)); } - } catch (ApkFormatException | BufferUnderflowException e) { + } catch(ApkFormatException | BufferUnderflowException e){ throw new IOException("Failed to parse V3SigningCertificateLineage object", e); - } catch (NoSuchAlgorithmException | InvalidKeyException - | InvalidAlgorithmParameterException | SignatureException e) { + } catch(NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e){ throw new SecurityException( "Failed to verify signature over signed data for certificate #" + nodeCount + " when parsing V3SigningCertificateLineage object", e); - } catch (CertificateException e) { + } catch(CertificateException e){ throw new SecurityException("Failed to decode certificate #" + nodeCount + " when parsing V3SigningCertificateLineage object", e); } @@ -184,7 +185,7 @@ public class V3SigningCertificateLineage { for (SigningCertificateNode node : signingCertificateLineage) { nodes.add(encodeSigningCertificateNode(node)); } - byte[] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes); + byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes); // add the version code (uint32) on top of the encoded nodes int payloadSize = 4 + encodedSigningCertificateLineage.length; @@ -245,31 +246,6 @@ public class V3SigningCertificateLineage { */ public static class SigningCertificateNode { - /** - * the signing cert for this node. This is part of the data signed by the parent node. - */ - public final X509Certificate signingCert; - /** - * the algorithm used by the this node's parent to bless this data. Its ID value is part of - * the data signed by the parent node. {@code null} for first node. - */ - public final SignatureAlgorithm parentSigAlgorithm; - /** - * signature over the signed data (above). The signature is from this node's parent - * signing certificate, which should correspond to the signing certificate used to sign an - * APK before rotating to this one, and is formed using {@code signatureAlgorithm}. - */ - public final byte[] signature; - /** - * the algorithm used by the this nodeto bless the next node's data. Its ID value is part - * of the signed data of the next node. {@code null} for the last node. - */ - public SignatureAlgorithm sigAlgorithm; - /** - * the flags detailing how the platform should treat this signing cert - */ - public int flags; - public SigningCertificateNode( X509Certificate signingCert, SignatureAlgorithm parentSigAlgorithm, @@ -298,5 +274,41 @@ public class V3SigningCertificateLineage { // we made it return true; } + + @Override + public int hashCode() { + int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags); + result = 31 * result + Arrays.hashCode(signature); + return result; + } + + /** + * the signing cert for this node. This is part of the data signed by the parent node. + */ + public final X509Certificate signingCert; + + /** + * the algorithm used by the this node's parent to bless this data. Its ID value is part of + * the data signed by the parent node. {@code null} for first node. + */ + public final SignatureAlgorithm parentSigAlgorithm; + + /** + * the algorithm used by the this nodeto bless the next node's data. Its ID value is part + * of the signed data of the next node. {@code null} for the last node. + */ + public SignatureAlgorithm sigAlgorithm; + + /** + * signature over the signed data (above). The signature is from this node's parent + * signing certificate, which should correspond to the signing certificate used to sign an + * APK before rotating to this one, and is formed using {@code signatureAlgorithm}. + */ + public final byte[] signature; + + /** + * the flags detailing how the platform should treat this signing cert + */ + public int flags; } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java index 19d1bcd6..416cf87c 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +16,16 @@ package com.android.apksig.internal.apk.v4; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates; -import static com.android.apksig.internal.apk.v2.V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; -import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; +import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils; -import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; @@ -45,9 +47,11 @@ import java.security.PublicKey; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -62,7 +66,6 @@ import java.util.Set; *

* (optional) verityTree: integer size prepended bytes of the verity hash tree. *

- * TODO(schfan): Add v4 unit tests */ public abstract class V4SchemeSigner { /** @@ -71,15 +74,33 @@ public abstract class V4SchemeSigner { private V4SchemeSigner() { } + public static class SignerConfig { + final public ApkSigningBlockUtils.SignerConfig v4Config; + final public ApkSigningBlockUtils.SignerConfig v41Config; + + public SignerConfig(List v4Configs, + List v41Configs) throws InvalidKeyException { + if (v4Configs == null || v4Configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4 Signature."); + } + if (v41Configs != null && v41Configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature."); + } + this.v4Config = v4Configs.get(0); + this.v41Config = v41Configs != null ? v41Configs.get(0) : null; + } + } + /** * Based on a public key, return a signing algorithm that supports verity. */ public static List getSuggestedSignatureAlgorithms(PublicKey signingKey, - int minSdkVersion, boolean apkSigningBlockPaddingSupported) + int minSdkVersion, boolean apkSigningBlockPaddingSupported, + boolean deterministicDsaSigning) throws InvalidKeyException { List algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( signingKey, minSdkVersion, - apkSigningBlockPaddingSupported); + apkSigningBlockPaddingSupported, deterministicDsaSigning); // Keeping only supported algorithms. for (Iterator iter = algorithms.listIterator(); iter.hasNext(); ) { final SignatureAlgorithm algorithm = iter.next(); @@ -95,21 +116,19 @@ public abstract class V4SchemeSigner { * output file. */ public static void generateV4Signature( - DataSource apkContent, SignerConfig signerConfig, File outputFile) - throws IOException, InvalidKeyException, NoSuchAlgorithmException { - Pair pair = generateV4Signature(apkContent, signerConfig); - try (final OutputStream output = new FileOutputStream(outputFile)) { - pair.getFirst().writeTo(output); - V4Signature.writeBytes(output, pair.getSecond()); - } catch (IOException e) { - outputFile.delete(); - throw e; - } + DataSource apkContent, SignerConfig signerConfig, File outputFile) + throws IOException, InvalidKeyException, NoSuchAlgorithmException { + Pair pair = generateV4Signature(apkContent, signerConfig); + try (final OutputStream output = new FileOutputStream(outputFile)) { + pair.getFirst().writeTo(output); + V4Signature.writeBytes(output, pair.getSecond()); + } catch (IOException e) { + outputFile.delete(); + throw e; + } } - /** - * Generate v4 signature and hash tree for a given APK. - */ + /** Generate v4 signature and hash tree for a given APK. */ public static Pair generateV4Signature( DataSource apkContent, SignerConfig signerConfig) @@ -121,8 +140,9 @@ public abstract class V4SchemeSigner { final long fileSize = apkContent.size(); - // Obtaining first supported digest from v2/v3 blocks (SHA256 or SHA512). - final byte[] apkDigest = getApkDigest(apkContent); + // Obtaining the strongest supported digest for each of the v2/v3/v3.1 blocks + // (CHUNKED_SHA256 or CHUNKED_SHA512). + final Map apkDigests = getApkDigests(apkContent); // Obtaining the merkle tree and the root hash in verity format. ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo = @@ -142,7 +162,7 @@ public abstract class V4SchemeSigner { // Generating SigningInfo and combining everything into V4Signature. final V4Signature signature; try { - signature = generateSignature(signerConfig, hashingInfo, apkDigest, additionalData, + signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData, fileSize); } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) { throw new InvalidKeyException("Signer failed", e); @@ -151,18 +171,15 @@ public abstract class V4SchemeSigner { return Pair.of(signature, tree); } - private static V4Signature generateSignature( - SignerConfig signerConfig, + private static V4Signature.SigningInfo generateSigningInfo( + ApkSigningBlockUtils.SignerConfig signerConfig, V4Signature.HashingInfo hashingInfo, - byte[] apkDigest, byte[] additionaData, long fileSize) + byte[] apkDigest, byte[] additionalData, long fileSize) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, CertificateEncodingException { if (signerConfig.certificates.isEmpty()) { throw new SignatureException("No certificates configured for signer"); } - if (signerConfig.certificates.size() != 1) { - throw new CertificateEncodingException("Should only have one certificate"); - } // Collecting data for signing. final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); @@ -171,9 +188,9 @@ public abstract class V4SchemeSigner { final byte[] encodedCertificate = encodedCertificates.get(0); final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest, - encodedCertificate, additionaData, publicKey.getEncoded(), -1, null); + encodedCertificate, additionalData, publicKey.getEncoded(), -1, null); - final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo, + final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo, signingInfoNoSignature); // Signing. @@ -186,16 +203,53 @@ public abstract class V4SchemeSigner { final int signatureAlgorithmId = signatures.get(0).getFirst(); final byte[] signature = signatures.get(0).getSecond(); - final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest, - encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId, + return new V4Signature.SigningInfo(apkDigest, + encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId, signature); - - return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(), - signingInfo.toByteArray()); } - // Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type. - private static byte[] getApkDigest(DataSource apk) throws IOException { + private static V4Signature generateSignature( + SignerConfig signerConfig, + V4Signature.HashingInfo hashingInfo, + Map apkDigests, byte[] additionalData, long fileSize) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + CertificateEncodingException { + byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3) + ? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3) + : apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2); + final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config, + hashingInfo, apkDigest, additionalData, fileSize); + + final V4Signature.SigningInfos signingInfos; + if (signerConfig.v41Config != null) { + if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) { + throw new IllegalStateException( + "V4.1 cannot be signed without a V3.1 content digest"); + } + apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31); + final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock( + APK_SIGNATURE_SCHEME_V31_BLOCK_ID, + generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest, + additionalData, fileSize).toByteArray()); + signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock); + } else { + signingInfos = new V4Signature.SigningInfos(signingInfo); + } + + return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(), + signingInfos.toByteArray()); + } + + /** + * Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the + * strongest supported content digest found in that version's signature block for the V2, + * V3, and V3.1 signatures in the provided {@code apk}. + * + *

If a supported content digest algorithm is not found in any of the signature blocks, + * or if the APK is not signed by any of these signature schemes, then an {@code IOException} + * is thrown. + */ + private static Map getApkDigests(DataSource apk) throws IOException { ApkUtils.ZipSections zipSections; try { zipSections = ApkUtils.findZipSections(apk); @@ -203,34 +257,60 @@ public abstract class V4SchemeSigner { throw new IOException("Malformed APK: not a ZIP archive", e); } - final SignatureException v3Exception; + Map sigSchemeToDigest = new HashMap<>(1); try { - return getBestV3Digest(apk, zipSections); + byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31); + sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest); + } catch (SignatureException expected) { + // It is expected to catch a SignatureException if the APK does not have a v3.1 + // signature. + } + + SignatureException v3Exception = null; + try { + byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3); + sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest); } catch (SignatureException e) { v3Exception = e; } - final SignatureException v2Exception; + SignatureException v2Exception = null; try { - return getBestV2Digest(apk, zipSections); + byte[] digest = getBestV2Digest(apk, zipSections); + sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest); } catch (SignatureException e) { v2Exception = e; } + if (sigSchemeToDigest.size() > 0) { + return sigSchemeToDigest; + } + throw new IOException( "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: " + v2Exception); } - private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections) - throws SignatureException { + private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections, + int v3SchemeVersion) throws SignatureException { final Set contentDigestsToVerify = new HashSet<>(1); final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + v3SchemeVersion); + final int blockId; + switch (v3SchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V31: + blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID; + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + break; + default: + throw new IllegalArgumentException( + "Invalid V3 scheme provided: " + v3SchemeVersion); + } try { final SignatureInfo signatureInfo = - ApkSigningBlockUtils.findSignature(apk, zipSections, - APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); + ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result); final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock; V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result); @@ -316,8 +396,6 @@ public abstract class V4SchemeSigner { return bestDigest; } - // Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest - // verification in framework works. public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) { switch (contentDigestAlgorithm) { case CHUNKED_SHA256: @@ -326,19 +404,20 @@ public abstract class V4SchemeSigner { return 1; case CHUNKED_SHA512: return 2; + default: + return -1; } - return -1; } private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm, - boolean forV3Digest) { + boolean forV3Digest) { if (contentDigestAlgorithm == null) { return false; } if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256 || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512 || (forV3Digest - && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) { + && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) { return true; } return false; diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java index 2c3aee49..c0a90136 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java @@ -90,20 +90,37 @@ public abstract class V4SchemeVerifier { V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray( signature.hashingInfo); - V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray( - signature.signingInfo); - final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo); + V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray( + signature.signingInfos); - // First, verify the signature over signedData. - ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock( - signingInfo, signedData); - result.signers.add(signerInfo); - if (result.containsErrors()) { - return result; + final ApkSigningBlockUtils.Result.SignerInfo signerInfo; + + // Verify the primary signature over signedData. + { + V4Signature.SigningInfo signingInfo = signingInfos.signingInfo; + final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, + signingInfo); + signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData); + result.signers.add(signerInfo); + if (result.containsErrors()) { + return result; + } } - // Second, check if the root hash and the tree are correct. + // Verify all subsequent signatures. + for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) { + V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray( + signingInfoBlock.signingInfo); + final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, + signingInfo); + result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData)); + if (result.containsErrors()) { + return result; + } + } + + // Check if the root hash and the tree are correct. verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree); if (!result.containsErrors()) { result.verified = true; @@ -218,8 +235,8 @@ public abstract class V4SchemeVerifier { } private static void verifyRootHashAndTree(DataSource apkContent, - ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest, - byte[] expectedTree) throws IOException, NoSuchAlgorithmException { + ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest, + byte[] expectedTree) throws IOException, NoSuchAlgorithmException { ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo = ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent); diff --git a/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java index 09df9961..1eac5a26 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java +++ b/apksigner/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java @@ -22,20 +22,186 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; public class V4Signature { public static final int CURRENT_VERSION = 2; public static final int HASHING_ALGORITHM_SHA256 = 1; public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12; - public final int version; // Always 2 for now. - public final byte[] hashingInfo; - public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later. - V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) { + public static final int MAX_SIGNING_INFOS_SIZE = 7168; + + public static class HashingInfo { + public final int hashAlgorithm; // only 1 == SHA256 supported + public final byte log2BlockSize; // only 12 (block size 4096) supported now + public final byte[] salt; // used exactly as in fs-verity, 32 bytes max + public final byte[] rawRootHash; // salted digest of the first Merkle tree page + + HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) { + this.hashAlgorithm = hashAlgorithm; + this.log2BlockSize = log2BlockSize; + this.salt = salt; + this.rawRootHash = rawRootHash; + } + + static HashingInfo fromByteArray(byte[] bytes) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + final int hashAlgorithm = buffer.getInt(); + final byte log2BlockSize = buffer.get(); + byte[] salt = readBytes(buffer); + byte[] rawRootHash = readBytes(buffer); + return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash); + } + + byte[] toByteArray() { + final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt) + + bytesSize(this.rawRootHash); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(this.hashAlgorithm); + buffer.put(this.log2BlockSize); + writeBytes(buffer, this.salt); + writeBytes(buffer, this.rawRootHash); + return buffer.array(); + } + } + + public static class SigningInfo { + public final byte[] apkDigest; // used to match with the corresponding APK + public final byte[] certificate; // ASN.1 DER form + public final byte[] additionalData; // a free-form binary data blob + public final byte[] publicKey; // ASN.1 DER, must match the certificate + public final int signatureAlgorithmId; // see the APK v2 doc for the list + public final byte[] signature; + + SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData, + byte[] publicKey, int signatureAlgorithmId, byte[] signature) { + this.apkDigest = apkDigest; + this.certificate = certificate; + this.additionalData = additionalData; + this.publicKey = publicKey; + this.signatureAlgorithmId = signatureAlgorithmId; + this.signature = signature; + } + + static SigningInfo fromByteArray(byte[] bytes) throws IOException { + return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)); + } + + static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException { + byte[] apkDigest = readBytes(buffer); + byte[] certificate = readBytes(buffer); + byte[] additionalData = readBytes(buffer); + byte[] publicKey = readBytes(buffer); + int signatureAlgorithmId = buffer.getInt(); + byte[] signature = readBytes(buffer); + return new SigningInfo(apkDigest, certificate, additionalData, publicKey, + signatureAlgorithmId, signature); + } + + byte[] toByteArray() { + final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize( + this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/ + + bytesSize(this.signature); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + writeBytes(buffer, this.apkDigest); + writeBytes(buffer, this.certificate); + writeBytes(buffer, this.additionalData); + writeBytes(buffer, this.publicKey); + buffer.putInt(this.signatureAlgorithmId); + writeBytes(buffer, this.signature); + return buffer.array(); + } + } + + public static class SigningInfoBlock { + public final int blockId; + public final byte[] signingInfo; + + public SigningInfoBlock(int blockId, byte[] signingInfo) { + this.blockId = blockId; + this.signingInfo = signingInfo; + } + + static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException { + int blockId = buffer.getInt(); + byte[] signingInfo = readBytes(buffer); + return new SigningInfoBlock(blockId, signingInfo); + } + + byte[] toByteArray() { + final int size = 4/*blockId*/ + bytesSize(this.signingInfo); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(this.blockId); + writeBytes(buffer, this.signingInfo); + return buffer.array(); + } + } + + public static class SigningInfos { + public final SigningInfo signingInfo; + public final SigningInfoBlock[] signingInfoBlocks; + + public SigningInfos(SigningInfo signingInfo) { + this.signingInfo = signingInfo; + this.signingInfoBlocks = new SigningInfoBlock[0]; + } + + public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) { + this.signingInfo = signingInfo; + this.signingInfoBlocks = signingInfoBlocks; + } + + public static SigningInfos fromByteArray(byte[] bytes) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer); + if (!buffer.hasRemaining()) { + return new SigningInfos(signingInfo); + } + ArrayList signingInfoBlocks = new ArrayList<>(1); + while (buffer.hasRemaining()) { + signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer)); + } + return new SigningInfos(signingInfo, + signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()])); + } + + byte[] toByteArray() { + byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][]; + arrays[0] = this.signingInfo.toByteArray(); + int size = arrays[0].length; + for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) { + arrays[i + 1] = this.signingInfoBlocks[i].toByteArray(); + size += arrays[i + 1].length; + } + if (size > MAX_SIGNING_INFOS_SIZE) { + throw new IllegalArgumentException( + "Combined SigningInfos length exceeded limit of 7K: " + size); + } + + // Combine all arrays into one. + byte[] result = Arrays.copyOf(arrays[0], size); + int offset = arrays[0].length; + for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) { + System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length); + offset += arrays[i + 1].length; + } + return result; + } + } + + // Always 2 for now. + public final int version; + public final byte[] hashingInfo; + // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock. + // Passed as-is to the kernel. Can be retrieved later. + public final byte[] signingInfos; + + V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) { this.version = version; this.hashingInfo = hashingInfo; - this.signingInfo = signingInfo; + this.signingInfos = signingInfos; } static V4Signature readFrom(InputStream stream) throws IOException { @@ -48,7 +214,13 @@ public class V4Signature { return new V4Signature(version, hashingInfo, signingInfo); } - static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) { + public void writeTo(OutputStream stream) throws IOException { + writeIntLE(stream, this.version); + writeBytes(stream, this.hashingInfo); + writeBytes(stream, this.signingInfos); + } + + static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) { final int size = 4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize( hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize( @@ -136,89 +308,4 @@ public class V4Signature { buffer.putInt(bytes.length); buffer.put(bytes); } - - public void writeTo(OutputStream stream) throws IOException { - writeIntLE(stream, this.version); - writeBytes(stream, this.hashingInfo); - writeBytes(stream, this.signingInfo); - } - - public static class HashingInfo { - public final int hashAlgorithm; // only 1 == SHA256 supported - public final byte log2BlockSize; // only 12 (block size 4096) supported now - public final byte[] salt; // used exactly as in fs-verity, 32 bytes max - public final byte[] rawRootHash; // salted digest of the first Merkle tree page - - HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) { - this.hashAlgorithm = hashAlgorithm; - this.log2BlockSize = log2BlockSize; - this.salt = salt; - this.rawRootHash = rawRootHash; - } - - static HashingInfo fromByteArray(byte[] bytes) throws IOException { - ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - final int hashAlgorithm = buffer.getInt(); - final byte log2BlockSize = buffer.get(); - byte[] salt = readBytes(buffer); - byte[] rawRootHash = readBytes(buffer); - return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash); - } - - byte[] toByteArray() { - final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt) - + bytesSize(this.rawRootHash); - ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); - buffer.putInt(this.hashAlgorithm); - buffer.put(this.log2BlockSize); - writeBytes(buffer, this.salt); - writeBytes(buffer, this.rawRootHash); - return buffer.array(); - } - } - - public static class SigningInfo { - public final byte[] apkDigest; // used to match with the corresponding APK - public final byte[] certificate; // ASN.1 DER form - public final byte[] additionalData; // a free-form binary data blob - public final byte[] publicKey; // ASN.1 DER, must match the certificate - public final int signatureAlgorithmId; // see the APK v2 doc for the list - public final byte[] signature; - - SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData, - byte[] publicKey, int signatureAlgorithmId, byte[] signature) { - this.apkDigest = apkDigest; - this.certificate = certificate; - this.additionalData = additionalData; - this.publicKey = publicKey; - this.signatureAlgorithmId = signatureAlgorithmId; - this.signature = signature; - } - - static SigningInfo fromByteArray(byte[] bytes) throws IOException { - ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - byte[] apkDigest = readBytes(buffer); - byte[] certificate = readBytes(buffer); - byte[] additionalData = readBytes(buffer); - byte[] publicKey = readBytes(buffer); - int signatureAlgorithmId = buffer.getInt(); - byte[] signature = readBytes(buffer); - return new SigningInfo(apkDigest, certificate, additionalData, publicKey, - signatureAlgorithmId, signature); - } - - byte[] toByteArray() { - final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize( - this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/ - + bytesSize(this.signature); - ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); - writeBytes(buffer, this.apkDigest); - writeBytes(buffer, this.certificate); - writeBytes(buffer, this.additionalData); - writeBytes(buffer, this.publicKey); - buffer.putInt(this.signatureAlgorithmId); - writeBytes(buffer, this.signature); - return buffer.array(); - } - } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java index 4f61e700..160dc4e2 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java +++ b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +22,6 @@ import com.android.apksig.internal.asn1.ber.BerDataValueReader; import com.android.apksig.internal.asn1.ber.BerEncoding; import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader; import com.android.apksig.internal.util.ByteBufferUtils; -import com.android.apksig.internal.util.ClassCompat; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -40,24 +38,24 @@ import java.util.List; * containing fields annotated with {@link Asn1Field}. */ public final class Asn1BerParser { - private Asn1BerParser() { - } + private Asn1BerParser() {} /** * Returns the ASN.1 structure contained in the BER encoded input. * - * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer - * is advanced to the first position following the end of the consumed structure. + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. * @param containerClass class describing the structure of the input. The class must meet the - * following requirements: - *

    - *
  • The class must be annotated with {@link Asn1Class}.
  • - *
  • The class must expose a public no-arg constructor.
  • - *
  • Member fields of the class which are populated with parsed input must be - * annotated with {@link Asn1Field} and be public and non-final.
  • - *
+ * following requirements: + *
    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * * @throws Asn1DecodingException if the input could not be decoded into the specified Java - * object + * object */ public static T parse(ByteBuffer encoded, Class containerClass) throws Asn1DecodingException { @@ -81,18 +79,19 @@ public final class Asn1BerParser { *

Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1 * SET may contain duplicate elements. * - * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer - * is advanced to the first position following the end of the consumed structure. + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. * @param elementClass class describing the structure of the values/elements contained in this - * container. The class must meet the following requirements: - *

    - *
  • The class must be annotated with {@link Asn1Class}.
  • - *
  • The class must expose a public no-arg constructor.
  • - *
  • Member fields of the class which are populated with parsed input must be - * annotated with {@link Asn1Field} and be public and non-final.
  • - *
+ * container. The class must meet the following requirements: + *
    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * * @throws Asn1DecodingException if the input could not be decoded into the specified Java - * object + * object */ public static List parseImplicitSetOf(ByteBuffer encoded, Class elementClass) throws Asn1DecodingException { @@ -122,7 +121,8 @@ public final class Asn1BerParser { case CHOICE: return parseChoice(container, containerClass); - case SEQUENCE: { + case SEQUENCE: + { int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL; int expectedTagNumber = BerEncoding.getTagNumber(dataType); if ((container.getTagClass() != expectedTagClass) @@ -198,7 +198,7 @@ public final class Asn1BerParser { } private static T parseSequence(BerDataValue container, Class containerClass, - boolean isUnencodedContainer) throws Asn1DecodingException { + boolean isUnencodedContainer) throws Asn1DecodingException { List fields = getAnnotatedFields(containerClass); Collections.sort( fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index()); @@ -311,7 +311,7 @@ public final class Asn1BerParser { private static Asn1Type getContainerAsn1Type(Class containerClass) throws Asn1DecodingException { - Asn1Class containerAnnotation = ClassCompat.getDeclaredAnnotation(containerClass, Asn1Class.class); + Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class); if (containerAnnotation == null) { throw new Asn1DecodingException( containerClass.getName() + " is not annotated with " @@ -332,13 +332,8 @@ public final class Asn1BerParser { private static Class getElementType(Field field) throws Asn1DecodingException, ClassNotFoundException { -// String type; -// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { -// type = field.getGenericType().getTypeName(); -// } else { - String type = field.getGenericType().toString(); -// } - int delimiterIndex = type.indexOf('<'); + String type = field.getGenericType().getTypeName(); + int delimiterIndex = type.indexOf('<'); if (delimiterIndex == -1) { throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); } @@ -352,105 +347,6 @@ public final class Asn1BerParser { return Class.forName(elementClassName); } - private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException { - if (!encodedOid.hasRemaining()) { - throw new Asn1DecodingException("Empty OBJECT IDENTIFIER"); - } - - // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2 - long firstComponent = decodeBase128UnsignedLong(encodedOid); - int firstNode = (int) Math.min(firstComponent / 40, 2); - long secondNode = firstComponent - firstNode * 40; - StringBuilder result = new StringBuilder(); - result.append(Long.toString(firstNode)).append('.') - .append(Long.toString(secondNode)); - - // Each consecutive node is encoded as a separate component - while (encodedOid.hasRemaining()) { - long node = decodeBase128UnsignedLong(encodedOid); - result.append('.').append(Long.toString(node)); - } - - return result.toString(); - } - - private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException { - if (!encoded.hasRemaining()) { - return 0; - } - long result = 0; - while (encoded.hasRemaining()) { - if (result > Long.MAX_VALUE >>> 7) { - throw new Asn1DecodingException("Base-128 number too large"); - } - int b = encoded.get() & 0xff; - result <<= 7; - result |= b & 0x7f; - if ((b & 0x80) == 0) { - return result; - } - } - throw new Asn1DecodingException( - "Truncated base-128 encoded input: missing terminating byte, with highest bit not" - + " set"); - } - - private static BigInteger integerToBigInteger(ByteBuffer encoded) { - if (!encoded.hasRemaining()) { - return BigInteger.ZERO; - } - return new BigInteger(ByteBufferUtils.toByteArray(encoded)); - } - - private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException { - BigInteger value = integerToBigInteger(encoded); - if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0 - || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { - throw new Asn1DecodingException( - String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value)); - } - return value.intValue(); - } - - private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { - BigInteger value = integerToBigInteger(encoded); - if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0 - || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) { - throw new Asn1DecodingException( - String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value)); - } - return value.longValue(); - } - - private static List getAnnotatedFields(Class containerClass) - throws Asn1DecodingException { - Field[] declaredFields = containerClass.getDeclaredFields(); - List result = new ArrayList<>(declaredFields.length); - for (Field field : declaredFields) { - Asn1Field annotation = field.getAnnotation(Asn1Field.class); - if (annotation == null) { - continue; - } - if (Modifier.isStatic(field.getModifiers())) { - throw new Asn1DecodingException( - Asn1Field.class.getName() + " used on a static field: " - + containerClass.getName() + "." + field.getName()); - } - - AnnotatedField annotatedField; - try { - annotatedField = new AnnotatedField(field, annotation); - } catch (Asn1DecodingException e) { - throw new Asn1DecodingException( - "Invalid ASN.1 annotation on " - + containerClass.getName() + "." + field.getName(), - e); - } - result.add(annotatedField); - } - return result; - } - private static final class AnnotatedField { private final Field mField; private final Asn1Field mAnnotation; @@ -524,17 +420,17 @@ public final class Asn1BerParser { if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) { throw new Asn1UnexpectedTagException( "Tag mismatch. Expected: " - + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber) - + ", but found " - + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber)); + + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber) + + ", but found " + + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber)); } } else { if (readTagClass != mBerTagClass) { throw new Asn1UnexpectedTagException( "Tag mismatch. Expected class: " - + BerEncoding.tagClassToString(mBerTagClass) - + ", but found " - + BerEncoding.tagClassToString(readTagClass)); + + BerEncoding.tagClassToString(mBerTagClass) + + ", but found " + + BerEncoding.tagClassToString(readTagClass)); } } @@ -559,15 +455,111 @@ public final class Asn1BerParser { } } - private static final class BerToJavaConverter { - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - - private BerToJavaConverter() { + private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException { + if (!encodedOid.hasRemaining()) { + throw new Asn1DecodingException("Empty OBJECT IDENTIFIER"); } + // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2 + long firstComponent = decodeBase128UnsignedLong(encodedOid); + int firstNode = (int) Math.min(firstComponent / 40, 2); + long secondNode = firstComponent - firstNode * 40; + StringBuilder result = new StringBuilder(); + result.append(Long.toString(firstNode)).append('.') + .append(Long.toString(secondNode)); + + // Each consecutive node is encoded as a separate component + while (encodedOid.hasRemaining()) { + long node = decodeBase128UnsignedLong(encodedOid); + result.append('.').append(Long.toString(node)); + } + + return result.toString(); + } + + private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException { + if (!encoded.hasRemaining()) { + return 0; + } + long result = 0; + while (encoded.hasRemaining()) { + if (result > Long.MAX_VALUE >>> 7) { + throw new Asn1DecodingException("Base-128 number too large"); + } + int b = encoded.get() & 0xff; + result <<= 7; + result |= b & 0x7f; + if ((b & 0x80) == 0) { + return result; + } + } + throw new Asn1DecodingException( + "Truncated base-128 encoded input: missing terminating byte, with highest bit not" + + " set"); + } + + private static BigInteger integerToBigInteger(ByteBuffer encoded) { + if (!encoded.hasRemaining()) { + return BigInteger.ZERO; + } + return new BigInteger(ByteBufferUtils.toByteArray(encoded)); + } + + private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0 + || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value)); + } + return value.intValue(); + } + + private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0 + || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value)); + } + return value.longValue(); + } + + private static List getAnnotatedFields(Class containerClass) + throws Asn1DecodingException { + Field[] declaredFields = containerClass.getDeclaredFields(); + List result = new ArrayList<>(declaredFields.length); + for (Field field : declaredFields) { + Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1DecodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + + AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(field, annotation); + } catch (Asn1DecodingException e) { + throw new Asn1DecodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static final class BerToJavaConverter { + private BerToJavaConverter() {} + public static void setFieldValue( Object obj, Field field, Asn1Type type, BerDataValue dataValue) - throws Asn1DecodingException { + throws Asn1DecodingException { try { switch (type) { case SET_OF: @@ -590,6 +582,8 @@ public final class Asn1BerParser { } } + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + @SuppressWarnings("unchecked") public static T convert( Asn1Type sourceType, @@ -645,21 +639,23 @@ public final class Asn1BerParser { } else { result = true; } - return (T) Boolean.valueOf(result); + return (T) new Boolean(result); } break; - case SEQUENCE: { + case SEQUENCE: + { Asn1Class containerAnnotation = - ClassCompat.getDeclaredAnnotation(targetType, Asn1Class.class); + targetType.getDeclaredAnnotation(Asn1Class.class); if ((containerAnnotation != null) && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { return parseSequence(dataValue, targetType); } break; } - case CHOICE: { + case CHOICE: + { Asn1Class containerAnnotation = - ClassCompat.getDeclaredAnnotation(targetType, Asn1Class.class); + targetType.getDeclaredAnnotation(Asn1Class.class); if ((containerAnnotation != null) && (containerAnnotation.type() == Asn1Type.CHOICE)) { return parseChoice(dataValue, targetType); diff --git a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java index 5b8b3142..901f5f30 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java +++ b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +17,6 @@ package com.android.apksig.internal.asn1; import com.android.apksig.internal.asn1.ber.BerEncoding; -import com.android.apksig.internal.util.ClassCompat; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; @@ -38,30 +36,24 @@ import java.util.List; * containing fields annotated with {@link Asn1Field}. */ public final class Asn1DerEncoder { - /** - * ASN.1 DER-encoded {@code NULL}. - */ - public static final Asn1OpaqueObject ASN1_DER_NULL = - new Asn1OpaqueObject(new byte[]{BerEncoding.TAG_NUMBER_NULL, 0}); - - private Asn1DerEncoder() { - } + private Asn1DerEncoder() {} /** * Returns the DER-encoded form of the provided ASN.1 structure. * * @param container container to be encoded. The container's class must meet the following - * requirements: - *
    - *
  • The class must be annotated with {@link Asn1Class}.
  • - *
  • Member fields of the class which are to be encoded must be annotated with - * {@link Asn1Field} and be public.
  • - *
+ * requirements: + *
    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • Member fields of the class which are to be encoded must be annotated with + * {@link Asn1Field} and be public.
  • + *
+ * * @throws Asn1EncodingException if the input could not be encoded */ public static byte[] encode(Object container) throws Asn1EncodingException { Class containerClass = container.getClass(); - Asn1Class containerAnnotation = ClassCompat.getDeclaredAnnotation(containerClass, Asn1Class.class); + Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class); if (containerAnnotation == null) { throw new Asn1EncodingException( containerClass.getName() + " not annotated with " + Asn1Class.class.getName()); @@ -196,13 +188,35 @@ public final class Asn1DerEncoder { serializedValues.toArray(new byte[0][])); } + /** + * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the + * two arrays are compared in ascending order. Elements at out of range indices are assumed to + * be smaller than the smallest possible value for an element. + */ + private static class ByteArrayLexicographicComparator implements Comparator { + private static final ByteArrayLexicographicComparator INSTANCE = + new ByteArrayLexicographicComparator(); + + @Override + public int compare(byte[] arr1, byte[] arr2) { + int commonLength = Math.min(arr1.length, arr2.length); + for (int i = 0; i < commonLength; i++) { + int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff); + if (diff != 0) { + return diff; + } + } + return arr1.length - arr2.length; + } + } + private static List getAnnotatedFields(Object container) throws Asn1EncodingException { Class containerClass = container.getClass(); Field[] declaredFields = containerClass.getDeclaredFields(); List result = new ArrayList<>(declaredFields.length); for (Field field : declaredFields) { - Asn1Field annotation = field.getAnnotation(Asn1Field.class); + Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class); if (annotation == null) { continue; } @@ -329,89 +343,6 @@ public final class Asn1DerEncoder { } } - private static byte[] createTag( - int tagClass, boolean constructed, int tagNumber, byte[]... contents) { - if (tagNumber >= 0x1f) { - throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber); - } - // tag class & number fit into the first byte - byte firstIdentifierByte = - (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber); - - int contentsLength = 0; - for (byte[] c : contents) { - contentsLength += c.length; - } - int contentsPosInResult; - byte[] result; - if (contentsLength < 0x80) { - // Length fits into one byte - contentsPosInResult = 2; - result = new byte[contentsPosInResult + contentsLength]; - result[0] = firstIdentifierByte; - result[1] = (byte) contentsLength; - } else { - // Length is represented as multiple bytes - // The low 7 bits of the first byte represent the number of length bytes (following the - // first byte) in which the length is in big-endian base-256 form - if (contentsLength <= 0xff) { - contentsPosInResult = 3; - result = new byte[contentsPosInResult + contentsLength]; - result[1] = (byte) 0x81; // 1 length byte - result[2] = (byte) contentsLength; - } else if (contentsLength <= 0xffff) { - contentsPosInResult = 4; - result = new byte[contentsPosInResult + contentsLength]; - result[1] = (byte) 0x82; // 2 length bytes - result[2] = (byte) (contentsLength >> 8); - result[3] = (byte) (contentsLength & 0xff); - } else if (contentsLength <= 0xffffff) { - contentsPosInResult = 5; - result = new byte[contentsPosInResult + contentsLength]; - result[1] = (byte) 0x83; // 3 length bytes - result[2] = (byte) (contentsLength >> 16); - result[3] = (byte) ((contentsLength >> 8) & 0xff); - result[4] = (byte) (contentsLength & 0xff); - } else { - contentsPosInResult = 6; - result = new byte[contentsPosInResult + contentsLength]; - result[1] = (byte) 0x84; // 4 length bytes - result[2] = (byte) (contentsLength >> 24); - result[3] = (byte) ((contentsLength >> 16) & 0xff); - result[4] = (byte) ((contentsLength >> 8) & 0xff); - result[5] = (byte) (contentsLength & 0xff); - } - result[0] = firstIdentifierByte; - } - for (byte[] c : contents) { - System.arraycopy(c, 0, result, contentsPosInResult, c.length); - contentsPosInResult += c.length; - } - return result; - } - - /** - * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the - * two arrays are compared in ascending order. Elements at out of range indices are assumed to - * be smaller than the smallest possible value for an element. - */ - private static class ByteArrayLexicographicComparator implements Comparator { - private static final ByteArrayLexicographicComparator INSTANCE = - new ByteArrayLexicographicComparator(); - - @Override - public int compare(byte[] arr1, byte[] arr2) { - int commonLength = Math.min(arr1.length, arr2.length); - for (int i = 0; i < commonLength; i++) { - int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff); - if (diff != 0) { - return diff; - } - } - return arr1.length - arr2.length; - } - } - private static final class AnnotatedField { private final Field mField; private final Object mObject; @@ -504,9 +435,69 @@ public final class Asn1DerEncoder { } } - private static final class JavaToDerConverter { - private JavaToDerConverter() { + private static byte[] createTag( + int tagClass, boolean constructed, int tagNumber, byte[]... contents) { + if (tagNumber >= 0x1f) { + throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber); } + // tag class & number fit into the first byte + byte firstIdentifierByte = + (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber); + + int contentsLength = 0; + for (byte[] c : contents) { + contentsLength += c.length; + } + int contentsPosInResult; + byte[] result; + if (contentsLength < 0x80) { + // Length fits into one byte + contentsPosInResult = 2; + result = new byte[contentsPosInResult + contentsLength]; + result[0] = firstIdentifierByte; + result[1] = (byte) contentsLength; + } else { + // Length is represented as multiple bytes + // The low 7 bits of the first byte represent the number of length bytes (following the + // first byte) in which the length is in big-endian base-256 form + if (contentsLength <= 0xff) { + contentsPosInResult = 3; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x81; // 1 length byte + result[2] = (byte) contentsLength; + } else if (contentsLength <= 0xffff) { + contentsPosInResult = 4; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x82; // 2 length bytes + result[2] = (byte) (contentsLength >> 8); + result[3] = (byte) (contentsLength & 0xff); + } else if (contentsLength <= 0xffffff) { + contentsPosInResult = 5; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x83; // 3 length bytes + result[2] = (byte) (contentsLength >> 16); + result[3] = (byte) ((contentsLength >> 8) & 0xff); + result[4] = (byte) (contentsLength & 0xff); + } else { + contentsPosInResult = 6; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x84; // 4 length bytes + result[2] = (byte) (contentsLength >> 24); + result[3] = (byte) ((contentsLength >> 16) & 0xff); + result[4] = (byte) ((contentsLength >> 8) & 0xff); + result[5] = (byte) (contentsLength & 0xff); + } + result[0] = firstIdentifierByte; + } + for (byte[] c : contents) { + System.arraycopy(c, 0, result, contentsPosInResult, c.length); + contentsPosInResult += c.length; + } + return result; + } + + private static final class JavaToDerConverter { + private JavaToDerConverter() {} public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType) throws Asn1EncodingException { @@ -567,18 +558,20 @@ public final class Asn1DerEncoder { return toOid((String) source); } break; - case SEQUENCE: { + case SEQUENCE: + { Asn1Class containerAnnotation = - ClassCompat.getDeclaredAnnotation(sourceType, Asn1Class.class); + sourceType.getDeclaredAnnotation(Asn1Class.class); if ((containerAnnotation != null) && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { return toSequence(source); } break; } - case CHOICE: { + case CHOICE: + { Asn1Class containerAnnotation = - ClassCompat.getDeclaredAnnotation(sourceType, Asn1Class.class); + sourceType.getDeclaredAnnotation(Asn1Class.class); if ((containerAnnotation != null) && (containerAnnotation.type() == Asn1Type.CHOICE)) { return toChoice(source); @@ -597,4 +590,7 @@ public final class Asn1DerEncoder { "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType); } } + /** ASN.1 DER-encoded {@code NULL}. */ + public static final Asn1OpaqueObject ASN1_DER_NULL = + new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0}); } diff --git a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java index b9a027f2..d2d3ce04 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java +++ b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java @@ -24,32 +24,22 @@ import java.lang.annotation.Target; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Asn1Field { - /** - * Index used to order fields in a container. Required for fields of SEQUENCE containers. - */ + /** Index used to order fields in a container. Required for fields of SEQUENCE containers. */ public int index() default 0; public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC; public Asn1Type type(); - /** - * Tagging mode. Default: NORMAL. - */ + /** Tagging mode. Default: NORMAL. */ public Asn1Tagging tagging() default Asn1Tagging.NORMAL; - /** - * Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used. - */ + /** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/ public int tagNumber() default -1; - /** - * {@code true} if this field is optional. Ignored for fields of CHOICE containers. - */ + /** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */ public boolean optional() default false; - /** - * Type of elements. Used only for SET_OF or SEQUENCE_OF. - */ + /** Type of elements. Used only for SET_OF or SEQUENCE_OF. */ public Asn1Type elementType() default Asn1Type.ANY; } diff --git a/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java index f732bfc1..d32330c0 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java +++ b/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java @@ -16,77 +16,90 @@ package com.android.apksig.internal.asn1.ber; -import com.android.apksig.internal.asn1.Asn1TagClass; import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1TagClass; /** * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}. */ public abstract class BerEncoding { + private BerEncoding() {} + /** * Constructed vs primitive flag in the first identifier byte. */ public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5; + /** * Tag class: UNIVERSAL */ public static final int TAG_CLASS_UNIVERSAL = 0; + /** * Tag class: APPLICATION */ public static final int TAG_CLASS_APPLICATION = 1; + /** * Tag class: CONTEXT SPECIFIC */ public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2; + /** * Tag class: PRIVATE */ public static final int TAG_CLASS_PRIVATE = 3; + /** * Tag number: BOOLEAN */ public static final int TAG_NUMBER_BOOLEAN = 0x1; + /** * Tag number: INTEGER */ public static final int TAG_NUMBER_INTEGER = 0x2; + /** * Tag number: BIT STRING */ public static final int TAG_NUMBER_BIT_STRING = 0x3; + /** * Tag number: OCTET STRING */ public static final int TAG_NUMBER_OCTET_STRING = 0x4; + /** * Tag number: NULL */ public static final int TAG_NUMBER_NULL = 0x05; + /** * Tag number: OBJECT IDENTIFIER */ public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6; + /** * Tag number: SEQUENCE */ public static final int TAG_NUMBER_SEQUENCE = 0x10; + /** * Tag number: SET */ public static final int TAG_NUMBER_SET = 0x11; + /** * Tag number: UTC_TIME */ public final static int TAG_NUMBER_UTC_TIME = 0x17; + /** * Tag number: GENERALIZED_TIME */ public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18; - private BerEncoding() { - } - public static int getTagNumber(Asn1Type dataType) { switch (dataType) { case INTEGER: diff --git a/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java b/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java index 0e645a8d..5fbca51d 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java +++ b/apksigner/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java @@ -35,6 +35,11 @@ public class InputStreamBerDataValueReader implements BerDataValueReader { mIn = in; } + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + return readDataValue(mIn); + } + /** * Returns the next data value or {@code null} if end of input has been reached. * @@ -224,11 +229,6 @@ public class InputStreamBerDataValueReader implements BerDataValueReader { } } - @Override - public BerDataValue readDataValue() throws BerDataValueFormatException { - return readDataValue(mIn); - } - private static class RecordingInputStream extends InputStream { private final InputStream mIn; private final ByteArrayOutputStream mBuf; @@ -298,8 +298,7 @@ public class InputStreamBerDataValueReader implements BerDataValueReader { } @Override - public synchronized void mark(int readlimit) { - } + public synchronized void mark(int readlimit) {} @Override public synchronized void reset() throws IOException { diff --git a/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestParser.java b/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestParser.java index 97630e77..ab0a5dad 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestParser.java +++ b/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestParser.java @@ -33,10 +33,10 @@ import java.util.jar.Attributes; */ public class ManifestParser { - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private final byte[] mManifest; private int mOffset; private int mEndOffset; + private byte[] mBufferedLine; /** @@ -55,28 +55,6 @@ public class ManifestParser { mEndOffset = offset + length; } - private static Attribute parseAttr(String attr) { - // Name is separated from value by a semicolon followed by a single SPACE character. - // This permits trailing spaces in names and leading and trailing spaces in values. - // Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual - // spaces to be able to parse such obfuscated APKs. - int delimiterIndex = attr.indexOf(": "); - if (delimiterIndex == -1) { - return new Attribute(attr, ""); - } else { - return new Attribute( - attr.substring(0, delimiterIndex), - attr.substring(delimiterIndex + ": ".length())); - } - } - - private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) { - byte[] result = new byte[arr1.length + length2]; - System.arraycopy(arr1, 0, result, 0, arr1.length); - System.arraycopy(arr2, offset2, result, arr1.length, length2); - return result; - } - /** * Returns the remaining sections of this file. */ @@ -122,6 +100,21 @@ public class ManifestParser { return new Section(sectionStartOffset, sectionSizeBytes, attrs); } + private static Attribute parseAttr(String attr) { + // Name is separated from value by a semicolon followed by a single SPACE character. + // This permits trailing spaces in names and leading and trailing spaces in values. + // Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual + // spaces to be able to parse such obfuscated APKs. + int delimiterIndex = attr.indexOf(": "); + if (delimiterIndex == -1) { + return new Attribute(attr, ""); + } else { + return new Attribute( + attr.substring(0, delimiterIndex), + attr.substring(delimiterIndex + ": ".length())); + } + } + /** * Returns the next attribute or empty {@code String} if end of section has been reached or * {@code null} if end of input has been reached. @@ -209,6 +202,15 @@ public class ManifestParser { } } + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) { + byte[] result = new byte[arr1.length + length2]; + System.arraycopy(arr1, 0, result, 0, arr1.length); + System.arraycopy(arr2, offset2, result, arr1.length, length2); + return result; + } + /** * Returns the next line (without line delimiter characters) or {@code null} if end of input has * been reached. @@ -293,8 +295,8 @@ public class ManifestParser { * Constructs a new {@code Section}. * * @param startOffset start offset (in bytes) of the section in the input file - * @param sizeBytes size (in bytes) of the section in the input file - * @param attrs attributes contained in the section + * @param sizeBytes size (in bytes) of the section in the input file + * @param attrs attributes contained in the section */ public Section(int startOffset, int sizeBytes, List attrs) { mStartOffset = startOffset; diff --git a/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java b/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java index 079d6447..fa01beb7 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java +++ b/apksigner/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java @@ -32,11 +32,10 @@ import java.util.jar.Attributes; */ public abstract class ManifestWriter { - private static final byte[] CRLF = new byte[]{'\r', '\n'}; + private static final byte[] CRLF = new byte[] {'\r', '\n'}; private static final int MAX_LINE_LENGTH = 70; - private ManifestWriter() { - } + private ManifestWriter() {} public static void writeMainSection(OutputStream out, Attributes attributes) throws IOException { @@ -72,17 +71,17 @@ public abstract class ManifestWriter { out.write(CRLF); } - static void writeAttribute(OutputStream out, Attributes.Name name, String value) + static void writeAttribute(OutputStream out, Attributes.Name name, String value) throws IOException { writeAttribute(out, name.toString(), value); } - private static void writeAttribute(OutputStream out, String name, String value) + private static void writeAttribute(OutputStream out, String name, String value) throws IOException { writeLine(out, name + ": " + value); } - private static void writeLine(OutputStream out, String line) throws IOException { + private static void writeLine(OutputStream out, String line) throws IOException { byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8); int offset = 0; int remaining = lineBytes.length; diff --git a/apksigner/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java b/apksigner/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java index c99bea21..fd8cbff8 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java +++ b/apksigner/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java @@ -27,8 +27,7 @@ import java.util.jar.Attributes; * @see JAR Manifest format */ public abstract class SignatureFileWriter { - private SignatureFileWriter() { - } + private SignatureFileWriter() {} public static void writeMainSection(OutputStream out, Attributes attributes) throws IOException { diff --git a/apksigner/src/main/java/com/android/apksig/internal/oid/OidConstants.java b/apksigner/src/main/java/com/android/apksig/internal/oid/OidConstants.java index 045eebca..d80cbaa6 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/oid/OidConstants.java +++ b/apksigner/src/main/java/com/android/apksig/internal/oid/OidConstants.java @@ -56,9 +56,6 @@ public class OidConstants { public static final Map> SUPPORTED_SIG_ALG_OIDS = new HashMap<>(); - public static final Map OID_TO_JCA_DIGEST_ALG = new HashMap<>(); - public static final Map OID_TO_JCA_SIGNATURE_ALG = new HashMap<>(); - static { addSupportedSigAlg( OID_DIGEST_MD5, OID_SIG_RSA, @@ -374,37 +371,6 @@ public class OidConstants { InclusiveIntRange.from(21)); } - static { - OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5"); - OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1"); - OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224"); - OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256"); - OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384"); - OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512"); - } - - static { - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA"); - - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA"); - - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA"); - OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA"); - } - - private OidConstants() { - } - public static void addSupportedSigAlg( String digestAlgorithmOid, String signatureAlgorithmOid, @@ -423,8 +389,9 @@ public class OidConstants { } public static class OidToUserFriendlyNameMapper { - private static final Map OID_TO_USER_FRIENDLY_NAME = new HashMap<>(); + private OidToUserFriendlyNameMapper() {} + private static final Map OID_TO_USER_FRIENDLY_NAME = new HashMap<>(); static { OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5"); OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1"); @@ -457,11 +424,40 @@ public class OidConstants { OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA"); } - private OidToUserFriendlyNameMapper() { - } - public static String getUserFriendlyNameForOid(String oid) { return OID_TO_USER_FRIENDLY_NAME.get(oid); } } + + public static final Map OID_TO_JCA_DIGEST_ALG = new HashMap<>(); + static { + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512"); + } + + public static final Map OID_TO_JCA_SIGNATURE_ALG = new HashMap<>(); + static { + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA"); + + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA"); + + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA"); + } + + private OidConstants() {} } diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java index 0dca6f3e..97127672 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java @@ -16,6 +16,7 @@ package com.android.apksig.internal.pkcs7; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL; import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1; import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256; @@ -50,8 +51,7 @@ public class AlgorithmIdentifier { @Asn1Field(index = 1, type = Asn1Type.ANY, optional = true) public Asn1OpaqueObject parameters; - public AlgorithmIdentifier() { - } + public AlgorithmIdentifier() {} public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) { this.algorithm = algorithmOid; @@ -78,7 +78,8 @@ public class AlgorithmIdentifier { * when signing with the specified key and digest algorithm. */ public static Pair getSignerInfoSignatureAlgorithm( - PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException { + PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning) + throws InvalidKeyException { String keyAlgorithm = publicKey.getAlgorithm(); String jcaDigestPrefixForSigAlg; switch (digestAlgorithm) { @@ -92,7 +93,7 @@ public class AlgorithmIdentifier { throw new IllegalArgumentException( "Unexpected digest algorithm: " + digestAlgorithm); } - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) { return Pair.of( jcaDigestPrefixForSigAlg + "withRSA", new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL)); @@ -116,7 +117,9 @@ public class AlgorithmIdentifier { throw new IllegalArgumentException( "Unexpected digest algorithm: " + digestAlgorithm); } - return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId); + String signingAlgorithmName = + jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA"); + return Pair.of(signingAlgorithmName, sigAlgId); } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { return Pair.of( jcaDigestPrefixForSigAlg + "withECDSA", diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java index 9d794581..a6c91efa 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java @@ -20,7 +20,6 @@ import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; import com.android.apksig.internal.asn1.Asn1OpaqueObject; import com.android.apksig.internal.asn1.Asn1Type; - import java.util.List; /** diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java index 13f98601..8ab722c2 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java @@ -19,8 +19,8 @@ package com.android.apksig.internal.pkcs7; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; import com.android.apksig.internal.asn1.Asn1OpaqueObject; -import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; /** * PKCS #7 {@code ContentInfo} as specified in RFC 5652. diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java index 9e6c8f7b..79f41af8 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java @@ -18,9 +18,8 @@ package com.android.apksig.internal.pkcs7; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; -import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.asn1.Asn1Type; - +import com.android.apksig.internal.asn1.Asn1Tagging; import java.nio.ByteBuffer; /** @@ -39,8 +38,7 @@ public class EncapsulatedContentInfo { optional = true) public ByteBuffer content; - public EncapsulatedContentInfo() { - } + public EncapsulatedContentInfo() {} public EncapsulatedContentInfo(String contentTypeOid) { contentType = contentTypeOid; diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java index ff81a26a..284b1176 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java @@ -20,7 +20,6 @@ import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; import com.android.apksig.internal.asn1.Asn1OpaqueObject; import com.android.apksig.internal.asn1.Asn1Type; - import java.math.BigInteger; /** @@ -35,8 +34,7 @@ public class IssuerAndSerialNumber { @Asn1Field(index = 1, type = Asn1Type.INTEGER) public BigInteger certificateSerialNumber; - public IssuerAndSerialNumber() { - } + public IssuerAndSerialNumber() {} public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) { this.issuer = issuer; diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java index 11568f63..1a115d51 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java @@ -20,11 +20,10 @@ package com.android.apksig.internal.pkcs7; * Assorted PKCS #7 constants from RFC 5652. */ public abstract class Pkcs7Constants { + private Pkcs7Constants() {} + public static final String OID_DATA = "1.2.840.113549.1.7.1"; public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2"; public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3"; public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4"; - - private Pkcs7Constants() { - } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java index dc0ee70e..56b6e502 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java @@ -19,9 +19,8 @@ package com.android.apksig.internal.pkcs7; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; import com.android.apksig.internal.asn1.Asn1OpaqueObject; -import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.asn1.Asn1Type; - +import com.android.apksig.internal.asn1.Asn1Tagging; import java.nio.ByteBuffer; import java.util.List; diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java index 9098c753..a3d70f16 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java @@ -18,9 +18,8 @@ package com.android.apksig.internal.pkcs7; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; -import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.asn1.Asn1Type; - +import com.android.apksig.internal.asn1.Asn1Tagging; import java.nio.ByteBuffer; /** @@ -35,8 +34,7 @@ public class SignerIdentifier { @Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0) public ByteBuffer subjectKeyIdentifier; - public SignerIdentifier() { - } + public SignerIdentifier() {} public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) { this.issuerAndSerialNumber = issuerAndSerialNumber; diff --git a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java index 44a5f51b..b885eb80 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java +++ b/apksigner/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java @@ -19,9 +19,8 @@ package com.android.apksig.internal.pkcs7; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; import com.android.apksig.internal.asn1.Asn1OpaqueObject; -import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.asn1.Asn1Type; - +import com.android.apksig.internal.asn1.Asn1Tagging; import java.nio.ByteBuffer; import java.util.List; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/apksigner/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java index cee0cdc1..90aee303 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java @@ -21,46 +21,54 @@ package com.android.apksig.internal.util; */ public abstract class AndroidSdkVersion { - /** - * Android 2.3. - */ + /** Hidden constructor to prevent instantiation. */ + private AndroidSdkVersion() {} + + /** Android 1.0 */ + public static final int INITIAL_RELEASE = 1; + + /** Android 2.3. */ public static final int GINGERBREAD = 9; - /** - * Android 4.3. The revenge of the beans. - */ + + /** Android 3.0 */ + public static final int HONEYCOMB = 11; + + /** Android 4.3. The revenge of the beans. */ public static final int JELLY_BEAN_MR2 = 18; - /** - * Android 4.4. KitKat, another tasty treat. - */ + + /** Android 4.4. KitKat, another tasty treat. */ public static final int KITKAT = 19; - /** - * Android 5.0. A flat one with beautiful shadows. But still tasty. - */ + + /** Android 5.0. A flat one with beautiful shadows. But still tasty. */ public static final int LOLLIPOP = 21; - /** - * Android 6.0. M is for Marshmallow! - */ + + /** Android 6.0. M is for Marshmallow! */ public static final int M = 23; - /** - * Android 7.0. N is for Nougat. - */ + + /** Android 7.0. N is for Nougat. */ public static final int N = 24; - /** - * Android O. - */ + + /** Android O. */ public static final int O = 26; - /** - * Android P. - */ + + /** Android P. */ public static final int P = 28; - /** - * Android R. - */ + + /** Android Q. */ + public static final int Q = 29; + + /** Android R. */ public static final int R = 30; - /** - * Hidden constructor to prevent instantiation. - */ - private AndroidSdkVersion() { - } + /** Android S. */ + public static final int S = 31; + + /** Android Sv2. */ + public static final int Sv2 = 32; + + /** Android Tiramisu. */ + public static final int T = 33; + + /** Android Upside Down Cake. */ + public static final int U = 34; } diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java b/apksigner/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java index 6ea29622..e5741a5b 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java @@ -19,7 +19,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; import com.android.apksig.util.ReadableDataSink; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java b/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java index 058d0d08..656c20e1 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java @@ -18,7 +18,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; - import java.io.IOException; import java.nio.ByteBuffer; @@ -99,7 +98,7 @@ public class ByteBufferDataSource implements DataSource { return new ByteBufferDataSource( getByteBuffer(offset, (int) size), false // no need to slice -- it's already a slice - ); + ); } private void checkChunkValid(long offset, long size) { @@ -120,7 +119,7 @@ public class ByteBufferDataSource implements DataSource { } if (endOffset > mSize) { throw new IndexOutOfBoundsException( - "offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")"); + "offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")"); } } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java b/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java index 67554e24..d7cbe035 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java @@ -17,7 +17,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; - import java.io.IOException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java b/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java index b85f2fcd..a7b4b5c8 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java @@ -19,8 +19,7 @@ package com.android.apksig.internal.util; import java.nio.ByteBuffer; public final class ByteBufferUtils { - private ByteBufferUtils() { - } + private ByteBufferUtils() {} /** * Returns the remaining data of the provided buffer as a new byte array and advances the diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ByteStreams.java b/apksigner/src/main/java/com/android/apksig/internal/util/ByteStreams.java index 7e462000..bca3b082 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ByteStreams.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/ByteStreams.java @@ -24,8 +24,7 @@ import java.io.InputStream; * Utilities for byte arrays and I/O streams. */ public final class ByteStreams { - private ByteStreams() { - } + private ByteStreams() {} /** * Returns the data remaining in the provided input stream as a byte array diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java b/apksigner/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java index 88d9f49b..a0baf1ae 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,14 +18,12 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; -/** - * Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. - */ +/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */ public class ChainedDataSource implements DataSource { private final DataSource[] mSources; @@ -34,9 +31,7 @@ public class ChainedDataSource implements DataSource { public ChainedDataSource(DataSource... sources) { mSources = sources; - long totalSize = 0; - if (sources != null) for (DataSource source : sources) totalSize += source.size(); - mTotalSize = totalSize; + mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum(); } @Override @@ -91,7 +86,7 @@ public class ChainedDataSource implements DataSource { ByteBuffer buffer = ByteBuffer.allocate(size); for (; i < mSources.length && buffer.hasRemaining(); i++) { long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining()); - mSources[i].copyTo(offset, MathCompat.toIntExact(sizeToCopy), buffer); + mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer); offset = 0; // may not be zero for the first source, but reset after that. } buffer.rewind(); @@ -134,7 +129,6 @@ public class ChainedDataSource implements DataSource { /** * Find the index of DataSource that offset is at. - * * @return Pair of DataSource index and the local offset in the DataSource. */ private Pair locateDataSource(long offset) { diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/ClassCompat.java b/apksigner/src/main/java/com/android/apksig/internal/util/ClassCompat.java deleted file mode 100644 index d15e413e..00000000 --- a/apksigner/src/main/java/com/android/apksig/internal/util/ClassCompat.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2020 Muntashir Al-Islam - * - * 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.internal.util; - -import java.lang.annotation.Annotation; -import java.util.Objects; - -public class ClassCompat { - public static A getDeclaredAnnotation(Class containerClass, - Class annotationClass) { - Objects.requireNonNull(annotationClass); - Objects.requireNonNull(containerClass); - // Loop over all directly-present annotations looking for a matching one - for (Annotation annotation : containerClass.getDeclaredAnnotations()) { - if (annotationClass.equals(annotation.annotationType())) { - // More robust to do a dynamic cast at runtime instead - // of compile-time only. - return annotationClass.cast(annotation); - } - } - return null; - } -} diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/apksigner/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java index 1b99c991..2a890f68 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -212,12 +211,9 @@ public class DelegatingX509Certificate extends X509Certificate { } @Override + @SuppressWarnings("AndroidJdkLibsChecker") public void verify(PublicKey key, Provider sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -// mDelegate.verify(key, sigProvider); -// } else { -// throw new UnsupportedOperationException("Not supported before API 24"); -// } + mDelegate.verify(key, sigProvider); } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java b/apksigner/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java index f13ebe4d..e4a421a7 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java @@ -18,7 +18,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; - import java.io.IOException; import java.io.RandomAccessFile; import java.nio.BufferOverflowException; @@ -66,29 +65,6 @@ public class FileChannelDataSource implements DataSource { mSize = size; } - private static void checkChunkValid(long offset, long size, long sourceSize) { - if (offset < 0) { - throw new IndexOutOfBoundsException("offset: " + offset); - } - if (size < 0) { - throw new IndexOutOfBoundsException("size: " + size); - } - if (offset > sourceSize) { - throw new IndexOutOfBoundsException( - "offset (" + offset + ") > source size (" + sourceSize + ")"); - } - long endOffset = offset + size; - if (endOffset < offset) { - throw new IndexOutOfBoundsException( - "offset (" + offset + ") + size (" + size + ") overflow"); - } - if (endOffset > sourceSize) { - throw new IndexOutOfBoundsException( - "offset (" + offset + ") + size (" + size - + ") > source size (" + sourceSize + ")"); - } - } - @Override public long size() { if (mSize == -1) { @@ -189,4 +165,27 @@ public class FileChannelDataSource implements DataSource { result.flip(); return result; } + + private static void checkChunkValid(long offset, long size, long sourceSize) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + if (offset > sourceSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") > source size (" + sourceSize + ")"); + } + long endOffset = offset + size; + if (endOffset < offset) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") overflow"); + } + if (endOffset > sourceSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + + ") > source size (" + sourceSize +")"); + } + } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java b/apksigner/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java index 09f7427e..d7866a9e 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java @@ -32,14 +32,6 @@ public class InclusiveIntRange { this.max = max; } - public static InclusiveIntRange fromTo(int min, int max) { - return new InclusiveIntRange(min, max); - } - - public static InclusiveIntRange from(int min) { - return new InclusiveIntRange(min, Integer.MAX_VALUE); - } - public int getMin() { return min; } @@ -48,6 +40,14 @@ public class InclusiveIntRange { return max; } + public static InclusiveIntRange fromTo(int min, int max) { + return new InclusiveIntRange(min, max); + } + + public static InclusiveIntRange from(int min) { + return new InclusiveIntRange(min, Integer.MAX_VALUE); + } + public List getValuesNotIn( List sortedNonOverlappingRanges) { if (sortedNonOverlappingRanges.isEmpty()) { diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java b/apksigner/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java index 5b9c82c6..733dd563 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java @@ -16,7 +16,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; - import java.nio.ByteBuffer; import java.security.MessageDigest; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java b/apksigner/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java index cc4d4151..f1b5ac6c 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java @@ -17,7 +17,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; - import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java b/apksigner/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java index 7a9fce29..bbd2d14a 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java @@ -17,7 +17,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; - import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/TeeDataSink.java b/apksigner/src/main/java/com/android/apksig/internal/util/TeeDataSink.java index 2b3dc2c1..2e46f18b 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/TeeDataSink.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/TeeDataSink.java @@ -17,7 +17,6 @@ package com.android.apksig.internal.util; import com.android.apksig.util.DataSink; - import java.io.IOException; import java.nio.ByteBuffer; diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/apksigner/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java index 7b07bcde..5c1f407a 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,10 +58,6 @@ public class VerityTreeBuilder implements AutoCloseable { * Typical prefetch size. */ private final static int MAX_PREFETCH_CHUNKS = 1024; - /** - * Minimum chunks to be processed by a single worker task. - */ - private final static int MIN_CHUNKS_PER_WORKER = 8; /** * Digest algorithm (JCA Digest algorithm name) used in the tree. @@ -87,61 +82,6 @@ public class VerityTreeBuilder implements AutoCloseable { mMd = getNewMessageDigest(); } - /** - * Returns an array of summed area table of level size in the verity tree. In other words, the - * returned array is offset of each level in the verity tree file format, plus an additional - * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size - * is level + 1. - */ - private static int[] calculateLevelOffset(long dataSize, int digestSize) { - // Compute total size of each level, bottom to top. - ArrayList levelSize = new ArrayList<>(); - while (true) { - long chunkCount = divideRoundup(dataSize, CHUNK_SIZE); - long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize, CHUNK_SIZE); - levelSize.add(size); - if (chunkCount * digestSize <= CHUNK_SIZE) { - break; - } - dataSize = chunkCount * digestSize; - } - - // Reverse and convert to summed area table. - int[] levelOffset = new int[levelSize.size() + 1]; - levelOffset[0] = 0; - for (int i = 0; i < levelSize.size(); i++) { - // We don't support verity tree if it is larger then Integer.MAX_VALUE. - levelOffset[i + 1] = levelOffset[i] + MathCompat.toIntExact( - levelSize.get(levelSize.size() - i - 1)); - } - return levelOffset; - } - - /** - * Divides a number and round up to the closest integer. - */ - private static long divideRoundup(long dividend, long divisor) { - return (dividend + divisor - 1) / divisor; - } - - /** - * Returns a slice of the buffer with shared the content. - */ - private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { - ByteBuffer b = buffer.duplicate(); - b.position(0); // to ensure position <= limit invariant. - b.limit(end); - b.position(begin); - return b.slice(); - } - - /** - * Obtains a new instance of the message digest algorithm. - */ - private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException { - return MessageDigest.getInstance(JCA_ALGORITHM); - } - @Override public void close() { mExecutor.shutdownNow(); @@ -149,13 +89,13 @@ public class VerityTreeBuilder implements AutoCloseable { /** * Returns the root hash of the APK verity tree built from ZIP blocks. - *

+ * * Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which * must be page aligned) and the "Central Directory offset" field in End of Central Directory * are skipped. */ public byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock, - DataSource centralDir, DataSource eocd) throws IOException { + DataSource centralDir, DataSource eocd) throws IOException { if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) { throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE + ": " + beforeApkSigningBlock.size()); @@ -172,7 +112,7 @@ public class VerityTreeBuilder implements AutoCloseable { ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); return generateVerityTreeRootHash(new ChainedDataSource(beforeApkSigningBlock, centralDir, - DataSources.asDataSource(eocdBuf))); + DataSources.asDataSource(eocdBuf))); } /** @@ -185,14 +125,14 @@ public class VerityTreeBuilder implements AutoCloseable { /** * Returns the byte buffer that contains the whole verity tree. - *

+ * * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the * input file. If the total size is larger than 4 KB, take this level as input and repeat the * same procedure, until the level is within 4 KB. If salt is given, it will apply to each * digestion before the actual data. - *

+ * * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt. - *

+ * * The tree is currently stored only in memory and is never written out. Nevertheless, it is * the actual verity tree format on disk, and is supposed to be re-generated on device. */ @@ -238,6 +178,36 @@ public class VerityTreeBuilder implements AutoCloseable { return saltedDigest(firstPage); } + /** + * Returns an array of summed area table of level size in the verity tree. In other words, the + * returned array is offset of each level in the verity tree file format, plus an additional + * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size + * is level + 1. + */ + private static int[] calculateLevelOffset(long dataSize, int digestSize) { + // Compute total size of each level, bottom to top. + ArrayList levelSize = new ArrayList<>(); + while (true) { + long chunkCount = divideRoundup(dataSize, CHUNK_SIZE); + long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize, CHUNK_SIZE); + levelSize.add(size); + if (chunkCount * digestSize <= CHUNK_SIZE) { + break; + } + dataSize = chunkCount * digestSize; + } + + // Reverse and convert to summed area table. + int[] levelOffset = new int[levelSize.size() + 1]; + levelOffset[0] = 0; + for (int i = 0; i < levelSize.size(); i++) { + // We don't support verity tree if it is larger then Integer.MAX_VALUE. + levelOffset[i + 1] = levelOffset[i] + Math.toIntExact( + levelSize.get(levelSize.size() - i - 1)); + } + return levelOffset; + } + /** * Digest data source by chunks then feeds them to the sink one by one. If the last unit is * less than the chunk size and padding is desired, feed with extra padding 0 to fill up the @@ -274,7 +244,7 @@ public class VerityTreeBuilder implements AutoCloseable { Runnable task = () -> { final MessageDigest md = cloneMessageDigest(); for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex; - offset < finish; offset += CHUNK_SIZE, ++chunkIndex) { + offset < finish; offset += CHUNK_SIZE, ++chunkIndex) { ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE); hashes[chunkIndex] = saltedDigest(md, chunk); } @@ -296,9 +266,7 @@ public class VerityTreeBuilder implements AutoCloseable { } } - /** - * Returns the digest of data with salt prepended. - */ + /** Returns the digest of data with salt prepended. */ private byte[] saltedDigest(ByteBuffer data) { return saltedDigest(mMd, data); } @@ -312,6 +280,27 @@ public class VerityTreeBuilder implements AutoCloseable { return md.digest(); } + /** Divides a number and round up to the closest integer. */ + private static long divideRoundup(long dividend, long divisor) { + return (dividend + divisor - 1) / divisor; + } + + /** Returns a slice of the buffer with shared the content. */ + private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { + ByteBuffer b = buffer.duplicate(); + b.position(0); // to ensure position <= limit invariant. + b.limit(end); + b.position(begin); + return b.slice(); + } + + /** + * Obtains a new instance of the message digest algorithm. + */ + private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(JCA_ALGORITHM); + } + /** * Clones the existing message digest, or creates a new instance if clone is unavailable. */ diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/apksigner/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java index 16d49f30..ca6271df 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java +++ b/apksigner/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +21,6 @@ import com.android.apksig.internal.asn1.Asn1DecodingException; import com.android.apksig.internal.asn1.Asn1DerEncoder; import com.android.apksig.internal.asn1.Asn1EncodingException; import com.android.apksig.internal.x509.Certificate; -import com.mcal.apksigner.utils.Base64; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -32,6 +30,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; /** @@ -41,6 +40,8 @@ import java.util.Collection; */ public class X509CertificateUtils { + private static volatile CertificateFactory sCertFactory = null; + // The PEM certificate header and footer as specified in RFC 7468: // There is exactly one space character (SP) separating the "BEGIN" or // "END" from the label. There are exactly five hyphen-minus (also @@ -48,12 +49,19 @@ public class X509CertificateUtils { // boundaries, no more, no less. public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes(); public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes(); - private static CertificateFactory sCertFactory = null; private static void buildCertFactory() { if (sCertFactory != null) { return; } + + buildCertFactoryHelper(); + } + + private static synchronized void buildCertFactoryHelper() { + if (sCertFactory != null) { + return; + } try { sCertFactory = CertificateFactory.getInstance("X.509"); } catch (CertificateException e) { @@ -84,9 +92,7 @@ public class X509CertificateUtils { */ public static X509Certificate generateCertificate(byte[] encodedForm) throws CertificateException { - if (sCertFactory == null) { - buildCertFactory(); - } + buildCertFactory(); return generateCertificate(encodedForm, sCertFactory); } @@ -97,7 +103,7 @@ public class X509CertificateUtils { * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate. */ public static X509Certificate generateCertificate(byte[] encodedForm, - CertificateFactory certFactory) throws CertificateException { + CertificateFactory certFactory) throws CertificateException { X509Certificate certificate; try { certificate = (X509Certificate) certFactory.generateCertificate( @@ -149,9 +155,7 @@ public class X509CertificateUtils { */ public static Collection generateCertificates( InputStream in) throws CertificateException { - if (sCertFactory == null) { - buildCertFactory(); - } + buildCertFactory(); return generateCertificates(in, sCertFactory); } @@ -210,7 +214,7 @@ public class X509CertificateUtils { * it is already DER encoded. If the buffer does begin with the PEM certificate header then the * certificate data is read from the buffer until the PEM certificate footer is reached; this * data is then base64 decoded and returned in a new ByteBuffer. - *

+ * * If the buffer is in PEM format then the position of the buffer is moved to the end of the * current certificate; if the buffer is already DER encoded then the position of the buffer is * not modified. @@ -261,7 +265,7 @@ public class X509CertificateUtils { + "valid certificate footer"); } } - byte[] derEncoding = Base64.decode(pemEncoding.toString()); + byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString()); // consume any trailing whitespace in the byte buffer int nextEncodedChar = certificateBuffer.position(); while (certificateBuffer.hasRemaining()) { diff --git a/apksigner/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java b/apksigner/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java index 22f8b449..922f52c2 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java +++ b/apksigner/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java @@ -18,8 +18,8 @@ package com.android.apksig.internal.x509; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1Field; -import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; import java.math.BigInteger; diff --git a/apksigner/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java b/apksigner/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java index f41e73ec..d2f444dd 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java +++ b/apksigner/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java @@ -17,7 +17,6 @@ package com.android.apksig.internal.zip; import com.android.apksig.zip.ZipFormatException; - import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -80,6 +79,50 @@ public class CentralDirectoryRecord { mNameSizeBytes = nameSizeBytes; } + public int getSize() { + return mData.remaining(); + } + + public String getName() { + return mName; + } + + public int getNameSizeBytes() { + return mNameSizeBytes; + } + + public short getGpFlags() { + return mGpFlags; + } + + public short getCompressionMethod() { + return mCompressionMethod; + } + + public int getLastModificationTime() { + return mLastModificationTime; + } + + public int getLastModificationDate() { + return mLastModificationDate; + } + + public long getCrc32() { + return mCrc32; + } + + public long getCompressedSize() { + return mCompressedSize; + } + + public long getUncompressedSize() { + return mUncompressedSize; + } + + public long getLocalFileHeaderOffset() { + return mLocalFileHeaderOffset; + } + /** * Returns the Central Directory Record starting at the current position of the provided buffer * and advances the buffer's position immediately past the end of the record. @@ -147,6 +190,31 @@ public class CentralDirectoryRecord { nameSize); } + public void copyTo(ByteBuffer output) { + output.put(mData.slice()); + } + + public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( + long localFileHeaderOffset) { + ByteBuffer result = ByteBuffer.allocate(mData.remaining()); + result.put(mData.slice()); + result.flip(); + result.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); + return new CentralDirectoryRecord( + result, + mGpFlags, + mCompressionMethod, + mLastModificationTime, + mLastModificationDate, + mCrc32, + mCompressedSize, + mUncompressedSize, + localFileHeaderOffset, + mName, + mNameSizeBytes); + } + public static CentralDirectoryRecord createWithDeflateCompressedData( String name, int lastModifiedTime, @@ -218,75 +286,6 @@ public class CentralDirectoryRecord { return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8); } - public int getSize() { - return mData.remaining(); - } - - public String getName() { - return mName; - } - - public int getNameSizeBytes() { - return mNameSizeBytes; - } - - public short getGpFlags() { - return mGpFlags; - } - - public short getCompressionMethod() { - return mCompressionMethod; - } - - public int getLastModificationTime() { - return mLastModificationTime; - } - - public int getLastModificationDate() { - return mLastModificationDate; - } - - public long getCrc32() { - return mCrc32; - } - - public long getCompressedSize() { - return mCompressedSize; - } - - public long getUncompressedSize() { - return mUncompressedSize; - } - - public long getLocalFileHeaderOffset() { - return mLocalFileHeaderOffset; - } - - public void copyTo(ByteBuffer output) { - output.put(mData.slice()); - } - - public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( - long localFileHeaderOffset) { - ByteBuffer result = ByteBuffer.allocate(mData.remaining()); - result.put(mData.slice()); - result.flip(); - result.order(ByteOrder.LITTLE_ENDIAN); - ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); - return new CentralDirectoryRecord( - result, - mGpFlags, - mCompressionMethod, - mLastModificationTime, - mLastModificationDate, - mCrc32, - mCompressedSize, - mUncompressedSize, - localFileHeaderOffset, - mName, - mNameSizeBytes); - } - private static class ByLocalFileHeaderOffsetComparator implements Comparator { @Override diff --git a/apksigner/src/main/java/com/android/apksig/internal/zip/EocdRecord.java b/apksigner/src/main/java/com/android/apksig/internal/zip/EocdRecord.java index 9c531f48..d2000b42 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/zip/EocdRecord.java +++ b/apksigner/src/main/java/com/android/apksig/internal/zip/EocdRecord.java @@ -45,4 +45,13 @@ public class EocdRecord { ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset); return result; } + + public static ByteBuffer createWithPaddedComment(ByteBuffer original, int padding) { + ByteBuffer result = ByteBuffer.allocate((int) original.remaining() + padding); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(original.slice()); + result.rewind(); + ZipUtils.updateZipEocdCommentLen(result); + return result; + } } diff --git a/apksigner/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java b/apksigner/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java index 9b621e34..50ce386a 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java +++ b/apksigner/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java @@ -20,7 +20,6 @@ import com.android.apksig.internal.util.ByteBufferSink; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; import com.android.apksig.zip.ZipFormatException; - import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; @@ -48,12 +47,14 @@ public class LocalFileRecord { private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12; private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; - private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0); + private final String mName; private final int mNameSizeBytes; private final ByteBuffer mExtra; + private final long mStartOffsetInArchive; private final long mSize; + private final int mDataStartOffset; private final long mDataSize; private final boolean mDataCompressed; @@ -80,6 +81,40 @@ public class LocalFileRecord { mUncompressedDataSize = uncompressedDataSize; } + public String getName() { + return mName; + } + + public ByteBuffer getExtra() { + return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra; + } + + public int getExtraFieldStartOffsetInsideRecord() { + return HEADER_SIZE_BYTES + mNameSizeBytes; + } + + public long getStartOffsetInArchive() { + return mStartOffsetInArchive; + } + + public int getDataStartOffsetInRecord() { + return mDataStartOffset; + } + + /** + * Returns the size (in bytes) of this record. + */ + public long getSize() { + return mSize; + } + + /** + * Returns {@code true} if this record's file data is stored in compressed form. + */ + public boolean isDataCompressed() { + return mDataCompressed; + } + /** * Returns the Local File record starting at the current position of the provided buffer * and advances the buffer's position immediately past the end of the record. The record @@ -95,7 +130,7 @@ public class LocalFileRecord { cdStartOffset, true, // obtain extra field contents true // include Data Descriptor (if present) - ); + ); } /** @@ -258,126 +293,6 @@ public class LocalFileRecord { uncompressedDataSizeFromCdRecord); } - /** - * Outputs the specified Local File Header record with its data and returns the number of bytes - * output. - */ - public static long outputRecordWithDeflateCompressedData( - String name, - int lastModifiedTime, - int lastModifiedDate, - byte[] compressedData, - long crc32, - long uncompressedSize, - DataSink output) throws IOException { - byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); - int recordSize = HEADER_SIZE_BYTES + nameBytes.length; - ByteBuffer result = ByteBuffer.allocate(recordSize); - result.order(ByteOrder.LITTLE_ENDIAN); - result.putInt(RECORD_SIGNATURE); - ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract - result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name - result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED); - ZipUtils.putUnsignedInt16(result, lastModifiedTime); - ZipUtils.putUnsignedInt16(result, lastModifiedDate); - ZipUtils.putUnsignedInt32(result, crc32); - ZipUtils.putUnsignedInt32(result, compressedData.length); - ZipUtils.putUnsignedInt32(result, uncompressedSize); - ZipUtils.putUnsignedInt16(result, nameBytes.length); - ZipUtils.putUnsignedInt16(result, 0); // Extra field length - result.put(nameBytes); - if (result.hasRemaining()) { - throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); - } - result.flip(); - - long outputByteCount = result.remaining(); - output.consume(result); - outputByteCount += compressedData.length; - output.consume(compressedData, 0, compressedData.length); - return outputByteCount; - } - - /** - * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the - * provided data sink. - */ - public static void outputUncompressedData( - DataSource source, - CentralDirectoryRecord cdRecord, - long cdStartOffsetInArchive, - DataSink sink) throws ZipFormatException, IOException { - // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform - // exhibited when reading an APK for the purposes of verifying its signatures. - // When verifying an APK, Android doesn't care reading the extra field or the Data - // Descriptor. - LocalFileRecord lfhRecord = - getRecord( - source, - cdRecord, - cdStartOffsetInArchive, - false, // don't care about the extra field - false // don't read the Data Descriptor - ); - lfhRecord.outputUncompressedData(source, sink); - } - - /** - * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record. - */ - public static byte[] getUncompressedData( - DataSource source, - CentralDirectoryRecord cdRecord, - long cdStartOffsetInArchive) throws ZipFormatException, IOException { - if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) { - throw new IOException( - cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize()); - } - byte[] result = new byte[(int) cdRecord.getUncompressedSize()]; - ByteBuffer resultBuf = ByteBuffer.wrap(result); - ByteBufferSink resultSink = new ByteBufferSink(resultBuf); - outputUncompressedData( - source, - cdRecord, - cdStartOffsetInArchive, - resultSink); - return result; - } - - public String getName() { - return mName; - } - - public ByteBuffer getExtra() { - return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra; - } - - public int getExtraFieldStartOffsetInsideRecord() { - return HEADER_SIZE_BYTES + mNameSizeBytes; - } - - public long getStartOffsetInArchive() { - return mStartOffsetInArchive; - } - - public int getDataStartOffsetInRecord() { - return mDataStartOffset; - } - - /** - * Returns the size (in bytes) of this record. - */ - public long getSize() { - return mSize; - } - - /** - * Returns {@code true} if this record's file data is stored in compressed form. - */ - public boolean isDataCompressed() { - return mDataCompressed; - } - /** * Outputs this record and returns returns the number of bytes output. */ @@ -414,6 +329,48 @@ public class LocalFileRecord { return outputByteCount; } + /** + * Outputs the specified Local File Header record with its data and returns the number of bytes + * output. + */ + public static long outputRecordWithDeflateCompressedData( + String name, + int lastModifiedTime, + int lastModifiedDate, + byte[] compressedData, + long crc32, + long uncompressedSize, + DataSink output) throws IOException { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + int recordSize = HEADER_SIZE_BYTES + nameBytes.length; + ByteBuffer result = ByteBuffer.allocate(recordSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(RECORD_SIGNATURE); + ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract + result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name + result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED); + ZipUtils.putUnsignedInt16(result, lastModifiedTime); + ZipUtils.putUnsignedInt16(result, lastModifiedDate); + ZipUtils.putUnsignedInt32(result, crc32); + ZipUtils.putUnsignedInt32(result, compressedData.length); + ZipUtils.putUnsignedInt32(result, uncompressedSize); + ZipUtils.putUnsignedInt16(result, nameBytes.length); + ZipUtils.putUnsignedInt16(result, 0); // Extra field length + result.put(nameBytes); + if (result.hasRemaining()) { + throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); + } + result.flip(); + + long outputByteCount = result.remaining(); + output.consume(result); + outputByteCount += compressedData.length; + output.consume(compressedData, 0, compressedData.length); + return outputByteCount; + } + + private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0); + /** * Sends uncompressed data of this record into the the provided data sink. */ @@ -446,13 +403,65 @@ public class LocalFileRecord { } catch (IOException e) { throw new IOException( "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed") - + " entry " + mName, + + " entry " + mName, e); } // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We // thus don't check either. } + /** + * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the + * provided data sink. + */ + public static void outputUncompressedData( + DataSource source, + CentralDirectoryRecord cdRecord, + long cdStartOffsetInArchive, + DataSink sink) throws ZipFormatException, IOException { + // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform + // exhibited when reading an APK for the purposes of verifying its signatures. + // When verifying an APK, Android doesn't care reading the extra field or the Data + // Descriptor. + LocalFileRecord lfhRecord = + getRecord( + source, + cdRecord, + cdStartOffsetInArchive, + false, // don't care about the extra field + false // don't read the Data Descriptor + ); + lfhRecord.outputUncompressedData(source, sink); + } + + /** + * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record. + */ + public static byte[] getUncompressedData( + DataSource source, + CentralDirectoryRecord cdRecord, + long cdStartOffsetInArchive) throws ZipFormatException, IOException { + if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) { + throw new IOException( + cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize()); + } + byte[] result = null; + try { + result = new byte[(int) cdRecord.getUncompressedSize()]; + } catch (OutOfMemoryError e) { + throw new IOException( + cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize(), e); + } + ByteBuffer resultBuf = ByteBuffer.wrap(result); + ByteBufferSink resultSink = new ByteBufferSink(resultBuf); + outputUncompressedData( + source, + cdRecord, + cdStartOffsetInArchive, + resultSink); + return result; + } + /** * {@link DataSink} which inflates received data and outputs the deflated data into the provided * delegate sink. diff --git a/apksigner/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/apksigner/src/main/java/com/android/apksig/internal/zip/ZipUtils.java index 687c3473..1c2e82cd 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/zip/ZipUtils.java +++ b/apksigner/src/main/java/com/android/apksig/internal/zip/ZipUtils.java @@ -16,13 +16,18 @@ package com.android.apksig.internal.zip; +import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.util.Pair; import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; import java.util.zip.CRC32; import java.util.zip.Deflater; @@ -33,20 +38,22 @@ import java.util.zip.Deflater; * order of these buffers is little-endian. */ public abstract class ZipUtils { + private ZipUtils() {} + public static final short COMPRESSION_METHOD_STORED = 0; public static final short COMPRESSION_METHOD_DEFLATED = 8; + public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08; public static final short GP_FLAG_EFS = 0x0800; + private static final int ZIP_EOCD_REC_MIN_SIZE = 22; private static final int ZIP_EOCD_REC_SIG = 0x06054b50; private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; - private static final int UINT16_MAX_VALUE = 0xffff; - private ZipUtils() { - } + private static final int UINT16_MAX_VALUE = 0xffff; /** * Sets the offset of the start of the ZIP Central Directory in the archive. @@ -62,6 +69,20 @@ public abstract class ZipUtils { offset); } + /** + * Sets the length of EOCD comment. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE; + setUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET, + commentLen); + } + /** * Returns the offset of the start of the ZIP Central Directory in the archive. * @@ -104,7 +125,8 @@ public abstract class ZipUtils { * Returns the ZIP End of Central Directory record of the provided ZIP file. * * @return contents of the ZIP End of Central Directory record and the record's offset in the - * file or {@code null} if the file does not contain the record. + * file or {@code null} if the file does not contain the record. + * * @throws IOException if an I/O error occurs while reading the file. */ public static Pair findZipEndOfCentralDirectoryRecord(DataSource zip) @@ -142,10 +164,12 @@ public abstract class ZipUtils { * Returns the ZIP End of Central Directory record of the provided ZIP file. * * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted - * value is from 0 to 65535 inclusive. The smaller the value, the faster this method - * locates the record, provided its comment field is no longer than this value. + * value is from 0 to 65535 inclusive. The smaller the value, the faster this method + * locates the record, provided its comment field is no longer than this value. + * * @return contents of the ZIP End of Central Directory record and the record's offset in the - * file or {@code null} if the file does not contain the record. + * file or {@code null} if the file does not contain the record. + * * @throws IOException if an I/O error occurs while reading the file. */ private static Pair findZipEndOfCentralDirectoryRecord( @@ -214,7 +238,7 @@ public abstract class ZipUtils { int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; - expectedCommentLength++) { + expectedCommentLength++) { int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { int actualCommentLength = @@ -243,6 +267,46 @@ public abstract class ZipUtils { return buffer.getShort() & 0xffff; } + public static List parseZipCentralDirectory( + DataSource apk, + ZipSections apkSections) + throws IOException, ApkFormatException { + // Read the ZIP Central Directory + long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); + if (cdSizeBytes > Integer.MAX_VALUE) { + throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); + } + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); + cd.order(ByteOrder.LITTLE_ENDIAN); + + // Parse the ZIP Central Directory + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List cdRecords = new ArrayList<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP Central Directory record #" + (i + 1) + + " at file offset " + (cdOffset + offsetInsideCd), + e); + } + String entryName = cdRecord.getName(); + if (entryName.endsWith("/")) { + // Ignore directory entries + continue; + } + cdRecords.add(cdRecord); + } + // There may be more data in Central Directory, but we don't warn or throw because Android + // ignores unused CD data. + + return cdRecords; + } + static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { if ((value < 0) || (value > 0xffff)) { throw new IllegalArgumentException("uint16 value of out range: " + value); diff --git a/apksigner/src/main/java/com/android/apksig/kms/KmsException.java b/apksigner/src/main/java/com/android/apksig/kms/KmsException.java new file mode 100644 index 00000000..9daceaad --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/kms/KmsException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 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.kms; + +/** Represents an exception thrown by the external KMS. */ +public class KmsException extends RuntimeException { + private final String mKmsType; + + public KmsException(String kmsType, String message) { + super(message); + this.mKmsType = kmsType; + } + + public KmsException(String kmsType, String message, Throwable cause) { + super(message, cause); + this.mKmsType = kmsType; + } + + public KmsException(String kmsType, Throwable cause) { + super(cause); + this.mKmsType = kmsType; + } + + @Override + public String getMessage() { + return "KMS " + mKmsType + " threw exception: " + super.getMessage(); + } +} diff --git a/apksigner/src/main/java/com/android/apksig/kms/KmsSignerEngineProvider.java b/apksigner/src/main/java/com/android/apksig/kms/KmsSignerEngineProvider.java new file mode 100644 index 00000000..957917b5 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/kms/KmsSignerEngineProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.kms; + +import com.android.apksig.KeyConfig; +import com.android.apksig.SignerEngine; + +import java.security.spec.AlgorithmParameterSpec; + +public interface KmsSignerEngineProvider { + + /** Instantiates a concrete signer engine */ + SignerEngine getInstance( + KeyConfig.Kms kmsConfig, + String jcaSignatureAlgorithm, + AlgorithmParameterSpec algorithmParameterSpec); + + /** Which KMS provider this engine applies to */ + String getKmsType(); +} diff --git a/apksigner/src/main/java/com/android/apksig/internal/util/SupplierCompat.java b/apksigner/src/main/java/com/android/apksig/kms/KmsType.java similarity index 70% rename from apksigner/src/main/java/com/android/apksig/internal/util/SupplierCompat.java rename to apksigner/src/main/java/com/android/apksig/kms/KmsType.java index 7e867c01..de81f4d1 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/util/SupplierCompat.java +++ b/apksigner/src/main/java/com/android/apksig/kms/KmsType.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Muntashir Al-Islam + * Copyright (C) 2024 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. @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.apksig.internal.util; +package com.android.apksig.kms; -@FunctionalInterface -public interface SupplierCompat { - T get(); +/** Represents the supported Key Management Services. */ +public class KmsType { + public static String AWS = "aws"; + public static String GCP = "gcp"; } diff --git a/apksigner/src/main/java/com/android/apksig/util/DataSink.java b/apksigner/src/main/java/com/android/apksig/util/DataSink.java index 31689e34..5042933f 100644 --- a/apksigner/src/main/java/com/android/apksig/util/DataSink.java +++ b/apksigner/src/main/java/com/android/apksig/util/DataSink.java @@ -31,7 +31,7 @@ public interface DataSink { * terminates. * * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if - * {@code offset + length} is greater than {@code buf.length}. + * {@code offset + length} is greater than {@code buf.length}. */ void consume(byte[] buf, int offset, int length) throws IOException; diff --git a/apksigner/src/main/java/com/android/apksig/util/DataSinks.java b/apksigner/src/main/java/com/android/apksig/util/DataSinks.java index dd6de5e9..d9562d83 100644 --- a/apksigner/src/main/java/com/android/apksig/util/DataSinks.java +++ b/apksigner/src/main/java/com/android/apksig/util/DataSinks.java @@ -20,7 +20,6 @@ import com.android.apksig.internal.util.ByteArrayDataSink; import com.android.apksig.internal.util.MessageDigestSink; import com.android.apksig.internal.util.OutputStreamDataSink; import com.android.apksig.internal.util.RandomAccessFileDataSink; - import java.io.OutputStream; import java.io.RandomAccessFile; import java.security.MessageDigest; @@ -29,8 +28,7 @@ import java.security.MessageDigest; * Utility methods for working with {@link DataSink} abstraction. */ public abstract class DataSinks { - private DataSinks() { - } + private DataSinks() {} /** * Returns a {@link DataSink} which outputs received data into the provided diff --git a/apksigner/src/main/java/com/android/apksig/util/DataSource.java b/apksigner/src/main/java/com/android/apksig/util/DataSource.java index fffc1e78..a89a87c5 100644 --- a/apksigner/src/main/java/com/android/apksig/util/DataSource.java +++ b/apksigner/src/main/java/com/android/apksig/util/DataSource.java @@ -61,9 +61,10 @@ public interface DataSource { * Feeds the specified chunk from this data source into the provided sink. * * @param offset index (in bytes) at which the chunk starts inside data source - * @param size size (in bytes) of the chunk + * @param size size (in bytes) of the chunk + * * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if - * {@code offset + size} is greater than {@link #size()}. + * {@code offset + size} is greater than {@link #size()}. */ void feed(long offset, long size, DataSink sink) throws IOException; @@ -76,9 +77,10 @@ public interface DataSource { * {@code size}. * * @param offset index (in bytes) at which the chunk starts inside data source - * @param size size (in bytes) of the chunk + * @param size size (in bytes) of the chunk + * * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if - * {@code offset + size} is greater than {@link #size()}. + * {@code offset + size} is greater than {@link #size()}. */ ByteBuffer getByteBuffer(long offset, int size) throws IOException; @@ -87,9 +89,10 @@ public interface DataSource { * advancing the destination buffer's position by {@code size}. * * @param offset index (in bytes) at which the chunk starts inside data source - * @param size size (in bytes) of the chunk + * @param size size (in bytes) of the chunk + * * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if - * {@code offset + size} is greater than {@link #size()}. + * {@code offset + size} is greater than {@link #size()}. */ void copyTo(long offset, int size, ByteBuffer dest) throws IOException; @@ -98,9 +101,10 @@ public interface DataSource { * to data represented by this data source will also be visible in the returned data source. * * @param offset index (in bytes) at which the region starts inside data source - * @param size size (in bytes) of the region + * @param size size (in bytes) of the region + * * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if - * {@code offset + size} is greater than {@link #size()}. + * {@code offset + size} is greater than {@link #size()}. */ DataSource slice(long offset, long size); } diff --git a/apksigner/src/main/java/com/android/apksig/util/DataSources.java b/apksigner/src/main/java/com/android/apksig/util/DataSources.java index 987e911f..1f0b40b6 100644 --- a/apksigner/src/main/java/com/android/apksig/util/DataSources.java +++ b/apksigner/src/main/java/com/android/apksig/util/DataSources.java @@ -18,7 +18,6 @@ package com.android.apksig.util; import com.android.apksig.internal.util.ByteBufferDataSource; import com.android.apksig.internal.util.FileChannelDataSource; - import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @@ -27,8 +26,7 @@ import java.nio.channels.FileChannel; * Utility methods for working with {@link DataSource} abstraction. */ public abstract class DataSources { - private DataSources() { - } + private DataSources() {} /** * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source diff --git a/apksigner/src/main/java/com/android/apksig/zip/ZipSections.java b/apksigner/src/main/java/com/android/apksig/zip/ZipSections.java new file mode 100644 index 00000000..17bce051 --- /dev/null +++ b/apksigner/src/main/java/com/android/apksig/zip/ZipSections.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 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.zip; + +import java.nio.ByteBuffer; + +/** + * Base representation of an APK's zip sections containing the central directory's offset, the size + * of the central directory in bytes, the number of records in the central directory, the offset + * of the end of central directory, and a ByteBuffer containing the end of central directory + * contents. + */ +public class ZipSections { + private final long mCentralDirectoryOffset; + private final long mCentralDirectorySizeBytes; + private final int mCentralDirectoryRecordCount; + private final long mEocdOffset; + private final ByteBuffer mEocd; + + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + mCentralDirectoryOffset = centralDirectoryOffset; + mCentralDirectorySizeBytes = centralDirectorySizeBytes; + mCentralDirectoryRecordCount = centralDirectoryRecordCount; + mEocdOffset = eocdOffset; + mEocd = eocd; + } + + /** + * Returns the start offset of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectoryOffset() { + return mCentralDirectoryOffset; + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectorySizeBytes() { + return mCentralDirectorySizeBytes; + } + + /** + * Returns the number of records in the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public int getZipCentralDirectoryRecordCount() { + return mCentralDirectoryRecordCount; + } + + /** + * Returns the start offset of the ZIP End of Central Directory record. The record extends + * until the very end of the APK. + */ + public long getZipEndOfCentralDirectoryOffset() { + return mEocdOffset; + } + + /** + * Returns the contents of the ZIP End of Central Directory. + */ + public ByteBuffer getZipEndOfCentralDirectory() { + return mEocd; + } +} \ No newline at end of file diff --git a/apksigner/src/main/java/com/android/apksigner/ApkSignerTool.java b/apksigner/src/main/java/com/android/apksigner/ApkSignerTool.java index cee15c24..f6c30cd1 100644 --- a/apksigner/src/main/java/com/android/apksigner/ApkSignerTool.java +++ b/apksigner/src/main/java/com/android/apksigner/ApkSignerTool.java @@ -26,8 +26,6 @@ import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.apksigner.utils.FileUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.spongycastle.jce.provider.BouncyCastleProvider; import java.io.BufferedReader; @@ -611,7 +609,6 @@ public class ApkSignerTool { } if ((warningsTreatedAsErrors) && (warningsEncountered)) { //System.exit(1); - return; } } diff --git a/apksigner/src/main/java/com/android/apksigner/PasswordRetriever.java b/apksigner/src/main/java/com/android/apksigner/PasswordRetriever.java index ede89acc..ed6927bb 100644 --- a/apksigner/src/main/java/com/android/apksigner/PasswordRetriever.java +++ b/apksigner/src/main/java/com/android/apksigner/PasswordRetriever.java @@ -361,7 +361,7 @@ public class PasswordRetriever implements AutoCloseable { *

NOTE: This method adds only the passwords/variants which are not yet in the list. */ private void addPasswords(List passwords, char[] pwd, Charset... additionalEncodings) { - if ((additionalEncodings != null) && (additionalEncodings.length > 0)) { + if (additionalEncodings != null) { for (Charset encoding : additionalEncodings) { // Password encoded using provided encoding (usually the console's character // encoding) and upcast into char[] diff --git a/apksigner/src/main/java/com/mcal/apksigner/ApkSigner.kt b/apksigner/src/main/java/com/mcal/apksigner/ApkSigner.kt index d37eb36d..7c80d7fd 100644 --- a/apksigner/src/main/java/com/mcal/apksigner/ApkSigner.kt +++ b/apksigner/src/main/java/com/mcal/apksigner/ApkSigner.kt @@ -60,6 +60,7 @@ class ApkSigner( ): Boolean { return try { val keystore = KeyStoreHelper.loadKeyStore(keyFile, password.toCharArray()) + @Suppress("DEPRECATION") ApkSigner.Builder( listOf( ApkSigner.SignerConfig.Builder( diff --git a/apksigner/src/main/java/com/mcal/apksigner/CertCreator.kt b/apksigner/src/main/java/com/mcal/apksigner/CertCreator.kt index c880ffdd..d8927950 100644 --- a/apksigner/src/main/java/com/mcal/apksigner/CertCreator.kt +++ b/apksigner/src/main/java/com/mcal/apksigner/CertCreator.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.mcal.apksigner import com.mcal.apksigner.utils.DistinguishedNameValues diff --git a/apksigner/src/main/java/com/mcal/apksigner/utils/DistinguishedNameValues.kt b/apksigner/src/main/java/com/mcal/apksigner/utils/DistinguishedNameValues.kt index a030cf3d..7e5253f4 100644 --- a/apksigner/src/main/java/com/mcal/apksigner/utils/DistinguishedNameValues.kt +++ b/apksigner/src/main/java/com/mcal/apksigner/utils/DistinguishedNameValues.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.mcal.apksigner.utils import org.spongycastle.asn1.ASN1ObjectIdentifier diff --git a/app/src/androidTest/java/org/autojs/autojs/statics/RhinoE4XTest.kt b/app/src/androidTest/java/org/autojs/autojs/statics/RhinoE4XTest.kt index 0be380bf..f579d825 100644 --- a/app/src/androidTest/java/org/autojs/autojs/statics/RhinoE4XTest.kt +++ b/app/src/androidTest/java/org/autojs/autojs/statics/RhinoE4XTest.kt @@ -1,20 +1,20 @@ -package org.autojs.autojs.statics; +package org.autojs.autojs.statics -import org.junit.Test; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.Scriptable; +import org.junit.Test +import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable /** * Created by Stardust on May 13, 2017. */ -public class RhinoE4XTest { - +class RhinoE4XTest { @Test - public void testAttributeName() { - Context context = Context.enter(); - Scriptable scriptable = context.initStandardObjects(); - context.setOptimizationLevel(-1); - Object o = context.evaluateString(scriptable, "XML.ignoreProcessingInstructions = true; ().attributes()[0].name()", "", 1, null); - System.out.println(o); + @Suppress("DEPRECATION") + fun testAttributeName() { + val context = Context.enter() + val scriptable: Scriptable = context.initStandardObjects() + context.optimizationLevel = -1 + val o = context.evaluateString(scriptable, "XML.ignoreProcessingInstructions = true; ().attributes()[0].name()", "", 1, null) + println(o) } } diff --git a/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt b/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt index e8d9bb68..46edba30 100644 --- a/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt +++ b/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt @@ -763,7 +763,7 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef private fun showInfo() { when { mExplorerItem.isInstallable -> { - ApkInfoDialogManager.showApkInfoDialog(context, mExplorerItem) + ApkInfoDialogManager.showApkInfoDialog(context, mExplorerItem.toScriptFile()) notifyItemOperated() } mExplorerItem.isMediaMenu || mExplorerItem.isMediaPlayable -> { diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt index 8b5dba2a..f3efa626 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt @@ -18,8 +18,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.autojs.autojs.model.explorer.ExplorerItem +import org.autojs.autojs.pio.PFiles import org.autojs.autojs.runtime.api.AppUtils +import org.autojs.autojs.util.IntentUtils import org.autojs.autojs6.R import org.autojs.autojs6.databinding.ApkFileInfoDialogListItemBinding import java.io.File @@ -28,28 +29,30 @@ object ApkInfoDialogManager { @JvmStatic @SuppressLint("SetTextI18n") - fun showApkInfoDialog(context: Context, explorerItem: ExplorerItem) { + fun showApkInfoDialog(context: Context, apkFile: File) { val binding = ApkFileInfoDialogListItemBinding.inflate(LayoutInflater.from(context)) val root = binding.root as ViewGroup + val apkFilePath = apkFile.absolutePath + val dialog = MaterialDialog.Builder(context) - .title(explorerItem.name) + .title(apkFile.name) .customView(root, false) .autoDismiss(false) .iconRes(R.drawable.transparent) .limitIconToDefaultSize() .positiveText(R.string.text_install) + .positiveColorRes(R.color.dialog_button_attraction) .onPositive { materialDialog, _ -> materialDialog.dismiss() - explorerItem.install(context) + IntentUtils.installApk(context, apkFilePath) } - .positiveColorRes(R.color.dialog_button_attraction) .negativeText(R.string.text_cancel) - .onNegative { materialDialog, _ -> materialDialog.dismiss() } + .negativeColorRes(R.color.dialog_button_default) .neutralColorRes(R.color.dialog_button_hint) + .onNegative { materialDialog, _ -> materialDialog.dismiss() } .show() - val apkFilePath = explorerItem.toScriptFile().absolutePath val packageManager = context.packageManager CoroutineScope(Dispatchers.Main).launch { @@ -67,6 +70,10 @@ object ApkInfoDialogManager { } } + val fileSize = withContext(Dispatchers.IO) { PFiles.getHumanReadableSize(apkFile.length()) } + + val signatureScheme = withContext(Dispatchers.IO) { getApkSignatureInfo(apkFile) } + withContext(Dispatchers.Main) { // @Hint by SuperMonster003 on Nov 27, 2024. // ! Prioritize handling "installed version" to determine whether to display its content view. @@ -85,12 +92,15 @@ object ApkInfoDialogManager { binding.deviceSdkValue.text = "${Build.VERSION.SDK_INT}" bindVersionInfo(binding, context, versionName, versionCode) + binding.fileSizeValue.setTextIfAbsent { fileSize } + binding.signatureSchemeValue.setTextIfAbsent { signatureScheme } + dialog.setIcon(applicationInfo?.apply { sourceDir = apkFilePath publicSourceDir = apkFilePath }?.loadIcon(packageManager) ?: context.getDrawable(R.drawable.ic_packaging)) - val apkInfo = getApkInfo(explorerItem.toScriptFile()) + val apkInfo = getApkInfo(apkFile) binding.labelNameValue.setTextIfAbsent { apkInfo?.label } binding.packageNameValue.setTextIfAbsent { apkInfo?.packageName } @@ -153,24 +163,22 @@ object ApkInfoDialogManager { } private fun restoreEssentialViews(binding: ApkFileInfoDialogListItemBinding, context: Context) { - binding.labelNameLabel.text = context.getString(R.string.text_label_name) - binding.labelNameColon.isVisible = true - binding.labelNameValue.isVisible = true - binding.packageNameLabel.text = context.getString(R.string.apk_info_package_name) - binding.packageNameColon.isVisible = true - binding.packageNameValue.isVisible = true - binding.versionPlaceholderLabel.text = context.getString(R.string.text_version) - binding.versionPlaceholderColon.isVisible = true - binding.versionPlaceholderValue.isVisible = true - binding.minSdkLabel.text = context.getString(R.string.apk_info_min_sdk) - binding.minSdkColon.isVisible = true - binding.minSdkValue.isVisible = true - binding.targetSdkLabel.text = context.getString(R.string.apk_info_target_sdk) - binding.targetSdkColon.isVisible = true - binding.targetSdkValue.isVisible = true - binding.deviceSdkLabel.text = context.getString(R.string.apk_info_device_sdk) - binding.deviceSdkColon.isVisible = true - binding.deviceSdkValue.isVisible = true + listOf( + Triple(binding.labelNameLabel, binding.labelNameColon, binding.labelNameValue) to R.string.text_label_name, + Triple(binding.packageNameLabel, binding.packageNameColon, binding.packageNameValue) to R.string.apk_info_package_name, + Triple(binding.versionPlaceholderLabel, binding.versionPlaceholderColon, binding.versionPlaceholderValue) to R.string.text_version, + Triple(binding.fileSizeLabel, binding.fileSizeColon, binding.fileSizeValue) to R.string.apk_info_file_size, + Triple(binding.signatureSchemeLabel, binding.signatureSchemeColon, binding.signatureSchemeValue) to R.string.apk_info_signature_scheme, + Triple(binding.minSdkLabel, binding.minSdkColon, binding.minSdkValue) to R.string.apk_info_min_sdk, + Triple(binding.targetSdkLabel, binding.targetSdkColon, binding.targetSdkValue) to R.string.apk_info_target_sdk, + Triple(binding.deviceSdkLabel, binding.deviceSdkColon, binding.deviceSdkValue) to R.string.apk_info_device_sdk, + ).forEach { pair -> + val (triple, labelTextRes) = pair + val (labelView, colonView, valueView) = triple + labelView.text = context.getString(labelTextRes) + colonView.isVisible = true + valueView.isVisible = true + } } private fun updateGuidelines(binding: ApkFileInfoDialogListItemBinding) { @@ -178,6 +186,8 @@ object ApkInfoDialogManager { binding.labelNameLabel to binding.labelNameGuideline, binding.packageNameLabel to binding.packageNameGuideline, binding.versionPlaceholderLabel to binding.versionPlaceholderGuideline, + binding.fileSizeLabel to binding.fileSizeGuideline, + binding.signatureSchemeLabel to binding.signatureSchemeGuideline, binding.minSdkLabel to binding.minSdkGuideline, binding.targetSdkLabel to binding.targetSdkGuideline, binding.installedVersionLabel to binding.installedVersionGuideline, @@ -194,8 +204,8 @@ object ApkInfoDialogManager { } } - private fun getApkSignatureInfo(apkFilePath: String): String? = runCatching { - ApkVerifier.Builder(File(apkFilePath)).build().verify().run { + private fun getApkSignatureInfo(apkFile: File): String? = runCatching { + ApkVerifier.Builder(apkFile).build().verify().run { listOfNotNull( "V1".takeIf { isVerifiedUsingV1Scheme }, "V2".takeIf { isVerifiedUsingV2Scheme }, diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt index ede31918..890db56c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ExplorerFragment.kt @@ -54,7 +54,7 @@ open class ExplorerFragment : ViewPagerFragment(0), OnFloatingActionButtonClickL override fun onItemClick(view: View?, item: ExplorerItem) { when { item.isTextEditable -> Scripts.edit(requireActivity(), item.toScriptFile()) - item.isInstallable -> ApkInfoDialogManager.showApkInfoDialog(requireActivity(), item) + item.isInstallable -> ApkInfoDialogManager.showApkInfoDialog(requireActivity(), item.toScriptFile()) item.isMediaMenu || item.isMediaPlayable -> MediaInfoDialogManager.showMediaInfoDialog(requireActivity(), item) else -> viewFile(item) } diff --git a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java index 57f9b4db..45f59230 100644 --- a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java +++ b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java @@ -42,6 +42,7 @@ import org.autojs.autojs.ui.BaseActivity; import org.autojs.autojs.ui.common.NotAskAgainDialog; import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder; import org.autojs.autojs.ui.keystore.ManageKeyStoreActivity; +import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager; import org.autojs.autojs.ui.shortcut.AppsIconSelectActivity; import org.autojs.autojs.ui.viewmodel.KeyStoreViewModel; import org.autojs.autojs.ui.widget.RoundCheckboxWithText; @@ -880,8 +881,13 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa .title(R.string.text_build_succeeded) .content(getString(R.string.format_build_succeeded, outApk.getPath())) .positiveText(R.string.text_install) - .negativeText(R.string.text_cancel) + .positiveColorRes(R.color.dialog_button_attraction) .onPositive((dialog, which) -> IntentUtils.installApk(BuildActivity.this, outApk.getPath())) + .negativeText(R.string.text_cancel) + .negativeColorRes(R.color.dialog_button_default) + .neutralText(R.string.dialog_button_file_information) + .neutralColorRes(R.color.dialog_button_hint) + .onNeutral((dialog, which) -> ApkInfoDialogManager.showApkInfoDialog(dialog.getContext(), outApk)) .show(); } diff --git a/app/src/main/res/layout/apk_file_info_dialog_list_item.xml b/app/src/main/res/layout/apk_file_info_dialog_list_item.xml index 4dd19df3..d0e62c72 100644 --- a/app/src/main/res/layout/apk_file_info_dialog_list_item.xml +++ b/app/src/main/res/layout/apk_file_info_dialog_list_item.xml @@ -237,7 +237,7 @@ android:layout_marginTop="4sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/version_placeholder_parent" - app:layout_constraintBottom_toTopOf="@id/min_sdk_parent" + app:layout_constraintBottom_toTopOf="@id/file_size_parent" app:layout_constraintEnd_toEndOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index b95a9147..edca81d7 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -890,5 +890,8 @@ 包名 (计算中...) 版本名称 (计算中...) 版本号 (计算中...) + 文件大小 + 签名方案 + 文件信息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5289bdf..ce4c644b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1123,5 +1123,8 @@ Version code (computing...) 1.0.0 1 + File size + Signature + File info diff --git a/settings.gradle.kts b/settings.gradle.kts index 76220f08..58df6311 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,32 +25,41 @@ // ! 查看 KSP (Kotlin Symbol Processing) 插件的发行版本, // ! 可访问 https://github.com/google/ksp/releases. -include( - ":app", - ":modules:jieba-analysis", - ":modules:apksigner", - - ":libs:android-job-simplified-1.4.3", - ":libs:androidx.appcompat-1.0.2", - ":libs:apk-parser-1.0.2", - ":libs:com.tencent.bugly.crashreport-4.0.4", - ":libs:jackpal.androidterm-1.0.70", - ":libs:jackpal.androidterm.emulatorview-1.0.42", - ":libs:jackpal.androidterm.libtermexec-1.0", - ":libs:org.opencv-4.8.0", - ":libs:paddleocr", - ":libs:rapidocr", - - ":libs:android-spackle-9.0.0", - ":libs:android-assertion-9.0.0", - ":libs:android-plugin-client-sdk-for-locale-9.0.0", - - ":libs:markwon-core-4.6.2", - ":libs:markwon-syntax-highlight-4.6.2", +private val modules = listOf( + "jieba-analysis", + "apksigner", ) -project(":modules:jieba-analysis").projectDir = file("jieba-analysis") -project(":modules:apksigner").projectDir = file("apksigner") +private val libs = listOf( + "android-job-simplified-1.4.3", + "androidx.appcompat-1.0.2", + "apk-parser-1.0.2", + "com.tencent.bugly.crashreport-4.0.4", + "org.opencv-4.8.0", + "paddleocr", + "rapidocr", + + "jackpal.androidterm-1.0.70", + "jackpal.androidterm.emulatorview-1.0.42", + "jackpal.androidterm.libtermexec-1.0", + + "android-spackle-9.0.0", + "android-assertion-9.0.0", + "android-plugin-client-sdk-for-locale-9.0.0", + + "markwon-core-4.6.2", + "markwon-syntax-highlight-4.6.2", +) + +include( + ":app", + *modules.map { ":modules:$it" }.toTypedArray(), + *libs.map { ":libs:$it" }.toTypedArray(), +) + +modules.forEach { + project(":modules:$it").projectDir = file(it) +} pluginManagement { diff --git a/version.properties b/version.properties index 1e8930cb..f3801b08 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Thu Jan 02 22:09:26 CST 2025 -BUILD_TIME=1735826966393 +#Fri Jan 03 16:34:34 CST 2025 +BUILD_TIME=1735893274360 COMPILE_SDK_VERSION=34 JAVA_VERSION=23 JAVA_VERSION_MIN_RADICAL=0 @@ -17,6 +17,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=34 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=2945 +VERSION_BUILD=2955 VERSION_NAME=6.6.2 Alpha VSCODE_EXT_REQUIRED_VERSION=1.0.8