Android签名验证原理解析

来源:互联网 发布:联通网络电视客户端 编辑:程序博客网 时间:2024/05/18 18:21

一、基本概念

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应用程序签名验证过程分析

原创粉丝点击