Android签名原理介绍(2)--schemev2

接上一篇,接下来介绍7.0后新增的v2签名认证方式

APK Signature Scheme v2

与v1方式签名不同的是,v2的签名是对整个文件的一种签名方式

直接从字节码上对apk进行签名,不必遍历每个入口进行计算签名

并且每一个改动都会使得签名失败,因此其提高了签名认证的速度和签名保护的安全性

但是有一点得注意的是,v2签名仅用在7.0以上的系统中,因为只有7.0以上的系统有有关v2签名认证的代码

因此为了适配早前的系统,正常还是采用v1签名,或是v1v2签名并存

在后面的代码可以看出来,7.0以上的系统会先进行v2签名的认证,若没检测到v2签名信息,则再进行v1的方法

但是由于一些应用需要打包渠道包,每次修改再编译再签名实在太耗时,大部分会选择关闭v2签名

在build Gradle中添加

v2SigningEnabled false

因为在v1签名中,META-INF中的内容是不会检测的,这使得在打包渠道包时不需要重复重新签名

渠道包指的是在各大应用市场,发布的apk包的清单文件中,某个meta-data标签下,配置的value不一样,这个标签的作用就是用来区分是哪个市场的

下图是从google上扒来的图,更详细的介绍可以看google

https://source.android.com/security/apksigning/v2

APK before and after signing

与不签名相比,v2签名在ZIP Central Directory section之前插入了一段signing block,这就是所有签名的信息

APK signature verification process

签名认证过程


这里是Android7.1.1_r28下signApk工具中使用v2签名方式进行签名的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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,
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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[] 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,
});
}

这里贴上关键的两个函数

通过分析可知,其签名过程分为以下几个步骤

  1. 把apk看为zip,把zip的三个块划分开
  2. 分别对三个块从提前获取到的摘要算法(signerConfigs)进行hash处理,这部分就是用于签名的信息
  3. 通过私钥对从这三个块获得的签名数据进行加密,获得签名,并编码公钥,签名结果

格式在注释中已说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//SIGNEDDATA 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

//BLOCK 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)

//SIGNED BLOCK 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

APK digest

对于验证过程

APK Signature Scheme v2 verification

  1. Locate the APK Signing Block and verify that:
    1. Two size fields of APK Signing Block contain the same value.
    2. ZIP Central Directory is immediately followed by ZIP End of Central Directory record.
    3. ZIP End of Central Directory is not followed by more data.
  2. Locate the first APK Signature Scheme v2 Block inside the APK Signing Block. If the v2 Block if present, proceed to step 3. Otherwise, fall back to verifying the APK using v1 scheme.
  3. For each signer in the APK Signature Scheme v2 Block:Choose the strongest supported signature algorithm ID from signatures. The strength ordering is up to each implementation/platform version.Verify the corresponding signature from signatures against signed data using public key. (It is now safe to parse signed data.)Verify that the ordered list of signature algorithm IDs in digests and signatures is identical. (This is to prevent signature stripping/addition.)Compute the digest of APK contents using the same digest algorithm as the digest algorithm used by the signature algorithm.Verify that the computed digest is identical to the corresponding digest from digests.Verify that SubjectPublicKeyInfo of the first certificate of certificates is identical to public key.
  4. Verification succeeds if at least one signer was found and step 3 succeeded for each found signer.

  1. 定位到signing block
  2. 获取签名证书的段,读取公钥,解密签名数据
  3. 获取摘要算法,对另外三个块进行hash,并与解密出来的数据对比

对于有多个不同签名的apk,将会拥有多个signing block

以下即是其中一个apk的v2签名块

startOfSignBlock

endOfSignBlock

中间还有很多的信息省略了

这里可以看到在签名块结尾的magic number

APK Sig Block 42


对于v2签名介绍已完毕,下一篇将会简单地对比两种签名方式 (好像该对比的都对比得差不多了23333)

×

赞助gif换电脑、吃双皮奶(逃

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. APK Signature Scheme v2
    1. 1.1. APK Signature Scheme v2 verification
,