diff options
author | Alex Buynytskyy <alexbuy@google.com> | 2021-10-13 16:30:03 -0700 |
---|---|---|
committer | Alex Buynytskyy <alexbuy@google.com> | 2021-10-14 18:00:27 +0000 |
commit | 9d96cb526207a61e47948f7ec1fb590ff9dda8cb (patch) | |
tree | ef82fb9bcb160de185e37fafcb473551350144b7 | |
parent | beedf367a5e2bb8c1cac193473fd42804aaf387e (diff) | |
download | apksig-9d96cb526207a61e47948f7ec1fb590ff9dda8cb.tar.gz |
v4.1: support v3.1 key rotation in v4 signature
Bug: 202011194
Test: ./gradlew test
Test: adb install on T/master
$ adb install orig.apk
Performing Incremental Install
Serving...
All files should be loaded. Notifying the device.
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl54972609.tmp/orig.apk using APK Signature Scheme v4: V4 signature certificate does not match V2/V3]
Performing Streamed Install
Success
Test: adb install on S (sc-dev)
$ adb install orig.apk
Performing Incremental Install
Serving...
All files should be loaded. Notifying the device.
Success
Install command complete in 229 ms
Change-Id: Id1910fbc88f1399ea5b1ea33a43962e7f36c009b
6 files changed, 266 insertions, 103 deletions
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java index fb8648c..31b13e3 100644 --- a/src/main/java/com/android/apksig/ApkVerifier.java +++ b/src/main/java/com/android/apksig/ApkVerifier.java @@ -490,9 +490,6 @@ public class ApkVerifier { // The apkDigest field in the v4 signature should match the selected v2/v3. if (result.isVerifiedUsingV4Scheme()) { List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners(); - if (v4Signers.size() != 1) { - result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); - } List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 = v4Signers.get(0).getContentDigests(); @@ -502,21 +499,22 @@ public class ApkVerifier { final byte[] digestFromV4 = digestsFromV4.get(0).getValue(); if (result.isVerifiedUsingV3Scheme()) { - List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners(); - if (v3Signers.size() != 1) { + int expectedSize = result.isVerifiedUsingV31Scheme() ? 2 : 1; + if (v4Signers.size() != expectedSize) { 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); + checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4, + result); + if (result.isVerifiedUsingV31Scheme()) { + checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts, + digestFromV4, result); } } else if (result.isVerifiedUsingV2Scheme()) { + if (v4Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners(); if (v2Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); @@ -931,6 +929,22 @@ public class ApkVerifier { return result; } + private static void checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers, + List<X509Certificate> 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); + } + } + private static void checkV4Certificate(List<X509Certificate> v4Certs, List<X509Certificate> v2v3Certs, Result result) { try { diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java index aee7cdb..6423804 100644 --- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java +++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -339,18 +339,43 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - private List<ApkSigningBlockUtils.SignerConfig> processV3Configs( - List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException { - List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>(); - + private int getDevReleaseRotationMinSdkVersion() { // TODO (b/199793805): Once the T SDK is finalized and T development releases are using // the new SDK version, this should be removed and mRotationMinSdkVersion should be used // as is for rotation SDK version targeting. // To support targeting the development release use the API level of the previous // platform release as this is the value returned from Build.Version.SDK_INT until // the SDK is finalized. - int rotationMinSdkVersion = mRotationMinSdkVersion == MIN_SDK_WITH_V31_SUPPORT + return mRotationMinSdkVersion == MIN_SDK_WITH_V31_SUPPORT ? DEV_RELEASE_ROTATION_MIN_SDK_VERSION : mRotationMinSdkVersion; + } + + private boolean signingLineageHas31Support() { + return mSigningCertificateLineage != null + && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT + && mMinSdkVersion < mRotationMinSdkVersion; + } + + private List<ApkSigningBlockUtils.SignerConfig> processV3Configs( + List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException { + // While the V3 signature scheme supports rotation, it is possible for a caller to specify + // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1; + // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block + // will use the original signing key. + if (signingLineageHas31Support()) { + SigningCertificateLineage subLineage = mSigningCertificateLineage + .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0)); + if (subLineage.size() != 1) { + throw new IllegalArgumentException( + "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage" + + " for the v3.0 signing block is missing. Please provide" + + " the oldest signer to enable v3.1 signing."); + } + } + + List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>(); + + int rotationMinSdkVersion = getDevReleaseRotationMinSdkVersion(); // we have our configs, now touch them up to appropriately cover all SDK levels since APK // signature scheme v3 was introduced int currentMinSdk = Integer.MAX_VALUE; @@ -418,38 +443,47 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs( boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { - // While the V3 signature scheme supports rotation, it is possible for a caller to specify - // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1; - // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block - // will use the original signing key. - if (mSigningCertificateLineage != null - && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT - && mMinSdkVersion < mRotationMinSdkVersion) { - SigningCertificateLineage subLineage = mSigningCertificateLineage - .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0)); - if (subLineage.size() != 1) { - throw new IllegalArgumentException( - "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage" - + " for the v3.0 signing block is missing. Please provide" - + " the oldest signer to enable v3.1 signing."); - } - } - return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3)); } - private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() throws InvalidKeyException { - List<ApkSigningBlockUtils.SignerConfig> configs = createSigningBlockSignerConfigs(true, - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); - if (configs.size() != 1) { - // V4 only uses signer config to connect back to v3. Use the same filtering logic. - configs = processV3Configs(configs); + private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs( + List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) { + // If the signing key has been rotated, the caller has requested to use the rotated + // signing key starting from an SDK version where v3.1 is supported, and the minimum + // SDK version for the APK is less than the requested rotation minimum, then the APK + // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0 + // scheme with the original signing key. If the APK's minSdkVersion is >= the requested + // SDK version for rotation then just use the v3.0 signing block for this. + if (!signingLineageHas31Support()) { + return null; } - if (configs.size() != 1) { - throw new InvalidKeyException("Only accepting one signer config for V4 Signature."); + + int rotationMinSdkVersion = getDevReleaseRotationMinSdkVersion(); + List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>(); + Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = + v3SignerConfigs.iterator(); + while (v3SignerIterator.hasNext()) { + ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next(); + // All signing configs with a min SDK version that supports v3.1 should be used + // in the v3.1 signing block and removed from the v3.0 block. + if (signerConfig.minSdkVersion >= rotationMinSdkVersion) { + v31SignerConfigs.add(signerConfig); + v3SignerIterator.remove(); + } } - return configs.get(0); + return v31SignerConfigs; + } + + private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException { + List<ApkSigningBlockUtils.SignerConfig> 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<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs); + return new V4SchemeSigner.SignerConfig(v4Configs, v41configs); } private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig() @@ -1045,32 +1079,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { invalidateV3Signature(); List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs = createV3SignerConfigs(apkSigningBlockPaddingSupported); - // If the signing key has been rotated, the caller has requested to use the rotated - // signing key starting from an SDK version where v3.1 is supported, and the minimum - // SDK version for the APK is less than the requested rotation minimum, then the APK - // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0 - // scheme with the original signing key. If the APK's minSdkVersion is >= the requested - // SDK version for rotation then just use the v3.0 signing block for this. - // TODO (b/199793805): Once the T SDK is finalized use the mRotationMinSdkVersion as - // is; this is required during T development since the SDK version of devices running - // T is the SDK version of the previous release. - int rotationMinSdkVersion = mRotationMinSdkVersion == MIN_SDK_WITH_V31_SUPPORT - ? DEV_RELEASE_ROTATION_MIN_SDK_VERSION : mRotationMinSdkVersion; - if (mSigningCertificateLineage != null - && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT - && mMinSdkVersion < mRotationMinSdkVersion) { - List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>(); - Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = - v3SignerConfigs.iterator(); - while (v3SignerIterator.hasNext()) { - ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next(); - // All signing configs with a min SDK version that supports v3.1 should be used - // in the v3.1 signing block and removed from the v3.0 block. - if (signerConfig.minSdkVersion >= rotationMinSdkVersion) { - v31SignerConfigs.add(signerConfig); - v3SignerIterator.remove(); - } - } + List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs( + v3SignerConfigs); + if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) { ApkSigningBlockUtils.SigningSchemeBlockAndDigests v31SigningSchemeBlockAndDigests = new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd, @@ -1086,10 +1097,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { zipCentralDirectory, eocd, v3SignerConfigs) .setRunnablesExecutor(mExecutor) .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); - if (mSigningCertificateLineage != null - && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT - && mMinSdkVersion < mRotationMinSdkVersion) { - builder.setRotationMinSdkVersion(rotationMinSdkVersion); + if (signingLineageHas31Support()) { + builder.setRotationMinSdkVersion(getDevReleaseRotationMinSdkVersion()); } v3SigningSchemeBlockAndDigests = builder.build().generateApkSignatureSchemeV3BlockAndDigests(); @@ -1164,7 +1173,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) { @@ -1181,7 +1190,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { throw new SignatureException("Missing V4 output streams."); } try { - ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig(); + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); Pair<V4Signature, byte[]> pair = V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig); pair.getFirst().writeTo(sigOutput); diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java index 74aa629..2f9ecb3 100644 --- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java @@ -22,11 +22,11 @@ import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE 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; import com.android.apksig.internal.apk.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeSigner; import com.android.apksig.internal.apk.v3.V3SchemeVerifier; import com.android.apksig.internal.util.Pair; @@ -43,6 +43,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; @@ -61,7 +62,6 @@ import java.util.Set; * </p> * (optional) verityTree: integer size prepended bytes of the verity hash tree. * <p> - * TODO(schfan): Add v4 unit tests */ public abstract class V4SchemeSigner { /** @@ -70,6 +70,23 @@ public abstract class V4SchemeSigner { private V4SchemeSigner() { } + public static class SignerConfig { + final public ApkSigningBlockUtils.SignerConfig v4Config; + final public ApkSigningBlockUtils.SignerConfig v41Config; + + public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs, + List<ApkSigningBlockUtils.SignerConfig> 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. */ @@ -149,10 +166,10 @@ 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()) { @@ -169,7 +186,7 @@ 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.getSignedData(fileSize, hashingInfo, signingInfoNoSignature); @@ -184,12 +201,33 @@ 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); + } + + private static V4Signature generateSignature( + SignerConfig signerConfig, + V4Signature.HashingInfo hashingInfo, + byte[] apkDigest, byte[] additionalData, long fileSize) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + CertificateEncodingException { + final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config, + hashingInfo, apkDigest, additionalData, fileSize); + + final V4Signature.SigningInfos signingInfos; + if (signerConfig.v41Config != null) { + final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock( + V3SchemeConstants.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(), - signingInfo.toByteArray()); + signingInfos.toByteArray()); } // Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type. diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java index a6cd9db..c0a9013 100644 --- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java +++ b/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.getSignedData(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; + } + } + + // 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; + } } - // Second, check if the root hash and the tree are correct. + // Check if the root hash and the tree are correct. verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree); if (!result.containsErrors()) { result.verified = true; diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java index deabe12..1eac5a2 100644 --- a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java +++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java @@ -22,6 +22,8 @@ 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; @@ -29,6 +31,8 @@ public class V4Signature { public static final int HASHING_ALGORITHM_SHA256 = 1; public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12; + 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 @@ -82,7 +86,10 @@ public class V4Signature { } static SigningInfo fromByteArray(byte[] bytes) throws IOException { - ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + 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); @@ -108,14 +115,93 @@ public class V4Signature { } } - public final int version; // Always 2 for now. + 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<SigningInfoBlock> 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; - public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later. + // 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[] signingInfo) { + 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 { @@ -131,7 +217,7 @@ public class V4Signature { public void writeTo(OutputStream stream) throws IOException { writeIntLE(stream, this.version); writeBytes(stream, this.hashingInfo); - writeBytes(stream, this.signingInfo); + writeBytes(stream, this.signingInfos); } static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) { diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java index 430a022..efadd7c 100644 --- a/src/test/java/com/android/apksig/ApkSignerTest.java +++ b/src/test/java/com/android/apksig/ApkSignerTest.java @@ -1858,7 +1858,6 @@ public class ApkSignerTest { } @Test - @Ignore("TODO(b/202011194): Re-enable once V4 supports V3.1") public void testV4_rotationMinSdkVersionT_signatureHasOrigAndRotatedKey() throws Exception { // When an APK is signed with a rotated key and the rotation-min-sdk-version X is set to T+, // a V3.1 block will be signed with the rotated signing key targeting X and later, and @@ -1881,7 +1880,7 @@ public class ApkSignerTest { .setV2SigningEnabled(true) .setV3SigningEnabled(true) .setV4SigningEnabled(true) - .setMinSdkVersionForRotation(AndroidSdkVersion.P) + .setMinSdkVersionForRotation(AndroidSdkVersion.T) .setSigningCertificateLineage(lineage)); ApkVerifier.Result result = verify(signedApk, null); |