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
package/README.md
CHANGED
|
@@ -205,11 +205,32 @@ zuc.zucMac(key, macIv, '616263'); // → 4 字节标签
|
|
|
205
205
|
|
|
206
206
|
---
|
|
207
207
|
|
|
208
|
+
### X.509 · CMS · WebCrypto
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { x509, cms, webcrypto } from 'sm-crypto-wasm';
|
|
212
|
+
|
|
213
|
+
const cert = x509.parseCertificate(pem);
|
|
214
|
+
const signed = cms.signData({ data, privateKey, certificate: cert });
|
|
215
|
+
const digest = await webcrypto.subtle.digest('SM3', data);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- **X.509**:证书解析、单证书验签、自签 / 简单 CA 签发([文档](https://jktantan.github.io/sm-crypto-wasm/guide/x509));
|
|
219
|
+
- **CMS**:SignedData detached / attached、`signedAttrs` 原始 DER 验签、BER([文档](https://jktantan.github.io/sm-crypto-wasm/guide/cms));
|
|
220
|
+
- **WebCrypto**:`subtle` 风格的 SM3 / HMAC(SM3) / SM2 / SM4 子集([文档](https://jktantan.github.io/sm-crypto-wasm/guide/webcrypto))。
|
|
221
|
+
|
|
222
|
+
X.509 / CMS 不做证书链路径校验、CRL/OCSP;WebCrypto 不是原生 `crypto.subtle` 的 polyfill。
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
208
226
|
## API 一览
|
|
209
227
|
|
|
210
228
|
| 模块 | 函数 |
|
|
211
229
|
|---|---|
|
|
212
|
-
| `sm2` | `generateKeyPair`(async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
|
|
230
|
+
| `sm2` | `generateKeyPair`(async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `preparePublicKey`(async) · `configurePreparedKeyCache` · `clearPreparedPublicKeys` · `getPreparedPublicKeys` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
|
|
231
|
+
| `webcrypto` | `subtle` · `CryptoKey` |
|
|
232
|
+
| `cms` | `signData` · `verifySignedData` · `parseSignedData` · `CmsError` |
|
|
233
|
+
| `x509` | `parseCertificate` · `parseCertificates` · `createSelfSignedCertificate` · `issueCertificate` · `parseDistinguishedName` · `verifyCertificateSignature` |
|
|
213
234
|
| `sm3` | `hash` · `hmac` |
|
|
214
235
|
| `sm4` | `encryptECB`/`decryptECB` · `encryptCBC`/`decryptCBC` · `encryptCTR`/`decryptCTR` · `encryptCFB`/`decryptCFB` · `encryptOFB`/`decryptOFB` · `encryptGCM`/`decryptGCM` |
|
|
215
236
|
| `sm9` | `generateKeyPair`(async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
|
|
@@ -228,7 +249,7 @@ import { hash } from 'sm-crypto-wasm/sm3';
|
|
|
228
249
|
import { encryptCBC } from 'sm-crypto-wasm/sm4';
|
|
229
250
|
```
|
|
230
251
|
|
|
231
|
-
可用子路径:`sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/zuc`。
|
|
252
|
+
可用子路径:`sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/x509` · `/cms` · `/webcrypto` · `/zuc`。
|
|
232
253
|
|
|
233
254
|
---
|
|
234
255
|
|
|
@@ -566,11 +587,32 @@ zuc.zucMac(key, macIv, '616263'); // → 4-byte tag
|
|
|
566
587
|
|
|
567
588
|
---
|
|
568
589
|
|
|
590
|
+
### X.509 · CMS · WebCrypto
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
import { x509, cms, webcrypto } from 'sm-crypto-wasm';
|
|
594
|
+
|
|
595
|
+
const cert = x509.parseCertificate(pem);
|
|
596
|
+
const signed = cms.signData({ data, privateKey, certificate: cert });
|
|
597
|
+
const digest = await webcrypto.subtle.digest('SM3', data);
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
- **X.509** — parsing, single-certificate verification, self-signed / simple CA issuing ([docs](https://jktantan.github.io/sm-crypto-wasm/en/guide/x509));
|
|
601
|
+
- **CMS** — detached / attached SignedData, original-DER `signedAttrs` verification, BER ([docs](https://jktantan.github.io/sm-crypto-wasm/en/guide/cms));
|
|
602
|
+
- **WebCrypto** — a `subtle`-style subset: SM3 / HMAC(SM3) / SM2 / SM4 ([docs](https://jktantan.github.io/sm-crypto-wasm/en/guide/webcrypto)).
|
|
603
|
+
|
|
604
|
+
No certificate-chain validation or CRL/OCSP for X.509/CMS; the WebCrypto adapter is not a polyfill of native `crypto.subtle`.
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
569
608
|
## API at a glance
|
|
570
609
|
|
|
571
610
|
| Module | Functions |
|
|
572
611
|
|---|---|
|
|
573
|
-
| `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
|
|
612
|
+
| `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `preparePublicKey` (async) · `configurePreparedKeyCache` · `clearPreparedPublicKeys` · `getPreparedPublicKeys` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
|
|
613
|
+
| `webcrypto` | `subtle` · `CryptoKey` |
|
|
614
|
+
| `cms` | `signData` · `verifySignedData` · `parseSignedData` · `CmsError` |
|
|
615
|
+
| `x509` | `parseCertificate` · `parseCertificates` · `createSelfSignedCertificate` · `issueCertificate` · `parseDistinguishedName` · `verifyCertificateSignature` |
|
|
574
616
|
| `sm3` | `hash` · `hmac` |
|
|
575
617
|
| `sm4` | `encryptECB` / `decryptECB` · `encryptCBC` / `decryptCBC` · `encryptCTR` / `decryptCTR` · `encryptCFB` / `decryptCFB` · `encryptOFB` / `decryptOFB` · `encryptGCM` / `decryptGCM` |
|
|
576
618
|
| `sm9` | `generateKeyPair` (async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
|
|
@@ -594,7 +636,7 @@ import { hash } from 'sm-crypto-wasm/sm3';
|
|
|
594
636
|
import { encryptCBC } from 'sm-crypto-wasm/sm4';
|
|
595
637
|
```
|
|
596
638
|
|
|
597
|
-
Available sub-paths: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/zuc`.
|
|
639
|
+
Available sub-paths: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/x509` · `/cms` · `/webcrypto` · `/zuc`.
|
|
598
640
|
|
|
599
641
|
---
|
|
600
642
|
|
|
@@ -961,11 +1003,32 @@ zuc.zucMac(key, macIv, '616263'); // → 4-байтовый тег
|
|
|
961
1003
|
|
|
962
1004
|
---
|
|
963
1005
|
|
|
1006
|
+
### X.509 · CMS · WebCrypto
|
|
1007
|
+
|
|
1008
|
+
```ts
|
|
1009
|
+
import { x509, cms, webcrypto } from 'sm-crypto-wasm';
|
|
1010
|
+
|
|
1011
|
+
const cert = x509.parseCertificate(pem);
|
|
1012
|
+
const signed = cms.signData({ data, privateKey, certificate: cert });
|
|
1013
|
+
const digest = await webcrypto.subtle.digest('SM3', data);
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
- **X.509** — разбор, проверка одного сертификата, самоподпись / выпуск через простой CA ([документация](https://jktantan.github.io/sm-crypto-wasm/ru/guide/x509));
|
|
1017
|
+
- **CMS** — detached / attached SignedData, проверка по исходным DER `signedAttrs`, BER ([документация](https://jktantan.github.io/sm-crypto-wasm/ru/guide/cms));
|
|
1018
|
+
- **WebCrypto** — подмножество в стиле `subtle`: SM3 / HMAC(SM3) / SM2 / SM4 ([документация](https://jktantan.github.io/sm-crypto-wasm/ru/guide/webcrypto)).
|
|
1019
|
+
|
|
1020
|
+
Для X.509 / CMS нет проверки цепочки и CRL/OCSP; WebCrypto — не polyfill нативного `crypto.subtle`.
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
964
1024
|
## Сводка API
|
|
965
1025
|
|
|
966
1026
|
| Модуль | Функции |
|
|
967
1027
|
|---|---|
|
|
968
|
-
| `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
|
|
1028
|
+
| `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `preparePublicKey` (async) · `configurePreparedKeyCache` · `clearPreparedPublicKeys` · `getPreparedPublicKeys` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
|
|
1029
|
+
| `webcrypto` | `subtle` · `CryptoKey` |
|
|
1030
|
+
| `cms` | `signData` · `verifySignedData` · `parseSignedData` · `CmsError` |
|
|
1031
|
+
| `x509` | `parseCertificate` · `parseCertificates` · `createSelfSignedCertificate` · `issueCertificate` · `parseDistinguishedName` · `verifyCertificateSignature` |
|
|
969
1032
|
| `sm3` | `hash` · `hmac` |
|
|
970
1033
|
| `sm4` | `encryptECB` / `decryptECB` · `encryptCBC` / `decryptCBC` · `encryptCTR` / `decryptCTR` · `encryptCFB` / `decryptCFB` · `encryptOFB` / `decryptOFB` · `encryptGCM` / `decryptGCM` |
|
|
971
1034
|
| `sm9` | `generateKeyPair` (async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
|
|
@@ -989,7 +1052,7 @@ import { hash } from 'sm-crypto-wasm/sm3';
|
|
|
989
1052
|
import { encryptCBC } from 'sm-crypto-wasm/sm4';
|
|
990
1053
|
```
|
|
991
1054
|
|
|
992
|
-
Доступные подпути: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/zuc`.
|
|
1055
|
+
Доступные подпути: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/x509` · `/cms` · `/webcrypto` · `/zuc`.
|
|
993
1056
|
|
|
994
1057
|
---
|
|
995
1058
|
|
|
@@ -1156,4 +1219,4 @@ biome).
|
|
|
1156
1219
|
- [gmkit / gmkitx](https://github.com/gmkits/gmkit) — источник, из
|
|
1157
1220
|
которого портирована реализация ZUC.
|
|
1158
1221
|
|
|
1159
|
-
</details>
|
|
1222
|
+
</details>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASN.1 DER/BER 解析器。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 X.509 与 CMS 需要的 BER/DER 子集:
|
|
5
|
+
* - definite-length(短/长格式);
|
|
6
|
+
* - BER indefinite-length(`0x80` + EOC `00 00`),**仅 `parseBer` 允许**;
|
|
7
|
+
* - 高 tag number(`0x1f` + base128);
|
|
8
|
+
* - constructed / primitive 递归。
|
|
9
|
+
*
|
|
10
|
+
* 严格性:
|
|
11
|
+
* - `parseDer` 拒绝 indefinite、非最小长度编码、EOC、尾部多余字节;
|
|
12
|
+
* - `parseBer` 放宽长度编码并允许 indefinite,但仍拒绝截断与尾部多余字节;
|
|
13
|
+
* - 单对象解析一律拒绝尾部多余字节(红线 #3)。
|
|
14
|
+
*
|
|
15
|
+
* 每个节点保留 `raw`(输入缓冲上的视图),供 CMS 验签与证书 TBS 直接使用。
|
|
16
|
+
*/
|
|
17
|
+
import { type Asn1Node } from './types.js';
|
|
18
|
+
/** 解析选项。 */
|
|
19
|
+
export interface ParseOptions {
|
|
20
|
+
/** 允许 BER indefinite-length。仅 CMS 路径应开启。 */
|
|
21
|
+
allowIndefinite?: boolean;
|
|
22
|
+
/** 是否严格校验长度编码的最小形式(DER 默认开启)。 */
|
|
23
|
+
strictLength?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 解析单个 DER 对象。
|
|
27
|
+
*
|
|
28
|
+
* @throws Asn1Error 截断、超出深度、非 DER 编码、或根节点后有多余字节。
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseDer(buf: Uint8Array): Asn1Node;
|
|
31
|
+
/**
|
|
32
|
+
* 解析单个 BER 对象(允许 indefinite-length)。
|
|
33
|
+
*
|
|
34
|
+
* @throws Asn1Error 截断、超出深度、或根节点后有多余字节。
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseBer(buf: Uint8Array): Asn1Node;
|
|
37
|
+
/**
|
|
38
|
+
* 解析连续的多个 DER 对象(证书 bundle / CMS 文件)。
|
|
39
|
+
*
|
|
40
|
+
* 返回按出现顺序排列的节点;允许空输入并返回空数组。
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseDerAll(buf: Uint8Array): Asn1Node[];
|
|
43
|
+
/** 解析连续的多个 BER 对象(允许 indefinite-length)。 */
|
|
44
|
+
export declare function parseBerAll(buf: Uint8Array): Asn1Node[];
|
|
45
|
+
/**
|
|
46
|
+
* 读取 INTEGER(二进制补码,支持负数)。
|
|
47
|
+
*
|
|
48
|
+
* @throws Asn1Error 节点不是 INTEGER。
|
|
49
|
+
*/
|
|
50
|
+
export declare function readInteger(node: Asn1Node, what?: string): bigint;
|
|
51
|
+
/** 读取 BOOLEAN(DER 中 TRUE 必须为 0xFF)。 */
|
|
52
|
+
export declare function readBoolean(node: Asn1Node, what?: string): boolean;
|
|
53
|
+
/** BIT STRING 内容。 */
|
|
54
|
+
export interface BitString {
|
|
55
|
+
/** 去掉 unused bits 后的有效字节(尾部未用位保留,由调用方屏蔽)。 */
|
|
56
|
+
readonly bytes: Uint8Array;
|
|
57
|
+
/** 末尾未使用位数(0..7)。 */
|
|
58
|
+
readonly unusedBits: number;
|
|
59
|
+
}
|
|
60
|
+
/** 读取 BIT STRING。 */
|
|
61
|
+
export declare function readBitString(node: Asn1Node, what?: string): BitString;
|
|
62
|
+
/** 读取 OCTET STRING。 */
|
|
63
|
+
export declare function readOctetString(node: Asn1Node, what?: string): Uint8Array;
|
|
64
|
+
/** 读取 OBJECT IDENTIFIER。 */
|
|
65
|
+
export declare function readOid(node: Asn1Node, what?: string): string;
|
|
66
|
+
/** 读取 UTCTime / GeneralizedTime。 */
|
|
67
|
+
export declare function readTime(node: Asn1Node, what?: string): Date;
|
|
68
|
+
/**
|
|
69
|
+
* 读取 ASN.1 字符串。
|
|
70
|
+
*
|
|
71
|
+
* 支持 UTF8String / PrintableString / IA5String / NumericString /
|
|
72
|
+
* TeletexString / BMPString / UniversalString;其他类型抛 `Asn1Error`。
|
|
73
|
+
*/
|
|
74
|
+
export declare function readString(node: Asn1Node, what?: string): string;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASN.1 DER/BER 解析器。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 X.509 与 CMS 需要的 BER/DER 子集:
|
|
5
|
+
* - definite-length(短/长格式);
|
|
6
|
+
* - BER indefinite-length(`0x80` + EOC `00 00`),**仅 `parseBer` 允许**;
|
|
7
|
+
* - 高 tag number(`0x1f` + base128);
|
|
8
|
+
* - constructed / primitive 递归。
|
|
9
|
+
*
|
|
10
|
+
* 严格性:
|
|
11
|
+
* - `parseDer` 拒绝 indefinite、非最小长度编码、EOC、尾部多余字节;
|
|
12
|
+
* - `parseBer` 放宽长度编码并允许 indefinite,但仍拒绝截断与尾部多余字节;
|
|
13
|
+
* - 单对象解析一律拒绝尾部多余字节(红线 #3)。
|
|
14
|
+
*
|
|
15
|
+
* 每个节点保留 `raw`(输入缓冲上的视图),供 CMS 验签与证书 TBS 直接使用。
|
|
16
|
+
*/
|
|
17
|
+
import { decodeOid } from './oid.js';
|
|
18
|
+
import { parseTime } from './time.js';
|
|
19
|
+
import { Asn1Class, Asn1Error, UniversalTag } from './types.js';
|
|
20
|
+
/** constructed 嵌套深度上限,防栈溢出与恶意深嵌套。 */
|
|
21
|
+
const MAX_DEPTH = 64;
|
|
22
|
+
/**
|
|
23
|
+
* 解析单个 DER 对象。
|
|
24
|
+
*
|
|
25
|
+
* @throws Asn1Error 截断、超出深度、非 DER 编码、或根节点后有多余字节。
|
|
26
|
+
*/
|
|
27
|
+
export function parseDer(buf) {
|
|
28
|
+
return parseSingle(buf, { allowIndefinite: false, strictLength: true }, 'parseDer');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 解析单个 BER 对象(允许 indefinite-length)。
|
|
32
|
+
*
|
|
33
|
+
* @throws Asn1Error 截断、超出深度、或根节点后有多余字节。
|
|
34
|
+
*/
|
|
35
|
+
export function parseBer(buf) {
|
|
36
|
+
return parseSingle(buf, { allowIndefinite: true, strictLength: false }, 'parseBer');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 解析连续的多个 DER 对象(证书 bundle / CMS 文件)。
|
|
40
|
+
*
|
|
41
|
+
* 返回按出现顺序排列的节点;允许空输入并返回空数组。
|
|
42
|
+
*/
|
|
43
|
+
export function parseDerAll(buf) {
|
|
44
|
+
return parseAll(buf, { allowIndefinite: false, strictLength: true }, 'parseDerAll');
|
|
45
|
+
}
|
|
46
|
+
/** 解析连续的多个 BER 对象(允许 indefinite-length)。 */
|
|
47
|
+
export function parseBerAll(buf) {
|
|
48
|
+
return parseAll(buf, { allowIndefinite: true, strictLength: false }, 'parseBerAll');
|
|
49
|
+
}
|
|
50
|
+
function parseSingle(buf, opts, where) {
|
|
51
|
+
const reader = new Reader(buf, opts, where);
|
|
52
|
+
const node = reader.readNode(0);
|
|
53
|
+
if (reader.remaining() !== 0) {
|
|
54
|
+
throw new Asn1Error(`${where}: ${reader.remaining()} trailing bytes after root node`);
|
|
55
|
+
}
|
|
56
|
+
return node;
|
|
57
|
+
}
|
|
58
|
+
function parseAll(buf, opts, where) {
|
|
59
|
+
const reader = new Reader(buf, opts, where);
|
|
60
|
+
const nodes = [];
|
|
61
|
+
while (reader.remaining() > 0) {
|
|
62
|
+
nodes.push(reader.readNode(0));
|
|
63
|
+
}
|
|
64
|
+
return nodes;
|
|
65
|
+
}
|
|
66
|
+
class Reader {
|
|
67
|
+
buf;
|
|
68
|
+
opts;
|
|
69
|
+
where;
|
|
70
|
+
pos = 0;
|
|
71
|
+
constructor(buf, opts, where) {
|
|
72
|
+
this.buf = buf;
|
|
73
|
+
this.opts = opts;
|
|
74
|
+
this.where = where;
|
|
75
|
+
}
|
|
76
|
+
remaining() {
|
|
77
|
+
return this.buf.length - this.pos;
|
|
78
|
+
}
|
|
79
|
+
byte() {
|
|
80
|
+
if (this.pos >= this.buf.length) {
|
|
81
|
+
throw new Asn1Error(`${this.where}: truncated at offset ${this.pos}`);
|
|
82
|
+
}
|
|
83
|
+
return this.buf[this.pos++];
|
|
84
|
+
}
|
|
85
|
+
/** 读取一个完整 TLV。 */
|
|
86
|
+
readNode(depth) {
|
|
87
|
+
if (depth > MAX_DEPTH) {
|
|
88
|
+
throw new Asn1Error(`${this.where}: nesting deeper than ${MAX_DEPTH}`);
|
|
89
|
+
}
|
|
90
|
+
const start = this.pos;
|
|
91
|
+
const first = this.byte();
|
|
92
|
+
const cls = ((first >> 6) & 0x03);
|
|
93
|
+
const constructed = (first & 0x20) !== 0;
|
|
94
|
+
const tag = (first & 0x1f) === 0x1f ? this.readHighTag() : first & 0x1f;
|
|
95
|
+
// EndOfContent 只能作为 indefinite-length 的结束标记出现。
|
|
96
|
+
if (cls === Asn1Class.Universal && tag === UniversalTag.EndOfContent && !constructed) {
|
|
97
|
+
const lb = this.byte();
|
|
98
|
+
if (lb !== 0)
|
|
99
|
+
throw new Asn1Error(`${this.where}: malformed EndOfContent`);
|
|
100
|
+
throw new Asn1Error(`${this.where}: unexpected EndOfContent at offset ${start}`);
|
|
101
|
+
}
|
|
102
|
+
const lb = this.byte();
|
|
103
|
+
let length = 0;
|
|
104
|
+
let indefinite = false;
|
|
105
|
+
if (lb === 0x80) {
|
|
106
|
+
if (!this.opts.allowIndefinite) {
|
|
107
|
+
throw new Asn1Error(`${this.where}: indefinite-length encoding is not allowed`);
|
|
108
|
+
}
|
|
109
|
+
if (!constructed) {
|
|
110
|
+
throw new Asn1Error(`${this.where}: indefinite length on primitive tag ${tag}`);
|
|
111
|
+
}
|
|
112
|
+
indefinite = true;
|
|
113
|
+
}
|
|
114
|
+
else if (lb < 0x80) {
|
|
115
|
+
length = lb;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
length = this.readLongLength(lb & 0x7f);
|
|
119
|
+
}
|
|
120
|
+
const contentStart = this.pos;
|
|
121
|
+
if (indefinite) {
|
|
122
|
+
// indefinite:一直读到 EOC(00 00)。
|
|
123
|
+
const children = [];
|
|
124
|
+
for (;;) {
|
|
125
|
+
if (this.remaining() < 2) {
|
|
126
|
+
throw new Asn1Error(`${this.where}: truncated indefinite-length content`);
|
|
127
|
+
}
|
|
128
|
+
if (this.buf[this.pos] === 0 && this.buf[this.pos + 1] === 0) {
|
|
129
|
+
this.pos += 2;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
children.push(this.readNode(depth + 1));
|
|
133
|
+
}
|
|
134
|
+
const contentEnd = this.pos - 2;
|
|
135
|
+
const value = this.buf.subarray(contentStart, contentEnd);
|
|
136
|
+
return {
|
|
137
|
+
cls,
|
|
138
|
+
tag,
|
|
139
|
+
constructed,
|
|
140
|
+
indefinite,
|
|
141
|
+
length: value.length,
|
|
142
|
+
value,
|
|
143
|
+
children,
|
|
144
|
+
raw: this.buf.subarray(start, this.pos),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (this.pos + length > this.buf.length) {
|
|
148
|
+
throw new Asn1Error(`${this.where}: truncated value, need ${length} bytes at offset ${this.pos}`);
|
|
149
|
+
}
|
|
150
|
+
this.pos += length;
|
|
151
|
+
const value = this.buf.subarray(contentStart, contentStart + length);
|
|
152
|
+
if (constructed) {
|
|
153
|
+
const inner = new Reader(value, this.opts, this.where);
|
|
154
|
+
const children = [];
|
|
155
|
+
while (inner.remaining() > 0) {
|
|
156
|
+
children.push(inner.readNode(depth + 1));
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
cls,
|
|
160
|
+
tag,
|
|
161
|
+
constructed,
|
|
162
|
+
indefinite,
|
|
163
|
+
length,
|
|
164
|
+
value,
|
|
165
|
+
children,
|
|
166
|
+
raw: this.buf.subarray(start, this.pos),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
cls,
|
|
171
|
+
tag,
|
|
172
|
+
constructed,
|
|
173
|
+
indefinite,
|
|
174
|
+
length,
|
|
175
|
+
value,
|
|
176
|
+
raw: this.buf.subarray(start, this.pos),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** 高 tag number:后续字节 base128。 */
|
|
180
|
+
readHighTag() {
|
|
181
|
+
let value = 0;
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (;;) {
|
|
184
|
+
const b = this.byte();
|
|
185
|
+
if (count === 0 && b === 0x80) {
|
|
186
|
+
throw new Asn1Error(`${this.where}: non-minimal high tag number`);
|
|
187
|
+
}
|
|
188
|
+
if (count >= 4) {
|
|
189
|
+
throw new Asn1Error(`${this.where}: tag number too large`);
|
|
190
|
+
}
|
|
191
|
+
value = value * 128 + (b & 0x7f);
|
|
192
|
+
count++;
|
|
193
|
+
if ((b & 0x80) === 0)
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** 长格式长度(`n` 为 1..4)。 */
|
|
198
|
+
readLongLength(n) {
|
|
199
|
+
if (n === 0)
|
|
200
|
+
throw new Asn1Error(`${this.where}: reserved length encoding 0x80`);
|
|
201
|
+
if (n > 4)
|
|
202
|
+
throw new Asn1Error(`${this.where}: length of ${n} bytes is not supported`);
|
|
203
|
+
const lengthStart = this.pos;
|
|
204
|
+
let length = 0;
|
|
205
|
+
for (let i = 0; i < n; i++) {
|
|
206
|
+
length = length * 256 + this.byte();
|
|
207
|
+
}
|
|
208
|
+
if (length < 0 || !Number.isSafeInteger(length)) {
|
|
209
|
+
throw new Asn1Error(`${this.where}: length out of range`);
|
|
210
|
+
}
|
|
211
|
+
if (this.opts.strictLength) {
|
|
212
|
+
if (this.buf[lengthStart] === 0) {
|
|
213
|
+
throw new Asn1Error(`${this.where}: non-minimal long-form length (leading zero)`);
|
|
214
|
+
}
|
|
215
|
+
if (length < 0x80) {
|
|
216
|
+
throw new Asn1Error(`${this.where}: non-minimal long-form length ${length}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return length;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ----------------- 常用值读取 -----------------
|
|
223
|
+
/**
|
|
224
|
+
* 读取 INTEGER(二进制补码,支持负数)。
|
|
225
|
+
*
|
|
226
|
+
* @throws Asn1Error 节点不是 INTEGER。
|
|
227
|
+
*/
|
|
228
|
+
export function readInteger(node, what = 'INTEGER') {
|
|
229
|
+
if (node.cls !== Asn1Class.Universal || node.tag !== UniversalTag.Integer) {
|
|
230
|
+
throw new Asn1Error(`${what}: expected INTEGER`);
|
|
231
|
+
}
|
|
232
|
+
if (node.value.length === 0)
|
|
233
|
+
throw new Asn1Error(`${what}: empty INTEGER`);
|
|
234
|
+
let value = 0n;
|
|
235
|
+
for (let i = 0; i < node.value.length; i++) {
|
|
236
|
+
value = (value << 8n) | BigInt(node.value[i]);
|
|
237
|
+
}
|
|
238
|
+
if ((node.value[0] & 0x80) !== 0) {
|
|
239
|
+
value -= 1n << BigInt(node.value.length * 8);
|
|
240
|
+
}
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
/** 读取 BOOLEAN(DER 中 TRUE 必须为 0xFF)。 */
|
|
244
|
+
export function readBoolean(node, what = 'BOOLEAN') {
|
|
245
|
+
if (node.cls !== Asn1Class.Universal || node.tag !== UniversalTag.Boolean) {
|
|
246
|
+
throw new Asn1Error(`${what}: expected BOOLEAN`);
|
|
247
|
+
}
|
|
248
|
+
if (node.value.length !== 1)
|
|
249
|
+
throw new Asn1Error(`${what}: BOOLEAN must be 1 byte`);
|
|
250
|
+
return node.value[0] !== 0;
|
|
251
|
+
}
|
|
252
|
+
/** 读取 BIT STRING。 */
|
|
253
|
+
export function readBitString(node, what = 'BIT STRING') {
|
|
254
|
+
if (node.cls !== Asn1Class.Universal || node.tag !== UniversalTag.BitString) {
|
|
255
|
+
throw new Asn1Error(`${what}: expected BIT STRING`);
|
|
256
|
+
}
|
|
257
|
+
if (node.value.length === 0)
|
|
258
|
+
throw new Asn1Error(`${what}: empty BIT STRING`);
|
|
259
|
+
const unusedBits = node.value[0];
|
|
260
|
+
if (unusedBits > 7)
|
|
261
|
+
throw new Asn1Error(`${what}: invalid unused-bit count ${unusedBits}`);
|
|
262
|
+
return { bytes: node.value.subarray(1), unusedBits };
|
|
263
|
+
}
|
|
264
|
+
/** 读取 OCTET STRING。 */
|
|
265
|
+
export function readOctetString(node, what = 'OCTET STRING') {
|
|
266
|
+
if (node.cls !== Asn1Class.Universal || node.tag !== UniversalTag.OctetString) {
|
|
267
|
+
throw new Asn1Error(`${what}: expected OCTET STRING`);
|
|
268
|
+
}
|
|
269
|
+
return node.value;
|
|
270
|
+
}
|
|
271
|
+
/** 读取 OBJECT IDENTIFIER。 */
|
|
272
|
+
export function readOid(node, what = 'OBJECT IDENTIFIER') {
|
|
273
|
+
if (node.cls !== Asn1Class.Universal || node.tag !== UniversalTag.ObjectIdentifier) {
|
|
274
|
+
throw new Asn1Error(`${what}: expected OBJECT IDENTIFIER`);
|
|
275
|
+
}
|
|
276
|
+
return decodeOid(node.value);
|
|
277
|
+
}
|
|
278
|
+
/** 读取 UTCTime / GeneralizedTime。 */
|
|
279
|
+
export function readTime(node, what = 'Time') {
|
|
280
|
+
if (node.cls !== Asn1Class.Universal) {
|
|
281
|
+
throw new Asn1Error(`${what}: expected UTCTime or GeneralizedTime`);
|
|
282
|
+
}
|
|
283
|
+
if (node.tag !== UniversalTag.UtcTime && node.tag !== UniversalTag.GeneralizedTime) {
|
|
284
|
+
throw new Asn1Error(`${what}: expected UTCTime or GeneralizedTime`);
|
|
285
|
+
}
|
|
286
|
+
const text = latin1(node.value);
|
|
287
|
+
return parseTime(text);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 读取 ASN.1 字符串。
|
|
291
|
+
*
|
|
292
|
+
* 支持 UTF8String / PrintableString / IA5String / NumericString /
|
|
293
|
+
* TeletexString / BMPString / UniversalString;其他类型抛 `Asn1Error`。
|
|
294
|
+
*/
|
|
295
|
+
export function readString(node, what = 'string') {
|
|
296
|
+
if (node.cls !== Asn1Class.Universal) {
|
|
297
|
+
throw new Asn1Error(`${what}: expected a UNIVERSAL string type`);
|
|
298
|
+
}
|
|
299
|
+
switch (node.tag) {
|
|
300
|
+
case UniversalTag.Utf8String:
|
|
301
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(node.value);
|
|
302
|
+
case UniversalTag.PrintableString:
|
|
303
|
+
case UniversalTag.Ia5String:
|
|
304
|
+
case UniversalTag.NumericString:
|
|
305
|
+
case UniversalTag.TeletexString:
|
|
306
|
+
return latin1(node.value);
|
|
307
|
+
case UniversalTag.BmpString: {
|
|
308
|
+
if (node.value.length % 2 !== 0)
|
|
309
|
+
throw new Asn1Error(`${what}: odd-length BMPString`);
|
|
310
|
+
let out = '';
|
|
311
|
+
for (let i = 0; i < node.value.length; i += 2) {
|
|
312
|
+
out += String.fromCharCode((node.value[i] << 8) | node.value[i + 1]);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
case UniversalTag.UniversalString: {
|
|
317
|
+
if (node.value.length % 4 !== 0)
|
|
318
|
+
throw new Asn1Error(`${what}: bad UniversalString`);
|
|
319
|
+
let out = '';
|
|
320
|
+
for (let i = 0; i < node.value.length; i += 4) {
|
|
321
|
+
const cp = node.value[i] * 0x1000000 +
|
|
322
|
+
(node.value[i + 1] << 16) +
|
|
323
|
+
(node.value[i + 2] << 8) +
|
|
324
|
+
node.value[i + 3];
|
|
325
|
+
out += String.fromCodePoint(cp);
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
default:
|
|
330
|
+
throw new Asn1Error(`${what}: unsupported string tag ${node.tag}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function latin1(bytes) {
|
|
334
|
+
let out = '';
|
|
335
|
+
for (let i = 0; i < bytes.length; i++)
|
|
336
|
+
out += String.fromCharCode(bytes[i]);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASN.1 DER 编码器与构造器。
|
|
3
|
+
*
|
|
4
|
+
* 规则:
|
|
5
|
+
* - `encode()` 按 `cls + constructed + tag` 重建首字节(修复旧实现丢掉
|
|
6
|
+
* constructed 位的问题),对 constructed 节点递归编码子节点;
|
|
7
|
+
* - SET OF 按编码字节排序(DER 要求);
|
|
8
|
+
* - BOOLEAN TRUE 编码为 0xFF;
|
|
9
|
+
* - INTEGER 使用最小补码编码;
|
|
10
|
+
* - 输出始终是 definite-length(BER indefinite 只在解析侧接受)。
|
|
11
|
+
*/
|
|
12
|
+
import { Asn1Class, type Asn1Node } from './types.js';
|
|
13
|
+
/** 编码单个节点为 DER 字节。 */
|
|
14
|
+
export declare function encode(node: Asn1Node): Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* `encode` 的旧名(兼容 `src/pem/asn1.ts` 的历史导出)。
|
|
17
|
+
*
|
|
18
|
+
* @deprecated 使用 `encode`。
|
|
19
|
+
*/
|
|
20
|
+
export declare const encodeDer: typeof encode;
|
|
21
|
+
/** 编码 tag 首字节(含高 tag number 的 base128 展开)。 */
|
|
22
|
+
export declare function encodeTag(cls: Asn1Class, constructed: boolean, tag: number): Uint8Array;
|
|
23
|
+
/** 编码 definite-length。 */
|
|
24
|
+
export declare function encodeLength(length: number): Uint8Array;
|
|
25
|
+
/** 构造 SEQUENCE。 */
|
|
26
|
+
export declare function seq(children: readonly Asn1Node[]): Asn1Node;
|
|
27
|
+
/** 构造 SET(保持给定顺序;ASN.1 SET 的排序语义由调用方负责)。 */
|
|
28
|
+
export declare function set(children: readonly Asn1Node[]): Asn1Node;
|
|
29
|
+
/** 构造 SET OF(按 DER 编码字节排序)。 */
|
|
30
|
+
export declare function setOf(children: readonly Asn1Node[]): Asn1Node;
|
|
31
|
+
/** 构造 `[tag] EXPLICIT` 包装。 */
|
|
32
|
+
export declare function explicit(tag: number, inner: Asn1Node): Asn1Node;
|
|
33
|
+
/**
|
|
34
|
+
* 构造 `[tag] IMPLICIT` 包装:只替换外层标签,内容保持不变。
|
|
35
|
+
*/
|
|
36
|
+
export declare function implicit(tag: number, inner: Asn1Node, cls?: Asn1Class): Asn1Node;
|
|
37
|
+
/** 构造 BIT STRING。`unusedBits` 为末尾未使用位数(0..7)。 */
|
|
38
|
+
export declare function bitString(bytes: Uint8Array, unusedBits?: number): Asn1Node;
|
|
39
|
+
/** 构造 OCTET STRING。 */
|
|
40
|
+
export declare function octetString(bytes: Uint8Array): Asn1Node;
|
|
41
|
+
/** 构造 BOOLEAN。 */
|
|
42
|
+
export declare function boolean(value: boolean): Asn1Node;
|
|
43
|
+
/** 构造 NULL。 */
|
|
44
|
+
export declare function nullValue(): Asn1Node;
|
|
45
|
+
/** 构造 INTEGER(最小补码编码)。 */
|
|
46
|
+
export declare function integer(value: bigint | number): Asn1Node;
|
|
47
|
+
/** 构造 OBJECT IDENTIFIER。 */
|
|
48
|
+
export declare function oid(value: string): Asn1Node;
|
|
49
|
+
/** 构造 UTF8String。 */
|
|
50
|
+
export declare function utf8String(value: string): Asn1Node;
|
|
51
|
+
/** 构造 PrintableString(仅允许 A-Z a-z 0-9 与 `'()+,-./:=?` 和空格)。 */
|
|
52
|
+
export declare function printableString(value: string): Asn1Node;
|
|
53
|
+
/** 构造 IA5String(仅 ASCII)。 */
|
|
54
|
+
export declare function ia5String(value: string): Asn1Node;
|
|
55
|
+
/** 构造 UTCTime(1950–2049)。 */
|
|
56
|
+
export declare function utcTime(date: Date): Asn1Node;
|
|
57
|
+
/** 构造 GeneralizedTime。 */
|
|
58
|
+
export declare function generalizedTime(date: Date): Asn1Node;
|
|
59
|
+
/** 按 RFC 5280 自动选择 UTCTime / GeneralizedTime。 */
|
|
60
|
+
export declare function time(date: Date): Asn1Node;
|
|
61
|
+
/**
|
|
62
|
+
* INTEGER 最小补码编码。
|
|
63
|
+
*
|
|
64
|
+
* - 0 → `00`;
|
|
65
|
+
* - 正数:最高位为 1 时前置 `00`;
|
|
66
|
+
* - 负数:选择最少字节数 n 使 `-2^(8n-1) <= v`,按 2^(8n) 取模。
|
|
67
|
+
*/
|
|
68
|
+
export declare function encodeIntegerValue(v: bigint): Uint8Array;
|