证书生成

2018/08/18 证书 HTTPS PKI

前言

资料

  代码(下面实现的代码都可以在此项目看到):
    https://github.com/viakiba/viakiba/tree/master/cert

  背景:
    聊聊HTTPS和SSL/TLS协议: http://www.techug.com/post/https-ssl-tls.html
    数字证书及 CA 的扫盲介绍: https://program-think.blogspot.com/2010/02/introduce-digital-certificate-and-ca.html
    RSA证书对消息的签名与验签:https://blog.viakiba.cn/2018/07/28/RSA%E7%9A%84%E8%AF%81%E4%B9%A6%E7%AD%BE%E5%90%8D%E4%B8%8E%E9%AA%8C%E7%AD%BE(Java)/
    https如何保证数据传输的安全性:https://juejin.im/post/5b7eb5ba51882542bf03364c
    根证书:https://en.wikipedia.org/wiki/Root_certificate
    中间人攻击:https://en.wikipedia.org/wiki/Man-in-the-middle_attack

这一片文章的缘起,是因为最近对接的系统需要使用证书对应的私钥对发送的数据做签名,并把证书发送给系统。而所需的证书恰好与HTTPS证书一样。他们不要求我们需要使用的证书是权威CA签发的,只需要密钥管理能够符合安全标准,所以我们就在此前提下,自己开展了证书的签发与生成,只要把用于签发用户证书的根证书提供给对方系统,他们就可以信任我们的用户证书了。因为证书的i案发过程会包含签发者的证书信息。这一片文章主要阐述证书签发解析的 JAVA实现,不对证书原理做基础解释,必要解释可以参考上面的链接进行通篇认识。

证书管理

HTTPS,其实是PKI非对称加密的一种应用场景。证书管理最重要的是私钥的管理与保护,比如阿里云与腾讯云也有提供密钥管理服务。如果只是存入存储模块,Java可以这么实现核心代码如下:

密钥的产生

// 一般要求是2048位 或更多
public static Map genKey() throws NoSuchAlgorithmException {
        //获得对象 KeyPairGenerator 参数 RSA 1024个字节
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGen.initialize(2048);
        //通过对象 KeyPairGenerator 获取对象KeyPair
        KeyPair keyPair = keyPairGen.generateKeyPair();
        //通过对象 KeyPair 获取RSA公私钥对象RSAPublicKey RSAPrivateKey
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        //公私钥对象存入map中
        keyMap.put(PUBLIC_KEY, publicKey);
        keyMap.put(PRIVATE_KEY, privateKey);
        return keyMap;
    }

证书的产生

CA证书(根证书)的特点就是自签发: 1: 颁发者信息与使用者信息一致 2: 基本约束带有CA标识 3: 使用证书含有公钥对应的私钥对自己进行证书签发 而 用户证书不会举要上述特点

/**
   *  根据如下参数获取对应base64编码格式的证书文件字符串
   *      issuerName 与 reqName 对象是同一个则认为生成的是CA证书
   * @param issuerName 颁发者信息
   * @param reqName   请求证主题信息
   *                  <br> issuerName == reqName ---> CA
   * @param serial 证书序列号
   *                 <br>eg: BigInteger serial = BigInteger.valueOf(System.currentTimeMillis() / 1000);
   * @param notBefore 有效期开始时间  2018-08-01 00:00:00
   * @param notAfter 有效期截至时间   2028-08-01 00:00:00
   * @param userPublicKey 请求者主题公钥信息
   * @param rootPrivateKey   颁发者私钥信息
   * @return String
   * @throws OperatorCreationException
   * @throws CertificateException
   * @throws IOException
   */
  public static String certBuilder(X500Name issuerName, X500Name reqName, BigInteger serial, Date notBefore, Date notAfter, PublicKey userPublicKey, PrivateKey rootPrivateKey) throws OperatorCreationException, CertificateException, IOException {
      JcaX509v3CertificateBuilder x509v3CertificateBuilder = new JcaX509v3CertificateBuilder(
              issuerName, serial, notBefore, notAfter, reqName, userPublicKey);
      // 签发者 与 使用者 信息一致则是CA证书生成,增加CA 基本约束属性
      if(issuerName == reqName){
          BasicConstraints constraint = new BasicConstraints(1);
          x509v3CertificateBuilder.addExtension(Extension.basicConstraints, false, constraint);
      }
      //签名的工具
      ContentSigner signer = new JcaContentSignerBuilder("SHA256WITHRSA").setProvider("BC").build(rootPrivateKey);
      //触发签名产生用户证书
      X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder.build(signer);
      JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
      certificateConverter.setProvider("BC");
      Certificate userCertificate = certificateConverter.getCertificate(x509CertificateHolder);
      String certStr = genCert(userCertificate);
      return certStr;
  }

测试样例

基于密钥产生,公私钥我就写成常量,这些需要多层安全保护,防止泄露,这里只是为了操作方便。

// 16进制
/**
 * CA
 */
public static String publicRootMudulus = "00F8FE59C1C5A1A51423E937E16F09A3FB9FCBCB2573C6D71343469B9AB5EF8A8662E60D3FCE5DCFCF697C64BCFDC559A3B13BDF195EF20AB9F8A00B37D3D1BFBE214553222007317523E7CB335DF344D23B8FE2A0541A035980680273C0DE0384BA2BDC05B5227F2D46AEDFA09C551C328AB9C348B43DB42483F3B989B5E5981E7CB23FE43B6253EA64F89879BDC8DC7427A6CBA32C11E598DD058376D723B6C5763B6B71C0FD469FC0B3BE3B4FFF50AF4BFB08930FC7BACA804D6D5A1410E44DB59E160D57098D3374F7D221372710F2B0005F040430CB49D4B86B457826A84AD6C3585F327FC70B10F006E595DE9569DACE74A051E6F50824BF8EF4879BFCA5";
public static String publicRootexpoent = "10001";
public static String privateRootexpoent = "009EF06D871D8AA37F89B4D370D99A43CCD9221398E2A0A8A5A92A2725C8C111A1DAFB92B58A1BA40D77FE69A7A22E199C3E0443D3442228EAB164280508F738F83AF0AFB276D360A4AFB8C4A31373B818A2E0A3FF47F01AF744DA1FC697F4A0365748ABF810B9E68896380693D57716BAC486F3BB3322B81D1F05B307CECEB21C719171142791F39121896E63DBE0B106BF6E06606764EA304B7A95B9A2F9280D9A93D75392239C0DA29217472EEC87B9DE0EDE6BD8CE368A3582053AE666AAEAA9C2E76A3AAE2E494D76E1ED9FE56CE93D8302DAE64246DDECD2B82A803D9D8B1F882052DE2317DF89CC2A6F6DB28590839659FD6581ADD6EE3EFE1BE118A5C1";

/**
 *  USER
 **/
public static String publicUserMudulus = "00B2C22D4997146374438623265AF1FA92AFE7AE4A4435D79F33D5155D823DD3B201E1BFEB0D1661B587397384473CE90DB90F816E65A1563E322D0BB590685A3345017AAA7F8FB6023CDA787A380CDD07213CEBDDE4C12B15D656ACDD3043B4D6A147500FB7056201EB179EB268F003ADEC6C7272671FF7A8B2411B1E6B7862D11177FBB4078FC47FBDF57C1720E973B3FF9648E0F8D1A213AF64C5742109D6ED0F75AF430688A78A6196E906F681E537DCA33CBC2B0CC358ECB79EF84F1599F68395FDCBA776A32195EF599A777C6EB807D58A59CE59576D2FC2A39447D58B7FED38B14417457B80E29E927990CF1B3524062DB0E763B71BAA38E83F442FF9B5";
public static String publicUserexpoent = "10001";
public static String privateUserexpoent = "0083A18EFDA89D9FDAA63A6939BE307FA67297B4F505236CC2D3C52DF56C89A0906CE8528D8056A1DEAD53B5E78B19A437B1B56446E9D9930B3BA18604CDF0B9B3153650A0AA4C25E7A1EDF257755CAB89AB8513DE92AB57D1BDC2978A4D171E5C09E8DC60A611F5A899F80BA92EB2C6D2D4CCCABDC98875B32887DEB358CA0E60D0223A0F946BF4F3B032E933D3A4BFFF7F792CBE0B11BEBC2DAA06BDF530CD11CA07B73F07540A812E6A26B43E41E1D7AF6F20511BA355AD68D49E3F4EE8BA282866900B2407B0C92325A091D75ADE455F987618E7DFA50A1010C51B7141354CB4B3C79C77C60906FE05229BCE77D51824862E8EC74C26E98C672047CFD2D761";

CER证书产生

CA证书的生成:

代码里的参数信息,Issue基本信息与证书序列号根据业务可以存库。当然得到的证书数据里面也会包含这些数据。输出的字符串存入 xxxx.cer 文件。windows双击打开就可以看到信息了。 CA证书截图

自签名还有一个就是用自己的私钥对数据签名拿到的证书,而用户证书是使用CA的私钥对自己证书进行签名。

/**
 * 签发CA证书
 */
@Test
public void genCaCertTest() throws Exception{
    //根证书Issue基本信息
    X500Name issuerName = getX500Name("Dev", "VK", "BeiJing", "BeiJing", "CN", "R&D");
    // 证书序列号
    BigInteger serial = BigInteger.valueOf(System.currentTimeMillis() / 1000);
    //证书有 起始日期 与 结束日期
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date notBefore = sdf.parse("2018-08-01 00:00:00");
    Date notAfter = sdf.parse("2028-08-01 00:00:00");

    //构建 用户证书 对应的公钥
    PublicKey userPublicKey = getPublicKey(16,publicRootMudulus,publicRootexpoent);
    //构建CAroot证书 对应的私钥
    PrivateKey rootPrivateKey = getPrivateKey(16,publicRootMudulus,publicRootexpoent);
    //构建证书的build
    String cert = certBuilder(issuerName, issuerName, serial, notBefore, notAfter, userPublicKey, rootPrivateKey);
    System.out.println("\n"+cert);
}

用户证书: 这里要用用户证书的公钥以及请求信息,使用CA证书的私钥做签名达到证书结果。

/**
  * 生成用户证书
  *  签发证书的颁发者信息 要与生成ca证书时的签发信息一致,不然会出错。证书链验证不过。
  */
  @Test
  public void genUserCertTest() throws Exception{
     //根证书Issue基本信息
     X500Name issuerName = getX500Name("Dev", "Vk", "BeiJing", "BeiJing", "CN", "R&D");
     // 用户证书 基本使用者
     X500Name reqName = getX500Name("vkuser", "vkuser", "BeiJing", "BeiJing", "CN", "R&D");
     // 证书序列号
     BigInteger serial = BigInteger.valueOf(System.currentTimeMillis() / 1000);
     //证书 起始日期 与 结束日期
     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     Date notBefore = sdf.parse("2018-08-02 00:00:00");
     Date notAfter = sdf.parse("2028-07-01 00:00:00");

     //构建 用户证书 对应的公钥
     PublicKey userPublicKey = getPublicKey(16,publicUserMudulus,publicUserexpoent);
     //构建CAroot证书 对应的私钥
     PrivateKey rootPrivateKey = getPrivateKey(16,publicRootMudulus,privateRootexpoent);
     //构建证书的build
     String cert = certBuilder(issuerName, reqName, serial, notBefore, notAfter, userPublicKey, rootPrivateKey);
     System.out.println("\n"+cert);
  }

PFX文件生成

得到的用户证书其实只包含公钥信息以及私钥信息,上面有说到公私钥可以保存到存储系统正,可是在HTTPS场景中,在配置HTTPS时,可以使用一个PFX格式的文件进行配置,其实PFX不仅包含公私钥信息,还会包含上面的证书信息。

所以在上面生成CERT证书的同时,也可以根据上面的信息生成PFX文件,PFX需要用户证书对应的私钥信息。分开来实现如下:

/**
 *  生成PFX证书文件
 * @param certificate 证书
 *             建议 CertUtil#genX509Certificate(java.math.BigInteger, java.util.Date, java.util.Date, org.bouncycastle.asn1.x500.X500Name, org.bouncycastle.asn1.x500.X500Name, java.security.PrivateKey, java.security.PublicKey)()
 * @param privateKey
 *              请求者私钥
 * @param passWord
 *              pfx生成后的密码
 * @param certPath
 *              pfx存储路径
 * @throws CertificateException
 * @throws NoSuchAlgorithmException
 * @throws IOException
 * @throws KeyStoreException
 */
public static void genPfx(Certificate certificate,PrivateKey privateKey,String passWord, String certPath) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
    KeyStore store = KeyStore.getInstance("PKCS12");
    store.load(null, null);
    store.setKeyEntry("", privateKey,
            passWord.toCharArray(), new Certificate[] { certificate });

    FileOutputStream fout =new FileOutputStream(certPath);
    store.store(fout, passWord.toCharArray());
    fout.close();
}

@Test
public void genUserPfxTest() throws NoSuchAlgorithmException,
        InvalidKeyException, SecurityException, SignatureException,
        KeyStoreException, CertificateException, IOException, OperatorCreationException, InvalidKeySpecException {

    String certPath = "D:/pfxGenUser.pfx";
    PrivateKey rootPrivateKey = getPrivateKey(16,publicRootMudulus,privateRootexpoent);
    PrivateKey userPrivateKey = getPrivateKey(16,publicUserMudulus,privateUserexpoent);
    PublicKey userPublicKey = getPublicKey(16,publicUserMudulus,publicUserexpoent);
    // PFX 密码
    String passWord = "12345678";
    X500Name issuer = getX500Name("Dev", "Vk", "BeiJing", "BeiJing", "CN", "R&D");
    X500Name reqSubject = getX500Name("vkuser", "vkuser", "BeiJing", "BeiJing", "CN", "R&D");

    X509Certificate x509Certificate = genX509Certificate(
            new BigInteger("111",10),(new Date(System.currentTimeMillis() - 500000)), (new Date(System.currentTimeMillis() + 500000)),
    issuer, reqSubject, rootPrivateKey,userPublicKey);

    genPfx(x509Certificate,userPrivateKey,passWord,certPath);

}

其实这一步就是HTTPS有直接联系,生成的文件可以进行HTTPS配置。

CSR请求文件

有些情况下,HTTPS证书生成签发,我们可以自己提供一个CSR请求文件,CA机构可以使用CSR文件进行解析后签发对应的CER证书。

生成CSR

/**
  *  生成 CSR 请求文件
  * @param reqName 请求者主体信息
  * @param userPublicKey 用户公钥
  * @param userPrivateKey 用户私钥
  * @return
  * @throws OperatorCreationException
  * @throws CertificateException
  * @throws IOException
  */
 public static String csrBuilder(X500Name reqName, PublicKey userPublicKey, PrivateKey userPrivateKey) throws OperatorCreationException, IOException {

     PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(reqName,userPublicKey );
     JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder("SHA256withRSA");
     ContentSigner csrSigner = csBuilder.build(userPrivateKey);
     PKCS10CertificationRequest csr = p10Builder.build(csrSigner);

     //处理证书 ANS.I DER 编码 =》 String Base64编码
     BASE64Encoder base64 = new BASE64Encoder();
     ByteArrayOutputStream cerFormat = new ByteArrayOutputStream();
     base64.encodeBuffer(csr.getEncoded(), cerFormat);

     StringBuilder sb = new StringBuilder();
     sb.append("-----BEGIN CERTIFICATE REQUEST-----"+"\n");
     sb.append(new String (cerFormat.toByteArray()));
     sb.append("-----END CERTIFICATE REQUEST-----");
     return sb.toString();
 }

解析CSR

/**
  *  根据csr文件字符串生成对应的PKCS10CertificationRequest对象
  * @param csrStr
  * @throws UnsupportedEncodingException
  * @throws IOException
  * @throws NoSuchAlgorithmException
  * @throws SignatureException
  */
 public static PKCS10CertificationRequest csrAnalyze(String csrStr) throws UnsupportedEncodingException, IOException {
     if( !csrStr.startsWith("-----BEGIN CERTIFICATE REQUEST-----") || !csrStr.endsWith("-----END CERTIFICATE REQUEST-----")){
         throw new IOException("csr 信息不合法");
     }
     csrStr = csrStr.replace("-----BEGIN CERTIFICATE REQUEST-----"+"\n","");
     csrStr = csrStr.replace("-----END CERTIFICATE REQUEST-----","");
     BASE64Decoder decoder = new BASE64Decoder();
     byte[] bArray = decoder.decodeBuffer(csrStr);
     PKCS10CertificationRequest csr = new PKCS10CertificationRequest(bArray);
     System.out.println(csr);
     return csr;
 }

/**
 *  获取包含的公钥 16进制 等信息
 * @param pkcs10CertificationRequest
 * @return
 * @throws NoSuchAlgorithmException
 * @throws InvalidKeyException
 */
public static PublicKey csrGetPublicKeyByPKCS(PKCS10CertificationRequest pkcs10CertificationRequest) throws NoSuchAlgorithmException, InvalidKeyException {
    JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = new JcaPKCS10CertificationRequest(pkcs10CertificationRequest);
    // 请求者的主体信息 (也就是 使用者信息)
    X500Name subject = jcaPKCS10CertificationRequest.getSubject();
    //证书的公钥信息
    PublicKey publicKey = jcaPKCS10CertificationRequest.getPublicKey();
    return publicKey;
}

这样就可以多一个CSR步骤进行用户证书的签发。

结束语

这一片主要是证书相关生成解析的实践,理论部分可以参考前面提供的链接进行初步的认识。所有代码都可以在前面的代码链接中看到,不足之处欢迎指正。

Search

    Table of Contents