一.需求背景
同志们,我又来了!不知道你们会不会遇到高标准严要求的上游规范。我司设备上游文档中标明:需要使用满足国密规范的密码器设备来创建、保护秘钥对,同时申请CA证书时,签署的CSR签名请求证书也要求用密码器签名,这样就能保证私钥在不暴露、不可读的前提下,签署证书文件、解密。
一听这个,我就知道麻烦来了,我以前也没有接触过这类设备的嵌入,但是我能理解对安全性的要求,没关系,迎难而上,我是开发小能手!
二.需求拆分
具体业务流程不便说明,但需求拆分出来,我们可以看做是需要签署一个CSR签名请求证书,提交到上游,上游颁发CA证书,之后的接口里面也需要签名,签名的算法是RSAwithSHA256(其实都一样,有些东西找到你对应的标记就行,后面会讲)。
三.解决办法
首先我们要简单CSR的结构体,通过openssl命令:
openssl req -text -in /路径/csr.pem
我们可以看到,一个csr的结构体大概是下面这样子的:
Certificate Request:
Data:
Version: 1 (0x0)
Subject: CN = example.com, O = Example Inc., L = San Francisco, ST = California, C = US
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:8e:24:89:13:de:8d:36:19:45:c0:65:d1:97:dc:
1b:f9:ad:60:b3:25:45:ba:20:8c:28:02:fd:a0:fb:
f7:ab:7d:f2:f1:2e:84:fb:46:69:e1:6e:7f:a4:6b:
be:b1:fa:ad:b1:6e:1f:51:31:47:25:36:7e:83:e0:
47:e6:79:3d:92:86:43:fe:0d:c5:1e:36:03:e9:bb:
71:33:d2:73:88:b6:e9:88:33:08:cf:ad:74:3e:ac:
27:98:2e:05:eb:78:ef:24:ac:e4:83:3b:b5:19:75:
aa:a7:0e:94:d5:6f:8c:29:18:32:63:30:99:29:79:
7a:18:e0:9d:9f:96:90:12:16:68:d2:a3:7d:2d:bd:
86:b8:ba:f1:ef:0f:5c:06:f1:17:f3:03:77:51:c9:
b7:26:2c:dc:c1:cf:45:80:a3:47:9c:db:de:9a:69:
f8:75:21:d2:5b:71:f9:7e:58:d7:5d:6e:3c:4b:f3:
f8:b4:65:13:1f:ce:19:b7:35:ed:8d:b4:ca:fb:80:
20:63:c2:6a:24:0f:65:74:17:33:7b:72:f5:ff:6f:
f9:7f:3f:e5:1d:58:e8:bf:23:0c:cc:2a:0f:00:66:
03:5b:d1:cc:19:b5:0c:98:eb:b0:b8:0f:72:cc:77:
fb:7c:90:e5:d0:f7:4a:05:76:74:60:96:e6:de:9e:
52:e3
Exponent: 65537 (0x10001)
Attributes:
a0:00
Signature Algorithm: sha256WithRSAEncryption
77:0b:39:c9:82:17:a4:35:a8:88:ad:81:95:3f:ec:2b:c7:47:
d5:3f:5b:36:3a:20:4c:de:62:39:69:04:00:a6:e8:d4:9d:57:
1f:55:96:30:c7:9c:9d:70:33:bf:ba:d3:5b:71:66:f8:b4:5b:
16:89:5f:b7:6e:83:58:e0:c3:34:3a:13:d4:6c:6d:e8:ec:58:
c4:5a:ce:41:2c:df:40:b5:ea:e7:2c:e1:6b:00:11:be:33:82:
22:ff:3c:e1:f4:2a:7d:67:59:10:74:e2:4d:fd:a8:28:1b:6d:
0c:0d:24:cb:85:35:ae:92:78:a5:32:00:14:f5:bc:e0:1d:d1:
10:2a:bc:39:f8:35:0d:4f:b6:25:f6:e5:30:6c:3d:b9:4a:db:
81:87:e6:94:10:5a:1d:cc:01:5c:fd:7c:ba:a9:c0:3e:c5:45:
30:2d:05:f0:5a:49:c1:79:e0:b7:b2:11:e4:c8:82:e6:d4:36:
b4:47:cc:d4:83:35:04:43:16:6d:3c:34:1d:47:0f:85:62:a7:
52:05:37:9d:00:2d:7f:bd:dd:7d:55:9c:68:28:56:6b:15:4f:
bb:32:12:80:2d:73:c8:8a:3a:97:be:fe:d4:3b:42:c0:f5:cb:
b8:11:69:61:4a:72:36:6c:9d:e7:74:d4:1c:ad:37:3e:38:d8:
31:8d:f8:51
-----BEGIN CERTIFICATE REQUEST-----
MIICrDCCAZQCAQAwZzEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFTATBgNVBAoMDEV4
YW1wbGUgSW5jLjEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECAwKQ2Fs
aWZvcm5pYTELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCOJIkT3o02GUXAZdGX3Bv5rWCzJUW6IIwoAv2g+/erffLxLoT7Rmnhbn+k
a76x+q2xbh9RMUclNn6D4EfmeT2ShkP+DcUeNgPpu3Ez0nOItumIMwjPrXQ+rCeY
LgXreO8krOSDO7UZdaqnDpTVb4wpGDJjMJkpeXoY4J2flpASFmjSo30tvYa4uvHv
D1wG8RfzA3dRybcmLNzBz0WAo0ec296aafh1IdJbcfl+WNddbjxL8/i0ZRMfzhm3
Ne2NtMr7gCBjwmokD2V0FzN7cvX/b/l/P+UdWOi/IwzMKg8AZgNb0cwZtQyY67C4
D3LMd/t8kOXQ90oFdnRglubenlLjAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEA
dws5yYIXpDWoiK2BlT/sK8dH1T9bNjogTN5iOWkEAKbo1J1XH1WWMMecnXAzv7rT
W3Fm+LRbFolft26DWODDNDoT1Gxt6OxYxFrOQSzfQLXq5yzhawARvjOCIv884fQq
fWdZEHTiTf2oKBttDA0ky4U1rpJ4pTIAFPW84B3RECq8Ofg1DU+2JfblMGw9uUrb
gYfmlBBaHcwBXP18uqnAPsVFMC0F8FpJwXngt7IR5MiC5tQ2tEfM1IM1BEMWbTw0
HUcPhWKnUgU3nQAtf73dfVWcaChWaxVPuzISgC1zyIo6l77+1DtCwPXLuBFpYUpy
Nmyd53TUHK03PjjYMY34UQ==
-----END CERTIFICATE REQUEST-----
简单解释一下:
Version :证书版本号,一般写0L(默认版本1)就行
Subject:明文信息比如公司地址、域名等,这个是你自己设置的
Public Key Algorithm:公钥算法
RSA Public-Key:秘钥模长
Modulus:公钥模数(根据模数和指数可以还原公钥对象来使用)
Exponent:公钥指数
Attributes:扩展属性,我这里没啥用,自己设置
Signature Algorithm:签名算法(下面的16进制就是签名密文)
在下面就是CSR的PEM格式全文
这些大概都清楚后就可以继续解决问题啦!本文只说手搓一个csr证书,至于普通方式如何生成PEM格式的CSR不做讨论,网上文章一搜一大把,也不说普通的利用Signature来签名,这种文章真的网上一搜一大把,都是基础,随便拿来用即可。
干货来啦,咱们跟着代码手搓一个CSR证书出来!
try {
List<RDN> rdns = new ArrayList<>();
rdns.add(new RDN(BCStyle.O, new DERUTF8String(csbm)));
rdns.add(new RDN(BCStyle.OU, new DERUTF8String(cpu)));
rdns.add(new RDN(BCStyle.CN, new DERUTF8String(deviceid)));
rdns.add(new RDN(BCStyle.DN_QUALIFIER, new DERIA5String(dnQualifier_value))); // 添加自定义 RDN
// 创建 X500Name
X500Name subjectDN = new X500Name(rdns.toArray(new RDN[0]));
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
LogUtil.e("test_mm", "公钥HEX===" + FileUtil.byte2hex(subjectPublicKeyInfo.getPublicKeyData().getEncoded()));
ASN1EncodableVector attributesVector = new ASN1EncodableVector();//生成一个空的属性集合
// 构建 CertificationRequestInfo
ASN1EncodableVector certReqInfoVector = new ASN1EncodableVector();
certReqInfoVector.add(new ASN1Integer(0L)); // 版本号 0 表示默认版本
certReqInfoVector.add(subjectDN); // 主题
certReqInfoVector.add(subjectPublicKeyInfo);// 公钥信息
certReqInfoVector.add(new DERTaggedObject(false, 0, new DERSet(attributesVector))); // 添加带有标签的 Attributes
// 序列化证书请求的各个组件
ASN1Sequence unsignedPart = new DERSequence(certReqInfoVector);
// 序列化 CertificationRequestInfo 的各个组件
CertificationRequestInfo certReqInfo = CertificationRequestInfo.getInstance(unsignedPart);
AlgorithmIdentifier sigAlgId = new AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption); // SHA256WithRSAEncryption
LogUtil.e("test_mm", "待签名数据Hex==" + FileUtil.byte2hex(unsignedPart.getEncoded()));
byte[] sha256Hash = FileUtil.getSHA256Hash(unsignedPart.getEncoded());
LogUtil.e("test_mm", "添加前缀前sha256Hash===" + FileUtil.byte2hex(sha256Hash));
sha256Hash = addAsn1PrefixForSHA256(sha256Hash);
LogUtil.e("test_mm", "添加前缀后sha256Hash===" + FileUtil.byte2hex(sha256Hash));
byte[] signedData = skfUtils.goSign(sha256Hash);
LogUtil.e("test_mm", "签名后的数据HEX==" + FileUtil.byte2hex(signedData));
CertificationRequest certReq = new CertificationRequest(certReqInfo, sigAlgId, new DERBitString(signedData));
LogUtil.e("test_mm", "certReq的HEX==" + FileUtil.byte2hex(certReq.getEncoded()));
// 将其包装成 PKCS#10 对象
PKCS10CertificationRequest csr = new PKCS10CertificationRequest(certReq);
LogUtil.e("test_mm", "csr的HEX======" + FileUtil.byte2hex(csr.getEncoded()));
// 以 PEM 格式输出 CSR
PemObject pemObject = new PemObject("CERTIFICATE REQUEST", csr.getEncoded());
try (PemWriter pemWriter = new PemWriter(new FileWriter("/你的输出路径/csr.pem"))) {
pemWriter.writeObject(pemObject);
}
} catch (Exception e) {
e.printStackTrace();
}
干货有了,时间紧任务急的小伙伴到此复制下代码应该就可以用了,后面我来说一下细节:
X500Name subjectDN 证书中的明文信息部分
SubjectPublicKeyInfo subjectPublicKeyInfo 证书中的公钥信息部分
ASN1EncodableVector certReqInfoVector 整个证书中需要签名的部分,记得序列化一下
ASN1Sequence unsignedPart = new DERSequence(certReqInfoVector);
AlgorithmIdentifier sigAlgId 证书中签名算法标识
CertificationRequest certReq 最终的证书结构体,分三个部分
1-信息体,包含主题和公钥信息、证书版本(待签名的源数据)
2-签名算法标志
3-签名后的数据
最终就是靠这个对象生成的csr请求证书。结构体的前两个部分我们已经有了,重点来了,就是签名!我们找到待签数据对象:
ASN1Sequence unsignedPart
什么叫签名呢?其实就是获取待签对象的摘要值,然后使用私钥进行加密,这个流程就叫“签名”,这里面有个坑,以前我们自己本地有公钥私钥的时候,我们只需要调用Signature类来帮助我们自动化进行签名即可,但是现在为了安全起见,我们的私钥是在密码器容器中,私钥数据不可读,所以我们要手动实现Signature干的事情。
签名原理刚才讲了,其实就是获取待签对象摘要值-->使用私钥加密。这里面就有个坑了,我也是找了很久才发现,看一下签名的代码:
byte[] sha256Hash = FileUtil.getSHA256Hash(unsignedPart.getEncoded());
sha256Hash = addAsn1PrefixForSHA256(sha256Hash);
重点是第二行代码,添加Asn1前缀!
public static byte[] addAsn1PrefixForSHA256(byte[] rawSig) throws Exception {
byte[] asn1Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
byte[] asn1Sig = new byte[asn1Prefix.length + rawSig.length];
System.arraycopy(asn1Prefix, 0, asn1Sig, 0, asn1Prefix.length);
System.arraycopy(rawSig, 0, asn1Sig, asn1Prefix.length, rawSig.length);
return asn1Sig;
}
如果你不加前缀的话,生成出的CSR大概率是无法通过openssl验签的,虽然直接拿出签名值和公钥,去三方网站解析,可能能验签通过,但是openssl的-verify命令大概率是无法通过的,就是因为你的摘要值前面没有加算法标志前缀。
那我们前面添加的前缀是个啥玩意呢?其实就是ASN.1(Abstract Syntax Notation One)编码的序列,也可以理解为摘要算法的标志。
SEQUENCE {
-- 0x30 开始标记
-- 0x31 长度(17 字节)
-- 0x30 SEQUENCE 开始
-- 0x0d 长度(13 字节)
-- 0x06 OBJECT IDENTIFIER (OID) 开始
-- 0x09 长度(9 字节)
-- 0x60 OID 第一部分
-- 0x86 OID 第二部分
-- 0x48 OID 第三部分
-- 0x01 OID 第四部分
-- 0x65 OID 第五部分
-- 0x03 OID 第六部分
-- 0x04 OID 第七部分
-- 0x02 OID 第八部分
-- 0x01 OID 第九部分
-- 0x05 NULL 开始
-- 0x00 NULL 长度(0 字节)
-- 0x04 OCTET STRING 开始
-- 0x20 长度(32 字节)
-- 后续 32 字节为 OCTET STRING 的内容
}
这个也是一个固定写法,比如SHA256固定增加前缀:
byte[] asn1Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
SHA1固定增加前缀:
byte[] asn1Prefix = new byte[]{0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14};
这块根据你的实际需求,添加不同的前缀即可。
之后就可以将待签数据提交给密码器的API去签名啦,这里密码器签名的方法就不放出了,都是国密标准API,调用即可。
byte[] signedData = skfUtils.goSign(sha256Hash);
至此,签名完成了,当我们三个结构体对象都有了,就可以生成CSR证书啦!
CertificationRequest certReq = new CertificationRequest(certReqInfo, sigAlgId, new DERBitString(signedData));
四.手搓JWT数据结构
讲完了使用密码器单独签名数据之后,其实JWT扩展咱们也可以手动签名了(我们业务流程中含有JWT编码格式),甚至都不用调用任何第三方的JWT框架,手撸一个即可。
简单的说JWT其实就是一个三个部分,以“.”连接的Base64字符串“头数据的JSON字符串Base64编码.实际数据的JSON字符串的Base64编码.把前两部分合起来签名后的Base64编码”。
看上去有点长哈,简单说就是3个Base64字符串,用"点"连接起来:A.B.C
A是JWT的头文件,举个例子:
{
"alg": "HS256",
"typ": "JWT"
}
主要标明签名算法和声明JWT结构,没啥可说的。
B是JWT负载数据,可以理解为实际要传递的各个参数,举个例子:
{
"username": "www.bejson.com",
"sub": "demo",
"iat": 1727676836,
"nbf": 1727676836,
"exp": 1727763236
}
C是A.B的摘要(记得添加刚才咱们学习的前缀哦)签名。(有点口误,删了,看下面一句)
C是A.B的签名(签名指的就是按算法取摘要,添加算法前缀后,对这个带前缀的数据进行私钥加密)
给大家一个实际的测试例子,大家去解析网站上看下就知道啦:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImlhdCI6MTcyNzY3NjgzNiwibmJmIjoxNzI3Njc2ODM2LCJleHAiOjE3Mjc3NjMyMzZ9.lloqUo2P2QbCUhXp9t-9sGQEkB8tx-n4_Tf8vSfZ65Y
文章至此完结撒花~
版权归原作者 jjdy2006 所有, 如有侵权,请联系我们删除。