Android签名验证原理解析

Android开发 专栏收录该内容
175 篇文章 6 订阅

一、基本概念

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

欢迎关注微信公众号:DroidMind
精品内容独家发布平台


呈现与博客不一样的技术干货

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值