samlesa 2.17.0 → 2.17.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/index.js +2 -1
- package/build/src/binding-artifact.js +330 -146
- package/build/src/entity-sp.js +21 -94
- package/build/src/extractor.js +32 -0
- package/build/src/flow.js +23 -112
- package/build/src/libsaml.js +325 -127
- package/build/src/libsamlSoap.js +115 -0
- package/build/src/schemaValidator.js +1 -5
- package/build/src/soap.js +123 -3
- package/package.json +77 -75
- package/types/api.d.ts +15 -0
- package/types/api.d.ts.map +1 -0
- package/types/binding-post.d.ts +48 -0
- package/types/binding-post.d.ts.map +1 -0
- package/types/binding-redirect.d.ts +54 -0
- package/types/binding-redirect.d.ts.map +1 -0
- package/types/binding-simplesign.d.ts +41 -0
- package/types/binding-simplesign.d.ts.map +1 -0
- package/types/entity-idp.d.ts +38 -0
- package/types/entity-idp.d.ts.map +1 -0
- package/types/entity-sp.d.ts +38 -0
- package/types/entity-sp.d.ts.map +1 -0
- package/types/entity.d.ts +100 -0
- package/types/entity.d.ts.map +1 -0
- package/types/extractor.d.ts +26 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/flow.d.ts +7 -0
- package/types/flow.d.ts.map +1 -0
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/libsaml.d.ts +208 -0
- package/types/libsaml.d.ts.map +1 -0
- package/types/metadata-idp.d.ts +25 -0
- package/types/metadata-idp.d.ts.map +1 -0
- package/types/metadata-sp.d.ts +37 -0
- package/types/metadata-sp.d.ts.map +1 -0
- package/types/metadata.d.ts +58 -0
- package/types/metadata.d.ts.map +1 -0
- package/types/src/binding-artifact.d.ts +24 -29
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +13 -24
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/extractor.d.ts +22 -0
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts +1 -0
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +4 -3
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/libsamlSoap.d.ts +7 -0
- package/types/src/libsamlSoap.d.ts.map +1 -0
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/soap.d.ts +33 -0
- package/types/src/soap.d.ts.map +1 -1
- package/types/src/validator.d.ts.map +1 -1
- package/types/types.d.ts +128 -0
- package/types/types.d.ts.map +1 -0
- package/types/urn.d.ts +195 -0
- package/types/urn.d.ts.map +1 -0
- package/types/utility.d.ts +133 -0
- package/types/utility.d.ts.map +1 -0
- package/types/validator.d.ts +4 -0
- package/types/validator.d.ts.map +1 -0
- package/build/src/schema/XMLSchema.dtd +0 -402
- package/build/src/schema/datatypes.dtd +0 -203
package/build/src/libsaml.js
CHANGED
|
@@ -69,7 +69,18 @@ const libSaml = () => {
|
|
|
69
69
|
context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><saml2p:ArtifactResolve xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml2:Issuer>{Issuer}</saml2:Issuer><saml2p:Artifact>{Art}</saml2p:Artifact></saml2p:ArtifactResolve></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
|
|
70
70
|
};
|
|
71
71
|
const defaultArtAuthnRequestTemplate = {
|
|
72
|
-
context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header></SOAP-ENV:Header><samlp:ArtifactResponse xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}" InResponseTo="{InResponseTo}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>{AuthnRequest}</samlp:ArtifactResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
|
|
72
|
+
context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header></SOAP-ENV:Header><SOAP-ENV:Body><samlp:ArtifactResponse xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}" InResponseTo="{InResponseTo}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>{AuthnRequest}</samlp:ArtifactResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
|
|
73
|
+
};
|
|
74
|
+
const defaultSoapResponseFailTemplate = {
|
|
75
|
+
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
|
+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
78
|
+
InResponseTo="{InResponseTo}" Version="2.0"
|
|
79
|
+
IssueInstant="{IssueInstant}">
|
|
80
|
+
<saml:Issuer>{Issuer}</saml:Issuer>
|
|
81
|
+
<samlp:Status>
|
|
82
|
+
<samlp:StatusCode Value="{StatusCode}"/>
|
|
83
|
+
</samlp:Status>{Response}</samlp:ArtifactResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
|
|
73
84
|
};
|
|
74
85
|
/**
|
|
75
86
|
* @desc Default AttributeStatement template
|
|
@@ -137,7 +148,6 @@ const libSaml = () => {
|
|
|
137
148
|
}
|
|
138
149
|
catch (inflateError) {
|
|
139
150
|
// 4. 解压失败,尝试直接解析为未压缩的XML
|
|
140
|
-
console.log("解压失败---------------------");
|
|
141
151
|
try {
|
|
142
152
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
143
153
|
xml = atob(base64Encoded);
|
|
@@ -211,6 +221,7 @@ const libSaml = () => {
|
|
|
211
221
|
defaultArtAuthnRequestTemplate,
|
|
212
222
|
defaultArtifactResolveTemplate,
|
|
213
223
|
defaultLoginResponseTemplate,
|
|
224
|
+
defaultSoapResponseFailTemplate,
|
|
214
225
|
defaultAttributeStatementTemplate,
|
|
215
226
|
defaultAttributeTemplate,
|
|
216
227
|
defaultLogoutRequestTemplate,
|
|
@@ -498,55 +509,292 @@ const libSaml = () => {
|
|
|
498
509
|
return [false, null, false, true]; // return encryptedAssert
|
|
499
510
|
/* throw new Error('ERR_ZERO_SIGNATURE');*/
|
|
500
511
|
},
|
|
501
|
-
verifySignatureSoap(xml, opts) {
|
|
502
|
-
const {
|
|
512
|
+
/* verifySignatureSoap(xml: string, opts: SignatureVerifierOptions & { isAssertion?: boolean }) {
|
|
513
|
+
const {dom} = getContext();
|
|
503
514
|
const doc = dom.parseFromString(xml, 'application/xml');
|
|
504
515
|
const docParser = new DOMParser();
|
|
505
|
-
|
|
516
|
+
|
|
517
|
+
let selection: any = [];
|
|
518
|
+
|
|
506
519
|
if (opts.isAssertion) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
520
|
+
// 断言模式下的专用逻辑
|
|
521
|
+
const assertionSignatureXpath = "./!*[local-name()='Signature']";
|
|
522
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
523
|
+
const signatureNode = select(assertionSignatureXpath, doc.documentElement);
|
|
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) {
|
|
563
|
+
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);
|
|
515
572
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
573
|
+
|
|
574
|
+
for (const signatureNode of selection) {
|
|
575
|
+
const sig = new SignedXml();
|
|
576
|
+
let verified = false;
|
|
577
|
+
|
|
578
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm!;
|
|
579
|
+
|
|
580
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
581
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (opts.keyFile) {
|
|
585
|
+
sig.publicCert = fs.readFileSync(opts.keyFile, 'utf-8');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (opts.metadata) {
|
|
589
|
+
const certificateNodes = select(".//!*[local-name(.)='X509Certificate']", signatureNode) as any[];
|
|
590
|
+
|
|
591
|
+
// 获取元数据中的证书
|
|
592
|
+
let metadataCert: any = opts.metadata.getX509Certificate(certUse.signing);
|
|
593
|
+
|
|
594
|
+
// 规范化元数据证书
|
|
595
|
+
if (Array.isArray(metadataCert)) {
|
|
596
|
+
metadataCert = flattenDeep(metadataCert);
|
|
597
|
+
} else if (typeof metadataCert === 'string') {
|
|
598
|
+
metadataCert = [metadataCert];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
602
|
+
|
|
603
|
+
// 检查证书可用性
|
|
604
|
+
if (certificateNodes.length === 0 && metadataCert.length === 0) {
|
|
605
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
606
|
+
}
|
|
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();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 加载签名
|
|
636
|
+
sig.loadSignature(signatureNode);
|
|
637
|
+
// 使用原始 XML 进行验证
|
|
638
|
+
verified = sig.checkSignature(xml);
|
|
639
|
+
|
|
640
|
+
if (!verified) {
|
|
641
|
+
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 检查签名引用
|
|
645
|
+
if (!(sig.getSignedReferences().length >= 1)) {
|
|
646
|
+
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
650
|
+
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'application/xml');
|
|
651
|
+
const rootNode = verifiedDoc.documentElement;
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
// 断言模式专用返回逻辑
|
|
655
|
+
if (opts.isAssertion) {
|
|
656
|
+
if (rootNode?.localName === 'Assertion') {
|
|
657
|
+
return [true, rootNode.toString(), false];
|
|
658
|
+
} else {
|
|
659
|
+
throw new Error('ERR_INVALID_ASSERTION_SIGNATURE');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 处理已验证的签名
|
|
664
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
665
|
+
if (rootNode.localName === 'ArtifactResponse') {
|
|
666
|
+
// 在 ArtifactResponse 中查找 Response
|
|
534
667
|
// @ts-expect-error misssing Node properties are not needed
|
|
535
|
-
const
|
|
668
|
+
const responseNodes = select(
|
|
669
|
+
"./!*[local-name()='Response']",
|
|
670
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
671
|
+
rootNode
|
|
672
|
+
) as Element[];
|
|
673
|
+
|
|
674
|
+
if (responseNodes.length === 0) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const responseNode = responseNodes[0];
|
|
679
|
+
|
|
680
|
+
// 在 Response 中查找断言
|
|
681
|
+
const encryptedAssertions = select(
|
|
682
|
+
"./!*[local-name()='EncryptedAssertion']",
|
|
683
|
+
responseNode
|
|
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];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (assertions.length === 1) {
|
|
696
|
+
return [true, assertions[0].toString(), false];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// 直接处理 Response
|
|
700
|
+
|
|
701
|
+
else if (rootNode?.localName === 'Response') {
|
|
536
702
|
// @ts-expect-error misssing Node properties are not needed
|
|
537
|
-
const
|
|
703
|
+
const encryptedAssertions = select(
|
|
704
|
+
"./!*[local-name()='EncryptedAssertion']",
|
|
705
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
706
|
+
rootNode
|
|
707
|
+
) as Element[];
|
|
538
708
|
// @ts-expect-error misssing Node properties are not needed
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
709
|
+
const assertions = select(
|
|
710
|
+
"./!*[local-name()='Assertion']",
|
|
711
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
712
|
+
rootNode
|
|
713
|
+
) as Element[];
|
|
714
|
+
|
|
715
|
+
if (encryptedAssertions.length === 1) {
|
|
716
|
+
return [true, encryptedAssertions[0].toString(), true];
|
|
543
717
|
}
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
718
|
+
|
|
719
|
+
if (assertions.length === 1) {
|
|
720
|
+
return [true, assertions[0].toString(), false];
|
|
547
721
|
}
|
|
548
|
-
|
|
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
|
+
}
|
|
549
734
|
}
|
|
735
|
+
|
|
736
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
737
|
+
},*/
|
|
738
|
+
verifySignatureSoap(xml, opts) {
|
|
739
|
+
const { dom } = getContext();
|
|
740
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
741
|
+
const docParser = new DOMParser();
|
|
742
|
+
// 为 SOAP 消息定义 XPath
|
|
743
|
+
const artifactResolveXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
744
|
+
const artifactResponseXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
745
|
+
// 检测 ArtifactResolve 或 ArtifactResponse 的存在
|
|
746
|
+
// @ts-expect-error
|
|
747
|
+
const artifactResolveNodes = select(artifactResolveXpath, doc);
|
|
748
|
+
// @ts-expect-error
|
|
749
|
+
const artifactResponseNodes = select(artifactResponseXpath, doc);
|
|
750
|
+
// 根据消息类型选择合适的 XPath
|
|
751
|
+
let basePath = "";
|
|
752
|
+
if (artifactResolveNodes.length > 0) {
|
|
753
|
+
basePath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
754
|
+
}
|
|
755
|
+
else if (artifactResponseNodes.length > 0) {
|
|
756
|
+
basePath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
throw new Error('ERR_UNSUPPORTED_SOAP_MESSAGE_TYPE');
|
|
760
|
+
}
|
|
761
|
+
// 基于 SOAP 结构重新定义 XPath
|
|
762
|
+
const messageSignatureXpath = `${basePath}/*[local-name(.)='Signature']`;
|
|
763
|
+
const assertionSignatureXpath = `${basePath}/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Signature']`;
|
|
764
|
+
const wrappingElementsXPath = `${basePath}/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Subject']/*[local-name(.)='SubjectConfirmation']/*[local-name(.)='SubjectConfirmationData']//*[local-name(.)='Assertion' or local-name(.)='Signature']`;
|
|
765
|
+
const encryptedAssertionsXpath = `${basePath}/*[local-name(.)='Response']/*[local-name(.)='EncryptedAssertion']`;
|
|
766
|
+
// 包装攻击检测
|
|
767
|
+
// @ts-expect-error
|
|
768
|
+
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
769
|
+
if (wrappingElementNode.length !== 0) {
|
|
770
|
+
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
771
|
+
}
|
|
772
|
+
// @ts-expect-error
|
|
773
|
+
const encryptedAssertions = select(encryptedAssertionsXpath, doc);
|
|
774
|
+
// @ts-expect-error
|
|
775
|
+
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
776
|
+
// @ts-expect-error
|
|
777
|
+
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
778
|
+
let selection = [];
|
|
779
|
+
if (messageSignatureNode.length > 0) {
|
|
780
|
+
selection = selection.concat(messageSignatureNode);
|
|
781
|
+
}
|
|
782
|
+
if (assertionSignatureNode.length > 0) {
|
|
783
|
+
selection = selection.concat(assertionSignatureNode);
|
|
784
|
+
}
|
|
785
|
+
// 处理加密断言的情况
|
|
786
|
+
if (selection.length === 0) {
|
|
787
|
+
if (encryptedAssertions.length > 0) {
|
|
788
|
+
if (encryptedAssertions.length > 1) {
|
|
789
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
790
|
+
}
|
|
791
|
+
return [false, null, true, true];
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (selection.length === 0) {
|
|
795
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
796
|
+
}
|
|
797
|
+
// 尝试所有签名节点
|
|
550
798
|
for (const signatureNode of selection) {
|
|
551
799
|
const sig = new SignedXml();
|
|
552
800
|
let verified = false;
|
|
@@ -555,13 +803,12 @@ const libSaml = () => {
|
|
|
555
803
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
556
804
|
}
|
|
557
805
|
if (opts.keyFile) {
|
|
558
|
-
sig.publicCert = fs.readFileSync(opts.keyFile
|
|
806
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
559
807
|
}
|
|
560
808
|
if (opts.metadata) {
|
|
561
|
-
const
|
|
562
|
-
//
|
|
809
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
810
|
+
// 证书处理逻辑
|
|
563
811
|
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
564
|
-
// 规范化元数据证书
|
|
565
812
|
if (Array.isArray(metadataCert)) {
|
|
566
813
|
metadataCert = flattenDeep(metadataCert);
|
|
567
814
|
}
|
|
@@ -569,108 +816,59 @@ const libSaml = () => {
|
|
|
569
816
|
metadataCert = [metadataCert];
|
|
570
817
|
}
|
|
571
818
|
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
572
|
-
//
|
|
573
|
-
if (
|
|
819
|
+
// 没有证书的情况
|
|
820
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
574
821
|
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
575
822
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// 安全获取证书数据
|
|
579
|
-
let x509CertificateData = '';
|
|
580
|
-
if (certificateNodes[0].firstChild) {
|
|
581
|
-
x509CertificateData = certificateNodes[0].firstChild.data;
|
|
582
|
-
}
|
|
583
|
-
else if (certificateNodes[0].textContent) {
|
|
584
|
-
x509CertificateData = certificateNodes[0].textContent;
|
|
585
|
-
}
|
|
823
|
+
if (certificateNode.length !== 0) {
|
|
824
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
586
825
|
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
587
|
-
|
|
588
|
-
if (metadataCert.length >= 1 &&
|
|
589
|
-
!metadataCert.find(cert => cert.trim() === x509Certificate.trim())) {
|
|
826
|
+
if (metadataCert.length >= 1 && !metadataCert.includes(x509Certificate)) {
|
|
590
827
|
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
591
828
|
}
|
|
592
829
|
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
593
830
|
}
|
|
594
831
|
else {
|
|
595
|
-
// 使用元数据中的第一个证书
|
|
596
832
|
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
597
833
|
}
|
|
598
834
|
}
|
|
599
|
-
// 加载签名
|
|
600
835
|
sig.loadSignature(signatureNode);
|
|
601
|
-
// 使用原始
|
|
602
|
-
verified = sig.checkSignature(xml);
|
|
836
|
+
verified = sig.checkSignature(xml); // 使用原始XML验证
|
|
603
837
|
if (!verified) {
|
|
604
838
|
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
605
839
|
}
|
|
606
|
-
|
|
607
|
-
if (!(sig.getSignedReferences().length >= 1)) {
|
|
840
|
+
if (sig.getSignedReferences().length < 1) {
|
|
608
841
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
609
842
|
}
|
|
610
843
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const assertions = select("./*[local-name()='Assertion']", responseNode);
|
|
637
|
-
if (encryptedAssertions.length === 1) {
|
|
638
|
-
return [true, encryptedAssertions[0].toString(), true];
|
|
639
|
-
}
|
|
640
|
-
if (assertions.length === 1) {
|
|
641
|
-
return [true, assertions[0].toString(), false];
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
// 直接处理 Response
|
|
645
|
-
else if (rootNode?.localName === 'Response') {
|
|
646
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
647
|
-
const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']",
|
|
648
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
649
|
-
rootNode);
|
|
650
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
651
|
-
const assertions = select("./*[local-name()='Assertion']",
|
|
652
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
653
|
-
rootNode);
|
|
654
|
-
if (encryptedAssertions.length === 1) {
|
|
655
|
-
return [true, encryptedAssertions[0].toString(), true];
|
|
656
|
-
}
|
|
657
|
-
if (assertions.length === 1) {
|
|
658
|
-
return [true, assertions[0].toString(), false];
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
// 直接处理 Assertion
|
|
662
|
-
else if (rootNode?.localName === 'Assertion') {
|
|
663
|
-
return [true, rootNode.toString(), false];
|
|
664
|
-
}
|
|
665
|
-
// 直接处理 EncryptedAssertion
|
|
666
|
-
else if (rootNode?.localName === 'EncryptedAssertion') {
|
|
667
|
-
return [true, rootNode.toString(), true];
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
console.warn("未知的根节点类型:", rootNode?.localName);
|
|
844
|
+
const rootNode = docParser.parseFromString(signedVerifiedXML, 'application/xml').documentElement;
|
|
845
|
+
// 处理签名的内容
|
|
846
|
+
switch (rootNode?.localName) {
|
|
847
|
+
case 'Response':
|
|
848
|
+
// @ts-expect-error
|
|
849
|
+
const encryptedAssert = select("./*[local-name()='EncryptedAssertion']", rootNode);
|
|
850
|
+
// @ts-expect-error
|
|
851
|
+
const assertions = select("./*[local-name()='Assertion']", rootNode);
|
|
852
|
+
if (encryptedAssert.length === 1) {
|
|
853
|
+
return [true, encryptedAssert[0].toString(), true, false];
|
|
854
|
+
}
|
|
855
|
+
if (assertions.length === 1) {
|
|
856
|
+
return [true, assertions[0].toString(), false, false];
|
|
857
|
+
}
|
|
858
|
+
return [true, null, false, true]; // 签名验证成功但未找到断言
|
|
859
|
+
case 'Assertion':
|
|
860
|
+
return [true, rootNode.toString(), false, false];
|
|
861
|
+
case 'EncryptedAssertion':
|
|
862
|
+
return [true, rootNode.toString(), true, false];
|
|
863
|
+
case 'ArtifactResolve':
|
|
864
|
+
case 'ArtifactResponse':
|
|
865
|
+
// 提取SOAP消息内部的实际内容
|
|
866
|
+
return [true, rootNode.toString(), false, false];
|
|
867
|
+
default:
|
|
868
|
+
return [true, null, false, true]; // 签名验证成功但未找到可识别的内容
|
|
671
869
|
}
|
|
672
870
|
}
|
|
673
|
-
|
|
871
|
+
return [false, null, encryptedAssertions.length > 0, false];
|
|
674
872
|
},
|
|
675
873
|
/**
|
|
676
874
|
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getContext } from "./api.js";
|
|
2
|
+
import { select } from "xpath";
|
|
3
|
+
import { SignedXml } from "xml-crypto";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import utility, { flattenDeep } from "./utility.js";
|
|
6
|
+
import libsaml from "./libsaml.js";
|
|
7
|
+
import { wording } from "./urn.js";
|
|
8
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
9
|
+
const certUse = wording.certUse;
|
|
10
|
+
const docParser = new DOMParser();
|
|
11
|
+
async function verifyAndDecryptSoapMessage(xml, opts) {
|
|
12
|
+
const { dom } = getContext();
|
|
13
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
14
|
+
const docParser = new DOMParser();
|
|
15
|
+
let type = '';
|
|
16
|
+
// 为 SOAP 消息定义 XPath
|
|
17
|
+
const artifactResolveXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
18
|
+
const artifactResponseXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
19
|
+
// 检测 ArtifactResolve 或 ArtifactResponse 的存在
|
|
20
|
+
// @ts-expect-error
|
|
21
|
+
const artifactResolveNodes = select(artifactResolveXpath, doc);
|
|
22
|
+
// @ts-expect-error
|
|
23
|
+
const artifactResponseNodes = select(artifactResponseXpath, doc);
|
|
24
|
+
// 根据消息类型选择合适的 XPath
|
|
25
|
+
let basePath = "";
|
|
26
|
+
if (artifactResolveNodes?.length > 0) {
|
|
27
|
+
type = 'artifactResolve';
|
|
28
|
+
basePath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
29
|
+
}
|
|
30
|
+
else if (artifactResponseNodes?.length > 0) {
|
|
31
|
+
type = 'artifactResponse';
|
|
32
|
+
basePath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new Error('ERR_UNSUPPORTED_SOAP_MESSAGE_TYPE');
|
|
36
|
+
}
|
|
37
|
+
// 基于 SOAP 结构重新定义 XPath
|
|
38
|
+
const messageSignatureXpath = `${basePath}/*[local-name(.)='Signature']`;
|
|
39
|
+
// @ts-expect-error
|
|
40
|
+
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
41
|
+
let selection = [];
|
|
42
|
+
if (messageSignatureNode?.length > 0) {
|
|
43
|
+
selection = selection.concat(messageSignatureNode);
|
|
44
|
+
}
|
|
45
|
+
if (selection.length === 0) {
|
|
46
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
47
|
+
}
|
|
48
|
+
return verifySignature(xml, selection, opts);
|
|
49
|
+
}
|
|
50
|
+
function verifySignature(xml, selection, opts) {
|
|
51
|
+
// 尝试所有签名节点
|
|
52
|
+
for (const signatureNode of selection) {
|
|
53
|
+
const sig = new SignedXml();
|
|
54
|
+
let verified = false;
|
|
55
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
56
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
57
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
58
|
+
}
|
|
59
|
+
if (opts.keyFile) {
|
|
60
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
61
|
+
}
|
|
62
|
+
if (opts.metadata) {
|
|
63
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
64
|
+
// 证书处理逻辑
|
|
65
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
66
|
+
if (Array.isArray(metadataCert)) {
|
|
67
|
+
metadataCert = flattenDeep(metadataCert);
|
|
68
|
+
}
|
|
69
|
+
else if (typeof metadataCert === 'string') {
|
|
70
|
+
metadataCert = [metadataCert];
|
|
71
|
+
}
|
|
72
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
73
|
+
// 没有证书的情况
|
|
74
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
75
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
76
|
+
}
|
|
77
|
+
if (certificateNode.length !== 0) {
|
|
78
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
79
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
80
|
+
if (metadataCert.length >= 1 && !metadataCert.includes(x509Certificate)) {
|
|
81
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
82
|
+
}
|
|
83
|
+
sig.publicCert = libsaml.getKeyInfo(x509Certificate).getKey();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
sig.publicCert = libsaml.getKeyInfo(metadataCert[0]).getKey();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
sig.loadSignature(signatureNode);
|
|
90
|
+
verified = sig.checkSignature(xml); // 使用原始XML验证
|
|
91
|
+
if (!verified) {
|
|
92
|
+
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
93
|
+
}
|
|
94
|
+
if (sig.getSignedReferences().length < 1) {
|
|
95
|
+
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
96
|
+
}
|
|
97
|
+
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
98
|
+
const rootNode = docParser.parseFromString(signedVerifiedXML, 'application/xml').documentElement;
|
|
99
|
+
// 处理签名的内容
|
|
100
|
+
switch (rootNode?.localName) {
|
|
101
|
+
case 'ArtifactResolve':
|
|
102
|
+
return [true, rootNode.toString(), false, false];
|
|
103
|
+
case 'ArtifactResponse':
|
|
104
|
+
// @ts-expect-error
|
|
105
|
+
const Response = select("/*[local-name()='ArtifactResponse']/*[local-name()='Response']", rootNode);
|
|
106
|
+
return [true, Response?.[0].toString(), false, false]; // 签名验证成功但未找到断言
|
|
107
|
+
default:
|
|
108
|
+
return [true, null, false, true]; // 签名验证成功但未找到可识别的内容
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return [false, null, false, true];
|
|
112
|
+
}
|
|
113
|
+
export default {
|
|
114
|
+
verifyAndDecryptSoapMessage
|
|
115
|
+
};
|