samlesa 2.17.3 → 2.18.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/build/src/binding-artifact.js +24 -14
- package/build/src/flow.js +169 -27
- package/build/src/libsaml.js +442 -213
- package/package.json +1 -1
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +50 -1
- package/types/src/libsaml.d.ts.map +1 -1
|
@@ -383,22 +383,32 @@ async function parseLoginResponseResolve(params) {
|
|
|
383
383
|
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
384
384
|
}
|
|
385
385
|
samlContent = verifiedAssertionNode1;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
386
|
+
// 改进的postFlow函数中关于签名验证的部分
|
|
387
|
+
const verificationResult = libsaml.verifySignature(samlContent, verificationOptions, self);
|
|
388
|
+
// 检查验证结果
|
|
389
|
+
if (!verificationResult.status) {
|
|
390
|
+
// 如果验证失败,根据具体情况返回错误
|
|
391
|
+
if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
|
|
392
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
393
|
+
}
|
|
394
|
+
if (verificationResult.isAssertionSigned && !verificationResult.AssertionSignatureStatus) {
|
|
395
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
396
|
+
}
|
|
397
|
+
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
398
|
+
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
399
|
+
}
|
|
400
|
+
// 通用验证失败
|
|
401
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
394
402
|
}
|
|
395
|
-
|
|
396
|
-
|
|
403
|
+
// 更新samlContent为验证后的版本(可能已解密)
|
|
404
|
+
samlContent = verificationResult.samlContent;
|
|
405
|
+
// 根据验证结果设置extractorFields
|
|
406
|
+
if (verificationResult.assertionContent) {
|
|
407
|
+
extractorFields = getDefaultExtractorFields(parserType, verificationResult.assertionContent);
|
|
397
408
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
extractorFields = getDefaultExtractorFields(parserType, result[1]);
|
|
409
|
+
else {
|
|
410
|
+
// 如果没有断言内容(例如注销请求/响应),使用适当的处理方式
|
|
411
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
402
412
|
}
|
|
403
413
|
const parseResult = {
|
|
404
414
|
samlContent: samlContent,
|
package/build/src/flow.js
CHANGED
|
@@ -151,23 +151,149 @@ async function postFlow(options) {
|
|
|
151
151
|
// check status based on different scenarios
|
|
152
152
|
await checkStatus(samlContent, parserType);
|
|
153
153
|
/**检查签名顺序 */
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
/*
|
|
155
|
+
const [verified, verifiedAssertionNode, isDecryptRequired, noSignature] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
156
|
+
decryptRequired = isDecryptRequired
|
|
157
|
+
if (isDecryptRequired && noSignature) {
|
|
158
|
+
|
|
159
|
+
const result = await libsaml.decryptAssertion(self, samlContent);
|
|
160
|
+
samlContent = result[0];
|
|
161
|
+
extractorFields = getDefaultExtractorFields(parserType, result[1]);
|
|
162
|
+
}
|
|
163
|
+
if (!verified && !noSignature && !isDecryptRequired) {
|
|
164
|
+
|
|
165
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
166
|
+
}
|
|
167
|
+
if (!isDecryptRequired) {
|
|
168
|
+
|
|
169
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
170
|
+
}
|
|
171
|
+
if (parserType === 'SAMLResponse' && isDecryptRequired && !noSignature) {
|
|
172
|
+
const result = await libsaml.decryptAssertion(self, samlContent);
|
|
173
|
+
samlContent = result[0];
|
|
174
|
+
extractorFields = getDefaultExtractorFields(parserType, result[1]);
|
|
175
|
+
console.log("走这里来了=========")
|
|
176
|
+
console.log(result[1])
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
const parseResult = {
|
|
181
|
+
samlContent: samlContent,
|
|
182
|
+
extract: extract(samlContent, extractorFields),
|
|
183
|
+
};
|
|
184
|
+
/!**
|
|
185
|
+
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
186
|
+
*!/
|
|
187
|
+
const targetEntityMetadata = from.entityMeta;
|
|
188
|
+
const issuer = targetEntityMetadata.getEntityID();
|
|
189
|
+
const extractedProperties = parseResult.extract;
|
|
190
|
+
// unmatched issuer
|
|
191
|
+
if (
|
|
192
|
+
(parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
193
|
+
&& extractedProperties
|
|
194
|
+
&& extractedProperties.issuer !== issuer
|
|
195
|
+
) {
|
|
196
|
+
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// invalid session time
|
|
200
|
+
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
201
|
+
if (
|
|
202
|
+
parserType === 'SAMLResponse'
|
|
203
|
+
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
204
|
+
&& !verifyTime(
|
|
205
|
+
undefined,
|
|
206
|
+
extractedProperties.sessionIndex.sessionNotOnOrAfter,
|
|
207
|
+
self.entitySetting.clockDrifts
|
|
208
|
+
)
|
|
209
|
+
) {
|
|
210
|
+
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// invalid time
|
|
214
|
+
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
215
|
+
if (
|
|
216
|
+
parserType === 'SAMLResponse'
|
|
217
|
+
&& extractedProperties.conditions
|
|
218
|
+
&& !verifyTime(
|
|
219
|
+
extractedProperties.conditions.notBefore,
|
|
220
|
+
extractedProperties.conditions.notOnOrAfter,
|
|
221
|
+
self.entitySetting.clockDrifts
|
|
222
|
+
)
|
|
223
|
+
) {
|
|
224
|
+
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
225
|
+
}
|
|
226
|
+
//valid destination
|
|
227
|
+
//There is no validation of the response here. The upper-layer application
|
|
228
|
+
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
229
|
+
// whether the response.id is used to prevent replay attacks.
|
|
230
|
+
/!*
|
|
231
|
+
let destination = extractedProperties?.response?.destination
|
|
232
|
+
let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
|
|
233
|
+
return item?.Location === destination
|
|
234
|
+
})
|
|
235
|
+
if (isExit?.length === 0) {
|
|
236
|
+
return Promise.reject('ERR_Destination_URL');
|
|
237
|
+
}
|
|
238
|
+
if (parserType === 'SAMLResponse') {
|
|
239
|
+
let destination = extractedProperties?.response?.destination
|
|
240
|
+
let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
|
|
241
|
+
return item?.Location === destination
|
|
242
|
+
})
|
|
243
|
+
if (isExit?.length === 0) {
|
|
244
|
+
return Promise.reject('ERR_Destination_URL');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
*!/
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
return Promise.resolve(parseResult);*/
|
|
251
|
+
// 改进的postFlow函数中关于签名验证的部分
|
|
252
|
+
const verificationResult = libsaml.verifySignature(samlContent, verificationOptions, self);
|
|
253
|
+
/* console.log(verificationResult)
|
|
254
|
+
console.log("解析对象")*/
|
|
255
|
+
let resultObject = {
|
|
256
|
+
isMessageSigned: true, //是否有外层的消息签名(Response或者Request 等最外层的签名)
|
|
257
|
+
MessageSignatureStatus: true, //外层的签名是否经过验证
|
|
258
|
+
isAssertionSigned: true, //是否有断言的签名
|
|
259
|
+
AssertionSignatureStatus: true, //断言签名是否经过验证
|
|
260
|
+
encrypted: true, //断言是否加密
|
|
261
|
+
decrypted: true, //断言加密后断言是否解密成功,
|
|
262
|
+
status: true, //是否全部通过验证,
|
|
263
|
+
samlContent: 'xxx', //xxx是通过验证后 解密后的整个响应,
|
|
264
|
+
assertionContent: 'xxx', //xxx是通过验证后 解密后的整个响应中的assertion 断言部分字符串
|
|
265
|
+
};
|
|
266
|
+
// 检查验证结果
|
|
267
|
+
if (!verificationResult.status) {
|
|
268
|
+
// 如果验证失败,根据具体情况返回错误
|
|
269
|
+
/** 需要判断是不是 */
|
|
270
|
+
if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
|
|
271
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
272
|
+
}
|
|
273
|
+
if (verificationResult.isAssertionSigned && !verificationResult.AssertionSignatureStatus) {
|
|
274
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
275
|
+
}
|
|
276
|
+
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
277
|
+
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
278
|
+
}
|
|
279
|
+
if (!verificationResult.isMessageSigned && verificationResult.type === 'LogoutRequest') {
|
|
280
|
+
return Promise.reject('ERR_LogoutRequest_Need_Signature');
|
|
281
|
+
}
|
|
282
|
+
if (!verificationResult.isMessageSigned && verificationResult.type === 'LogoutResponse') {
|
|
283
|
+
return Promise.reject('ERR_LogoutResponse_Need_Signature');
|
|
284
|
+
}
|
|
285
|
+
// 通用验证失败
|
|
286
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
163
287
|
}
|
|
164
|
-
|
|
165
|
-
|
|
288
|
+
// 更新samlContent为验证后的版本(可能已解密)
|
|
289
|
+
samlContent = verificationResult.samlContent;
|
|
290
|
+
// 根据验证结果设置extractorFields
|
|
291
|
+
if (verificationResult.assertionContent) {
|
|
292
|
+
extractorFields = getDefaultExtractorFields(parserType, verificationResult.assertionContent);
|
|
166
293
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
extractorFields = getDefaultExtractorFields(parserType, result[1]);
|
|
294
|
+
else {
|
|
295
|
+
// 如果没有断言内容(例如注销请求/响应),使用适当的处理方式
|
|
296
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
171
297
|
}
|
|
172
298
|
const parseResult = {
|
|
173
299
|
samlContent: samlContent,
|
|
@@ -245,18 +371,34 @@ async function postArtifactFlow(options) {
|
|
|
245
371
|
// check status based on different scenarios
|
|
246
372
|
await checkStatus(samlContent, parserType);
|
|
247
373
|
/**检查签名顺序 */
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (!
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
374
|
+
// 改进的postFlow函数中关于签名验证的部分
|
|
375
|
+
const verificationResult = libsaml.verifySignature(samlContent, verificationOptions, self);
|
|
376
|
+
console.log(verificationResult);
|
|
377
|
+
console.log("最终结果====");
|
|
378
|
+
// 检查验证结果
|
|
379
|
+
if (!verificationResult.status) {
|
|
380
|
+
// 如果验证失败,根据具体情况返回错误
|
|
381
|
+
if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
|
|
382
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
383
|
+
}
|
|
384
|
+
if (verificationResult.isAssertionSigned && !verificationResult.AssertionSignatureStatus) {
|
|
385
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
386
|
+
}
|
|
387
|
+
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
388
|
+
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
389
|
+
}
|
|
390
|
+
// 通用验证失败
|
|
391
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
392
|
+
}
|
|
393
|
+
// 更新samlContent为验证后的版本(可能已解密)
|
|
394
|
+
samlContent = verificationResult.samlContent;
|
|
395
|
+
// 根据验证结果设置extractorFields
|
|
396
|
+
if (verificationResult.assertionContent) {
|
|
397
|
+
extractorFields = getDefaultExtractorFields(parserType, verificationResult.assertionContent);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// 如果没有断言内容(例如注销请求/响应),使用适当的处理方式
|
|
401
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
260
402
|
}
|
|
261
403
|
const parseResult = {
|
|
262
404
|
samlContent: samlContent,
|
package/build/src/libsaml.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @author tngan
|
|
4
4
|
* @desc A simple library including some common functions
|
|
5
5
|
*/
|
|
6
|
+
import { X509Certificate } from 'node:crypto';
|
|
6
7
|
import xml from 'xml';
|
|
7
8
|
import utility, { flattenDeep, inflateString, isString } from './utility.js';
|
|
8
9
|
import { algorithms, namespace, wording } from './urn.js';
|
|
@@ -73,9 +74,9 @@ const libSaml = () => {
|
|
|
73
74
|
};
|
|
74
75
|
const defaultSoapResponseFailTemplate = {
|
|
75
76
|
context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header></SOAP-ENV:Header>
|
|
76
|
-
<samlp:ArtifactResponse xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
77
|
+
<samlp:ArtifactResponse xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
77
78
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
78
|
-
InResponseTo="{InResponseTo}" Version="2.0"
|
|
79
|
+
InResponseTo="{InResponseTo}" Version="2.0"
|
|
79
80
|
IssueInstant="{IssueInstant}">
|
|
80
81
|
<saml:Issuer>{Issuer}</saml:Issuer>
|
|
81
82
|
<samlp:Status>
|
|
@@ -336,6 +337,37 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
336
337
|
}
|
|
337
338
|
return isBase64Output ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml();
|
|
338
339
|
},
|
|
340
|
+
// 安全的证书验证函数
|
|
341
|
+
validateCertificate(certificateBase64, expectedIssuer) {
|
|
342
|
+
try {
|
|
343
|
+
const cert = new X509Certificate(Buffer.from(certificateBase64, 'base64'));
|
|
344
|
+
// 1. 检查有效期
|
|
345
|
+
const now = new Date();
|
|
346
|
+
if (new Date(cert.validFrom) > now || new Date(cert.validTo) < now) {
|
|
347
|
+
throw new Error('Certificate has expired or is not yet valid');
|
|
348
|
+
}
|
|
349
|
+
// 2. 检查颁发者(如果提供)
|
|
350
|
+
if (expectedIssuer && !cert.subject.includes(expectedIssuer)) {
|
|
351
|
+
throw new Error('Certificate issuer does not match expected value');
|
|
352
|
+
}
|
|
353
|
+
// 3. 检查公钥类型(推荐 RSA 或 EC)
|
|
354
|
+
if (!['rsa', 'ec'].includes(cert.publicKey.type.toLowerCase())) {
|
|
355
|
+
throw new Error('Certificate uses unsupported public key type');
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
isValid: true,
|
|
359
|
+
subject: cert.subject,
|
|
360
|
+
issuer: cert.issuer,
|
|
361
|
+
publicKey: cert.publicKey
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return {
|
|
366
|
+
isValid: false,
|
|
367
|
+
error: error.message
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
},
|
|
339
371
|
/**
|
|
340
372
|
* @desc Verify the XML signature
|
|
341
373
|
* @param {string} xml xml
|
|
@@ -345,7 +377,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
345
377
|
* - The second element is the cryptographically authenticated assertion node as a string, or `null` if not found.
|
|
346
378
|
*/
|
|
347
379
|
// tslint:disable-next-line:no-shadowed-variable
|
|
348
|
-
|
|
380
|
+
verifySignature1(xml, opts) {
|
|
349
381
|
const { dom } = getContext();
|
|
350
382
|
const doc = dom.parseFromString(xml, 'application/xml');
|
|
351
383
|
const docParser = new DOMParser();
|
|
@@ -389,6 +421,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
389
421
|
if (selection.length === 0) {
|
|
390
422
|
/** 判断有没有加密如果没有加密返回 [false, null]*/
|
|
391
423
|
if (encryptedAssertions.length > 0) {
|
|
424
|
+
console.log("走加密了====");
|
|
392
425
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
393
426
|
return [false, null, false, true]; // we return false now
|
|
394
427
|
}
|
|
@@ -399,6 +432,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
399
432
|
}
|
|
400
433
|
}
|
|
401
434
|
if (selection.length !== 0) {
|
|
435
|
+
console.log("走加密了1====");
|
|
402
436
|
/** 判断有没有加密如果没有加密返回 [false, null]*/
|
|
403
437
|
if (logoutRequestSignature.length === 0 && LogoutResponseSignatureElementNode.length === 0 && encryptedAssertions.length > 0) {
|
|
404
438
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
@@ -418,10 +452,16 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
418
452
|
if (!opts.keyFile && !opts.metadata) {
|
|
419
453
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
420
454
|
}
|
|
455
|
+
console.log("开始1");
|
|
421
456
|
if (opts.keyFile) {
|
|
457
|
+
console.log("开始11");
|
|
458
|
+
let publicCertResult = this.validateCertificate(fs.readFileSync(opts.keyFile));
|
|
459
|
+
console.log(publicCertResult);
|
|
460
|
+
console.log("结果");
|
|
422
461
|
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
423
462
|
}
|
|
424
463
|
if (opts.metadata) {
|
|
464
|
+
console.log("开始111");
|
|
425
465
|
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
426
466
|
// certificate in metadata
|
|
427
467
|
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
@@ -448,10 +488,17 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
448
488
|
// to make sure the response certificate is one of those specified in metadata
|
|
449
489
|
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
450
490
|
}
|
|
491
|
+
let publicCertResult = this.validateCertificate(x509Certificate);
|
|
492
|
+
console.log(publicCertResult);
|
|
493
|
+
console.log("结果");
|
|
451
494
|
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
452
495
|
}
|
|
453
496
|
else {
|
|
497
|
+
console.log("开始11111");
|
|
454
498
|
// Select first one from metadata
|
|
499
|
+
let publicCertResult = this.validateCertificate(metadataCert[0]);
|
|
500
|
+
console.log(publicCertResult);
|
|
501
|
+
console.log("结果");
|
|
455
502
|
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
456
503
|
}
|
|
457
504
|
}
|
|
@@ -509,232 +556,317 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
509
556
|
return [false, null, false, true]; // return encryptedAssert
|
|
510
557
|
/* throw new Error('ERR_ZERO_SIGNATURE');*/
|
|
511
558
|
},
|
|
512
|
-
|
|
513
|
-
|
|
559
|
+
/**
|
|
560
|
+
* 改进的SAML签名验证函数,支持多种签名和加密组合场景
|
|
561
|
+
* @param xml SAML XML内容
|
|
562
|
+
* @param opts 验证选项
|
|
563
|
+
* @param self
|
|
564
|
+
* @returns 验证结果对象
|
|
565
|
+
*/
|
|
566
|
+
/**
|
|
567
|
+
* 改进的SAML签名验证函数,支持多种签名和加密组合场景
|
|
568
|
+
* @param xml SAML XML内容
|
|
569
|
+
* @param opts 验证选项
|
|
570
|
+
* @param self
|
|
571
|
+
* @returns 验证结果对象
|
|
572
|
+
*/
|
|
573
|
+
verifySignature(xml, opts, self) {
|
|
574
|
+
const { dom } = getContext();
|
|
514
575
|
const doc = dom.parseFromString(xml, 'application/xml');
|
|
515
576
|
const docParser = new DOMParser();
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (signatureNode.length === 0) {
|
|
526
|
-
throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
selection = selection.concat(signatureNode);
|
|
530
|
-
} else {
|
|
531
|
-
// 原始的SOAP响应验证逻辑
|
|
532
|
-
const messageSignatureXpath =
|
|
533
|
-
"/!*[local-name()='Envelope']/!*[local-name()='Body']" +
|
|
534
|
-
"/!*[local-name()='ArtifactResponse']/!*[local-name()='Signature'] | " +
|
|
535
|
-
"/!*[local-name()='Envelope']/!*[local-name()='Body']" +
|
|
536
|
-
"/!*[local-name()='ArtifactResponse']/!*[local-name()='Response']/!*[local-name()='Signature']";
|
|
537
|
-
|
|
538
|
-
const assertionSignatureXpath =
|
|
539
|
-
"/!*[local-name()='Envelope']/!*[local-name()='Body']" +
|
|
540
|
-
"/!*[local-name()='ArtifactResponse']/!*[local-name()='Response']" +
|
|
541
|
-
"/!*[local-name()='Assertion']/!*[local-name()='Signature'] | " +
|
|
542
|
-
"/!*[local-name()='Envelope']/!*[local-name()='Body']" +
|
|
543
|
-
"/!*[local-name()='ArtifactResponse']/!*[local-name()='Response']" +
|
|
544
|
-
"/!*[local-name()='EncryptedAssertion']";
|
|
545
|
-
|
|
546
|
-
const wrappingElementsXPath =
|
|
547
|
-
"/!*[local-name()='Envelope']/!*[local-name()='Body']" +
|
|
548
|
-
"/!*[local-name()='ArtifactResponse']/!*[local-name()='Response']" +
|
|
549
|
-
"/!*[local-name()='Assertion']/!*[local-name()='Subject']" +
|
|
550
|
-
"/!*[local-name()='SubjectConfirmation']" +
|
|
551
|
-
"/!*[local-name()='SubjectConfirmationData']" +
|
|
552
|
-
"//!*[local-name()='Assertion' or local-name()='Signature']";
|
|
553
|
-
|
|
554
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
555
|
-
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
556
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
557
|
-
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
558
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
559
|
-
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
560
|
-
|
|
561
|
-
// 检测包装攻击
|
|
562
|
-
if (wrappingElementNode.length !== 0) {
|
|
577
|
+
// 定义各种XPath路径
|
|
578
|
+
const messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']";
|
|
579
|
+
const assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']";
|
|
580
|
+
const wrappingElementsXPath = "/*[contains(local-name(), 'Response')]/*[local-name(.)='Assertion']/*[local-name(.)='Subject']/*[local-name(.)='SubjectConfirmation']/*[local-name(.)='SubjectConfirmationData']//*[local-name(.)='Assertion' or local-name(.)='Signature']";
|
|
581
|
+
const encryptedAssertionsXPath = "/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']";
|
|
582
|
+
// 检测包装攻击
|
|
583
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
584
|
+
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
585
|
+
if (wrappingElementNode.length !== 0) {
|
|
563
586
|
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// 保证响应中至少有一个签名
|
|
567
|
-
if (messageSignatureNode.length === 0 && assertionSignatureNode.length === 0) {
|
|
568
|
-
throw new Error('ERR_ZERO_SIGNATURE');
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
selection = selection.concat(messageSignatureNode, assertionSignatureNode);
|
|
572
587
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
588
|
+
// 获取各种元素
|
|
589
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
590
|
+
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
591
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
592
|
+
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
593
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
594
|
+
const encryptedAssertions = select(encryptedAssertionsXPath, doc);
|
|
595
|
+
// 初始化验证状态
|
|
596
|
+
let isMessageSigned = messageSignatureNode.length > 0;
|
|
597
|
+
let isAssertionSigned = assertionSignatureNode.length > 0;
|
|
598
|
+
let encrypted = encryptedAssertions.length > 0;
|
|
599
|
+
let decrypted = false;
|
|
600
|
+
let MessageSignatureStatus = false;
|
|
601
|
+
let AssertionSignatureStatus = false;
|
|
602
|
+
let status = false;
|
|
603
|
+
let samlContent = xml;
|
|
604
|
+
let assertionContent = null;
|
|
605
|
+
// 检测SAML消息类型
|
|
606
|
+
// 检测SAML消息类型 - 使用精确匹配避免包含关系导致的误判
|
|
607
|
+
const rootElementName = doc.documentElement.localName;
|
|
608
|
+
let type = 'Unknown';
|
|
609
|
+
// 使用精确字符串比较,避免包含关系的问题
|
|
610
|
+
switch (rootElementName) {
|
|
611
|
+
case 'AuthnRequest':
|
|
612
|
+
type = 'AuthnRequest';
|
|
613
|
+
break;
|
|
614
|
+
case 'Response':
|
|
615
|
+
type = 'Response';
|
|
616
|
+
break;
|
|
617
|
+
case 'LogoutRequest':
|
|
618
|
+
type = 'LogoutRequest';
|
|
619
|
+
break;
|
|
620
|
+
case 'LogoutResponse':
|
|
621
|
+
type = 'LogoutResponse';
|
|
622
|
+
break;
|
|
623
|
+
default:
|
|
624
|
+
// 如果不是完全匹配,尝试模糊匹配
|
|
625
|
+
if (rootElementName.includes('AuthnRequest')) {
|
|
626
|
+
type = 'AuthnRequest';
|
|
627
|
+
}
|
|
628
|
+
else if (rootElementName.includes('LogoutResponse')) {
|
|
629
|
+
type = 'LogoutResponse';
|
|
630
|
+
}
|
|
631
|
+
else if (rootElementName.includes('LogoutRequest')) {
|
|
632
|
+
type = 'LogoutRequest';
|
|
633
|
+
}
|
|
634
|
+
else if (rootElementName.includes('Response')) {
|
|
635
|
+
type = 'Response';
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
type = 'Unknown';
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// 特殊情况:带未签名断言的未签名SAML响应,应该拒绝
|
|
642
|
+
if (!isMessageSigned && !isAssertionSigned && !encrypted) {
|
|
643
|
+
return {
|
|
644
|
+
isMessageSigned,
|
|
645
|
+
MessageSignatureStatus,
|
|
646
|
+
isAssertionSigned,
|
|
647
|
+
AssertionSignatureStatus,
|
|
648
|
+
encrypted,
|
|
649
|
+
decrypted,
|
|
650
|
+
type, // 添加类型字段
|
|
651
|
+
status: false, // 明确拒绝未签名未加密的响应
|
|
652
|
+
samlContent,
|
|
653
|
+
assertionContent
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// 处理最外层有签名且断言加密的情况(关键逻辑补充)
|
|
657
|
+
if (isMessageSigned && encrypted) {
|
|
658
|
+
// 1. 首先解密断言
|
|
659
|
+
try {
|
|
660
|
+
const result = this.decryptAssertionSync(self, xml, opts);
|
|
661
|
+
// 更新文档为解密后的版本
|
|
662
|
+
samlContent = result[0];
|
|
663
|
+
assertionContent = result[1];
|
|
664
|
+
// 更新验证状态
|
|
665
|
+
decrypted = true;
|
|
666
|
+
AssertionSignatureStatus = result?.[2]?.AssertionSignatureStatus || false;
|
|
667
|
+
isAssertionSigned = result?.[2]?.isAssertionSigned || false;
|
|
668
|
+
// 解密后的文档
|
|
669
|
+
const decryptedDoc = dom.parseFromString(samlContent, 'application/xml');
|
|
670
|
+
// 2. 验证最外层消息签名(使用解密后的文档)
|
|
671
|
+
const signatureNode = messageSignatureNode[0];
|
|
672
|
+
const sig = new SignedXml();
|
|
673
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
674
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
675
|
+
}
|
|
676
|
+
if (opts.keyFile) {
|
|
677
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
678
|
+
}
|
|
679
|
+
else if (opts.metadata) {
|
|
680
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
681
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
682
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
683
|
+
if (Array.isArray(metadataCert)) {
|
|
684
|
+
metadataCert = flattenDeep(metadataCert);
|
|
685
|
+
}
|
|
686
|
+
else if (typeof metadataCert === 'string') {
|
|
687
|
+
metadataCert = [metadataCert];
|
|
688
|
+
}
|
|
689
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
690
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
691
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
692
|
+
}
|
|
693
|
+
if (certificateNode.length !== 0) {
|
|
694
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
695
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
696
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
697
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
698
|
+
}
|
|
699
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
706
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
707
|
+
sig.loadSignature(signatureNode);
|
|
708
|
+
// 使用解密后的文档验证最外层签名
|
|
709
|
+
MessageSignatureStatus = sig.checkSignature(decryptedDoc.toString());
|
|
710
|
+
if (!MessageSignatureStatus) {
|
|
711
|
+
throw new Error('ERR_FAILED_TO_VERIFY_MESSAGE_SIGNATURE_AFTER_DECRYPTION');
|
|
712
|
+
}
|
|
713
|
+
// 3. 验证解密后断言的签名(如果存在)
|
|
714
|
+
if (isAssertionSigned && AssertionSignatureStatus) {
|
|
715
|
+
/* console.log("断言签名验证已通过");*/
|
|
716
|
+
}
|
|
717
|
+
else if (isAssertionSigned && !AssertionSignatureStatus) {
|
|
718
|
+
throw new Error('ERR_FAILED_TO_VERIFY_ASSERTION_SIGNATURE_AFTER_DECRYPTION');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
throw err;
|
|
599
723
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
724
|
+
}
|
|
725
|
+
// 处理最外层有签名但断言未加密的情况
|
|
726
|
+
else if (isMessageSigned && !encrypted) {
|
|
727
|
+
const signatureNode = messageSignatureNode[0];
|
|
728
|
+
const sig = new SignedXml();
|
|
729
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
730
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
606
731
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (certificateNodes.length !== 0) {
|
|
610
|
-
// 安全获取证书数据
|
|
611
|
-
let x509CertificateData = '';
|
|
612
|
-
if (certificateNodes[0].firstChild) {
|
|
613
|
-
x509CertificateData = certificateNodes[0].firstChild.data;
|
|
614
|
-
} else if (certificateNodes[0].textContent) {
|
|
615
|
-
x509CertificateData = certificateNodes[0].textContent;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
619
|
-
|
|
620
|
-
// 验证证书匹配
|
|
621
|
-
if (
|
|
622
|
-
metadataCert.length >= 1 &&
|
|
623
|
-
!metadataCert.find(cert => cert.trim() === x509Certificate.trim())
|
|
624
|
-
) {
|
|
625
|
-
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
629
|
-
} else {
|
|
630
|
-
// 使用元数据中的第一个证书
|
|
631
|
-
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
732
|
+
if (opts.keyFile) {
|
|
733
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
632
734
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
} else {
|
|
659
|
-
throw new Error('ERR_INVALID_ASSERTION_SIGNATURE');
|
|
735
|
+
else if (opts.metadata) {
|
|
736
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
737
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
738
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
739
|
+
if (Array.isArray(metadataCert)) {
|
|
740
|
+
metadataCert = flattenDeep(metadataCert);
|
|
741
|
+
}
|
|
742
|
+
else if (typeof metadataCert === 'string') {
|
|
743
|
+
metadataCert = [metadataCert];
|
|
744
|
+
}
|
|
745
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
746
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
747
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
748
|
+
}
|
|
749
|
+
if (certificateNode.length !== 0) {
|
|
750
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
751
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
752
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
753
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
754
|
+
}
|
|
755
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
759
|
+
}
|
|
660
760
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
// 处理已验证的签名
|
|
664
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
665
|
-
if (rootNode.localName === 'ArtifactResponse') {
|
|
666
|
-
// 在 ArtifactResponse 中查找 Response
|
|
761
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
667
762
|
// @ts-expect-error misssing Node properties are not needed
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
) as Element[];
|
|
673
|
-
|
|
674
|
-
if (responseNodes.length === 0) {
|
|
675
|
-
continue;
|
|
763
|
+
sig.loadSignature(signatureNode);
|
|
764
|
+
MessageSignatureStatus = sig.checkSignature(doc.toString());
|
|
765
|
+
if (!MessageSignatureStatus) {
|
|
766
|
+
throw new Error('ERR_FAILED_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
676
767
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
) as Element[];
|
|
685
|
-
|
|
686
|
-
const assertions = select(
|
|
687
|
-
"./!*[local-name()='Assertion']",
|
|
688
|
-
responseNode
|
|
689
|
-
) as Element[];
|
|
690
|
-
|
|
691
|
-
if (encryptedAssertions.length === 1) {
|
|
692
|
-
return [true, encryptedAssertions[0].toString(), true];
|
|
768
|
+
}
|
|
769
|
+
// 验证断言级签名(如果存在且未加密)
|
|
770
|
+
if (isAssertionSigned && !encrypted) {
|
|
771
|
+
const signatureNode = assertionSignatureNode[0];
|
|
772
|
+
const sig = new SignedXml();
|
|
773
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
774
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
693
775
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
return [true, assertions[0].toString(), false];
|
|
776
|
+
if (opts.keyFile) {
|
|
777
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
697
778
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
779
|
+
else if (opts.metadata) {
|
|
780
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
781
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
782
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
783
|
+
if (Array.isArray(metadataCert)) {
|
|
784
|
+
metadataCert = flattenDeep(metadataCert);
|
|
785
|
+
}
|
|
786
|
+
else if (typeof metadataCert === 'string') {
|
|
787
|
+
metadataCert = [metadataCert];
|
|
788
|
+
}
|
|
789
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
790
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
791
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
792
|
+
}
|
|
793
|
+
if (certificateNode.length !== 0) {
|
|
794
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
795
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
796
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
797
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
798
|
+
}
|
|
799
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
702
806
|
// @ts-expect-error misssing Node properties are not needed
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
706
|
-
rootNode
|
|
707
|
-
) as Element[];
|
|
807
|
+
sig.loadSignature(signatureNode);
|
|
808
|
+
// 为断言签名验证,我们需要从根文档中获取断言部分
|
|
708
809
|
// @ts-expect-error misssing Node properties are not needed
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
810
|
+
const assertionNode = select("/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']", doc)[0];
|
|
811
|
+
if (assertionNode) {
|
|
812
|
+
const assertionDoc = dom.parseFromString(assertionNode.toString(), 'application/xml');
|
|
813
|
+
AssertionSignatureStatus = sig.checkSignature(assertionDoc.toString());
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
AssertionSignatureStatus = false;
|
|
817
|
+
}
|
|
818
|
+
if (!AssertionSignatureStatus) {
|
|
819
|
+
throw new Error('ERR_FAILED_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// 处理仅加密断言的情况(无消息签名)
|
|
823
|
+
if (encrypted && !isMessageSigned) {
|
|
824
|
+
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
825
|
+
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
826
|
+
}
|
|
827
|
+
if (encryptedAssertions.length > 1) {
|
|
828
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
717
829
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
830
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
831
|
+
// 解密断言
|
|
832
|
+
try {
|
|
833
|
+
const result = this.decryptAssertionSync(self, xml, opts);
|
|
834
|
+
samlContent = result[0];
|
|
835
|
+
assertionContent = result[1];
|
|
836
|
+
decrypted = true;
|
|
837
|
+
AssertionSignatureStatus = result?.[2]?.AssertionSignatureStatus;
|
|
838
|
+
isAssertionSigned = result?.[2]?.isAssertionSigned;
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
throw new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION');
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (!encrypted && (isMessageSigned || isAssertionSigned)) {
|
|
845
|
+
// 如果没有加密但有签名,提取断言内容
|
|
846
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
847
|
+
const assertions = select("/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']", doc);
|
|
848
|
+
if (assertions.length > 0) {
|
|
849
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
850
|
+
assertionContent = assertions[0].toString();
|
|
721
851
|
}
|
|
722
|
-
}
|
|
723
|
-
// 直接处理 Assertion
|
|
724
|
-
else if (rootNode?.localName === 'Assertion') {
|
|
725
|
-
return [true, rootNode.toString(), false];
|
|
726
|
-
}
|
|
727
|
-
// 直接处理 EncryptedAssertion
|
|
728
|
-
else if (rootNode?.localName === 'EncryptedAssertion') {
|
|
729
|
-
return [true, rootNode.toString(), true];
|
|
730
|
-
} else {
|
|
731
|
-
|
|
732
|
-
console.warn("未知的根节点类型:", rootNode?.localName);
|
|
733
|
-
}
|
|
734
852
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
853
|
+
// 检查整体状态
|
|
854
|
+
status = (!isMessageSigned || MessageSignatureStatus) &&
|
|
855
|
+
(!isAssertionSigned || AssertionSignatureStatus) &&
|
|
856
|
+
(!encrypted || decrypted);
|
|
857
|
+
return {
|
|
858
|
+
isMessageSigned,
|
|
859
|
+
MessageSignatureStatus,
|
|
860
|
+
isAssertionSigned,
|
|
861
|
+
AssertionSignatureStatus,
|
|
862
|
+
encrypted,
|
|
863
|
+
decrypted,
|
|
864
|
+
type, // 添加类型字段
|
|
865
|
+
status,
|
|
866
|
+
samlContent,
|
|
867
|
+
assertionContent
|
|
868
|
+
};
|
|
869
|
+
},
|
|
738
870
|
verifySignatureSoap(xml, opts) {
|
|
739
871
|
const { dom } = getContext();
|
|
740
872
|
const doc = dom.parseFromString(xml, 'application/xml');
|
|
@@ -930,7 +1062,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
930
1062
|
verifier.update(octetString);
|
|
931
1063
|
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
932
1064
|
return isValid
|
|
933
|
-
|
|
1065
|
+
|
|
934
1066
|
},*/
|
|
935
1067
|
/**
|
|
936
1068
|
* @desc Verifies message signature
|
|
@@ -1063,6 +1195,103 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
1063
1195
|
});
|
|
1064
1196
|
});
|
|
1065
1197
|
},
|
|
1198
|
+
/**
|
|
1199
|
+
* 同步版本的断言解密函数
|
|
1200
|
+
*/
|
|
1201
|
+
/**
|
|
1202
|
+
* 同步版本的断言解密函数,支持解密后验证断言签名
|
|
1203
|
+
*/
|
|
1204
|
+
decryptAssertionSync(here, entireXML, opts) {
|
|
1205
|
+
const hereSetting = here.entitySetting;
|
|
1206
|
+
const { dom } = getContext();
|
|
1207
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
1208
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1209
|
+
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
1210
|
+
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
1211
|
+
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
1212
|
+
}
|
|
1213
|
+
if (encryptedAssertions.length > 1) {
|
|
1214
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
1215
|
+
}
|
|
1216
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
1217
|
+
let decryptedResult = null;
|
|
1218
|
+
// 使用同步方式处理解密
|
|
1219
|
+
xmlenc.decrypt(encAssertionNode.toString(), {
|
|
1220
|
+
key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass),
|
|
1221
|
+
}, (err, res) => {
|
|
1222
|
+
if (err) {
|
|
1223
|
+
throw new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION');
|
|
1224
|
+
}
|
|
1225
|
+
if (!res) {
|
|
1226
|
+
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
1227
|
+
}
|
|
1228
|
+
decryptedResult = res;
|
|
1229
|
+
});
|
|
1230
|
+
if (!decryptedResult) {
|
|
1231
|
+
throw new Error('ERR_UNDEFINED_DECRYPTED_ASSERTION');
|
|
1232
|
+
}
|
|
1233
|
+
// 解密完成后,检查解密后的断言是否还有签名需要验证
|
|
1234
|
+
const decryptedAssertionDoc = dom.parseFromString(decryptedResult, 'application/xml');
|
|
1235
|
+
let AssertionSignatureStatus = false;
|
|
1236
|
+
// 检查解密后的断言是否有签名
|
|
1237
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1238
|
+
const assertionSignatureNode = select("/*[local-name(.)='Assertion']/*[local-name(.)='Signature']", decryptedAssertionDoc);
|
|
1239
|
+
if (assertionSignatureNode.length > 0 && opts) {
|
|
1240
|
+
// 解密后的断言有签名,需要验证
|
|
1241
|
+
const signatureNode = assertionSignatureNode[0];
|
|
1242
|
+
const sig = new SignedXml();
|
|
1243
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
1244
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
1245
|
+
}
|
|
1246
|
+
if (opts.keyFile) {
|
|
1247
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
1248
|
+
}
|
|
1249
|
+
else if (opts.metadata) {
|
|
1250
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1251
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
1252
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
1253
|
+
if (Array.isArray(metadataCert)) {
|
|
1254
|
+
metadataCert = flattenDeep(metadataCert);
|
|
1255
|
+
}
|
|
1256
|
+
else if (typeof metadataCert === 'string') {
|
|
1257
|
+
metadataCert = [metadataCert];
|
|
1258
|
+
}
|
|
1259
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
1260
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
1261
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
1262
|
+
}
|
|
1263
|
+
if (certificateNode.length !== 0) {
|
|
1264
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
1265
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
1266
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
1267
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
1268
|
+
}
|
|
1269
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
1276
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1277
|
+
sig.loadSignature(signatureNode);
|
|
1278
|
+
// 验证解密后断言的签名
|
|
1279
|
+
const assertionDocForVerification = dom.parseFromString(decryptedResult, 'application/xml');
|
|
1280
|
+
const assertionValid = sig.checkSignature(assertionDocForVerification.toString());
|
|
1281
|
+
AssertionSignatureStatus = assertionValid;
|
|
1282
|
+
if (!assertionValid) {
|
|
1283
|
+
throw new Error('ERR_FAILED_TO_VERIFY_DECRYPTED_ASSERTION_SIGNATURE');
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
// 将解密后的断言替换原始文档中的加密断言
|
|
1287
|
+
const rawAssertionDoc = dom.parseFromString(decryptedResult, 'application/xml');
|
|
1288
|
+
// @ts-ignore
|
|
1289
|
+
doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode);
|
|
1290
|
+
return [doc.toString(), decryptedResult, {
|
|
1291
|
+
isAssertionSigned: !!(assertionSignatureNode.length > 0 && opts),
|
|
1292
|
+
AssertionSignatureStatus: AssertionSignatureStatus
|
|
1293
|
+
}];
|
|
1294
|
+
},
|
|
1066
1295
|
/**
|
|
1067
1296
|
* 解密 SOAP 响应中的加密断言
|
|
1068
1297
|
* @param self 当前实体(SP 或 IdP)
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binding-artifact.d.ts","sourceRoot":"","sources":["../../src/binding-artifact.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAMhD,OAAO,EACH,2BAA2B,IAAI,gBAAgB,EAC/C,0BAA0B,IAAI,eAAe,EAChD,MAAM,YAAY,CAAC;AAwCpB;;;;;GAKG;AACH,iBAAS,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,GAAG,GAAG,CA+FlI;AAED;;;;;;;;GAQG;AACH,iBAAe,iBAAiB,CAAC,WAAW,EAAE,GAAG,YAAK,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,EAAE,oBAAoB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,EAAE,eAAe,GAAE,OAAe,EAAE,kBAAkB,UAAK,GAAG,OAAO,CAAC,cAAc,CAAC,CAqIpO;AAED,iBAAe,wBAAwB,CAAC,MAAM,EAAE;IAC5C,GAAG,EAAE,gBAAgB,CAAC;IACtB,EAAE,EAAE,eAAe,CAAC;IACpB,GAAG,EAAE,MAAM,CAAA;CACd;;;GAqDA;AAED,iBAAe,yBAAyB,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,gBAAgB,CAAC;IAAC,EAAE,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;;;
|
|
1
|
+
{"version":3,"file":"binding-artifact.d.ts","sourceRoot":"","sources":["../../src/binding-artifact.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAMhD,OAAO,EACH,2BAA2B,IAAI,gBAAgB,EAC/C,0BAA0B,IAAI,eAAe,EAChD,MAAM,YAAY,CAAC;AAwCpB;;;;;GAKG;AACH,iBAAS,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,GAAG,GAAG,CA+FlI;AAED;;;;;;;;GAQG;AACH,iBAAe,iBAAiB,CAAC,WAAW,EAAE,GAAG,YAAK,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,EAAE,oBAAoB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,EAAE,eAAe,GAAE,OAAe,EAAE,kBAAkB,UAAK,GAAG,OAAO,CAAC,cAAc,CAAC,CAqIpO;AAED,iBAAe,wBAAwB,CAAC,MAAM,EAAE;IAC5C,GAAG,EAAE,gBAAgB,CAAC;IACtB,EAAE,EAAE,eAAe,CAAC;IACpB,GAAG,EAAE,MAAM,CAAA;CACd;;;GAqDA;AAED,iBAAe,yBAAyB,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,gBAAgB,CAAC;IAAC,EAAE,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;;;GAqQ3G;AAED,QAAA,MAAM,mBAAmB;;;;;CAOxB,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
package/types/src/flow.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../../src/flow.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,UAAU;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,GAAG,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../../src/flow.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,UAAU;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,GAAG,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAgsBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BhG;AAED,wBAAgB,IAAI,CAAC,OAAO,KAAA,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BjD"}
|
package/types/src/libsaml.d.ts
CHANGED
|
@@ -164,6 +164,19 @@ declare const _default: {
|
|
|
164
164
|
* @return {string} base64 encoded string
|
|
165
165
|
*/
|
|
166
166
|
constructSAMLSignature(opts: SignatureConstructor): string;
|
|
167
|
+
validateCertificate(certificateBase64: string, expectedIssuer?: string): {
|
|
168
|
+
isValid: boolean;
|
|
169
|
+
subject: string;
|
|
170
|
+
issuer: string;
|
|
171
|
+
publicKey: import("crypto").KeyObject;
|
|
172
|
+
error?: undefined;
|
|
173
|
+
} | {
|
|
174
|
+
isValid: boolean;
|
|
175
|
+
error: any;
|
|
176
|
+
subject?: undefined;
|
|
177
|
+
issuer?: undefined;
|
|
178
|
+
publicKey?: undefined;
|
|
179
|
+
};
|
|
167
180
|
/**
|
|
168
181
|
* @desc Verify the XML signature
|
|
169
182
|
* @param {string} xml xml
|
|
@@ -172,7 +185,33 @@ declare const _default: {
|
|
|
172
185
|
* - The first element is `true` if the signature is valid, `false` otherwise.
|
|
173
186
|
* - The second element is the cryptographically authenticated assertion node as a string, or `null` if not found.
|
|
174
187
|
*/
|
|
175
|
-
|
|
188
|
+
verifySignature1(xml: string, opts: SignatureVerifierOptions): (boolean | null)[] | (string | boolean)[];
|
|
189
|
+
/**
|
|
190
|
+
* 改进的SAML签名验证函数,支持多种签名和加密组合场景
|
|
191
|
+
* @param xml SAML XML内容
|
|
192
|
+
* @param opts 验证选项
|
|
193
|
+
* @param self
|
|
194
|
+
* @returns 验证结果对象
|
|
195
|
+
*/
|
|
196
|
+
/**
|
|
197
|
+
* 改进的SAML签名验证函数,支持多种签名和加密组合场景
|
|
198
|
+
* @param xml SAML XML内容
|
|
199
|
+
* @param opts 验证选项
|
|
200
|
+
* @param self
|
|
201
|
+
* @returns 验证结果对象
|
|
202
|
+
*/
|
|
203
|
+
verifySignature(xml: string, opts: SignatureVerifierOptions, self: any): {
|
|
204
|
+
isMessageSigned: boolean;
|
|
205
|
+
MessageSignatureStatus: boolean;
|
|
206
|
+
isAssertionSigned: boolean;
|
|
207
|
+
AssertionSignatureStatus: boolean;
|
|
208
|
+
encrypted: boolean;
|
|
209
|
+
decrypted: boolean;
|
|
210
|
+
type: "AuthnRequest" | "LogoutRequest" | "Response" | "LogoutResponse" | "Unknown";
|
|
211
|
+
status: boolean;
|
|
212
|
+
samlContent: string;
|
|
213
|
+
assertionContent: null;
|
|
214
|
+
};
|
|
176
215
|
verifySignatureSoap(xml: string, opts: SignatureVerifierOptions): (boolean | null)[] | (string | boolean)[];
|
|
177
216
|
/**
|
|
178
217
|
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
|
|
@@ -226,6 +265,16 @@ declare const _default: {
|
|
|
226
265
|
* @return {function} a promise to get back the entire xml with decrypted assertion
|
|
227
266
|
*/
|
|
228
267
|
decryptAssertion(here: any, entireXML: string): Promise<[string, any]>;
|
|
268
|
+
/**
|
|
269
|
+
* 同步版本的断言解密函数
|
|
270
|
+
*/
|
|
271
|
+
/**
|
|
272
|
+
* 同步版本的断言解密函数,支持解密后验证断言签名
|
|
273
|
+
*/
|
|
274
|
+
decryptAssertionSync(here: any, entireXML: string, opts?: SignatureVerifierOptions): (string | {
|
|
275
|
+
isAssertionSigned: boolean;
|
|
276
|
+
AssertionSignatureStatus: boolean;
|
|
277
|
+
})[];
|
|
229
278
|
/**
|
|
230
279
|
* 解密 SOAP 响应中的加密断言
|
|
231
280
|
* @param self 当前实体(SP 或 IdP)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"libsaml.d.ts","sourceRoot":"","sources":["../../src/libsaml.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"libsaml.d.ts","sourceRoot":"","sources":["../../src/libsaml.ts"],"names":[],"mappings":"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"}
|