sm-crypto-wasm 0.3.1 → 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 +70 -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/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 +15 -3
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X.509 证书签发(自签 + 用 CA 证书签发终端证书)。
|
|
3
|
+
*
|
|
4
|
+
* 安全边界(红线 #4 / #5 / #6):
|
|
5
|
+
* - 证书版本固定 v3;
|
|
6
|
+
* - `basicConstraints.ca=false` 的证书不得用于签发子证书,且该证书不能带
|
|
7
|
+
* `keyUsage: keyCertSign`;
|
|
8
|
+
* - 用作签发者的证书必须是 CA(`basicConstraints.ca === true`),若带 KeyUsage
|
|
9
|
+
* 必须包含 `keyCertSign`;
|
|
10
|
+
* - CA 证书的 KeyUsage(若提供)必须包含 `keyCertSign`;
|
|
11
|
+
* - 序列号为正整数;默认生成 16 字节随机数并把最高位清零;
|
|
12
|
+
* - SAN 的 dNSName / rfc822Name / URI 必须为 ASCII,iPAddress 必须是合法 4/16 字节。
|
|
13
|
+
*
|
|
14
|
+
* 本模块**不生成密钥、不管理私钥**:私钥由调用方提供,签名后不驻留。
|
|
15
|
+
*/
|
|
16
|
+
import { parseSm2PublicKeyPem } from '../pem/sm2.js';
|
|
17
|
+
import { bytesToPem, hexToBytes, toBytes } from '../runtime/convert.js';
|
|
18
|
+
import { randomBytes } from '../runtime/random.js';
|
|
19
|
+
import * as sm2 from '../sm2/index.js';
|
|
20
|
+
import * as sm3 from '../sm3/index.js';
|
|
21
|
+
import { encodeAuthorityKeyIdentifier, encodeBasicConstraints, encodeCertificate, encodeExtendedKeyUsage, encodeKeyUsage, encodeSubjectAltName, encodeSubjectKeyIdentifier, encodeTbsCertificate, normalizePublicKey, } from './encode.js';
|
|
22
|
+
import { CertificateError } from './errors.js';
|
|
23
|
+
import { OID_AUTHORITY_KEY_IDENTIFIER, OID_BASIC_CONSTRAINTS, OID_EXTENDED_KEY_USAGE, OID_KEY_USAGE, OID_SUBJECT_ALT_NAME, OID_SUBJECT_KEY_IDENTIFIER, } from './oids.js';
|
|
24
|
+
import { parseCertificate } from './parse.js';
|
|
25
|
+
import { sha1Hex } from './sha1.js';
|
|
26
|
+
import { rawSignatureToDer } from './sig.js';
|
|
27
|
+
/**
|
|
28
|
+
* 签发一张 SM2 证书。
|
|
29
|
+
*
|
|
30
|
+
* @throws CertificateError 输入不满足证书约束。
|
|
31
|
+
* @throws RangeError 序列号/有效期/路径长度越界。
|
|
32
|
+
*/
|
|
33
|
+
export function issueCertificate(opts) {
|
|
34
|
+
validateSubject(opts.subject, 'subject');
|
|
35
|
+
const issuer = opts.issuer ??
|
|
36
|
+
(opts.signer.certificate ? nameToInput(opts.signer.certificate.subject) : opts.subject);
|
|
37
|
+
validateSubject(issuer, 'issuer');
|
|
38
|
+
validateSigner(opts.signer);
|
|
39
|
+
validateBasicConstraints(opts);
|
|
40
|
+
const serialNumber = opts.serialNumber ?? randomSerialNumber();
|
|
41
|
+
validateSerialNumber(serialNumber);
|
|
42
|
+
const notBefore = opts.notBefore ?? new Date(Date.now() - 5 * 60_000);
|
|
43
|
+
const validity = { notBefore, notAfter: opts.notAfter };
|
|
44
|
+
validateValidity(validity);
|
|
45
|
+
const publicKey = resolveSubjectPublicKey(opts);
|
|
46
|
+
const extensions = buildExtensions(opts, publicKey, issuer === opts.subject && !opts.signer.certificate);
|
|
47
|
+
const tbs = encodeTbsCertificate({
|
|
48
|
+
version: 3,
|
|
49
|
+
serialNumber,
|
|
50
|
+
issuer,
|
|
51
|
+
validity,
|
|
52
|
+
subject: opts.subject,
|
|
53
|
+
publicKey,
|
|
54
|
+
extensions: extensions.list,
|
|
55
|
+
});
|
|
56
|
+
// 默认空标识:与 OpenSSL 的 X.509 路径一致(`openssl verify` 可直接验证)。
|
|
57
|
+
// 需要 GM/T 默认标识时由调用方显式传 `DEFAULT_SM2_USER_ID`。
|
|
58
|
+
const signatureHex = sm2.signWithID(opts.signer.privateKey, tbs.raw, opts.userID ?? new Uint8Array(0));
|
|
59
|
+
const signatureDer = rawSignatureToDer(hexToBytes(signatureHex, 'signature'));
|
|
60
|
+
const der = encodeCertificate(tbs, signatureDer);
|
|
61
|
+
return {
|
|
62
|
+
pem: bytesToPem(der, 'CERTIFICATE'),
|
|
63
|
+
der,
|
|
64
|
+
certificate: parseCertificate(der),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 生成自签证书(issuer = subject)。
|
|
69
|
+
*
|
|
70
|
+
* @throws CertificateError / RangeError 同 `issueCertificate`。
|
|
71
|
+
*/
|
|
72
|
+
export function createSelfSignedCertificate(opts) {
|
|
73
|
+
const { privateKey, ...rest } = opts;
|
|
74
|
+
return issueCertificate({ ...rest, signer: { privateKey } });
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 解析 RFC 4514 子集形式的 DN 字符串,如 `"C=CN,O=Example,CN=Example CA"`。
|
|
78
|
+
*
|
|
79
|
+
* 支持反斜杠转义(`\,`、`\=`、`\\`);不做完整 RFC 4514(多值 RDN、
|
|
80
|
+
* `#hex` 编码、十六进制转义等)。
|
|
81
|
+
*
|
|
82
|
+
* @throws CertificateError 格式非法或属性名未知。
|
|
83
|
+
*/
|
|
84
|
+
export function parseDistinguishedName(input) {
|
|
85
|
+
if (typeof input !== 'string' || input.trim().length === 0) {
|
|
86
|
+
throw new CertificateError('parseDistinguishedName: empty input');
|
|
87
|
+
}
|
|
88
|
+
const result = {};
|
|
89
|
+
for (const part of splitUnescaped(input, ',')) {
|
|
90
|
+
const eq = indexOfUnescaped(part, '=');
|
|
91
|
+
if (eq < 0)
|
|
92
|
+
throw new CertificateError(`parseDistinguishedName: missing '=' in "${part}"`);
|
|
93
|
+
const label = decodeEscapes(part.slice(0, eq)).trim();
|
|
94
|
+
const value = decodeEscapes(part.slice(eq + 1)).trim();
|
|
95
|
+
const field = FIELD_BY_LABEL[label.toUpperCase()];
|
|
96
|
+
if (field === undefined) {
|
|
97
|
+
throw new CertificateError(`parseDistinguishedName: unsupported attribute "${label}"`);
|
|
98
|
+
}
|
|
99
|
+
result[field] = value;
|
|
100
|
+
}
|
|
101
|
+
if (result.commonName === undefined) {
|
|
102
|
+
throw new CertificateError('parseDistinguishedName: CN is required');
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
commonName: result.commonName,
|
|
106
|
+
...(result.organization === undefined ? {} : { organization: result.organization }),
|
|
107
|
+
...(result.organizationalUnit === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { organizationalUnit: result.organizationalUnit }),
|
|
110
|
+
...(result.country === undefined ? {} : { country: result.country }),
|
|
111
|
+
...(result.state === undefined ? {} : { state: result.state }),
|
|
112
|
+
...(result.locality === undefined ? {} : { locality: result.locality }),
|
|
113
|
+
...(result.emailAddress === undefined ? {} : { emailAddress: result.emailAddress }),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// ---------- 校验 ----------
|
|
117
|
+
function validateSubject(dn, what) {
|
|
118
|
+
if (typeof dn !== 'object' || dn === null || typeof dn.commonName !== 'string') {
|
|
119
|
+
throw new TypeError(`${what}: commonName is required`);
|
|
120
|
+
}
|
|
121
|
+
if (dn.commonName.length === 0) {
|
|
122
|
+
throw new CertificateError(`${what}: commonName must not be empty`);
|
|
123
|
+
}
|
|
124
|
+
for (const key of [
|
|
125
|
+
'organization',
|
|
126
|
+
'organizationalUnit',
|
|
127
|
+
'state',
|
|
128
|
+
'locality',
|
|
129
|
+
'emailAddress',
|
|
130
|
+
]) {
|
|
131
|
+
const value = dn[key];
|
|
132
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
133
|
+
throw new TypeError(`${what}.${key} must be a string`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (dn.country !== undefined && !/^[A-Za-z]{2}$/.test(dn.country)) {
|
|
137
|
+
throw new CertificateError(`${what}.country must be a 2-letter code`);
|
|
138
|
+
}
|
|
139
|
+
if (dn.emailAddress !== undefined) {
|
|
140
|
+
assertAscii(dn.emailAddress, `${what}.emailAddress`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function validateSigner(signer) {
|
|
144
|
+
if (typeof signer !== 'object' || signer === null || signer.privateKey === undefined) {
|
|
145
|
+
throw new TypeError('signer.privateKey is required');
|
|
146
|
+
}
|
|
147
|
+
const cert = signer.certificate;
|
|
148
|
+
if (cert === undefined)
|
|
149
|
+
return;
|
|
150
|
+
// 红线 #4:只有 CA 证书才能签发子证书。
|
|
151
|
+
if (cert.basicConstraints?.ca !== true) {
|
|
152
|
+
throw new CertificateError('signer certificate is not a CA (basicConstraints.ca !== true)');
|
|
153
|
+
}
|
|
154
|
+
if (cert.keyUsage !== undefined && !cert.keyUsage.keyCertSign) {
|
|
155
|
+
throw new CertificateError('signer certificate keyUsage does not include keyCertSign');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function validateBasicConstraints(opts) {
|
|
159
|
+
const basic = opts.basicConstraints;
|
|
160
|
+
const keyUsage = opts.keyUsage;
|
|
161
|
+
if (basic === undefined)
|
|
162
|
+
return;
|
|
163
|
+
if (basic.ca) {
|
|
164
|
+
if (keyUsage !== undefined && !keyUsage.includes('keyCertSign')) {
|
|
165
|
+
throw new CertificateError('CA certificate requires keyUsage keyCertSign');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
if (keyUsage?.includes('keyCertSign')) {
|
|
170
|
+
throw new CertificateError('basicConstraints.ca=false cannot be combined with keyUsage keyCertSign');
|
|
171
|
+
}
|
|
172
|
+
if (basic.pathLen !== undefined) {
|
|
173
|
+
throw new RangeError('basicConstraints.pathLen is only valid for CA certificates');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function validateSerialNumber(serialNumber) {
|
|
178
|
+
if (typeof serialNumber !== 'bigint')
|
|
179
|
+
throw new TypeError('serialNumber must be a bigint');
|
|
180
|
+
if (serialNumber <= 0n)
|
|
181
|
+
throw new RangeError('serialNumber must be positive');
|
|
182
|
+
if (serialNumber >= 1n << 160n) {
|
|
183
|
+
throw new RangeError('serialNumber must fit in 20 bytes');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validateValidity(validity) {
|
|
187
|
+
for (const [name, date] of [
|
|
188
|
+
['notBefore', validity.notBefore],
|
|
189
|
+
['notAfter', validity.notAfter],
|
|
190
|
+
]) {
|
|
191
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
192
|
+
throw new TypeError(`${name} must be a valid Date`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// 证书时间精度到秒;按秒比较避免毫秒差导致编码后两者相等。
|
|
196
|
+
const nb = Math.floor(validity.notBefore.getTime() / 1000);
|
|
197
|
+
const na = Math.floor(validity.notAfter.getTime() / 1000);
|
|
198
|
+
if (na <= nb)
|
|
199
|
+
throw new RangeError('notAfter must be later than notBefore');
|
|
200
|
+
}
|
|
201
|
+
function validateSubjectAltNames(names) {
|
|
202
|
+
if (names === undefined)
|
|
203
|
+
return;
|
|
204
|
+
for (const name of names) {
|
|
205
|
+
if (name === null || typeof name !== 'object' || typeof name.value !== 'string') {
|
|
206
|
+
throw new TypeError('subjectAltNames: each entry needs { type, value }');
|
|
207
|
+
}
|
|
208
|
+
if (name.value.length === 0) {
|
|
209
|
+
throw new CertificateError('subjectAltNames: value must not be empty');
|
|
210
|
+
}
|
|
211
|
+
switch (name.type) {
|
|
212
|
+
case 'dns':
|
|
213
|
+
assertAscii(name.value, 'subjectAltNames dNSName');
|
|
214
|
+
break;
|
|
215
|
+
case 'email':
|
|
216
|
+
assertAscii(name.value, 'subjectAltNames rfc822Name');
|
|
217
|
+
break;
|
|
218
|
+
case 'uri':
|
|
219
|
+
assertAscii(name.value, 'subjectAltNames URI');
|
|
220
|
+
break;
|
|
221
|
+
case 'ip':
|
|
222
|
+
// 具体解析在编码阶段完成;这里只做早期校验。
|
|
223
|
+
break;
|
|
224
|
+
default:
|
|
225
|
+
throw new CertificateError(`subjectAltNames: unknown type "${String(name.type)}"`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function assertAscii(value, what) {
|
|
230
|
+
for (let i = 0; i < value.length; i++) {
|
|
231
|
+
if (value.charCodeAt(i) > 0x7f)
|
|
232
|
+
throw new CertificateError(`${what} must be ASCII`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ---------- 扩展 ----------
|
|
236
|
+
function buildExtensions(opts, publicKey, selfSigned) {
|
|
237
|
+
validateSubjectAltNames(opts.subjectAltNames);
|
|
238
|
+
const list = [];
|
|
239
|
+
if (opts.basicConstraints !== undefined) {
|
|
240
|
+
list.push({
|
|
241
|
+
oid: OID_BASIC_CONSTRAINTS,
|
|
242
|
+
critical: true,
|
|
243
|
+
value: encodeBasicConstraints(opts.basicConstraints),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (opts.keyUsage !== undefined && opts.keyUsage.length > 0) {
|
|
247
|
+
list.push({ oid: OID_KEY_USAGE, critical: true, value: encodeKeyUsage(opts.keyUsage) });
|
|
248
|
+
}
|
|
249
|
+
if (opts.extendedKeyUsage !== undefined && opts.extendedKeyUsage.length > 0) {
|
|
250
|
+
list.push({
|
|
251
|
+
oid: OID_EXTENDED_KEY_USAGE,
|
|
252
|
+
critical: false,
|
|
253
|
+
value: encodeExtendedKeyUsage(opts.extendedKeyUsage),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (opts.subjectAltNames !== undefined && opts.subjectAltNames.length > 0) {
|
|
257
|
+
list.push({
|
|
258
|
+
oid: OID_SUBJECT_ALT_NAME,
|
|
259
|
+
critical: false,
|
|
260
|
+
value: encodeSubjectAltName(opts.subjectAltNames),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const skiOption = opts.subjectKeyIdentifier ?? true;
|
|
264
|
+
let ski;
|
|
265
|
+
if (skiOption !== false) {
|
|
266
|
+
const hash = typeof skiOption === 'object' ? skiOption.hash : 'sm3';
|
|
267
|
+
const digest = hash === 'sha1' ? sha1Hex(publicKey) : sm3.hash(publicKey);
|
|
268
|
+
ski = hexToBytes(digest, 'subjectKeyIdentifier').slice(0, 20);
|
|
269
|
+
list.push({
|
|
270
|
+
oid: OID_SUBJECT_KEY_IDENTIFIER,
|
|
271
|
+
critical: false,
|
|
272
|
+
value: encodeSubjectKeyIdentifier(ski),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const wantAki = opts.authorityKeyIdentifier ?? true;
|
|
276
|
+
if (wantAki) {
|
|
277
|
+
const aki = opts.signer.certificate?.subjectKeyIdentifier ?? (selfSigned ? ski : undefined);
|
|
278
|
+
if (aki !== undefined) {
|
|
279
|
+
list.push({
|
|
280
|
+
oid: OID_AUTHORITY_KEY_IDENTIFIER,
|
|
281
|
+
critical: false,
|
|
282
|
+
value: encodeAuthorityKeyIdentifier(aki),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { list };
|
|
287
|
+
}
|
|
288
|
+
// ---------- 辅助 ----------
|
|
289
|
+
function resolveSubjectPublicKey(opts) {
|
|
290
|
+
if (opts.subjectPublicKey !== undefined) {
|
|
291
|
+
return readPublicKey(opts.subjectPublicKey, 'subjectPublicKey');
|
|
292
|
+
}
|
|
293
|
+
if (opts.signer.certificate !== undefined) {
|
|
294
|
+
throw new CertificateError('subjectPublicKey is required when signing with a CA certificate');
|
|
295
|
+
}
|
|
296
|
+
return readPublicKey(sm2.getPublicKey(opts.signer.privateKey), 'signer public key');
|
|
297
|
+
}
|
|
298
|
+
function readPublicKey(input, where) {
|
|
299
|
+
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
300
|
+
try {
|
|
301
|
+
return new Uint8Array(parseSm2PublicKeyPem(input));
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
throw new CertificateError(`${where}: invalid SM2 public key PEM`, { cause: err });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const bytes = typeof input === 'string' ? toBytes(input, where) : new Uint8Array(input);
|
|
308
|
+
return normalizePublicKey(bytes);
|
|
309
|
+
}
|
|
310
|
+
function nameToInput(name) {
|
|
311
|
+
return {
|
|
312
|
+
// Name.commonName 理论上可缺失;encodeName 会在全空时报错。
|
|
313
|
+
commonName: name.commonName ?? '',
|
|
314
|
+
...(name.organization === undefined ? {} : { organization: name.organization }),
|
|
315
|
+
...(name.organizationalUnit === undefined
|
|
316
|
+
? {}
|
|
317
|
+
: { organizationalUnit: name.organizationalUnit }),
|
|
318
|
+
...(name.country === undefined ? {} : { country: name.country }),
|
|
319
|
+
...(name.state === undefined ? {} : { state: name.state }),
|
|
320
|
+
...(name.locality === undefined ? {} : { locality: name.locality }),
|
|
321
|
+
...(name.emailAddress === undefined ? {} : { emailAddress: name.emailAddress }),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function randomSerialNumber() {
|
|
325
|
+
for (;;) {
|
|
326
|
+
const bytes = randomBytes(16);
|
|
327
|
+
// 最高位清零:编码时无需额外补 0x00,也满足“正整数”约束。
|
|
328
|
+
bytes[0] = bytes[0] & 0x7f;
|
|
329
|
+
let value = 0n;
|
|
330
|
+
for (const byte of bytes)
|
|
331
|
+
value = (value << 8n) | BigInt(byte);
|
|
332
|
+
if (value > 0n)
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const FIELD_BY_LABEL = {
|
|
337
|
+
CN: 'commonName',
|
|
338
|
+
'2.5.4.3': 'commonName',
|
|
339
|
+
O: 'organization',
|
|
340
|
+
'2.5.4.10': 'organization',
|
|
341
|
+
OU: 'organizationalUnit',
|
|
342
|
+
'2.5.4.11': 'organizationalUnit',
|
|
343
|
+
C: 'country',
|
|
344
|
+
'2.5.4.6': 'country',
|
|
345
|
+
ST: 'state',
|
|
346
|
+
'2.5.4.8': 'state',
|
|
347
|
+
L: 'locality',
|
|
348
|
+
'2.5.4.7': 'locality',
|
|
349
|
+
EMAILADDRESS: 'emailAddress',
|
|
350
|
+
E: 'emailAddress',
|
|
351
|
+
'1.2.840.113549.1.9.1': 'emailAddress',
|
|
352
|
+
};
|
|
353
|
+
function splitUnescaped(input, separator) {
|
|
354
|
+
const parts = [];
|
|
355
|
+
let current = '';
|
|
356
|
+
let escaped = false;
|
|
357
|
+
for (const ch of input) {
|
|
358
|
+
if (escaped) {
|
|
359
|
+
current += `\\${ch}`;
|
|
360
|
+
escaped = false;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (ch === '\\') {
|
|
364
|
+
escaped = true;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (ch === separator) {
|
|
368
|
+
parts.push(current);
|
|
369
|
+
current = '';
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
current += ch;
|
|
373
|
+
}
|
|
374
|
+
if (escaped)
|
|
375
|
+
throw new CertificateError('parseDistinguishedName: trailing escape');
|
|
376
|
+
parts.push(current);
|
|
377
|
+
return parts;
|
|
378
|
+
}
|
|
379
|
+
function indexOfUnescaped(input, target) {
|
|
380
|
+
let escaped = false;
|
|
381
|
+
for (let i = 0; i < input.length; i++) {
|
|
382
|
+
const ch = input[i];
|
|
383
|
+
if (escaped) {
|
|
384
|
+
escaped = false;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (ch === '\\') {
|
|
388
|
+
escaped = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (ch === target)
|
|
392
|
+
return i;
|
|
393
|
+
}
|
|
394
|
+
return -1;
|
|
395
|
+
}
|
|
396
|
+
function decodeEscapes(input) {
|
|
397
|
+
let out = '';
|
|
398
|
+
let escaped = false;
|
|
399
|
+
for (const ch of input) {
|
|
400
|
+
if (escaped) {
|
|
401
|
+
out += ch;
|
|
402
|
+
escaped = false;
|
|
403
|
+
}
|
|
404
|
+
else if (ch === '\\') {
|
|
405
|
+
escaped = true;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
out += ch;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (escaped)
|
|
412
|
+
throw new CertificateError('parseDistinguishedName: trailing escape');
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X.509 / CMS 使用的 OID 常量。
|
|
3
|
+
*
|
|
4
|
+
* 只收录本库直接需要的项;未知 OID 一律保留原值,不做白名单校验
|
|
5
|
+
* (红线 #9:国密证书常带私有扩展)。
|
|
6
|
+
*/
|
|
7
|
+
/** `id-ecPublicKey`(1.2.840.10045.2.1)。SM2 证书的常见算法标识。 */
|
|
8
|
+
export declare const OID_ID_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
|
|
9
|
+
/** `sm2p256v1` 推荐曲线(1.2.156.10197.1.301)。 */
|
|
10
|
+
export declare const OID_SM2_CURVE = "1.2.156.10197.1.301";
|
|
11
|
+
/** SM3withSM2(1.2.156.10197.1.501)。 */
|
|
12
|
+
export declare const OID_SM3_WITH_SM2 = "1.2.156.10197.1.501";
|
|
13
|
+
/** `rsaEncryption`(1.2.840.113549.1.1.1)。 */
|
|
14
|
+
export declare const OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
|
|
15
|
+
/** 任意 `sha*WithRSAEncryption` 的公共前缀。 */
|
|
16
|
+
export declare const OID_RSA_SIGNATURE_PREFIX = "1.2.840.113549.1.1.";
|
|
17
|
+
/** `sha256WithRSAEncryption`(1.2.840.113549.1.1.11)。 */
|
|
18
|
+
export declare const OID_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
|
|
19
|
+
export declare const OID_COMMON_NAME = "2.5.4.3";
|
|
20
|
+
export declare const OID_ORGANIZATION = "2.5.4.10";
|
|
21
|
+
export declare const OID_ORGANIZATIONAL_UNIT = "2.5.4.11";
|
|
22
|
+
export declare const OID_COUNTRY = "2.5.4.6";
|
|
23
|
+
export declare const OID_STATE = "2.5.4.8";
|
|
24
|
+
export declare const OID_LOCALITY = "2.5.4.7";
|
|
25
|
+
export declare const OID_EMAIL_ADDRESS = "1.2.840.113549.1.9.1";
|
|
26
|
+
/** Name 属性 OID → RFC 4514 简写。 */
|
|
27
|
+
export declare const NAME_OID_LABELS: Readonly<Record<string, string>>;
|
|
28
|
+
export declare const OID_SUBJECT_KEY_IDENTIFIER = "2.5.29.14";
|
|
29
|
+
export declare const OID_KEY_USAGE = "2.5.29.15";
|
|
30
|
+
export declare const OID_SUBJECT_ALT_NAME = "2.5.29.17";
|
|
31
|
+
export declare const OID_ISSUER_ALT_NAME = "2.5.29.18";
|
|
32
|
+
export declare const OID_BASIC_CONSTRAINTS = "2.5.29.19";
|
|
33
|
+
export declare const OID_AUTHORITY_KEY_IDENTIFIER = "2.5.29.35";
|
|
34
|
+
export declare const OID_EXTENDED_KEY_USAGE = "2.5.29.37";
|
|
35
|
+
export declare const OID_EKU_SERVER_AUTH = "1.3.6.1.5.5.7.3.1";
|
|
36
|
+
export declare const OID_EKU_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
|
|
37
|
+
export declare const OID_EKU_CODE_SIGNING = "1.3.6.1.5.5.7.3.3";
|
|
38
|
+
export declare const OID_EKU_EMAIL_PROTECTION = "1.3.6.1.5.5.7.3.4";
|
|
39
|
+
export declare const OID_EKU_TIME_STAMPING = "1.3.6.1.5.5.7.3.8";
|
|
40
|
+
export declare const OID_EKU_OCSP_SIGNING = "1.3.6.1.5.5.7.3.9";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X.509 / CMS 使用的 OID 常量。
|
|
3
|
+
*
|
|
4
|
+
* 只收录本库直接需要的项;未知 OID 一律保留原值,不做白名单校验
|
|
5
|
+
* (红线 #9:国密证书常带私有扩展)。
|
|
6
|
+
*/
|
|
7
|
+
// ---------- 算法 ----------
|
|
8
|
+
/** `id-ecPublicKey`(1.2.840.10045.2.1)。SM2 证书的常见算法标识。 */
|
|
9
|
+
export const OID_ID_EC_PUBLIC_KEY = '1.2.840.10045.2.1';
|
|
10
|
+
/** `sm2p256v1` 推荐曲线(1.2.156.10197.1.301)。 */
|
|
11
|
+
export const OID_SM2_CURVE = '1.2.156.10197.1.301';
|
|
12
|
+
/** SM3withSM2(1.2.156.10197.1.501)。 */
|
|
13
|
+
export const OID_SM3_WITH_SM2 = '1.2.156.10197.1.501';
|
|
14
|
+
/** `rsaEncryption`(1.2.840.113549.1.1.1)。 */
|
|
15
|
+
export const OID_RSA_ENCRYPTION = '1.2.840.113549.1.1.1';
|
|
16
|
+
/** 任意 `sha*WithRSAEncryption` 的公共前缀。 */
|
|
17
|
+
export const OID_RSA_SIGNATURE_PREFIX = '1.2.840.113549.1.1.';
|
|
18
|
+
/** `sha256WithRSAEncryption`(1.2.840.113549.1.1.11)。 */
|
|
19
|
+
export const OID_SHA256_WITH_RSA = '1.2.840.113549.1.1.11';
|
|
20
|
+
// ---------- Name 属性 ----------
|
|
21
|
+
export const OID_COMMON_NAME = '2.5.4.3';
|
|
22
|
+
export const OID_ORGANIZATION = '2.5.4.10';
|
|
23
|
+
export const OID_ORGANIZATIONAL_UNIT = '2.5.4.11';
|
|
24
|
+
export const OID_COUNTRY = '2.5.4.6';
|
|
25
|
+
export const OID_STATE = '2.5.4.8';
|
|
26
|
+
export const OID_LOCALITY = '2.5.4.7';
|
|
27
|
+
export const OID_EMAIL_ADDRESS = '1.2.840.113549.1.9.1';
|
|
28
|
+
/** Name 属性 OID → RFC 4514 简写。 */
|
|
29
|
+
export const NAME_OID_LABELS = {
|
|
30
|
+
[OID_COMMON_NAME]: 'CN',
|
|
31
|
+
[OID_ORGANIZATION]: 'O',
|
|
32
|
+
[OID_ORGANIZATIONAL_UNIT]: 'OU',
|
|
33
|
+
[OID_COUNTRY]: 'C',
|
|
34
|
+
[OID_STATE]: 'ST',
|
|
35
|
+
[OID_LOCALITY]: 'L',
|
|
36
|
+
[OID_EMAIL_ADDRESS]: 'emailAddress',
|
|
37
|
+
};
|
|
38
|
+
// ---------- 扩展 ----------
|
|
39
|
+
export const OID_SUBJECT_KEY_IDENTIFIER = '2.5.29.14';
|
|
40
|
+
export const OID_KEY_USAGE = '2.5.29.15';
|
|
41
|
+
export const OID_SUBJECT_ALT_NAME = '2.5.29.17';
|
|
42
|
+
export const OID_ISSUER_ALT_NAME = '2.5.29.18';
|
|
43
|
+
export const OID_BASIC_CONSTRAINTS = '2.5.29.19';
|
|
44
|
+
export const OID_AUTHORITY_KEY_IDENTIFIER = '2.5.29.35';
|
|
45
|
+
export const OID_EXTENDED_KEY_USAGE = '2.5.29.37';
|
|
46
|
+
// ---------- Extended Key Usage ----------
|
|
47
|
+
export const OID_EKU_SERVER_AUTH = '1.3.6.1.5.5.7.3.1';
|
|
48
|
+
export const OID_EKU_CLIENT_AUTH = '1.3.6.1.5.5.7.3.2';
|
|
49
|
+
export const OID_EKU_CODE_SIGNING = '1.3.6.1.5.5.7.3.3';
|
|
50
|
+
export const OID_EKU_EMAIL_PROTECTION = '1.3.6.1.5.5.7.3.4';
|
|
51
|
+
export const OID_EKU_TIME_STAMPING = '1.3.6.1.5.5.7.3.8';
|
|
52
|
+
export const OID_EKU_OCSP_SIGNING = '1.3.6.1.5.5.7.3.9';
|
|
@@ -0,0 +1,24 @@
|
|
|
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 type { Certificate, CertificateInput } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* 解析单张证书。
|
|
14
|
+
*
|
|
15
|
+
* @param input PEM 字符串、base64 字符串、hex 字符串或 DER 字节。
|
|
16
|
+
* @throws CertificateError 结构非法、截断或尾部有多余字节。
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseCertificate(input: CertificateInput): Certificate;
|
|
19
|
+
/**
|
|
20
|
+
* 解析一个或多个证书(PEM bundle / 连续 DER)。
|
|
21
|
+
*
|
|
22
|
+
* @throws CertificateError 任一证书结构非法。
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseCertificates(input: CertificateInput): Certificate[];
|