samlesa 2.16.6 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of samlesa might be problematic. Click here for more details.
- package/README.md +30 -50
- 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 +0 -2
- package/build/src/extractor.js +16 -4
- package/build/src/flow.js +16 -69
- package/build/src/libsaml.js +166 -160
- package/build/src/schema/xml.xsd +88 -88
- package/build/src/schemaValidator.js +9 -13
- package/build/src/utility.js +12 -7
- package/package.json +14 -20
- package/types/src/api.d.ts +3 -3
- package/types/src/api.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.map +1 -1
- package/types/src/entity.d.ts.map +1 -1
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +12 -4
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts +1 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/utility.d.ts.map +1 -1
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',
|
|
@@ -126,6 +111,21 @@ const libSaml = () => {
|
|
|
126
111
|
const defaultLogoutResponseTemplate = {
|
|
127
112
|
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
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* @private
|
|
116
|
+
* @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module
|
|
117
|
+
* @param {string} sigAlg signature algorithm
|
|
118
|
+
* @return {string/null} signing algorithm short-hand for the module node-rsa
|
|
119
|
+
*/
|
|
120
|
+
function getSigningScheme(sigAlg) {
|
|
121
|
+
if (sigAlg) {
|
|
122
|
+
const algAlias = nrsaAliasMapping[sigAlg];
|
|
123
|
+
if (!(algAlias === undefined)) {
|
|
124
|
+
return algAlias;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1];
|
|
128
|
+
}
|
|
129
129
|
function validateAndInflateSamlResponse(urlEncodedResponse) {
|
|
130
130
|
// 3. 尝试DEFLATE解压(SAML规范要求使用原始DEFLATE)
|
|
131
131
|
let xml = "";
|
|
@@ -133,18 +133,14 @@ const libSaml = () => {
|
|
|
133
133
|
try { // 1. URL解码
|
|
134
134
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
135
135
|
// 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 });
|
|
136
|
+
xml = inflateString(base64Encoded);
|
|
142
137
|
}
|
|
143
138
|
catch (inflateError) {
|
|
144
139
|
// 4. 解压失败,尝试直接解析为未压缩的XML
|
|
140
|
+
console.log("解压失败---------------------");
|
|
145
141
|
try {
|
|
146
142
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
147
|
-
xml =
|
|
143
|
+
xml = atob(base64Encoded);
|
|
148
144
|
return { compressed: false, xml, error: null };
|
|
149
145
|
}
|
|
150
146
|
catch (xmlError) {
|
|
@@ -277,6 +273,9 @@ const libSaml = () => {
|
|
|
277
273
|
};
|
|
278
274
|
// 生成 XML(关闭自动声明头)
|
|
279
275
|
const xmlString = xml([attributeStatement], { declaration: false });
|
|
276
|
+
if (xmlString.trim() === '<saml:AttributeStatement></saml:AttributeStatement>') {
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
280
279
|
return xmlString.trim();
|
|
281
280
|
},
|
|
282
281
|
/**
|
|
@@ -324,7 +323,7 @@ const libSaml = () => {
|
|
|
324
323
|
else {
|
|
325
324
|
sig.computeSignature(rawSamlMessage);
|
|
326
325
|
}
|
|
327
|
-
return isBase64Output
|
|
326
|
+
return isBase64Output ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml();
|
|
328
327
|
},
|
|
329
328
|
/**
|
|
330
329
|
* @desc Verify the XML signature
|
|
@@ -337,29 +336,68 @@ const libSaml = () => {
|
|
|
337
336
|
// tslint:disable-next-line:no-shadowed-variable
|
|
338
337
|
verifySignature(xml, opts) {
|
|
339
338
|
const { dom } = getContext();
|
|
340
|
-
const doc = dom.parseFromString(xml);
|
|
339
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
341
340
|
const docParser = new DOMParser();
|
|
342
341
|
// In order to avoid the wrapping attack, we have changed to use absolute xpath instead of naively fetching the signature element
|
|
342
|
+
const LogoutResponseSignatureXpath = "/*[local-name()='LogoutResponse']/*[local-name()='Signature']";
|
|
343
|
+
const logoutRequestSignatureXpath = "/*[local-name()='LogoutRequest']/*[local-name()='Signature']";
|
|
343
344
|
// message signature (logout response / saml response)
|
|
344
345
|
const messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']";
|
|
345
346
|
// assertion signature (logout response / saml response)
|
|
346
347
|
const assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']";
|
|
347
348
|
// check if there is a potential malicious wrapping signature
|
|
348
349
|
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']";
|
|
350
|
+
// 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']";
|
|
351
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
352
|
+
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
353
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
349
354
|
// select the signature node
|
|
350
355
|
let selection = [];
|
|
356
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
351
357
|
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
358
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
352
359
|
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
360
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
353
361
|
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
363
|
+
const LogoutResponseSignatureElementNode = select(LogoutResponseSignatureXpath, doc);
|
|
356
364
|
// try to catch potential wrapping attack
|
|
357
365
|
if (wrappingElementNode.length !== 0) {
|
|
358
366
|
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
359
367
|
}
|
|
368
|
+
// 优先检测 LogoutRequest 签名
|
|
369
|
+
// @ts-expect-error missing Node properties
|
|
370
|
+
const logoutRequestSignature = select(logoutRequestSignatureXpath, doc);
|
|
371
|
+
if (logoutRequestSignature.length > 0) {
|
|
372
|
+
selection = selection.concat(logoutRequestSignature);
|
|
373
|
+
}
|
|
374
|
+
selection = selection.concat(messageSignatureNode);
|
|
375
|
+
selection = selection.concat(assertionSignatureNode);
|
|
376
|
+
selection = selection.concat(LogoutResponseSignatureElementNode);
|
|
360
377
|
// guarantee to have a signature in saml response
|
|
361
378
|
if (selection.length === 0) {
|
|
362
|
-
|
|
379
|
+
/** 判断有没有加密如果没有加密返回 [false, null]*/
|
|
380
|
+
if (encryptedAssertions.length > 0) {
|
|
381
|
+
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
382
|
+
return [false, null, false, true]; // we return false now
|
|
383
|
+
}
|
|
384
|
+
if (encryptedAssertions.length > 1) {
|
|
385
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
386
|
+
}
|
|
387
|
+
return [false, null, true, true]; // return encryptedAssert
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (selection.length !== 0) {
|
|
391
|
+
/** 判断有没有加密如果没有加密返回 [false, null]*/
|
|
392
|
+
if (logoutRequestSignature.length === 0 && LogoutResponseSignatureElementNode.length === 0 && encryptedAssertions.length > 0) {
|
|
393
|
+
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
394
|
+
return [false, null, true, false]; // we return false now
|
|
395
|
+
}
|
|
396
|
+
if (encryptedAssertions.length > 1) {
|
|
397
|
+
throw new Error('ERR_MULTIPLE_ASSERTION');
|
|
398
|
+
}
|
|
399
|
+
return [false, null, true, false]; // return encryptedAssert
|
|
400
|
+
}
|
|
363
401
|
}
|
|
364
402
|
// need to refactor later on
|
|
365
403
|
for (const signatureNode of selection) {
|
|
@@ -407,7 +445,6 @@ const libSaml = () => {
|
|
|
407
445
|
}
|
|
408
446
|
}
|
|
409
447
|
sig.loadSignature(signatureNode);
|
|
410
|
-
doc.removeChild(signatureNode);
|
|
411
448
|
verified = sig.checkSignature(doc.toString());
|
|
412
449
|
// immediately throw error when any one of the signature is failed to get verified
|
|
413
450
|
if (!verified) {
|
|
@@ -420,86 +457,56 @@ const libSaml = () => {
|
|
|
420
457
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
421
458
|
}
|
|
422
459
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
423
|
-
const rootNode = docParser.parseFromString(signedVerifiedXML, '
|
|
460
|
+
const rootNode = docParser.parseFromString(signedVerifiedXML, 'application/xml').documentElement;
|
|
424
461
|
// process the verified signature:
|
|
425
462
|
// case 1, rootSignedDoc is a response:
|
|
426
|
-
if (rootNode
|
|
463
|
+
if (rootNode?.localName === 'Response') {
|
|
427
464
|
// try getting the Xml from the first assertion
|
|
428
|
-
const EncryptedAssertions = select("./*[local-name()='EncryptedAssertion']",
|
|
429
|
-
|
|
465
|
+
const EncryptedAssertions = select("./*[local-name()='EncryptedAssertion']",
|
|
466
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
467
|
+
rootNode);
|
|
468
|
+
const assertions = select("./*[local-name()='Assertion']",
|
|
469
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
470
|
+
rootNode);
|
|
430
471
|
/**第三个参数代表是否加密*/
|
|
431
472
|
// now we can process the assertion as an assertion
|
|
432
473
|
if (EncryptedAssertions.length === 1) {
|
|
433
474
|
/** 已加密*/
|
|
434
|
-
return [true, EncryptedAssertions[0].toString(), true];
|
|
475
|
+
return [true, EncryptedAssertions[0].toString(), true, false];
|
|
435
476
|
}
|
|
436
477
|
if (assertions.length === 1) {
|
|
437
|
-
return [true, assertions[0].toString(), false];
|
|
478
|
+
return [true, assertions[0].toString(), false, false];
|
|
438
479
|
}
|
|
439
480
|
}
|
|
440
|
-
else if (rootNode
|
|
441
|
-
return [true, rootNode.toString(), false];
|
|
481
|
+
else if (rootNode?.localName === 'Assertion') {
|
|
482
|
+
return [true, rootNode.toString(), false, false];
|
|
442
483
|
}
|
|
443
|
-
else if (rootNode
|
|
444
|
-
return [true, rootNode.toString(), true];
|
|
484
|
+
else if (rootNode?.localName === 'EncryptedAssertion') {
|
|
485
|
+
return [true, rootNode.toString(), true, false];
|
|
486
|
+
}
|
|
487
|
+
else if (rootNode?.localName === 'LogoutRequest') {
|
|
488
|
+
return [true, rootNode.toString(), false, false];
|
|
489
|
+
}
|
|
490
|
+
else if (rootNode?.localName === 'LogoutResponse') {
|
|
491
|
+
return [true, rootNode.toString(), false, false];
|
|
445
492
|
}
|
|
446
493
|
else {
|
|
447
|
-
return [true, null]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
|
|
494
|
+
return [true, null, false, false]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
|
|
448
495
|
}
|
|
449
496
|
}
|
|
450
497
|
// something has gone seriously wrong if we are still here
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// default we will take the assertion section under root
|
|
454
|
-
/* if (messageSignatureNode.length === 1) {
|
|
455
|
-
const node = select("/!*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/!*[local-name(.)='Assertion']", doc);
|
|
456
|
-
if (node.length === 1) {
|
|
457
|
-
assertionNode = node[0].toString();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (assertionSignatureNode.length === 1) {
|
|
462
|
-
const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{
|
|
463
|
-
key: 'refURI',
|
|
464
|
-
localPath: ['Signature', 'SignedInfo', 'Reference'],
|
|
465
|
-
attributes: ['URI']
|
|
466
|
-
}]);
|
|
467
|
-
// get the assertion supposed to be the one should be verified
|
|
468
|
-
const desiredAssertionInfo = extract(doc.toString(), [{
|
|
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();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return [verified, assertionNode];*/
|
|
498
|
+
return [false, null, false, true]; // return encryptedAssert
|
|
499
|
+
/* throw new Error('ERR_ZERO_SIGNATURE');*/
|
|
494
500
|
},
|
|
495
501
|
verifySignatureSoap(xml, opts) {
|
|
496
502
|
const { dom } = getContext();
|
|
497
|
-
const doc = dom.parseFromString(xml);
|
|
503
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
498
504
|
const docParser = new DOMParser();
|
|
499
505
|
let selection = [];
|
|
500
506
|
if (opts.isAssertion) {
|
|
501
507
|
// 断言模式下的专用逻辑
|
|
502
508
|
const assertionSignatureXpath = "./*[local-name()='Signature']";
|
|
509
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
503
510
|
const signatureNode = select(assertionSignatureXpath, doc.documentElement);
|
|
504
511
|
if (signatureNode.length === 0) {
|
|
505
512
|
throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
|
|
@@ -524,8 +531,11 @@ const libSaml = () => {
|
|
|
524
531
|
"/*[local-name()='SubjectConfirmation']" +
|
|
525
532
|
"/*[local-name()='SubjectConfirmationData']" +
|
|
526
533
|
"//*[local-name()='Assertion' or local-name()='Signature']";
|
|
534
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
527
535
|
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
536
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
528
537
|
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
538
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
529
539
|
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
530
540
|
// 检测包装攻击
|
|
531
541
|
if (wrappingElementNode.length !== 0) {
|
|
@@ -590,9 +600,7 @@ const libSaml = () => {
|
|
|
590
600
|
sig.loadSignature(signatureNode);
|
|
591
601
|
// 使用原始 XML 进行验证
|
|
592
602
|
verified = sig.checkSignature(xml);
|
|
593
|
-
console.log("签名验证结果:", verified);
|
|
594
603
|
if (!verified) {
|
|
595
|
-
console.error("签名验证失败");
|
|
596
604
|
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
597
605
|
}
|
|
598
606
|
// 检查签名引用
|
|
@@ -600,12 +608,11 @@ const libSaml = () => {
|
|
|
600
608
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
601
609
|
}
|
|
602
610
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
603
|
-
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, '
|
|
611
|
+
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'application/xml');
|
|
604
612
|
const rootNode = verifiedDoc.documentElement;
|
|
605
|
-
console.log("签名引用根节点:", rootNode.localName);
|
|
606
613
|
// 断言模式专用返回逻辑
|
|
607
614
|
if (opts.isAssertion) {
|
|
608
|
-
if (rootNode
|
|
615
|
+
if (rootNode?.localName === 'Assertion') {
|
|
609
616
|
return [true, rootNode.toString(), false];
|
|
610
617
|
}
|
|
611
618
|
else {
|
|
@@ -613,11 +620,14 @@ const libSaml = () => {
|
|
|
613
620
|
}
|
|
614
621
|
}
|
|
615
622
|
// 处理已验证的签名
|
|
623
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
616
624
|
if (rootNode.localName === 'ArtifactResponse') {
|
|
617
625
|
// 在 ArtifactResponse 中查找 Response
|
|
618
|
-
|
|
626
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
627
|
+
const responseNodes = select("./*[local-name()='Response']",
|
|
628
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
629
|
+
rootNode);
|
|
619
630
|
if (responseNodes.length === 0) {
|
|
620
|
-
console.warn("ArtifactResponse 中没有找到 Response 元素");
|
|
621
631
|
continue;
|
|
622
632
|
}
|
|
623
633
|
const responseNode = responseNodes[0];
|
|
@@ -632,9 +642,15 @@ const libSaml = () => {
|
|
|
632
642
|
}
|
|
633
643
|
}
|
|
634
644
|
// 直接处理 Response
|
|
635
|
-
else if (rootNode
|
|
636
|
-
|
|
637
|
-
const
|
|
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);
|
|
638
654
|
if (encryptedAssertions.length === 1) {
|
|
639
655
|
return [true, encryptedAssertions[0].toString(), true];
|
|
640
656
|
}
|
|
@@ -643,15 +659,15 @@ const libSaml = () => {
|
|
|
643
659
|
}
|
|
644
660
|
}
|
|
645
661
|
// 直接处理 Assertion
|
|
646
|
-
else if (rootNode
|
|
662
|
+
else if (rootNode?.localName === 'Assertion') {
|
|
647
663
|
return [true, rootNode.toString(), false];
|
|
648
664
|
}
|
|
649
665
|
// 直接处理 EncryptedAssertion
|
|
650
|
-
else if (rootNode
|
|
666
|
+
else if (rootNode?.localName === 'EncryptedAssertion') {
|
|
651
667
|
return [true, rootNode.toString(), true];
|
|
652
668
|
}
|
|
653
669
|
else {
|
|
654
|
-
console.warn("未知的根节点类型:", rootNode
|
|
670
|
+
console.warn("未知的根节点类型:", rootNode?.localName);
|
|
655
671
|
}
|
|
656
672
|
}
|
|
657
673
|
throw new Error('ERR_ZERO_SIGNATURE');
|
|
@@ -694,59 +710,43 @@ const libSaml = () => {
|
|
|
694
710
|
* @param signingAlgorithm - 签名算法 (默认 'rsa-sha256')
|
|
695
711
|
* @returns 消息签名
|
|
696
712
|
*/
|
|
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
|
-
}
|
|
713
|
+
constructMessageSignature(octetString, key, passphrase, isBase64, signingAlgorithm) {
|
|
714
|
+
// Default returning base64 encoded signature
|
|
715
|
+
// Embed with node-rsa module
|
|
716
|
+
const decryptedKey = new nrsa(utility.readPrivateKey(key, passphrase), undefined, {
|
|
717
|
+
signingScheme: getSigningScheme(signingAlgorithm),
|
|
718
|
+
});
|
|
719
|
+
const signature = decryptedKey.sign(octetString);
|
|
720
|
+
// Use private key to sign data
|
|
721
|
+
return isBase64 !== false ? signature.toString('base64') : signature;
|
|
722
722
|
},
|
|
723
|
-
/*
|
|
723
|
+
/* verifyMessageSignature(
|
|
724
|
+
metadata,
|
|
724
725
|
octetString: string,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
isBase64?: boolean,
|
|
728
|
-
signingAlgorithm?: string
|
|
726
|
+
signature: string | Buffer,
|
|
727
|
+
verifyAlgorithm?: string
|
|
729
728
|
) {
|
|
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;
|
|
729
|
+
const signCert = metadata.getX509Certificate(certUse.signing);
|
|
730
|
+
const signingScheme = getSigningSchemeForNode(verifyAlgorithm);
|
|
731
|
+
const verifier = createVerify(signingScheme);
|
|
732
|
+
verifier.update(octetString);
|
|
733
|
+
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
734
|
+
return isValid
|
|
735
|
+
|
|
742
736
|
},*/
|
|
737
|
+
/**
|
|
738
|
+
* @desc Verifies message signature
|
|
739
|
+
* @param {Metadata} metadata metadata object of identity provider or service provider
|
|
740
|
+
* @param {string} octetString see "Bindings for the OASIS Security Assertion Markup Language (SAML V2.0)" P.17/46
|
|
741
|
+
* @param {string} signature context of XML signature
|
|
742
|
+
* @param {string} verifyAlgorithm algorithm used to verify
|
|
743
|
+
* @return {boolean} verification result
|
|
744
|
+
*/
|
|
743
745
|
verifyMessageSignature(metadata, octetString, signature, verifyAlgorithm) {
|
|
744
746
|
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;
|
|
747
|
+
const signingScheme = getSigningScheme(verifyAlgorithm);
|
|
748
|
+
const key = new nrsa(utility.getPublicKeyPemFromCertificate(signCert), 'public', { signingScheme });
|
|
749
|
+
return key.verify(Buffer.from(octetString), Buffer.from(signature));
|
|
750
750
|
},
|
|
751
751
|
/**
|
|
752
752
|
* @desc Get the public key in string format
|
|
@@ -781,7 +781,8 @@ const libSaml = () => {
|
|
|
781
781
|
const sourceEntitySetting = sourceEntity.entitySetting;
|
|
782
782
|
const targetEntityMetadata = targetEntity.entityMeta;
|
|
783
783
|
const { dom } = getContext();
|
|
784
|
-
const doc = dom.parseFromString(xml);
|
|
784
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
785
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
785
786
|
const assertions = select("//*[local-name(.)='Assertion']", doc);
|
|
786
787
|
if (!Array.isArray(assertions) || assertions.length === 0) {
|
|
787
788
|
throw new Error('ERR_NO_ASSERTION');
|
|
@@ -799,7 +800,7 @@ const libSaml = () => {
|
|
|
799
800
|
pem: Buffer.from(`-----BEGIN CERTIFICATE-----${targetEntityMetadata.getX509Certificate(certUse.encrypt)}-----END CERTIFICATE-----`),
|
|
800
801
|
encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm,
|
|
801
802
|
keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm,
|
|
802
|
-
keyEncryptionDigest: 'SHA-512'
|
|
803
|
+
/* keyEncryptionDigest: 'SHA-512',*/
|
|
803
804
|
disallowEncryptionWithInsecureAlgorithm: true,
|
|
804
805
|
warnInsecureAlgorithm: true
|
|
805
806
|
}, (err, res) => {
|
|
@@ -810,7 +811,8 @@ const libSaml = () => {
|
|
|
810
811
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
811
812
|
}
|
|
812
813
|
const { encryptedAssertion: encAssertionPrefix } = sourceEntitySetting.tagPrefix;
|
|
813
|
-
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion
|
|
814
|
+
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion>`, 'application/xml');
|
|
815
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
814
816
|
doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode);
|
|
815
817
|
return resolve(utility.base64Encode(doc.toString()));
|
|
816
818
|
});
|
|
@@ -837,7 +839,8 @@ const libSaml = () => {
|
|
|
837
839
|
// Perform encryption depends on the setting of where the message is sent, default is false
|
|
838
840
|
const hereSetting = here.entitySetting;
|
|
839
841
|
const { dom } = getContext();
|
|
840
|
-
const doc = dom.parseFromString(entireXML);
|
|
842
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
843
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
841
844
|
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
842
845
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
843
846
|
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
@@ -850,13 +853,13 @@ const libSaml = () => {
|
|
|
850
853
|
key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass),
|
|
851
854
|
}, (err, res) => {
|
|
852
855
|
if (err) {
|
|
853
|
-
console.error(err);
|
|
854
856
|
return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION'));
|
|
855
857
|
}
|
|
856
858
|
if (!res) {
|
|
857
859
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
858
860
|
}
|
|
859
|
-
const rawAssertionDoc = dom.parseFromString(res);
|
|
861
|
+
const rawAssertionDoc = dom.parseFromString(res, 'application/xml');
|
|
862
|
+
// @ts-ignore
|
|
860
863
|
doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode);
|
|
861
864
|
return resolve([doc.toString(), res]);
|
|
862
865
|
});
|
|
@@ -872,11 +875,14 @@ const libSaml = () => {
|
|
|
872
875
|
const { dom } = getContext();
|
|
873
876
|
try {
|
|
874
877
|
// 1. 解析 XML
|
|
875
|
-
|
|
878
|
+
// @ts-ignore
|
|
879
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
876
880
|
// 2. 定位加密断言
|
|
877
881
|
const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
878
882
|
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
879
|
-
"/*[local-name()='EncryptedAssertion']",
|
|
883
|
+
"/*[local-name()='EncryptedAssertion']",
|
|
884
|
+
// @ts-ignore
|
|
885
|
+
doc);
|
|
880
886
|
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
881
887
|
throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
|
|
882
888
|
}
|
|
@@ -890,7 +896,6 @@ const libSaml = () => {
|
|
|
890
896
|
const decryptedAssertion = await new Promise((resolve, reject) => {
|
|
891
897
|
xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
|
|
892
898
|
if (err) {
|
|
893
|
-
console.error('解密错误:', err);
|
|
894
899
|
return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
|
|
895
900
|
}
|
|
896
901
|
if (!result) {
|
|
@@ -900,27 +905,28 @@ const libSaml = () => {
|
|
|
900
905
|
});
|
|
901
906
|
});
|
|
902
907
|
// 5. 创建解密断言的 DOM
|
|
903
|
-
|
|
908
|
+
// @ts-ignore
|
|
909
|
+
const decryptedDoc = dom.parseFromString(decryptedAssertion, 'application/xml');
|
|
904
910
|
const decryptedAssertionNode = decryptedDoc.documentElement;
|
|
905
911
|
// 6. 替换加密断言为解密后的断言
|
|
906
912
|
const parentNode = encAssertionNode.parentNode;
|
|
907
913
|
if (!parentNode) {
|
|
908
914
|
throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
|
|
909
915
|
}
|
|
916
|
+
// @ts-ignore
|
|
910
917
|
parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
|
|
911
918
|
// 7. 序列化更新后的文档
|
|
912
919
|
const updatedSoapXml = doc.toString();
|
|
913
920
|
return [updatedSoapXml, decryptedAssertion];
|
|
914
921
|
}
|
|
915
922
|
catch (error) {
|
|
916
|
-
console.error('SOAP断言解密失败:', error);
|
|
917
923
|
throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
|
|
918
924
|
}
|
|
919
925
|
},
|
|
920
926
|
/**
|
|
921
927
|
* @desc Check if the xml string is valid and bounded
|
|
922
928
|
*/
|
|
923
|
-
async isValidXml(input) {
|
|
929
|
+
async isValidXml(input, soap = false) {
|
|
924
930
|
// check if global api contains the validate function
|
|
925
931
|
const { validate } = getContext();
|
|
926
932
|
/**
|
|
@@ -934,7 +940,7 @@ const libSaml = () => {
|
|
|
934
940
|
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
941
|
}
|
|
936
942
|
try {
|
|
937
|
-
return await validate(input);
|
|
943
|
+
return await validate(input, soap);
|
|
938
944
|
}
|
|
939
945
|
catch (e) {
|
|
940
946
|
throw e;
|