RSA公私钥格式分析及其在Java和Openssl之间的转换方法

文章目录

  • PKCS#1和PKCS#8
  • X.509公钥证书
  • ASN.1抽象语法标记
  • DER和PEM编码
  • OID对象标识符
  • 用openssl命令生成PKCS1#格式的RSA密钥对
    • 生成私钥
    • 从私钥中导出公钥
  • PKCS#1格式的RSA公钥
  • PKCS#1格式的RSA私钥
  • PKCS#8格式的RSA私钥(未加密)
  • PKCS#8格式的RSA公钥 与 X.509中RSA公钥
  • Java中的RSA公私钥
    • RSAPublicKey 方法接口
    • RSAPrivateKey 方法接口
    • RSA公私钥的底层实现类
  • Java与Openssl的RSA公私钥的转换
    • Java中Key的getEncoded()与密钥重构
    • Java与Openssl的RSA公私钥的转换
  • 待续

本文主要描述了RSA私钥的PKCS#1与PKCS#8编码RSA公钥的PKCS#1与X.509编码,以及RSA公私钥在Openssl和Java之间的转换

PKCS#1和PKCS#8

PKCS(The Public-Key Cryptography Standards)是一系列公钥密码学的相关标准,其中 《PKCS#1: RSA Cryptography Standard》 是RSA密码学标准的基础部分,而 《PKCS#8: Private-Key Information Syntax Standard》 是私钥信息语法格式。

在PKCS#1中,详细地定义了RSA公钥和私钥的语法格式,通常将符合这种语法的公私钥称为PKCS#1格式的公私钥。
在PKCS#8中,描述了一种私钥信息的语法,同时还提供一种加密密钥的语法。比如RSA私钥证书中的加密私钥就是用此描述的。相应地,符合此标准的私钥也称为PKCS#8格式的私钥。
通常,在java中的RSA私钥是使用PKCS#8格式,在Openssl中是PKCS#1格式。

X.509公钥证书

X.509是一系列的证书标准。在这里提及是因为Java中的RSA公钥的编码就是符合X.509证书公钥的。通常,Openssl中的公钥采用PKCS#1编码,而Java中则通常是X.509编码。

ASN.1抽象语法标记

ASN.1描述了一种对数据进行表示、编码、传输和解码的数据格式。 一般都采用ASN.1来定义一个通用的、抽象的数据结构。PKCS系列标准中也是如此,在PKCS#1标准的附录A可以看到RSA公私钥的ASN.1的详细描述;在PKCS#8中可以看到私钥(加密私钥)信息语法的ASN.1的详细描述。
ASN.1中常见的数据类型定义如下:

类型码 ASN.1类型 作用
1 布尔型 储存布尔值
2 整数 储存大整数
3 位串 存储位数组
4 八位位串 存储字节数组
5 预留位
6 对象标识符 标识算法及协议

DER和PEM编码

证书和密钥的常见编码有DER和PEM两种。

  • DER(Distinguished Encoding Rules)可辨别编码规则是BER的一个子集。而BER基本编码规则,是指在 ASN.1 标准中描述的数据编码/解码规则。
  • PEM编码就是把DER编码的数据用Base64进行编码,再加上开始和结束行,如证书文件的"-----BEGIN CERTIFICATE-----“和”-----END CERTIFICATE-----"。

DER编码的数据是用二进制表示的,直接查看只能看到一些乱码。而PEM是Base64编码的,所以可以直接查看。

OID对象标识符

在证书和密钥等结构的ASN.1描述中, 通常需要用OID来标识一个对象。比如标识一个私钥是RSA私钥或标识一个哈希算法是SHA1算法等待。在ASN.1中定义了这种数据类型,它的标识代码是06。

用openssl命令生成PKCS1#格式的RSA密钥对

在分析之前,我们先来看看用Openssl命令怎么来生成RSA密钥。

生成私钥

openssl genrsa -out prikey.pem 1024

上面这条命令可以生成一个PKCS#1格式的,PEM编码的,1024位的RSA私钥。用NotePad++打开查看:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第1张图片

从私钥中导出公钥

openssl rsa –in prikey.pem –RSAPublicKey_out –out pubkey.pkcs1.pem

这条命令可以从RSA私钥中PKCS#1格式的、PEM编码的RSA公钥。用NotePad++打开查看:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第2张图片

PKCS#1格式的RSA公钥

PKCS#1格式的RSA公钥的ASN.1描述如下:

RSAPublicKey ::= SEQUENCE {
    modulus           INTEGER,  -- n
    publicExponent    INTEGER   -- e 
}

其中:

  • modulus: 是RSA模数,是一个正整数
  • publicExponent: 是RSA公钥幂指数,是一个正整数

我们用Asn1View来查看一下前面生成的pubkey.pkcs1.pem这个RSA公钥:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第3张图片
可以看到,它是符合PKCS标准的。其中包括了n和e这两个RSA公钥元素。其中e为65537,即0x10001,这是一个最常用的公钥幂指数。
RSA算法中的公钥指数目前在用的有两个,其一名为F0,等于3;其二名为F4,等于65537。

PKCS#1格式的RSA私钥

PKCS#1格式的RSA私钥的ASN.1描述如下:

RSAPrivateKey ::= SEQUENCE {
    version           Version, 
    modulus           INTEGER,  -- n
    publicExponent    INTEGER,  -- e
    privateExponent   INTEGER,  -- d
    prime1            INTEGER,  -- p
    prime2            INTEGER,  -- q
    exponent1         INTEGER,  -- d mod (p-1)
    exponent2         INTEGER,  -- d mod (q-1) 
    coefficient       INTEGER,  -- (inverse of q) mod p
    otherPrimeInfos   OtherPrimeInfos OPTIONAL 
}

其中各字段含义如下:

  • version:版本号,为了与本文档的今后版本兼容。V2中这个版本号应该是0,但如果使用了多素数,则版本号应该是1。
    Version ::= INTEGER { two-prime(0), multi(1) }
    (CONSTRAINED BY {-- version must be multi if otherPrimeInfos present --})
  • modulus:RSA的模数n
  • publicExponent:RSA的公钥幂指数e
  • privateExponent:RSA的私钥幂指数d
  • prime1:n的素因子p
  • prime2:n的素因子q
  • exponent1:=d mod (p-1)
  • exponent2:=d mod (q-1)
  • coefficient:=q-1 mod p,代表CRT(中国剩余定理)系数,
  • otherPrimeInfos:按顺序包含了其他的素数“r3,……ru”。如果version是0,则它被忽略;如果version是1,那么它至少应该包含一个OtherPrimeInfo实例。OtherPrimeInfo的定义如下:
OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo
OtherPrimeInfo的ASN.1结构如下:
OtherPrimeInfo ::= SEQUENCE {
    prime             INTEGER,  -- ri
    exponent          INTEGER,  -- di
    coefficient       INTEGER   -- ti
}

OtherPrimeInfo中各字段含义如下:

  • prime:n的素因子ri,where i >= 3
  • exponent:di = d mod (ri - 1)
  • coefficient:ti = (r1 · r2 · … · ri–1)-1 mod ri

我们用Asn1View来查看一下前面生成的prikey.pem这个RSA私钥:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第4张图片
从上面的PKCS#1的RSA私钥结构中我们可以知道,RSA公钥被包含于其中,所以在前面我们才可以用Openssl命令从RSA私钥中导出公钥。

PKCS#8格式的RSA私钥(未加密)

在PKCS#8标准中给出的私钥信息(未加密)的语法格式如下:

PrivateKeyInfo ::= SEQUENCE {
	verion					Version
	privateKeyAlgorithm		PrivateKeyAlgorithmIdentifier
    privateKey				PrivateKey
    attributes				[0] IMPLICIT Attributes OPTIONAL
}

Version ::= INTEGER

PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier

PrivateKey ::= OCTET STRING

Attributes ::= SET OF Attribute

AlgorithmIdentifier ::= SEQUENCE {
	algorithm       		OBJECT IDENTIFIER,
	parameters      		ANY DEFINED BY algorithm OPTIONAL
}

其中个字段含义为:

  • version:版本号,目前为0
  • privateKeyAlgorithm:私钥算法标识
  • privateKey:私钥字节串,内容为私钥的值
  • attributes:属性集。这些属性是连同私钥信息一起被加密的扩展信息

我们使用下面的Openssl命令将前面的PKCS#1编码的RSA私钥转为PKCS#8编码的RSA私钥:

openssl pkcs8 -topk8 -in prikey.pem -out prikey.pkcs8.pem -nocrypt

然后用Asn1View来查看一下:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第5张图片
在这个PKCS#8编码的RSA私钥中,私钥算法OID为1.2.840.113549.1.1.1,其中1.2.840.113549.1.1是PKCS1标准的OID,它下面有很多分支,第一个分支标识RSA私钥。其中的privateKey这个私钥数据就是一个PKCS#1编码的RSA私钥的BER编码。

PKCS#8格式的RSA公钥 与 X.509中RSA公钥

在X.509公钥证书中定义的公钥信息如下:

SubjectPublicKeyInfo ::= SEQUENCE {
	algorithm 				AlgorithmIdentifier,
	subjectPublicKey 		BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
	algorithm       		OBJECT IDENTIFIER,
	parameters      		ANY DEFINED BY algorithm OPTIONAL
}

其中:

  • algorithm:公钥算法标识
  • subjectPublicKey:公钥数据

我们使用下面的Openssl命令将前面的PKCS#1编码的RSA公钥钥转为X509中的RSA公钥:

openssl rsa –in prikey.pem -pubout -out pubkey.x509.pem

然后用Asn1View来查看一下:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第6张图片
在这个X.509的RSA公钥中,公钥算法OID为1.2.840.113549.1.1.1,表示是一个PKCS#1格式的公钥。其中的subjectPublicKey这个公钥数据就是一个PKCS#1编码的RSA公钥的BER编码。

PKCS#8公钥
我们可以从X.509的RSA公钥中看到,它的ASN.1结构描述和PKCS#8中的私钥信息语法基本相同,都是一个算法OID加上密钥数据。所以有时候我们可能也简便地将X.509公钥称为PKCS#8格式的公钥,这样可以将公私钥与PKCS#1和PKCS#8一同提及。但应该明白,PKCS#8标准中并没有定义公钥消息语法,它是定义在X.509公钥证书标准中的。

Java中的RSA公私钥

RSAPublicKey 方法接口

Java中的RSA公钥接口RSAPublicKey的结构图如下:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第7张图片

RSAPrivateKey 方法接口

Java中的RSA私钥接口RSAPrivateKey的结构图如下:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第8张图片
可以从Java中RSA公私钥的接口方法中看到,公私钥只包括了必要的2个元素,即模数幂指数,因为RSA算法的基本计算模型就是:

输入数据.modPow(幂指数, 模数)

其中输入数据、幂指数和模数都是BigInteger类型。

RSA公私钥的底层实现类

上面的RSAPublicKeyRSAPrivateKey只是方法接口,具体的底层实现类其实是在RSAPublicKeyImplRSAPrivateCrtKeyImpl中,它们的类结构图如下:
RSA公私钥格式分析及其在Java和Openssl之间的转换方法_第9张图片
RSAPrivateCrtKeyImpl的源码中可以看到,该结构包含了在PKCS#1中定义的RSA私钥的除otherPrimeInfos之外的所有字段;同时继承自PKCS8Key,它实现了RSA公钥的PKCS8编码。
RSAPublicKeyImpl的源码中也可以看到,该结构包含了在PKCS#1中定义的RSA公钥结构的所有元素;同时继承自X509Key,它实现了RSA公钥的X509编码。

Java与Openssl的RSA公私钥的转换

在Java和Openssl中互用RSA公私钥时,主要涉及到RSA私钥的PKCS#1与PKCS#8的相互转换、RSA公钥的PKCS#1和X509格式之间的转换、DER和PEM编码之间的转换,以及Java中通过RSA密钥字节数组构造RSAPublicKey和RSAPrivateKey结构的方法。
注意:这里涉及到的RSA私钥都是没有加密的。

Java中Key的getEncoded()与密钥重构

这里的字节数组是指从RSAPublicKey和RSAPrivateKey的getEncoded方法获取的数组,这样才可以使用下面的方法将其还原。

    public static RSAPrivateKey genPrivateKey(byte[] derPrikeyPkcs8Encoded) throws Exception
    {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(derPrikeyPkcs8Encoded);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return (RSAPrivateKey)keyFactory.generatePrivate(keySpec);
    }

    public static RSAPublicKey genPublicKey(byte[] derPubkeyX509Encoded) throws Exception
    {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derPubkeyX509Encoded);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return (RSAPublicKey)keyFactory.generatePublic(keySpec);
    }

Java与Openssl的RSA公私钥的转换

对于Openssl和Java之间的RSA公私钥的转换,这里基于BouncyCastle库提供一种转换的实现方式,代码如下:

package com.yy.rsa;

import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.bouncycastle.util.io.pem.PemWriter;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.security.Key;

/**
 * RSA密钥格式转换.
 *
 * 私钥的PKCS#1和PKCS#8之间的转换.
 * 公钥的PKCS#1和X509之间的转换.
 *
 * PKCS1编码的密钥的PEM格式的前缀带有算法名称;非PKCS1的则没有.
 *
 * Created by YaoYuan on 2019/4/30.
 */
public class RsaKeyCodec {
    public static final String TYPE_PREFIX_PKCS1_PRIVATE_KEY = "RSA PRIVATE KEY";
    public static final String TYPE_PREFIX_PKCS1_PUBLIC_KEY = "RSA PUBLIC KEY";
    public static final String TYPE_PREFIX_PKCS8_PRIVATE_KEY = "PRIVATE KEY";
    public static final String TYPE_PREFIX_X509_PUBLIC_KEY = "PUBLIC KEY";

    /**
     * DER转PEM
     *
     * @param typePrefix 密钥类型前缀,如"PRIVATE KEY".
     *                   参考类型:
     *                   {@link #TYPE_PREFIX_PKCS1_PRIVATE_KEY},
     *                   {@link #TYPE_PREFIX_PKCS1_PUBLIC_KEY},
     *                   {@link #TYPE_PREFIX_PKCS8_PRIVATE_KEY},
     *                   {@link #TYPE_PREFIX_X509_PUBLIC_KEY}
     * @param derKeyEncoded 编码后的密钥数据
     * @return PKCS8编码的PEM
     */
    public static String derToPem(String typePrefix, byte[] derKeyEncoded) throws Exception {
        PemObject pemObject = new PemObject(typePrefix, derKeyEncoded);
        StringWriter stringWriter = new StringWriter();
        PemWriter pemWriter = new PemWriter(stringWriter);
        pemWriter.writeObject(pemObject);
        pemWriter.close();
        return stringWriter.toString();
    }

    /**
     * PEM转DER
     *
     * @param pemKey PEM的key
     * @return DER字节数组
     */
    public static byte[] pemToDer(String pemKey) throws IOException {
        InputStreamReader isr = new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes()));
        PemReader pemReader = new PemReader(isr);
        return  pemReader.readPemObject().getContent();
    }


    /**
     * PKCS8编码的私钥转为PKCS1,DER格式.
     *
     * @param derPrikeyPkcs8Encoded 编码后的PKCS8私钥数据,通过java的{@link Key#getEncoded()}获取
     * @return PKCS1编码的DER私钥
     */
    public static byte[] der_prikey_pkcs8ToPkcs1(byte[] derPrikeyPkcs8Encoded) throws Exception {
        PrivateKeyInfo pki = PrivateKeyInfo.getInstance(derPrikeyPkcs8Encoded);
        org.bouncycastle.asn1.pkcs.RSAPrivateKey rsaPrivateKey = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(pki.parsePrivateKey());
        return rsaPrivateKey.getEncoded();
    }


    /**
     * X509编码的公钥转为PKCS1,DER格式.
     *
     * @param derPubkeyX509Encoded 编码后的X509公钥数据,通过java的{@link Key#getEncoded()}获取
     * @return PKCS1编码的DER公钥
     */
    public static byte[] der_pubkey_x509ToPkcs1(byte[] derPubkeyX509Encoded) throws Exception {
        SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(derPubkeyX509Encoded);
        org.bouncycastle.asn1.pkcs.RSAPublicKey rsaPublicKey = org.bouncycastle.asn1.pkcs.RSAPublicKey.getInstance(spki.parsePublicKey());
        return rsaPublicKey.getEncoded();
    }


    /**
     * PKCS1编码的私钥转为PKCS8,DER格式.
     *
     * @param derPrikeyPkcs1Encoded 编码后的PKCS1私钥数据
     * @return PKCS8编码的DER私钥
     */
    public static byte[] der_prikey_pkcs1ToPkcs8(byte[] derPrikeyPkcs1Encoded) throws Exception {
        AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(PKCSObjectIdentifiers.pkcs8ShroudedKeyBag);
        ASN1Object asn1Object = ASN1Primitive.fromByteArray(derPrikeyPkcs1Encoded);
        PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(algorithmIdentifier, asn1Object);
        return privateKeyInfo.getEncoded();
    }

    /**
     * PKCS1编码的公钥转为X509,DER格式.
     *
     * @param derPubkeyPkcs1Encoded 编码后的PKCS1公钥数据
     * @return X509编码的DER公钥
     */
    public static byte[] der_pubkey_pkcs1ToX509(byte[] derPubkeyPkcs1Encoded) throws Exception {
        AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(PKCSObjectIdentifiers.pkcs8ShroudedKeyBag);
        ASN1Object asn1Object = ASN1Primitive.fromByteArray(derPubkeyPkcs1Encoded);
        SubjectPublicKeyInfo spki = new SubjectPublicKeyInfo(algorithmIdentifier, asn1Object);
        return spki.getEncoded();
    }
}

待续

你可能感兴趣的:(程序设计,PKCS1,PKCS8,RSA公钥,RSA私钥,Openssl)