samlesa 2.16.5 → 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 +88 -3
- package/build/src/binding-simplesign.js +0 -1
- package/build/src/entity-idp.js +1 -5
- package/build/src/entity-sp.js +115 -23
- package/build/src/extractor.js +29 -4
- package/build/src/flow.js +36 -103
- package/build/src/libsaml.js +172 -162
- package/build/src/metadata-sp.js +2 -0
- package/build/src/metadata.js +0 -2
- package/build/src/schema/saml-schema-ecp-2.0.xsd +1 -1
- package/build/src/schema/saml-schema-metadata-2.0.xsd +3 -3
- package/build/src/schema/saml-schema-protocol-2.0.xsd +1 -1
- package/build/src/schema/{env.xsd → soap-envelope.xsd} +1 -33
- package/build/src/schema/xml.xsd +88 -0
- package/build/src/schemaValidator.js +29 -12
- 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 +14 -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 +44 -21
- 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 +5 -0
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +15 -4
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
- package/types/src/metadata.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/index.js.map +0 -1
- package/build/src/api.js.map +0 -1
- package/build/src/binding-post.js.map +0 -1
- package/build/src/binding-redirect.js.map +0 -1
- package/build/src/binding-simplesign.js.map +0 -1
- package/build/src/entity-idp.js.map +0 -1
- package/build/src/entity-sp.js.map +0 -1
- package/build/src/entity.js.map +0 -1
- package/build/src/extractor.js.map +0 -1
- package/build/src/flow.js.map +0 -1
- package/build/src/libsaml.js.map +0 -1
- package/build/src/metadata-idp.js.map +0 -1
- package/build/src/metadata-sp.js.map +0 -1
- package/build/src/metadata.js.map +0 -1
- package/build/src/types.js.map +0 -1
- package/build/src/urn.js.map +0 -1
- package/build/src/utility.js.map +0 -1
- package/build/src/validator.js.map +0 -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',
|
|
@@ -70,19 +55,22 @@ const libSaml = () => {
|
|
|
70
55
|
context: '<samlp:AuthnRequest 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}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
|
|
71
56
|
};
|
|
72
57
|
/**
|
|
73
|
-
* @desc Default
|
|
58
|
+
* @desc Default logout request template
|
|
74
59
|
* @type {LogoutRequestTemplate}
|
|
75
60
|
*/
|
|
76
61
|
const defaultLogoutRequestTemplate = {
|
|
77
62
|
context: '<samlp:LogoutRequest 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}"><saml:Issuer>{Issuer}</saml:Issuer><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID></samlp:LogoutRequest>',
|
|
78
63
|
};
|
|
79
64
|
/**
|
|
80
|
-
* @desc Default
|
|
65
|
+
* @desc Default art request template
|
|
81
66
|
* @type {LogoutRequestTemplate}
|
|
82
67
|
*/
|
|
83
68
|
const defaultArtifactResolveTemplate = {
|
|
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
|
};
|
|
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>`,
|
|
73
|
+
};
|
|
86
74
|
/**
|
|
87
75
|
* @desc Default AttributeStatement template
|
|
88
76
|
* @type {AttributeStatementTemplate}
|
|
@@ -123,6 +111,21 @@ const libSaml = () => {
|
|
|
123
111
|
const defaultLogoutResponseTemplate = {
|
|
124
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>',
|
|
125
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
|
+
}
|
|
126
129
|
function validateAndInflateSamlResponse(urlEncodedResponse) {
|
|
127
130
|
// 3. 尝试DEFLATE解压(SAML规范要求使用原始DEFLATE)
|
|
128
131
|
let xml = "";
|
|
@@ -130,18 +133,14 @@ const libSaml = () => {
|
|
|
130
133
|
try { // 1. URL解码
|
|
131
134
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
132
135
|
// 2. Base64解码为Uint8Array
|
|
133
|
-
|
|
134
|
-
const compressedData = new Uint8Array(binaryStr.length);
|
|
135
|
-
for (let i = 0; i < binaryStr.length; i++) {
|
|
136
|
-
compressedData[i] = binaryStr.charCodeAt(i);
|
|
137
|
-
}
|
|
138
|
-
xml = inflate(compressedData, { to: 'string', raw: true });
|
|
136
|
+
xml = inflateString(base64Encoded);
|
|
139
137
|
}
|
|
140
138
|
catch (inflateError) {
|
|
141
139
|
// 4. 解压失败,尝试直接解析为未压缩的XML
|
|
140
|
+
console.log("解压失败---------------------");
|
|
142
141
|
try {
|
|
143
142
|
const base64Encoded = decodeURIComponent(urlEncodedResponse);
|
|
144
|
-
xml =
|
|
143
|
+
xml = atob(base64Encoded);
|
|
145
144
|
return { compressed: false, xml, error: null };
|
|
146
145
|
}
|
|
147
146
|
catch (xmlError) {
|
|
@@ -209,6 +208,7 @@ const libSaml = () => {
|
|
|
209
208
|
createXPath,
|
|
210
209
|
getQueryParamByType,
|
|
211
210
|
defaultLoginRequestTemplate,
|
|
211
|
+
defaultArtAuthnRequestTemplate,
|
|
212
212
|
defaultArtifactResolveTemplate,
|
|
213
213
|
defaultLoginResponseTemplate,
|
|
214
214
|
defaultAttributeStatementTemplate,
|
|
@@ -273,6 +273,9 @@ const libSaml = () => {
|
|
|
273
273
|
};
|
|
274
274
|
// 生成 XML(关闭自动声明头)
|
|
275
275
|
const xmlString = xml([attributeStatement], { declaration: false });
|
|
276
|
+
if (xmlString.trim() === '<saml:AttributeStatement></saml:AttributeStatement>') {
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
276
279
|
return xmlString.trim();
|
|
277
280
|
},
|
|
278
281
|
/**
|
|
@@ -320,7 +323,7 @@ const libSaml = () => {
|
|
|
320
323
|
else {
|
|
321
324
|
sig.computeSignature(rawSamlMessage);
|
|
322
325
|
}
|
|
323
|
-
return isBase64Output
|
|
326
|
+
return isBase64Output ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml();
|
|
324
327
|
},
|
|
325
328
|
/**
|
|
326
329
|
* @desc Verify the XML signature
|
|
@@ -333,29 +336,68 @@ const libSaml = () => {
|
|
|
333
336
|
// tslint:disable-next-line:no-shadowed-variable
|
|
334
337
|
verifySignature(xml, opts) {
|
|
335
338
|
const { dom } = getContext();
|
|
336
|
-
const doc = dom.parseFromString(xml);
|
|
339
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
337
340
|
const docParser = new DOMParser();
|
|
338
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']";
|
|
339
344
|
// message signature (logout response / saml response)
|
|
340
345
|
const messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']";
|
|
341
346
|
// assertion signature (logout response / saml response)
|
|
342
347
|
const assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']";
|
|
343
348
|
// check if there is a potential malicious wrapping signature
|
|
344
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];
|
|
345
354
|
// select the signature node
|
|
346
355
|
let selection = [];
|
|
356
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
347
357
|
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
358
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
348
359
|
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
360
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
349
361
|
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
350
|
-
|
|
351
|
-
|
|
362
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
363
|
+
const LogoutResponseSignatureElementNode = select(LogoutResponseSignatureXpath, doc);
|
|
352
364
|
// try to catch potential wrapping attack
|
|
353
365
|
if (wrappingElementNode.length !== 0) {
|
|
354
366
|
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
355
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);
|
|
356
377
|
// guarantee to have a signature in saml response
|
|
357
378
|
if (selection.length === 0) {
|
|
358
|
-
|
|
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
|
+
}
|
|
359
401
|
}
|
|
360
402
|
// need to refactor later on
|
|
361
403
|
for (const signatureNode of selection) {
|
|
@@ -403,7 +445,6 @@ const libSaml = () => {
|
|
|
403
445
|
}
|
|
404
446
|
}
|
|
405
447
|
sig.loadSignature(signatureNode);
|
|
406
|
-
doc.removeChild(signatureNode);
|
|
407
448
|
verified = sig.checkSignature(doc.toString());
|
|
408
449
|
// immediately throw error when any one of the signature is failed to get verified
|
|
409
450
|
if (!verified) {
|
|
@@ -416,86 +457,56 @@ const libSaml = () => {
|
|
|
416
457
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
417
458
|
}
|
|
418
459
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
419
|
-
const rootNode = docParser.parseFromString(signedVerifiedXML, '
|
|
460
|
+
const rootNode = docParser.parseFromString(signedVerifiedXML, 'application/xml').documentElement;
|
|
420
461
|
// process the verified signature:
|
|
421
462
|
// case 1, rootSignedDoc is a response:
|
|
422
|
-
if (rootNode
|
|
463
|
+
if (rootNode?.localName === 'Response') {
|
|
423
464
|
// try getting the Xml from the first assertion
|
|
424
|
-
const EncryptedAssertions = select("./*[local-name()='EncryptedAssertion']",
|
|
425
|
-
|
|
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);
|
|
426
471
|
/**第三个参数代表是否加密*/
|
|
427
472
|
// now we can process the assertion as an assertion
|
|
428
473
|
if (EncryptedAssertions.length === 1) {
|
|
429
474
|
/** 已加密*/
|
|
430
|
-
return [true, EncryptedAssertions[0].toString(), true];
|
|
475
|
+
return [true, EncryptedAssertions[0].toString(), true, false];
|
|
431
476
|
}
|
|
432
477
|
if (assertions.length === 1) {
|
|
433
|
-
return [true, assertions[0].toString(), false];
|
|
478
|
+
return [true, assertions[0].toString(), false, false];
|
|
434
479
|
}
|
|
435
480
|
}
|
|
436
|
-
else if (rootNode
|
|
437
|
-
return [true, rootNode.toString(), false];
|
|
481
|
+
else if (rootNode?.localName === 'Assertion') {
|
|
482
|
+
return [true, rootNode.toString(), false, false];
|
|
438
483
|
}
|
|
439
|
-
else if (rootNode
|
|
440
|
-
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];
|
|
441
492
|
}
|
|
442
493
|
else {
|
|
443
|
-
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
|
|
444
495
|
}
|
|
445
496
|
}
|
|
446
497
|
// something has gone seriously wrong if we are still here
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// default we will take the assertion section under root
|
|
450
|
-
/* if (messageSignatureNode.length === 1) {
|
|
451
|
-
const node = select("/!*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/!*[local-name(.)='Assertion']", doc);
|
|
452
|
-
if (node.length === 1) {
|
|
453
|
-
assertionNode = node[0].toString();
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (assertionSignatureNode.length === 1) {
|
|
458
|
-
const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{
|
|
459
|
-
key: 'refURI',
|
|
460
|
-
localPath: ['Signature', 'SignedInfo', 'Reference'],
|
|
461
|
-
attributes: ['URI']
|
|
462
|
-
}]);
|
|
463
|
-
// get the assertion supposed to be the one should be verified
|
|
464
|
-
const desiredAssertionInfo = extract(doc.toString(), [{
|
|
465
|
-
key: 'id',
|
|
466
|
-
localPath: ['~Response', 'Assertion'],
|
|
467
|
-
attributes: ['ID']
|
|
468
|
-
}]);
|
|
469
|
-
// 5.4.2 References
|
|
470
|
-
// SAML assertions and protocol messages MUST supply a value for the ID attribute on the root element of
|
|
471
|
-
// the assertion or protocol message being signed. The assertion’s or protocol message's root element may
|
|
472
|
-
// or may not be the root element of the actual XML document containing the signed assertion or protocol
|
|
473
|
-
// message (e.g., it might be contained within a SOAP envelope).
|
|
474
|
-
// Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID
|
|
475
|
-
// attribute value of the root element of the assertion or protocol message being signed. For example, if the
|
|
476
|
-
// ID attribute value is "foo", then the URI attribute in the <ds:Reference> element MUST be "#foo".
|
|
477
|
-
if (verifiedAssertionInfo.refURI !== `#${desiredAssertionInfo.id}`) {
|
|
478
|
-
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
479
|
-
}
|
|
480
|
-
const verifiedDoc = extract(doc.toString(), [{
|
|
481
|
-
key: 'assertion',
|
|
482
|
-
localPath: ['~Response', 'Assertion'],
|
|
483
|
-
attributes: [],
|
|
484
|
-
context: true
|
|
485
|
-
}]);
|
|
486
|
-
assertionNode = verifiedDoc.assertion.toString();
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return [verified, assertionNode];*/
|
|
498
|
+
return [false, null, false, true]; // return encryptedAssert
|
|
499
|
+
/* throw new Error('ERR_ZERO_SIGNATURE');*/
|
|
490
500
|
},
|
|
491
501
|
verifySignatureSoap(xml, opts) {
|
|
492
502
|
const { dom } = getContext();
|
|
493
|
-
const doc = dom.parseFromString(xml);
|
|
503
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
494
504
|
const docParser = new DOMParser();
|
|
495
505
|
let selection = [];
|
|
496
506
|
if (opts.isAssertion) {
|
|
497
507
|
// 断言模式下的专用逻辑
|
|
498
508
|
const assertionSignatureXpath = "./*[local-name()='Signature']";
|
|
509
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
499
510
|
const signatureNode = select(assertionSignatureXpath, doc.documentElement);
|
|
500
511
|
if (signatureNode.length === 0) {
|
|
501
512
|
throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
|
|
@@ -520,8 +531,11 @@ const libSaml = () => {
|
|
|
520
531
|
"/*[local-name()='SubjectConfirmation']" +
|
|
521
532
|
"/*[local-name()='SubjectConfirmationData']" +
|
|
522
533
|
"//*[local-name()='Assertion' or local-name()='Signature']";
|
|
534
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
523
535
|
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
536
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
524
537
|
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
538
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
525
539
|
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
526
540
|
// 检测包装攻击
|
|
527
541
|
if (wrappingElementNode.length !== 0) {
|
|
@@ -586,9 +600,7 @@ const libSaml = () => {
|
|
|
586
600
|
sig.loadSignature(signatureNode);
|
|
587
601
|
// 使用原始 XML 进行验证
|
|
588
602
|
verified = sig.checkSignature(xml);
|
|
589
|
-
console.log("签名验证结果:", verified);
|
|
590
603
|
if (!verified) {
|
|
591
|
-
console.error("签名验证失败");
|
|
592
604
|
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
593
605
|
}
|
|
594
606
|
// 检查签名引用
|
|
@@ -596,12 +608,11 @@ const libSaml = () => {
|
|
|
596
608
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
597
609
|
}
|
|
598
610
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
599
|
-
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, '
|
|
611
|
+
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'application/xml');
|
|
600
612
|
const rootNode = verifiedDoc.documentElement;
|
|
601
|
-
console.log("签名引用根节点:", rootNode.localName);
|
|
602
613
|
// 断言模式专用返回逻辑
|
|
603
614
|
if (opts.isAssertion) {
|
|
604
|
-
if (rootNode
|
|
615
|
+
if (rootNode?.localName === 'Assertion') {
|
|
605
616
|
return [true, rootNode.toString(), false];
|
|
606
617
|
}
|
|
607
618
|
else {
|
|
@@ -609,11 +620,14 @@ const libSaml = () => {
|
|
|
609
620
|
}
|
|
610
621
|
}
|
|
611
622
|
// 处理已验证的签名
|
|
623
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
612
624
|
if (rootNode.localName === 'ArtifactResponse') {
|
|
613
625
|
// 在 ArtifactResponse 中查找 Response
|
|
614
|
-
|
|
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);
|
|
615
630
|
if (responseNodes.length === 0) {
|
|
616
|
-
console.warn("ArtifactResponse 中没有找到 Response 元素");
|
|
617
631
|
continue;
|
|
618
632
|
}
|
|
619
633
|
const responseNode = responseNodes[0];
|
|
@@ -628,9 +642,15 @@ const libSaml = () => {
|
|
|
628
642
|
}
|
|
629
643
|
}
|
|
630
644
|
// 直接处理 Response
|
|
631
|
-
else if (rootNode
|
|
632
|
-
|
|
633
|
-
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);
|
|
634
654
|
if (encryptedAssertions.length === 1) {
|
|
635
655
|
return [true, encryptedAssertions[0].toString(), true];
|
|
636
656
|
}
|
|
@@ -639,15 +659,15 @@ const libSaml = () => {
|
|
|
639
659
|
}
|
|
640
660
|
}
|
|
641
661
|
// 直接处理 Assertion
|
|
642
|
-
else if (rootNode
|
|
662
|
+
else if (rootNode?.localName === 'Assertion') {
|
|
643
663
|
return [true, rootNode.toString(), false];
|
|
644
664
|
}
|
|
645
665
|
// 直接处理 EncryptedAssertion
|
|
646
|
-
else if (rootNode
|
|
666
|
+
else if (rootNode?.localName === 'EncryptedAssertion') {
|
|
647
667
|
return [true, rootNode.toString(), true];
|
|
648
668
|
}
|
|
649
669
|
else {
|
|
650
|
-
console.warn("未知的根节点类型:", rootNode
|
|
670
|
+
console.warn("未知的根节点类型:", rootNode?.localName);
|
|
651
671
|
}
|
|
652
672
|
}
|
|
653
673
|
throw new Error('ERR_ZERO_SIGNATURE');
|
|
@@ -690,59 +710,43 @@ const libSaml = () => {
|
|
|
690
710
|
* @param signingAlgorithm - 签名算法 (默认 'rsa-sha256')
|
|
691
711
|
* @returns 消息签名
|
|
692
712
|
*/
|
|
693
|
-
constructMessageSignature(octetString, key, passphrase, isBase64
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// 3. 加载私钥
|
|
703
|
-
const privateKey = createPrivateKey({
|
|
704
|
-
key: key,
|
|
705
|
-
format: 'pem',
|
|
706
|
-
passphrase: passphrase,
|
|
707
|
-
encoding: 'utf8'
|
|
708
|
-
});
|
|
709
|
-
signer.write(octetString);
|
|
710
|
-
signer.end();
|
|
711
|
-
const signature = signer.sign(privateKey, 'base64');
|
|
712
|
-
// 5. 处理编码输出
|
|
713
|
-
return isBase64 ? signature.toString() : signature;
|
|
714
|
-
}
|
|
715
|
-
catch (error) {
|
|
716
|
-
throw new Error(`SAML 签名失败: ${error.message}`);
|
|
717
|
-
}
|
|
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;
|
|
718
722
|
},
|
|
719
|
-
/*
|
|
723
|
+
/* verifyMessageSignature(
|
|
724
|
+
metadata,
|
|
720
725
|
octetString: string,
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
isBase64?: boolean,
|
|
724
|
-
signingAlgorithm?: string
|
|
726
|
+
signature: string | Buffer,
|
|
727
|
+
verifyAlgorithm?: string
|
|
725
728
|
) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
);
|
|
735
|
-
const signature = decryptedKey.sign(octetString);
|
|
736
|
-
// Use private key to sign data
|
|
737
|
-
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
|
+
|
|
738
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
|
+
*/
|
|
739
745
|
verifyMessageSignature(metadata, octetString, signature, verifyAlgorithm) {
|
|
740
746
|
const signCert = metadata.getX509Certificate(certUse.signing);
|
|
741
|
-
const signingScheme =
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
745
|
-
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));
|
|
746
750
|
},
|
|
747
751
|
/**
|
|
748
752
|
* @desc Get the public key in string format
|
|
@@ -777,7 +781,8 @@ const libSaml = () => {
|
|
|
777
781
|
const sourceEntitySetting = sourceEntity.entitySetting;
|
|
778
782
|
const targetEntityMetadata = targetEntity.entityMeta;
|
|
779
783
|
const { dom } = getContext();
|
|
780
|
-
const doc = dom.parseFromString(xml);
|
|
784
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
785
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
781
786
|
const assertions = select("//*[local-name(.)='Assertion']", doc);
|
|
782
787
|
if (!Array.isArray(assertions) || assertions.length === 0) {
|
|
783
788
|
throw new Error('ERR_NO_ASSERTION');
|
|
@@ -795,7 +800,7 @@ const libSaml = () => {
|
|
|
795
800
|
pem: Buffer.from(`-----BEGIN CERTIFICATE-----${targetEntityMetadata.getX509Certificate(certUse.encrypt)}-----END CERTIFICATE-----`),
|
|
796
801
|
encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm,
|
|
797
802
|
keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm,
|
|
798
|
-
keyEncryptionDigest: 'SHA-512'
|
|
803
|
+
/* keyEncryptionDigest: 'SHA-512',*/
|
|
799
804
|
disallowEncryptionWithInsecureAlgorithm: true,
|
|
800
805
|
warnInsecureAlgorithm: true
|
|
801
806
|
}, (err, res) => {
|
|
@@ -806,7 +811,8 @@ const libSaml = () => {
|
|
|
806
811
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
807
812
|
}
|
|
808
813
|
const { encryptedAssertion: encAssertionPrefix } = sourceEntitySetting.tagPrefix;
|
|
809
|
-
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
|
|
810
816
|
doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode);
|
|
811
817
|
return resolve(utility.base64Encode(doc.toString()));
|
|
812
818
|
});
|
|
@@ -833,7 +839,8 @@ const libSaml = () => {
|
|
|
833
839
|
// Perform encryption depends on the setting of where the message is sent, default is false
|
|
834
840
|
const hereSetting = here.entitySetting;
|
|
835
841
|
const { dom } = getContext();
|
|
836
|
-
const doc = dom.parseFromString(entireXML);
|
|
842
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
843
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
837
844
|
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc);
|
|
838
845
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
839
846
|
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
@@ -846,13 +853,13 @@ const libSaml = () => {
|
|
|
846
853
|
key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass),
|
|
847
854
|
}, (err, res) => {
|
|
848
855
|
if (err) {
|
|
849
|
-
console.error(err);
|
|
850
856
|
return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION'));
|
|
851
857
|
}
|
|
852
858
|
if (!res) {
|
|
853
859
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
854
860
|
}
|
|
855
|
-
const rawAssertionDoc = dom.parseFromString(res);
|
|
861
|
+
const rawAssertionDoc = dom.parseFromString(res, 'application/xml');
|
|
862
|
+
// @ts-ignore
|
|
856
863
|
doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode);
|
|
857
864
|
return resolve([doc.toString(), res]);
|
|
858
865
|
});
|
|
@@ -868,11 +875,14 @@ const libSaml = () => {
|
|
|
868
875
|
const { dom } = getContext();
|
|
869
876
|
try {
|
|
870
877
|
// 1. 解析 XML
|
|
871
|
-
|
|
878
|
+
// @ts-ignore
|
|
879
|
+
const doc = dom.parseFromString(entireXML, 'application/xml');
|
|
872
880
|
// 2. 定位加密断言
|
|
873
881
|
const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
874
882
|
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
875
|
-
"/*[local-name()='EncryptedAssertion']",
|
|
883
|
+
"/*[local-name()='EncryptedAssertion']",
|
|
884
|
+
// @ts-ignore
|
|
885
|
+
doc);
|
|
876
886
|
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
877
887
|
throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
|
|
878
888
|
}
|
|
@@ -886,7 +896,6 @@ const libSaml = () => {
|
|
|
886
896
|
const decryptedAssertion = await new Promise((resolve, reject) => {
|
|
887
897
|
xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
|
|
888
898
|
if (err) {
|
|
889
|
-
console.error('解密错误:', err);
|
|
890
899
|
return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
|
|
891
900
|
}
|
|
892
901
|
if (!result) {
|
|
@@ -896,27 +905,28 @@ const libSaml = () => {
|
|
|
896
905
|
});
|
|
897
906
|
});
|
|
898
907
|
// 5. 创建解密断言的 DOM
|
|
899
|
-
|
|
908
|
+
// @ts-ignore
|
|
909
|
+
const decryptedDoc = dom.parseFromString(decryptedAssertion, 'application/xml');
|
|
900
910
|
const decryptedAssertionNode = decryptedDoc.documentElement;
|
|
901
911
|
// 6. 替换加密断言为解密后的断言
|
|
902
912
|
const parentNode = encAssertionNode.parentNode;
|
|
903
913
|
if (!parentNode) {
|
|
904
914
|
throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
|
|
905
915
|
}
|
|
916
|
+
// @ts-ignore
|
|
906
917
|
parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
|
|
907
918
|
// 7. 序列化更新后的文档
|
|
908
919
|
const updatedSoapXml = doc.toString();
|
|
909
920
|
return [updatedSoapXml, decryptedAssertion];
|
|
910
921
|
}
|
|
911
922
|
catch (error) {
|
|
912
|
-
console.error('SOAP断言解密失败:', error);
|
|
913
923
|
throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
|
|
914
924
|
}
|
|
915
925
|
},
|
|
916
926
|
/**
|
|
917
927
|
* @desc Check if the xml string is valid and bounded
|
|
918
928
|
*/
|
|
919
|
-
async isValidXml(input) {
|
|
929
|
+
async isValidXml(input, soap = false) {
|
|
920
930
|
// check if global api contains the validate function
|
|
921
931
|
const { validate } = getContext();
|
|
922
932
|
/**
|
|
@@ -930,7 +940,7 @@ const libSaml = () => {
|
|
|
930
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)');
|
|
931
941
|
}
|
|
932
942
|
try {
|
|
933
|
-
return await validate(input);
|
|
943
|
+
return await validate(input, soap);
|
|
934
944
|
}
|
|
935
945
|
catch (e) {
|
|
936
946
|
throw e;
|
package/build/src/metadata-sp.js
CHANGED
|
@@ -71,8 +71,10 @@ export class SpMetadata extends Metadata {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
if (isNonEmptyArray(artifactResolutionService)) {
|
|
74
|
+
let indexCount = 0;
|
|
74
75
|
artifactResolutionService.forEach(a => {
|
|
75
76
|
const attr = {
|
|
77
|
+
index: String(indexCount++),
|
|
76
78
|
Binding: a.Binding,
|
|
77
79
|
Location: a.Location,
|
|
78
80
|
};
|