目录
Java自带库函数读取X.509证书字段
这个很简单,直接上代码。读取然后全部输出,或者输出对应字段。
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
// 使用自带库函数读取X.509证书
private static void readX_509(String certificateName){
try{
CertificateFactory CF = CertificateFactory.getInstance("X.509"); // 从证书工厂中获取X.509的单例类
FileInputStream fileIn = new FileInputStream(certificateName); // 将证书读入文件流
Certificate C = CF.generateCertificate(fileIn); // 将文件流的证书转化为证书类
String certificateStr = C.toString();
System.out.println("使用[自带库函数]读入证书结果如下:");
System.out.print(certificateStr);
System.out.println("--------------------------------------\n证书主要字段:");
X509Certificate cer = (X509Certificate)C;
System.out.println("版本号:" + cer.getVersion());
System.out.println("序列号:" + cer.getSerialNumber().toString());
System.out.println("颁发者:" + cer.getSubjectDN());
// System.out.println("颁发者唯一标识符: " + cer.getSubjectUniqueID().toString());
System.out.println("使用者:" + cer.getIssuerDN());
// System.out.println("使用者唯一标识符: " + cer.getIssuerUniqueID().toString());
System.out.println("有效期:from:" + cer.getNotBefore() + " to: " + cer.getNotAfter());
System.out.println("签发算法" + cer.getSigAlgName());
System.out.println("签发算法ID:" + cer.getSigAlgOID());
System.out.println("证书签名:" + cer.getSignature().toString());
byte [] sig = cer.getSigAlgParams();
PublicKey pk = cer.getPublicKey();
byte [] pkenc = pk.getEncoded();
System.out.println("公钥:");
for(int i = 0; i < pkenc.length; i++){
System.out.print(pkenc[i]);
}
}
catch(Exception e){
e.printStackTrace();
}
}
理解证书数据格式,然后解析
网上相关的代码太少了,估计大家都用比较成熟的库函数,很少人自己去体会实际的证书内部数据格式,以及解析的方式。以下内容仅供参考,大致可以获取基本证书字段,不同证书扩展字段不一样,所以扩展字段的解析不是很理想。不过有一份完备详细的证书数据结构文档的话,按照本文思路花点功夫也可以实现完整解析。
基本概念
证书编码方式为ANS1方式,详细见此博客。
看完博客之后,可以得出:一个数据块基本构成成分是 数据类型标识符 + 数据长度字段标识符 + 数据部分。
我们需要做的就是识别数据类型标识符和数据长度字段,然后读取对应字段的数据部分,并按照不同的编码方式(如UTC时间格式,Assci码格式)输出数据字段。
详细的数据结构必须要看白皮书才权威,不过个人觉得好像也不是很详细,还要多查阅许多相关文档才能了解。。其中重点关注第四章里头介绍证书各个基本字段。
算法思路
整个算法的过程总结如下:
- 读取二进制证书文件,保存证书数据到字节数组中。
- 字节数组转为二进制比特字符串。
- 分不同的数据类型初始化多个数据存储容器。
- 以数据块为单元分析比特字节串。
- 数据类型标识符分析
- 数据长度值分析
- 数据值分析。
- 将分析结果按类型存储到数据存储容器中。
- 按对应的数据编码方式输出证书主要字段。
核心步骤
1. 读取证书
// 将文件字节流转为字节数组
public static byte[] FiletoByteArray(String filename) throws IOException{
byte[] by = {0};
try{
File f=new File(filename);
FileInputStream fis=new FileInputStream(f);
int size = fis.available(); // 返回输入流中可以被读的bytes字节估量值
DataInputStream dis=new DataInputStream(fis);
by=new byte[size];//要读取的位数(一次性全部读取
dis.read(by);
return by;
}
catch (Exception e){
e.printStackTrace();
}
return by;
}
// 将字节数组转为0 1 比特字符串
public static String byteToBitString(byte [] by){
String res = "";
for (int i = 0; i < by.length; i++){
res = res + byteToBit(by[i]);
}
System.out.println(res);
return res;
}
// 读取证书文件,转为字节数组
byte [] byteCer = FiletoByteArray(certificatefile);
// 字节数组转为 0 1 比特字符串
String bitStringCer = byteToBitString(byteCer);
2. 初始化数据容器
Integer[] integerArr = new Integer[30] ; // 存储integer
int intIndex = 0;
String [] bigIntegerArr = new String[30]; // 存储证书拥有的bigInteger
int BigIntIndex = 0;
String [] bitStringArr = new String[30]; // 存储位串
int bitStringIndex = 0;
String [] printableStringArr = new String[30];// 存储打印的信息串
int printableStringIndex = 0;
String [] timeArr = new String[30]; // 存储UTC 时间
int timeIndex = 0;
String []subjectOidStringArr = new String[30]; // 存储 主体信息OID
String[] subjectStringArr = new String[30]; // 存储主体信息内容
int subjectStringIndex = 0;
…
3. 将分析数据过程放入一个循环,利用一个标志位start作为解析的起点,直到start移动到数据末尾。
分析数据标识类型。
while(start != bitStringCer.length()){
System.out.println("----开始分析---------------------");
// 分析数据标识类型
String dataSign = bitStringCer.substring(start, start + 8); // 数据块头部标识 一个字节
System.out.format("1 获取比特串start : %d, end: %d\n", start, start + 8);
System.out.format("1 更新比特串start : %d\n",start + 8);
System.out.println("dataSign-part: " + dataSign + " hex: " + bit2Hex(dataSign));
start = start + 8; // 更新start
String bit87 = dataSign.substring(0,2); // 用来标识TAG,有四种类型
int bit51 = 0; // 记录 bit5 - bit1的10进制
bit51 = BitToTen(dataSign.substring(3, 8)); // 获取bit5- 1的数字
分析数据长度
// 分析数据块长度////////////////////////////////////////////////
String dataLength = bitStringCer.substring(start , start + 8); // 取数据块长度 第一个字节
System.out.format("2 获取比特串start : %d, end: %d\n", start, start + 8);
System.out.format("2 更新比特串start : %d\n",start + 8);
System.out.println("dataLength-part: " + dataLength + " hex:" + bit2Hex(dataLength));
start = start + 8; // 更新start
char bit8 = dataLength.charAt(0);
int length = 0; //数据块长度值
if(bit8 == '0'){
// 长度值小于4 个字节
length = BitToTen(dataLength.substring(1,8)); //获取数据块长度
System.out.format("data length = %d < 127\n", length);
}
else if(bit8 == '1'){
// 长度值大于127
int byteLen = BitToTen(dataLength.substring(1, 8)); // 得到实际的字节数,用来表示长度值
length = BitToTen(bitStringCer.substring(start, start + 8 * byteLen)); // 获取数据块长度
System.out.format("3 获取比特串start : %d, end: %d\n", start, start + 8 * byteLen);
System.out.format("3 更新比特串start : %d\n",start + 8*byteLen);
System.out.format("data length = %d > 127 hex : %s\n" ,length, bit2Hex(bitStringCer.substring(start, start + 8 * byteLen)));
start = start + 8 * byteLen; // 更新start
}
else if(dataLength == "10000000"){
// 数据块长度补丁,由数据块结束标识字段(0x0000)结束数据块
length = -1;
}
分析数据块
String dataBlock = bitStringCer.substring(start, endBound); // 获取数据块
System.out.format("4 获取比特串start : %d, end: %d\n", start, endBound);
System.out.format("4 更新比特串start : %d\n",endBound);
start = endBound; // 更新start位置
System.out.println("dataBlock: " + bit2Hex(dataBlock));
System.out.format("bit87: %s bit51: %d\n", bit87, bit51);
if(bit87.charAt(0) == '0' && bit87.charAt(1) == '0'){
if(bit51 == 1){
// boolean
System.out.println("data type: boolean.");
if(dataBlock == "11111111")
boolArr[boolIndex++] = true;
if(dataBlock == "00000000")
boolArr[boolIndex++] = false;
}
else if(bit51 == 2){
// integer
System.out.format("data type: integer. %s\n", bit2Hex(dataBlock));
if(dataBlock.length() < 10)
integerArr[intIndex++] = BitToTen(dataBlock);
else
bigIntegerArr[BigIntIndex++] = bit2Hex(dataBlock);
}
………… // 还有其他解析字段
4. 从数据容器中输出字段值
System.out.println("输出证书读取结果");
System.out.println("---------\n证书主体部分:");
System.out.format("主体唯一ID:");
System.out.println(issuerUniqueID);
System.out.format("issuer唯一ID:");
System.out.println(issuerUniqueID);
System.out.format("证书版本号:%d\n", integerArr[0] + 1 );
System.out.format("证书序列号:%s\n", bigIntegerArr[0]);
System.out.format("证书签名值:%s\n",bitStringArr[0]);
System.out.format("主体: \n");
for(int i = 0; i < printableStringIndex; i++){
System.out.format("%s = %s ", BitToOID(subjectOidStringArr[i]), printStr2text(printableStringArr[i]));
}
System.out.format("\n有效期:从 %s 到 %s \n", UTCTimeToString(timeArr[0]), UTCTimeToString(timeArr[1]));
System.out.println("---------\n证书签名算法标识:");
System.out.format("签名算法:%s\n", BitToOID(algorithmOIDArr[0]));
System.out.format("module: %s\n", bitStringArr[1]);
System.out.format("exponent: %d\n", exponent);
System.out.println("----------\n证书扩展部分:");
for(int i = 0; i < expandIndex; i++){
System.out.format("OID:%s 内容:%s, octstring: %s", BitToOID(expandOIDArr[i]), expandArr[i], octetStringArr[i]);
}
System.out.println("----------\n未知部分:");
for(int i = 0; i < unknownIndex; i++){
System.out.format("%s \n", unknownStrArr[i]);
}
此外还涉及一些编码模块。
将16进制的数据块转为text文本
// 将printable类型的16进制数据转为text文本
public static String printStr2text(String printStr){
assert (printStr.length()%8 == 0);
String text = "";
String hexStr = printStr;
// System.out.println(printStr);
for(int i = 0; i < hexStr.length(); i = i +2){
String tmpStr = printStr.substring(i, i+2);
char res = (char)string2Ansic(tmpStr);
text += res;
}
return text;
}
将16进制UTC时间格式转为常规时间格式。
// 将16进制时间格式转为 YY MM DD HH MM SS Z
public static String UTCTimeToString(String bitStr){
String timeString = "";
int len = bitStr.length()/2;
String []timeArr = new String[len];
for(int i = 0; i < bitStr.length(); i= i+2){
String tmpStr = bitStr.substring(i, i+2);
String ansicStr = String.valueOf(((char)string2Ansic(tmpStr)));
timeArr[i/2] = ansicStr;
}
timeString = String.format("UTC time: YY:%s%s/MM:%s%s/DD:%s%s/HH:%s%s/MM:%s%s/SS:%s%s",timeArr[0],timeArr[1],
timeArr[2],timeArr[3],timeArr[4],timeArr[5],timeArr[6],timeArr[7],
timeArr[8],timeArr[9],timeArr[10],timeArr[11]);
return timeString;
}
以上代码仅供参考。整体思路正确,但是由于证书格式,字段有许多不确定性,存在较多的没能正确解析的数据。无法确定的数据可以考虑加入一个未解析的数据容器。对于扩展字段的解析不太了解,主要是OID(对象标识符)比较混乱。。真的烦。。白皮书好像也找不到较为详细的介绍。如果有大佬知道怎么办,麻烦告知一下。
(java自带库函数可能可以找到实现源码。。接触java不久,,找不到,,IDEA可能不太智能吧?!)
某个测试样例
证书内容长这样子,都是16进制的格式,官网上的例子也是这种格式,当然还有base64编码的证书,转为这个格式再解析也是一样的。
解析结果如下:
完整源码 12月17号公布。