forked from MarcoBuster/Magisk
773 lines
34 KiB
Java
773 lines
34 KiB
Java
package com.topjohnwu.signing;
|
|
|
|
import java.nio.BufferUnderflowException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.security.DigestException;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.KeyFactory;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.PrivateKey;
|
|
import java.security.PublicKey;
|
|
import java.security.Signature;
|
|
import java.security.SignatureException;
|
|
import java.security.cert.CertificateEncodingException;
|
|
import java.security.cert.X509Certificate;
|
|
import java.security.spec.AlgorithmParameterSpec;
|
|
import java.security.spec.InvalidKeySpecException;
|
|
import java.security.spec.MGF1ParameterSpec;
|
|
import java.security.spec.PSSParameterSpec;
|
|
import java.security.spec.X509EncodedKeySpec;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* APK Signature Scheme v2 signer.
|
|
*
|
|
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
|
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
|
* uncompressed contents of ZIP entries.
|
|
*/
|
|
public abstract class ApkSignerV2 {
|
|
/*
|
|
* The two main goals of APK Signature Scheme v2 are:
|
|
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
|
|
* cover every byte of the APK being signed.
|
|
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
|
|
* only a minimal amount of APK parsing before the signature is verified, thus completely
|
|
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
|
|
* employing a hash tree.
|
|
*
|
|
* The generated signature block is wrapped into an APK Signing Block and inserted into the
|
|
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
|
|
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
|
|
* extensibility. For example, a future signature scheme could insert its signatures there as
|
|
* well. The contract of the APK Signing Block is that all contents outside of the block must be
|
|
* protected by signatures inside the block.
|
|
*/
|
|
|
|
public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
|
|
public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
|
|
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
|
|
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
|
|
public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
|
|
public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
|
|
public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
|
|
public static final int SIGNATURE_DSA_WITH_SHA512 = 0x0302;
|
|
|
|
/**
|
|
* {@code .SF} file header section attribute indicating that the APK is signed not just with
|
|
* JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
|
|
* facilitates v2 signature stripping detection.
|
|
*
|
|
* <p>The attribute contains a comma-separated set of signature scheme IDs.
|
|
*/
|
|
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
|
|
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2";
|
|
|
|
private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
|
|
private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
|
|
|
|
private static final int 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 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
|
|
|
private ApkSignerV2() {}
|
|
|
|
/**
|
|
* Signer configuration.
|
|
*/
|
|
public static final class SignerConfig {
|
|
/** Private key. */
|
|
public PrivateKey privateKey;
|
|
|
|
/**
|
|
* Certificates, with the first certificate containing the public key corresponding to
|
|
* {@link #privateKey}.
|
|
*/
|
|
public List<X509Certificate> certificates;
|
|
|
|
/**
|
|
* List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
|
|
*/
|
|
public List<Integer> signatureAlgorithms;
|
|
}
|
|
|
|
/**
|
|
* Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
|
|
* consecutive chunks.
|
|
*
|
|
* <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
|
|
* of META-INF/*.SF files of APK being signed must contain the
|
|
* {@code X-Android-APK-Signed: true} attribute.
|
|
*
|
|
* @param inputApk contents of the APK to be signed. The APK starts at the current position
|
|
* of the buffer and ends at the limit of the buffer.
|
|
* @param signerConfigs signer configurations, one for each signer.
|
|
*
|
|
* @throws ApkParseException if the APK cannot be parsed.
|
|
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
|
* cannot be used in general.
|
|
* @throws SignatureException if an error occurs when computing digests of generating
|
|
* signatures.
|
|
*/
|
|
public static ByteBuffer[] sign(
|
|
ByteBuffer inputApk,
|
|
List<SignerConfig> signerConfigs)
|
|
throws ApkParseException, InvalidKeyException, SignatureException {
|
|
// Slice/create a view in the inputApk to make sure that:
|
|
// 1. inputApk is what's between position and limit of the original inputApk, and
|
|
// 2. changes to position, limit, and byte order are not reflected in the original.
|
|
ByteBuffer originalInputApk = inputApk;
|
|
inputApk = originalInputApk.slice();
|
|
inputApk.order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
// Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
|
|
// Directory is immediately followed by the ZIP End of Central Directory.
|
|
int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
|
|
if (eocdOffset == -1) {
|
|
throw new ApkParseException("Failed to locate ZIP End of Central Directory");
|
|
}
|
|
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {
|
|
throw new ApkParseException("ZIP64 format not supported");
|
|
}
|
|
inputApk.position(eocdOffset);
|
|
long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);
|
|
if (centralDirSizeLong > Integer.MAX_VALUE) {
|
|
throw new ApkParseException(
|
|
"ZIP Central Directory size out of range: " + centralDirSizeLong);
|
|
}
|
|
int centralDirSize = (int) centralDirSizeLong;
|
|
long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);
|
|
if (centralDirOffsetLong > Integer.MAX_VALUE) {
|
|
throw new ApkParseException(
|
|
"ZIP Central Directory offset in file out of range: " + centralDirOffsetLong);
|
|
}
|
|
int centralDirOffset = (int) centralDirOffsetLong;
|
|
int expectedEocdOffset = centralDirOffset + centralDirSize;
|
|
if (expectedEocdOffset < centralDirOffset) {
|
|
throw new ApkParseException(
|
|
"ZIP Central Directory extent too large. Offset: " + centralDirOffset
|
|
+ ", size: " + centralDirSize);
|
|
}
|
|
if (eocdOffset != expectedEocdOffset) {
|
|
throw new ApkParseException(
|
|
"ZIP Central Directory not immeiately followed by ZIP End of"
|
|
+ " Central Directory. CD end: " + expectedEocdOffset
|
|
+ ", EoCD start: " + eocdOffset);
|
|
}
|
|
|
|
// Create ByteBuffers holding the contents of everything before ZIP Central Directory,
|
|
// ZIP Central Directory, and ZIP End of Central Directory.
|
|
inputApk.clear();
|
|
ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
|
|
ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
|
|
// Create a copy of End of Central Directory because we'll need modify its contents later.
|
|
byte[] eocdBytes = new byte[inputApk.remaining()];
|
|
inputApk.get(eocdBytes);
|
|
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
|
|
eocd.order(inputApk.order());
|
|
|
|
// Figure which which digests to use for APK contents.
|
|
Set<Integer> contentDigestAlgorithms = new HashSet<>();
|
|
for (SignerConfig signerConfig : signerConfigs) {
|
|
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
|
contentDigestAlgorithms.add(
|
|
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
|
|
}
|
|
}
|
|
|
|
// Compute digests of APK contents.
|
|
Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
|
|
try {
|
|
contentDigests =
|
|
computeContentDigests(
|
|
contentDigestAlgorithms,
|
|
new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
|
|
} catch (DigestException e) {
|
|
throw new SignatureException("Failed to compute digests of APK", e);
|
|
}
|
|
|
|
// Sign the digests and wrap the signatures and signer info into an APK Signing Block.
|
|
ByteBuffer apkSigningBlock =
|
|
ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
|
|
|
|
// Update Central Directory Offset in End of Central Directory Record. Central Directory
|
|
// follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
|
|
centralDirOffset += apkSigningBlock.remaining();
|
|
eocd.clear();
|
|
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
|
|
|
|
// Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
|
|
originalInputApk.position(originalInputApk.limit());
|
|
|
|
// Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
|
|
// Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
|
|
// Contrary to the name, this does not clear the contents of these ByteBuffer.
|
|
beforeCentralDir.clear();
|
|
centralDir.clear();
|
|
eocd.clear();
|
|
|
|
// Insert APK Signing Block immediately before the ZIP Central Directory.
|
|
return new ByteBuffer[] {
|
|
beforeCentralDir,
|
|
apkSigningBlock,
|
|
centralDir,
|
|
eocd,
|
|
};
|
|
}
|
|
|
|
private static Map<Integer, byte[]> computeContentDigests(
|
|
Set<Integer> digestAlgorithms,
|
|
ByteBuffer[] contents) throws DigestException {
|
|
// For each digest algorithm the result is computed as follows:
|
|
// 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
|
|
// The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
|
|
// No chunks are produced for empty (zero length) segments.
|
|
// 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
|
|
// length in bytes (uint32 little-endian) and the chunk's contents.
|
|
// 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
|
|
// chunks (uint32 little-endian) and the concatenation of digests of chunks of all
|
|
// segments in-order.
|
|
|
|
int chunkCount = 0;
|
|
for (ByteBuffer input : contents) {
|
|
chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
|
}
|
|
|
|
final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
|
|
for (int digestAlgorithm : digestAlgorithms) {
|
|
int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
|
|
byte[] concatenationOfChunkCountAndChunkDigests =
|
|
new byte[5 + chunkCount * digestOutputSizeBytes];
|
|
concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
|
|
setUnsignedInt32LittleEngian(
|
|
chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
|
|
digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
|
|
}
|
|
|
|
int chunkIndex = 0;
|
|
byte[] chunkContentPrefix = new byte[5];
|
|
chunkContentPrefix[0] = (byte) 0xa5;
|
|
// Optimization opportunity: digests of chunks can be computed in parallel.
|
|
for (ByteBuffer input : contents) {
|
|
while (input.hasRemaining()) {
|
|
int chunkSize =
|
|
Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
|
final ByteBuffer chunk = getByteBuffer(input, chunkSize);
|
|
for (int digestAlgorithm : digestAlgorithms) {
|
|
String jcaAlgorithmName =
|
|
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
|
|
MessageDigest md;
|
|
try {
|
|
md = MessageDigest.getInstance(jcaAlgorithmName);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new DigestException(
|
|
jcaAlgorithmName + " MessageDigest not supported", e);
|
|
}
|
|
// Reset position to 0 and limit to capacity. Position would've been modified
|
|
// by the preceding iteration of this loop. NOTE: Contrary to the method name,
|
|
// this does not modify the contents of the chunk.
|
|
chunk.clear();
|
|
setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
|
|
md.update(chunkContentPrefix);
|
|
md.update(chunk);
|
|
byte[] concatenationOfChunkCountAndChunkDigests =
|
|
digestsOfChunks.get(digestAlgorithm);
|
|
int expectedDigestSizeBytes =
|
|
getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
|
|
int actualDigestSizeBytes =
|
|
md.digest(
|
|
concatenationOfChunkCountAndChunkDigests,
|
|
5 + chunkIndex * expectedDigestSizeBytes,
|
|
expectedDigestSizeBytes);
|
|
if (actualDigestSizeBytes != expectedDigestSizeBytes) {
|
|
throw new DigestException(
|
|
"Unexpected output size of " + md.getAlgorithm()
|
|
+ " digest: " + actualDigestSizeBytes);
|
|
}
|
|
}
|
|
chunkIndex++;
|
|
}
|
|
}
|
|
|
|
Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
|
|
for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
|
|
int digestAlgorithm = entry.getKey();
|
|
byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
|
|
String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
|
|
MessageDigest md;
|
|
try {
|
|
md = MessageDigest.getInstance(jcaAlgorithmName);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
|
|
}
|
|
result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static int getChunkCount(int inputSize, int chunkSize) {
|
|
return (inputSize + chunkSize - 1) / chunkSize;
|
|
}
|
|
|
|
private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) {
|
|
result[offset] = (byte) (value & 0xff);
|
|
result[offset + 1] = (byte) ((value >> 8) & 0xff);
|
|
result[offset + 2] = (byte) ((value >> 16) & 0xff);
|
|
result[offset + 3] = (byte) ((value >> 24) & 0xff);
|
|
}
|
|
|
|
private static byte[] generateApkSigningBlock(
|
|
List<SignerConfig> signerConfigs,
|
|
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
|
byte[] apkSignatureSchemeV2Block =
|
|
generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
|
|
return generateApkSigningBlock(apkSignatureSchemeV2Block);
|
|
}
|
|
|
|
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
|
|
// FORMAT:
|
|
// uint64: size (excluding this field)
|
|
// repeated ID-value pairs:
|
|
// uint64: size (excluding this field)
|
|
// uint32: ID
|
|
// (size - 4) bytes: value
|
|
// uint64: size (same as the one above)
|
|
// uint128: magic
|
|
|
|
int resultSize =
|
|
8 // size
|
|
+ 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
|
|
+ 8 // size
|
|
+ 16 // magic
|
|
;
|
|
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
long blockSizeFieldValue = resultSize - 8;
|
|
result.putLong(blockSizeFieldValue);
|
|
|
|
long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
|
|
result.putLong(pairSizeFieldValue);
|
|
result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
|
result.put(apkSignatureSchemeV2Block);
|
|
|
|
result.putLong(blockSizeFieldValue);
|
|
result.put(APK_SIGNING_BLOCK_MAGIC);
|
|
|
|
return result.array();
|
|
}
|
|
|
|
private static byte[] generateApkSignatureSchemeV2Block(
|
|
List<SignerConfig> signerConfigs,
|
|
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
|
// FORMAT:
|
|
// * length-prefixed sequence of length-prefixed signer blocks.
|
|
|
|
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
|
int signerNumber = 0;
|
|
for (SignerConfig signerConfig : signerConfigs) {
|
|
signerNumber++;
|
|
byte[] signerBlock;
|
|
try {
|
|
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
|
} catch (InvalidKeyException e) {
|
|
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
|
} catch (SignatureException e) {
|
|
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
|
}
|
|
signerBlocks.add(signerBlock);
|
|
}
|
|
|
|
return encodeAsSequenceOfLengthPrefixedElements(
|
|
new byte[][] {
|
|
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
|
});
|
|
}
|
|
|
|
private static byte[] generateSignerBlock(
|
|
SignerConfig signerConfig,
|
|
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
|
if (signerConfig.certificates.isEmpty()) {
|
|
throw new SignatureException("No certificates configured for signer");
|
|
}
|
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
|
|
|
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
|
|
|
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
|
|
try {
|
|
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
|
} catch (CertificateEncodingException e) {
|
|
throw new SignatureException("Failed to encode certificates", e);
|
|
}
|
|
|
|
List<Pair<Integer, byte[]>> digests =
|
|
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
|
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
|
int contentDigestAlgorithm =
|
|
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
|
|
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
|
if (contentDigest == null) {
|
|
throw new RuntimeException(
|
|
getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
|
|
+ " content digest for "
|
|
+ getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
|
|
+ " not computed");
|
|
}
|
|
digests.add(Pair.create(signatureAlgorithm, contentDigest));
|
|
}
|
|
signedData.digests = digests;
|
|
|
|
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
|
|
// FORMAT:
|
|
// * 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).
|
|
// * length-prefixed sequence of length-prefixed additional attributes:
|
|
// * uint32: ID
|
|
// * (length - 4) bytes: value
|
|
signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
|
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
|
|
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
|
|
// additional attributes
|
|
new byte[0],
|
|
});
|
|
signer.publicKey = encodedPublicKey;
|
|
signer.signatures = new ArrayList<>();
|
|
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
|
Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
|
|
getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
|
|
String jcaSignatureAlgorithm = signatureParams.getFirst();
|
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
|
|
byte[] signatureBytes;
|
|
try {
|
|
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
|
signature.initSign(signerConfig.privateKey);
|
|
if (jcaSignatureAlgorithmParams != null) {
|
|
signature.setParameter(jcaSignatureAlgorithmParams);
|
|
}
|
|
signature.update(signer.signedData);
|
|
signatureBytes = signature.sign();
|
|
} catch (InvalidKeyException e) {
|
|
throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
|
|
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
|
|
| SignatureException e) {
|
|
throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
|
|
}
|
|
|
|
try {
|
|
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
|
signature.initVerify(publicKey);
|
|
if (jcaSignatureAlgorithmParams != null) {
|
|
signature.setParameter(jcaSignatureAlgorithmParams);
|
|
}
|
|
signature.update(signer.signedData);
|
|
if (!signature.verify(signatureBytes)) {
|
|
throw new SignatureException("Signature did not verify");
|
|
}
|
|
} catch (InvalidKeyException e) {
|
|
throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
|
|
+ " signature using public key from certificate", e);
|
|
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
|
|
| SignatureException e) {
|
|
throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
|
|
+ " signature using public key from certificate", e);
|
|
}
|
|
|
|
signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
|
|
}
|
|
|
|
// FORMAT:
|
|
// * length-prefixed signed data
|
|
// * length-prefixed sequence of length-prefixed signatures:
|
|
// * uint32: signature algorithm ID
|
|
// * 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,
|
|
});
|
|
}
|
|
|
|
private static final class V2SignatureSchemeBlock {
|
|
private static final class Signer {
|
|
public byte[] signedData;
|
|
public List<Pair<Integer, byte[]>> signatures;
|
|
public byte[] publicKey;
|
|
}
|
|
|
|
private static final class SignedData {
|
|
public List<Pair<Integer, byte[]>> digests;
|
|
public List<byte[]> certificates;
|
|
}
|
|
}
|
|
|
|
private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
|
|
byte[] encodedPublicKey = null;
|
|
if ("X.509".equals(publicKey.getFormat())) {
|
|
encodedPublicKey = publicKey.getEncoded();
|
|
}
|
|
if (encodedPublicKey == null) {
|
|
try {
|
|
encodedPublicKey =
|
|
KeyFactory.getInstance(publicKey.getAlgorithm())
|
|
.getKeySpec(publicKey, X509EncodedKeySpec.class)
|
|
.getEncoded();
|
|
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
|
throw new InvalidKeyException(
|
|
"Failed to obtain X.509 encoded form of public key " + publicKey
|
|
+ " of class " + publicKey.getClass().getName(),
|
|
e);
|
|
}
|
|
}
|
|
if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
|
|
throw new InvalidKeyException(
|
|
"Failed to obtain X.509 encoded form of public key " + publicKey
|
|
+ " of class " + publicKey.getClass().getName());
|
|
}
|
|
return encodedPublicKey;
|
|
}
|
|
|
|
public static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
|
|
throws CertificateEncodingException {
|
|
List<byte[]> result = new ArrayList<>();
|
|
for (X509Certificate certificate : certificates) {
|
|
result.add(certificate.getEncoded());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
|
|
return encodeAsSequenceOfLengthPrefixedElements(
|
|
sequence.toArray(new byte[sequence.size()][]));
|
|
}
|
|
|
|
private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
|
|
int payloadSize = 0;
|
|
for (byte[] element : sequence) {
|
|
payloadSize += 4 + element.length;
|
|
}
|
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
for (byte[] element : sequence) {
|
|
result.putInt(element.length);
|
|
result.put(element);
|
|
}
|
|
return result.array();
|
|
}
|
|
|
|
private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
|
List<Pair<Integer, byte[]>> sequence) {
|
|
int resultSize = 0;
|
|
for (Pair<Integer, byte[]> element : sequence) {
|
|
resultSize += 12 + element.getSecond().length;
|
|
}
|
|
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
for (Pair<Integer, byte[]> 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();
|
|
}
|
|
|
|
/**
|
|
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
|
* position of this buffer.
|
|
*
|
|
* <p>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);
|
|
}
|
|
}
|
|
|
|
private static Pair<String, ? extends AlgorithmParameterSpec>
|
|
getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
|
|
switch (sigAlgorithm) {
|
|
case SIGNATURE_RSA_PSS_WITH_SHA256:
|
|
return Pair.create(
|
|
"SHA256withRSA/PSS",
|
|
new PSSParameterSpec(
|
|
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
|
|
case SIGNATURE_RSA_PSS_WITH_SHA512:
|
|
return Pair.create(
|
|
"SHA512withRSA/PSS",
|
|
new PSSParameterSpec(
|
|
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
|
|
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
|
|
return Pair.create("SHA256withRSA", null);
|
|
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
|
|
return Pair.create("SHA512withRSA", null);
|
|
case SIGNATURE_ECDSA_WITH_SHA256:
|
|
return Pair.create("SHA256withECDSA", null);
|
|
case SIGNATURE_ECDSA_WITH_SHA512:
|
|
return Pair.create("SHA512withECDSA", null);
|
|
case SIGNATURE_DSA_WITH_SHA256:
|
|
return Pair.create("SHA256withDSA", null);
|
|
case SIGNATURE_DSA_WITH_SHA512:
|
|
return Pair.create("SHA512withDSA", null);
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Unknown signature algorithm: 0x"
|
|
+ Long.toHexString(sigAlgorithm & 0xffffffff));
|
|
}
|
|
}
|
|
|
|
private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
|
|
switch (sigAlgorithm) {
|
|
case SIGNATURE_RSA_PSS_WITH_SHA256:
|
|
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
|
|
case SIGNATURE_ECDSA_WITH_SHA256:
|
|
case SIGNATURE_DSA_WITH_SHA256:
|
|
return CONTENT_DIGEST_CHUNKED_SHA256;
|
|
case SIGNATURE_RSA_PSS_WITH_SHA512:
|
|
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
|
|
case SIGNATURE_ECDSA_WITH_SHA512:
|
|
case SIGNATURE_DSA_WITH_SHA512:
|
|
return CONTENT_DIGEST_CHUNKED_SHA512;
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Unknown signature algorithm: 0x"
|
|
+ Long.toHexString(sigAlgorithm & 0xffffffff));
|
|
}
|
|
}
|
|
|
|
private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
|
|
switch (digestAlgorithm) {
|
|
case CONTENT_DIGEST_CHUNKED_SHA256:
|
|
return "SHA-256";
|
|
case CONTENT_DIGEST_CHUNKED_SHA512:
|
|
return "SHA-512";
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Unknown content digest algorthm: " + digestAlgorithm);
|
|
}
|
|
}
|
|
|
|
private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
|
|
switch (digestAlgorithm) {
|
|
case CONTENT_DIGEST_CHUNKED_SHA256:
|
|
return 256 / 8;
|
|
case CONTENT_DIGEST_CHUNKED_SHA512:
|
|
return 512 / 8;
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Unknown content digest algorthm: " + digestAlgorithm);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that APK file could not be parsed.
|
|
*/
|
|
public static class ApkParseException extends Exception {
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
public ApkParseException(String message) {
|
|
super(message);
|
|
}
|
|
|
|
public ApkParseException(String message, Throwable cause) {
|
|
super(message, cause);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pair of two elements.
|
|
*/
|
|
private static class Pair<A, B> {
|
|
private final A mFirst;
|
|
private final B mSecond;
|
|
|
|
private Pair(A first, B second) {
|
|
mFirst = first;
|
|
mSecond = second;
|
|
}
|
|
|
|
public static <A, B> Pair<A, B> create(A first, B second) {
|
|
return new Pair<>(first, second);
|
|
}
|
|
|
|
public A getFirst() {
|
|
return mFirst;
|
|
}
|
|
|
|
public B getSecond() {
|
|
return mSecond;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
final int prime = 31;
|
|
int result = 1;
|
|
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
|
|
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (this == obj) {
|
|
return true;
|
|
}
|
|
if (obj == null) {
|
|
return false;
|
|
}
|
|
if (getClass() != obj.getClass()) {
|
|
return false;
|
|
}
|
|
@SuppressWarnings("rawtypes")
|
|
Pair other = (Pair) obj;
|
|
if (mFirst == null) {
|
|
if (other.mFirst != null) {
|
|
return false;
|
|
}
|
|
} else if (!mFirst.equals(other.mFirst)) {
|
|
return false;
|
|
}
|
|
if (mSecond == null) {
|
|
return other.mSecond == null;
|
|
} else return mSecond.equals(other.mSecond);
|
|
}
|
|
}
|
|
}
|