第18课:Java

第18课:Java 操作公私钥和 SSL 证书

近两年炒的热火朝天的区块链技术里面,在生成客户端的钱包的时候,首先就需要生成一个公钥和私钥,因为公钥太长不好记,所以会把公钥进行一个签名,交易的时候,就通过公钥和公钥的签名进行用户身份的确认,同时私钥是唯一的只能由用户知道的秘密文件,一旦私钥被盗,账户里面的货币就会被转走。那么,如果和 Java 生成公钥和私钥呢?如何用 Java 来操作 SSL 证书呢?比如把多个 SSL 证书一起倒入到一个 JKS 文件。如何用 Java 代码来查看 SSL 证书和私钥呢?在这篇文章中,笔者将会和大家一起分享这方面的相关知识和代码,希望能够起到抛砖引玉的作用。

Java 操作公钥和私钥

Java 提供了一个非常重要的 package:java.security,其为安全框架提供了一些类和接口,包括实现可轻松配置的细粒度访问控制安全体系结构的类,该软件包还支持密码公钥对的生成和存储,以及许多可导出的加密操作,包括用于消息摘要和签名生成的操作。最后,这个包提供了支持签名/防护对象和安全随机数生成的类。该软件包中提供的许多类(特别是加密和安全随机数生成器类)都是基于提供程序的(Provider)。类本身定义了应用程序可以编写的编程接口。这些实现本身可以由独立的第三方供应商编写,并根据需要无缝插入。因此,应用程序开发人员可以利用任何数量的基于提供者的实现,而无需添加或重写代码。这个类真心觉得强大,好了,下面来具体看看代码,看其是如何生成公钥和私钥的。

得到 KeyPairGenerator 的实例

第一步使用 KeyPairGenerator 类的一个工厂方法得到 KeyPairGenerator 的实例对象。比如我们指定的算法为 RSA,代码如下:

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");

初始化私钥的长度为 2048 位

初始化私钥的长度,长度越长越难被破解,但是在进行加密和解密的时候,速度越慢;当前推荐,如果是用于 SSL 证书,推荐的长度为 2048 位。

kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();

得到公钥和私钥对象

从上面的 KeyPair 对象,分别调用 getPublic() 和 getPrivate() 方法就能得到公钥和私钥的对象:

Key publicKey= kp.getPublic();
Key privateKey= kp.getPrivate();

把公钥和私钥以二进制的形式进行保存

上面只是得到了私钥和公钥的对象,但是如何把其内容导出到文件里面呢?首先来看如何以二进制的形式进行导出。

String outFile = ...;
out = new FileOutputStream(outFile + ".key");
out.write(privateKey.getEncoded());
out.close();

out = new FileOutputStream(outFile + ".pub");
out.write(publicKey.getEncoded());
out.close();

值得一提的是,生成的私钥以 PKCS#8 格式保存,而生成的公钥以 X.509 格式保存。那么我是如何知道呢?当然就是通过下面的代码。

System.err.println("Private key format: " + privateKey.getFormat());

System.err.println("Public key format: " + publicKey.getFormat());

把私钥和公钥以 Base 64 的形式进行保存

上面的二进制形式可读性不强,而且很多支持 SSL 配置的服务器不支持二进制;且在网页或者其他客户端进行显示的时候,也不方便,所以需要使用 Base64 编码的格式,把二进制转成 Base64 的格式,关于什么是 Base64 的格式?下面引用知乎上的一个回答进行一下简单入门:

  • 标准 Base64 只有 64 个字符(英文大小写、数字和 +、/)以及用作后缀等号。
  • 等号一定用作后缀,且数目一定是0个、1个或2个。这是因为如果原文长度不能被3整除,Base64 要在后面添加\0凑齐3n位。为了正确还原,添加了几个\0就加上几个等号,显然添加等号的数目只能是0、1或2。
  • 严格来说 Base64 不能算是一种加密,只能说是编码转换。使用 Base64 的初衷是为了方便把含有不可见字符串的信息用可见字符串表示出来,以便复制粘贴。

具体代码如下:

(1)私钥的 Base64 导出

Base64.Encoder encoder = Base64.getEncoder();
String outFile = ...;
Writer out = new FileWriter(outFile + ".key");
out.write("-----BEGIN RSA PRIVATE KEY-----\n");
out.write(encoder.encodeToString(privateKey.getEncoded()));
out.write("\n-----END RSA PRIVATE KEY-----\n");
out.close();

(2)公钥的 Base64 导出

out = new FileWriter(outFile + ".pub");
out.write("-----BEGIN RSA PUBLIC KEY-----\n");
out.write(encoder.encodeToString(publicKey.getPublic()));
out.write("\n-----END RSA PUBLIC KEY-----\n");
out.close();

使用私钥进行文件签名

总所周知,公钥密码的目的之一是数字签名,即您可以从文件内容生成数字签名,用您的私钥签名并将签名与文件一起发送,然后收件人可以使用您的公钥来验证签名是否与文件内容匹配。

这是如何做到的呢?比如,使用签名算法“SHA256withRSA”,保证所有 JVM 都支持该算法。使用私钥(生成或从文件加载)初始化 Signature 对象进行签名,然后使用数据文件中的内容更新,生成签名并写入输出文件。此输出文件包含数字签名,必须发送给收件人进行验证,具体代码如下:

public static void  sign(String privateKeyFile,  String dataFile,String signFile){
    /* Read the private key bytes */
    Path path = Paths.get(privateKeyFile);
    byte[] bytes;
    try {
      bytes = Files.readAllBytes(path);
      PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(bytes);
      KeyFactory kf = KeyFactory.getInstance("RSA");
      PrivateKey pvt = kf.generatePrivate(ks);
      Signature sign = Signature.getInstance("SHA256withRSA");
      sign.initSign(pvt);
      InputStream in = null;
      try {
          in = new FileInputStream(dataFile);
          byte[] buf = new byte[2048];
          int len;
          while ((len = in.read(buf)) != -1) {
          sign.update(buf, 0, len);
          }
      } catch (SignatureException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      } finally {
          if ( in != null ) in.close();
      }

      OutputStream out = null;
      try {
          out = new FileOutputStream(signFile);
          byte[] signature = sign.sign();
          out.write(signature);
      } catch (SignatureException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      } finally {
          if ( out != null ) out.close();
      }

    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvalidKeySpecException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvalidKeyException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }

   }

使用公钥进行验证文件签名

收件人使用与数据文件一起发送的数字签名来验证数据文件是否未被篡改。它需要访问发件人的公钥,并且可以根据需要从文件中加载,如上所述。下面的代码使用数据文件中的数据更新 Signature 对象。然后它从文件加载签名并使用 Signature.verify() 检查签名是否有效。

public static boolean  verify(String publicKeyFile,  String dataFile,String signFile){

    /* Read the public key bytes */
    Path path = Paths.get(publicKeyFile);
    Signature sign=null;
    byte[] bytes;
    boolean isVerify=false;
    try {
      bytes = Files.readAllBytes(path);
      X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
      KeyFactory kf = KeyFactory.getInstance("RSA");
      PublicKey pub = kf.generatePublic(ks);
      sign = Signature.getInstance("SHA256withRSA");
      sign.initVerify(pub);

      InputStream in = null;
      try {
          in = new FileInputStream(dataFile);
          byte[] buf = new byte[2048];
          int len;
          while ((len = in.read(buf)) != -1) {
          sign.update(buf, 0, len);
          }
      } catch (SignatureException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      } finally {
          if ( in != null ) in.close();
      }

      /* Read the signature bytes */
      path = Paths.get(signFile);
      bytes = Files.readAllBytes(path);
      isVerify= sign.verify(bytes);
    } catch (IOException e) {
      e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvalidKeySpecException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvalidKeyException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (SignatureException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }  
    return isVerify;

  }

整个项目的已经放到了 Github 上,单击这里可以下载

导入 SSL 证书到 Java 的 keystore

我们在开发或者使用 SSL 的过程中,很多的软件需要我们提供 Java 的 keystore,特别是一些基于 Java 的中间件产品。我们常规的做法是 JDK 自带的工具命令(keytool)去做,比如,下面的例子:

keytool -import -v -alias EnTrust2048 -file D:\certs\EnTrust2048.cer -keystore D:\certs\test.jks

keytool -import -v -alias EntrustCertificationAuthorityL1C -file D:\certs\EntrustCertificationAuthorityL1C.cer -keystore D:\certs\test.jks

keytool -import -v -alias test.com -file D:\certs\Service-now.com.cer -keystore D:\certs\test.jks

但是这种方式比较繁琐,假设我们一个文件夹下面有 100 个 SSL 的证书,那么我们就要输入 100 个类似于上面的命令。如果是文件夹里面套文件夹里面还有证书,就更麻烦,那么有没有好的办法呢?笔者就跟大家分享一下如何用 Java 的程序代码去实现。

import java.io.File;  
import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.security.KeyStore;  
import java.security.cert.CertificateFactory;  
import java.security.cert.X509Certificate;  
import java.util.List;  
import javax.naming.ldap.LdapName;  
import javax.naming.ldap.Rdn;  
import javax.security.auth.x500.X500Principal;  

public class KeyStoreHelper {  
    public static void createTrustJKSKeyStore(final String originalTrustFolder,  
            final String jksTrustStoreLocation, final String password) {  
        File keyStoreFile = new File(jksTrustStoreLocation);  
        if (!keyStoreFile.exists()) {  
            try {  
                KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());  
                keystore.load(null, password.toCharArray());  
                File trustedFolder = new File(originalTrustFolder);  
                File[] certs = trustedFolder.listFiles();  
                if (certs != null) {  
                for (File cert : certs) {  
                CertificateFactory factory = CertificateFactory.getInstance("X.509");  
                try {  
                X509Certificate certificate = (X509Certificate) factory.generateCertificate(new FileInputStream(cert));  
                X500Principal principal = certificate.getSubjectX500Principal();  
                LdapName ldapDN = new LdapName(principal.getName());  
                List<Rdn> rdns = ldapDN.getRdns();  
                    for (Rdn rdn : rdns) {  
                    String type = rdn.getType();  
                    if (type.equals("CN")) {  
                      keystore.setCertificateEntry((String) rdn.getValue(),certificate);  
                                    break;  
                    }         
                                 }  
                } catch (Exception ex) {  
                    continue;  
                   }  
                }  
                }  
                FileOutputStream fos = new FileOutputStream(jksTrustStoreLocation);  
                keystore.store(fos, password.toCharArray());  
                fos.close();  
            } catch (Exception exp) {  
            }  
        }  
    }  

    /** 
     * @param args 
     */  
    public static void main(String[] args) {  

        KeyStoreHelper.createTrustJKSKeyStore("D:\\cacerts", "D:\\cacerts\\test.jks", "test123");  
    }  

}  

上面这个 Java 类可以帮助我们做这个事情,同时还可以依据上面这个核心类,发一个可视化的 Eclipse 插件,这样就更加方便了,下图就是笔者自己开发的一个简单的自娱自乐的 Eclipse Plugin 插件的界面设计(仅供自娱自乐)。

enter image description here

程序代码结构如下:

enter image description here

因为 Eclipse 插件开发历史比较悠久,而且在当前的行业中能够用到的公司不多,所以也就不再详细赘述如何进行 Eclipse 插件开发。如果想进一步的了解的话,可以在读者圈留言和我一起探讨,并且已经把这个简单的插件的源代码上传到了 Github 上,详见这里,只是纯玩性质的一个插件,功能能够正常工作,但是如果大家有需要的话,笔者可以进一步的完善和细化。

对于从 JKS 里面导出 SSL 证书和私钥,大家可以参考上篇文章《 第17课:如何从 JKS 中导出 SSL 证书》 的代码,这里不再赘述。

总结

本文通过实战的代码,演示了如何使用 Java 原生的类去生成私钥和公钥,用到的就是 Java 提供的一个非常重要的 package,[java.security]里面的类如下:

import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;

通过上面的这些类,不但能生成私钥和公钥,还能把其导出成二进制文件,借助于 java.util.Base64 还能把其导出成 Base64 格式的 ASCII 格式字符从而利于传输和使用。

最后,给出了如何通过 Java 代码批量导入 SSL 证书到 JKS 文件中,并提供了一个自娱自乐的 Eclipse 的 Plugin 的例子。大家如果有兴趣可以自行到 Github 去下载、参考,最后祝大家学习愉快。

上一篇
下一篇
目录