Merge "Implement the new v4 signing scheme in apksigner" into rvc-dev
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index d2f8a1b..f4a948e 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -327,6 +327,7 @@
.setV2SigningEnabled(v2SigningEnabled)
.setV3SigningEnabled(v3SigningEnabled)
.setV4SigningEnabled(v4SigningEnabled)
+ .setV4SigningRequested(v4SigningEnabled && v4SigningFlagFound)
.setDebuggableApkPermitted(debuggableApkPermitted)
.setSigningCertificateLineage(lineage);
if (minSdkVersionSpecified) {
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 3b63744..d1e31f1 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -90,6 +90,7 @@
private final boolean mV2SigningEnabled;
private final boolean mV3SigningEnabled;
private final boolean mV4SigningEnabled;
+ private final boolean mV4SigningRequested;
private final boolean mDebuggableApkPermitted;
private final boolean mOtherSignersSignaturesPreserved;
private final String mCreatedBy;
@@ -115,6 +116,7 @@
boolean v2SigningEnabled,
boolean v3SigningEnabled,
boolean v4SigningEnabled,
+ boolean v4SigningRequested,
boolean debuggableApkPermitted,
boolean otherSignersSignaturesPreserved,
String createdBy,
@@ -134,6 +136,7 @@
mV2SigningEnabled = v2SigningEnabled;
mV3SigningEnabled = v3SigningEnabled;
mV4SigningEnabled = v4SigningEnabled;
+ mV4SigningRequested = v4SigningRequested;
mDebuggableApkPermitted = debuggableApkPermitted;
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
mCreatedBy = createdBy;
@@ -572,7 +575,7 @@
// Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
if (mV4SigningEnabled) {
- signerEngine.signV4(outputApkIn, mOutputV4File);
+ signerEngine.signV4(outputApkIn, mOutputV4File, !mV4SigningRequested);
}
}
@@ -993,6 +996,7 @@
private boolean mV2SigningEnabled = true;
private boolean mV3SigningEnabled = true;
private boolean mV4SigningEnabled = true;
+ private boolean mV4SigningRequested = false;
private boolean mDebuggableApkPermitted = true;
private boolean mOtherSignersSignaturesPreserved;
private String mCreatedBy;
@@ -1279,6 +1283,15 @@
}
/**
+ * Explicitly request V4 signing. The tool will fail if V4 signing fails.
+ */
+ public Builder setV4SigningRequested(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV4SigningRequested = enabled;
+ return this;
+ }
+
+ /**
* Sets whether the APK should be signed even if it is marked as debuggable ({@code
* android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
* compatibility reasons, the default value of this setting is {@code true}.
@@ -1391,6 +1404,7 @@
mV2SigningEnabled,
mV3SigningEnabled,
mV4SigningEnabled,
+ mV4SigningRequested,
mDebuggableApkPermitted,
mOtherSignersSignaturesPreserved,
mCreatedBy,
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 49a136b..c79f232 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -312,7 +312,8 @@
* 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 Serialized V4 Signature protobuf.
+ * @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 NoSuchAlgorithmException if a signature could not be generated because a required
@@ -320,7 +321,7 @@
* @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)
+ void signV4(DataSource data, File outputFile, boolean ignoreFailures)
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
/**
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 76a8d74..dbe3034 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -2039,7 +2039,7 @@
* </ul>
*/
V4_SIG_UNKNOWN_SIG_ALGORITHM(
- "V4 signature has unknown signing algorithm"),
+ "V4 signature has unknown signing algorithm: %1$#x"),
/**
* This APK Signature Scheme V4 signer offers no signatures.
@@ -2055,38 +2055,34 @@
"V4 signature has no supported signature"),
/**
- * PKCS7 signature block in the APK Signature Scheme V4 signature of this signer
- * could not be parsed.
- *
- * <ul>
- * <li>Parameter 1: error details ({@code Throwable})</li>
- * </ul>
- */
- V4_SIG_MALFORMED_PKCS7(
- "V4 signature has malformed pkcs7 signature block."),
-
- /**
- * APK Signature Scheme V4 signature over the signed data did not verify.
- * The signed data includes hash root and v3 digest.
+ * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
*
* <ul>
* <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
* </ul>
*/
- V4_SIG_DID_NOT_VERIFY(
- "V4 signature's pkcs7 signature does not verify"),
+ V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
/**
- * An exception was encountered while verifying APK Signature Scheme V4 signature
- * of this signer.
+ * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+ * signer.
*
* <ul>
* <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
* <li>Parameter 2: exception ({@code Throwable})</li>
* </ul>
*/
- V4_SIG_VERIFY_EXCEPTION(
- "V4 signature cannot be verified"),
+ V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be
+ * parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
/**
* This APK Signature Scheme V4 signer's certificate could not be parsed.
@@ -2117,31 +2113,7 @@
* </ul>
*/
V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
- "V4 signature has mismatched certificate and signature"),
-
- /**
- * Failed to parse this signer's digest record contained in the APK Signature Scheme
- * V4 signature.
- *
- * <ul>
- * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
- * </ul>
- */
- V4_SIG_MALFORMED_DIGEST(
- "V4 signature has malformed content digest (hash tree root)"),
-
- /**
- * This APK Signature Scheme V4 signer's signature algorithms listed in the
- * public key field of the signature proto do not match the signature algorithms listed in
- * the signature field of the proto.
- *
- * <ul>
- * <li>Parameter 1: signature algorithms from public key field ({@code List<Integer>})</li>
- * <li>Parameter 2: signature algorithms from signature field ({@code List<Integer>})</li>
- * </ul>
- */
- V4_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
- "V4 signature has mismatched signature and digest"),
+ "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"),
/**
* The APK's hash root (aka digest) does not match the hash root contained in the Signature
@@ -2170,12 +2142,6 @@
"V4 signature's hash tree did not verity"),
/**
- * No signer found in v4 signature block
- */
- V4_SIG_NO_SIGNER(
- "V4 signature has no signer"),
-
- /**
* Using more than one Signer to sign APK Signature Scheme V4 signature.
*/
V4_SIG_MULTIPLE_SIGNERS(
@@ -2192,22 +2158,6 @@
"V4 signature and V3 signature have mismatched v3 digests"),
/**
- * The hash root value stored as one of the v4 signature fields does not match with the hash
- * root value that is embedded as part of the pcks7's attached data.
- */
- V4_SIG_ROOT_HASH_MISMATCH_WITH_ATTACHED_DATA(
- "V4 signature's root hash in the signature file does not match with the "
- + "root hash embedded in the pkcs7's attached data"),
-
- /**
- * The v3 digest value stored as one of the v4 signature fields does not match with the hash
- * v3 digest value that is embedded as part of the pcks7's attached data.
- */
- V4_SIG_V3_DIGEST_MISMATCH_WITH_ATTACHED_DATA(
- "V4 signature's v3 digest in the signature file does not match with the "
- + "v3 digest embedded in the pkcs7's attached data"),
-
- /**
* The v4 signature format version isn't the same as the tool's current version, something
* may go wrong.
*/
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 0e0b554..788b78e 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -362,8 +362,7 @@
createSigningBlockSignerConfigs(
true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
if (configs.size() != 1) {
- throw new IllegalStateException(
- "Only accepting one signer config for V4 Signature Proto.");
+ throw new IllegalStateException("Only accepting one signer config for V4 Signature.");
}
return configs.get(0);
}
@@ -438,10 +437,11 @@
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
try {
newSignerConfig.signatureAlgorithms =
- Collections.singletonList(
- V4SchemeSigner.getSuggestedSignatureAlgorithm(publicKey));
+ V4SchemeSigner.getSuggestedSignatureAlgorithms(publicKey,
+ mMinSdkVersion, apkSigningBlockPaddingSupported);
} catch (InvalidKeyException e) {
- newSignerConfig.signatureAlgorithms = new ArrayList<>();
+ // V4 is an optional signing schema, ok to proceed without.
+ newSignerConfig.signatureAlgorithms = null;
}
break;
case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
@@ -896,15 +896,22 @@
}
@Override
- public void signV4(DataSource dataSource, File outputFile) {
+ public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+ throws SignatureException {
if (outputFile == null) {
- return;
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("Missing V4 output file.");
}
try {
ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
- } catch (InvalidKeyException | IOException | NoSuchAlgorithmException ignored) {
- // It is okay to fail v4 signing for now.
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("V4 signing failed", e);
}
}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 944b9b8..f7b34f0 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,8 @@
package com.android.apksig.internal.apk;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;
+
import com.android.apksig.ApkVerifier;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
@@ -204,7 +206,7 @@
centralDir,
new ByteBufferDataSource(modifiedEocd));
// Special checks for the verity algorithm requirements.
- if (actualContentDigests.containsKey(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) {
if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
throw new RuntimeException(
"APK Signing Block is not aligned on 4k boundary: " +
@@ -444,7 +446,7 @@
new DataSource[] { beforeCentralDir, centralDir, eocd },
contentDigests);
- if (digestAlgorithms.contains(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
}
return contentDigests;
@@ -765,16 +767,16 @@
byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd);
encoded.put(rootHash);
encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
- outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array());
+ outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array());
}
private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) {
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint8[32] Merkle tree root hash of SHA-256
- // * @+32 bytes int64 Length of source data (Optional)
+ // * @+32 bytes int64 (optional) Length of source data
int backBufferSize =
- ContentDigestAlgorithm.VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
+ VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
if (includeSourceDataSize) {
backBufferSize += Long.SIZE / Byte.SIZE;
}
@@ -783,18 +785,29 @@
return encoded;
}
- public static void computeChunkVerityTreeAndDigest(DataSource dataSource,
- Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> outputHashTreeAndDigests)
+ 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 VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
throws IOException, NoSuchAlgorithmException {
ByteBuffer encoded = createVerityDigestBuffer(false);
// Use 0s as salt for now. This also needs to be consistent in the fsverify header for
// kernel to use.
VerityTreeBuilder builder = new VerityTreeBuilder(null);
- ByteBuffer tree = builder.generateVerityTree(dataSource, false /* padding */);
+ ByteBuffer tree = builder.generateVerityTree(dataSource);
byte[] rootHash = builder.getRootHashFromTree(tree);
encoded.put(rootHash);
- outputHashTreeAndDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
- Pair.of(tree.array(), encoded.array()));
+ return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array());
}
private static long getChunkCount(long inputSize, long chunkSize) {
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 866402f..2475fae 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
@@ -18,11 +18,6 @@
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
-import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
-import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
-import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
-import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
-import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
@@ -31,81 +26,66 @@
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.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
-import com.android.apksig.internal.asn1.Asn1EncodingException;
-import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
-import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
-import java.security.interfaces.ECKey;
-import java.security.interfaces.RSAKey;
-import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.Set;
/**
- * APK Signature Scheme V4 signer.
- * V4 scheme file contains 3 mandatory fields - used during installation.
- * And optional verity tree - has to be present during session commit.
- *
+ * APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
+ * installation. And optional verity tree - has to be present during session commit.
+ * <p>
* The fields:
* <p>
- * 1. verityRootHash: bytes of the hash tree root (digest of first 1-page of the tree),
- * 2. V3Digest: digest from v2/v3 signing schema,
- * 3. pkcs7SignatureBlock: bytes of the signature over encoded signed data, which includes 1 and 2,
+ * 1. hashingInfo - verity root hash and hashing info,
+ * 2. signingInfo - certificate, public key and signature,
+ * For more details see V4Signature.
* </p>
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
- *
- * TODO(schfan): Pass v3 digest to v4 signature proto and add verification code
- * TODO(schfan): Add v4 unit tests
+ * <p>
+ * TODO(schfan): Pass v3 digest to v4 signature proto and add verification code TODO(schfan): Add v4
+ * unit tests
*/
public abstract class V4SchemeSigner {
- /** Hidden constructor to prevent instantiation. */
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
private V4SchemeSigner() {
}
/**
* Based on a public key, return a signing algorithm that supports verity.
*/
- public static SignatureAlgorithm getSuggestedSignatureAlgorithm(PublicKey signingKey)
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean apkSigningBlockPaddingSupported)
throws InvalidKeyException {
- final String keyAlgorithm = signingKey.getAlgorithm();
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- final int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
- if (modulusLengthBits <= 3072) {
- return SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
- } else {
- // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
- // digest being the weak link. SHA-512 is the next strongest supported digest.
- throw new InvalidKeyException(
- "Key requires SHA-512 signature algorithm, not yet supported with verity");
+ List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ signingKey, minSdkVersion,
+ apkSigningBlockPaddingSupported);
+ // Keeping only supported algorithms.
+ for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
+ final SignatureAlgorithm algorithm = iter.next();
+ if (!isSupported(algorithm.getContentDigestAlgorithm())) {
+ iter.remove();
}
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- return SignatureAlgorithm.VERITY_DSA_WITH_SHA256;
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- final int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
- if (keySizeBits <= 256) {
- return SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256;
- } else {
- throw new InvalidKeyException(
- "Key requires SHA-512 signature algorithm, not yet supported with verity");
- }
- } else {
- throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
+ return algorithms;
}
/**
@@ -116,9 +96,14 @@
SignerConfig signerConfig,
File outputFile)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
- Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> verityDigest = new HashMap<>();
- ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent, verityDigest);
+ // Salt has to stay empty for fs-verity compatibility.
+ final byte[] salt = null;
+ // Not used by apksigner.
+ final byte[] additionalData = null;
+ final long fileSize = apkContent.size();
+
+ // Obtaining first supported digest from v3 block (SHA256 or SHA512).
byte[] v3digest;
try {
v3digest = getV3Digest(apkContent);
@@ -127,79 +112,83 @@
throw new IOException("Failed to parse V3-signed apk to read its V3 digest");
}
- final Pair<V4Signature, byte[]> signaturePair;
+ // Obtaining the merkle tree and the root hash in verity format.
+ ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
+ ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+ final ContentDigestAlgorithm verityContentDigestAlgorithm =
+ verityContentDigestInfo.contentDigestAlgorithm;
+ final byte[] rootHash = verityContentDigestInfo.rootHash;
+ final byte[] tree = verityContentDigestInfo.tree;
+
+ final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
+ verityContentDigestAlgorithm);
+ final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
+ hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
+ salt, rootHash);
+
+ // Generating SigningInfo and combining everything into V4Signature.
+ final V4Signature signature;
try {
- signaturePair = generateSignatureObject(signerConfig, verityDigest, v3digest);
- } catch (InvalidKeyException | SignatureException |
- CertificateEncodingException | Asn1EncodingException e) {
+ signature = generateSignature(signerConfig, hashingInfo, v3digest, additionalData,
+ fileSize);
+ } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
throw new InvalidKeyException("Signer failed", e);
}
- V4Signature signature = signaturePair.getFirst();
- byte[] tree = signaturePair.getSecond();
-
- try (final DataOutputStream output = new DataOutputStream(
- new FileOutputStream(outputFile))) {
+ try (final OutputStream output = new FileOutputStream(outputFile)) {
signature.writeTo(output);
- if (tree != null && tree.length != 0) {
- V4Signature.writeBytes(output, tree);
- }
+ V4Signature.writeBytes(output, tree);
+ } catch (IOException e) {
+ outputFile.delete();
+ throw e;
}
}
- private static Pair<V4Signature, byte[]> generateSignatureObject(
+ private static V4Signature generateSignature(
SignerConfig signerConfig,
- Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> contentDigests,
- byte[] v3Digest)
+ V4Signature.HashingInfo hashingInfo,
+ byte[] v3Digest, byte[] additionaData, long fileSize)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
- CertificateEncodingException, Asn1EncodingException {
+ CertificateEncodingException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
- final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
- final List<byte[]> certificates = encodeCertificates(signerConfig.certificates);
- if (certificates.size() != 1) {
+ if (signerConfig.certificates.size() != 1) {
throw new CertificateEncodingException("Should only have one certificate");
}
- if (signerConfig.signatureAlgorithms.size() != 1) {
- throw new SignatureException("Should only be one signature algorithm");
- }
+ // Collecting data for signing.
+ final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
- final SignatureAlgorithm signatureAlgorithm = signerConfig.signatureAlgorithms.get(0);
- final ContentDigestAlgorithm contentDigestAlgorithm =
- signatureAlgorithm.getContentDigestAlgorithm();
- final Pair<byte[], byte[]> contentDigest = contentDigests.get(contentDigestAlgorithm);
- if (contentDigest == null) {
- throw new SignatureException("Cannot find computed digest");
- }
- byte[] tree = contentDigest.getFirst();
- byte[] rootHash = contentDigest.getSecond();
+ final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
+ final byte[] encodedCertificate = encodedCertificates.get(0);
- byte[] data = ByteBuffer.allocate(rootHash.length + v3Digest.length)
- .put(rootHash).put(v3Digest).array();
+ final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(v3Digest,
+ encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
+ final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
+ signingInfoNoSignature);
+
+ // Signing.
final List<Pair<Integer, byte[]>> signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
if (signatures.size() != 1) {
throw new SignatureException("Should only be one signature generated");
}
- byte[] pkcs7SignatureBlock = ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
- signatures.get(0).getSecond(), /* signature bytes */
- ByteBuffer.wrap(data),
- signerConfig.certificates,
- new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL), /* digest algo id */
- getSignatureAlgorithmIdentifier(publicKey));
+ final int signatureAlgorithmId = signatures.get(0).getFirst();
+ final byte[] signature = signatures.get(0).getSecond();
- final V4Signature signature =
- new V4Signature(V4Signature.CURRENT_VERSION,
- rootHash, v3Digest, pkcs7SignatureBlock);
- return Pair.of(signature, tree);
+ final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(v3Digest,
+ encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
+ signature);
+
+ return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
+ signingInfo.toByteArray());
}
- // Get V3 digest by parsing the V3-signed apk
+ // Get V3 digest by parsing the V3-signed apk and choosing the first digest of supported type.
private static byte[] getV3Digest(DataSource apk) throws ApkFormatException, IOException,
ApkSigningBlockUtils.SignatureNotFoundException, NoSuchAlgorithmException,
SignatureException {
@@ -227,40 +216,49 @@
if (contentDigests.isEmpty()) {
throw new SignatureException("Should have at least one digest");
}
+
+ int bestAlgorithm = -1;
+ byte[] bestDigest = null;
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
final SignatureAlgorithm signatureAlgorithm =
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
- if (signatureAlgorithm == null) {
- continue;
- }
final ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
- if (contentDigestAlgorithm == null) {
+ if (!isSupported(contentDigestAlgorithm)) {
continue;
}
- if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
- || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
- return contentDigest.getValue();
+ // Prefer SHA512 over SHA256, if both are available.
+ if (bestAlgorithm < contentDigestAlgorithm.getId()) {
+ bestAlgorithm = contentDigestAlgorithm.getId();
+ bestDigest = contentDigest.getValue();
}
}
- throw new SignatureException("Failed to find any V3 digest in the source APK");
+ if (bestDigest == null) {
+ throw new SignatureException("Failed to find a supported V3 digest in the source APK");
+ }
+ return bestDigest;
}
- /**
- * Returns the JCA SHA256 {@code AlgorithmIdentifier} and PKCS #7 {@code SignatureAlgorithm} to
- * use when signing with the specified key.
- */
- private static AlgorithmIdentifier getSignatureAlgorithmIdentifier(
- PublicKey publicKey) throws InvalidKeyException {
- String keyAlgorithm = publicKey.getAlgorithm();
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- return new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL);
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- return new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- return new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL);
- } else {
- throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm) {
+ if (contentDigestAlgorithm == null) {
+ return false;
+ }
+ if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+ || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
+ return true;
+ }
+ return false;
+ }
+
+ private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
+ throws NoSuchAlgorithmException {
+ switch (algorithm) {
+ case VERITY_CHUNKED_SHA256:
+ return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
+ V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
+ default:
+ throw new NoSuchAlgorithmException(
+ "Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
}
}
}
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 96294da..18f9202 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
@@ -17,73 +17,61 @@
package com.android.apksig.internal.apk.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
-import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm;
-import static com.android.apksig.internal.x509.Certificate.findCertificate;
-import static com.android.apksig.internal.x509.Certificate.parseCertificates;
import com.android.apksig.ApkVerifier;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.asn1.Asn1BerParser;
-import com.android.apksig.internal.asn1.Asn1DecodingException;
-import com.android.apksig.internal.pkcs7.ContentInfo;
-import com.android.apksig.internal.pkcs7.Pkcs7Constants;
-import com.android.apksig.internal.pkcs7.SignedData;
-import com.android.apksig.internal.pkcs7.SignerInfo;
-import com.android.apksig.internal.util.ByteBufferUtils;
-import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
-import java.io.DataInputStream;
-import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
+import java.security.KeyFactory;
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.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
/**
* APK Signature Scheme V4 verifier.
- *
+ * <p>
* Verifies the serialized V4Signature file against an APK.
*/
public abstract class V4SchemeVerifier {
- /** Hidden constructor to prevent instantiation. */
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
private V4SchemeVerifier() {
}
/**
* <p>
- * The main goals of the verifier are:
- * 1) parse V4Signature file fields
- * 2) verifies the PKCS7 signature block against the raw root hash bytes in the proto field
- * 3) verifies that the raw root hash matches with the actual hash tree root of the give APK
- * 4) if the file contains a verity tree, verifies that it matches with the actual verity
- * tree computed from the given APK.
+ * The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
+ * signature block against the raw root hash bytes in the proto field 3) verifies that the raw
+ * root hash matches with the actual hash tree root of the give APK 4) if the file contains a
+ * verity tree, verifies that it matches with the actual verity tree computed from the given
+ * APK.
* </p>
*/
public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
throws IOException, NoSuchAlgorithmException {
-
- V4Signature signature = null;
- byte[] tree = null;
- try (final DataInputStream input = new DataInputStream(
- new FileInputStream(v4SignatureFile))) {
+ final V4Signature signature;
+ final byte[] tree;
+ try (InputStream input = new FileInputStream(v4SignatureFile)) {
signature = V4Signature.readFrom(input);
tree = V4Signature.readBytes(input);
- } catch (EOFException e) {
}
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
@@ -100,203 +88,163 @@
V4Signature.CURRENT_VERSION);
}
- final byte[] pkcs7Signature = signature.pkcs7SignatureBlock;
- final ByteBuffer pkcs7SignatureBlock =
- ByteBuffer.wrap(pkcs7Signature).order(ByteOrder.LITTLE_ENDIAN);
- final ByteBuffer verityRootHash = ByteBuffer.wrap(signature.verityRootHash);
- final ByteBuffer v3Digest = ByteBuffer.wrap(signature.v3Digest);
+ V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
+ signature.hashingInfo);
+ V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+ signature.signingInfo);
- result.signers.add(parseAndVerifySignatureBlock(
- pkcs7SignatureBlock, verityRootHash, v3Digest));
+ final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
+
+ // First, verify the signature over signedData.
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
+ signingInfo, signedData);
+ result.signers.add(signerInfo);
if (result.containsErrors()) {
return result;
}
- verifyRootHashAndTree(apk, result, signature.verityRootHash, tree);
+ // Second, check if the root hash and the tree are correct.
+ verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
if (!result.containsErrors()) {
result.verified = true;
}
- // Add v3Content digest from the file to the result
- result.signers.get(0).contentDigests.add(
- new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
- 0 /* signature algorithm id doesn't matter here */,
- signature.v3Digest));
+
return result;
}
/**
- * Parses the provided pkcs7 signature block and populates the {@code result}.
- *
- * This verifies {@pkcs7SignatureBlock} over {@code verityRootHash}, as well as
- * parsing the certificate contained in the signature block.
- * This method adds one or more errors to the {@code result}.
+ * Parses the provided signature block and populates the {@code result}.
+ * <p>
+ * This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
+ * contained in the signature block. This method adds one or more errors to the {@code result}.
*/
private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
- ByteBuffer pkcs7SignatureBlock,
- ByteBuffer verityRootHash, ByteBuffer v3Digest) {
+ V4Signature.SigningInfo signingInfo,
+ final byte[] signedData) throws NoSuchAlgorithmException {
final ApkSigningBlockUtils.Result.SignerInfo result =
new ApkSigningBlockUtils.Result.SignerInfo();
- SignedData signedData;
+ result.index = 0;
+
+ final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
+ final byte[] sigBytes = signingInfo.signature;
+ result.signatures.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
+
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ return result;
+ }
+
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+
+ String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+
+ final byte[] publicKeyBytes = signingInfo.publicKey;
+ PublicKey publicKey;
try {
- final ContentInfo contentInfo =
- Asn1BerParser.parse(pkcs7SignatureBlock, ContentInfo.class);
- if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) {
- result.addError(Issue.V4_SIG_MALFORMED_PKCS7,
- "Unsupported ContentInfo.contentType: "
- + contentInfo.contentType);
+ publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
+ new X509EncodedKeySpec(publicKeyBytes));
+ } catch (Exception e) {
+ result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
+ return result;
+ }
+
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
}
- signedData = Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class);
- } catch (Asn1DecodingException e) {
- result.addError(Issue.V4_SIG_MALFORMED_PKCS7, e);
+ sig.update(signedData);
+ if (!sig.verify(sigBytes)) {
+ result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+ return result;
+ }
+ result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | SignatureException e) {
+ result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return result;
}
- if (signedData.signerInfos.isEmpty()) {
- result.addError(Issue.V4_SIG_NO_SIGNER);
+ if (signingInfo.certificate == null) {
+ result.addError(Issue.V4_SIG_NO_CERTIFICATE);
return result;
}
- if (signedData.signerInfos.size() != 1) {
- result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
- return result;
- }
-
- ByteBuffer attachedData = signedData.encapContentInfo.content;
- byte[] rootHashInAttachedData = new byte[verityRootHash.array().length];
- attachedData.get(rootHashInAttachedData);
- // Embedded root hash should be equal to the external one
- if (!Arrays.equals(rootHashInAttachedData, verityRootHash.array())) {
- result.addError(Issue.V4_SIG_ROOT_HASH_MISMATCH_WITH_ATTACHED_DATA);
- return result;
- }
- byte[] v3digestInAttachedData = new byte[v3Digest.array().length];
- attachedData.get(v3digestInAttachedData);
- // Embedded root hash should be equal to the external one
- if (!Arrays.equals(v3digestInAttachedData, v3Digest.array())) {
- result.addError(Issue.V4_SIG_V3_DIGEST_MISMATCH_WITH_ATTACHED_DATA);
- return result;
- }
- attachedData.flip();
-
- SignerInfo unverifiedSignerInfo = signedData.signerInfos.get(0);
- List<X509Certificate> signedDataCertificates;
+ final X509Certificate certificate;
try {
- signedDataCertificates = parseCertificates(signedData.certificates);
+ // 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(
+ X509CertificateUtils.generateCertificate(signingInfo.certificate),
+ signingInfo.certificate);
} catch (CertificateException e) {
result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
return result;
}
+ result.certs.add(certificate);
- // Verify SignerInfo
- verifySignerInfo(signedDataCertificates, unverifiedSignerInfo, attachedData, result);
+ byte[] certificatePublicKeyBytes;
+ try {
+ certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+ certificate.getPublicKey());
+ } catch (InvalidKeyException e) {
+ System.out.println("Caught an exception encoding the public key: " + e);
+ e.printStackTrace();
+ certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
+ }
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ result.addError(
+ Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+ ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+ ApkSigningBlockUtils.toHex(publicKeyBytes));
+ return result;
+ }
+
+ // Add v3Content digest from the file to the result.
+ ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
+ new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+ 0 /* signature algorithm id doesn't matter here */, signingInfo.v3Digest);
+ result.contentDigests.add(contentDigest);
+
return result;
}
- private static void verifySignerInfo(
- List<X509Certificate> signedDataCertificates, SignerInfo signerInfo,
- ByteBuffer data, ApkSigningBlockUtils.Result.SignerInfo result) {
- final String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm;
- final String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm;
- final X509Certificate signingCertificate =
- findCertificate(signedDataCertificates, signerInfo.sid);
- result.certs.clear();
- result.certs.add(signingCertificate);
- if (signingCertificate == null) {
- result.addError(Issue.V4_SIG_NO_CERTIFICATE);
- return;
- }
- // Check whether the signing certificate is acceptable. Android performs these
- // checks explicitly, instead of delegating this to
- // Signature.initVerify(Certificate).
- if (signingCertificate.hasUnsupportedCriticalExtension()) {
- result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE,
- "Signing certificate has unsupported critical extensions");
- return;
- }
- final boolean[] keyUsageExtension = signingCertificate.getKeyUsage();
- if (keyUsageExtension != null) {
- boolean digitalSignature = (keyUsageExtension.length >= 1) && keyUsageExtension[0];
- boolean nonRepudiation = (keyUsageExtension.length >= 2) && keyUsageExtension[1];
- if (!digitalSignature && !nonRepudiation) {
- result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE,
- "Signing certificate not authorized for use in digital signatures"
- + ": keyUsage extension missing digitalSignature and"
- + " nonRepudiation");
- return;
- }
- }
-
- Signature s;
- try {
- final String jcaSignatureAlgorithm = getJcaSignatureAlgorithm(
- digestAlgorithmOid, signatureAlgorithmOid);
- s = Signature.getInstance(jcaSignatureAlgorithm);
- } catch (SignatureException | NoSuchAlgorithmException e) {
- result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM);
- return;
- }
- if (signerInfo.signedAttrs != null) {
- result.addError(Issue.V4_SIG_MALFORMED_SIGNERS, "Should not contain signed attributes");
- }
- try {
- s.initVerify(signingCertificate.getPublicKey());
- s.update(data);
- final byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice());
- if (!s.verify(sigBytes)) {
- result.addError(Issue.V4_SIG_DID_NOT_VERIFY);
- }
- } catch (InvalidKeyException | SignatureException e) {
- result.addError(Issue.V4_SIG_VERIFY_EXCEPTION);
- }
- }
-
private static void verifyRootHashAndTree(DataSource apkContent,
- ApkSigningBlockUtils.Result result, byte[] rootHashInResult, byte[] treeInResult)
- throws IOException, NoSuchAlgorithmException {
- final Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> actualContentDigests =
- new HashMap<>();
- ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent, actualContentDigests);
- if (result.signers.size() != 1) {
- throw new IllegalStateException("There should only be one signer for V4");
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
+ byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
+ ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
+ ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+ ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
+ final byte[] actualDigest = actualContentDigestInfo.rootHash;
+ final byte[] actualTree = actualContentDigestInfo.tree;
+
+ if (!Arrays.equals(expectedDigest, actualDigest)) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
+ algorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ return;
}
- final ApkSigningBlockUtils.Result.SignerInfo signerInfo = result.signers.get(0);
- for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest expected
- : signerInfo.contentDigests) {
- final SignatureAlgorithm signatureAlgorithm =
- SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
- if (signatureAlgorithm == null) {
- continue;
- }
- final ContentDigestAlgorithm contentDigestAlgorithm =
- signatureAlgorithm.getContentDigestAlgorithm();
- // should not happen
- if (contentDigestAlgorithm != ContentDigestAlgorithm.VERITY_CHUNKED_SHA256) {
- continue;
- }
- final byte[] expectedDigest = expected.getValue();
- final byte[] actualDigest =
- actualContentDigests.get(contentDigestAlgorithm).getSecond();
- final byte[] actualTree =
- actualContentDigests.get(contentDigestAlgorithm).getFirst();
- if (!Arrays.equals(expectedDigest, actualDigest)
- || !Arrays.equals(expectedDigest, rootHashInResult)) {
- signerInfo.addError(
- ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
- contentDigestAlgorithm,
- toHex(expectedDigest),
- toHex(actualDigest));
- continue;
- }
- // Only check verity tree if it is not empty
- if (treeInResult != null && !Arrays.equals(treeInResult, actualTree)) {
- signerInfo.addError(
- ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
- contentDigestAlgorithm,
- toHex(expectedDigest),
- toHex(actualDigest));
- continue;
- }
- signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
+ // Only check verity tree if it is not empty
+ if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
+ algorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ return;
}
+
+ signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
}
}
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 2963cd1..0aefade 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
@@ -16,48 +16,210 @@
package com.android.apksig.internal.apk.v4;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
+import java.io.EOFException;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
public class V4Signature {
- public static final int CURRENT_VERSION = 1;
+ public static final int CURRENT_VERSION = 2;
- public final int version;
- public final byte[] verityRootHash;
- public final byte[] v3Digest;
- public final byte[] pkcs7SignatureBlock;
+ public static final int HASHING_ALGORITHM_SHA256 = 1;
+ public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
- V4Signature(int version, byte[] verityRootHash, byte[] v3Digest, byte[] pkcs7SignatureBlock) {
+ 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[] v3Digest; // 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[] v3Digest, byte[] certificate, byte[] additionalData,
+ byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
+ this.v3Digest = v3Digest;
+ 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[] v3Digest = 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(v3Digest, certificate, additionalData, publicKey,
+ signatureAlgorithmId, signature);
+ }
+
+ byte[] toByteArray() {
+ final int size = bytesSize(this.v3Digest) + 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.v3Digest);
+ writeBytes(buffer, this.certificate);
+ writeBytes(buffer, this.additionalData);
+ writeBytes(buffer, this.publicKey);
+ buffer.putInt(this.signatureAlgorithmId);
+ writeBytes(buffer, this.signature);
+ return buffer.array();
+ }
+ }
+
+ 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) {
this.version = version;
- this.verityRootHash = verityRootHash;
- this.v3Digest = v3Digest;
- this.pkcs7SignatureBlock = pkcs7SignatureBlock;
+ this.hashingInfo = hashingInfo;
+ this.signingInfo = signingInfo;
}
- static byte[] readBytes(DataInputStream stream) throws IOException {
- byte[] result = new byte[stream.readInt()];
- stream.read(result);
- return result;
+ static V4Signature readFrom(InputStream stream) throws IOException {
+ final int version = readIntLE(stream);
+ if (version != CURRENT_VERSION) {
+ throw new IOException("Invalid signature version.");
+ }
+ final byte[] hashingInfo = readBytes(stream);
+ final byte[] signingInfo = readBytes(stream);
+ return new V4Signature(version, hashingInfo, signingInfo);
}
- static V4Signature readFrom(DataInputStream stream) throws IOException {
- final int version = stream.readInt();
- byte[] verityRootHash = readBytes(stream);
- byte[] v3Digest = readBytes(stream);
- byte[] pkcs7SignatureBlock = readBytes(stream);
- return new V4Signature(version, verityRootHash, v3Digest, pkcs7SignatureBlock);
+ void writeTo(OutputStream stream) throws IOException {
+ writeIntLE(stream, this.version);
+ writeBytes(stream, this.hashingInfo);
+ writeBytes(stream, this.signingInfo);
}
- static void writeBytes(DataOutputStream stream, byte[] bytes) throws IOException {
- stream.writeInt(bytes.length);
+ static byte[] getSigningData(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(
+ signingInfo.v3Digest) + bytesSize(signingInfo.certificate) + bytesSize(
+ signingInfo.additionalData);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(size);
+ buffer.putLong(fileSize);
+ buffer.putInt(hashingInfo.hashAlgorithm);
+ buffer.put(hashingInfo.log2BlockSize);
+ writeBytes(buffer, hashingInfo.salt);
+ writeBytes(buffer, hashingInfo.rawRootHash);
+ writeBytes(buffer, signingInfo.v3Digest);
+ writeBytes(buffer, signingInfo.certificate);
+ writeBytes(buffer, signingInfo.additionalData);
+ return buffer.array();
+ }
+
+ // Utility methods.
+ static int bytesSize(byte[] bytes) {
+ return 4/*length*/ + (bytes == null ? 0 : bytes.length);
+ }
+
+ static void readFully(InputStream stream, byte[] buffer) throws IOException {
+ int len = buffer.length;
+ int n = 0;
+ while (n < len) {
+ int count = stream.read(buffer, n, len - n);
+ if (count < 0) {
+ throw new EOFException();
+ }
+ n += count;
+ }
+ }
+
+ static int readIntLE(InputStream stream) throws IOException {
+ final byte[] buffer = new byte[4];
+ readFully(stream, buffer);
+ return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ }
+
+ static void writeIntLE(OutputStream stream, int v) throws IOException {
+ final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
+ stream.write(buffer);
+ }
+
+ static byte[] readBytes(InputStream stream) throws IOException {
+ try {
+ final int size = readIntLE(stream);
+ final byte[] bytes = new byte[size];
+ readFully(stream, bytes);
+ return bytes;
+ } catch (EOFException ignored) {
+ return null;
+ }
+ }
+
+ static byte[] readBytes(ByteBuffer buffer) throws IOException {
+ if (buffer.remaining() < 4) {
+ throw new EOFException();
+ }
+ final int size = buffer.getInt();
+ if (buffer.remaining() < size) {
+ throw new EOFException();
+ }
+ final byte[] bytes = new byte[size];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
+ if (bytes == null) {
+ writeIntLE(stream, 0);
+ return;
+ }
+ writeIntLE(stream, bytes.length);
stream.write(bytes);
}
- void writeTo(DataOutputStream stream) throws IOException {
- stream.writeInt(this.version);
- writeBytes(stream, this.verityRootHash);
- writeBytes(stream, this.v3Digest);
- writeBytes(stream, this.pkcs7SignatureBlock);
+ static void writeBytes(ByteBuffer buffer, byte[] bytes) {
+ if (bytes == null) {
+ buffer.putInt(0);
+ return;
+ }
+ buffer.putInt(bytes.length);
+ buffer.put(bytes);
}
}
diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
index 3133606..dc048f0 100644
--- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
+++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -82,9 +82,8 @@
/**
* Returns the root hash of the verity tree built from the data source.
*/
- public byte[] generateVerityTreeRootHash(DataSource fileSource)
- throws IOException {
- ByteBuffer verityBuffer = generateVerityTree(fileSource, true /* addPadding */);
+ public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+ ByteBuffer verityBuffer = generateVerityTree(fileSource);
return getRootHashFromTree(verityBuffer);
}
@@ -101,8 +100,7 @@
* 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.
*/
- public ByteBuffer generateVerityTree(DataSource fileSource, boolean addPadding)
- throws IOException {
+ public ByteBuffer generateVerityTree(DataSource fileSource) throws IOException {
int digestSize = mMd.getDigestLength();
// Calculate the summed area table of level size. In other word, this is the offset
@@ -118,11 +116,11 @@
DataSource src;
if (i == levelOffset.length - 2) {
src = fileSource;
- digestDataByChunks(src, middleBufferSink, addPadding);
+ digestDataByChunks(src, middleBufferSink);
} else {
src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
levelOffset[i + 1], levelOffset[i + 2]));
- digestDataByChunks(src, middleBufferSink, addPadding);
+ digestDataByChunks(src, middleBufferSink);
}
// If the output is not full chunk, pad with 0s.
@@ -179,8 +177,7 @@
* less than the chunk size and padding is desired, feed with extra padding 0 to fill up the
* chunk before digesting.
*/
- private void digestDataByChunks(DataSource dataSource, DataSink dataSink, boolean addPadding)
- throws IOException {
+ private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException {
long size = dataSource.size();
long offset = 0;
for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) {
@@ -198,9 +195,6 @@
buffer = ByteBuffer.allocate(CHUNK_SIZE); // initialized to 0.
dataSource.copyTo(offset, remaining, buffer);
buffer.rewind();
- if (!addPadding) {
- buffer.limit(remaining);
- }
byte[] hash = saltedDigest(buffer);
dataSink.consume(hash, 0, hash.length);
}