sm-crypto-wasm 0.3.1 → 0.7.1
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 +62 -834
- package/dist/asn1/decode.d.ts +165 -0
- package/dist/asn1/decode.js +457 -0
- package/dist/asn1/encode.d.ts +169 -0
- package/dist/asn1/encode.js +414 -0
- package/dist/asn1/index.d.ts +17 -0
- package/dist/asn1/index.js +17 -0
- package/dist/asn1/oid.d.ts +40 -0
- package/dist/asn1/oid.js +136 -0
- package/dist/asn1/time.d.ts +71 -0
- package/dist/asn1/time.js +165 -0
- package/dist/asn1/types.d.ts +160 -0
- package/dist/asn1/types.js +112 -0
- package/dist/cms/errors.d.ts +23 -0
- package/dist/cms/errors.js +17 -0
- package/dist/cms/index.d.ts +18 -0
- package/dist/cms/index.js +15 -0
- package/dist/cms/oids.d.ts +47 -0
- package/dist/cms/oids.js +47 -0
- package/dist/cms/parse.d.ts +217 -0
- package/dist/cms/parse.js +420 -0
- package/dist/cms/sign.d.ts +113 -0
- package/dist/cms/sign.js +194 -0
- package/dist/cms/verify.d.ts +117 -0
- package/dist/cms/verify.js +222 -0
- package/dist/common/hybrid.d.ts +25 -0
- package/dist/common/hybrid.js +25 -0
- package/dist/common/sealed.d.ts +119 -15
- package/dist/common/sealed.js +67 -9
- package/dist/esbuild-plugin.d.ts +74 -5
- package/dist/esbuild-plugin.js +69 -4
- package/dist/index.d.ts +22 -0
- package/dist/index.js +25 -0
- package/dist/pem/asn1.d.ts +19 -65
- package/dist/pem/asn1.js +19 -288
- package/dist/pem/index.d.ts +2 -0
- package/dist/pem/index.js +2 -0
- package/dist/pem/pkcs8.d.ts +64 -8
- package/dist/pem/pkcs8.js +75 -7
- package/dist/pem/sm2.d.ts +90 -2
- package/dist/pem/sm2.js +204 -15
- package/dist/pem/sm9.d.ts +42 -2
- package/dist/pem/sm9.js +42 -2
- package/dist/runtime/base64.core.d.ts +25 -1
- package/dist/runtime/base64.core.js +31 -1
- package/dist/runtime/convert.d.ts +70 -7
- package/dist/runtime/convert.js +129 -9
- package/dist/runtime/errors.d.ts +64 -8
- package/dist/runtime/errors.js +64 -8
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/loadWasm.browser.d.ts +33 -0
- package/dist/runtime/loadWasm.browser.js +46 -0
- package/dist/runtime/loadWasm.core.d.ts +59 -3
- package/dist/runtime/loadWasm.core.js +53 -3
- package/dist/runtime/loadWasm.d.ts +44 -0
- package/dist/runtime/loadWasm.js +79 -4
- package/dist/runtime/random.browser.d.ts +24 -0
- package/dist/runtime/random.browser.js +31 -1
- package/dist/runtime/random.core.d.ts +21 -0
- package/dist/runtime/random.core.js +27 -0
- package/dist/runtime/random.d.ts +28 -0
- package/dist/runtime/random.js +40 -1
- package/dist/sm2/exchange.d.ts +132 -16
- package/dist/sm2/exchange.js +120 -9
- package/dist/sm2/hybrid.d.ts +65 -3
- package/dist/sm2/hybrid.js +53 -1
- package/dist/sm2/index.d.ts +324 -16
- package/dist/sm2/index.js +530 -19
- package/dist/sm2/kem.d.ts +62 -5
- package/dist/sm2/kem.js +37 -2
- package/dist/sm2/keyInput.d.ts +36 -3
- package/dist/sm2/keyInput.js +36 -3
- package/dist/sm2/sm2.wasm +0 -0
- package/dist/sm3/index.d.ts +30 -0
- package/dist/sm3/index.js +55 -1
- package/dist/sm4/index.d.ts +125 -17
- package/dist/sm4/index.js +201 -14
- package/dist/sm9/core.d.ts +58 -8
- package/dist/sm9/core.js +36 -5
- package/dist/sm9/hybrid.d.ts +66 -3
- package/dist/sm9/hybrid.js +46 -1
- package/dist/sm9/index.d.ts +265 -19
- package/dist/sm9/index.js +175 -5
- package/dist/sm9/input.d.ts +37 -2
- package/dist/sm9/input.js +50 -1
- package/dist/sm9/kem.d.ts +57 -4
- package/dist/sm9/kem.js +47 -1
- package/dist/webcrypto/algorithms.d.ts +88 -0
- package/dist/webcrypto/algorithms.js +168 -0
- package/dist/webcrypto/cryptoKey.d.ts +130 -0
- package/dist/webcrypto/cryptoKey.js +167 -0
- package/dist/webcrypto/errors.d.ts +43 -0
- package/dist/webcrypto/errors.js +69 -0
- package/dist/webcrypto/index.d.ts +24 -0
- package/dist/webcrypto/index.js +21 -0
- package/dist/webcrypto/subtle.d.ts +118 -0
- package/dist/webcrypto/subtle.js +426 -0
- package/dist/x509/encode.d.ts +129 -0
- package/dist/x509/encode.js +248 -0
- package/dist/x509/errors.d.ts +29 -0
- package/dist/x509/errors.js +29 -0
- package/dist/x509/extensions.d.ts +66 -0
- package/dist/x509/extensions.js +273 -0
- package/dist/x509/index.d.ts +21 -0
- package/dist/x509/index.js +30 -0
- package/dist/x509/ip.d.ts +21 -0
- package/dist/x509/ip.js +122 -0
- package/dist/x509/issue.d.ts +87 -0
- package/dist/x509/issue.js +521 -0
- package/dist/x509/oids.d.ts +74 -0
- package/dist/x509/oids.js +103 -0
- package/dist/x509/parse.d.ts +45 -0
- package/dist/x509/parse.js +550 -0
- package/dist/x509/sha1.d.ts +20 -0
- package/dist/x509/sha1.js +89 -0
- package/dist/x509/sha256.d.ts +20 -0
- package/dist/x509/sha256.js +114 -0
- package/dist/x509/sig.d.ts +35 -0
- package/dist/x509/sig.js +104 -0
- package/dist/x509/types.d.ts +490 -0
- package/dist/x509/types.js +13 -0
- package/dist/x509/verify.d.ts +91 -0
- package/dist/x509/verify.js +105 -0
- package/dist/zuc/index.d.ts +128 -10
- package/dist/zuc/index.js +136 -7
- package/package.json +57 -28
package/dist/cms/sign.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS SignedData 签名(detached / attached)。
|
|
3
|
+
*
|
|
4
|
+
* 签名的计算对象是 `signedAttrs` 的 DER 编码(SET tag 0x31)——与验签层
|
|
5
|
+
* 使用完全相同的规则;`contentType` / `messageDigest` / `signingTime` 三个
|
|
6
|
+
* 属性按 DER SET OF 排序后编码。
|
|
7
|
+
*
|
|
8
|
+
* 只做 SignedData,不做 EnvelopedData / RFC 3161 时间戳。
|
|
9
|
+
*
|
|
10
|
+
* CMS SignedData signing (detached / attached).
|
|
11
|
+
*
|
|
12
|
+
* The signed object is the DER encoding of `signedAttrs` (SET tag 0x31) — exactly the
|
|
13
|
+
* same rule the verification layer uses; the `contentType` / `messageDigest` /
|
|
14
|
+
* `signingTime` attributes are encoded after DER SET OF sorting.
|
|
15
|
+
*
|
|
16
|
+
* SignedData only: no EnvelopedData, no RFC 3161 timestamping.
|
|
17
|
+
*/
|
|
18
|
+
import { explicit, implicit, integer, octetString, oid, parseDer, seq, setOf, time, } from '../asn1/index.js';
|
|
19
|
+
import { bytesToBase64, bytesToPem, hexToBytes } from '../runtime/convert.js';
|
|
20
|
+
import * as sm2 from '../sm2/index.js';
|
|
21
|
+
import * as sm3 from '../sm3/index.js';
|
|
22
|
+
import { parseCertificate } from '../x509/parse.js';
|
|
23
|
+
import { rawSignatureToDer } from '../x509/sig.js';
|
|
24
|
+
import { CmsError } from './errors.js';
|
|
25
|
+
import { OID_CMS_CONTENT_TYPE, OID_CMS_DATA, OID_CMS_MESSAGE_DIGEST, OID_CMS_SIGNED_DATA, OID_CMS_SIGNING_TIME, OID_SM3, OID_SM3_WITH_SM2, } from './oids.js';
|
|
26
|
+
/**
|
|
27
|
+
* 生成 CMS SignedData(单签名者)。
|
|
28
|
+
*
|
|
29
|
+
* 调用前需要预热 wasm:`await sm2.ensureLoaded(); await sm3.ensureLoaded();`
|
|
30
|
+
*
|
|
31
|
+
* @throws CmsError 缺少签名者证书或输入不合法。
|
|
32
|
+
*
|
|
33
|
+
* Builds a CMS SignedData structure (single signer).
|
|
34
|
+
*
|
|
35
|
+
* The wasm modules must be warmed up beforehand:
|
|
36
|
+
* `await sm2.ensureLoaded(); await sm3.ensureLoaded();`
|
|
37
|
+
*
|
|
38
|
+
* @throws CmsError When the signer certificate is missing or the input is invalid.
|
|
39
|
+
*/
|
|
40
|
+
export function signData(opts) {
|
|
41
|
+
const content = toDataBytes(opts.data);
|
|
42
|
+
const signerCertificate = resolveSignerCertificate(opts);
|
|
43
|
+
const chain = (opts.certificates ?? []).map((item) => resolveCertificate(item));
|
|
44
|
+
const certificates = dedupeCertificates([signerCertificate, ...chain]);
|
|
45
|
+
const digest = hexToBytes(sm3.hash(content), 'messageDigest');
|
|
46
|
+
const signingTime = opts.signingTime === false ? undefined : (opts.signingTime ?? new Date());
|
|
47
|
+
const attributes = [
|
|
48
|
+
attribute(OID_CMS_CONTENT_TYPE, setOf([oid(OID_CMS_DATA)])),
|
|
49
|
+
attribute(OID_CMS_MESSAGE_DIGEST, setOf([octetString(digest)])),
|
|
50
|
+
];
|
|
51
|
+
if (signingTime !== undefined) {
|
|
52
|
+
attributes.push(attribute(OID_CMS_SIGNING_TIME, setOf([time(signingTime)])));
|
|
53
|
+
}
|
|
54
|
+
const signedAttrs = setOf(attributes);
|
|
55
|
+
const signature = hexToBytes(sm2.signWithID(opts.privateKey, signedAttrs.raw, opts.userID ?? new Uint8Array(0)), 'signature');
|
|
56
|
+
const signatureDer = rawSignatureToDer(signature);
|
|
57
|
+
const signerInfo = seq([
|
|
58
|
+
integer(1),
|
|
59
|
+
issuerAndSerialNumber(signerCertificate),
|
|
60
|
+
algorithmIdentifier(OID_SM3),
|
|
61
|
+
implicit(0, signedAttrs),
|
|
62
|
+
algorithmIdentifier(OID_SM3_WITH_SM2),
|
|
63
|
+
octetString(signatureDer),
|
|
64
|
+
]);
|
|
65
|
+
const encapContentInfo = opts.detached === false
|
|
66
|
+
? seq([oid(OID_CMS_DATA), explicit(0, octetString(content))])
|
|
67
|
+
: seq([oid(OID_CMS_DATA)]);
|
|
68
|
+
const signedDataChildren = [
|
|
69
|
+
integer(1),
|
|
70
|
+
setOf([algorithmIdentifier(OID_SM3)]),
|
|
71
|
+
encapContentInfo,
|
|
72
|
+
];
|
|
73
|
+
if (certificates.length > 0) {
|
|
74
|
+
signedDataChildren.push(implicit(0, setOf(certificates.map((cert) => parseDer(cert.raw)))));
|
|
75
|
+
}
|
|
76
|
+
signedDataChildren.push(setOf([signerInfo]));
|
|
77
|
+
const signedData = seq(signedDataChildren);
|
|
78
|
+
const contentInfo = seq([oid(OID_CMS_SIGNED_DATA), explicit(0, signedData)]);
|
|
79
|
+
const der = new Uint8Array(contentInfo.raw);
|
|
80
|
+
return {
|
|
81
|
+
der,
|
|
82
|
+
pem: bytesToPem(der, 'CMS'),
|
|
83
|
+
base64: bytesToBase64(der),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// ---------- 编码辅助 ----------
|
|
87
|
+
//
|
|
88
|
+
// ---------- Encoding helpers ----------
|
|
89
|
+
// 构造 CMS Attribute(OID + SET OF 值)。
|
|
90
|
+
//
|
|
91
|
+
// Builds a CMS Attribute (OID + SET OF values).
|
|
92
|
+
function attribute(attrOid, values) {
|
|
93
|
+
return seq([oid(attrOid), values]);
|
|
94
|
+
}
|
|
95
|
+
// 构造 AlgorithmIdentifier。
|
|
96
|
+
//
|
|
97
|
+
// Builds an AlgorithmIdentifier.
|
|
98
|
+
function algorithmIdentifier(oidValue) {
|
|
99
|
+
return seq([oid(oidValue)]);
|
|
100
|
+
}
|
|
101
|
+
// 构造 IssuerAndSerialNumber 签名者标识。
|
|
102
|
+
//
|
|
103
|
+
// Builds an IssuerAndSerialNumber signer identifier.
|
|
104
|
+
function issuerAndSerialNumber(cert) {
|
|
105
|
+
let issuer;
|
|
106
|
+
try {
|
|
107
|
+
issuer = parseDer(cert.issuer.raw);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
throw new CmsError('signData: failed to re-encode issuer name', { cause: err });
|
|
111
|
+
}
|
|
112
|
+
return seq([issuer, integer(cert.serialNumber)]);
|
|
113
|
+
}
|
|
114
|
+
// ---------- 输入辅助 ----------
|
|
115
|
+
//
|
|
116
|
+
// ---------- Input helpers ----------
|
|
117
|
+
/**
|
|
118
|
+
* 字符串按 UTF-8 处理;CMS 的 data 语义是报文内容,不是 hex。
|
|
119
|
+
*
|
|
120
|
+
* Strings are treated as UTF-8; the CMS data field carries message content, not hex.
|
|
121
|
+
*/
|
|
122
|
+
function toDataBytes(data) {
|
|
123
|
+
if (data instanceof Uint8Array)
|
|
124
|
+
return new Uint8Array(data);
|
|
125
|
+
if (typeof data === 'string')
|
|
126
|
+
return new TextEncoder().encode(data);
|
|
127
|
+
throw new TypeError('signData: data must be a string or Uint8Array');
|
|
128
|
+
}
|
|
129
|
+
// 从 options 解析签名者证书。
|
|
130
|
+
//
|
|
131
|
+
// Resolves the signer certificate from options.
|
|
132
|
+
function resolveSignerCertificate(opts) {
|
|
133
|
+
const input = opts.certificate ?? opts.certificates?.[0];
|
|
134
|
+
if (input === undefined) {
|
|
135
|
+
throw new CmsError('signData: certificate is required to build the SignerInfo');
|
|
136
|
+
}
|
|
137
|
+
return resolveCertificate(input);
|
|
138
|
+
}
|
|
139
|
+
// 把 Certificate / PEM / DER 输入统一为 Certificate。
|
|
140
|
+
//
|
|
141
|
+
// Normalizes Certificate / PEM / DER input.
|
|
142
|
+
function resolveCertificate(input) {
|
|
143
|
+
if (isCertificateLike(input))
|
|
144
|
+
return input;
|
|
145
|
+
try {
|
|
146
|
+
return parseCertificate(input);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
throw new CmsError('signData: invalid certificate', { cause: err });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 判断值是否像 Certificate。
|
|
153
|
+
//
|
|
154
|
+
// Checks whether a value looks like a Certificate.
|
|
155
|
+
function isCertificateLike(value) {
|
|
156
|
+
return (typeof value === 'object' &&
|
|
157
|
+
value !== null &&
|
|
158
|
+
!(value instanceof Uint8Array) &&
|
|
159
|
+
'tbsCertificate' in value &&
|
|
160
|
+
'subjectPublicKey' in value);
|
|
161
|
+
}
|
|
162
|
+
// 按 DER 去重证书。
|
|
163
|
+
//
|
|
164
|
+
// Deduplicates certificates by DER.
|
|
165
|
+
function dedupeCertificates(certificates) {
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const cert of certificates) {
|
|
169
|
+
const key = base64Key(cert.raw);
|
|
170
|
+
if (seen.has(key))
|
|
171
|
+
continue;
|
|
172
|
+
seen.add(key);
|
|
173
|
+
out.push(cert);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
// 把证书 DER 字节映射成单字节字符串,作为去重 key(只用于等值比较,不是 base64)。
|
|
178
|
+
//
|
|
179
|
+
// Maps certificate DER bytes to a one-code-unit-per-byte string used as a dedup key
|
|
180
|
+
// (equality comparison only; this is not a base64 encoding).
|
|
181
|
+
function base64Key(bytes) {
|
|
182
|
+
let key = '';
|
|
183
|
+
for (let i = 0; i < bytes.length; i++)
|
|
184
|
+
key += String.fromCharCode(bytes[i]);
|
|
185
|
+
return key;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 证书集合中的证书原始 DER(供外部检查)。
|
|
189
|
+
*
|
|
190
|
+
* Raw DER of each certificate in the set, for external inspection.
|
|
191
|
+
*/
|
|
192
|
+
export function certificateChainDer(certificates) {
|
|
193
|
+
return certificates.map((cert) => new Uint8Array(cert.raw));
|
|
194
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS SignedData 验签。
|
|
3
|
+
*
|
|
4
|
+
* 只验证签名与 messageDigest,**不建立信任链**、不检查证书有效期/用途;
|
|
5
|
+
* 需要信任判断时由调用方对 `signers[i].certificate` 另行处理。
|
|
6
|
+
*
|
|
7
|
+
* CMS SignedData verification.
|
|
8
|
+
*
|
|
9
|
+
* Only the signature and messageDigest are checked; **no trust chain is built** and
|
|
10
|
+
* certificate validity/key usage are not examined. When a trust decision is needed, the
|
|
11
|
+
* caller handles `signers[i].certificate` separately.
|
|
12
|
+
*/
|
|
13
|
+
import type { Certificate } from '../x509/types.js';
|
|
14
|
+
import { type CmsVerifyErrorCode } from './errors.js';
|
|
15
|
+
import { type CmsSignerInfo } from './parse.js';
|
|
16
|
+
/**
|
|
17
|
+
* `verifySignedData` 的选项。
|
|
18
|
+
*
|
|
19
|
+
* Options of `verifySignedData`.
|
|
20
|
+
*/
|
|
21
|
+
export interface VerifySignedDataOptions {
|
|
22
|
+
/**
|
|
23
|
+
* detached 模式下的原始内容(字符串按 UTF-8 编码)。
|
|
24
|
+
*
|
|
25
|
+
* The original content in detached mode (strings are treated as UTF-8).
|
|
26
|
+
*/
|
|
27
|
+
readonly detachedData?: string | Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* 额外候选证书(CMS 未内嵌签名者证书时使用)。
|
|
30
|
+
*
|
|
31
|
+
* Extra candidate certificates, used when the CMS does not embed the signer
|
|
32
|
+
* certificate.
|
|
33
|
+
*/
|
|
34
|
+
readonly certificates?: readonly (Certificate | string | Uint8Array)[];
|
|
35
|
+
/**
|
|
36
|
+
* 显式指定 SM2 用户标识;省略时依次尝试空标识与 GM/T 默认标识。
|
|
37
|
+
*
|
|
38
|
+
* An explicit SM2 user identifier; when omitted, the empty identifier and then the
|
|
39
|
+
* GM/T default identifier are tried in turn.
|
|
40
|
+
*/
|
|
41
|
+
readonly userID?: Uint8Array;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 单个签名者的验签结果。
|
|
45
|
+
*
|
|
46
|
+
* The verification result of a single signer.
|
|
47
|
+
*/
|
|
48
|
+
export interface VerifySignerResult {
|
|
49
|
+
/**
|
|
50
|
+
* 是否通过校验(签名 + messageDigest 均通过)。
|
|
51
|
+
*
|
|
52
|
+
* Whether both the signature and the messageDigest checks passed.
|
|
53
|
+
*/
|
|
54
|
+
readonly valid: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 匹配到的签名者证书。
|
|
57
|
+
*
|
|
58
|
+
* The matched signer certificate.
|
|
59
|
+
*/
|
|
60
|
+
readonly certificate?: Certificate;
|
|
61
|
+
/**
|
|
62
|
+
* signedAttrs 中的 signingTime(该属性缺失时为 `undefined`)。
|
|
63
|
+
*
|
|
64
|
+
* The signingTime from signedAttrs (`undefined` when the attribute is absent).
|
|
65
|
+
*/
|
|
66
|
+
readonly signingTime?: Date;
|
|
67
|
+
/**
|
|
68
|
+
* 人类可读的补充说明。
|
|
69
|
+
*
|
|
70
|
+
* A human-readable explanation.
|
|
71
|
+
*/
|
|
72
|
+
readonly error?: string;
|
|
73
|
+
/**
|
|
74
|
+
* 结构化失败原因(`valid === true` 时不存在)。
|
|
75
|
+
*
|
|
76
|
+
* The structured failure reason (absent when `valid === true`).
|
|
77
|
+
*/
|
|
78
|
+
readonly errorCode?: CmsVerifyErrorCode;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `verifySignedData` 的结果。
|
|
82
|
+
*
|
|
83
|
+
* Result of `verifySignedData`.
|
|
84
|
+
*/
|
|
85
|
+
export interface VerifySignedDataResult {
|
|
86
|
+
/**
|
|
87
|
+
* 至少有一个签名者且所有签名者都通过校验。
|
|
88
|
+
*
|
|
89
|
+
* True when there is at least one signer and every signer is valid.
|
|
90
|
+
*/
|
|
91
|
+
readonly valid: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* 每个签名者的结果,顺序与 `signerInfos` 一致。
|
|
94
|
+
*
|
|
95
|
+
* Per-signer results, in `signerInfos` order.
|
|
96
|
+
*/
|
|
97
|
+
readonly signers: readonly VerifySignerResult[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 验证 CMS SignedData。
|
|
101
|
+
*
|
|
102
|
+
* 结构完全无法解析时返回 `{ valid: false, signers: [{ errorCode: 'MALFORMED' }] }`,
|
|
103
|
+
* 而不是抛错;wasm 未预热等环境错误仍会抛出。
|
|
104
|
+
*
|
|
105
|
+
* Verifies a CMS SignedData.
|
|
106
|
+
*
|
|
107
|
+
* When the structure cannot be parsed at all it returns
|
|
108
|
+
* `{ valid: false, signers: [{ errorCode: 'MALFORMED' }] }` instead of throwing;
|
|
109
|
+
* environment errors such as an unloaded wasm module are still thrown.
|
|
110
|
+
*/
|
|
111
|
+
export declare function verifySignedData(cms: string | Uint8Array, opts?: VerifySignedDataOptions): VerifySignedDataResult;
|
|
112
|
+
/**
|
|
113
|
+
* 供调试:签名者标识的十六进制表示。
|
|
114
|
+
*
|
|
115
|
+
* For debugging: the signer identifier as hex.
|
|
116
|
+
*/
|
|
117
|
+
export declare function signerIdentifierHex(signer: CmsSignerInfo): string;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS SignedData 验签。
|
|
3
|
+
*
|
|
4
|
+
* 只验证签名与 messageDigest,**不建立信任链**、不检查证书有效期/用途;
|
|
5
|
+
* 需要信任判断时由调用方对 `signers[i].certificate` 另行处理。
|
|
6
|
+
*
|
|
7
|
+
* CMS SignedData verification.
|
|
8
|
+
*
|
|
9
|
+
* Only the signature and messageDigest are checked; **no trust chain is built** and
|
|
10
|
+
* certificate validity/key usage are not examined. When a trust decision is needed, the
|
|
11
|
+
* caller handles `signers[i].certificate` separately.
|
|
12
|
+
*/
|
|
13
|
+
import { bytesToHex, hexToBytes } from '../runtime/convert.js';
|
|
14
|
+
import * as sm2 from '../sm2/index.js';
|
|
15
|
+
import * as sm3 from '../sm3/index.js';
|
|
16
|
+
import { parseCertificate } from '../x509/parse.js';
|
|
17
|
+
import { derSignatureToRaw } from '../x509/sig.js';
|
|
18
|
+
import { DEFAULT_SM2_USER_ID } from '../x509/verify.js';
|
|
19
|
+
import { CmsError } from './errors.js';
|
|
20
|
+
import { OID_CMS_CONTENT_TYPE, OID_CMS_MESSAGE_DIGEST, OID_CMS_SIGNING_TIME, OID_SM3, OID_SM3_WITH_SM2, } from './oids.js';
|
|
21
|
+
import { attributeOctets, attributeOid, attributeTime, parseSignedData, } from './parse.js';
|
|
22
|
+
/**
|
|
23
|
+
* SM2 空用户标识(OpenSSL CMS 默认)。
|
|
24
|
+
*
|
|
25
|
+
* The empty SM2 user identifier (the OpenSSL CMS default).
|
|
26
|
+
*/
|
|
27
|
+
const EMPTY_USER_ID = new Uint8Array(0);
|
|
28
|
+
/**
|
|
29
|
+
* 验证 CMS SignedData。
|
|
30
|
+
*
|
|
31
|
+
* 结构完全无法解析时返回 `{ valid: false, signers: [{ errorCode: 'MALFORMED' }] }`,
|
|
32
|
+
* 而不是抛错;wasm 未预热等环境错误仍会抛出。
|
|
33
|
+
*
|
|
34
|
+
* Verifies a CMS SignedData.
|
|
35
|
+
*
|
|
36
|
+
* When the structure cannot be parsed at all it returns
|
|
37
|
+
* `{ valid: false, signers: [{ errorCode: 'MALFORMED' }] }` instead of throwing;
|
|
38
|
+
* environment errors such as an unloaded wasm module are still thrown.
|
|
39
|
+
*/
|
|
40
|
+
export function verifySignedData(cms, opts = {}) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = parseSignedData(cms);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
signers: [
|
|
49
|
+
{
|
|
50
|
+
valid: false,
|
|
51
|
+
errorCode: 'MALFORMED',
|
|
52
|
+
error: err instanceof Error ? err.message : String(err),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const certificates = [
|
|
58
|
+
...parsed.certificates,
|
|
59
|
+
...(opts.certificates ?? []).map((cert) => resolveCertificate(cert)),
|
|
60
|
+
];
|
|
61
|
+
const detachedData = opts.detachedData === undefined ? undefined : toDataBytes(opts.detachedData);
|
|
62
|
+
const signers = parsed.signerInfos.map((signer) => verifySigner(parsed, signer, certificates, detachedData, opts.userID));
|
|
63
|
+
return {
|
|
64
|
+
valid: signers.length > 0 && signers.every((signer) => signer.valid),
|
|
65
|
+
signers,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 验证单个 SignerInfo。
|
|
69
|
+
//
|
|
70
|
+
// Verifies a single SignerInfo.
|
|
71
|
+
function verifySigner(parsed, signer, certificates, detachedData, userID) {
|
|
72
|
+
if (signer.digestAlgorithm !== OID_SM3) {
|
|
73
|
+
return fail('UNSUPPORTED_ALGORITHM', `unsupported digest algorithm ${signer.digestAlgorithm}`);
|
|
74
|
+
}
|
|
75
|
+
if (signer.signatureAlgorithm !== OID_SM3_WITH_SM2) {
|
|
76
|
+
return fail('UNSUPPORTED_ALGORITHM', `unsupported signature algorithm ${signer.signatureAlgorithm}`);
|
|
77
|
+
}
|
|
78
|
+
const certificate = findCertificate(signer, certificates);
|
|
79
|
+
if (certificate === undefined) {
|
|
80
|
+
return fail('NO_CERTIFICATE', 'signer certificate is not embedded and was not provided');
|
|
81
|
+
}
|
|
82
|
+
if (!certificate.supported) {
|
|
83
|
+
return fail('UNSUPPORTED_ALGORITHM', 'signer certificate public key is not SM2');
|
|
84
|
+
}
|
|
85
|
+
const content = parsed.eContent ?? detachedData;
|
|
86
|
+
let signingInput;
|
|
87
|
+
let signingTime;
|
|
88
|
+
if (signer.signedAttrs !== undefined) {
|
|
89
|
+
if (content === undefined) {
|
|
90
|
+
return fail('MALFORMED', 'detached data is required but was not supplied');
|
|
91
|
+
}
|
|
92
|
+
const attributes = signer.signedAttrs.attributes;
|
|
93
|
+
const contentType = attributeOid(attributeByOid(attributes, OID_CMS_CONTENT_TYPE));
|
|
94
|
+
if (contentType !== parsed.eContentType) {
|
|
95
|
+
return fail('DIGEST_MISMATCH', `contentType attribute ${String(contentType)} does not match ${parsed.eContentType}`);
|
|
96
|
+
}
|
|
97
|
+
const expectedDigest = attributeOctets(attributeByOid(attributes, OID_CMS_MESSAGE_DIGEST));
|
|
98
|
+
if (expectedDigest === undefined) {
|
|
99
|
+
return fail('MALFORMED', 'missing messageDigest attribute');
|
|
100
|
+
}
|
|
101
|
+
const actualDigest = hexToBytes(sm3.hash(content), 'messageDigest');
|
|
102
|
+
if (!equalBytes(expectedDigest, actualDigest)) {
|
|
103
|
+
return fail('DIGEST_MISMATCH', 'messageDigest does not match the content');
|
|
104
|
+
}
|
|
105
|
+
signingInput = signer.signedAttrs.raw;
|
|
106
|
+
signingTime = attributeTime(attributeByOid(attributes, OID_CMS_SIGNING_TIME));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (content === undefined) {
|
|
110
|
+
return fail('MALFORMED', 'detached data is required but was not supplied');
|
|
111
|
+
}
|
|
112
|
+
signingInput = content;
|
|
113
|
+
}
|
|
114
|
+
let signature;
|
|
115
|
+
try {
|
|
116
|
+
signature = derSignatureToRaw(signer.signature);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return fail('SIGNATURE_INVALID', 'malformed DER signature');
|
|
120
|
+
}
|
|
121
|
+
const valid = verifyWithUserId(certificate.subjectPublicKey, signingInput, signature, userID);
|
|
122
|
+
if (!valid)
|
|
123
|
+
return fail('SIGNATURE_INVALID', 'SM2 signature verification failed');
|
|
124
|
+
return {
|
|
125
|
+
valid: true,
|
|
126
|
+
certificate,
|
|
127
|
+
...(signingTime === undefined ? {} : { signingTime }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// 使用指定 SM2 用户 ID 验签。
|
|
131
|
+
//
|
|
132
|
+
// Verifies with an explicit SM2 user ID.
|
|
133
|
+
function verifyWithUserId(publicKey, message, signature, userID) {
|
|
134
|
+
if (userID !== undefined) {
|
|
135
|
+
return sm2.verifyWithID(publicKey, message, signature, userID);
|
|
136
|
+
}
|
|
137
|
+
// 先试 OpenSSL CMS 的空标识,再试 GM/T 默认标识。
|
|
138
|
+
//
|
|
139
|
+
// Try the empty OpenSSL-CMS identifier first, then the GM/T default identifier.
|
|
140
|
+
if (sm2.verifyWithID(publicKey, message, signature, EMPTY_USER_ID))
|
|
141
|
+
return true;
|
|
142
|
+
return sm2.verifyWithID(publicKey, message, signature, DEFAULT_SM2_USER_ID);
|
|
143
|
+
}
|
|
144
|
+
// 按签名者标识查找证书。
|
|
145
|
+
//
|
|
146
|
+
// Finds the signer certificate by identifier.
|
|
147
|
+
function findCertificate(signer, certificates) {
|
|
148
|
+
const sid = signer.sid;
|
|
149
|
+
if (sid.type === 'issuerAndSerialNumber') {
|
|
150
|
+
return certificates.find((cert) => cert.serialNumber === sid.serialNumber && equalBytes(cert.issuer.raw, sid.issuerRaw));
|
|
151
|
+
}
|
|
152
|
+
return certificates.find((cert) => cert.subjectKeyIdentifier !== undefined &&
|
|
153
|
+
equalBytes(cert.subjectKeyIdentifier, sid.keyIdentifier));
|
|
154
|
+
}
|
|
155
|
+
// 按 OID 查找签名属性。
|
|
156
|
+
//
|
|
157
|
+
// Finds a signed attribute by OID.
|
|
158
|
+
function attributeByOid(attributes, target) {
|
|
159
|
+
return attributes.find((attribute) => attribute.oid === target);
|
|
160
|
+
}
|
|
161
|
+
// 把 Certificate / PEM / DER 输入统一为 Certificate。
|
|
162
|
+
//
|
|
163
|
+
// Normalizes Certificate / PEM / DER input.
|
|
164
|
+
function resolveCertificate(input) {
|
|
165
|
+
if (isCertificateLike(input))
|
|
166
|
+
return input;
|
|
167
|
+
try {
|
|
168
|
+
return parseCertificate(input);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
throw new CmsError('verifySignedData: invalid certificate', { cause: err });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 判断值是否像 Certificate。
|
|
175
|
+
//
|
|
176
|
+
// Checks whether a value looks like a Certificate.
|
|
177
|
+
function isCertificateLike(value) {
|
|
178
|
+
return (typeof value === 'object' &&
|
|
179
|
+
value !== null &&
|
|
180
|
+
!(value instanceof Uint8Array) &&
|
|
181
|
+
'tbsCertificate' in value &&
|
|
182
|
+
'subjectPublicKey' in value);
|
|
183
|
+
}
|
|
184
|
+
// 把 data 输入统一为字节。
|
|
185
|
+
//
|
|
186
|
+
// Normalizes data input to bytes.
|
|
187
|
+
function toDataBytes(data) {
|
|
188
|
+
if (data instanceof Uint8Array)
|
|
189
|
+
return new Uint8Array(data);
|
|
190
|
+
if (typeof data === 'string')
|
|
191
|
+
return new TextEncoder().encode(data);
|
|
192
|
+
throw new TypeError('verifySignedData: detachedData must be a string or Uint8Array');
|
|
193
|
+
}
|
|
194
|
+
// 常量时间比较两个字节数组。
|
|
195
|
+
//
|
|
196
|
+
// Compares two byte arrays in constant time.
|
|
197
|
+
function equalBytes(a, b) {
|
|
198
|
+
if (a.length !== b.length)
|
|
199
|
+
return false;
|
|
200
|
+
let diff = 0;
|
|
201
|
+
for (let i = 0; i < a.length; i++) {
|
|
202
|
+
diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
203
|
+
}
|
|
204
|
+
return diff === 0;
|
|
205
|
+
}
|
|
206
|
+
// 构造失败结果。
|
|
207
|
+
//
|
|
208
|
+
// Builds a failure result.
|
|
209
|
+
function fail(code, message) {
|
|
210
|
+
return { valid: false, errorCode: code, error: message };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 供调试:签名者标识的十六进制表示。
|
|
214
|
+
*
|
|
215
|
+
* For debugging: the signer identifier as hex.
|
|
216
|
+
*/
|
|
217
|
+
export function signerIdentifierHex(signer) {
|
|
218
|
+
if (signer.sid.type === 'subjectKeyIdentifier') {
|
|
219
|
+
return bytesToHex(signer.sid.keyIdentifier);
|
|
220
|
+
}
|
|
221
|
+
return `${bytesToHex(signer.sid.issuerRaw)}#${signer.sid.serialNumber.toString(16)}`;
|
|
222
|
+
}
|
package/dist/common/hybrid.d.ts
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
* 避免“空输入静默成功”。
|
|
9
9
|
*
|
|
10
10
|
* IV 同样按 hex / Uint8Array 处理,未提供时由宿主 CSPRNG 生成 12 字节。
|
|
11
|
+
*
|
|
12
|
+
* Shared input conventions for hybrid encryption.
|
|
13
|
+
*
|
|
14
|
+
* The two hybrid modules (SM2 / SM9) apply **the same** string semantics to `message`:
|
|
15
|
+
* - `Uint8Array`: used as is (copied);
|
|
16
|
+
* - `string`: must be hex (a `0x` prefix is allowed); base64 / UTF-8 are no longer
|
|
17
|
+
* guessed;
|
|
18
|
+
* - pass `new Uint8Array(0)` for an empty message — an empty string is rejected
|
|
19
|
+
* outright by the existing `toBytes` contract, which avoids "empty input silently
|
|
20
|
+
* succeeds".
|
|
21
|
+
*
|
|
22
|
+
* The IV is likewise hex / Uint8Array; when omitted, 12 bytes are generated from the
|
|
23
|
+
* host CSPRNG.
|
|
11
24
|
*/
|
|
12
25
|
/**
|
|
13
26
|
* 把 hybrid 的 message 归一化为字节。
|
|
@@ -15,11 +28,23 @@
|
|
|
15
28
|
* @param input hex 字符串或字节数组。
|
|
16
29
|
* @param label 错误信息里的字段名。
|
|
17
30
|
* @throws TypeError 空字符串、奇数长度、非 hex 字符或类型不对。
|
|
31
|
+
*
|
|
32
|
+
* Normalize the hybrid `message` into bytes.
|
|
33
|
+
*
|
|
34
|
+
* @param input A hex string or a byte array.
|
|
35
|
+
* @param label Field name used in error messages.
|
|
36
|
+
* @throws TypeError The string is empty or of odd length, contains a non-hex character,
|
|
37
|
+
* or the value has the wrong type.
|
|
18
38
|
*/
|
|
19
39
|
export declare function toMessageBytes(input: string | Uint8Array, label?: string): Uint8Array;
|
|
20
40
|
/**
|
|
21
41
|
* 归一化 hybrid 的 IV:未提供则新生成 12 字节;提供则必须恰好 12 字节。
|
|
22
42
|
*
|
|
23
43
|
* @throws RangeError 长度不是 12。
|
|
44
|
+
*
|
|
45
|
+
* Normalize the hybrid IV: generate 12 fresh bytes when absent; when supplied it must
|
|
46
|
+
* be exactly 12 bytes.
|
|
47
|
+
*
|
|
48
|
+
* @throws RangeError The length is not 12.
|
|
24
49
|
*/
|
|
25
50
|
export declare function resolveIv(iv: string | Uint8Array | undefined): Uint8Array;
|
package/dist/common/hybrid.js
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
* 避免“空输入静默成功”。
|
|
9
9
|
*
|
|
10
10
|
* IV 同样按 hex / Uint8Array 处理,未提供时由宿主 CSPRNG 生成 12 字节。
|
|
11
|
+
*
|
|
12
|
+
* Shared input conventions for hybrid encryption.
|
|
13
|
+
*
|
|
14
|
+
* The two hybrid modules (SM2 / SM9) apply **the same** string semantics to `message`:
|
|
15
|
+
* - `Uint8Array`: used as is (copied);
|
|
16
|
+
* - `string`: must be hex (a `0x` prefix is allowed); base64 / UTF-8 are no longer
|
|
17
|
+
* guessed;
|
|
18
|
+
* - pass `new Uint8Array(0)` for an empty message — an empty string is rejected
|
|
19
|
+
* outright by the existing `toBytes` contract, which avoids "empty input silently
|
|
20
|
+
* succeeds".
|
|
21
|
+
*
|
|
22
|
+
* The IV is likewise hex / Uint8Array; when omitted, 12 bytes are generated from the
|
|
23
|
+
* host CSPRNG.
|
|
11
24
|
*/
|
|
12
25
|
import { hexToBytes } from '../runtime/convert.js';
|
|
13
26
|
import { randomBytes } from '../runtime/random.js';
|
|
@@ -18,6 +31,13 @@ import { SEALED_IV_LEN } from './sealed.js';
|
|
|
18
31
|
* @param input hex 字符串或字节数组。
|
|
19
32
|
* @param label 错误信息里的字段名。
|
|
20
33
|
* @throws TypeError 空字符串、奇数长度、非 hex 字符或类型不对。
|
|
34
|
+
*
|
|
35
|
+
* Normalize the hybrid `message` into bytes.
|
|
36
|
+
*
|
|
37
|
+
* @param input A hex string or a byte array.
|
|
38
|
+
* @param label Field name used in error messages.
|
|
39
|
+
* @throws TypeError The string is empty or of odd length, contains a non-hex character,
|
|
40
|
+
* or the value has the wrong type.
|
|
21
41
|
*/
|
|
22
42
|
export function toMessageBytes(input, label = 'message') {
|
|
23
43
|
if (input instanceof Uint8Array)
|
|
@@ -34,6 +54,11 @@ export function toMessageBytes(input, label = 'message') {
|
|
|
34
54
|
* 归一化 hybrid 的 IV:未提供则新生成 12 字节;提供则必须恰好 12 字节。
|
|
35
55
|
*
|
|
36
56
|
* @throws RangeError 长度不是 12。
|
|
57
|
+
*
|
|
58
|
+
* Normalize the hybrid IV: generate 12 fresh bytes when absent; when supplied it must
|
|
59
|
+
* be exactly 12 bytes.
|
|
60
|
+
*
|
|
61
|
+
* @throws RangeError The length is not 12.
|
|
37
62
|
*/
|
|
38
63
|
export function resolveIv(iv) {
|
|
39
64
|
const out = iv === undefined
|