sm-crypto-wasm 0.3.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -7
- package/dist/asn1/decode.d.ts +74 -0
- package/dist/asn1/decode.js +338 -0
- package/dist/asn1/encode.d.ts +68 -0
- package/dist/asn1/encode.js +275 -0
- package/dist/asn1/index.d.ts +11 -0
- package/dist/asn1/index.js +11 -0
- package/dist/asn1/oid.d.ts +22 -0
- package/dist/asn1/oid.js +107 -0
- package/dist/asn1/time.d.ts +39 -0
- package/dist/asn1/time.js +111 -0
- package/dist/asn1/types.d.ts +76 -0
- package/dist/asn1/types.js +71 -0
- package/dist/cms/errors.d.ts +13 -0
- package/dist/cms/errors.js +11 -0
- package/dist/cms/index.d.ts +13 -0
- package/dist/cms/index.js +10 -0
- package/dist/cms/oids.d.ts +17 -0
- package/dist/cms/oids.js +17 -0
- package/dist/cms/parse.d.ts +75 -0
- package/dist/cms/parse.js +326 -0
- package/dist/cms/sign.d.ts +43 -0
- package/dist/cms/sign.js +142 -0
- package/dist/cms/verify.d.ts +42 -0
- package/dist/cms/verify.js +173 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/pem/asn1.d.ts +10 -69
- package/dist/pem/asn1.js +10 -292
- package/dist/pem/pkcs8.js +9 -1
- package/dist/pem/sm2.d.ts +16 -0
- package/dist/pem/sm2.js +37 -5
- package/dist/runtime/base64.core.d.ts +22 -0
- package/dist/runtime/base64.core.js +68 -0
- package/dist/runtime/convert.d.ts +12 -3
- package/dist/runtime/convert.js +31 -12
- package/dist/sm2/index.d.ts +69 -0
- package/dist/sm2/index.js +188 -0
- package/dist/sm2/sm2.wasm +0 -0
- package/dist/webcrypto/algorithms.d.ts +42 -0
- package/dist/webcrypto/algorithms.js +120 -0
- package/dist/webcrypto/cryptoKey.d.ts +64 -0
- package/dist/webcrypto/cryptoKey.js +109 -0
- package/dist/webcrypto/errors.d.ts +19 -0
- package/dist/webcrypto/errors.js +45 -0
- package/dist/webcrypto/index.d.ts +14 -0
- package/dist/webcrypto/index.js +11 -0
- package/dist/webcrypto/subtle.d.ts +68 -0
- package/dist/webcrypto/subtle.js +358 -0
- package/dist/x509/encode.d.ts +59 -0
- package/dist/x509/encode.js +175 -0
- package/dist/x509/errors.d.ts +14 -0
- package/dist/x509/errors.js +14 -0
- package/dist/x509/extensions.d.ts +27 -0
- package/dist/x509/extensions.js +215 -0
- package/dist/x509/index.d.ts +14 -0
- package/dist/x509/index.js +11 -0
- package/dist/x509/ip.d.ts +12 -0
- package/dist/x509/ip.js +101 -0
- package/dist/x509/issue.d.ts +44 -0
- package/dist/x509/issue.js +414 -0
- package/dist/x509/oids.d.ts +40 -0
- package/dist/x509/oids.js +52 -0
- package/dist/x509/parse.d.ts +24 -0
- package/dist/x509/parse.js +436 -0
- package/dist/x509/sha1.d.ts +9 -0
- package/dist/x509/sha1.js +72 -0
- package/dist/x509/sha256.d.ts +9 -0
- package/dist/x509/sha256.js +84 -0
- package/dist/x509/sig.d.ts +20 -0
- package/dist/x509/sig.js +77 -0
- package/dist/x509/types.d.ts +235 -0
- package/dist/x509/types.js +7 -0
- package/dist/x509/verify.d.ts +44 -0
- package/dist/x509/verify.js +66 -0
- package/package.json +16 -3
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X.509 证书解析(只读)。
|
|
3
|
+
*
|
|
4
|
+
* 支持 DER / PEM / base64 输入;不做链校验、不拉 CRL/OCSP。
|
|
5
|
+
*
|
|
6
|
+
* 严格性(红线 #3):
|
|
7
|
+
* - 单个证书必须恰好是一个完整 DER 对象,尾部多余字节直接拒绝;
|
|
8
|
+
* - X.509 路径不使用 BER indefinite-length;
|
|
9
|
+
* - 未知扩展 / 未知算法 / 未知 Name 属性不抛错,保留原始值(红线 #9)。
|
|
10
|
+
*/
|
|
11
|
+
import { Asn1Class, expectSequence, expectUniversal, parseDer, parseDerAll, readBitString, readBoolean, readInteger, readOctetString, readOid, readString, readTime, UniversalTag, } from '../asn1/index.js';
|
|
12
|
+
import { decodeOid } from '../asn1/oid.js';
|
|
13
|
+
import { base64ToBytes, bytesToHex, hexToBytes } from '../runtime/convert.js';
|
|
14
|
+
import * as sm3 from '../sm3/index.js';
|
|
15
|
+
import { CertificateError } from './errors.js';
|
|
16
|
+
import { parseAuthorityKeyIdentifier, parseBasicConstraints, parseExtendedKeyUsage, parseKeyUsage, parseSubjectAltName, parseSubjectKeyIdentifier, } from './extensions.js';
|
|
17
|
+
import { NAME_OID_LABELS, OID_AUTHORITY_KEY_IDENTIFIER, OID_BASIC_CONSTRAINTS, OID_COMMON_NAME, OID_COUNTRY, OID_EMAIL_ADDRESS, OID_EXTENDED_KEY_USAGE, OID_ID_EC_PUBLIC_KEY, OID_KEY_USAGE, OID_LOCALITY, OID_ORGANIZATION, OID_ORGANIZATIONAL_UNIT, OID_RSA_ENCRYPTION, OID_RSA_SIGNATURE_PREFIX, OID_SM2_CURVE, OID_STATE, OID_SUBJECT_ALT_NAME, OID_SUBJECT_KEY_IDENTIFIER, } from './oids.js';
|
|
18
|
+
import { sha256Hex } from './sha256.js';
|
|
19
|
+
import { isUsableSm2PublicKey, isWithinValidity, verifyCertificateSignature, } from './verify.js';
|
|
20
|
+
// ---------- 公开入口 ----------
|
|
21
|
+
/**
|
|
22
|
+
* 解析单张证书。
|
|
23
|
+
*
|
|
24
|
+
* @param input PEM 字符串、base64 字符串、hex 字符串或 DER 字节。
|
|
25
|
+
* @throws CertificateError 结构非法、截断或尾部有多余字节。
|
|
26
|
+
*/
|
|
27
|
+
export function parseCertificate(input) {
|
|
28
|
+
const der = decodeCertificateInput(input);
|
|
29
|
+
try {
|
|
30
|
+
return certificateFromNode(parseDer(der));
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof CertificateError)
|
|
34
|
+
throw err;
|
|
35
|
+
throw new CertificateError(`parseCertificate: ${errorMessage(err)}`, { cause: err });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 解析一个或多个证书(PEM bundle / 连续 DER)。
|
|
40
|
+
*
|
|
41
|
+
* @throws CertificateError 任一证书结构非法。
|
|
42
|
+
*/
|
|
43
|
+
export function parseCertificates(input) {
|
|
44
|
+
if (typeof input === 'string') {
|
|
45
|
+
const text = input.trim();
|
|
46
|
+
const blocks = extractPemBlocks(text);
|
|
47
|
+
if (blocks.length > 0) {
|
|
48
|
+
return blocks.map((block) => parseCertificate(block));
|
|
49
|
+
}
|
|
50
|
+
return [parseCertificate(text)];
|
|
51
|
+
}
|
|
52
|
+
if (isPemText(input)) {
|
|
53
|
+
return parseCertificates(new TextDecoder().decode(input));
|
|
54
|
+
}
|
|
55
|
+
let nodes;
|
|
56
|
+
try {
|
|
57
|
+
nodes = parseDerAll(input);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new CertificateError(`parseCertificates: ${errorMessage(err)}`, { cause: err });
|
|
61
|
+
}
|
|
62
|
+
if (nodes.length === 0)
|
|
63
|
+
throw new CertificateError('parseCertificates: empty input');
|
|
64
|
+
return nodes.map((node) => {
|
|
65
|
+
try {
|
|
66
|
+
return certificateFromNode(node);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (err instanceof CertificateError)
|
|
70
|
+
throw err;
|
|
71
|
+
throw new CertificateError(`parseCertificates: ${errorMessage(err)}`, { cause: err });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// ---------- 输入解码 ----------
|
|
76
|
+
function decodeCertificateInput(input) {
|
|
77
|
+
if (input instanceof Uint8Array) {
|
|
78
|
+
if (input.length === 0)
|
|
79
|
+
throw new CertificateError('parseCertificate: empty input');
|
|
80
|
+
if (isPemText(input))
|
|
81
|
+
return decodeCertificateInput(new TextDecoder().decode(input));
|
|
82
|
+
return input;
|
|
83
|
+
}
|
|
84
|
+
if (typeof input !== 'string') {
|
|
85
|
+
throw new TypeError('parseCertificate: expected string or Uint8Array');
|
|
86
|
+
}
|
|
87
|
+
const text = input.trim();
|
|
88
|
+
if (text.length === 0)
|
|
89
|
+
throw new CertificateError('parseCertificate: empty input');
|
|
90
|
+
const block = extractPemBlocks(text)[0];
|
|
91
|
+
if (block !== undefined)
|
|
92
|
+
return pemBodyToDer(block);
|
|
93
|
+
// 非 PEM:依次尝试 hex 与 base64,以“能否解析出完整 DER 对象”为准。
|
|
94
|
+
const candidates = [];
|
|
95
|
+
const compact = text.replace(/\s+/g, '');
|
|
96
|
+
if (/^[0-9a-fA-F]+$/.test(compact) && compact.length % 2 === 0) {
|
|
97
|
+
candidates.push(hexToBytes(compact, 'certificate'));
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
candidates.push(base64ToBytes(compact, 'certificate'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// 不是 base64;保留 hex 候选(若也没有,下面统一报错)。
|
|
104
|
+
}
|
|
105
|
+
for (const candidate of candidates) {
|
|
106
|
+
try {
|
|
107
|
+
parseDer(candidate);
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// 换下一个候选。
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new CertificateError('parseCertificate: input is neither PEM, hex nor base64 DER');
|
|
115
|
+
}
|
|
116
|
+
function extractPemBlocks(text) {
|
|
117
|
+
const blocks = [];
|
|
118
|
+
const re = /-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/g;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const match = re.exec(text);
|
|
121
|
+
if (match === null)
|
|
122
|
+
break;
|
|
123
|
+
blocks.push(match[0]);
|
|
124
|
+
}
|
|
125
|
+
return blocks;
|
|
126
|
+
}
|
|
127
|
+
function pemBodyToDer(pem) {
|
|
128
|
+
const match = /-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/.exec(pem);
|
|
129
|
+
if (match === null)
|
|
130
|
+
throw new CertificateError('parseCertificate: missing CERTIFICATE PEM block');
|
|
131
|
+
const body = match[1].replace(/\s+/g, '');
|
|
132
|
+
try {
|
|
133
|
+
return base64ToBytes(body, 'certificate');
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
throw new CertificateError('parseCertificate: invalid base64 in PEM block', { cause: err });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function isPemText(bytes) {
|
|
140
|
+
const prefix = '-----BEGIN ';
|
|
141
|
+
if (bytes.length < prefix.length)
|
|
142
|
+
return false;
|
|
143
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
144
|
+
if (bytes[i] !== prefix.charCodeAt(i))
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
// ---------- Certificate ----------
|
|
150
|
+
function certificateFromNode(top) {
|
|
151
|
+
const kids = expectSequence(top, 'Certificate');
|
|
152
|
+
if (kids.length !== 3) {
|
|
153
|
+
throw new CertificateError(`Certificate: expected 3 elements, got ${kids.length}`);
|
|
154
|
+
}
|
|
155
|
+
const tbsNode = expectUniversal(kids[0], UniversalTag.Sequence, 'tbsCertificate');
|
|
156
|
+
const signatureAlgorithm = parseAlgorithmIdentifier(kids[1], 'signatureAlgorithm');
|
|
157
|
+
const signatureValue = new Uint8Array(readBitString(expectUniversal(kids[2], UniversalTag.BitString, 'signatureValue'), 'signatureValue').bytes);
|
|
158
|
+
const tbs = parseTbsCertificate(tbsNode);
|
|
159
|
+
const raw = copy(top.raw);
|
|
160
|
+
const publicKeyAlgorithm = classifyPublicKey(tbs.subjectPublicKeyInfo.algorithm);
|
|
161
|
+
const subjectPublicKey = tbs.subjectPublicKeyInfo.publicKey;
|
|
162
|
+
const supported = publicKeyAlgorithm === 'SM2' &&
|
|
163
|
+
(subjectPublicKey.length === 64 ||
|
|
164
|
+
(subjectPublicKey.length === 65 && subjectPublicKey[0] === 0x04));
|
|
165
|
+
const extensions = tbs.extensions;
|
|
166
|
+
const cert = {
|
|
167
|
+
version: tbs.version,
|
|
168
|
+
serialNumber: tbs.serialNumber,
|
|
169
|
+
serialNumberHex: tbs.serialNumber.toString(16),
|
|
170
|
+
signatureAlgorithm,
|
|
171
|
+
issuer: tbs.issuer,
|
|
172
|
+
validity: tbs.validity,
|
|
173
|
+
subject: tbs.subject,
|
|
174
|
+
subjectPublicKeyInfo: tbs.subjectPublicKeyInfo,
|
|
175
|
+
extensions,
|
|
176
|
+
signatureValue,
|
|
177
|
+
tbsCertificate: copy(tbsNode.raw),
|
|
178
|
+
raw,
|
|
179
|
+
subjectPublicKey,
|
|
180
|
+
publicKeyAlgorithm,
|
|
181
|
+
supported,
|
|
182
|
+
keyUsage: extensionValue(extensions, OID_KEY_USAGE, parseKeyUsage),
|
|
183
|
+
basicConstraints: extensionValue(extensions, OID_BASIC_CONSTRAINTS, parseBasicConstraints),
|
|
184
|
+
subjectAltNames: extensionValue(extensions, OID_SUBJECT_ALT_NAME, parseSubjectAltName) ?? [],
|
|
185
|
+
extendedKeyUsage: extensionValue(extensions, OID_EXTENDED_KEY_USAGE, parseExtendedKeyUsage) ?? [],
|
|
186
|
+
subjectKeyIdentifier: extensionValue(extensions, OID_SUBJECT_KEY_IDENTIFIER, parseSubjectKeyIdentifier),
|
|
187
|
+
authorityKeyIdentifier: extensionValue(extensions, OID_AUTHORITY_KEY_IDENTIFIER, parseAuthorityKeyIdentifier),
|
|
188
|
+
getExtension(oid) {
|
|
189
|
+
return extensions.find((ext) => ext.oid === oid);
|
|
190
|
+
},
|
|
191
|
+
checkValidity(at = new Date()) {
|
|
192
|
+
return isWithinValidity(cert.validity, at);
|
|
193
|
+
},
|
|
194
|
+
verifySignature(issuer, opts = {}) {
|
|
195
|
+
if (!cert.supported)
|
|
196
|
+
return false;
|
|
197
|
+
const publicKey = resolveIssuerPublicKey(issuer);
|
|
198
|
+
if (publicKey === null)
|
|
199
|
+
return false;
|
|
200
|
+
return verifyCertificateSignature(cert, publicKey, opts);
|
|
201
|
+
},
|
|
202
|
+
fingerprint(hash = 'sm3') {
|
|
203
|
+
if (hash === 'sm3')
|
|
204
|
+
return sm3.hash(raw);
|
|
205
|
+
if (hash === 'sha256')
|
|
206
|
+
return sha256Hex(raw);
|
|
207
|
+
throw new TypeError(`fingerprint: unsupported hash "${String(hash)}"`);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
return cert;
|
|
211
|
+
}
|
|
212
|
+
function parseTbsCertificate(tbsNode) {
|
|
213
|
+
const kids = expectSequence(tbsNode, 'TBSCertificate');
|
|
214
|
+
let index = 0;
|
|
215
|
+
let version = 1;
|
|
216
|
+
const first = kids[0];
|
|
217
|
+
if (first !== undefined && first.cls === Asn1Class.ContextSpecific && first.tag === 0) {
|
|
218
|
+
const inner = first.children?.[0];
|
|
219
|
+
const rawVersion = Number(readInteger(expectUniversal(inner, UniversalTag.Integer, 'version')));
|
|
220
|
+
if (rawVersion < 0 || rawVersion > 2) {
|
|
221
|
+
throw new CertificateError(`TBSCertificate: unsupported version ${rawVersion}`);
|
|
222
|
+
}
|
|
223
|
+
version = (rawVersion + 1);
|
|
224
|
+
index = 1;
|
|
225
|
+
}
|
|
226
|
+
const serialNode = kids[index++];
|
|
227
|
+
const serialNumber = serialNode
|
|
228
|
+
? readInteger(serialNode, 'serialNumber')
|
|
229
|
+
: (() => {
|
|
230
|
+
throw new CertificateError('TBSCertificate: missing serialNumber');
|
|
231
|
+
})();
|
|
232
|
+
// TBS 内签名算法(与外层应一致;这里只解析,不做强一致断言)。
|
|
233
|
+
parseAlgorithmIdentifier(kids[index++], 'signature');
|
|
234
|
+
const issuer = parseName(kids[index++], 'issuer');
|
|
235
|
+
const validity = parseValidity(kids[index++]);
|
|
236
|
+
const subject = parseName(kids[index++], 'subject');
|
|
237
|
+
const spkiNode = expectUniversal(kids[index++], UniversalTag.Sequence, 'subjectPublicKeyInfo');
|
|
238
|
+
const subjectPublicKeyInfo = parseSpki(spkiNode);
|
|
239
|
+
let extensions = [];
|
|
240
|
+
for (; index < kids.length; index++) {
|
|
241
|
+
const node = kids[index];
|
|
242
|
+
if (node === undefined)
|
|
243
|
+
continue;
|
|
244
|
+
if (node.cls === Asn1Class.ContextSpecific && node.tag === 3) {
|
|
245
|
+
extensions = parseExtensions(node);
|
|
246
|
+
}
|
|
247
|
+
// [1] issuerUniqueID / [2] subjectUniqueID 与未知尾部字段:忽略。
|
|
248
|
+
}
|
|
249
|
+
return { version, serialNumber, issuer, validity, subject, subjectPublicKeyInfo, extensions };
|
|
250
|
+
}
|
|
251
|
+
function parseValidity(node) {
|
|
252
|
+
const kids = expectSequence(node, 'validity');
|
|
253
|
+
if (kids.length < 2)
|
|
254
|
+
throw new CertificateError('validity: expected notBefore and notAfter');
|
|
255
|
+
return {
|
|
256
|
+
notBefore: readTime(kids[0], 'notBefore'),
|
|
257
|
+
notAfter: readTime(kids[1], 'notAfter'),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function parseSpki(node) {
|
|
261
|
+
const kids = expectSequence(node, 'subjectPublicKeyInfo');
|
|
262
|
+
if (kids.length < 2)
|
|
263
|
+
throw new CertificateError('subjectPublicKeyInfo: missing public key');
|
|
264
|
+
const algorithm = parseAlgorithmIdentifier(kids[0], 'subjectPublicKeyInfo.algorithm');
|
|
265
|
+
const bits = readBitString(kids[1], 'subjectPublicKey');
|
|
266
|
+
return {
|
|
267
|
+
algorithm,
|
|
268
|
+
publicKey: new Uint8Array(bits.bytes),
|
|
269
|
+
raw: copy(node.raw),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function parseAlgorithmIdentifier(node, what) {
|
|
273
|
+
const kids = expectSequence(node, what);
|
|
274
|
+
const first = kids[0];
|
|
275
|
+
if (first === undefined)
|
|
276
|
+
throw new CertificateError(`${what}: missing algorithm OID`);
|
|
277
|
+
const oid = readOid(first, what);
|
|
278
|
+
const parameterNode = kids[1];
|
|
279
|
+
if (parameterNode === undefined)
|
|
280
|
+
return { oid };
|
|
281
|
+
return {
|
|
282
|
+
oid,
|
|
283
|
+
parameters: new Uint8Array(parameterNode.value),
|
|
284
|
+
parametersRaw: copy(parameterNode.raw),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// ---------- Name ----------
|
|
288
|
+
function parseName(node, what) {
|
|
289
|
+
const rdns = expectSequence(node, what);
|
|
290
|
+
const attributes = [];
|
|
291
|
+
for (const rdn of rdns) {
|
|
292
|
+
const set = expectUniversal(rdn, UniversalTag.Set, what);
|
|
293
|
+
const atvs = set.children;
|
|
294
|
+
if (!atvs)
|
|
295
|
+
continue;
|
|
296
|
+
for (const atv of atvs) {
|
|
297
|
+
const parts = expectSequence(atv, what);
|
|
298
|
+
const typeNode = parts[0];
|
|
299
|
+
const valueNode = parts[1];
|
|
300
|
+
if (typeNode === undefined || valueNode === undefined) {
|
|
301
|
+
throw new CertificateError(`${what}: malformed AttributeTypeAndValue`);
|
|
302
|
+
}
|
|
303
|
+
attributes.push({
|
|
304
|
+
oid: readOid(typeNode, what),
|
|
305
|
+
value: decodeAttributeValue(valueNode),
|
|
306
|
+
raw: copy(atv.raw),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const find = (oid) => attributes.find((attribute) => attribute.oid === oid)?.value;
|
|
311
|
+
return {
|
|
312
|
+
commonName: find(OID_COMMON_NAME),
|
|
313
|
+
organization: find(OID_ORGANIZATION),
|
|
314
|
+
organizationalUnit: find(OID_ORGANIZATIONAL_UNIT),
|
|
315
|
+
country: find(OID_COUNTRY),
|
|
316
|
+
state: find(OID_STATE),
|
|
317
|
+
locality: find(OID_LOCALITY),
|
|
318
|
+
emailAddress: find(OID_EMAIL_ADDRESS),
|
|
319
|
+
attributes,
|
|
320
|
+
raw: copy(node.raw),
|
|
321
|
+
toString() {
|
|
322
|
+
return attributes.map((a) => formatNameAttribute(a.oid, a.value)).join(',');
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function decodeAttributeValue(node) {
|
|
327
|
+
try {
|
|
328
|
+
return readString(node, 'Name attribute');
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// 未知字符串类型(如私有编码):保留原始内容,绝不抛错。
|
|
332
|
+
return `#${bytesToHex(node.value)}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function formatNameAttribute(oid, value) {
|
|
336
|
+
const label = NAME_OID_LABELS[oid] ?? oid;
|
|
337
|
+
return `${label}=${escapeRdnValue(value)}`;
|
|
338
|
+
}
|
|
339
|
+
/** RFC 4514 的最小转义集。 */
|
|
340
|
+
function escapeRdnValue(value) {
|
|
341
|
+
let out = value.replace(/([,+"\\<>;])/g, '\\$1');
|
|
342
|
+
if (out.startsWith(' '))
|
|
343
|
+
out = `\\${out}`;
|
|
344
|
+
if (out.endsWith(' '))
|
|
345
|
+
out = `${out.slice(0, -1)}\\ `;
|
|
346
|
+
if (out.startsWith('#'))
|
|
347
|
+
out = `\\${out}`;
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
// ---------- Extensions ----------
|
|
351
|
+
function parseExtensions(node) {
|
|
352
|
+
const inner = node.children?.[0];
|
|
353
|
+
const listNode = expectUniversal(inner, UniversalTag.Sequence, 'extensions');
|
|
354
|
+
const entries = listNode.children ?? [];
|
|
355
|
+
return entries.map((entry) => {
|
|
356
|
+
const kids = expectSequence(entry, 'Extension');
|
|
357
|
+
const oidNode = kids[0];
|
|
358
|
+
if (oidNode === undefined)
|
|
359
|
+
throw new CertificateError('Extension: missing OID');
|
|
360
|
+
const oid = readOid(oidNode, 'Extension');
|
|
361
|
+
let index = 1;
|
|
362
|
+
let critical = false;
|
|
363
|
+
const criticalNode = kids[index];
|
|
364
|
+
if (criticalNode !== undefined &&
|
|
365
|
+
criticalNode.cls === Asn1Class.Universal &&
|
|
366
|
+
criticalNode.tag === UniversalTag.Boolean) {
|
|
367
|
+
critical = readBoolean(criticalNode, 'Extension.critical');
|
|
368
|
+
index++;
|
|
369
|
+
}
|
|
370
|
+
const valueNode = kids[index];
|
|
371
|
+
if (valueNode === undefined)
|
|
372
|
+
throw new CertificateError(`Extension ${oid}: missing value`);
|
|
373
|
+
return {
|
|
374
|
+
oid,
|
|
375
|
+
critical,
|
|
376
|
+
value: new Uint8Array(readOctetString(valueNode, `Extension ${oid}`)),
|
|
377
|
+
raw: copy(entry.raw),
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function extensionValue(extensions, oid, parse) {
|
|
382
|
+
const extension = extensions.find((item) => item.oid === oid);
|
|
383
|
+
if (extension === undefined)
|
|
384
|
+
return undefined;
|
|
385
|
+
try {
|
|
386
|
+
return parse(extension.value);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// 已知扩展损坏 → 视为缺失(红线 #9:不让真实证书解析失败)。
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ---------- 辅助 ----------
|
|
394
|
+
function classifyPublicKey(algorithm) {
|
|
395
|
+
if (algorithm.oid === OID_SM2_CURVE)
|
|
396
|
+
return 'SM2';
|
|
397
|
+
if (algorithm.oid === OID_ID_EC_PUBLIC_KEY) {
|
|
398
|
+
if (algorithm.parameters === undefined)
|
|
399
|
+
return 'UNKNOWN';
|
|
400
|
+
try {
|
|
401
|
+
return decodeOid(algorithm.parameters) === OID_SM2_CURVE ? 'SM2' : 'UNKNOWN';
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return 'UNKNOWN';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (algorithm.oid === OID_RSA_ENCRYPTION || algorithm.oid.startsWith(OID_RSA_SIGNATURE_PREFIX)) {
|
|
408
|
+
return 'RSA';
|
|
409
|
+
}
|
|
410
|
+
return 'UNKNOWN';
|
|
411
|
+
}
|
|
412
|
+
function resolveIssuerPublicKey(issuer) {
|
|
413
|
+
if (isCertificateLike(issuer))
|
|
414
|
+
return issuer.subjectPublicKey;
|
|
415
|
+
if (issuer.length === 64 || issuer.length === 65) {
|
|
416
|
+
return isUsableSm2PublicKey(issuer) ? issuer : null;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
return parseCertificate(issuer).subjectPublicKey;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function isCertificateLike(value) {
|
|
426
|
+
return (typeof value === 'object' &&
|
|
427
|
+
value !== null &&
|
|
428
|
+
'tbsCertificate' in value &&
|
|
429
|
+
'subjectPublicKey' in value);
|
|
430
|
+
}
|
|
431
|
+
function copy(bytes) {
|
|
432
|
+
return new Uint8Array(bytes);
|
|
433
|
+
}
|
|
434
|
+
function errorMessage(err) {
|
|
435
|
+
return err instanceof Error ? err.message : String(err);
|
|
436
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 JS SHA-1(仅用于 SubjectKeyIdentifier 的 OpenSSL 兼容选项)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `sha256.ts` 同样的理由:X.509 API 是同步的,不能用 `crypto.subtle`;
|
|
5
|
+
* `node:crypto` 会破坏浏览器构建。SHA-1 在这里只作为 SKI 的哈希选项
|
|
6
|
+
* (`subjectKeyIdentifier: { hash: 'sha1' }`),不用于任何安全判断。
|
|
7
|
+
*/
|
|
8
|
+
/** SHA-1 摘要(小写 hex)。 */
|
|
9
|
+
export declare function sha1Hex(input: Uint8Array): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 JS SHA-1(仅用于 SubjectKeyIdentifier 的 OpenSSL 兼容选项)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `sha256.ts` 同样的理由:X.509 API 是同步的,不能用 `crypto.subtle`;
|
|
5
|
+
* `node:crypto` 会破坏浏览器构建。SHA-1 在这里只作为 SKI 的哈希选项
|
|
6
|
+
* (`subjectKeyIdentifier: { hash: 'sha1' }`),不用于任何安全判断。
|
|
7
|
+
*/
|
|
8
|
+
/** SHA-1 摘要(小写 hex)。 */
|
|
9
|
+
export function sha1Hex(input) {
|
|
10
|
+
const length = input.length;
|
|
11
|
+
const paddedLength = Math.ceil((length + 9) / 64) * 64;
|
|
12
|
+
const buffer = new Uint8Array(paddedLength);
|
|
13
|
+
buffer.set(input);
|
|
14
|
+
buffer[length] = 0x80;
|
|
15
|
+
const view = new DataView(buffer.buffer);
|
|
16
|
+
view.setUint32(paddedLength - 8, Math.floor(length / 0x20000000));
|
|
17
|
+
view.setUint32(paddedLength - 4, (length * 8) >>> 0);
|
|
18
|
+
let h0 = 0x67452301;
|
|
19
|
+
let h1 = 0xefcdab89;
|
|
20
|
+
let h2 = 0x98badcfe;
|
|
21
|
+
let h3 = 0x10325476;
|
|
22
|
+
let h4 = 0xc3d2e1f0;
|
|
23
|
+
const w = new Uint32Array(80);
|
|
24
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
25
|
+
for (let i = 0; i < 16; i++)
|
|
26
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
27
|
+
for (let i = 16; i < 80; i++) {
|
|
28
|
+
const x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
|
|
29
|
+
w[i] = rotl(x, 1);
|
|
30
|
+
}
|
|
31
|
+
let a = h0;
|
|
32
|
+
let b = h1;
|
|
33
|
+
let c = h2;
|
|
34
|
+
let d = h3;
|
|
35
|
+
let e = h4;
|
|
36
|
+
for (let i = 0; i < 80; i++) {
|
|
37
|
+
let f;
|
|
38
|
+
let k;
|
|
39
|
+
if (i < 20) {
|
|
40
|
+
f = (b & c) | (~b & d);
|
|
41
|
+
k = 0x5a827999;
|
|
42
|
+
}
|
|
43
|
+
else if (i < 40) {
|
|
44
|
+
f = b ^ c ^ d;
|
|
45
|
+
k = 0x6ed9eba1;
|
|
46
|
+
}
|
|
47
|
+
else if (i < 60) {
|
|
48
|
+
f = (b & c) | (b & d) | (c & d);
|
|
49
|
+
k = 0x8f1bbcdc;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
f = b ^ c ^ d;
|
|
53
|
+
k = 0xca62c1d6;
|
|
54
|
+
}
|
|
55
|
+
const temp = (rotl(a, 5) + f + e + k + w[i]) >>> 0;
|
|
56
|
+
e = d;
|
|
57
|
+
d = c;
|
|
58
|
+
c = rotl(b, 30);
|
|
59
|
+
b = a;
|
|
60
|
+
a = temp;
|
|
61
|
+
}
|
|
62
|
+
h0 = (h0 + a) >>> 0;
|
|
63
|
+
h1 = (h1 + b) >>> 0;
|
|
64
|
+
h2 = (h2 + c) >>> 0;
|
|
65
|
+
h3 = (h3 + d) >>> 0;
|
|
66
|
+
h4 = (h4 + e) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
return [h0, h1, h2, h3, h4].map((v) => v.toString(16).padStart(8, '0')).join('');
|
|
69
|
+
}
|
|
70
|
+
function rotl(value, shift) {
|
|
71
|
+
return ((value << shift) | (value >>> (32 - shift))) >>> 0;
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 JS SHA-256(仅用于证书指纹)。
|
|
3
|
+
*
|
|
4
|
+
* 不引入新依赖、不新增 wasm:X.509 API 是同步的,无法调用宿主的
|
|
5
|
+
* `crypto.subtle.digest`;Node 的 `node:crypto` 又会让浏览器构建失败。
|
|
6
|
+
* 这里实现 FIPS 180-4 的标准算法,输出小写 hex。
|
|
7
|
+
*/
|
|
8
|
+
/** SHA-256 摘要(小写 hex)。 */
|
|
9
|
+
export declare function sha256Hex(input: Uint8Array): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 JS SHA-256(仅用于证书指纹)。
|
|
3
|
+
*
|
|
4
|
+
* 不引入新依赖、不新增 wasm:X.509 API 是同步的,无法调用宿主的
|
|
5
|
+
* `crypto.subtle.digest`;Node 的 `node:crypto` 又会让浏览器构建失败。
|
|
6
|
+
* 这里实现 FIPS 180-4 的标准算法,输出小写 hex。
|
|
7
|
+
*/
|
|
8
|
+
const K = new Uint32Array([
|
|
9
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
10
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
11
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
12
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
13
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
14
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
15
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
16
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
17
|
+
]);
|
|
18
|
+
/** SHA-256 摘要(小写 hex)。 */
|
|
19
|
+
export function sha256Hex(input) {
|
|
20
|
+
const length = input.length;
|
|
21
|
+
// padding:0x80 + 若干 0 + 8 字节长度,总长为 64 的倍数。
|
|
22
|
+
const paddedLength = Math.ceil((length + 9) / 64) * 64;
|
|
23
|
+
const buffer = new Uint8Array(paddedLength);
|
|
24
|
+
buffer.set(input);
|
|
25
|
+
buffer[length] = 0x80;
|
|
26
|
+
const view = new DataView(buffer.buffer);
|
|
27
|
+
// 长度是 64-bit big-endian(输入实际不会超过 2^32 位,高 32 位取 0)。
|
|
28
|
+
view.setUint32(paddedLength - 8, Math.floor(length / 0x20000000));
|
|
29
|
+
view.setUint32(paddedLength - 4, (length * 8) >>> 0);
|
|
30
|
+
const h = new Uint32Array([
|
|
31
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
32
|
+
]);
|
|
33
|
+
const w = new Uint32Array(64);
|
|
34
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
35
|
+
for (let i = 0; i < 16; i++)
|
|
36
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
37
|
+
for (let i = 16; i < 64; i++) {
|
|
38
|
+
const w15 = w[i - 15];
|
|
39
|
+
const w2 = w[i - 2];
|
|
40
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
41
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
42
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
43
|
+
}
|
|
44
|
+
let a = h[0];
|
|
45
|
+
let b = h[1];
|
|
46
|
+
let c = h[2];
|
|
47
|
+
let d = h[3];
|
|
48
|
+
let e = h[4];
|
|
49
|
+
let f = h[5];
|
|
50
|
+
let g = h[6];
|
|
51
|
+
let hh = h[7];
|
|
52
|
+
for (let i = 0; i < 64; i++) {
|
|
53
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
54
|
+
const ch = (e & f) ^ (~e & g);
|
|
55
|
+
const temp1 = (hh + s1 + ch + K[i] + w[i]) >>> 0;
|
|
56
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
57
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
58
|
+
const temp2 = (s0 + maj) >>> 0;
|
|
59
|
+
hh = g;
|
|
60
|
+
g = f;
|
|
61
|
+
f = e;
|
|
62
|
+
e = (d + temp1) >>> 0;
|
|
63
|
+
d = c;
|
|
64
|
+
c = b;
|
|
65
|
+
b = a;
|
|
66
|
+
a = (temp1 + temp2) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
h[0] = (h[0] + a) >>> 0;
|
|
69
|
+
h[1] = (h[1] + b) >>> 0;
|
|
70
|
+
h[2] = (h[2] + c) >>> 0;
|
|
71
|
+
h[3] = (h[3] + d) >>> 0;
|
|
72
|
+
h[4] = (h[4] + e) >>> 0;
|
|
73
|
+
h[5] = (h[5] + f) >>> 0;
|
|
74
|
+
h[6] = (h[6] + g) >>> 0;
|
|
75
|
+
h[7] = (h[7] + hh) >>> 0;
|
|
76
|
+
}
|
|
77
|
+
let out = '';
|
|
78
|
+
for (let i = 0; i < 8; i++)
|
|
79
|
+
out += h[i].toString(16).padStart(8, '0');
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function rotr(value, shift) {
|
|
83
|
+
return ((value >>> shift) | (value << (32 - shift))) >>> 0;
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM2 签名值的两种编码转换:
|
|
3
|
+
* - 裸格式:64 字节 `r ‖ s`(`sm2.sign` 的返回);
|
|
4
|
+
* - DER 格式:`SEQUENCE { INTEGER r, INTEGER s }`(X.509 / CMS 要求)。
|
|
5
|
+
*
|
|
6
|
+
* 红线 #2:签发给外部系统的证书/CMS 必须是 DER 格式;直接塞裸 `r‖s`
|
|
7
|
+
* 会产出非标准数据。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* DER `SEQUENCE { r, s }` → 64 字节 `r ‖ s`。
|
|
11
|
+
*
|
|
12
|
+
* @throws CertificateError 结构非法、r/s 为负或超过 32 字节。
|
|
13
|
+
*/
|
|
14
|
+
export declare function derSignatureToRaw(der: Uint8Array, what?: string): Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* 64 字节 `r ‖ s` → DER `SEQUENCE { r, s }`。
|
|
17
|
+
*
|
|
18
|
+
* @throws CertificateError 长度不是 64 字节。
|
|
19
|
+
*/
|
|
20
|
+
export declare function rawSignatureToDer(raw: Uint8Array, what?: string): Uint8Array;
|