samlesa 2.17.3 → 2.18.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.
Potentially problematic release.
This version of samlesa might be problematic. Click here for more details.
- package/build/src/binding-artifact.js +24 -14
- package/build/src/flow.js +169 -27
- package/build/src/libsaml.js +454 -213
- package/package.json +77 -77
- 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
- package/types/api.d.ts +0 -15
- package/types/api.d.ts.map +0 -1
- package/types/binding-post.d.ts +0 -48
- package/types/binding-post.d.ts.map +0 -1
- package/types/binding-redirect.d.ts +0 -54
- package/types/binding-redirect.d.ts.map +0 -1
- package/types/binding-simplesign.d.ts +0 -41
- package/types/binding-simplesign.d.ts.map +0 -1
- package/types/entity-idp.d.ts +0 -38
- package/types/entity-idp.d.ts.map +0 -1
- package/types/entity-sp.d.ts +0 -38
- package/types/entity-sp.d.ts.map +0 -1
- package/types/entity.d.ts +0 -100
- package/types/entity.d.ts.map +0 -1
- package/types/extractor.d.ts +0 -26
- package/types/extractor.d.ts.map +0 -1
- package/types/flow.d.ts +0 -7
- package/types/flow.d.ts.map +0 -1
- package/types/libsaml.d.ts +0 -208
- package/types/libsaml.d.ts.map +0 -1
- package/types/metadata-idp.d.ts +0 -25
- package/types/metadata-idp.d.ts.map +0 -1
- package/types/metadata-sp.d.ts +0 -37
- package/types/metadata-sp.d.ts.map +0 -1
- package/types/metadata.d.ts +0 -58
- package/types/metadata.d.ts.map +0 -1
- package/types/types.d.ts +0 -128
- package/types/types.d.ts.map +0 -1
- package/types/urn.d.ts +0 -195
- package/types/urn.d.ts.map +0 -1
- package/types/utility.d.ts +0 -133
- package/types/utility.d.ts.map +0 -1
- package/types/validator.d.ts +0 -4
- package/types/validator.d.ts.map +0 -1
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,327 @@ 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
|
+
// 使用解密后的文档验证最外层签名.默认采用的都是采用的先签名后加密的顺序,对应sp应该先解密 然后验证签名。 如果解密后验证外层签名失败有可能是先加密后签名,此时sp应该直接验证没解密的外层签名
|
|
709
|
+
MessageSignatureStatus = sig.checkSignature(decryptedDoc.toString());
|
|
710
|
+
console.log(MessageSignatureStatus);
|
|
711
|
+
console.log("验证MessageSignatureStatus==========================");
|
|
712
|
+
if (!MessageSignatureStatus) {
|
|
713
|
+
/** 签名验证失败 再直接验证外层*/
|
|
714
|
+
throw new Error('ERR_FAILED_TO_VERIFY_MESSAGE_SIGNATURE_AFTER_DECRYPTION');
|
|
715
|
+
let MessageSignatureStatus2 = sig.checkSignature(xml);
|
|
716
|
+
if (!MessageSignatureStatus2) {
|
|
717
|
+
throw new Error('ERR_FAILED_TO_VERIFY_MESSAGE_SIGNATURE_AFTER_DECRYPTION');
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
MessageSignatureStatus = MessageSignatureStatus2;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// 3. 验证解密后断言的签名(如果存在)
|
|
724
|
+
if (isAssertionSigned && AssertionSignatureStatus) {
|
|
725
|
+
/* console.log("断言签名验证已通过");*/
|
|
726
|
+
}
|
|
727
|
+
else if (isAssertionSigned && !AssertionSignatureStatus) {
|
|
728
|
+
throw new Error('ERR_FAILED_TO_VERIFY_ASSERTION_SIGNATURE_AFTER_DECRYPTION');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
throw err;
|
|
599
733
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
734
|
+
}
|
|
735
|
+
// 处理最外层有签名但断言未加密的情况
|
|
736
|
+
else if (isMessageSigned && !encrypted) {
|
|
737
|
+
const signatureNode = messageSignatureNode[0];
|
|
738
|
+
const sig = new SignedXml();
|
|
739
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
740
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
606
741
|
}
|
|
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();
|
|
742
|
+
if (opts.keyFile) {
|
|
743
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
632
744
|
}
|
|
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');
|
|
745
|
+
else if (opts.metadata) {
|
|
746
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
747
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
748
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
749
|
+
if (Array.isArray(metadataCert)) {
|
|
750
|
+
metadataCert = flattenDeep(metadataCert);
|
|
751
|
+
}
|
|
752
|
+
else if (typeof metadataCert === 'string') {
|
|
753
|
+
metadataCert = [metadataCert];
|
|
754
|
+
}
|
|
755
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
756
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
757
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
758
|
+
}
|
|
759
|
+
if (certificateNode.length !== 0) {
|
|
760
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
761
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
762
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
763
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
764
|
+
}
|
|
765
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
769
|
+
}
|
|
660
770
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
// 处理已验证的签名
|
|
664
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
665
|
-
if (rootNode.localName === 'ArtifactResponse') {
|
|
666
|
-
// 在 ArtifactResponse 中查找 Response
|
|
771
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
667
772
|
// @ts-expect-error misssing Node properties are not needed
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
) as Element[];
|
|
673
|
-
|
|
674
|
-
if (responseNodes.length === 0) {
|
|
675
|
-
continue;
|
|
773
|
+
sig.loadSignature(signatureNode);
|
|
774
|
+
MessageSignatureStatus = sig.checkSignature(doc.toString());
|
|
775
|
+
if (!MessageSignatureStatus) {
|
|
776
|
+
throw new Error('ERR_FAILED_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
676
777
|
}
|
|
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];
|
|
778
|
+
}
|
|
779
|
+
// 验证断言级签名(如果存在且未加密)
|
|
780
|
+
if (isAssertionSigned && !encrypted) {
|
|
781
|
+
const signatureNode = assertionSignatureNode[0];
|
|
782
|
+
const sig = new SignedXml();
|
|
783
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
784
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
693
785
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
return [true, assertions[0].toString(), false];
|
|
786
|
+
if (opts.keyFile) {
|
|
787
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
697
788
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
789
|
+
else if (opts.metadata) {
|
|
790
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
791
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
792
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
793
|
+
if (Array.isArray(metadataCert)) {
|
|
794
|
+
metadataCert = flattenDeep(metadataCert);
|
|
795
|
+
}
|
|
796
|
+
else if (typeof metadataCert === 'string') {
|
|
797
|
+
metadataCert = [metadataCert];
|
|
798
|
+
}
|
|
799
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
800
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
801
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
802
|
+
}
|
|
803
|
+
if (certificateNode.length !== 0) {
|
|
804
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
805
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
806
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
807
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
808
|
+
}
|
|
809
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
702
816
|
// @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[];
|
|
817
|
+
sig.loadSignature(signatureNode);
|
|
818
|
+
// 为断言签名验证,我们需要从根文档中获取断言部分
|
|
708
819
|
// @ts-expect-error misssing Node properties are not needed
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
820
|
+
const assertionNode = select("/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']", doc)[0];
|
|
821
|
+
if (assertionNode) {
|
|
822
|
+
const assertionDoc = dom.parseFromString(assertionNode.toString(), 'application/xml');
|
|
823
|
+
AssertionSignatureStatus = sig.checkSignature(assertionDoc.toString());
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
AssertionSignatureStatus = false;
|
|
827
|
+
}
|
|
828
|
+
if (!AssertionSignatureStatus) {
|
|
829
|
+
throw new Error('ERR_FAILED_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
717
830
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
831
|
+
}
|
|
832
|
+
// 处理仅加密断言的情况(无消息签名)
|
|
833
|
+
if (encrypted && !isMessageSigned) {
|
|
834
|
+
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
835
|
+
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
836
|
+
}
|
|
837
|
+
if (encryptedAssertions.length > 1) {
|
|
838
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
839
|
+
}
|
|
840
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
841
|
+
// 解密断言
|
|
842
|
+
try {
|
|
843
|
+
const result = this.decryptAssertionSync(self, xml, opts);
|
|
844
|
+
samlContent = result[0];
|
|
845
|
+
assertionContent = result[1];
|
|
846
|
+
decrypted = true;
|
|
847
|
+
AssertionSignatureStatus = result?.[2]?.AssertionSignatureStatus;
|
|
848
|
+
isAssertionSigned = result?.[2]?.isAssertionSigned;
|
|
849
|
+
}
|
|
850
|
+
catch (err) {
|
|
851
|
+
throw new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION');
|
|
721
852
|
}
|
|
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
853
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
854
|
+
else if (!encrypted && (isMessageSigned || isAssertionSigned)) {
|
|
855
|
+
// 如果没有加密但有签名,提取断言内容
|
|
856
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
857
|
+
const assertions = select("/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']", doc);
|
|
858
|
+
if (assertions.length > 0) {
|
|
859
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
860
|
+
assertionContent = assertions[0].toString();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// 检查整体状态
|
|
864
|
+
status = (!isMessageSigned || MessageSignatureStatus) &&
|
|
865
|
+
(!isAssertionSigned || AssertionSignatureStatus) &&
|
|
866
|
+
(!encrypted || decrypted);
|
|
867
|
+
return {
|
|
868
|
+
isMessageSigned,
|
|
869
|
+
MessageSignatureStatus,
|
|
870
|
+
isAssertionSigned,
|
|
871
|
+
AssertionSignatureStatus,
|
|
872
|
+
encrypted,
|
|
873
|
+
decrypted,
|
|
874
|
+
type, // 添加类型字段
|
|
875
|
+
status,
|
|
876
|
+
samlContent,
|
|
877
|
+
assertionContent
|
|
878
|
+
};
|
|
879
|
+
},
|
|
738
880
|
verifySignatureSoap(xml, opts) {
|
|
739
881
|
const { dom } = getContext();
|
|
740
882
|
const doc = dom.parseFromString(xml, 'application/xml');
|
|
@@ -930,7 +1072,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
930
1072
|
verifier.update(octetString);
|
|
931
1073
|
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
932
1074
|
return isValid
|
|
933
|
-
|
|
1075
|
+
|
|
934
1076
|
},*/
|
|
935
1077
|
/**
|
|
936
1078
|
* @desc Verifies message signature
|
|
@@ -1063,6 +1205,105 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
1063
1205
|
});
|
|
1064
1206
|
});
|
|
1065
1207
|
},
|
|
1208
|
+
/**
|
|
1209
|
+
* 同步版本的断言解密函数
|
|
1210
|
+
*/
|
|
1211
|
+
/**
|
|
1212
|
+
* 同步版本的断言解密函数,支持解密后验证断言签名
|
|
1213
|
+
*/
|
|
1214
|
+
decryptAssertionSync(here, entireXML, opts) {
|
|
1215
|
+
const hereSetting = here.entitySetting;
|
|
1216
|
+
const { dom } = getContext();
|
|
1217
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
1218
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1219
|
+
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
1220
|
+
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
1221
|
+
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
1222
|
+
}
|
|
1223
|
+
if (encryptedAssertions.length > 1) {
|
|
1224
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
1225
|
+
}
|
|
1226
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
1227
|
+
let decryptedResult = null;
|
|
1228
|
+
// 使用同步方式处理解密
|
|
1229
|
+
xmlenc.decrypt(encAssertionNode.toString(), {
|
|
1230
|
+
key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass),
|
|
1231
|
+
}, (err, res) => {
|
|
1232
|
+
if (err) {
|
|
1233
|
+
throw new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION');
|
|
1234
|
+
}
|
|
1235
|
+
if (!res) {
|
|
1236
|
+
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
1237
|
+
}
|
|
1238
|
+
decryptedResult = res;
|
|
1239
|
+
});
|
|
1240
|
+
if (!decryptedResult) {
|
|
1241
|
+
throw new Error('ERR_UNDEFINED_DECRYPTED_ASSERTION');
|
|
1242
|
+
}
|
|
1243
|
+
// 解密完成后,检查解密后的断言是否还有签名需要验证
|
|
1244
|
+
const decryptedAssertionDoc = dom.parseFromString(decryptedResult, 'application/xml');
|
|
1245
|
+
let AssertionSignatureStatus = false;
|
|
1246
|
+
// 检查解密后的断言是否有签名
|
|
1247
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1248
|
+
const assertionSignatureNode = select("/*[local-name(.)='Assertion']/*[local-name(.)='Signature']", decryptedAssertionDoc);
|
|
1249
|
+
if (assertionSignatureNode.length > 0 && opts) {
|
|
1250
|
+
// 解密后的断言有签名,需要验证
|
|
1251
|
+
const signatureNode = assertionSignatureNode[0];
|
|
1252
|
+
const sig = new SignedXml();
|
|
1253
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
1254
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
1255
|
+
}
|
|
1256
|
+
if (opts.keyFile) {
|
|
1257
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
1258
|
+
}
|
|
1259
|
+
else if (opts.metadata) {
|
|
1260
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1261
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
1262
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
1263
|
+
if (Array.isArray(metadataCert)) {
|
|
1264
|
+
metadataCert = flattenDeep(metadataCert);
|
|
1265
|
+
}
|
|
1266
|
+
else if (typeof metadataCert === 'string') {
|
|
1267
|
+
metadataCert = [metadataCert];
|
|
1268
|
+
}
|
|
1269
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
1270
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
1271
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
1272
|
+
}
|
|
1273
|
+
if (certificateNode.length !== 0) {
|
|
1274
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
1275
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
1276
|
+
if (metadataCert.length >= 1 && !metadataCert.find((cert) => cert.trim() === x509Certificate.trim())) {
|
|
1277
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
1278
|
+
}
|
|
1279
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
1286
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
1287
|
+
sig.loadSignature(signatureNode);
|
|
1288
|
+
// 验证解密后断言的签名
|
|
1289
|
+
const assertionDocForVerification = dom.parseFromString(decryptedResult, 'application/xml');
|
|
1290
|
+
const assertionValid = sig.checkSignature(assertionDocForVerification.toString());
|
|
1291
|
+
AssertionSignatureStatus = assertionValid;
|
|
1292
|
+
console.log(AssertionSignatureStatus);
|
|
1293
|
+
console.log("验证通过了====");
|
|
1294
|
+
if (!assertionValid) {
|
|
1295
|
+
throw new Error('ERR_FAILED_TO_VERIFY_DECRYPTED_ASSERTION_SIGNATURE');
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// 将解密后的断言替换原始文档中的加密断言
|
|
1299
|
+
const rawAssertionDoc = dom.parseFromString(decryptedResult, 'application/xml');
|
|
1300
|
+
// @ts-ignore
|
|
1301
|
+
doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode);
|
|
1302
|
+
return [doc.toString(), decryptedResult, {
|
|
1303
|
+
isAssertionSigned: !!(assertionSignatureNode.length > 0 && opts),
|
|
1304
|
+
AssertionSignatureStatus: AssertionSignatureStatus
|
|
1305
|
+
}];
|
|
1306
|
+
},
|
|
1066
1307
|
/**
|
|
1067
1308
|
* 解密 SOAP 响应中的加密断言
|
|
1068
1309
|
* @param self 当前实体(SP 或 IdP)
|