手搓CSR签名请求证书,使用满足SM规范的密码器签名(保护私钥安全),顺道讲一下JWT标准

一.需求背景

        同志们,我又来了!不知道你们会不会遇到高标准严要求的上游规范。我司设备上游文档中标明:需要使用满足国密规范的密码器设备来创建、保护秘钥对,同时申请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

文章至此完结撒花~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值