VasDolly对V1签名机制的支持
V1的签名机制
V1签名主要就是依靠在META-INF
目录下的MANIFEST.MF
、CERT.SF
、CERT.RSA
。
生成签名的流程
签名可以使用2个工具:
- jarsigner
- signapk
前者是java原生自带的工具,签名时使用.keystore
或.jks
文件;而signapk则是android专门为自己定制的签名工具,签名时会用到2个文件.pk8
和.x509[.pem]
,他们各有自己的作用。两者的签名算法其实没啥差别。
Eclispe里一般还是采用jarsign工具进行签名,跟这个配套的还有keytool工具,常用的命令有:
【生成】证书库
keytool -genkey -v -keystore strange.keystore -alias strange -keyalg RSA -keysize 2048 -validity 10000
【查看】证书库
keytool -list -keystore stange.keystore
【签名】签名
jarsigner -verbose -sigalg SHA1withDSA -digestalg SHA1 -keystore D:\stange.keystore -storepass 123456 D:\123.apk stange
AS里现在都是采用的signapk的签名方式,.x509[.pem]
包含证书的信息,包括证书链,公钥和加密算法;.pk8
存放的就是私钥。跟signapk配套的生成工具是openssl,常用的命令有:
【生成】密钥
openssl genrsa -out key.pem 2048
【生成】证书请求
openssl req -new -key key.pem -out request.pem
【生成】 pem格式的 x.509 证书
openssl x509 -req -days 10000 -in request.pem -signkey key.pem -out certificate.pem -sha256
【生成】 pk8 格式密钥
openssl pkcs8 -topk8 -outform DER -in key.pem -inform PEM -out key.pk8 -nocrypt
【查看】pem证书
openssl x509 -in publicKey.x509.pem -text -noout
【签名】签名
java -jar signapk.jar [-w] publickey.x509[.pem] privatekey.pk8 input.jar output.jar
为啥AS采用signapk签名,但是放入keystore也是可以的?
原因是keystore和pk8、x509(pem)是可以相关转换的。这个在AS打包的时候其实已经做了的。
SignApk.java源码分析
1. 从publickey.x509[.pem]中读出公钥
private static X509Certificate readPublicKey(File file)
throws IOException, GeneralSecurityException {
FileInputStream input = new FileInputStream(file);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(input);
} finally {
input.close();
}
}
2. 从privatekey.pk8中读取私钥
private static PrivateKey readPrivateKey(File file)
throws IOException, GeneralSecurityException {
DataInputStream input = new DataInputStream(new FileInputStream(file));
try {
byte[] bytes = new byte[(int) file.length()];
input.read(bytes);
KeySpec spec = decryptPrivateKey(bytes, file);
if (spec == null) {
spec = new PKCS8EncodedKeySpec(bytes);
}
try {
return KeyFactory.getInstance("RSA").generatePrivate(spec);
} catch (InvalidKeySpecException ex) {
return KeyFactory.getInstance("DSA").generatePrivate(spec);
}
} finally {
input.close();
}
}
3. 写MANIFEST.MF
private static Manifest addDigestsToManifest(JarFile jar)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
if (input != null) {
main.putAll(input.getMainAttributes());
} else {
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
}
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] buffer = new byte[4096];
int num;
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
byName.put(entry.getName(), entry);
}
for (JarEntry entry: byName.values()) {
String name = entry.getName();
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
!name.equals(OTACERT_NAME) &&
(stripPattern == null ||
!stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
md.update(buffer, 0, num);
}
Attributes attr = null;
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
attr.putValue("SHA1-Digest",
new String(Base64.encode(md.digest()), "ASCII"));
output.getEntries().put(name, attr);
}
}
return output;
}
可以看出来,MANIFEST.MF里的数据是除了三个签名文件外的所有文件签名记录,而签名的策略是对每个文件的内容采用SHA1,然后做base64。
4. 写CERT.MF
private static void writeSignatureFile(Manifest manifest, OutputStream out)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest",
new String(Base64.encode(md.digest()), "ASCII"));
sf.getEntries().put(entry.getKey(), sfAttr);
}
CountOutputStream cout = new CountOutputStream(out);
sf.write(cout);
}
它首先是将上面的MANIFEST.MF文件整体的做了SHA1+base64的处理,然后对文件里的每个entry块(注意大循环里面还有个小的循环)进行SHA1+base64的处理,最后将这些信息写入到CERT.MF中。
5. 写CERT.RSA
RSA文件是二进制文件,已经加密过,无法正常查看,需要用命令:
openssl pkcs7 -inform DER -in CERT.RSA -text -print_certs
代码如下:
private static void writeSignatureBlock(
Signature signature, X509Certificate publicKey, OutputStream out)
throws IOException, GeneralSecurityException {
SignerInfo signerInfo = new SignerInfo(
new X500Name(publicKey.getIssuerX500Principal().getName()),
publicKey.getSerialNumber(),
AlgorithmId.get("SHA1"),
AlgorithmId.get("RSA"),
signature.sign());
PKCS7 pkcs7 = new PKCS7(
new AlgorithmId[] { AlgorithmId.get("SHA1") },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] { publicKey },
new SignerInfo[] { signerInfo });
pkcs7.encodeSignedData(out);
}
这里会把之前生成的CERT.SF文件,用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入CERT.RSA中保存。
验证签名的流程
代码在PackageParser.java
里,在安装APK时,Android系统会校验签名,检查APK是否被篡改。
源码:
private static void collectCertificates(Package pkg, File apkFile, int parseFlags)
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
// Try to verify the APK using APK Signature Scheme v2.
boolean verified = false;
{
Certificate[][] allSignersCerts = null;
Signature[] signatures = null;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
signatures = convertToSignatures(allSignersCerts);
// APK verified using APK Signature Scheme v2.
verified = true;
} catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) {
// No APK Signature Scheme v2 signature found
if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in ephemeral package " + apkPath,
e);
}
// Static shared libraries must use only the V2 signing scheme
if (pkg.applicationInfo.isStaticSharedLibrary()) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Static shared libs must use v2 signature scheme " + apkPath);
}
} catch (Exception e) {
// APK Signature Scheme v2 signature was found but did not verify
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath
+ " using APK Signature Scheme v2",
e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
if (verified) {
if (pkg.mCertificates == null) {
pkg.mCertificates = allSignersCerts;
pkg.mSignatures = signatures;
pkg.mSigningKeys = new ArraySet<>(allSignersCerts.length);
for (int i = 0; i < allSignersCerts.length; i++) {
Certificate[] signerCerts = allSignersCerts[i];
Certificate signerCert = signerCerts[0];
pkg.mSigningKeys.add(signerCert.getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, signatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
apkPath + " has mismatched certificates");
}
}
// Not yet done, because we need to confirm that AndroidManifest.xml exists and,
// if requested, that classes.dex exists.
}
}
StrictJarFile jarFile = null;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
// Ignore signature stripping protections when verifying APKs from system partition.
// For those APKs we only care about extracting signer certificates, and don't care
// about verifying integrity.
boolean signatureSchemeRollbackProtectionsEnforced =
(parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
jarFile = new StrictJarFile(
apkPath,
!verified, // whether to verify JAR signature
signatureSchemeRollbackProtectionsEnforced);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
// Always verify manifest, regardless of source
final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
}
// Optimization: early termination when APK already verified
if (verified) {
return;
}
// APK's integrity needs to be verified using JAR signature scheme.
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1");
final List<ZipEntry> toVerify = new ArrayList<>();
toVerify.add(manifestEntry);
// If we're parsing an untrusted package, verify all contents
if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) {
final Iterator<ZipEntry> i = jarFile.iterator();
while (i.hasNext()) {
final ZipEntry entry = i.next();
if (entry.isDirectory()) continue;
final String entryName = entry.getName();
if (entryName.startsWith("META-INF/")) continue;
if (entryName.equals(ANDROID_MANIFEST_FILENAME)) continue;
toVerify.add(entry);
}
}
// Verify that entries are signed consistently with the first entry
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.
for (ZipEntry entry : toVerify) {
final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
if (ArrayUtils.isEmpty(entryCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Package " + apkPath + " has no certificates at entry "
+ entry.getName());
}
final Signature[] entrySignatures = convertToSignatures(entryCerts);
if (pkg.mCertificates == null) {
pkg.mCertificates = entryCerts;
pkg.mSignatures = entrySignatures;
pkg.mSigningKeys = new ArraySet<PublicKey>();
for (int i=0; i < entryCerts.length; i++) {
pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
+ " has mismatched certificates at entry "
+ entry.getName());
}
}
}
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath, e);
} finally {
closeQuietly(jarFile);
}
}
可以看出,解析是同时针对V1和V2的。
VasDolly对的多渠道支持
结合上一篇的zip文件个是的分析,和现在V1签名的机制分析。我们发现其实可以在不重新签名的情况下,完成多个渠道的打包。
我们可以将各个渠道的数据写入到apk文件(即zip)的最后面,即EOCD里面,因为这个是不参与V1校验的。而里面的comment字段是最好的选择。
因此具体的方案是:在apk文件(即zip)的注释字段里,添加渠道信息。
- 拷贝原始apk文件
- 找到apk文件的EOCD数据块
- 修改EOCD数据块里的comment长度
- 增加对应的渠道信息
- 添加渠道信息的字段长度
- 添加魔数(用处是查找渠道信息时,更方便的定位)
这样处理之后,apk里的EOCD块就成了这个样子:
查找就相对简单很多:
- 定位到魔数
- 向前读两个字节,确定渠道信息的长度LEN
- 继续向前读LEN字节,就是渠道信息了。
源码分析
1. 判断是否是V1签名
public static boolean containV1Signature(File file) {
JarFile jarFile;
try {
jarFile = new JarFile(file);
JarEntry manifestEntry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
JarEntry sfEntry = null;
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().matches("META-INF/\\w+\\.SF")) {
sfEntry = jarFile.getJarEntry(entry.getName());
break;
}
}
if (manifestEntry != null && sfEntry != null) {
return true;
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
2. apk文件的拷贝
public static void copyFileUsingNio(File source, File dest) throws IOException {
FileChannel in = null;
FileChannel out = null;
FileInputStream inStream = null;
FileOutputStream outStream = null;
File parent = dest.getParentFile();
if (parent != null && (!parent.exists())) {
parent.mkdirs();
}
try {
inStream = new FileInputStream(source);
outStream = new FileOutputStream(dest, false);
in = inStream.getChannel();
out = outStream.getChannel();
in.transferTo(0, in.size(), out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inStream != null) {
inStream.close();
}
if (outStream != null) {
outStream.close();
}
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
3. 得到EOCD块
public static Pair<ByteBuffer, Long> getEocd(File apk) throws IOException, ApkSignatureSchemeV2Verifier.SignatureNotFoundException {
if (apk == null || !apk.exists() || !apk.isFile()) {
return null;
}
RandomAccessFile raf = new RandomAccessFile(apk, "r");
//find the EOCD
Pair<ByteBuffer, Long> eocdAndOffsetInFile = ApkSignatureSchemeV2Verifier.getEocd(raf);
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(raf, eocdAndOffsetInFile.getSecond())) {
throw new ApkSignatureSchemeV2Verifier.SignatureNotFoundException("ZIP64 APK not supported");
}
return eocdAndOffsetInFile;
}
4. 写入
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//1.locate comment length field
raf.seek(file.length() - ChannelConstants.SHORT_LENGTH);
//2.write zip comment length (content field length + length field length + magic field length)
writeShort(comment.length + ChannelConstants.SHORT_LENGTH + ChannelConstants.V1_MAGIC.length, raf);
//3.write content
raf.write(comment);
//4.write content length
writeShort(comment.length, raf);
//5. write magic bytes
raf.write(ChannelConstants.V1_MAGIC);
raf.close();
6. 校验写入的渠道值是否正确
7. 校验V1签名是否正确
/**
* verify V1 signature
*
* @param inputApk
* @return
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws ZipFormatException
* @throws ApkFormatException
*/
public static boolean verifyV1Signature(File inputApk) throws NoSuchAlgorithmException, IOException, ZipFormatException, ApkFormatException {
ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(inputApk);
ApkVerifier apkVerifier = apkVerifierBuilder.build();
ApkVerifier.Result result = apkVerifier.verify();
boolean verified = result.isVerified();
System.out.println("verified : " + verified);
if (verified) {
System.out.println("Verified using v1 scheme (JAR signing): " + result.isVerifiedUsingV1Scheme());
System.out.println("Verified using v2 scheme (APK Signature Scheme v2): " + result.isVerifiedUsingV2Scheme());
if (result.isVerifiedUsingV1Scheme()) {
return true;
}
}
return false;
}
如何读取值呢?
read source...