四川天府银行国密双向认证

四川天府银行国密双向认证

初步分析

1
https://m.tf.cn:4453/mcloudbank/ODC/operation/market/activity/app/drawActivityAwardBz

使用chrome直接访问,提示ERR_SSL_VERSION_OR_CIPHER_MISMATCH

image-20250608135212767

使用支持国密的浏览器访问,提示:SSL server requires client certificate

image-20250608135329093

请求https的逻辑全部在libGMJNISSL.so中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package com.zsmarter.zsbird.network.core.gmssl;


public final class CallServerInterceptor implements Interceptor {

@Override // com.zsmarter.zsbird.network.core.gmssl.Interceptor
public Response intercept(Chain interceptor$Chain0) throws IOException {
long v5;
MediaType mediaType0;
Intrinsics.checkNotNullParameter(interceptor$Chain0, "chain");
RealChain realChain0 = (RealChain)interceptor$Chain0;
RealCall realCall0 = realChain0.getCall$zsbird_network_release();
int v = realChain0.getTimeout$zsbird_network_release();
Request request0 = realChain0.request();
String s = request0.getUrl();
String s1 = request0.getMethod();
Headers headers0 = request0.getHeaders();
RequestBody requestBody0 = request0.getBody();
if(requestBody0 == null) {
mediaType0 = MediaType.Companion.get("application/json;charset=utf-8");
}
else {
mediaType0 = requestBody0.contentType();
if(mediaType0 == null) {
mediaType0 = MediaType.Companion.get("application/json;charset=utf-8");
}
}

long v1 = System.currentTimeMillis();
Uri uri0 = Uri.parse(s);
String s2 = uri0.getHost();
int v2 = uri0.getPort();
if(v2 == -1) {
v2 = 443;
}

String s3 = StringsKt.substringAfter$default(s, (uri0.getPort() == -1 ? String.valueOf(s2) : s2 + ':' + v2), null, 2, null);
GMJNISSL gMJNISSL0 = new GMJNISSL();
gMJNISSL0.setTimeout(v * 1000);
realCall0.setOnCancel$zsbird_network_release(((Function0)new CallServerInterceptor.intercept.gmjnissl.1.1(gMJNISSL0)));
gMJNISSL0.setHostname(s2);
gMJNISSL0.setPort(v2);
gMJNISSL0.setUri(s3);
if(gMJNISSL0.create()) {
this.setupTwoWayAuth(gMJNISSL0);
Map map0 = MapsKt.toMutableMap(MapsKt.toMap(((Iterable)headers0)));
map0.put("Content-Type", mediaType0.toString());
for(Object object0: map0.entrySet()) {
Map.Entry map$Entry0 = (Map.Entry)object0;
gMJNISSL0.addHttpHeader(((String)map$Entry0.getKey()) + ':' + ((String)map$Entry0.getValue()));
}

int v3 = requestBody0 == null ? 0 : ((int)requestBody0.contentLength());
if((Intrinsics.areEqual(s1, "GET")) && gMJNISSL0.get() != 0) {//<--------------
throw this.currentException(gMJNISSL0.errCode());
}

if(Intrinsics.areEqual(s1, "POST")) {
if(gMJNISSL0.post(v3) == 0) {//<--------------
RequestBodySink callServerInterceptor$RequestBodySink0 = new RequestBodySink(gMJNISSL0, v);
realCall0.setOnCancel$zsbird_network_release(((Function0)new CallServerInterceptor.intercept.3(callServerInterceptor$RequestBodySink0)));
BufferedSink bufferedSink0 = Okio.buffer(((Sink)callServerInterceptor$RequestBodySink0));
if(requestBody0 != null) {
requestBody0.writeTo(bufferedSink0);
}

bufferedSink0.flush();
goto label_156;
}

throw this.currentException(gMJNISSL0.errCode());
}

label_156:
if(gMJNISSL0.readResponseHeader() == 0) {
byte[] arr_b = gMJNISSL0.getResponseProtocolName();
Intrinsics.checkNotNullExpressionValue(arr_b, "gmjnissl.responseProtocolName");
String s4 = new String(arr_b, Charsets.UTF_8);
int v4 = gMJNISSL0.statusCode();
byte[] arr_b1 = gMJNISSL0.getResponseProtocolMessage();
Intrinsics.checkNotNullExpressionValue(arr_b1, "gmjnissl.responseProtocolMessage");
String s5 = new String(arr_b1, Charsets.UTF_8);
com.zsmarter.zsbird.network.core.gmssl.Headers.Companion headers$Companion0 = Headers.Companion;
byte[] arr_b2 = gMJNISSL0.getResponseHeader();
Intrinsics.checkNotNullExpressionValue(arr_b2, "gmjnissl.responseHeader");
Iterable iterable0 = (Iterable)StringsKt.split$default(((CharSequence)StringsKt.trim(((CharSequence)new String(arr_b2, Charsets.UTF_8))).toString()), new String[]{"\r\n"}, false, 0, 6, null);
Collection collection0 = (Collection)new ArrayList(CollectionsKt.collectionSizeOrDefault(iterable0, 10));
for(Object object1: iterable0) {
List list0 = StringsKt.split$default(((CharSequence)(((String)object1))), new String[]{":"}, false, 0, 6, null);
collection0.add(TuplesKt.to(StringsKt.trim(((CharSequence)(((String)list0.get(0))))).toString(), StringsKt.trim(((CharSequence)(((String)list0.get(1))))).toString()));
}

Object[] arr_object = ((Collection)(((List)collection0))).toArray(new Pair[0]);
if(arr_object != null) {
Pair[] arr_pair = (Pair[])arr_object;
Headers headers1 = headers$Companion0.of(MapsKt.mapOf(((Pair[])Arrays.copyOf(arr_pair, arr_pair.length))));
Ref.ObjectRef ref$ObjectRef0 = new Ref.ObjectRef();
ref$ObjectRef0.element = new ResponseBodySource(gMJNISSL0, v);
if(StringsKt.equals$default(headers1.get("Transfer-Encoding"), "chunked", false, 2, null)) {
ref$ObjectRef0.element = new ChunkedSource(Okio.buffer(((Source)ref$ObjectRef0.element)));
}

realCall0.setOnCancel$zsbird_network_release(((Function0)new CallServerInterceptor.intercept.4(ref$ObjectRef0)));
String s6 = headers1.get("Content-Type");
MediaType mediaType1 = s6 == null ? null : MediaType.Companion.parse(s6);
String s7 = headers1.get("Content-Length");
if(s7 == null) {
v5 = -1L;
}
else {
Long long0 = StringsKt.toLongOrNull(s7);
v5 = long0 == null ? -1L : ((long)long0);
}

BufferedSource bufferedSource0 = Okio.buffer(((Source)ref$ObjectRef0.element));
ResponseBody responseBody0 = ResponseBody.Companion.asResponseBody(bufferedSource0, mediaType1, v5);
return new Builder().code(v4).message(s5).protocol(s4).headers(headers1).body(responseBody0).sentRequestAtMillis(v1).receivedResponseAtMillis(System.currentTimeMillis()).build();
}

throw new NullPointerException("null cannot be cast to non-null type kotlin.Array<T of kotlin.collections.ArraysKt__ArraysJVMKt.toTypedArray>");
}

throw new IOException("GMJNISSL read response line and headers failed.");
}

throw new IOException("GMJNISSL create failed.");
}

private final void setupTwoWayAuth(GMJNISSL gMJNISSL0) {
JniLib1747810361.cV(new Object[]{CallServerInterceptor.class, this, gMJNISSL0, ((int)0x4315)});
}
}


image-20250604033826164

通过frida hook一些导出函数,最后发现请求时会调用这些关键函数,用来设置证书、密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Interceptor.attach(Module.findExportByName("libGMJNISSL.so", "_ZN13GmsslProvider14parseServerPfxEv"), {
onEnter: function (args) {
console.log('_ZN13GmsslProvider14parseServerPfxEv...')
},
onLeave: function (retval) {

}
});

Interceptor.attach(Module.findExportByName("libGMJNISSL.so", "_ZN13GmsslProvider17parseClientSigPfxEv"), {
onEnter: function (args) {
console.log('_ZN13GmsslProvider17parseClientSigPfxEv...')
},
onLeave: function (retval) {

}
});

Interceptor.attach(Module.findExportByName("libGMJNISSL.so", "_ZN13GmsslProvider17parseClientEncPfxEv"), {
onEnter: function (args) {
console.log('_ZN13GmsslProvider17parseClientEncPfxEv...')
},
onLeave: function (retval) {

}
});

Interceptor.attach(Module.findExportByName("libGMJNISSL.so", "SSL_CTX_set_cipher_list"), {
onEnter: function (args) {
console.log('SSL_CTX_set_cipher_list...')
console.log('SSL_CTX_set_cipher_list args[1]', Memory.readCString(args[1]))
},
onLeave: function (retval) {

}
});

image-20250608224821689

获取enc, sig证书及密钥

使用frida hookPKCS12_parse 可以获取到密钥

1
int PKCS12_parse(PKCS12 *p12, const char *pass, EVP_PKEY **pkey, X509 **cert, STACK_OF(X509) **ca);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Java.perform(function () {
const crypto = Process.findModuleByName("libGMJNISSL.so");

// 获取目标函数地址
const PKCS12_parse = Module.findExportByName("libGMJNISSL.so", "PKCS12_parse");

// 获取 OpenSSL 辅助函数
const BIO_s_mem = Module.findExportByName("libGMJNISSL.so", "BIO_s_mem");
const BIO_new = Module.findExportByName("libGMJNISSL.so", "BIO_new");
const BIO_free = Module.findExportByName("libGMJNISSL.so", "BIO_free");
const PEM_write_bio_PrivateKey = Module.findExportByName("libGMJNISSL.so", "PEM_write_bio_PrivateKey");
const PEM_write_bio_X509 = Module.findExportByName("libGMJNISSL.so", "PEM_write_bio_X509");
const BIO_ctrl_pending = Module.findExportByName("libGMJNISSL.so", "BIO_ctrl_pending");
const BIO_read = Module.findExportByName("libGMJNISSL.so", "BIO_read");

// 创建 NativeFunction 对象
const bio_s_mem = new NativeFunction(BIO_s_mem, "pointer", []);
const bio_new = new NativeFunction(BIO_new, "pointer", ["pointer"]);
const bio_free = new NativeFunction(BIO_free, "int", ["pointer"]);
const pem_write_pkey = new NativeFunction(
PEM_write_bio_PrivateKey,
"int",
["pointer", "pointer", "pointer", "pointer", "int", "pointer", "pointer"]
);
const pem_write_x509 = new NativeFunction(
PEM_write_bio_X509,
"int",
["pointer", "pointer"]
);
const bio_ctrl_pending = new NativeFunction(
BIO_ctrl_pending,
"int",
["pointer"]
);
const bio_read = new NativeFunction(
BIO_read,
"int",
["pointer", "pointer", "int"]
);

// Hook PKCS12_parse
Interceptor.attach(PKCS12_parse, {
onEnter: function (args) {
this.p12 = args[0];
this.pass = args[1].readCString(); // 读取密码
this.pkeyPtr = args[2]; // 保存二级指针地址
this.certPtr = args[3]; // 保存二级指针地址
},

onLeave: function (retval) {
if (retval.toInt32() === 1) { // 仅当解析成功时处理
// 1. 获取实际指针值
const pkey = this.pkeyPtr.readPointer();
const cert = this.certPtr.readPointer();

// 2. 创建内存 BIO
const mem_bio_method = bio_s_mem();
const bio = bio_new(mem_bio_method);

// 3. 导出私钥为 PEM
pem_write_pkey(bio, pkey, NULL, NULL, 0, NULL, NULL);
const pkeySize = bio_ctrl_pending(bio);
const pkeyBuffer = Memory.alloc(pkeySize + 1);
bio_read(bio, pkeyBuffer, pkeySize);
const pemPkey = pkeyBuffer.readCString();

// 4. 导出证书为 PEM
pem_write_x509(bio, cert);
const certSize = bio_ctrl_pending(bio);
const certBuffer = Memory.alloc(certSize + 1);
bio_read(bio, certBuffer, certSize);
const pemCert = certBuffer.readCString();

// 5. 清理 BIO
bio_free(bio);

// 6. 输出结果
console.log("PKCS12 解析成功!");
console.log("使用的密码: " + this.pass);
console.log(pemPkey);
console.log(pemCert);


}
}
});

console.log("[+] PKCS12_parse hook 已安装");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[+] PKCS12_parse hook 已安装
SSL_CTX_set_cipher_list...
SSL_CTX_set_cipher_list args[1] ECC-SM4-CBC-SM3
_ZN13GmsslProvider14parseServerPfxEv...
Error: access violation accessing 0x0
at <anonymous> (frida/runtime/core.js:145)
at onLeave (/script1.js:93)
_ZN13GmsslProvider17parseClientSigPfxEv...
PKCS12 解析成功!
使用的密码: tfGMclient
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgn6WY2gadCo7PTqTp
+tRGgf187Sht5tNo3/f8JjaiDuuhRANCAASJYoqGOABt7785LoGtXv9WTZ1nzlJq
wfabGHBcj2Ck39MdY5/L4wpnY8Z3X2IQBFd1drjLuvSirrmfRux4QJoo
-----END PRIVATE KEY-----

-----BEGIN CERTIFICATE-----
MIICGzCCAcGgAwIBAgIBBTAKBggqgRzPVQGDdTBTMQswCQYDVQQGEwJDTjEQMA4G
A1UECAwHU2ljaHVhbjETMBEGA1UECgwKdGlhbmZ1YmFuazEMMAoGA1UECwwDaXRj
MQ8wDQYDVQQDDAZ0ZmdtY2EwHhcNMjIwNDI3MDE0MzM3WhcNNDIwNTEyMDE0MzM3
WjBRMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHU2ljaHVhbjETMBEGA1UECgwKdGlh
bmZ1YmFuazEMMAoGA1UECwwDaXRjMQ0wCwYDVQQDDARzaWduMFkwEwYHKoZIzj0C
AQYIKoEcz1UBgi0DQgAEiWKKhjgAbe+/OS6BrV7/Vk2dZ85SasH2mxhwXI9gpN/T
HWOfy+MKZ2PGd19iEARXdXa4y7r0oq65n0bseECaKKOBhzCBhDAJBgNVHRMEAjAA
MAsGA1UdDwQEAwIHgDAqBglghkgBhvhCAQ0EHRYbR21TU0wgR2VuZXJhdGVkIENl
cnRpZmljYXRlMB0GA1UdDgQWBBR06vQ672Zv8lVpo+OtMYRDyTxVBzAfBgNVHSME
GDAWgBT+nKMxi5B7iewYpxB1aa71yvFWODAKBggqgRzPVQGDdQNIADBFAiAgdfvA
1RRnnL46G/T/I3o4kkjXBYDPlEeZ0km0RL0n7AIhALE4o8qEhy5L6g3NCQ5MhM9y
lDRe3L1/Zt+3MQ5AejRG
-----END CERTIFICATE-----

_ZN13GmsslProvider17parseClientEncPfxEv...
PKCS12 解析成功!
使用的密码: tfGMclient
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgCqmO4/AGWSMoZdn4
fxPTYAwGIcS8nBQP4WFKP8O/fjuhRANCAAQNthT09j0SWQOl44LyDnLE7q7nuy8r
AmWCSTC56vpTf0iz0HUneNp0w46dU1SkrG8irEAh9WaRcxTTAYOBcQ8z
-----END PRIVATE KEY-----

-----BEGIN CERTIFICATE-----
MIICGjCCAcGgAwIBAgIBBDAKBggqgRzPVQGDdTBTMQswCQYDVQQGEwJDTjEQMA4G
A1UECAwHU2ljaHVhbjETMBEGA1UECgwKdGlhbmZ1YmFuazEMMAoGA1UECwwDaXRj
MQ8wDQYDVQQDDAZ0ZmdtY2EwHhcNMjIwNDI3MDE0MDI2WhcNNDIwNTEyMDE0MDI2
WjBRMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHU2ljaHVhbjETMBEGA1UECgwKdGlh
bmZ1YmFuazEMMAoGA1UECwwDaXRjMQ0wCwYDVQQDDARlbmNyMFkwEwYHKoZIzj0C
AQYIKoEcz1UBgi0DQgAEDbYU9PY9ElkDpeOC8g5yxO6u57svKwJlgkkwuer6U39I
s9B1J3jadMOOnVNUpKxvIqxAIfVmkXMU0wGDgXEPM6OBhzCBhDAJBgNVHRMEAjAA
MAsGA1UdDwQEAwIFIDAqBglghkgBhvhCAQ0EHRYbR21TU0wgR2VuZXJhdGVkIENl
cnRpZmljYXRlMB0GA1UdDgQWBBT0eO5nfFlCWPG7AeneYQ8hdv/fbDAfBgNVHSME
GDAWgBT+nKMxi5B7iewYpxB1aa71yvFWODAKBggqgRzPVQGDdQNHADBEAiAllfo3
7TSMjTOUc3FSOdXycx+LKTb51ZOGJ3BUfArFrwIgVlwjx41qetuY2gpdGYEPeQDB
FBp7VfnmoOweNPli+gs=
-----END CERTIFICATE-----

使用dump出来的密钥、证书+支持国密的SSL即可访问开启双向校验的网站。(curl -k 忽略证书错误)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/root/curl/src/curl --tlcp -kv --sign-cert  client.sig.crt.pem --sign-key client.sig.key.pem --enc-cert client.enc.crt.pem --enc-key client.enc.key.pem https://m.tf.cn:4453/

* Host m.tf.cn:4453 was resolved.
* IPv6: (none)
* IPv4: 218.88.113.203
* Trying 218.88.113.203:4453...
* ALPN: curl offers http/1.1
* (101) (OUT), , Unknown (1):
* (101) (IN), , Unknown (2):
* (101) (IN), , Unknown (11):
* (101) (IN), , Unknown (12):
* (101) (IN), , Unknown (13):
* (101) (IN), , Unknown (14):
* (101) (OUT), , Unknown (11):
* (101) (OUT), , Unknown (16):
* (101) (OUT), , Unknown (15):
* (101) (OUT), , Change cipher spec (1):
* (101) (OUT), , Unknown (20):
* (101) (IN), , Unknown (20):
* SSL connection using NTLSv1.1 / ECC-SM2-SM4-CBC-SM3 / UNDEF / SM2
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: C=CN; ST=\U56DB\U5DDD\U7701; L=\U5357\U5145\U5E02; O=\U56DB\U5DDD\U5929\U5E9C\U94F6\U884C\U80A1\U4EFD\U6709\U9650\U516C\U53F8; OU=\U56DB\U5DDD\U5929\U5E9C\U94F6\U884C\U80A1\U4EFD\U6709\U9650\U516C\U53F8; CN=*.tf.cn
* start date: May 13 07:17:12 2025 GMT
* expire date: May 13 07:17:12 2026 GMT
* issuer: C=CN; O=Sichuan Digital Certificate Authority Management Center; OU=SM2 Certificate System; CN=SCCA SSL CA1
* SSL certificate verify result: self-signed certificate in certificate chain (19), continuing anyway.
* Certificate level 0: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Certificate level 1: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Certificate level 2: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Certificate level 3: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Connected to m.tf.cn (218.88.113.203) port 4453
* using HTTP/1.x
> GET / HTTP/1.1
> Host: m.tf.cn:4453
> User-Agent: curl/8.12.0-DEV
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: openresty
< Date: Sun, 08 Jun 2025 13:46:25 GMT
< Content-Type: text/html
< Last-Modified: Wed, 20 Oct 2021 09:20:13 GMT
< ETag: "616fdf4d-69"
< Accept-Ranges: bytes
< Content-Length: 105
< Connection: Keep-alive
< Via: 1.1 ID-0314217225247104 uproxy-9
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome !</title>
</head>
<body>
<h1>Welcome !</h1>
</body>
</html>
* Connection #0 to host m.tf.cn left intact

获取完整CA证书链

方法一

bool __cdecl GmsslProvider::parseServerPfx(GmsslProvider *this) 会调用PKCS12_parse解析证书,PKCS12_parse的最后一个参数是STACK_OF(X509) **ca, 但通过尝试,这儿dump出来的CA证书链并不是完整的(不完整的CA证书链也没有出现在ssl_verify_cert_chain导出的证书链中?)。

于是尝试hookssl_verify_cert_chain

1
int ssl_verify_cert_chain(SSL *s, STACK_OF(X509) *sk);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
Java.perform(function () {
console.log('start...')
const libcrypto = Process.findModuleByName("libGMJNISSL.so");

// 获取目标函数地址
const PKCS12_parse = Module.findExportByName("libGMJNISSL.so", "PKCS12_parse");

// 获取 OpenSSL 辅助函数
const BIO_s_mem = Module.findExportByName("libGMJNISSL.so", "BIO_s_mem");
const BIO_new = Module.findExportByName("libGMJNISSL.so", "BIO_new");
const BIO_free = Module.findExportByName("libGMJNISSL.so", "BIO_free");
const PEM_write_bio_PrivateKey = Module.findExportByName("libGMJNISSL.so", "PEM_write_bio_PrivateKey");
const PEM_write_bio_X509 = Module.findExportByName("libGMJNISSL.so", "PEM_write_bio_X509");
const BIO_ctrl_pending = Module.findExportByName("libGMJNISSL.so", "BIO_ctrl_pending");
const BIO_read = Module.findExportByName("libGMJNISSL.so", "BIO_read");

// STACK_OF(X509) 相关函数
const sk_X509_num = Module.findExportByName("libGMJNISSL.so", "OPENSSL_sk_num");
const sk_X509_value = Module.findExportByName("libGMJNISSL.so", "OPENSSL_sk_value");
const sk_X509_free = Module.findExportByName("libGMJNISSL.so", "OPENSSL_sk_free");

// 创建 NativeFunction 对象
const bio_s_mem = new NativeFunction(BIO_s_mem, "pointer", []);
const bio_new = new NativeFunction(BIO_new, "pointer", ["pointer"]);
const bio_free = new NativeFunction(BIO_free, "int", ["pointer"]);
const pem_write_pkey = new NativeFunction(
PEM_write_bio_PrivateKey,
"int",
["pointer", "pointer", "pointer", "pointer", "int", "pointer", "pointer"]
);
const pem_write_x509 = new NativeFunction(
PEM_write_bio_X509,
"int",
["pointer", "pointer"]
);
const bio_ctrl_pending = new NativeFunction(
BIO_ctrl_pending,
"int",
["pointer"]
);
const bio_read = new NativeFunction(
BIO_read,
"int",
["pointer", "pointer", "int"]
);

// STACK_OF(X509) 函数
const stack_num = new NativeFunction(
sk_X509_num,
"int",
["pointer"]
);
const stack_value = new NativeFunction(
sk_X509_value,
"pointer",
["pointer", "int"]
);
const stack_free = new NativeFunction(
sk_X509_free,
"void",
["pointer"]
);

// 导出单个证书为PEM的辅助函数
function exportX509ToPEM(certPtr) {
const mem_bio_method = bio_s_mem();
const bio = bio_new(mem_bio_method);

pem_write_x509(bio, certPtr);
const certSize = bio_ctrl_pending(bio);
const certBuffer = Memory.alloc(certSize + 1);
bio_read(bio, certBuffer, certSize);
const pemCert = certBuffer.readCString();

bio_free(bio);
return pemCert;
}


// int ssl_verify_cert_chain(SSL *s, STACK_OF(X509) *sk);
Interceptor.attach(Module.findExportByName("libGMJNISSL.so", "ssl_verify_cert_chain"), {
onEnter: function (args) {
console.log('ssl_verify_cert_chain...')
this.sk = args[1];
},
onLeave: function (retval) {
var caPtr = ptr(this.sk)
if (!caPtr.isNull()) {
const caStackPtr = caPtr
const numCAs = stack_num(caStackPtr);
console.log(`找到 ${numCAs} 个CA证书`);
let caChainPEMs = [];
for (let i = 0; i < numCAs; i++) {
const caCertPtr = stack_value(caStackPtr, i);
const pemCA = exportX509ToPEM(caCertPtr);
caChainPEMs.push(pemCA);
}

caChainPEMs.forEach((pem, index) => {

console.log(pem);
});

// 可选:释放证书链内存
// stack_free(caStackPtr);
}

}
});



console.log("[+] ssl_verify_cert_chain hook 已安装");
});


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
ssl_verify_cert_chain...
找到 4 个CA证书
-----BEGIN CERTIFICATE-----
MIIECDCCA6ugAwIBAgIUQrr0homSy4TN0/wc79FMWgyqH1IwDAYIKoEcz1UBg3UF
ADCBhzELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFTATBgNVBAMMDFNDQ0EgU1NMIENBMTAeFw0y
NTA1MTMwNzE3MTJaFw0yNjA1MTMwNzE3MTJaMIGlMQswCQYDVQQGDAJDTjESMBAG
A1UECAwJ5Zub5bed55yBMRIwEAYDVQQHDAnljZflhYXluIIxLTArBgNVBAoMJOWb
m+W3neWkqeW6nOmTtuihjOiCoeS7veaciemZkOWFrOWPuDEtMCsGA1UECwwk5Zub
5bed5aSp5bqc6ZO26KGM6IKh5Lu95pyJ6ZmQ5YWs5Y+4MRAwDgYDVQQDDAcqLnRm
LmNuMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEadhW/75zIEleH5fveW9cvNrP
kZdxOACUz2j4FQdhnstibLXTtcb0dzIASR4d3ARB6mq8mHSHt6+Hq78v8g4cZ6OC
AdEwggHNMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMBIGA1UdEQQLMAmCByoudGYuY24wfAYIKwYBBQUHAQEEcDBuMC4GCCsGAQUF
BzABhiJodHRwOi8vdG9wY2Euc2NjYS5jb20uY246ODE4NS9vY3NwMDwGCCsGAQUF
BzAChjBodHRwOi8vdG9wY2Euc2NjYS5jb20uY24vY2FjZXJ0L3NjY2Ffc3NsX2Nh
MS5jcnQwcAYDVR0fBGkwZzBloGOgYYZfaHR0cDovL3RvcGNhLnNjY2EuY29tLmNu
L3B1YmxpYy9pdHJ1c2NybD9DQT0wRUE0RUUxRTU1MUVGOUUzNDJEQjkxMzY4NTk1
QUNGRkYyQzJGMjhBJmVuY29kZT1kZXIwHwYDVR0jBBgwFoAU+aX+qel2+nOLKxyp
VMb10zlqezkwHQYDVR0OBBYEFOdx8mFRwcBeFzsciBocTAtER5owMEoGA1UdIARD
MEEwNQYJKoEchvAGAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc2NjYS5j
b20uY24vY3BzMAgGBmeBDAECAjAOBgNVHQ8BAf8EBAMCBsAwDAYIKoEcz1UBg3UF
AANJADBGAiEAiBSyX2VtxU/pizvrxb+eHJJpMjsBm6gxdurTqxtOaboCIQDk6kuj
MEQ+11kcf9408Euhthxd+Ov0Ajq2rX5Wl9+0Ow==
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
MIIEBzCCA6ugAwIBAgIUCd9jL72dnvOV3GKwwY/1dM/4TAkwDAYIKoEcz1UBg3UF
ADCBhzELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFTATBgNVBAMMDFNDQ0EgU1NMIENBMTAeFw0y
NTA1MTMwNzE3MTJaFw0yNjA1MTMwNzE3MTJaMIGlMQswCQYDVQQGDAJDTjESMBAG
A1UECAwJ5Zub5bed55yBMRIwEAYDVQQHDAnljZflhYXluIIxLTArBgNVBAoMJOWb
m+W3neWkqeW6nOmTtuihjOiCoeS7veaciemZkOWFrOWPuDEtMCsGA1UECwwk5Zub
5bed5aSp5bqc6ZO26KGM6IKh5Lu95pyJ6ZmQ5YWs5Y+4MRAwDgYDVQQDDAcqLnRm
LmNuMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE4Hb8nzV5u2gQBMZFPivH66G8
VY/sGUWKwLngd34AFN6KYxgH2RAEOrq95PHRnW/bc1PjakjvscPrOnSAJh7rn6OC
AdEwggHNMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMBIGA1UdEQQLMAmCByoudGYuY24wfAYIKwYBBQUHAQEEcDBuMC4GCCsGAQUF
BzABhiJodHRwOi8vdG9wY2Euc2NjYS5jb20uY246ODE4NS9vY3NwMDwGCCsGAQUF
BzAChjBodHRwOi8vdG9wY2Euc2NjYS5jb20uY24vY2FjZXJ0L3NjY2Ffc3NsX2Nh
MS5jcnQwcAYDVR0fBGkwZzBloGOgYYZfaHR0cDovL3RvcGNhLnNjY2EuY29tLmNu
L3B1YmxpYy9pdHJ1c2NybD9DQT0wRUE0RUUxRTU1MUVGOUUzNDJEQjkxMzY4NTk1
QUNGRkYyQzJGMjhBJmVuY29kZT1kZXIwHwYDVR0jBBgwFoAU+aX+qel2+nOLKxyp
VMb10zlqezkwHQYDVR0OBBYEFO6B7chHuhTsLU/k1at2Mg05/GjvMEoGA1UdIARD
MEEwNQYJKoEchvAGAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc2NjYS5j
b20uY24vY3BzMAgGBmeBDAECAjAOBgNVHQ8BAf8EBAMCAzgwDAYIKoEcz1UBg3UF
AANIADBFAiEA9TPZX7N9trdGZUz8TL7jlx+dAwoLVIKciZdBL22jCsUCIEILa8EG
xynO9AT4TD/CyQgUSJ0ZCDeDjMsIsr+xbTXY
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
MIIDoDCCA0SgAwIBAgIUDqTuHlUe+eNC25E2hZWs//LC8oowDAYIKoEcz1UBg3UF
ADCBiDELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFjAUBgNVBAMMDVNDQ0EgUm9vdCBDQTEwHhcN
MjIwODI5MDg1NjI4WhcNNDIwODI0MDg1NjI4WjCBhzELMAkGA1UEBhMCQ04xQDA+
BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgTWFu
YWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNNMiBDZXJ0aWZpY2F0ZSBTeXN0ZW0x
FTATBgNVBAMMDFNDQ0EgU1NMIENBMTBZMBMGByqGSM49AgEGCCqBHM9VAYItA0IA
BFNTIvnr+0ik91e6RvQ4ZwPqxoWMAwf9qiZWuxOURaBudRbYbbWFPn/1MSqlaAQc
OC8Qb5++Ec9+CGtkuPAQj3mjggGHMIIBgzAfBgNVHSMEGDAWgBQjF/5MGKmyHjDu
BX6yKPnUYDzsrTAdBgNVHQ4EFgQU+aX+qel2+nOLKxypVMb10zlqezkwfQYIKwYB
BQUHAQEEcTBvMC4GCCsGAQUFBzABhiJodHRwOi8vdG9wY2Euc2NjYS5jb20uY246
ODE4NS9vY3NwMD0GCCsGAQUFBzAChjFodHRwOi8vdG9wY2Euc2NjYS5jb20uY24v
Y2FjZXJ0L3NjY2Ffcm9vdF9jYTEuY3J0MEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHA6
Ly90b3BjYS5zY2NhLmNvbS5jbi9jYXJsL3NjY2Ffcm9vdF9jYTEuY3JsMEAGA1Ud
IAQ5MDcwNQYJKoEchvAGAQECMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc2Nj
YS5jb20uY24vY3BzMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNV
HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAMBggqgRzPVQGDdQUAA0gAMEUC
IQDtoL36XmBwzuvL0QCcVLcpJGTxzrLMbPiWsESZqSbZDwIgNRP0dUdu6jYVzHgk
YKyaSuiPr/wpH9Kdusiqqge/Y8A=
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
MIICezCCAh+gAwIBAgIUTHK30RB8qDw+IdS0cNZ0M+O/UcQwDAYIKoEcz1UBg3UF
ADCBiDELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFjAUBgNVBAMMDVNDQ0EgUm9vdCBDQTEwHhcN
MjIwODI5MDcxMjAwWhcNNDcwODIwMDcxMjAwWjCBiDELMAkGA1UEBhMCQ04xQDA+
BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgTWFu
YWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNNMiBDZXJ0aWZpY2F0ZSBTeXN0ZW0x
FjAUBgNVBAMMDVNDQ0EgUm9vdCBDQTEwWTATBgcqhkjOPQIBBggqgRzPVQGCLQNC
AASq4IiHhfqbZKjpJwghCJ0jifnGIYpPskjd1R3f2CzqGhFGIAhTTNsX/lSTH7oZ
AKWcr1SJXcu+pT9asQAo7GVNo2MwYTAdBgNVHQ4EFgQUIxf+TBipsh4w7gV+sij5
1GA87K0wHwYDVR0jBBgwFoAUIxf+TBipsh4w7gV+sij51GA87K0wDwYDVR0TAQH/
BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDAYIKoEcz1UBg3UFAANIADBFAiBEMYX3
OvlmmXlk/fv3eU+uirPlEHFoWRjttxn5DhdmSwIhAM1vJBDy6UWaiusyd7rUv1Ij
h3CRHFgTKC8pzWSrwF86
-----END CERTIFICATE-----
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/root/curl/src/curl --tlcp -v --cacert ca.pem --sign-cert  client.sig.crt.pem --sign-key  client.sig.key.pem --enc-cert client.enc.crt.pem --enc-key client.enc.key.pem https://m.tf.cn:4453/
* Host m.tf.cn:4453 was resolved.
* IPv6: (none)
* IPv4: 218.88.113.203
* Trying 218.88.113.203:4453...
* ALPN: curl offers http/1.1
* (101) (OUT), , Unknown (1):
* CAfile: ca.pem
* CApath: /etc/ssl/certs
* (101) (IN), , Unknown (2):
* (101) (IN), , Unknown (11):
* (101) (IN), , Unknown (12):
* (101) (IN), , Unknown (13):
* (101) (IN), , Unknown (14):
* (101) (OUT), , Unknown (11):
* (101) (OUT), , Unknown (16):
* (101) (OUT), , Unknown (15):
* (101) (OUT), , Change cipher spec (1):
* (101) (OUT), , Unknown (20):
* (101) (IN), , Unknown (20):
* SSL connection using NTLSv1.1 / ECC-SM2-SM4-CBC-SM3 / UNDEF / SM2
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: C=CN; ST=\U56DB\U5DDD\U7701; L=\U5357\U5145\U5E02; O=\U56DB\U5DDD\U5929\U5E9C\U94F6\U884C\U80A1\U4EFD\U6709\U9650\U516C\U53F8; OU=\U56DB\U5DDD\U5929\U5E9C\U94F6\U884C\U80A1\U4EFD\U6709\U9650\U516C\U53F8; CN=*.tf.cn
* start date: May 13 07:17:12 2025 GMT
* expire date: May 13 07:17:12 2026 GMT
* subjectAltName: host "m.tf.cn" matched cert's "*.tf.cn"
* issuer: C=CN; O=Sichuan Digital Certificate Authority Management Center; OU=SM2 Certificate System; CN=SCCA SSL CA1
* SSL certificate verify ok.
* Certificate level 0: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Certificate level 1: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Connected to m.tf.cn (218.88.113.203) port 4453
* using HTTP/1.x
> GET / HTTP/1.1
> Host: m.tf.cn:4453
> User-Agent: curl/8.12.0-DEV
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: openresty
< Date: Sun, 08 Jun 2025 14:52:37 GMT
< Content-Type: text/html
< Last-Modified: Wed, 20 Oct 2021 09:20:13 GMT
< ETag: "616fdf4d-69"
< Accept-Ranges: bytes
< Content-Length: 105
< Connection: Keep-alive
< Via: 1.1 ID-5301755310443456 uproxy-2
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome !</title>
</head>
<body>
<h1>Welcome !</h1>
</body>
</html>
* Connection #0 to host m.tf.cn left intact

方法二

在支持国密的浏览器中打开目标网站,依此导出证书文件。

image-20250608230417717

将3个证书文件合并,就是完整的CA链了。

1
cat 1.pem 2.pem 3.pem > ca.pem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-----BEGIN CERTIFICATE-----
MIIECDCCA6ugAwIBAgIUQrr0homSy4TN0/wc79FMWgyqH1IwDAYIKoEcz1UBg3UF
ADCBhzELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFTATBgNVBAMMDFNDQ0EgU1NMIENBMTAeFw0y
NTA1MTMwNzE3MTJaFw0yNjA1MTMwNzE3MTJaMIGlMQswCQYDVQQGDAJDTjESMBAG
A1UECAwJ5Zub5bed55yBMRIwEAYDVQQHDAnljZflhYXluIIxLTArBgNVBAoMJOWb
m+W3neWkqeW6nOmTtuihjOiCoeS7veaciemZkOWFrOWPuDEtMCsGA1UECwwk5Zub
5bed5aSp5bqc6ZO26KGM6IKh5Lu95pyJ6ZmQ5YWs5Y+4MRAwDgYDVQQDDAcqLnRm
LmNuMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEadhW/75zIEleH5fveW9cvNrP
kZdxOACUz2j4FQdhnstibLXTtcb0dzIASR4d3ARB6mq8mHSHt6+Hq78v8g4cZ6OC
AdEwggHNMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMBIGA1UdEQQLMAmCByoudGYuY24wfAYIKwYBBQUHAQEEcDBuMC4GCCsGAQUF
BzABhiJodHRwOi8vdG9wY2Euc2NjYS5jb20uY246ODE4NS9vY3NwMDwGCCsGAQUF
BzAChjBodHRwOi8vdG9wY2Euc2NjYS5jb20uY24vY2FjZXJ0L3NjY2Ffc3NsX2Nh
MS5jcnQwcAYDVR0fBGkwZzBloGOgYYZfaHR0cDovL3RvcGNhLnNjY2EuY29tLmNu
L3B1YmxpYy9pdHJ1c2NybD9DQT0wRUE0RUUxRTU1MUVGOUUzNDJEQjkxMzY4NTk1
QUNGRkYyQzJGMjhBJmVuY29kZT1kZXIwHwYDVR0jBBgwFoAU+aX+qel2+nOLKxyp
VMb10zlqezkwHQYDVR0OBBYEFOdx8mFRwcBeFzsciBocTAtER5owMEoGA1UdIARD
MEEwNQYJKoEchvAGAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc2NjYS5j
b20uY24vY3BzMAgGBmeBDAECAjAOBgNVHQ8BAf8EBAMCBsAwDAYIKoEcz1UBg3UF
AANJADBGAiEAiBSyX2VtxU/pizvrxb+eHJJpMjsBm6gxdurTqxtOaboCIQDk6kuj
MEQ+11kcf9408Euhthxd+Ov0Ajq2rX5Wl9+0Ow==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDoDCCA0SgAwIBAgIUDqTuHlUe+eNC25E2hZWs//LC8oowDAYIKoEcz1UBg3UF
ADCBiDELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFjAUBgNVBAMMDVNDQ0EgUm9vdCBDQTEwHhcN
MjIwODI5MDg1NjI4WhcNNDIwODI0MDg1NjI4WjCBhzELMAkGA1UEBhMCQ04xQDA+
BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgTWFu
YWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNNMiBDZXJ0aWZpY2F0ZSBTeXN0ZW0x
FTATBgNVBAMMDFNDQ0EgU1NMIENBMTBZMBMGByqGSM49AgEGCCqBHM9VAYItA0IA
BFNTIvnr+0ik91e6RvQ4ZwPqxoWMAwf9qiZWuxOURaBudRbYbbWFPn/1MSqlaAQc
OC8Qb5++Ec9+CGtkuPAQj3mjggGHMIIBgzAfBgNVHSMEGDAWgBQjF/5MGKmyHjDu
BX6yKPnUYDzsrTAdBgNVHQ4EFgQU+aX+qel2+nOLKxypVMb10zlqezkwfQYIKwYB
BQUHAQEEcTBvMC4GCCsGAQUFBzABhiJodHRwOi8vdG9wY2Euc2NjYS5jb20uY246
ODE4NS9vY3NwMD0GCCsGAQUFBzAChjFodHRwOi8vdG9wY2Euc2NjYS5jb20uY24v
Y2FjZXJ0L3NjY2Ffcm9vdF9jYTEuY3J0MEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHA6
Ly90b3BjYS5zY2NhLmNvbS5jbi9jYXJsL3NjY2Ffcm9vdF9jYTEuY3JsMEAGA1Ud
IAQ5MDcwNQYJKoEchvAGAQECMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc2Nj
YS5jb20uY24vY3BzMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNV
HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAMBggqgRzPVQGDdQUAA0gAMEUC
IQDtoL36XmBwzuvL0QCcVLcpJGTxzrLMbPiWsESZqSbZDwIgNRP0dUdu6jYVzHgk
YKyaSuiPr/wpH9Kdusiqqge/Y8A=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICezCCAh+gAwIBAgIUTHK30RB8qDw+IdS0cNZ0M+O/UcQwDAYIKoEcz1UBg3UF
ADCBiDELMAkGA1UEBhMCQ04xQDA+BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0
aWZpY2F0ZSBBdXRob3JpdHkgTWFuYWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNN
MiBDZXJ0aWZpY2F0ZSBTeXN0ZW0xFjAUBgNVBAMMDVNDQ0EgUm9vdCBDQTEwHhcN
MjIwODI5MDcxMjAwWhcNNDcwODIwMDcxMjAwWjCBiDELMAkGA1UEBhMCQ04xQDA+
BgNVBAoMN1NpY2h1YW4gRGlnaXRhbCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgTWFu
YWdlbWVudCBDZW50ZXIxHzAdBgNVBAsMFlNNMiBDZXJ0aWZpY2F0ZSBTeXN0ZW0x
FjAUBgNVBAMMDVNDQ0EgUm9vdCBDQTEwWTATBgcqhkjOPQIBBggqgRzPVQGCLQNC
AASq4IiHhfqbZKjpJwghCJ0jifnGIYpPskjd1R3f2CzqGhFGIAhTTNsX/lSTH7oZ
AKWcr1SJXcu+pT9asQAo7GVNo2MwYTAdBgNVHQ4EFgQUIxf+TBipsh4w7gV+sij5
1GA87K0wHwYDVR0jBBgwFoAUIxf+TBipsh4w7gV+sij51GA87K0wDwYDVR0TAQH/
BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDAYIKoEcz1UBg3UFAANIADBFAiBEMYX3
OvlmmXlk/fv3eU+uirPlEHFoWRjttxn5DhdmSwIhAM1vJBDy6UWaiusyd7rUv1Ij
h3CRHFgTKC8pzWSrwF86
-----END CERTIFICATE-----

对比

连个方法获取的CA链有一点区别,方法一中获取到了4个证书,方法二只有3个。对比后发现方法一中多出的一个证书是用于Digital Signature的(左边)。

image-20250608231030820

参考资料

https://curl.gmssl.cn/

https://www.tongsuo.net/docs/features/curl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/root/curl/src/curl -v --tlcp --cacert sm2.ca.pem --sign-cert sm2.user1.sig.crt.pem --sign-key sm2.user1.sig.key.pem --enc-cert sm2.user1.enc.crt.pem --enc-key sm2.user1.enc.key.pem https://demo.gmssl.cn:2443/
* Host demo.gmssl.cn:2443 was resolved.
* IPv6: (none)
* IPv4: 47.93.114.141
* Trying 47.93.114.141:2443...
* ALPN: curl offers http/1.1
* (101) (OUT), , Unknown (1):
* CAfile: sm2.ca.pem
* CApath: /etc/ssl/certs
* (101) (IN), , Unknown (2):
* (101) (IN), , Unknown (11):
* (101) (IN), , Unknown (12):
* (101) (IN), , Unknown (13):
* (101) (IN), , Unknown (14):
* (101) (OUT), , Unknown (11):
* (101) (OUT), , Unknown (16):
* (101) (OUT), , Unknown (15):
* (101) (OUT), , Change cipher spec (1):
* (101) (OUT), , Unknown (20):
* (101) (IN), , Unknown (20):
* SSL connection using NTLSv1.1 / ECC-SM2-SM4-GCM-SM3 / UNDEF / SM2
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: C=CN; CN=demo.gmssl.cn
* start date: Dec 3 16:00:00 2023 GMT
* expire date: Dec 3 16:00:00 2028 GMT
* subjectAltName: host "demo.gmssl.cn" matched cert's "demo.gmssl.cn"
* issuer: C=CN; O=GMSSL; OU=PKI/SM2; CN=MiddleCA for Test
* SSL certificate verify ok.
* Certificate level 0: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Certificate level 1: Public key type SM2/SM2 (256/128 Bits/secBits), signed using SM2-with-SM3
* Connected to demo.gmssl.cn (47.93.114.141) port 2443
* using HTTP/1.x
> GET / HTTP/1.1
> Host: demo.gmssl.cn:2443
> User-Agent: curl/8.12.0-DEV
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Sun, 08 Jun 2025 14:08:49 GMT
< Content-Type: text/html
< Content-Length: 615
< Last-Modified: Wed, 07 Feb 2024 12:17:59 GMT
< Connection: keep-alive
< ETag: "65c374f7-267"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host demo.gmssl.cn left intact