深入理解Android之Java Security(第一部分)
深入理解Android之Java Security(第一部分)
邓凡平 发表于3年前
深入理解Android之Java Security(第一部分)
  • 发表于 3年前
  • 阅读 752
  • 收藏 3
  • 点赞 0
  • 评论 0
摘要: 从事Android工作4年以来,只有前1年不到的时间是用C++在开发东西(主要是开发DLNA组件,目前我已将它们全部开源,参考http://blog.csdn.net/innost/article/details/40216763),后面的工作几乎都在用Java。自以为Java相关的东西都见过了,可前段时间有个朋友给我花了1个多小时讲解他们某套系统的安全体系结构,其中涉及到很多专业术语,比如Message Digest(消息摘要)、Digital Signature(数字签名)、KeyStore(恕我不知道翻译成什么好,还是用英文原称吧)、CA(Certificate Authority...

深入理解Android之Java Security(第一部分)

从事Android工作4年以来,只有前1年不到的时间是用C++在开发东西(主要是开发DLNA组件,目前我已将它们全部开源,参考http://blog.csdn.net/innost/article/details/40216763),后面的工作几乎都在用Java。自以为Java相关的东西都见过了,可前段时间有个朋友给我花了1个多小时讲解他们某套系统的安全体系结构,其中涉及到很多专业术语,比如Message Digest(消息摘要)、Digital Signature(数字签名)、KeyStore(恕我不知道翻译成什么好,还是用英文原称吧)、CA(Certificate Authority)等。我当时脑袋就大了,尼玛搞Java这么久,从来没接触过啊。为此,我特意到AndroidFramework代码中查询了下,Android平台里与之相关的东西还有一个KeyChain。

原来,上述内容都属于Java世界中一个早已存在的知识模块,那就是JavaSecurity。Java Security包含很多知识点,常见的有MD5,DigitalSignature等,而Android在Java Seurity之外,拓展了一个android.security包,此包中就提供了KeyChain。

本文将介绍Java Security相关的基础知识,然后介绍下Android平台上与之相关的使用场景。

实际上,在一些金融,银行,电子支付方面的应用程序中,JavaSecurity使用的地方非常多。

代码路径:

  • Security.java:libcore/lunl/src/main/java/java/security/
  • TrustedCertificateStore.java:libcore /crypto/src/main/java/org/conscrypt/

本文例子:

https://code.csdn.net/Innost/androidsecuritydemo  目前已公开。

一  Java Security背景知识

Java Security其实是Java平台中一个比较独立的模块。除了软件实现上内容外,它实际上对应了一系列的规范。从Java2开始,Java Security包含主要三个重要的规范:

  • JavaCryptography Extension(简写为JCE),JCE所包含的内容有加解密,密钥交换,消息摘要(Message Digest,比如MD5等),密钥管理等。本文所涉及的大部分内容都属于JCE的范畴。
  • JavaSecure Socket Extension(简写为JSSE),JSSE所包含的内容就是Java层的SSL/TLS。简单点说,使用JSSE就可以创建SSL/TLS socket了。
  • JavaAuthentication and Authorization Service(简写为JAAS),JSSA和认证/授权有关。这部分内容在客户端接触得会比较少一点,所以本文不拟讨论它。

在上述三个子模块或规范中,JCE是JavaSecurity的大头,其他两个子模块JSSE和JAAS都依赖于它,比如SSL/TLS在工作过程中需要使用密钥对数据进行加解密,那么密钥的创建和使用就依靠JCE子模块了。

另外,既然和安全相关,那么对安全敏感的相关部门或政府肯定会有所干涉。Java是在美国被发明的,所以美国政府对于Java Security方面的出口(比如哪些模块,哪些功能能给其他国家使用)有相关的限制。例如,不允许出口的JCE(从软件实现上看,可能就是从Java官网上下载到的几个Jar包文件)支持一些高级的加解密功能(比如在密钥长度等方面有所限制)。

注意,Java Security包含的内容非常多,而本文将重点关注Java SecurityAPI与其使用方法方面的知识。对Java Security其他细节感兴趣的同学可在阅读完本文后,再阅读参考文献[1]。

 

1.1  JCE介绍

1.1.1  JCE设计架构介绍

介绍JCE之前,先来讲解下JCE的设计架构。JCE在设计时重点关注两个问题:

  • 其功能具体实现的独立性和可互操作性。
  • 使用算法的独立性和可扩展性。

对于独立性而言,一个最通用的做法就是把定义和实现通过抽象类或基类的方式进行解耦合。在JCE中:

  • 它首先把功能组合成一个一个的Service(也称作Engine)。比如,对应“加解密”的API组合成CipherService,对应“消息摘要”的API组合成MessageDigest Service,对应“签名”的API组合成SignatureService。JCE为这些Service都定义了一个诸如XXXSpi的抽象类。其中,SPI是Service Provider Interface的缩写。
  • 上述SPI的具体实现则由不同的Provider来提供。比如JDK会提供一组默认的实现(当然,这些实现以前是由Sun公司,现在由Oracle提供。在这个时候,前面提到的政府管制就能插一脚了)。注意,Provider可以提供一组Service的实现,也可以提供单个Service的实现。

注意,可互操作性是指Provider A实现的MD5值,能被Provider B识别。显然,这个要求是合理的。

图1所示为JCE中一些类的定义:


图1  JCE中一些类的定义

图1展示了JCE中的一些类定义。大家先关注左下角的Provider和Security类:

  • Security是java.security包的主要管理类。通过它可向JCE框架注册不同的Provider。
  • Android平台上,每一个应用程序在启动的时候都会默认注册一个类型为AndroidKeyStoreProvider的对象。

注意,由于历史原因,JCE的Service分散定义在好些个Package中:

  • java.security:此Package包含最多的Service定义。
  • javax.crypto:此package包含加解密相关的Service定义。

上述这两个package都属于JCE的内容。从使用者的角度来看,其实不太需要关注它们到底定义在哪个Package中(代码中通过一句import xxx就可以把对应的包引入,后续编写代码时候,直接使用相关类就好了)。

BTW,个人感觉Java类定义非常多,而且有些类和它们所在包的关系似乎有些混乱。

 

(1)  Android平台中Provider的注册

前面已经说过,JCE框架对使用者提供的是基础类(抽象类或接口类),而具体实现需要有地方注册到JCE框架中。所以,我们来看看Android平台中,JCE框架都注册了哪些Provider:

[-->Security.java]

static {

   boolean loaded = false;

   try {

       //从资源文件里搜索是否有security.properties定义,Android平台是没有这个文件的

      InputStreamconfigStream =

         Security.class.getResourceAsStream("security.properties");

       InputStream input = newBufferedInputStream(configStream);

       secprops.load(input);

       loaded = true;

       configStream.close();

        }......

     if (!loaded) {//注册默认的Provider

        registerDefaultProviders();

     }

      ......

    }

 

   private static void registerDefaultProviders() {

     /*

    JCE对Provider的管理非常简单,就是将Provider类名和对应的优先级存在属性列表里

    比如下面的:OpenSSLProvider,它对应的优先级是1.

     优先级是什么意思呢?前面说过,不同的Provider可以实现同一个功能集合,比如都实现

     MessageDigestSpi。那么用户在创建MessageDigest的实例时,如果没有指明Provider名,

     JCE默认从第一个(按照优先级,由1到n)Provider开始搜索,直到找到第一个实现了

     MessageDigestSpi的Provider(比如Provider X)为止。然后,MessageDigest的实例

      就会由Provider X创建。图2展示了一个例子

    */

     secprops.put("security.provider.1",

           "com.android.org.conscrypt.OpenSSLProvider");

     secprops.put("security.provider.2",

            "org.apache.harmony.security.provider.cert.DRLCertFactory");

    secprops.put("security.provider.3",

       "com.android.org.bouncycastle.jce.provider.BouncyCastleProvider");

    secprops.put("security.provider.4",

        "org.apache.harmony.security.provider.crypto.CryptoProvider");

      //JSSE有关

      secprops.put("security.provider.5",

        "com.android.org.conscrypt.JSSEProvider");

 }

图2展示了Provider优先级使用的例子:


图2  Provider优先级示例

图2中:

  • 左边的Application要使用MD5算法来创建一个MessageDigest实例。而左下角有三个Provider(A、B、C)。其中B和C支持MD5算法。所以JCE会使用第一个实现MD5算法的Provider(即Provider B)来创建MessageDigest实例。
  • 右边的Application也要使用MD5算法来创建MD实例。但是它在参数中指明了Provider的名字(“ProviderC”),所以JCE就会直接使用ProviderC来创建这个MD实例。

注意,图2中的SHA-1/256和MD5都是MessageDigest的一种算法,本文后面会介绍它们。

(2) AndroidKeyStoreProvider

Android平台中,每个应用程序启动时都会注册一个类型为“AndroidKeyStoreProvider”的对象。这是在ActivityThread中完成的,代码如下所示:

[-->ActivityThread.java::main]

public static void main(String[] args) {

   ......

  Security.addProvider(new AndroidKeyStoreProvider());

  ......

}

来看看AndroidKeyStoreProvider是什么,代码如下所示:

[-->AndroidKeyStoreProvider::AndroidKeyStoreProvider]

public class AndroidKeyStoreProvider extends Provider {

  public static final StringPROVIDER_NAME = "AndroidKeyStore";

  publicAndroidKeyStoreProvider() {

    super(PROVIDER_NAME, 1.0,"Android KeyStore security provider");

    put("KeyStore." + AndroidKeyStore.NAME,

          AndroidKeyStore.class.getName());

    put("KeyPairGenerator.RSA",AndroidKeyPairGenerator.class.getName());

  }

}

AndroidKeyStoreProvider很简单,但是看上去还是不明白它是干什么的?其实,这个问题的更深层次的问题是:Provider是干什么的?

  • Provider从Properties类派生。Properties本质上是一个属性列表,也就是它保存了一串的属性名和对应的属性值。
  • 从图2的例子可知,应用程序只说要创建MD5的MessageDigest实例。在代码中,“MD5”是一个字符串,而MessageDigest实例肯定是由某个类创建的(假设这个类名叫com.md5.test)。那么,这个“MD5”字符串就是一个Key,对应的属性值就是这个类的类名(“com.md5.test”)。JCE根据属性key,找到属性value,然后创建这个类的实例就完成了工作!

当然,Provider的内容和功能比这要复杂,不过我们对Provider的实现没什么兴趣,大家只要知道它存储了一系列的属性key和value就可以了。JCE会根据情况去查询这些key和对应的value。

(3)  实例介绍

来个例子,看看Android系统上都有哪些Provider:

[-->DemoActivity.java::testProvider]

void testProvider(){

   e(TAG, "***Begin TestProviders***");

   //获取系统所有注册的Provider

   Provider[] providers = Security.getProviders();

   for(Providerprovider:providers){

      //输出Provider名

      e(TAG,"Provider:" + provider+" info:");

      //前面说了,provider其实就是包含了一组key/value值。下面将打印每个Provider的

      //这些信息

      Set<Entry<Object,Object>>allKeyAndValues = provider.entrySet();

     Iterator<Entry<Object, Object>> iterator =allKeyAndValues.iterator();

      while(iterator.hasNext()){

        Entry<Object,Object> oneKeyAndValue =iterator.next();

        Object key = oneKeyAndValue.getKey();

        Object value =oneKeyAndValue.getValue();

        //打印Key的类型和值

       e(TAG,"===>" + "Keytype="+key.getClass().getSimpleName()+"

                           Key="+key.toString());

        //打印Value的类型和值

      e(TAG,"===>" + "Valuetype="+value.getClass().getSimpleName()+"

                           Value="+value.toString());

        }

     }

     e(TAG, "***End TestProviders***\n\n");

 }

在控制台中,通过adb logcat | grep ASDemo就可以显示testProvider的输出信息了,如图3所示:


图3  testProvider输出示例

图3打出了AndroidOpenSSLprovider的信息:

  • 其中Key和Value的类型都是String。
  • Key的值其实都是JCE中一些Service或算法的名称或别名。此处先不讨论这些细节,以后碰到再说。

了解完JCE框架后,我们分别来介绍JCE中的一些重要Service。

1.1.2  Key知识介绍

谈到安全,大家第一想到的就是密钥,即Key。那么大家再仔细想想下面这两个问题:

  • Key从何而来?即代码中怎么创建Key。
  • Key怎么传递给外部使用者?外部使用者可能是一个开发者,这时候就不能把代码中的一个对象通过email发给他了,而是要把Key的书面表达形式(参考资料[1]把它叫做externalrepresentations,即外部表达形式。我为了强调这种表达形式,把它称为书面表达形式。即可有把Key写到文档里,发给其他人)发给人家。

图4解释了上述问题:


图4  Key示意

图4中:

  • Key怎么创建?在JCE中是通过Generator类创建的,这时候在代码中得到的是一个Key实例对象。
  • Key怎么传递?这就涉及到如何书面表达Key了,最最常用的方式就是把它表示成16进制(比如图4中下部Encoded Key Data“0AF34C4E56...”)。或者,因为Key产生是基于算法的,这时候就可以把参与计算的关键变量的值搞出来。比如图4右上角的“Param P=3,ParamQ=4”。所以,Key的书面表达形式有两种,一种是16进制密钥数据,一种是基于算法的关键变量(这种方式叫KeySpecification)。
  • 此后,我们可以把16进制或者关键变量发给对方。对方拿到Key的书面表达式后,下一步要做的就是还原出代码中的key对象。这时候要用到的就是KeyFactory了。所以,KeyFactory的输入是Key的二进制数据或者KeySpecification,输出就是Key对象。

在安全领域中,Key分为两种:

  • 对称Key:即加密和解密用得是同一个Key。JCE中,对称key的创建由KeyGenerator类来完成。
  • 非对称Key:即加密和解密用得是两个Key。这两个Key构成一个Key对(KeyPair)。其中一个Key叫公钥(PublicKey),另外一个Key叫私钥(PrivateKey)。公钥加密的数据只能用私钥解密,而私钥加密的数据只能用公钥解密。私钥一般自己保存,而公钥是需要发给合作方的。(此处我们还不讨论公钥和私钥的使用场景,仅限介绍公钥和私钥的概念)。JCE中,非对称Key的创建由KeyPairGenerator类来完成。

图5所示为JCE中Key相关的类和继承关系。


图5  JCE Key相关类

图5中:

  • PublicKey,PrivateKey和SecretKey都派生自Key接口。所以,这三个类也是接口类,而且没有定义新的接口。
  • DSAPublicKey和RSAPublicKey也派生自PublicKey接口。DSA和RSA是两种不同的算法。
(1)  Key实例介绍

先来看对称key的创建和导入(也就是把Key的书面表达导入到程序中并生成Key对象)

[-->DemoActivity.java::testKey]

{//对称key即SecretKey创建和导入

     //假设双方约定使用DES算法来生成对称密钥

     e(TAG,"==>secret key: generated it using DES");

     KeyGeneratorkeyGenerator = KeyGenerator.getInstance("DES");

     //设置密钥长度。注意,每种算法所支持的密钥长度都是不一样的。DES只支持64位长度密钥

     //(也许是算法本身的限制,或者是不同Provider的限制,或者是政府管制的限制

     keyGenerator.init(64);

     //生成SecretKey对象,即创建一个对称密钥

     SecretKey secretKey = keyGenerator.generateKey();

     //获取二进制的书面表达

     byte[] keyData =secretKey.getEncoded();

     //日常使用时,一般会把上面的二进制数组通过Base64编码转换成字符串,然后发给使用者

     String keyInBase64 =Base64.encodeToString(keyData,Base64.DEFAULT);

     e(TAG,"==>secret key: encrpted data ="+ bytesToHexString(keyData));

     e(TAG,"==>secrety key:base64code=" + keyInBase64);

     e(TAG,"==>secrety key:alg=" + secretKey.getAlgorithm());

    

    //假设对方收到了base64编码后的密钥,首先要得到其二进制表达式

   byte[] receivedKeyData =Base64.decode(keyInBase64,Base64.DEFAULT);

   //用二进制数组构造KeySpec对象。对称key使用SecretKeySpec类

   SecretKeySpec keySpec =new SecretKeySpec(receivedKeyData,”DES”);

   //创建对称Key导入用的SecretKeyFactory

   SecretKeyFactorysecretKeyFactory = SecretKeyFactory.getInstance(”DES”);

   //根据KeySpec还原Key对象,即把key的书面表达式转换成了Key对象

   SecretKey receivedKeyObject = secretKeyFactory.generateSecret(keySpec);

   byte[]encodedReceivedKeyData = receivedKeyObject.getEncoded();

   e(TAG,"==>secret key: received key encoded data ="

                                +bytesToHexString(encodedReceivedKeyData));

如果一切正常的话,红色代码和绿色代码打印出的二进制表示应该完全一样。此测试的结果如图6所示:


图6  SecretKey测试结果

此处有几点说明:

  • 对称key的创建有不同的算法支持,根据参考资料[1],一般有Blowfish, DES, DESede,HmacMD5,HmacSHA1,PBEWithMD5AndDES, and PBEWithMD5AndTripleDES这些算法。但是Android平台,甚至不同手机是否都支持这些算法,则需要在testProvider那个例子去查询。
  • 另外,KeyGenerator如果支持上面的算法,但是SecretKeyFactory则不一定支持。比如ScreteKeyFactory则不支持HmacSHA1。

注意,本文会讨论太多算法相关的内容。

再来看KeyPair的用例:

[-->DemoActivity.java::KeyPair测试]

{//public/private key test

  e(TAG, "==>keypair: generated it using RSA");

  //使用RSA算法创建KeyPair

  KeyPairGeneratorkeyPairGenerator = KeyPairGenerator.getInstance("RSA");

  //设置密钥长度

  keyPairGenerator.initialize(1024);

  //创建非对称密钥对,即KeyPair对象

  KeyPair keyPair =keyPairGenerator.generateKeyPair();

  //获取密钥对中的公钥和私钥对象

  PublicKey publicKey =keyPair.getPublic();

  PrivateKey privateKey =keyPair.getPrivate();

  //打印base64编码后的公钥和私钥值

  e(TAG,"==>publickey:"+bytesToHexString(publicKey.getEncoded()));

  e(TAG, "==>privatekey:"+bytesToHexString(privateKey.getEncoded()));

 

  /*

   现在要考虑如何把公钥传递给使用者。虽然可以和对称密钥一样,把二进制数组取出来,但是

   对于非对称密钥来说,JCE不支持直接通过二进制数组来还原KeySpec(可能是算法不支持)。

   那该怎么办呢?前面曾说了,除了直接还原二进制数组外,还可以通过具体算法的参数来还原

   RSA非对称密钥就得使用这种方法:

   1 首先我们要获取RSA公钥的KeySpec。

  */

  //获取RSAPublicKeySpec的class对象

  Class spec = Class.forName("java.security.spec.RSAPublicKeySpec");

   //创建KeyFactory,并获取RSAPublicKeySpec

  KeyFactory keyFactory = KeyFactory.getInstance("RSA");

  RSAPublicKeySpecrsaPublicKeySpec =

            (RSAPublicKeySpec)keyFactory.getKeySpec(publicKey, spec);

  //对RSA算法来说,只要获取modulusexponent这两个RSA算法特定的参数就可以了

  BigInteger modulus =rsaPublicKeySpec.getModulus();

  BigInteger exponent =rsaPublicKeySpec.getPublicExponent();

  //把这两个参数转换成Base64编码,然后发送给对方

  e(TAG,"==>rsa pubkey spec:modulus="+

             bytesToHexString(modulus.toByteArray()));

  e(TAG,"==>rsa pubkey spec:exponent="+

             bytesToHexString(exponent.toByteArray()));

 

  //假设接收方收到了代表modulus和exponent的base64字符串并得到了它们的二进制表达式

  byte[] modulusByteArry =modulus.toByteArray();

  byte[] exponentByteArry =exponent.toByteArray();

  //由接收到的参数构造RSAPublicKeySpec对象

  RSAPublicKeySpecreceivedKeySpec = new RSAPublicKeySpec(

                    newBigInteger(modulusByteArry),

                    new BigInteger(exponentByteArry));

  //根据RSAPublicKeySpec对象获取公钥对象

  KeyFactoryreceivedKeyFactory = keyFactory.getInstance("RSA");

  PublicKey receivedPublicKey =

                 receivedKeyFactory.generatePublic(receivedKeySpec);

  e(TAG, "==>received pubkey:"+

                   bytesToHexString(receivedPublicKey.getEncoded()));

}

如果一切正常的话,上述代码中红色和黑色代码段将输出完全一样的公钥二进制数据。如图7所示:


图7  KeyPair测试示意图

在Android平台的JCE中,非对称Key的常用算法有“RSA”、“DSA”、“Diffie−Hellman”、“Elliptic Curve (EC)”等。

(2)  Key知识小结

我自己在学习Key的时候,最迷惑的就是前面提到的两个问题:

Key是什么?虽然“密钥”这个词经常让我联想到实际生活中的钥匙,但是在学习JavaSecurity之前,我一直不知道在代码中(或者编程时),它到底是个什么玩意。并且,它到底怎么创建。

创建Key以后,我怎么把它传递给其他人。就好比钥匙一样,你总得给个实物给人家吧?

现在来看这两个问题的总结性回答:):

  • JCE中,Key根据加密方法的不同,分为对称Key和非对称Key两大类。其中对称Key由类SecretKey表达,而非对称Key往往是公钥和私钥构成一个密钥对。在程序里,他们的类型分别是PublicKey、PrivateKey和KeyPair。
  • SecretKey和KeyPair都有对应的Generator类来创建。其中,创建对称Key的是KeyGenerator,创建KeyPair的是KeyPairGenerator。一方创建Key后,怎么把密钥信息传递给其他人呢?这时就需要用到Key的外部表现形式了(我把它叫做书面表达形式)。
  • Key的书面表达式有两种,一种是直接把它的二进制数组搞出来,然后编码成base64发给对方。对方从这个base64编码字符串中得到二进制数组。这个二进制数组叫KeyEncoded Data。怎么把它转换成代码中的Key实例呢?对于对称Key来说,先用这个二进制数组构造一个SecretKeySpec,然后再用SecretKeyFactory构造出最终的SecretKey对象。
  • Key的另外一种表示式就是利用创建Key的算法。这个主要针对非对称Key。Key创建时,总是需要依赖一些特定的算法,而这些算法也会有一些参数。类似于学数学时,算法对应于一个公式,我们只要把参数值带入进去就能得到结果一样。所以,在这种情况下,我们只要把参数保存起来,然后传递给对方可以了。对方拿到这些个参数,再构造出对应算法所需要的KeySpec对象,最后由KeyPairFactory创建最终的Key对象。

不理解上述内容的同学,请把实例代码再仔细看看!

 

1.1.3  Certificates知识介绍

JCE中,Certificates(是复数喔!)是证书之意。“证书”也是一个日常生活中常用的词,但是在JCE中,或者说在Java Security中,它有什么用呢?

这个问题的答案还是和Key的传递有关系。前面例子中我们说,创建密钥的人一般会把Key的书面表达形式转换成Base64编码后的字符串发给使用者。使用者再解码,然后还原Key就可以用了。

上面这个流程本身没有什么隐患,但是,是不是随意一个人(一个组织,一个机构等等等等)给你发一个key,你就敢用呢?简单点说,你怎么判断是否该信任给你发Key的某个人或某个机构呢?

好吧,这就好比现实生活中有个人说自己是警察,那你肯定得要他掏出警官证或者什么别的东西来证明他是警察。这个警官证的作用就是证书的作用。

一般而言,我们会把Key的二进制表达式放到证书里,证书本身再填上其他信息(比如此证书是谁签发的,什么时候签发的,有效期多久,证书的数字签名等等)。

初看起来,好像证书比直接发base64字符串要正规点,一方面它包含了证书签发者的信息,而且还有数字签名以防止内容被篡改。

但是,证书解决了信任的问题吗?很显然是没有的。因为证书是谁都可以制作的。既然不是人人都可以相信,那么,也不是随便什么证书都可以被信任。

怎么办?先来看现实生活中是怎么解决信任问题的。

现实生活中也有很多证书,大到房产证、身份证,小到离职证明、介绍信。对方怎么判断你拿的这些证是真实有效的呢?

对头,看证书是谁/或者哪个机构的“手墨”!比如,结婚证首先要看是不是民政局的章。那....民政局是否应该被信任呢???

好吧。关于民政局是否应该被信任的这个问题在技术上基本无解,它是一个死循环。因为,即使能找到另外一个机构证明民政局合法有效,但这个问题依然会顺流而上,说民政局该被信任的那个机构其本身是否能被信任呢?....此问题最终会没完没了的问下去。

那怎么办?没什么好办法。只要大家公认民政局能被信任,就可以了。

同理,对证书的是否可信任问题而言:

  • 首先,在全世界范围内(或者是一个国家,一个地区)设置一些顶级证书签发机构,凡是由这些证书签发机构(CertificateAuthorities)签发的证书,我们要无条件的信任(不考虑其他伪造等因素,证书是否被篡改可通过数字签名来判断)。就和民政局似的,不信民政局还信谁?:)。
  • 这么多证书要发,顶级CA肯定忙不过来,所以还可以设立各种级别的CA,比如北京市的民政局搞一个CA,湖南省的公安厅搞一个CA。这种级别的CA是否该无条件信任呢?不一定。看你对它的了解。anyway,这个级别的CA可以把自己拿到顶级CA那去认个证。比方说,湖南省公安厅发了一个证书,它可以到顶级CA那去盖个章。由于顶级CA是无条件信任的,所以湖南省公安厅这个证书可以被信任。
  • 公司A要发证书给客户使用,最好是先拿到上一级或者相关领域的CA(不一定是顶级CA)那去盖个章。最后,公司A把盖了章的证书a发给客户使用就可以了。

客户拿到证书a后,首先要检查下:

  • 客户拿到证书a后,发现是某个CA签发的。如果是顶级CA签发的,那好办,直接信任。如果不是顶级CA签发的。客户再去查看这个发证的CA能不能被信任,溯流而上,直到跟踪到顶级CA为止。所以,证书,往往不是一个证书的事情,而是一个证书链的事情!
  • 另外,如果客户本身就信任公司A,那其实公司A也不需要去找CA认证,直接把证书a给客户就可以了。当然,这个时候的证书a就不需要CA的章了。

......唧唧歪歪半天,其实关于证书的核心问题就一个:

证书背后往往是一个证书链。

  • 大家不要小看这个问题。证书链经常会把问题搞复杂。比如甲方有CA A签发的证书,而乙方有CA B签发的证书。那么甲乙双方要互相信任的话,得把甲乙双方证书链上的证书都搞过来校验一遍,直到顶级CA为止。当然,甲乙证书链的顶级CA可能还不是同一个。

.......

为了方便,系统(PC,Android,甚至浏览器)等都会把一些顶级CA(也叫Root CA,即根CA)的证书默认集成到系统里。这些RootCA用作自己身份证明的证书(包含该CA的公钥等信息)叫根证书。根证书理论上是需要被信任的。以Android为例,它在libcore/luni/src/main/files/cacerts下放了150多个根证书(以Android 4.4为例),如图8所示:


图8  Android自带的根证书文件

我们随便打开一个根证书文件看看,如下所示:

[某证书文件的内容,用记事本打开即可]

-----BEGIN CERTIFICATE-----

MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMC

VVMxFDASBgNVBAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBD

ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9v

dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDAxMDExMTY0MTI4WhcNMjEwMTE0

MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dlbGxzIEZhcmdvMSww

KgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEvMC0G

A1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEi

MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n13

5zHCLielTWi5MbqNQ1mXx3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHE

SxP9cMIlrCL1dQu3U+SlK93OvRw6esP3E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4O

JgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5OEL8pahbSCOz6+MlsoCu

ltQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4jsNtlAHCE

AQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMB

AAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcB

CzAyMDAGCCsGAQUFBwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRw

b2xpY3kwDQYJKoZIhvcNAQEFBQADggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo

7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrvm+0fazbuSCUlFLZWohDo7qd/

0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0ROhPs7fpvcmR7

nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx

x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ

33ZwmVxwQ023tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s=

-----END CERTIFICATE-----

Certificate:  #下面是证书的明文内容

    Data:

        Version: 3 (0x2)

        Serial Number:971282334 (0x39e4979e)

    Signature Algorithm: sha1WithRSAEncryption

        Issuer: C=US, O=Wells Fargo, OU=Wells Fargo CertificationAuthority, CN=Wells Fargo Root Certificate Authority

        Validity

            Not Before: Oct11 16:41:28 2000 GMT

            Not After : Jan14 16:41:28 2021 GMT

        Subject: C=US, O=Wells Fargo, OU=Wells Fargo CertificationAuthority, CN=Wells Fargo Root Certificate Authority

        Subject Public Key Info:#Public KeyKeySpec表达式

            Public Key Algorithm: rsaEncryption  #PublicKey的算法

                Public-Key: (2048 bit)

                Modulus:

                   00:d5:a8:33:3b:26:f9:34:ff:cd:9b:7e:e5:04:47:

                   ce:00:e2:7d:77:e7:31:c2:2e:27:a5:4d:68:b9:31:

                   ba:8d:43:59:97:c7:73:aa:7f:3d:5c:40:9e:05:e5:

                    a1:e2:89:d9:4c:b8:3f:9b:f9:0c:b4:c8:62:19:2c:

                   45:ae:91:1e:73:71:41:c4:4b:13:fd:70:c2:25:ac:

                   22:f5:75:0b:b7:53:e4:a5:2b:dd:ce:bd:1c:3a:7a:

                   c3:f7:13:8f:26:54:9c:16:6b:6b:af:fb:d8:96:b1:

                    60:9a:48:e0:25:22:24:79:34:ce:0e:26:00:0b:4e:

                   ab:fd:8b:ce:82:d7:2f:08:70:68:c1:a8:0a:f9:74:

                   4f:07:ab:a4:f9:e2:83:7e:27:73:74:3e:b8:f9:38:

                   42:fc:a5:a8:5b:48:23:b3:eb:e3:25:b2:80:ae:96:

                    d4:0a:9c:c2:78:9a:c6:68:18:ae:37:62:37:5e:51:

                   75:a8:58:63:c0:51:ee:40:78:7e:a8:af:1a:a0:e1:

                   b0:78:9d:50:8c:7b:e7:b3:fc:8e:23:b0:db:65:00:

                   70:84:01:08:00:14:6e:54:86:9a:ba:cc:f9:37:10:

                   f6:e0:de:84:2d:9d:a4:85:37:d3:87:e3:15:d0:c1:

                   17:90:7e:19:21:6a:12:a9:76:fd:12:02:e9:4f:21:

                    5e:17

                Exponent: 65537 (0x10001)

        X509v3 extensions:

            X509v3 BasicConstraints: critical

                CA:TRUE

            X509v3Certificate Policies:

                Policy:2.16.840.1.114171.903.1.11

                  CPS:http://www.wellsfargo.com/certpolicy

 

    Signature Algorithm: sha1WithRSAEncryption  #数字签名,以后再讲

        d2:27:dd:9c:0a:77:2b:bb:22:f2:02:b5:4a:4a:91:f9:d1:2d:

        be:e4:bb:1a:68:ef:0e:a4:00:e9:ee:e7:ef:ee:f6:f9:e5:74:

        a4:c2:d8:52:58:c4:74:fb:ce:6b:b5:3b:29:79:18:5a:ef:9b:

        ed:1f:6b:36:ee:48:25:25:14:b6:56:a2:10:e8:ee:a7:7f:d0:

        3f:a3:d0:c3:5d:26:ee:07:cc:c3:c1:24:21:87:1e:df:2a:12:

        53:6f:41:16:e7:ed:ae:94:fa:8c:72:fa:13:47:f0:3c:7e:ae:

        7d:11:3a:13:ec:ed:fa:6f:72:64:7b:9d:7d:7f:26:fd:7a:fb:

        25:ad:ea:3e:29:7f:4c:e3:00:57:32:b0:b3:e9:ed:53:17:d9:

        8b:b2:14:0e:30:e8:e5:d5:13:c6:64:af:c4:00:d5:d8:58:24:

        fc:f5:8f:ec:f1:c7:7d:a5:db:0f:27:d1:c6:f2:40:88:e6:1f:

        f6:61:a8:f4:42:c8:b9:37:d3:a9:be:2c:56:78:c2:72:9b:59:

        5d:35:40:8a:e8:4e:63:1a:b6:e9:20:6a:51:e2:ce:a4:90:df:

        76:70:99:5c:70:43:4d:b7:b6:a7:19:64:4e:92:b7:c5:91:3c:

        7f:48:16:65:7b:16:fd:cb:fc:fb:d9:d5:d6:4f:21:65:3b:4a:

         7f:47:a3:fb

SHA1 Fingerprint=93:E6:AB:22:03:03:B5:23:28:DC:DA:56:9E:BA:E4:D1:D1:CC:FB:65

关于证书文件,还有一些容易混淆的事情要交待:

  • 前面讲过,证书有很多格式,但是目前通用格式为X.509格式。在上面的证书文件中,从Certificate这一行开始到文件最后,都是符合X.509格式的。那文件前面的“-----BEGINCERTIFICATE-----”到“-----END CERTIFICATE-----”是些什么?
  • X.509是证书的格式,但是证书和我们看到的文件还是有一些差异。证书需要封装在文件里。不同系统支持不同的证书文件,每种证书文件,所要求包含的具体的X.509证书内容也不一样。另外,某些证书文件还能包含除X.509之外的其他有用的数据。

下面是一些常见的证书文件格式,一般用文件后缀名标示。

  • .pem(Privacy-enhanced ElectronicMail) Base64 编码的证书,编码信息(即将示例中X.509证书的明文内容用Base64编码后得到的那个字符串)放在"-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----"之间。所以,Android平台里的根CA文件都是PEM证书文件。
  • .cer,.crt, .der:证书内容为ASCII编码,二进制格式,但也可以和PEM一样采用base64编码。
  • .p7b,.p7c – PKCS#7(Public-Key CryptographyStandards ,是由RSA实验室与其它安全系统开发商为促进公钥密码的发展而制订的一系列标准,#7表示第7个标准,PKCS一共有15个标准)封装的文件。其中,p7b可包含证书链信息,但是不能携带私钥,而p7c只包含证书。
  •  .p12– PKCS#12标准,可包含公钥或私钥信息。如果包含了私钥信息的话,该文件内容可能需要输入密码才能查看。

特别注意:

1  证书文件相关的规范特别多,读者感兴趣的话可阅读参考文献[2][3][4][5]。

2  X509证书规范本身是不包含私钥信息的。但是某些证书文件却可以,而且某些证书文件能包含一条证书链上所有证书的信息。

关于证书的理论知识,我们先介绍到这,只要大家心中有下面几个概念就可以了:

  • 证书包含很多信息,但一般就是各种Key的内容。
  • 证书由CA签发。为了校验某个证书是否可信,往往需要把一整条证书链都校验一把,直到根证书。
  • 系统一般会集成很多根证书,这样免得使用者自己去下载根证书了。
  • 证书自己的格式通用为X.509,但是证书文件的格式却有很多。不同的系统可能支持不同的证书文件。

注意:后面介绍KeyStore的时候,我们还会继续讨论证书文件的问题。

(1)  证书实例介绍

证书实例主要关注于拿到人家给的证书后,我们怎么导入进来使用:

[-->DemoActivity.java::testCertificate]

void testCertificate() {

    e(TAG, "***Begintest Certificates***");

    try {

      //在res/assets目录下放了一个名为“my-root-cert.pem”的证书文件

      AssetManagerassetManager = this.getAssets();

      InputStream inputStream= assetManager.open("my-root-cert.pem");

 

      //导入证书得需要使用CertificateFactory

      CertificateFactorycertificateFactory =

                      CertificateFactory.getInstance("X.509");

      /*

      从my-root-cert.pem中提取X509证书信息,并保存在X509Certificate对象中

      注意,如果一个证书文件包含多个证书(证书链的情况),generateCertificate将只返回

      第一个证书

      调用generateCertificates函数可以返回一个证书数组,

      */

      X509CertificatemyX509Cer =

          (X509Certificate)certificateFactory

              .generateCertificate(inputStream);

     //打印X509证书的一些信息。DN是Distinguished Name。DN通过设定很多项(类似于地址

     //一样,比如包括国家、省份、街道等信息)来唯一标示一个持有东西(比如发布此证书的机构等)

      e(TAG, "==>SubjecteDN:" + myX509Cer.getSubjectDN().getName());

      e(TAG,"==>Issuer DN:" + myX509Cer.getIssuerDN().getName());

      e(TAG,"==>Public Key:"

          +bytesToHexString(myX509Cer.getPublicKey().getEncoded()));

      inputStream.close();

    } ......

    }

 

    e(TAG, "***End testCertificates***");

  }

注意,CertificateFactory只能导入pemder格式的证书文件

(2)  证书文件格式讨论

证书格式和证书文件格式可能是最难搞清楚的了。根据参考资料[6],我把证书格式和证书文件格式的相关讨论整理如下:

  • X.509:这个是数字证书的规范。X.509只能携带公钥信息。
  • PKCS#7和#12是证书的封装格式。PKCS#7一般是把证书分成两个文件,一个公钥一个私钥,有PEM和DER两种编码方式。PEM比较多见,就是纯文本的,PKCS#7一般是分发公钥用,看到的就是一串可见字符串,扩展名经常是.crt,.cer,.key等。DER是二进制编码。
  • PKCS#12是“个人信息交换语法(Personal InfomationExchange)”。它可以用来将x.509的证书和证书对应的私钥打包,进行交换。比如在windows下,可以将IE里的证书连带私钥导出,并设置一个口令保护。这个pfx格式的文件,就是按照pkcs#12的格式打包的。当然pkcs#12不仅仅只是作以上用途的。它可以用来打包交换任何信息。pfx文件可以含有私钥,同时可以有公钥,有口令保护。

还是有点模模糊糊的感觉,我个人体会是:

  • X.509包含证书的基本信息。
  • PKCS#7和12包含更多的一些信息。PKCS#12由于可包含私钥信息,而且文件本身还可通过密码保护,所以更适合信息交换。

图9是我在Ubuntu上打开一个p12文件的时候,系统提示需要输入密码:


图9  打开p12文件时候提示输入密码

 

1.1.4  Key管理

现实生活中,人这一生是会持有或使用很多证件的,比如身份证、房产证、结婚证,党员证,学生证等。显然,这些证件是需要好好保管的。此情况反应到JCE中,就引出了Key Management这个东西。

JCE中,Key管理有几个基本概念:

  • keystore:keystore就是存储Key的一个文件(也可以不是文件,但总之是存储,管理Key的一个东西。简单点说,Key就存在KeyStore里,就好像我们用一个保险箱存储了很多证件一样)。JCE为KeyStore设置了一些API,通过这些API,我们可以操作一个KeyStore。
  • alias:别名。在KeyStore中,每一个存储项都对应有一个别名。说白了,别名就是方便你找到Key的。
  • KeyStore里边存储的东西可分为两种类型。一种存储类型叫Key Entry:KE可携带KeyPair,或者SecretKey信息。如果KE存储的是KeyPair的话,它可能会携带一整条证书链信息。另外一种存储类型是Certificate Entry:CE用于存储根证书。根证书只包含公钥。而且CE一般对应的是可信任的CA证书,即顶级CA的证书。这些证书存储在xxx/cacerts目录下。比如前面见到的Android平台中的那150多个根证书。
  • KeyStore的实现类型:在JCE中,KeyStore也是一个服务(或者叫Engine),而其实现有不同的算法。常见的有JKS, JCEKS,and PKCS12。其中功能比较全的是JCEKS。而PKCS12一般用于导入p12的证书文件。注意,Android平台也实现了一个KeyStore,比如我们在“AndroidKeyStoreProvider”一节介绍的AndroidKeyStore。

Java平台提供了一个工具可以用来创建或者管理KeyStore,这个工具叫Keytool。本文我们不对它进行介绍。感兴趣的同学们请阅读参考文献[1][7]。

(1)  KeyStore实例

[-->DemoActivity.java::testKeyStore]

 void testKeyStore() {

    e(TAG, "***Begintest KeyStore***");

    try {

      AssetManagerassetManager = this.getAssets();

      //assets目录下放了一个pkcs12的证书文件

      InputStream inputStream= assetManager.open("test-keychain.p12");

     //创建KeyStore实例

      KeyStore myKeyStore = KeyStore.getInstance("PKCS12");

     /*

      KeyStore实例默认是不关联任何文件的,所以需要用keystore文件来初始化它

      load函数:第一个参数代表keystore文件的InputStream

      第二个参数是keystore文件的密码。和保险箱类似,KeyStore文件本身是用密码保护的

      一些KeyStore文件的默认密码叫“changeit”。

      如果不传密码的话,KeyStore初始化后,只能取出公开的证书信息。注意,不同KeyStore

      实现方法对此处理情况不完全相同。

     */

      myKeyStore.load(inputStream,"changeit".toCharArray());

      //就本例而言,KeyStore对象所代表KeyStore实际上就是test-keychain.p12文件

      //获取KeyStore中定义的别名

     Enumeration<String> aliasEnum = myKeyStore.aliases();

      while(aliasEnum.hasMoreElements()) {

        String alias =aliasEnum.nextElement();

        //判断别名对应的项是CE还是KE。注意,CE对应的是可信任CA的根证书。

        boolean bCE =myKeyStore.isCertificateEntry(alias);

        boolean bKE =myKeyStore.isKeyEntry(alias);

       //本例中,存储的是KE信息

       e(TAG,"==>Alias:"+alias + " is CE:"+bCE + "is KE:"+bKE);

        //从KeyStore中取出别名对应的证书链

        Certificate[]certificates = myKeyStore.getCertificateChain(alias);

 

       //打印证书链的信息

        for (Certificate cert: certificates) {

          X509CertificatemyCert = (X509Certificate) cert;

          e(TAG,"==>I am a certificate:"  );

          e(TAG,"==>Subjecte DN:" + myCert.getSubjectDN().getName());

          e(TAG,"==>Issuer DN:" + myCert.getIssuerDN().getName());

          e(TAG,"==>Public Key:"+ bytesToHexString(myCert.getPublicKey()

                 .getEncoded()));

        }

        //取出别名对应的Key信息,一般取出的是私钥或者SecretKey

       // 注意,不同的别名可能对应不同的Entry。本例中,KECE都使用一样的别名

        Key myKey =myKeyStore.getKey(alias,"changit".toCharArray());

        if(myKey instanceof PrivateKey){

          e(TAG,"==>I am a private key:" +

                             bytesToHexString(myKey.getEncoded()));

        } else if(myKeyinstanceof SecretKey){

          if(myKey instanceofPrivateKey){

            e(TAG,"==>I am a secret key:" +

                             bytesToHexString(myKey.getEncoded()));

          }

        }

       

      }

    } ......

e(TAG, "***Endtest KeyStore***");

图10所示为testKeyStore的输出。


图10  testKeyStore实例

(2)  KeyStore知识小结

通过前述的知识点以及代码实例,读者可能觉得KeyStore有点名不副实啊。按理说,Android平台应该有一个统一的证件管理保险箱吧。怎么咱们这个实例是用一个p12文件来初始化KeyStore呢?

眼睛够毒的啊!是的,Android平台确实有一个系统范围内统一的KeyStore。由于Android代码也遵循JCE规范,所以这个统一的KeyStore是通过AndroidKeyStoreProvider注册上去的。只要在KeyStore.getInstance的参数传递“AndroidKeyStore”,你就能得到Android系统级别的KeyStore了。

系统级别的KeyStore有啥好处呢?很明显,当我们把一个证书导入到系统级别的KeyStore后,其他应用程序就可以使用了。而且,Android系统对这个KeyStore保护很好,甚至要求用硬件实现KeyStore保护!

注意,关于AndroidKeyStore的使用方法,请阅读参考资料[8].

1.1.5  Message Digest和Signature

过了KeyStore这一关,JCE后面的东西就比较简单了。现在来看看Message Digest和Signature。

(1)  MessageDigest

Message Digest(以后简写为MD)的中文翻译是消息摘要。摘要这一词大家应该不陌生。写完一篇论文后,前面要写一个论文摘要。论文摘要的目的其实是用短短几句话来描述论文的内容。这样,查阅者只要看完摘要就能了解整篇论文的大概意思了。

在Security里,MD其实和论文摘要的意思差不多:

  • 先有一个消息。当然,这里的消息可以是任何能用二进制数组表达的数据。
  • 然后用某种方法来得到这个消息的摘要。当然,摘要最好要独一无二,即相同的消息数据能得到一样的摘要。不同的消息数据绝对不能得到相同的摘要。

和论文摘要不同的地方是,人们看到论文摘要是能大概了解到论文是说什么的,但是看到消息摘要,我们肯定不能猜出原始消息数据。

那么,MD的作用何在呢?MD的作用是为了防止数据被篡改:

  • 数据发布者:对预发布的数据进行MD计算,得到MD值,然后放到一个公开的地方。
  • 数据下载者:下载数据后,也计算MD值,把计算值和发布者提供的MD值进行比较,如果一样就表示下载的数据没有被篡改。

JCE中,MD的使用比较简单,直接上例子:

[-->DemoActivity.java::testMessageDigest]

  void testMessageDigest(){

    e(TAG, "***Begintest MessageDigest***");

    try {

      //创建一个MD计算器,类型是MessageDigest,计算方法有MD5SHA

      MessageDigest messageDigest = MessageDigest.getInstance("MD5");

      //消息数据

      String  data = "This is a message:)";

      e(TAG,"==>Message is:" + data);

      //计算MD值时,只要不断调用MessageDigest的update函数即可,其参数是待计算的数据

      messageDigest.update(data.getBytes());

      //获取摘要信息,也是用二进制数组表达

      byte[] mdValue =messageDigest.digest();

      e(TAG,"==>MDValue is:"+bytesToHexString(mdValue));

      //重置MessageDigest对象,这样就能重复使用它

      messageDigest.reset();

     

      AssetManagerassetManager = this.getAssets();

      InputStream inputStream= assetManager.open("test-keychain.p12");

      //这次我们要计算一个文件的MD值

      e(TAG,"==>Message is a file:" + "test-keychain.p12");

      //创建一个DigestInputStream对象,需要将它和一个MD对象绑定

      DigestInputStreamdigestInputStream = new DigestInputStream(inputStream,

                         messageDigest);

      byte[] buffer = newbyte[1024];

      //读取文件的数据,DigestInputStream内部会调用MD对象计算MD值

     while(digestInputStream.read(buffer)> 0){

      }

      //文件读完了,MD也计算出来了

      mdValue =messageDigest.digest();

      e(TAG,"==>MDValue is:"+bytesToHexString(mdValue));

     

     digestInputStream.close();

      inputStream.close();

 

      /*

      MD值其实并不能真正解决数据被篡改的问题。因为作假者可以搞一个假网站,然后提供

      假数据和根据假数据得到的MD值。这样,下载者下载到假数据,计算的MD值和假网站提供的

      MD数据确实一样,但是这份数据是被篡改过了的。

      解决这个问题的一种方法是:计算MD的时候,输入除了消息数据外,还有一个密钥。

      由于作假者没有密钥信息,所以它在假网站上上提供的MD肯定会和数据下载者根据密钥+

      数据得到的MD值不一样。

      这种方法得到的MDMessage Authentication Code,简称MAC

     */

      e(TAG,"==>Calcualte MAC");

      //创建MAC计算对象,其常用算法有“HmacSHA1”和“HmacMD5”。其中SHA1和MD5是

      //计算消息摘要的算法,Hmac是生成MAC码的算法

      Mac myMac = Mac.getInstance("HmacSHA1");

      //计算my-root-cert.pem的MAC值

      inputStream =assetManager.open("my-root-cert.pem");

      //创建一个SecretKey对象

      KeyGeneratorkeyGenerator = KeyGenerator.getInstance("DES");

      keyGenerator.init(64);

      SecretKey key =keyGenerator.generateKey();

      //用密钥初始化MAC对象

      myMac.init(key);

 

      buffer = newbyte[1024];

      int nread = 0;

      //计算文件的MAC

      while ((nread =inputStream.read(buffer)) > 0) {

        myMac.update(buffer, 0, nread);

      }

      //得到最后的MAC

      byte[] macValue =myMac.doFinal();

      e(TAG, "==>MACValue is:" + bytesToHexString(macValue));

 

      inputStream.close();

 

    } ......

    e(TAG, "***End testMessageDigest***");

  }

图11为该示例运行的结果:


图11  testMessageDigest示意

在Ubuntu下,可用md5sum对test-keychain.p12文件计算MD值,其结果和图11的数据完全一致。

(2)  Signature

现在来看Signature。Signature的作用其实和MAC差不太多,但是它用得更广泛点。其使用步骤一般如下:

  • 数据发送者先计算数据的摘要,然后利用私钥对摘要进行签名操作,得到一个签名值。
  • 数据接收者下载数据和签名值,也计算摘要。然后用公钥对摘要进行操作,得到一个计算值。然后比较计算值和下载得到的签名值,如果一样就表明数据没有被篡改。

由于Signature用到了公钥和私钥,而公钥和私钥又可以通过证书来传播,所以只要部署了证书,数据验证就比较方便。

特别注意:

1  从理论上说,签名不一定是针对摘要的,也可以对原始数据计算签名。但是由于签名所涉及的计算量比较大,所以往往我们只对数据摘要进行签名。在JCE中,签名都针对MD而言。

2  为了防止证书被篡改,我们在证书里边往往也会包含签名信息。证书A的签名信息其实是上一级的CA(假设为B)利用B的私钥对证书A进行签名操作,然后把这个签名值放到证书A里边。如果要验证证书A是否被篡改的话,我们需要CA B的证书B,因为证书B会提供CA B的公钥。明白了吧?,所以证书校验会是一个链式的情况。当最后校验到根CA的时候,由于根CA是利用自己的私钥对自己的证书进行签名,然后把自己的公钥,签名放到根证书里边。所以这个链最后就会终止在根证书这了....

来看个例子:

[-->DemoActivity.java::testSignature]

   void testSignature(){

    e(TAG, "***Begintest Signature***");

    try {

      AssetManagerassetManager = this.getAssets();

       //本例中,私钥和公钥信息都放在test-keychain.p12文件中,我们先从里边提取它们

      InputStream inputStream= assetManager.open("test-keychain.p12");

     

      KeyStore myKeyStore =KeyStore.getInstance("PKCS12");

     myKeyStore.load(inputStream, "changeit".toCharArray());

     

      String alias = "MyKey Chain";

      Certificate cert =myKeyStore.getCertificate(alias);

      PublicKey publicKey =cert.getPublicKey();

 

      PrivateKey privateKey =(PrivateKey)myKeyStore.getKey(alias,

                              "changit".toCharArray());

      inputStream.close();

     

      //对my-root-cert.pem进行签名

      e(TAG, "==>start sign of file : my-root-cert.pem");

       //MD5表示MD的计算方法,RSA表示加密的计算方法。常用的签名算法还有“SHA1withRSA”

       //“SHA256withRSA”

      Signature signature =Signature.getInstance("MD5withRSA");

      //计算签名时,需要调用initSign,并传入一个私钥

      signature.initSign(privateKey);

      byte[] data = newbyte[1024];

      int nread = 0;

      InputStreaminputStreamToBeSigned = assetManager.open(

                             "my-root-cert.pem");

      while((nread =inputStreamToBeSigned.read(data))>0){

        signature.update(data, 0, nread);//读取文件并计算签名

      }

      //得到签名值

      byte[] sig = signature.sign();

     

      e(TAG, "==>Signed Signautre:" + bytesToHexStaring(sig));

      signature = null;

     inputStreamToBeSigned.close();

 

       //校验签名

      e(TAG, "==>start verfiy of file : my-root-cert.pem");

      inputStreamToBeSigned =assetManager.open("my-root-cert.pem");

      signature = Signature.getInstance("MD5withRSA");

      //校验时候需要调用initVerify,并传入公钥对象

      signature.initVerify(publicKey);

      data = new byte[1024];

      nread = 0;

      while((nread =inputStreamToBeSigned.read(data))>0){

        signature.update(data, 0, nread);//读取文件并计算校验值

      }

      //比较签名和内部计算得到的检验结果,如果一致,则返回true

      boolean isSigCorrect =signature.verify(sig);

      e(TAG, "==>IsSignature Correct :" + isSigCorrect);

     

     inputStreamToBeSigned.close();

     

    } catch (Exception e) {

      e(TAG," " +e.getMessage());

    }

    e(TAG, "***End testSignature***");

  }

图12所示为testSignature的结果:


图12  testSignature的结果

1.1.6  加解密

JCE的加解密就比较简单了,主要用到一个Class就是Cipher。Cipher类实例在创建时需要指明相关算法和模式(即Cipher.getInstance的参数)。根据JCE的要求:

  • 可以仅指明“算法”,比如“DES”。
  • 要么指明“算法/反馈模式/填充模式”(反馈模式和填充模式都和算法的计算方式有关),比如“AES/CBC/PKCS5Padding”。

JCE中,

  • 常见的算法有“DES”,“DESede”、“PBEWithMD5AndDES”、“Blowfish”。
  • 常见的反馈模式有“ECB”、“CBC”、“CFB”、“OFB”、“PCBC”。
  • 常见的填充模式有“PKCS5Padding”、“NoPadding”。

注意,算法、反馈模式和填充模式不是任意组合的。

具体这些算法、模式是干什么的,我个人觉得反倒不必要关注这么细,这毕竟是数学家的事情,不是么?

直接上例子吧:

[-->DemoActivity.java::testCipher]

  void testCipher(){

try {

      //加解密要用到Key,本例使用SecretKey进行对称加解密运算

      KeyGenerator  keyGenerator = KeyGenerator.getInstance("DES");

      SecretKey key =keyGenerator.generateKey();

     

      //待加密的数据是一个字符串

      String data ="This is our data";

      e(TAG, "==>RawData : " + data);

      e(TAG, "==>RawData in hex: " + bytesToHexString(data.getBytes()));

     //创建一个Cipher对象,注意这里用的算法需要和Key的算法匹配

      Cipher encryptor = Cipher.getInstance("DES/CBC/PKCS5Padding");

      //设置Cipher对象为加密模式,同时把Key传进去

      encryptor.init(Cipher.ENCRYPT_MODE, key);

      //开始加密,注意update的返回值为输入buffer加密后得到的数据,需要保存起来

      byte[] encryptedData =encryptor.update(data.getBytes());

      //调用doFinal以表示加密结束。doFinal有可能也返回一串数据,也有可能返回null。因为

      byte[] encryptedData1 =encryptor.doFinal();

     //finalencrpytedData为最终加密后得到的数据,它是update和doFinal的返回数据的

     //集合

      byte[]finalEncrpytedData =

             concateTwoBuffers(encryptedData,encryptedData1);

      e(TAG, "==>EncryptedData : " + bytesToHexString(finalEncrpytedData));

      //获取本次加密时使用的初始向量。初始向量属于加密算法使用的一组参数。使用不同的加密算法

     //时,需要保存的参数不完全相同。Cipher会提供相应的API

      byte[] iv = encryptor.getIV();

      e(TAG,"==>Initial Vector of Encryptor: " + bytesToHexString(iv));

     

      /*

         解密:解密时,需要把加密后的数据,密钥和初始向量发给解密方。

         再次强调,不同算法加解密时,可能需要加密对象当初加密时使用的其他算法参数

*/

      Cipher decryptor =Cipher.getInstance("DES/CBC/PKCS5Padding");

      IvParameterSpecivParameterSpec = new IvParameterSpec(iv);

      //设置Cipher为解密工作模式,需要把Key和算法参数传进去

      decryptor.init(Cipher.DECRYPT_MODE, key,ivParameterSpec);

     

      //解密数据,也是调用update和doFinal

      byte[] decryptedData =decryptor.update(finalEncrpytedData);

      byte[] decryptedData1 =decryptor.doFinal();

      //将数据组合到一起,得到最终的数据

      byte[]finaldecrpytedData =

               concateTwoBuffers(decryptedData,decryptedData1);

      e(TAG,"==>Decrypted Data  in hex:" +

                  bytesToHexString(finaldecrpytedData));

      e(TAG,"==>Decrypted Data  : " + newString(finaldecrpytedData));

    } ......

    e(TAG, "***End testCipher***");

  }

图13展示了testCipher的结果:


图13  testCipher结果示意

1.1.7  JCE总结

到此,JCE常见的知识和API就介绍到此:

  • 我个人觉得比较难理解的就是KeyStore以及证书等方面的知识。尤其是KeyStore。以后我们会介绍Android平台中的统一KeyStore,即AndroidKeyStore。
  • MD和Signature的作用也需要大家了解。
  • 另外,对于KeyPair来说,Public Key一般用于加密数据和校验签名,而Privatekey用于解密数据和生成签名(详情见参考资料[9])。

  1.2 JSSE介绍

JSSE是Java Secure Socket Extension的简写。这个名字已经非常明确的指出JSSE的目的了,就是在网络通讯的时候能更安全点。JSSE实际上Java平台对SSL/TLS的某种实现。SSL和TLS又是什么呢?

  • SSL是Secure Socket Layer的简写。它最早由NetScape公司提出来。说白了就是对通过Socket传输的数据进行加密保护。当然,这么好的东西最好是规范化。所以,现在,SSL是在TCP之上的一个协议。最新版本是1996年推出的3.0,。
  • TLS是Transport Layer Security的简写,它是IETF组织在SSL协议基础上做得一些修订,但是二者使用的版本号不一样。TLS1.0相当于SSL的3.1版本。2015年2月,TLS 1.3草案已经提出来了。关于SSL和TLS的历史,大家可阅读参考资料[10]。

总之,大家可以把SSL和TLS当做一个东西来看,反正就是在TCP 之上的一个安全协议。目前常用的地方是https,它就是基于SSL/TLS的。

SSL初看起来好像复杂得不得了,但实际上你如果掌握了前面的JCE的话,SSL还是比较好理解的。虽然我们前面用得是单独的小例子来说明JCE各个服务如何使用,但毕竟都是单独的。而SSL把这些东西用到了网络,用到了客户端、服务器当中。

图14是一个示例:


图14  SSL握手

图14中:客户端和服务器建立关系之前:

  • 互相告知对方自己支持什么加解密算法。
  • 服务器告诉客户端自己的身份(发证书给客户端,目的是让客户端验证服务端是否可信)。当然,某些应用场景下,客户端也必须把自己的身份发给服务器。
  • 服务器和客户端交换Key。
  • 后面二者的数据就通过Key进行加密传输,接收方再解密。

注意:

为什么不是所有场景都要求服务器验证客户端呢(即客户端把自己的证书发给服务器)呢?这是因为在C/S架构下,一个服务器要服务很多客户。每个客户都提供证书给服务器,服务器得存着啊。而且,假如客户端更新了证书,那服务器怎么处理呢?反正有很多现实问题不好处理,所有就没有强制要求客户端发证书给服务器了。

 

1.2.1  JSSE知识点

JCE中,我们见到过KeyStore,KeyStore就像个保险箱似的,能存储证书文件。在JSSE中,有一个类似的概念,叫TrustStore。TrustStore和KeyStore其实从编程角度看都是ClassKeyStore,二者的区别主要体现在用途上。

  • SSL中,客户端需要连接服务端。比如客户端是个浏览器,它可以连接很多很多服务器。那么,根据刚才所说,这些服务器需要把自己的证书发给浏览器的。浏览器收到证书后,需要验证它们。由于证书链的事情,最终都会验证到根证书那。前面也提到过,有很多根CA,根CA又可能签发不同用途的根证书。为了方便,系统往往会集成一些常用的根证书,放到一个统一的目录下(比如前面在Android提到的cacerts文件夹下放的150多个根证书文件),或者放到一个文件里(在台式机上,一般放在jre目录/lib/security/cacerts文件中)。有了TrustStore,我们就可以利用其中存储的根证书了。
  • 相比较而言,KeyStore一般存储的是私钥等相关信息。但从技术上说,KeyStore和TrustStore可以是同一个文件。这个由具体实现来定。在Android平台上,系统级的KeyStore(也就是前面提到的AndroidKeyStore)和TrustStore是同一个东西。

JSSE没有其他特别需要强调的知识点了,我们直接来看一个例子以了解JSSE相关API的用法:

1.2.2  JSSE API使用案例

这个案例分两个部分,一个是服务端,另外一个是客户端。服务端类似于一个echo服务器,即打印客户端发来的一个字符串。

由前述内容可知,服务端需要keystore,该keystore保存了私钥信息,因为服务端要用它来为证书签名。

[-->DemoActivity::startServer]

  private void startServer(){

    //Server单独跑在一个线程里

    Thread serverThread = newThread(new Runnable() {

      @Override

      public void run() {

        try {

          e(TAG,"==>prepare keystore for server:");

          ServerSocketFactoryserverSocketFactory = null;

         //下面这段代码在testKeyStore的时候介绍过。本例中,私钥,证书文件其实都存在

         //test-keychain.p12文件里

          AssetManager assetManager= DemoActivity.this.getAssets();

          InputStreamkeyInputStream = assetManager.open("test-keychain.p12");

          KeyStoreserverKeyStore = KeyStore.getInstance("PKCS12");

          //初始化KeyStore

          serverKeyStore.load(keyInputStream,"changeit".toCharArray());

         keyInputStream.close();

 

          //我们要用这个keystore来初始化一个SSLContext对象SSLContext使得我们

          //能够控制SSLSocket和KeyStore,TrustStore相关的部分,而不是使用系统默认值

          SSLContextsslContext = SSLContext.getInstance("TLS");

          KeyManagerFactorykeyManagerFactory = KeyManagerFactory

              .getInstance(KeyManagerFactory.getDefaultAlgorithm());

         //先用KeyStore对象初始化KeyManagerFactory

          keyManagerFactory.init(serverKeyStore,"changeit".toCharArray());

          /*

           然后初始化SSLContext,init函数有三个参数,第一个是KeyManager数组

           第二个是TrustManager数组,第三个是SecureRandom,用来创建随机数的

            显然,第一个参数用来创建服务端Socket的,而第二个参数用于创建客户端Socket

          */

          sslContext.init(keyManagerFactory.getKeyManagers(), null,null);

          e(TAG,"==>start server:");

          //得到服务端socket创建工厂对象

          serverSocketFactory= sslContext.getServerSocketFactory();

          //在localhost:1500端口号监听

          InetAddresslistenAddr = Inet4Address.getLocalHost();

          ServerSocketserverSocket = serverSocketFactory

              .createServerSocket(1500, 5,listenAddr);

          //启动客户端,待会再分析这个函数

          startClient();

          //接收数据并打印,然后关闭服务端

          Socket clientSock =serverSocket.accept();

          InputStreaminputStream = clientSock.getInputStream();

          byte[] readBuffer =new byte[1024];

          int nread =inputStream.read(readBuffer);

          e(TAG,"==>echo from Client:" + new String(readBuffer, 0, nread));

          clientSock.close();

         serverSocket.close();

        } ......

        e(TAG,"==>server quit");

      }

    });

    serverThread.start();

  }

从上述代码可知,SSL服务端创建的关键是先得到一个ServerSocketFactory。

特别注意:如果不使用SSLContext话,我们可以直接调用SSLServerSocketFactory的getDefault函数返回一个ServerSocketFactory。但是这个工厂创建的ServerSocket在accept的时候会出错。出错的原因是“Could not find anykey store entries to support the enabled cipher suites.”,也就是说,找不到Keystore有哪一个KE能支持所使用的加解密算法。报错的代码在libcore/crypto/src/main/java/org/conscrypt/SSLServerSocketFactory的checkEnabledCipherSuites函数中。这个函数会检查几种常见的Key类型(比如RSA等),然后检查KeyStore里有没有这种类型的PrivateKey,如果没有就报错。

其实,SSLServerSocketFactory内部也会查找和绑定一个KeyStore,这些操作和我们在示例代码中看到的几乎一样,只不过它们使用默认的KeyStore来创建罢了。为了保持Java的一致性,Android里边JCE的默认属性和PC上是一样的,它并没有利用Android系统统一的“AndroidKeyStore”替换。

注意:即使我们创建一个AndroidKeyStore类型的KeyStore传到SSLContext里,我们也无法使用。为什么?因为Android平台有自己的一套Key管理。我们后续分析代码的时候会见到。

下面来看客户端的代码:

[-->DemoActivity.java::startClient]

  private void startClient(){

    Thread clientThread = newThread(new Runnable() {

      @Override

      public void run() {

        try {

          e(TAG,"==>prepare client truststore");

          SocketFactorysocketFactory = null;

          //注意,如果我们把证书文件导入到Android系统后,就可以利用默认的设置来创建

          //客户端Socket工厂了,否则还得和服务端一样,自己绑定TrustStore

          if (isOurKeyChainAvailabe()) {

            e(TAG, "wehave installed key in the system");

            socketFactory =SSLSocketFactory.getDefault();

          } else {

            e(TAG,"prepare truststore manually");

            AssetManagerassetManager = DemoActivity.this.getAssets();

            InputStreamkeyInputStream = assetManager

               .open("test-keychain.p12");

           //客户端Socket工厂用TrustManagerFactory来构造

           TrustManagerFactory tmf = TrustManagerFactory

                .getInstance(TrustManagerFactory.getDefaultAlgorithm());

            KeyStore keyStore= KeyStore.getInstance("PKCS12");

           keyStore.load(keyInputStream, "changeit".toCharArray());

           keyInputStream.close();

            //用KeyStore来初始化TrustManagerFactory

            tmf.init(keyStore);

            //同样是创建一个SSLContext对象

            SSLContextsslContext = SSLContext.getInstance("TLS");

            //初始化SSLContext,这个时候只要传递第二个参数就可以了

            sslContext.init(null,tmf.getTrustManagers(), null);

            socketFactory =sslContext.getSocketFactory();

          }

 

          e(TAG,"==>start client:");

          //连接到服务端

          InetAddressserverAddr = Inet4Address.getLocalHost();

          Socket mySocket =socketFactory.createSocket(serverAddr,1500);

          OutputStreamoutputStream = mySocket.getOutputStream();

          //发送数据并退出

          String data ="I am client";

          e(TAG,"==>Client sent:" + data);

         outputStream.write(data.getBytes());

          mySocket.close();

        } ......

        e(TAG,"==>client quit");

      }

    });

    clientThread.start();

  }

注意,如果我们没有导入证书,并且也没有显示得绑定证书文件,那么在createSocket的时候会报错,报错的原因很简单,就是说本地的TrustStore里边没有证书来验证服务端的身份。为什么没有呢?因为服务端用的东西是自己签发的证书,也就是没有根CA来给它盖章。这样的证书发给了客户端,客户端默认使用系统TrustStore(名为“AndroidCAStore”)里边没有任何这个证书的信息,所以无法验证。

所以,客户端有两种做法:

  • 把证书导入到Android系统里边。这样默认的TrustStore就多了这一条信息。我们后文会讲这种方法的实现原理。
  • 代码中显示绑定证书文件到一个TrustStore。

图15展示了Android系统配置这个TrustStore的代码:


图15  TrustStore配置文件示意

/etc/security/cacerts目录对应的是TrustStore!图15对应的代码在TrustedCertificateStore.java中。

另外,参考文献[11]列出了和上述代码类似一些问题的解法。

1.3  小结

JCE/JSSE介绍就到此为止。在实际编程过程中,一个比较大的难点是如何选择匹配的算法,即xxx.getInstance的参数是什么。比如我在用RSA私钥加解密的时候,参数传得不对导致解密总是得不到正确的值。

另外,对Java加解密更深入知识感兴趣的童鞋,请参考[12]。

接下来,我们来看Android的KeyStore相关的知识。

 

参考文献

Java Security

[1]  Java Security, 2nd Edition:http://shop.oreilly.com/product/9780596001575.do  中文名为《Java安全第二版》,此书是关于Java Security最好的参考书。

http://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html

关于证书和证书文件格式

[2] http://www.360doc.com/content/13/0417/10/11791971_278827661.shtml

[3] http://www.blogjava.net/lihao336/archive/2011/08/18/356763.html

[4] http://en.wikipedia.org/wiki/PKCS

[5] http://en.wikipedia.org/wiki/X.509

X.509和PKCS介绍

[6]  http://bbs.csdn.net/topics/190044123

关于X.509和PCKS规范之间的关系的讨论

Key管理

[7]  《Java加密与解密的艺术》,作者梁栋,国人关于JavaSecurity的一本好书。

[8]  http://developer.android.com/training/articles/keystore.html

[9]   http://en.wikipedia.org/wiki/Public-key_cryptography

JSSE

[10]  http://en.wikipedia.org/wiki/Transport_Layer_Security

TLS和SSL的历史。

[11] https://developer.android.com/training/articles/security-ssl.html

Android开发文档中关于SSL方面的知识。

Cipher资料

[12]  http://www.javamex.com/tutorials/cryptography/index.shtml

 

 

共有 人打赏支持
邓凡平
粉丝 1407
博文 79
码字总数 488831
×
邓凡平
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: