一、基本概念
1、keytool
keytool 是个密钥和证书管理工具。它使用户能够管理和生成自己的公钥/私钥对及相关证书
2、keystore
keystore是一个密钥库,里面存放着一个一个的密钥对实体。也就是说密钥对是存放在keystore里面。
二、签名文件keystore的生成过程
使用keytool工具
keytool -genkeypair -alias "test" -keyalg "RSA" -keystore "test.keystore"
上面命令的作用就是创建一个别名为test的证书,该证书存放在名为test.keystore的密钥库中,若test.keystore密钥库不存在则创建。
下面我们来看看具体的源码实现(KeyTool.java):
// 如果使用-genkeypair指令
if (command == GENKEYPAIR) {
if (keyAlgName == null) {
keyAlgName = "DSA";
}
doGenKeyPair(alias, dname, keyAlgName, keysize, sigAlgName);
kssave = true;
}
从上面代码可以看出,会调用doGenKeyPair方法来产生一个密钥对。
private void doGenKeyPair(String alias, String dname, String keyAlgName,
int keysize, String sigAlgName)
throws Exception
{
//1、创建一个密钥对生成对象
CertAndKeyGen keypair =
new CertAndKeyGen(keyAlgName, sigAlgName, providerName);
X500Name x500Name = new X500Name(dname);
// 2、生成一个密钥对
keypair.generate(keysize);
PrivateKey privKey = keypair.getPrivateKey();
// 3、生成一个证书链
X509Certificate[] chain = new X509Certificate[1];
chain[0] = keypair.getSelfCertificate(
x500Name, getStartDate(startDate), validity*24L*60L*60L);
if (keyPass == null) {
keyPass = promptForKeyPass(alias, null, storePass);
}
// 创建一个密钥对实体,并将其放入keystore中
keyStore.setKeyEntry(alias, privKey, keyPass, chain);
}
(1)、创建一个密钥对生成对象
public CertAndKeyGen (String keyType, String sigAlg, String providerName)
throws NoSuchAlgorithmException, NoSuchProviderException
{
if (providerName == null) {
keyGen = KeyPairGenerator.getInstance(keyType);
} else {
try {
keyGen = KeyPairGenerator.getInstance(keyType, providerName);
} catch (Exception e) {
// try first available provider instead
keyGen = KeyPairGenerator.getInstance(keyType);
}
}
this.sigAlg = sigAlg;
}
可以看到里面封装的就是一个KeyPairGenerator对象,KeyPairGenerator就是一个密钥对生成类
(2)、生成一个密钥对
public void generate (int keyBits)
throws InvalidKeyException
{
KeyPair pair;
try {
if (prng == null) {
prng = new SecureRandom();
}
keyGen.initialize(keyBits, prng);
pair = keyGen.generateKeyPair();
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage());
}
publicKey = pair.getPublic();
privateKey = pair.getPrivate();
}
上面就是使用KeyPairGenerator对象来生成一个密钥对。密钥对包含有公钥和私钥。
(3)、生成一个证书链
public X509Certificate getSelfCertificate (
X500Name myname, Date firstDate, long validity)
throws CertificateException, InvalidKeyException, SignatureException,
NoSuchAlgorithmException, NoSuchProviderException
{
X509CertImpl cert;
Date lastDate;
try {
lastDate = new Date ();
lastDate.setTime (firstDate.getTime () + validity * 1000);
CertificateValidity interval =
new CertificateValidity(firstDate,lastDate);
X509CertInfo info = new X509CertInfo();
info.set(X509CertInfo.VERSION,
new CertificateVersion(CertificateVersion.V3));
info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(
new java.util.Random().nextInt() & 0x7fffffff));
AlgorithmId algID = AlgorithmId.getAlgorithmId(sigAlg);
info.set(X509CertInfo.ALGORITHM_ID,
new CertificateAlgorithmId(algID));
info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(myname));
// 将生成的公钥放入证书里面
info.set(X509CertInfo.KEY, new CertificateX509Key(publicKey));
info.set(X509CertInfo.VALIDITY, interval);
info.set(X509CertInfo.ISSUER, new CertificateIssuerName(myname));
cert = new X509CertImpl(info);
cert.sign(privateKey, this.sigAlg);
return (X509Certificate)cert;
} catch (IOException e) {
throw new CertificateEncodingException("getSelfCert: " +
e.getMessage());
}
}
上面就是利用给的的信息,生成一个证书X509Certificate,需要注意的是证书中包含有生成的公钥。也就是说我们的公钥是公开的,私钥是自己保存。
从上面的分析可以知道,在签名文件keystore中包含有一个密钥实体,密钥实体中包含有私钥和证书信息,在证书中包含有公钥信息。
二、APK签名过程
Android中签名有两个工具:jarsign和signapk
jarsign是Java本生自带的一个工具,他可以对jar进行签名的,它使用的是keystore文件来进行签名
signapk是专门为Android应用程序apk进行签名的工具,它使用的是.pk8和.x509.pem这两个文件进行签名,k8是私钥文件,x509.pem是含有公钥的文件。
下面主要来看看jarsign的签名过程,因为它使用的是keystore文件(JarSigner.java)
1、初始化过程
public static void main(String args[]) throws Exception {
JarSigner js = new JarSigner();
js.run(args);
}
public void run(String args[]) {
// 1、处理传入的参数
parseArgs(args);
// 2、加载keystore文件
loadKeyStore(keystore, true);
// 3、获取相关信息
getAliasInfo(alias);
// 4、进行签名
signJar(jarfile, alias, args);
}
(1)、处理传入的参数
主要对传入的参数进行处理
(2)加载keystore文件
KeyStore store = KeyStore.getInstance(storetype, providerName);
store.load(is, storepass);
得到一个KeyStore对象,并且载入keystore文件
(3)获取相关信息
key = store.getKey(alias, storepass);
privateKey = (PrivateKey)key;
certChain = new X509Certificate[cs.length];
for (int i=0; i<cs.length; i++) {
certChain[i] = (X509Certificate)cs[i];
}
这里主要获得到了密钥信息和证书信息,这个也是keystore存放的主要信息。
(4)进行签名
MANIFEST.MF文件生成
zipFile = new ZipFile(jarName);
BASE64Encoder encoder = new JarBASE64Encoder();
Vector<ZipEntry> mfFiles = new Vector<>();
boolean wasSigned = false;
for (Enumeration<? extends ZipEntry> enum_=zipFile.entries();
enum_.hasMoreElements();) {
ZipEntry ze = enum_.nextElement();
if (!ze.isDirectory()) {
// Add entry to manifest
Attributes attrs = getDigestAttributes(ze, zipFile,
digests,
encoder);
mfEntries.put(ze.getName(), attrs);
mfModified = true;
}
}
private Attributes getDigestAttributes(ZipEntry ze, ZipFile zf,
MessageDigest[] digests,
BASE64Encoder encoder)
throws IOException {
String[] base64Digests = getDigests(ze, zf, digests, encoder);
Attributes attrs = new Attributes();
for (int i=0; i<digests.length; i++) {
attrs.putValue(digests[i].getAlgorithm()+"-Digest",
base64Digests[i]);
}
return attrs;
}
逐一遍历APK中的所有条目,如果是目录就跳过,如果是一个文件,就用SHA1(或者SHA256)消息摘要算法提取出该文件的摘要然后进行BASE64编码后,作为“SHA1-Digest”属性的值写入到MANIFEST.MF文件中的一个块中。另外该块有一个“Name”属性,其值就是该文件在apk包中的路径。
CERT.SF文件生成
Map<String,Attributes> entries = sf.getEntries();
Iterator<Map.Entry<String,Attributes>> mit = mf.getEntries().entrySet().iterator();
while(mit.hasNext()) {
Map.Entry<String,Attributes> e = mit.next();
String name = e.getKey();
mde = md.get(name, false);
if (mde != null) {
Attributes attr = new Attributes();
for (int i=0; i < digests.length; i++) {
attr.putValue(digests[i].getAlgorithm()+"-Digest",
encoder.encode(mde.digest(digests[i])));
}
entries.put(name, attr);
}
}
逐条计算MANIFEST.MF文件中每一个块的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest
CERT.RSA文件生成
这个是我们重点关注的内容,因为我们的签名证书就是在这个地方发挥作用的。
public byte[] generateSignedData(ContentSignerParameters parameters,
boolean omitContent, boolean applyTimestamp)
throws NoSuchAlgorithmException, CertificateException, IOException {
String signatureAlgorithm = parameters.getSignatureAlgorithm();
String keyAlgorithm =
AlgorithmId.getEncAlgFromSigAlg(signatureAlgorithm);
String digestAlgorithm =
AlgorithmId.getDigAlgFromSigAlg(signatureAlgorithm);
AlgorithmId digestAlgorithmId = AlgorithmId.get(digestAlgorithm);
X509Certificate[] signerCertificateChain =
parameters.getSignerCertificateChain();
Principal issuerName = signerCertificateChain[0].getIssuerDN();
BigInteger serialNumber = signerCertificateChain[0].getSerialNumber();
byte[] content = parameters.getContent();
ContentInfo contentInfo;
contentInfo = new ContentInfo(content);
byte[] signature = parameters.getSignature();
SignerInfo signerInfo = null;
signerInfo = new SignerInfo((X500Name)issuerName, serialNumber,
digestAlgorithmId, AlgorithmId.get(keyAlgorithm), signature);
SignerInfo[] signerInfos = {signerInfo};
AlgorithmId[] algorithms = {digestAlgorithmId};
// Create the PKCS #7 signed data message
PKCS7 p7 = new PKCS7(algorithms, contentInfo, signerCertificateChain,
null, signerInfos);
ByteArrayOutputStream p7out = new ByteArrayOutputStream();
p7.encodeSignedData(p7out);
return p7out.toByteArray();
}
它会把前面生成的 CERT.SF文件用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。
详细的过程可以参考文章:Android签名机制之—签名过程详解
三、APK安装校验过程
1、通过在CERT.RSA文件中记录的签名信息,验证了CERT.SF没有被篡改过
libcore\luni\src\main\java\org\apache\harmony\security\utils\JarUtils.java
// InputStream signature 对应 CERT.SF文件
// InputStream signatureBlock 对应 CERT.RSA文件
public static Certificate[] verifySignature(InputStream signature, InputStream
signatureBlock) throws IOException, GeneralSecurityException {
BerInputStream bis = new BerInputStream(signatureBlock);
ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
SignedData signedData = info.getSignedData();
Collection<org.apache.harmony.security.x509.Certificate> encCerts
= signedData.getCertificates();
if (encCerts.isEmpty()) {
return null;
}
X509Certificate[] certs = new X509Certificate[encCerts.size()];
int i = 0;
for (org.apache.harmony.security.x509.Certificate encCert : encCerts) {
certs[i++] = new X509CertImpl(encCert);
}
List<SignerInfo> sigInfos = signedData.getSignerInfos();
SignerInfo sigInfo;
if (!sigInfos.isEmpty()) {
sigInfo = sigInfos.get(0);
} else {
return null;
}
// Issuer
X500Principal issuer = sigInfo.getIssuer();
// Certificate serial number
BigInteger snum = sigInfo.getSerialNumber();
// Locate the certificate
int issuerSertIndex = 0;
for (i = 0; i < certs.length; i++) {
if (issuer.equals(certs[i].getIssuerDN()) &&
snum.equals(certs[i].getSerialNumber())) {
issuerSertIndex = i;
break;
}
}
sig = Signature.getInstance(alg);
// 这里实质会使用证书里面的公钥进行初始化
sig.initVerify(certs[issuerSertIndex]);
List<AttributeTypeAndValue> atr = sigInfo.getAuthenticatedAttributes();
byte[] sfBytes = new byte[signature.available()];
signature.read(sfBytes);
sig.update(sfBytes);
// 使用签名进行验证
if (!sig.verify(sigInfo.getEncryptedDigest())) {
throw new SecurityException("Incorrect signature");
}
return createChain(certs[issuerSertIndex], certs);
}
这里就是使用CERT.RSA文件中保存的对CERT.SF文件的签名信息和公钥信息来对当前的CERT.SF文件进行验证。
关于签名验证过程,如果不明白可以参考文章:
JAVA RSA密钥对的生成与验证
Java&keytool生成RSA密钥
2、通过CERT.SF文件中记录的摘要值,验证了MANIFEST.MF没有被修改过
3、apk内文件的摘要值要与MANIFEST.MF文件中记录的一致
后面两个思路估计简单,这里省略,具体参考:Android应用程序签名验证过程分析