samlesa 2.16.6 → 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/README.md +30 -50
- package/build/index.js +2 -1
- package/build/src/binding-artifact.js +330 -146
- package/build/src/binding-post.js +45 -31
- package/build/src/binding-redirect.js +0 -10
- package/build/src/binding-simplesign.js +0 -1
- package/build/src/entity-idp.js +1 -5
- package/build/src/entity-sp.js +21 -96
- package/build/src/extractor.js +48 -4
- package/build/src/flow.js +24 -166
- package/build/src/libsaml.js +468 -264
- package/build/src/libsamlSoap.js +115 -0
- package/build/src/schema/xml.xsd +88 -88
- package/build/src/schemaValidator.js +5 -13
- package/build/src/soap.js +123 -3
- package/build/src/utility.js +12 -7
- package/package.json +77 -81
- 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/api.d.ts +3 -3
- package/types/src/api.d.ts.map +1 -1
- 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 +22 -22
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/binding-redirect.d.ts.map +1 -1
- package/types/src/binding-simplesign.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts +3 -4
- package/types/src/entity-idp.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/entity.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 +16 -7
- 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 +1 -1
- 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/utility.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
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @desc A simple library including some common functions
|
|
5
5
|
*/
|
|
6
6
|
import xml from 'xml';
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import { algorithms, wording, namespace } from './urn.js';
|
|
7
|
+
import utility, { flattenDeep, inflateString, isString } from './utility.js';
|
|
8
|
+
import { algorithms, namespace, wording } from './urn.js';
|
|
10
9
|
import { select } from 'xpath';
|
|
10
|
+
import nrsa from 'node-rsa';
|
|
11
11
|
import { SignedXml } from 'xml-crypto';
|
|
12
12
|
import * as xmlenc from 'xml-encryption';
|
|
13
13
|
import camelCase from 'camelcase';
|
|
@@ -15,26 +15,10 @@ import { getContext } from './api.js';
|
|
|
15
15
|
import xmlEscape from 'xml-escape';
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import { DOMParser } from '@xmldom/xmldom';
|
|
18
|
-
import { inflate } from 'pako';
|
|
19
18
|
const signatureAlgorithms = algorithms.signature;
|
|
20
19
|
const digestAlgorithms = algorithms.digest;
|
|
21
20
|
const certUse = wording.certUse;
|
|
22
21
|
const urlParams = wording.urlParams;
|
|
23
|
-
/**
|
|
24
|
-
* 算法名称映射表 (兼容 X.509 和 SAML 规范)
|
|
25
|
-
*/
|
|
26
|
-
function mapSignAlgorithm(algorithm) {
|
|
27
|
-
const algorithmMap = {
|
|
28
|
-
'rsa-sha1': 'RSA-SHA1',
|
|
29
|
-
'rsa-sha256': 'RSA-SHA256',
|
|
30
|
-
'rsa-sha384': 'RSA-SHA384',
|
|
31
|
-
'rsa-sha512': 'RSA-SHA512',
|
|
32
|
-
'ecdsa-sha256': 'ECDSA-SHA256',
|
|
33
|
-
'ecdsa-sha384': 'ECDSA-SHA384',
|
|
34
|
-
'ecdsa-sha512': 'ECDSA-SHA512'
|
|
35
|
-
};
|
|
36
|
-
return algorithmMap[algorithm.toLowerCase()] || algorithm;
|
|
37
|
-
}
|
|
38
22
|
const libSaml = () => {
|
|
39
23
|
/**
|
|
40
24
|
* @desc helper function to get back the query param for redirect binding for SLO/SSO
|
|
@@ -52,6 +36,7 @@ const libSaml = () => {
|
|
|
52
36
|
/**
|
|
53
37
|
*
|
|
54
38
|
*/
|
|
39
|
+
// 签名算法映射表
|
|
55
40
|
const nrsaAliasMapping = {
|
|
56
41
|
'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'pkcs1-sha1',
|
|
57
42
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'pkcs1-sha256',
|
|
@@ -84,7 +69,18 @@ const libSaml = () => {
|
|
|
84
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>`,
|
|
85
70
|
};
|
|
86
71
|
const defaultArtAuthnRequestTemplate = {
|
|
87
|
-
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>`,
|
|
88
84
|
};
|
|
89
85
|
/**
|
|
90
86
|
* @desc Default AttributeStatement template
|
|
@@ -126,6 +122,21 @@ const libSaml = () => {
|
|
|
126
122
|
const defaultLogoutResponseTemplate = {
|
|
127
123
|
context: '<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status></samlp:LogoutResponse>',
|
|
128
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* @private
|
|
127
|
+
* @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module
|
|
128
|
+
* @param {string} sigAlg signature algorithm
|
|
129
|
+
* @return {string/null} signing algorithm short-hand for the module node-rsa
|
|
130
|
+
*/
|
|
131
|
+
function getSigningScheme(sigAlg) {
|
|
132
|
+
if (sigAlg) {
|
|
133
|
+
const algAlias = nrsaAliasMapping[sigAlg];
|
|
134
|
+
if (!(algAlias === undefined)) {
|
|
135
|
+
return algAlias;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1];
|
|
139
|
+
}
|
|
129
140
|
function validateAndInflateSamlResponse(urlEncodedResponse) {
|
|
130
141
|
// 3. 尝试DEFLATE解压(SAML规范要求使用原始DEFLATE)
|
|
131
142
|
let xml = "";
|
|
@@ -133,18 +144,13 @@ const libSaml = () => {
|
|
|
133
144
|
try { // 1. URL解码
|
|
134
145
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
135
146
|
// 2. Base64解码为Uint8Array
|
|
136
|
-
|
|
137
|
-
const compressedData = new Uint8Array(binaryStr.length);
|
|
138
|
-
for (let i = 0; i < binaryStr.length; i++) {
|
|
139
|
-
compressedData[i] = binaryStr.charCodeAt(i);
|
|
140
|
-
}
|
|
141
|
-
xml = inflate(compressedData, { to: 'string', raw: true });
|
|
147
|
+
xml = inflateString(base64Encoded);
|
|
142
148
|
}
|
|
143
149
|
catch (inflateError) {
|
|
144
150
|
// 4. 解压失败,尝试直接解析为未压缩的XML
|
|
145
151
|
try {
|
|
146
152
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
147
|
-
xml =
|
|
153
|
+
xml = atob(base64Encoded);
|
|
148
154
|
return { compressed: false, xml, error: null };
|
|
149
155
|
}
|
|
150
156
|
catch (xmlError) {
|
|
@@ -215,6 +221,7 @@ const libSaml = () => {
|
|
|
215
221
|
defaultArtAuthnRequestTemplate,
|
|
216
222
|
defaultArtifactResolveTemplate,
|
|
217
223
|
defaultLoginResponseTemplate,
|
|
224
|
+
defaultSoapResponseFailTemplate,
|
|
218
225
|
defaultAttributeStatementTemplate,
|
|
219
226
|
defaultAttributeTemplate,
|
|
220
227
|
defaultLogoutRequestTemplate,
|
|
@@ -277,6 +284,9 @@ const libSaml = () => {
|
|
|
277
284
|
};
|
|
278
285
|
// 生成 XML(关闭自动声明头)
|
|
279
286
|
const xmlString = xml([attributeStatement], { declaration: false });
|
|
287
|
+
if (xmlString.trim() === '<saml:AttributeStatement></saml:AttributeStatement>') {
|
|
288
|
+
return '';
|
|
289
|
+
}
|
|
280
290
|
return xmlString.trim();
|
|
281
291
|
},
|
|
282
292
|
/**
|
|
@@ -324,7 +334,7 @@ const libSaml = () => {
|
|
|
324
334
|
else {
|
|
325
335
|
sig.computeSignature(rawSamlMessage);
|
|
326
336
|
}
|
|
327
|
-
return isBase64Output
|
|
337
|
+
return isBase64Output ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml();
|
|
328
338
|
},
|
|
329
339
|
/**
|
|
330
340
|
* @desc Verify the XML signature
|
|
@@ -337,29 +347,68 @@ const libSaml = () => {
|
|
|
337
347
|
// tslint:disable-next-line:no-shadowed-variable
|
|
338
348
|
verifySignature(xml, opts) {
|
|
339
349
|
const { dom } = getContext();
|
|
340
|
-
const doc = dom.parseFromString(xml);
|
|
350
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
341
351
|
const docParser = new DOMParser();
|
|
342
352
|
// In order to avoid the wrapping attack, we have changed to use absolute xpath instead of naively fetching the signature element
|
|
353
|
+
const LogoutResponseSignatureXpath = "/*[local-name()='LogoutResponse']/*[local-name()='Signature']";
|
|
354
|
+
const logoutRequestSignatureXpath = "/*[local-name()='LogoutRequest']/*[local-name()='Signature']";
|
|
343
355
|
// message signature (logout response / saml response)
|
|
344
356
|
const messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']";
|
|
345
357
|
// assertion signature (logout response / saml response)
|
|
346
358
|
const assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']";
|
|
347
359
|
// check if there is a potential malicious wrapping signature
|
|
348
360
|
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']";
|
|
361
|
+
// 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']";
|
|
362
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
363
|
+
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
364
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
349
365
|
// select the signature node
|
|
350
366
|
let selection = [];
|
|
367
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
351
368
|
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
369
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
352
370
|
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
371
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
353
372
|
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
354
|
-
|
|
355
|
-
|
|
373
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
374
|
+
const LogoutResponseSignatureElementNode = select(LogoutResponseSignatureXpath, doc);
|
|
356
375
|
// try to catch potential wrapping attack
|
|
357
376
|
if (wrappingElementNode.length !== 0) {
|
|
358
377
|
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
359
378
|
}
|
|
379
|
+
// 优先检测 LogoutRequest 签名
|
|
380
|
+
// @ts-expect-error missing Node properties
|
|
381
|
+
const logoutRequestSignature = select(logoutRequestSignatureXpath, doc);
|
|
382
|
+
if (logoutRequestSignature.length > 0) {
|
|
383
|
+
selection = selection.concat(logoutRequestSignature);
|
|
384
|
+
}
|
|
385
|
+
selection = selection.concat(messageSignatureNode);
|
|
386
|
+
selection = selection.concat(assertionSignatureNode);
|
|
387
|
+
selection = selection.concat(LogoutResponseSignatureElementNode);
|
|
360
388
|
// guarantee to have a signature in saml response
|
|
361
389
|
if (selection.length === 0) {
|
|
362
|
-
|
|
390
|
+
/** 判断有没有加密如果没有加密返回 [false, null]*/
|
|
391
|
+
if (encryptedAssertions.length > 0) {
|
|
392
|
+
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
393
|
+
return [false, null, false, true]; // we return false now
|
|
394
|
+
}
|
|
395
|
+
if (encryptedAssertions.length > 1) {
|
|
396
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
397
|
+
}
|
|
398
|
+
return [false, null, true, true]; // return encryptedAssert
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (selection.length !== 0) {
|
|
402
|
+
/** 判断有没有加密如果没有加密返回 [false, null]*/
|
|
403
|
+
if (logoutRequestSignature.length === 0 && LogoutResponseSignatureElementNode.length === 0 && encryptedAssertions.length > 0) {
|
|
404
|
+
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
405
|
+
return [false, null, true, false]; // we return false now
|
|
406
|
+
}
|
|
407
|
+
if (encryptedAssertions.length > 1) {
|
|
408
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
409
|
+
}
|
|
410
|
+
return [false, null, true, false]; // return encryptedAssert
|
|
411
|
+
}
|
|
363
412
|
}
|
|
364
413
|
// need to refactor later on
|
|
365
414
|
for (const signatureNode of selection) {
|
|
@@ -407,7 +456,6 @@ const libSaml = () => {
|
|
|
407
456
|
}
|
|
408
457
|
}
|
|
409
458
|
sig.loadSignature(signatureNode);
|
|
410
|
-
doc.removeChild(signatureNode);
|
|
411
459
|
verified = sig.checkSignature(doc.toString());
|
|
412
460
|
// immediately throw error when any one of the signature is failed to get verified
|
|
413
461
|
if (!verified) {
|
|
@@ -420,123 +468,333 @@ const libSaml = () => {
|
|
|
420
468
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
421
469
|
}
|
|
422
470
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
423
|
-
const rootNode = docParser.parseFromString(signedVerifiedXML, '
|
|
471
|
+
const rootNode = docParser.parseFromString(signedVerifiedXML, 'application/xml').documentElement;
|
|
424
472
|
// process the verified signature:
|
|
425
473
|
// case 1, rootSignedDoc is a response:
|
|
426
|
-
if (rootNode
|
|
474
|
+
if (rootNode?.localName === 'Response') {
|
|
427
475
|
// try getting the Xml from the first assertion
|
|
428
|
-
const EncryptedAssertions = select("./*[local-name()='EncryptedAssertion']",
|
|
429
|
-
|
|
476
|
+
const EncryptedAssertions = select("./*[local-name()='EncryptedAssertion']",
|
|
477
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
478
|
+
rootNode);
|
|
479
|
+
const assertions = select("./*[local-name()='Assertion']",
|
|
480
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
481
|
+
rootNode);
|
|
430
482
|
/**第三个参数代表是否加密*/
|
|
431
483
|
// now we can process the assertion as an assertion
|
|
432
484
|
if (EncryptedAssertions.length === 1) {
|
|
433
485
|
/** 已加密*/
|
|
434
|
-
return [true, EncryptedAssertions[0].toString(), true];
|
|
486
|
+
return [true, EncryptedAssertions[0].toString(), true, false];
|
|
435
487
|
}
|
|
436
488
|
if (assertions.length === 1) {
|
|
437
|
-
return [true, assertions[0].toString(), false];
|
|
489
|
+
return [true, assertions[0].toString(), false, false];
|
|
438
490
|
}
|
|
439
491
|
}
|
|
440
|
-
else if (rootNode
|
|
441
|
-
return [true, rootNode.toString(), false];
|
|
492
|
+
else if (rootNode?.localName === 'Assertion') {
|
|
493
|
+
return [true, rootNode.toString(), false, false];
|
|
494
|
+
}
|
|
495
|
+
else if (rootNode?.localName === 'EncryptedAssertion') {
|
|
496
|
+
return [true, rootNode.toString(), true, false];
|
|
497
|
+
}
|
|
498
|
+
else if (rootNode?.localName === 'LogoutRequest') {
|
|
499
|
+
return [true, rootNode.toString(), false, false];
|
|
442
500
|
}
|
|
443
|
-
else if (rootNode
|
|
444
|
-
return [true, rootNode.toString(),
|
|
501
|
+
else if (rootNode?.localName === 'LogoutResponse') {
|
|
502
|
+
return [true, rootNode.toString(), false, false];
|
|
445
503
|
}
|
|
446
504
|
else {
|
|
447
|
-
return [true, null]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
|
|
505
|
+
return [true, null, false, false]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
|
|
448
506
|
}
|
|
449
507
|
}
|
|
450
508
|
// something has gone seriously wrong if we are still here
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
509
|
+
return [false, null, false, true]; // return encryptedAssert
|
|
510
|
+
/* throw new Error('ERR_ZERO_SIGNATURE');*/
|
|
511
|
+
},
|
|
512
|
+
/* verifySignatureSoap(xml: string, opts: SignatureVerifierOptions & { isAssertion?: boolean }) {
|
|
513
|
+
const {dom} = getContext();
|
|
514
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
515
|
+
const docParser = new DOMParser();
|
|
516
|
+
|
|
517
|
+
let selection: any = [];
|
|
518
|
+
|
|
519
|
+
if (opts.isAssertion) {
|
|
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);
|
|
572
|
+
}
|
|
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;
|
|
459
616
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
key: 'id',
|
|
470
|
-
localPath: ['~Response', 'Assertion'],
|
|
471
|
-
attributes: ['ID']
|
|
472
|
-
}]);
|
|
473
|
-
// 5.4.2 References
|
|
474
|
-
// SAML assertions and protocol messages MUST supply a value for the ID attribute on the root element of
|
|
475
|
-
// the assertion or protocol message being signed. The assertion’s or protocol message's root element may
|
|
476
|
-
// or may not be the root element of the actual XML document containing the signed assertion or protocol
|
|
477
|
-
// message (e.g., it might be contained within a SOAP envelope).
|
|
478
|
-
// Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID
|
|
479
|
-
// attribute value of the root element of the assertion or protocol message being signed. For example, if the
|
|
480
|
-
// ID attribute value is "foo", then the URI attribute in the <ds:Reference> element MUST be "#foo".
|
|
481
|
-
if (verifiedAssertionInfo.refURI !== `#${desiredAssertionInfo.id}`) {
|
|
482
|
-
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
483
|
-
}
|
|
484
|
-
const verifiedDoc = extract(doc.toString(), [{
|
|
485
|
-
key: 'assertion',
|
|
486
|
-
localPath: ['~Response', 'Assertion'],
|
|
487
|
-
attributes: [],
|
|
488
|
-
context: true
|
|
489
|
-
}]);
|
|
490
|
-
assertionNode = verifiedDoc.assertion.toString();
|
|
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');
|
|
491
626
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
667
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
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') {
|
|
702
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
703
|
+
const encryptedAssertions = select(
|
|
704
|
+
"./!*[local-name()='EncryptedAssertion']",
|
|
705
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
706
|
+
rootNode
|
|
707
|
+
) as Element[];
|
|
708
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
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];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (assertions.length === 1) {
|
|
720
|
+
return [true, assertions[0].toString(), false];
|
|
721
|
+
}
|
|
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
|
+
}
|
|
735
|
+
|
|
736
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
737
|
+
},*/
|
|
495
738
|
verifySignatureSoap(xml, opts) {
|
|
496
739
|
const { dom } = getContext();
|
|
497
|
-
const doc = dom.parseFromString(xml);
|
|
740
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
498
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);
|
|
499
778
|
let selection = [];
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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];
|
|
506
792
|
}
|
|
507
|
-
selection = selection.concat(signatureNode);
|
|
508
793
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const messageSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
512
|
-
"/*[local-name()='ArtifactResponse']/*[local-name()='Signature'] | " +
|
|
513
|
-
"/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
514
|
-
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']/*[local-name()='Signature']";
|
|
515
|
-
const assertionSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
516
|
-
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
517
|
-
"/*[local-name()='Assertion']/*[local-name()='Signature'] | " +
|
|
518
|
-
"/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
519
|
-
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
520
|
-
"/*[local-name()='EncryptedAssertion']";
|
|
521
|
-
const wrappingElementsXPath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
522
|
-
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
523
|
-
"/*[local-name()='Assertion']/*[local-name()='Subject']" +
|
|
524
|
-
"/*[local-name()='SubjectConfirmation']" +
|
|
525
|
-
"/*[local-name()='SubjectConfirmationData']" +
|
|
526
|
-
"//*[local-name()='Assertion' or local-name()='Signature']";
|
|
527
|
-
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
528
|
-
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
529
|
-
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
530
|
-
// 检测包装攻击
|
|
531
|
-
if (wrappingElementNode.length !== 0) {
|
|
532
|
-
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
533
|
-
}
|
|
534
|
-
// 保证响应中至少有一个签名
|
|
535
|
-
if (messageSignatureNode.length === 0 && assertionSignatureNode.length === 0) {
|
|
536
|
-
throw new Error('ERR_ZERO_SIGNATURE');
|
|
537
|
-
}
|
|
538
|
-
selection = selection.concat(messageSignatureNode, assertionSignatureNode);
|
|
794
|
+
if (selection.length === 0) {
|
|
795
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
539
796
|
}
|
|
797
|
+
// 尝试所有签名节点
|
|
540
798
|
for (const signatureNode of selection) {
|
|
541
799
|
const sig = new SignedXml();
|
|
542
800
|
let verified = false;
|
|
@@ -545,13 +803,12 @@ const libSaml = () => {
|
|
|
545
803
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
546
804
|
}
|
|
547
805
|
if (opts.keyFile) {
|
|
548
|
-
sig.publicCert = fs.readFileSync(opts.keyFile
|
|
806
|
+
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
549
807
|
}
|
|
550
808
|
if (opts.metadata) {
|
|
551
|
-
const
|
|
552
|
-
//
|
|
809
|
+
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
810
|
+
// 证书处理逻辑
|
|
553
811
|
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
554
|
-
// 规范化元数据证书
|
|
555
812
|
if (Array.isArray(metadataCert)) {
|
|
556
813
|
metadataCert = flattenDeep(metadataCert);
|
|
557
814
|
}
|
|
@@ -559,102 +816,59 @@ const libSaml = () => {
|
|
|
559
816
|
metadataCert = [metadataCert];
|
|
560
817
|
}
|
|
561
818
|
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
562
|
-
//
|
|
563
|
-
if (
|
|
819
|
+
// 没有证书的情况
|
|
820
|
+
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
564
821
|
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
565
822
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
// 安全获取证书数据
|
|
569
|
-
let x509CertificateData = '';
|
|
570
|
-
if (certificateNodes[0].firstChild) {
|
|
571
|
-
x509CertificateData = certificateNodes[0].firstChild.data;
|
|
572
|
-
}
|
|
573
|
-
else if (certificateNodes[0].textContent) {
|
|
574
|
-
x509CertificateData = certificateNodes[0].textContent;
|
|
575
|
-
}
|
|
823
|
+
if (certificateNode.length !== 0) {
|
|
824
|
+
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
576
825
|
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
577
|
-
|
|
578
|
-
if (metadataCert.length >= 1 &&
|
|
579
|
-
!metadataCert.find(cert => cert.trim() === x509Certificate.trim())) {
|
|
826
|
+
if (metadataCert.length >= 1 && !metadataCert.includes(x509Certificate)) {
|
|
580
827
|
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
581
828
|
}
|
|
582
829
|
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
583
830
|
}
|
|
584
831
|
else {
|
|
585
|
-
// 使用元数据中的第一个证书
|
|
586
832
|
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
587
833
|
}
|
|
588
834
|
}
|
|
589
|
-
// 加载签名
|
|
590
835
|
sig.loadSignature(signatureNode);
|
|
591
|
-
// 使用原始
|
|
592
|
-
verified = sig.checkSignature(xml);
|
|
593
|
-
console.log("签名验证结果:", verified);
|
|
836
|
+
verified = sig.checkSignature(xml); // 使用原始XML验证
|
|
594
837
|
if (!verified) {
|
|
595
|
-
console.error("签名验证失败");
|
|
596
838
|
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
597
839
|
}
|
|
598
|
-
|
|
599
|
-
if (!(sig.getSignedReferences().length >= 1)) {
|
|
840
|
+
if (sig.getSignedReferences().length < 1) {
|
|
600
841
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
601
842
|
}
|
|
602
843
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return [true, encryptedAssertions[0].toString(), true];
|
|
629
|
-
}
|
|
630
|
-
if (assertions.length === 1) {
|
|
631
|
-
return [true, assertions[0].toString(), false];
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
// 直接处理 Response
|
|
635
|
-
else if (rootNode.localName === 'Response') {
|
|
636
|
-
const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", rootNode);
|
|
637
|
-
const assertions = select("./*[local-name()='Assertion']", rootNode);
|
|
638
|
-
if (encryptedAssertions.length === 1) {
|
|
639
|
-
return [true, encryptedAssertions[0].toString(), true];
|
|
640
|
-
}
|
|
641
|
-
if (assertions.length === 1) {
|
|
642
|
-
return [true, assertions[0].toString(), false];
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
// 直接处理 Assertion
|
|
646
|
-
else if (rootNode.localName === 'Assertion') {
|
|
647
|
-
return [true, rootNode.toString(), false];
|
|
648
|
-
}
|
|
649
|
-
// 直接处理 EncryptedAssertion
|
|
650
|
-
else if (rootNode.localName === 'EncryptedAssertion') {
|
|
651
|
-
return [true, rootNode.toString(), true];
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
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]; // 签名验证成功但未找到可识别的内容
|
|
655
869
|
}
|
|
656
870
|
}
|
|
657
|
-
|
|
871
|
+
return [false, null, encryptedAssertions.length > 0, false];
|
|
658
872
|
},
|
|
659
873
|
/**
|
|
660
874
|
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
|
|
@@ -694,59 +908,43 @@ const libSaml = () => {
|
|
|
694
908
|
* @param signingAlgorithm - 签名算法 (默认 'rsa-sha256')
|
|
695
909
|
* @returns 消息签名
|
|
696
910
|
*/
|
|
697
|
-
constructMessageSignature(octetString, key, passphrase, isBase64
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
// 3. 加载私钥
|
|
707
|
-
const privateKey = createPrivateKey({
|
|
708
|
-
key: key,
|
|
709
|
-
format: 'pem',
|
|
710
|
-
passphrase: passphrase,
|
|
711
|
-
encoding: 'utf8'
|
|
712
|
-
});
|
|
713
|
-
signer.write(octetString);
|
|
714
|
-
signer.end();
|
|
715
|
-
const signature = signer.sign(privateKey, 'base64');
|
|
716
|
-
// 5. 处理编码输出
|
|
717
|
-
return isBase64 ? signature.toString() : signature;
|
|
718
|
-
}
|
|
719
|
-
catch (error) {
|
|
720
|
-
throw new Error(`SAML 签名失败: ${error.message}`);
|
|
721
|
-
}
|
|
911
|
+
constructMessageSignature(octetString, key, passphrase, isBase64, signingAlgorithm) {
|
|
912
|
+
// Default returning base64 encoded signature
|
|
913
|
+
// Embed with node-rsa module
|
|
914
|
+
const decryptedKey = new nrsa(utility.readPrivateKey(key, passphrase), undefined, {
|
|
915
|
+
signingScheme: getSigningScheme(signingAlgorithm),
|
|
916
|
+
});
|
|
917
|
+
const signature = decryptedKey.sign(octetString);
|
|
918
|
+
// Use private key to sign data
|
|
919
|
+
return isBase64 !== false ? signature.toString('base64') : signature;
|
|
722
920
|
},
|
|
723
|
-
/*
|
|
921
|
+
/* verifyMessageSignature(
|
|
922
|
+
metadata,
|
|
724
923
|
octetString: string,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
isBase64?: boolean,
|
|
728
|
-
signingAlgorithm?: string
|
|
924
|
+
signature: string | Buffer,
|
|
925
|
+
verifyAlgorithm?: string
|
|
729
926
|
) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
);
|
|
739
|
-
const signature = decryptedKey.sign(octetString);
|
|
740
|
-
// Use private key to sign data
|
|
741
|
-
return isBase64 !== false ? signature.toString('base64') : signature;
|
|
927
|
+
const signCert = metadata.getX509Certificate(certUse.signing);
|
|
928
|
+
const signingScheme = getSigningSchemeForNode(verifyAlgorithm);
|
|
929
|
+
const verifier = createVerify(signingScheme);
|
|
930
|
+
verifier.update(octetString);
|
|
931
|
+
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
932
|
+
return isValid
|
|
933
|
+
|
|
742
934
|
},*/
|
|
935
|
+
/**
|
|
936
|
+
* @desc Verifies message signature
|
|
937
|
+
* @param {Metadata} metadata metadata object of identity provider or service provider
|
|
938
|
+
* @param {string} octetString see "Bindings for the OASIS Security Assertion Markup Language (SAML V2.0)" P.17/46
|
|
939
|
+
* @param {string} signature context of XML signature
|
|
940
|
+
* @param {string} verifyAlgorithm algorithm used to verify
|
|
941
|
+
* @return {boolean} verification result
|
|
942
|
+
*/
|
|
743
943
|
verifyMessageSignature(metadata, octetString, signature, verifyAlgorithm) {
|
|
744
944
|
const signCert = metadata.getX509Certificate(certUse.signing);
|
|
745
|
-
const signingScheme =
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
749
|
-
return isValid;
|
|
945
|
+
const signingScheme = getSigningScheme(verifyAlgorithm);
|
|
946
|
+
const key = new nrsa(utility.getPublicKeyPemFromCertificate(signCert), 'public', { signingScheme });
|
|
947
|
+
return key.verify(Buffer.from(octetString), Buffer.from(signature));
|
|
750
948
|
},
|
|
751
949
|
/**
|
|
752
950
|
* @desc Get the public key in string format
|
|
@@ -781,7 +979,8 @@ const libSaml = () => {
|
|
|
781
979
|
const sourceEntitySetting = sourceEntity.entitySetting;
|
|
782
980
|
const targetEntityMetadata = targetEntity.entityMeta;
|
|
783
981
|
const { dom } = getContext();
|
|
784
|
-
const doc = dom.parseFromString(xml);
|
|
982
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
983
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
785
984
|
const assertions = select("//*[local-name(.)='Assertion']", doc);
|
|
786
985
|
if (!Array.isArray(assertions) || assertions.length === 0) {
|
|
787
986
|
throw new Error('ERR_NO_ASSERTION');
|
|
@@ -799,7 +998,7 @@ const libSaml = () => {
|
|
|
799
998
|
pem: Buffer.from(`-----BEGIN CERTIFICATE-----${targetEntityMetadata.getX509Certificate(certUse.encrypt)}-----END CERTIFICATE-----`),
|
|
800
999
|
encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm,
|
|
801
1000
|
keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm,
|
|
802
|
-
keyEncryptionDigest: 'SHA-512'
|
|
1001
|
+
/* keyEncryptionDigest: 'SHA-512',*/
|
|
803
1002
|
disallowEncryptionWithInsecureAlgorithm: true,
|
|
804
1003
|
warnInsecureAlgorithm: true
|
|
805
1004
|
}, (err, res) => {
|
|
@@ -810,7 +1009,8 @@ const libSaml = () => {
|
|
|
810
1009
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
811
1010
|
}
|
|
812
1011
|
const { encryptedAssertion: encAssertionPrefix } = sourceEntitySetting.tagPrefix;
|
|
813
|
-
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion
|
|
1012
|
+
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion>`, 'application/xml');
|
|
1013
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
814
1014
|
doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode);
|
|
815
1015
|
return resolve(utility.base64Encode(doc.toString()));
|
|
816
1016
|
});
|
|
@@ -837,7 +1037,8 @@ const libSaml = () => {
|
|
|
837
1037
|
// Perform encryption depends on the setting of where the message is sent, default is false
|
|
838
1038
|
const hereSetting = here.entitySetting;
|
|
839
1039
|
const { dom } = getContext();
|
|
840
|
-
const doc = dom.parseFromString(entireXML);
|
|
1040
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
1041
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
841
1042
|
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
842
1043
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
843
1044
|
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
@@ -850,13 +1051,13 @@ const libSaml = () => {
|
|
|
850
1051
|
key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass),
|
|
851
1052
|
}, (err, res) => {
|
|
852
1053
|
if (err) {
|
|
853
|
-
console.error(err);
|
|
854
1054
|
return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION'));
|
|
855
1055
|
}
|
|
856
1056
|
if (!res) {
|
|
857
1057
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
858
1058
|
}
|
|
859
|
-
const rawAssertionDoc = dom.parseFromString(res);
|
|
1059
|
+
const rawAssertionDoc = dom.parseFromString(res, 'application/xml');
|
|
1060
|
+
// @ts-ignore
|
|
860
1061
|
doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode);
|
|
861
1062
|
return resolve([doc.toString(), res]);
|
|
862
1063
|
});
|
|
@@ -872,11 +1073,14 @@ const libSaml = () => {
|
|
|
872
1073
|
const { dom } = getContext();
|
|
873
1074
|
try {
|
|
874
1075
|
// 1. 解析 XML
|
|
875
|
-
|
|
1076
|
+
// @ts-ignore
|
|
1077
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
876
1078
|
// 2. 定位加密断言
|
|
877
1079
|
const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
878
1080
|
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
879
|
-
"/*[local-name()='EncryptedAssertion']",
|
|
1081
|
+
"/*[local-name()='EncryptedAssertion']",
|
|
1082
|
+
// @ts-ignore
|
|
1083
|
+
doc);
|
|
880
1084
|
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
881
1085
|
throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
|
|
882
1086
|
}
|
|
@@ -890,7 +1094,6 @@ const libSaml = () => {
|
|
|
890
1094
|
const decryptedAssertion = await new Promise((resolve, reject) => {
|
|
891
1095
|
xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
|
|
892
1096
|
if (err) {
|
|
893
|
-
console.error('解密错误:', err);
|
|
894
1097
|
return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
|
|
895
1098
|
}
|
|
896
1099
|
if (!result) {
|
|
@@ -900,27 +1103,28 @@ const libSaml = () => {
|
|
|
900
1103
|
});
|
|
901
1104
|
});
|
|
902
1105
|
// 5. 创建解密断言的 DOM
|
|
903
|
-
|
|
1106
|
+
// @ts-ignore
|
|
1107
|
+
const decryptedDoc = dom.parseFromString(decryptedAssertion, 'application/xml');
|
|
904
1108
|
const decryptedAssertionNode = decryptedDoc.documentElement;
|
|
905
1109
|
// 6. 替换加密断言为解密后的断言
|
|
906
1110
|
const parentNode = encAssertionNode.parentNode;
|
|
907
1111
|
if (!parentNode) {
|
|
908
1112
|
throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
|
|
909
1113
|
}
|
|
1114
|
+
// @ts-ignore
|
|
910
1115
|
parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
|
|
911
1116
|
// 7. 序列化更新后的文档
|
|
912
1117
|
const updatedSoapXml = doc.toString();
|
|
913
1118
|
return [updatedSoapXml, decryptedAssertion];
|
|
914
1119
|
}
|
|
915
1120
|
catch (error) {
|
|
916
|
-
console.error('SOAP断言解密失败:', error);
|
|
917
1121
|
throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
|
|
918
1122
|
}
|
|
919
1123
|
},
|
|
920
1124
|
/**
|
|
921
1125
|
* @desc Check if the xml string is valid and bounded
|
|
922
1126
|
*/
|
|
923
|
-
async isValidXml(input) {
|
|
1127
|
+
async isValidXml(input, soap = false) {
|
|
924
1128
|
// check if global api contains the validate function
|
|
925
1129
|
const { validate } = getContext();
|
|
926
1130
|
/**
|
|
@@ -934,7 +1138,7 @@ const libSaml = () => {
|
|
|
934
1138
|
return Promise.reject('Your application is potentially vulnerable because no validation function found. Please read the documentation on how to setup the validator. (https://github.com/tngan/samlify#installation)');
|
|
935
1139
|
}
|
|
936
1140
|
try {
|
|
937
|
-
return await validate(input);
|
|
1141
|
+
return await validate(input, soap);
|
|
938
1142
|
}
|
|
939
1143
|
catch (e) {
|
|
940
1144
|
throw e;
|