samlesa 2.16.1 → 2.16.6

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.

Files changed (75) hide show
  1. package/build/src/binding-artifact.js +333 -0
  2. package/build/src/binding-redirect.js +97 -2
  3. package/build/src/entity-sp.js +138 -21
  4. package/build/src/extractor.js +13 -0
  5. package/build/src/flow.js +225 -5
  6. package/build/src/libsaml.js +233 -2
  7. package/build/src/metadata-idp.js +22 -0
  8. package/build/src/metadata-sp.js +19 -15
  9. package/build/src/metadata.js +50 -31
  10. package/build/src/schema/saml-schema-ecp-2.0.xsd +1 -1
  11. package/build/src/schema/saml-schema-metadata-2.0.xsd +3 -3
  12. package/build/src/schema/saml-schema-protocol-2.0.xsd +1 -1
  13. package/build/src/schema/soap-envelope.xsd +68 -0
  14. package/build/src/schema/xml.xsd +88 -0
  15. package/build/src/schemaValidator.js +28 -6
  16. package/build/src/soap.js +25 -0
  17. package/build/src/urn.js +5 -3
  18. package/package.json +2 -1
  19. package/types/{binding-post.d.ts → src/binding-artifact.d.ts} +25 -25
  20. package/types/src/binding-artifact.d.ts.map +1 -0
  21. package/types/src/binding-redirect.d.ts +14 -1
  22. package/types/src/binding-redirect.d.ts.map +1 -1
  23. package/types/src/entity-sp.d.ts +50 -20
  24. package/types/src/entity-sp.d.ts.map +1 -1
  25. package/types/src/extractor.d.ts +5 -0
  26. package/types/src/extractor.d.ts.map +1 -1
  27. package/types/src/flow.d.ts.map +1 -1
  28. package/types/src/libsaml.d.ts +16 -0
  29. package/types/src/libsaml.d.ts.map +1 -1
  30. package/types/src/metadata-idp.d.ts +6 -0
  31. package/types/src/metadata-idp.d.ts.map +1 -1
  32. package/types/src/metadata-sp.d.ts.map +1 -1
  33. package/types/src/metadata.d.ts +34 -27
  34. package/types/src/metadata.d.ts.map +1 -1
  35. package/types/src/schemaValidator.d.ts.map +1 -1
  36. package/types/src/soap.d.ts +2 -0
  37. package/types/src/soap.d.ts.map +1 -0
  38. package/types/src/urn.d.ts +2 -0
  39. package/types/src/urn.d.ts.map +1 -1
  40. package/build/.idea/build.iml +0 -12
  41. package/build/.idea/deployment.xml +0 -14
  42. package/build/.idea/modules.xml +0 -8
  43. package/types/api.d.ts +0 -15
  44. package/types/api.d.ts.map +0 -1
  45. package/types/binding-post.d.ts.map +0 -1
  46. package/types/binding-redirect.d.ts +0 -54
  47. package/types/binding-redirect.d.ts.map +0 -1
  48. package/types/binding-simplesign.d.ts +0 -41
  49. package/types/binding-simplesign.d.ts.map +0 -1
  50. package/types/entity-idp.d.ts +0 -38
  51. package/types/entity-idp.d.ts.map +0 -1
  52. package/types/entity-sp.d.ts +0 -38
  53. package/types/entity-sp.d.ts.map +0 -1
  54. package/types/entity.d.ts +0 -100
  55. package/types/entity.d.ts.map +0 -1
  56. package/types/extractor.d.ts +0 -26
  57. package/types/extractor.d.ts.map +0 -1
  58. package/types/flow.d.ts +0 -7
  59. package/types/flow.d.ts.map +0 -1
  60. package/types/libsaml.d.ts +0 -208
  61. package/types/libsaml.d.ts.map +0 -1
  62. package/types/metadata-idp.d.ts +0 -25
  63. package/types/metadata-idp.d.ts.map +0 -1
  64. package/types/metadata-sp.d.ts +0 -37
  65. package/types/metadata-sp.d.ts.map +0 -1
  66. package/types/metadata.d.ts +0 -58
  67. package/types/metadata.d.ts.map +0 -1
  68. package/types/types.d.ts +0 -128
  69. package/types/types.d.ts.map +0 -1
  70. package/types/urn.d.ts +0 -195
  71. package/types/urn.d.ts.map +0 -1
  72. package/types/utility.d.ts +0 -133
  73. package/types/utility.d.ts.map +0 -1
  74. package/types/validator.d.ts +0 -4
  75. package/types/validator.d.ts.map +0 -1
@@ -68,6 +68,19 @@ export const loginResponseStatusFields = [
68
68
  }
69
69
  ];
70
70
  // support two-tiers status code
71
+ export const loginArtifactResponseStatusFields = [
72
+ {
73
+ key: 'top',
74
+ localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'],
75
+ attributes: ['Value'],
76
+ },
77
+ {
78
+ key: 'second',
79
+ localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode', 'StatusCode'],
80
+ attributes: ['Value'],
81
+ }
82
+ ];
83
+ // support two-tiers status code
71
84
  export const logoutResponseStatusFields = [
72
85
  {
73
86
  key: 'top',
package/build/src/flow.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import { base64Decode } from './utility.js';
2
2
  import { verifyTime } from './validator.js';
3
3
  import libsaml from './libsaml.js';
4
- import { extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields, logoutResponseStatusFields, loginResponseStatusFields } from './extractor.js';
5
- import { BindingNamespace, ParserType, wording, StatusCode } from './urn.js';
4
+ import * as uuid from 'uuid';
5
+ import { select } from 'xpath';
6
+ import { DOMParser } from '@xmldom/xmldom';
7
+ import { sendArtifactResolve } from "./soap.js";
8
+ import { extract, loginRequestFields, loginResponseFields, loginResponseStatusFields, loginArtifactResponseStatusFields, logoutRequestFields, logoutResponseFields, logoutResponseStatusFields } from './extractor.js';
9
+ import { BindingNamespace, ParserType, StatusCode, wording } from './urn.js';
6
10
  const bindDict = wording.binding;
7
11
  const urlParams = wording.urlParams;
8
12
  // get the default extractor fields based on the parserType
@@ -122,6 +126,218 @@ async function redirectFlow(options) {
122
126
  }
123
127
  // proceed the post flow
124
128
  async function postFlow(options) {
129
+ const { soap = false, request, from, self, parserType, checkSignature = true } = options;
130
+ const { body } = request;
131
+ const direction = libsaml.getQueryParamByType(parserType);
132
+ let encodedRequest = '';
133
+ let samlContent = '';
134
+ if (soap === false) {
135
+ encodedRequest = body[direction];
136
+ // @ts-ignore
137
+ samlContent = String(base64Decode(encodedRequest));
138
+ }
139
+ /** 增加判断是不是Soap 工件绑定*/
140
+ if (soap) {
141
+ const metadata = {
142
+ idp: from.entityMeta,
143
+ sp: self.entityMeta,
144
+ };
145
+ const spSetting = self.entitySetting;
146
+ let ID = '_' + uuid.v4();
147
+ let url = metadata.idp.getArtifactResolutionService(bindDict.soap);
148
+ let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
149
+ ID: ID,
150
+ Destination: url,
151
+ Issuer: metadata.sp.getEntityID(),
152
+ IssueInstant: new Date().toISOString(),
153
+ Art: request.Art
154
+ });
155
+ if (!metadata.idp.isWantAuthnRequestsSigned()) {
156
+ samlContent = await sendArtifactResolve(url, samlSoapRaw);
157
+ }
158
+ if (metadata.idp.isWantAuthnRequestsSigned()) {
159
+ const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
160
+ let signatureSoap = libsaml.constructSAMLSignature({
161
+ referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
162
+ isMessageSigned: false,
163
+ isBase64Output: false,
164
+ transformationAlgorithms: transformationAlgorithms,
165
+ privateKey,
166
+ privateKeyPass,
167
+ signatureAlgorithm,
168
+ rawSamlMessage: samlSoapRaw,
169
+ signingCert: metadata.sp.getX509Certificate('signing'),
170
+ signatureConfig: {
171
+ prefix: 'ds',
172
+ location: {
173
+ reference: "//*[local-name(.)='Issuer']",
174
+ action: 'after'
175
+ }
176
+ }
177
+ });
178
+ samlContent = await sendArtifactResolve(url, signatureSoap);
179
+ }
180
+ }
181
+ const verificationOptions = {
182
+ metadata: from.entityMeta,
183
+ signatureAlgorithm: from.entitySetting.requestSignatureAlgorithm,
184
+ };
185
+ /** 断言是否加密应根据响应里面的字段判断*/
186
+ let decryptRequired = from.entitySetting.isAssertionEncrypted;
187
+ let extractorFields = [];
188
+ // validate the xml first
189
+ let res = await libsaml.isValidXml(samlContent).catch((error) => {
190
+ return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
191
+ });
192
+ if (res !== true) {
193
+ return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
194
+ }
195
+ if (parserType !== urlParams.samlResponse) {
196
+ extractorFields = getDefaultExtractorFields(parserType, null);
197
+ }
198
+ // check status based on different scenarios
199
+ await checkStatus(samlContent, parserType, soap);
200
+ /**检查签名顺序 */
201
+ /* if (
202
+ checkSignature &&
203
+ from.entitySetting.messageSigningOrder === MessageSignatureOrder.ETS
204
+ ) {
205
+ console.log("===============我走的这里=========================")
206
+ const [verified, verifiedAssertionNode,isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
207
+ console.log(verified);
208
+ console.log("verified")
209
+ decryptRequired = isDecryptRequired
210
+ if (!verified) {
211
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
212
+ }
213
+ if (!decryptRequired) {
214
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
215
+ }
216
+ }*/
217
+ if (soap === true) {
218
+ const [verified, verifiedAssertionNode, isDecryptRequired] = libsaml.verifySignatureSoap(samlContent, verificationOptions);
219
+ decryptRequired = isDecryptRequired;
220
+ if (!verified) {
221
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
222
+ }
223
+ if (!decryptRequired) {
224
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
225
+ }
226
+ if (parserType === 'SAMLResponse' && decryptRequired) {
227
+ // 1. 解密断言
228
+ const [decryptedSAML, decryptedAssertion] = await libsaml.decryptAssertionSoap(self, samlContent);
229
+ // 2. 检查解密后的断言是否包含签名
230
+ const assertionDoc = new DOMParser().parseFromString(decryptedAssertion, 'text/xml');
231
+ const assertionSignatureNodes = select("./*[local-name()='Signature']", assertionDoc.documentElement);
232
+ // 3. 如果存在签名则验证
233
+ if (assertionSignatureNodes.length > 0) {
234
+ // 3.1 创建新的验证选项(保持原配置)
235
+ const assertionVerificationOptions = {
236
+ ...verificationOptions,
237
+ isAssertion: true // 添加标识表示正在验证断言
238
+ };
239
+ // 3.2 验证断言签名
240
+ const [assertionVerified, result] = libsaml.verifySignatureSoap(decryptedAssertion, assertionVerificationOptions);
241
+ if (!assertionVerified) {
242
+ console.error("解密后的断言签名验证失败");
243
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
244
+ }
245
+ if (assertionVerified) {
246
+ // @ts-ignore
247
+ samlContent = result;
248
+ extractorFields = getDefaultExtractorFields(parserType, result);
249
+ }
250
+ }
251
+ else {
252
+ samlContent = decryptedAssertion;
253
+ extractorFields = getDefaultExtractorFields(parserType, decryptedAssertion);
254
+ }
255
+ }
256
+ }
257
+ if (soap === false) {
258
+ const [verified, verifiedAssertionNode, isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
259
+ decryptRequired = isDecryptRequired;
260
+ if (!verified) {
261
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
262
+ }
263
+ if (!decryptRequired) {
264
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
265
+ }
266
+ if (parserType === 'SAMLResponse' && decryptRequired) {
267
+ const result = await libsaml.decryptAssertion(self, samlContent);
268
+ samlContent = result[0];
269
+ extractorFields = getDefaultExtractorFields(parserType, result[1]);
270
+ }
271
+ }
272
+ // verify the signatures (the response is signed then encrypted, then decrypt first then verify)
273
+ /* if (
274
+ checkSignature &&
275
+ from.entitySetting.messageSigningOrder === MessageSignatureOrder.STE
276
+ ) {
277
+ const [verified, verifiedAssertionNode,isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
278
+ decryptRequired = isDecryptRequired
279
+ if (verified) {
280
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
281
+ } else {
282
+ return Promise.reject('ERR_FAIL_TO_VERIFY_STE_SIGNATURE');
283
+ }
284
+ }*/
285
+ const parseResult = {
286
+ samlContent: samlContent,
287
+ extract: extract(samlContent, extractorFields),
288
+ };
289
+ /**
290
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
291
+ */
292
+ const targetEntityMetadata = from.entityMeta;
293
+ const issuer = targetEntityMetadata.getEntityID();
294
+ const extractedProperties = parseResult.extract;
295
+ // unmatched issuer
296
+ if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
297
+ && extractedProperties
298
+ && extractedProperties.issuer !== issuer) {
299
+ return Promise.reject('ERR_UNMATCH_ISSUER');
300
+ }
301
+ // invalid session time
302
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
303
+ if (parserType === 'SAMLResponse'
304
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
305
+ && !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
306
+ return Promise.reject('ERR_EXPIRED_SESSION');
307
+ }
308
+ // invalid time
309
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
310
+ if (parserType === 'SAMLResponse'
311
+ && extractedProperties.conditions
312
+ && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
313
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
314
+ }
315
+ //valid destination
316
+ //There is no validation of the response here. The upper-layer application
317
+ // should verify the result by itself to see if the destination is equal to the SP acs and
318
+ // whether the response.id is used to prevent replay attacks.
319
+ /*
320
+ let destination = extractedProperties?.response?.destination
321
+ let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
322
+ return item?.Location === destination
323
+ })
324
+ if (isExit?.length === 0) {
325
+ return Promise.reject('ERR_Destination_URL');
326
+ }
327
+ if (parserType === 'SAMLResponse') {
328
+ let destination = extractedProperties?.response?.destination
329
+ let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
330
+ return item?.Location === destination
331
+ })
332
+ if (isExit?.length === 0) {
333
+ return Promise.reject('ERR_Destination_URL');
334
+ }
335
+ }
336
+ */
337
+ return Promise.resolve(parseResult);
338
+ }
339
+ // proceed the post Artifact flow
340
+ async function postArtifactFlow(options) {
125
341
  const { request, from, self, parserType, checkSignature = true } = options;
126
342
  const { body } = request;
127
343
  const direction = libsaml.getQueryParamByType(parserType);
@@ -330,16 +546,20 @@ async function postSimpleSignFlow(options) {
330
546
  }
331
547
  return Promise.resolve(parseResult);
332
548
  }
333
- function checkStatus(content, parserType) {
549
+ function checkStatus(content, parserType, soap) {
334
550
  // only check response parser
335
551
  if (parserType !== urlParams.samlResponse && parserType !== urlParams.logoutResponse) {
336
552
  return Promise.resolve('SKIPPED');
337
553
  }
338
- const fields = parserType === urlParams.samlResponse
554
+ let fields = parserType === urlParams.samlResponse
339
555
  ? loginResponseStatusFields
340
556
  : logoutResponseStatusFields;
557
+ if (soap === true) {
558
+ fields = parserType === urlParams.samlResponse
559
+ ? loginArtifactResponseStatusFields
560
+ : logoutResponseStatusFields;
561
+ }
341
562
  const { top, second } = extract(content, fields);
342
- console.log(top, second);
343
563
  // only resolve when top-tier status code is success
344
564
  if (top === StatusCode.Success) {
345
565
  return Promise.resolve('OK');
@@ -76,6 +76,16 @@ const libSaml = () => {
76
76
  const defaultLogoutRequestTemplate = {
77
77
  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
78
  };
79
+ /**
80
+ * @desc Default art request template
81
+ * @type {LogoutRequestTemplate}
82
+ */
83
+ const defaultArtifactResolveTemplate = {
84
+ 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
+ };
86
+ 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>`,
88
+ };
79
89
  /**
80
90
  * @desc Default AttributeStatement template
81
91
  * @type {AttributeStatementTemplate}
@@ -202,6 +212,8 @@ const libSaml = () => {
202
212
  createXPath,
203
213
  getQueryParamByType,
204
214
  defaultLoginRequestTemplate,
215
+ defaultArtAuthnRequestTemplate,
216
+ defaultArtifactResolveTemplate,
205
217
  defaultLoginResponseTemplate,
206
218
  defaultAttributeStatementTemplate,
207
219
  defaultAttributeTemplate,
@@ -445,7 +457,7 @@ const libSaml = () => {
445
457
  assertionNode = node[0].toString();
446
458
  }
447
459
  }
448
-
460
+
449
461
  if (assertionSignatureNode.length === 1) {
450
462
  const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{
451
463
  key: 'refURI',
@@ -477,9 +489,173 @@ const libSaml = () => {
477
489
  }]);
478
490
  assertionNode = verifiedDoc.assertion.toString();
479
491
  }
480
-
492
+
481
493
  return [verified, assertionNode];*/
482
494
  },
495
+ verifySignatureSoap(xml, opts) {
496
+ const { dom } = getContext();
497
+ const doc = dom.parseFromString(xml);
498
+ const docParser = new DOMParser();
499
+ let selection = [];
500
+ if (opts.isAssertion) {
501
+ // 断言模式下的专用逻辑
502
+ const assertionSignatureXpath = "./*[local-name()='Signature']";
503
+ const signatureNode = select(assertionSignatureXpath, doc.documentElement);
504
+ if (signatureNode.length === 0) {
505
+ throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
506
+ }
507
+ selection = selection.concat(signatureNode);
508
+ }
509
+ else {
510
+ // 原始的SOAP响应验证逻辑
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);
539
+ }
540
+ for (const signatureNode of selection) {
541
+ const sig = new SignedXml();
542
+ let verified = false;
543
+ sig.signatureAlgorithm = opts.signatureAlgorithm;
544
+ if (!opts.keyFile && !opts.metadata) {
545
+ throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
546
+ }
547
+ if (opts.keyFile) {
548
+ sig.publicCert = fs.readFileSync(opts.keyFile, 'utf-8');
549
+ }
550
+ if (opts.metadata) {
551
+ const certificateNodes = select(".//*[local-name(.)='X509Certificate']", signatureNode);
552
+ // 获取元数据中的证书
553
+ let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
554
+ // 规范化元数据证书
555
+ if (Array.isArray(metadataCert)) {
556
+ metadataCert = flattenDeep(metadataCert);
557
+ }
558
+ else if (typeof metadataCert === 'string') {
559
+ metadataCert = [metadataCert];
560
+ }
561
+ metadataCert = metadataCert.map(utility.normalizeCerString);
562
+ // 检查证书可用性
563
+ if (certificateNodes.length === 0 && metadataCert.length === 0) {
564
+ throw new Error('NO_SELECTED_CERTIFICATE');
565
+ }
566
+ // 响应中有证书节点
567
+ if (certificateNodes.length !== 0) {
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
+ }
576
+ const x509Certificate = utility.normalizeCerString(x509CertificateData);
577
+ // 验证证书匹配
578
+ if (metadataCert.length >= 1 &&
579
+ !metadataCert.find(cert => cert.trim() === x509Certificate.trim())) {
580
+ throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
581
+ }
582
+ sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
583
+ }
584
+ else {
585
+ // 使用元数据中的第一个证书
586
+ sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
587
+ }
588
+ }
589
+ // 加载签名
590
+ sig.loadSignature(signatureNode);
591
+ // 使用原始 XML 进行验证
592
+ verified = sig.checkSignature(xml);
593
+ console.log("签名验证结果:", verified);
594
+ if (!verified) {
595
+ console.error("签名验证失败");
596
+ throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
597
+ }
598
+ // 检查签名引用
599
+ if (!(sig.getSignedReferences().length >= 1)) {
600
+ throw new Error('NO_SIGNATURE_REFERENCES');
601
+ }
602
+ const signedVerifiedXML = sig.getSignedReferences()[0];
603
+ const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'text/xml');
604
+ const rootNode = verifiedDoc.documentElement;
605
+ console.log("签名引用根节点:", rootNode.localName);
606
+ // 断言模式专用返回逻辑
607
+ if (opts.isAssertion) {
608
+ if (rootNode.localName === 'Assertion') {
609
+ return [true, rootNode.toString(), false];
610
+ }
611
+ else {
612
+ throw new Error('ERR_INVALID_ASSERTION_SIGNATURE');
613
+ }
614
+ }
615
+ // 处理已验证的签名
616
+ if (rootNode.localName === 'ArtifactResponse') {
617
+ // 在 ArtifactResponse 中查找 Response
618
+ const responseNodes = select("./*[local-name()='Response']", rootNode);
619
+ if (responseNodes.length === 0) {
620
+ console.warn("ArtifactResponse 中没有找到 Response 元素");
621
+ continue;
622
+ }
623
+ const responseNode = responseNodes[0];
624
+ // 在 Response 中查找断言
625
+ const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", responseNode);
626
+ const assertions = select("./*[local-name()='Assertion']", responseNode);
627
+ if (encryptedAssertions.length === 1) {
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);
655
+ }
656
+ }
657
+ throw new Error('ERR_ZERO_SIGNATURE');
658
+ },
483
659
  /**
484
660
  * @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
485
661
  * @param {string} use type of certificate (e.g. signing, encrypt)
@@ -686,6 +862,61 @@ const libSaml = () => {
686
862
  });
687
863
  });
688
864
  },
865
+ /**
866
+ * 解密 SOAP 响应中的加密断言
867
+ * @param self 当前实体(SP 或 IdP)
868
+ * @param entireXML 完整的 SOAP XML 响应
869
+ * @returns [解密后的完整 SOAP XML, 解密后的断言 XML]
870
+ */
871
+ async decryptAssertionSoap(self, entireXML) {
872
+ const { dom } = getContext();
873
+ try {
874
+ // 1. 解析 XML
875
+ const doc = dom.parseFromString(entireXML);
876
+ // 2. 定位加密断言
877
+ const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
878
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
879
+ "/*[local-name()='EncryptedAssertion']", doc);
880
+ if (!encryptedAssertions || encryptedAssertions.length === 0) {
881
+ throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
882
+ }
883
+ if (encryptedAssertions.length > 1) {
884
+ console.warn('发现多个加密断言,仅处理第一个');
885
+ }
886
+ const encAssertionNode = encryptedAssertions[0];
887
+ // 3. 准备解密密钥
888
+ const privateKey = utility.readPrivateKey(self.entitySetting.encPrivateKey, self.entitySetting.encPrivateKeyPass);
889
+ // 4. 解密断言
890
+ const decryptedAssertion = await new Promise((resolve, reject) => {
891
+ xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
892
+ if (err) {
893
+ console.error('解密错误:', err);
894
+ return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
895
+ }
896
+ if (!result) {
897
+ return reject(new Error('ERR_EMPTY_DECRYPTED_ASSERTION'));
898
+ }
899
+ resolve(result);
900
+ });
901
+ });
902
+ // 5. 创建解密断言的 DOM
903
+ const decryptedDoc = dom.parseFromString(decryptedAssertion);
904
+ const decryptedAssertionNode = decryptedDoc.documentElement;
905
+ // 6. 替换加密断言为解密后的断言
906
+ const parentNode = encAssertionNode.parentNode;
907
+ if (!parentNode) {
908
+ throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
909
+ }
910
+ parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
911
+ // 7. 序列化更新后的文档
912
+ const updatedSoapXml = doc.toString();
913
+ return [updatedSoapXml, decryptedAssertion];
914
+ }
915
+ catch (error) {
916
+ console.error('SOAP断言解密失败:', error);
917
+ throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
918
+ }
919
+ },
689
920
  /**
690
921
  * @desc Check if the xml string is valid and bounded
691
922
  */
@@ -102,6 +102,13 @@ export class IdpMetadata extends Metadata {
102
102
  attributePath: [],
103
103
  attributes: ['Location']
104
104
  },
105
+ {
106
+ key: 'artifactResolutionService',
107
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ArtifactResolutionService'],
108
+ index: ['Binding'],
109
+ attributePath: [],
110
+ attributes: ['Location']
111
+ },
105
112
  ]);
106
113
  }
107
114
  /**
@@ -130,4 +137,19 @@ export class IdpMetadata extends Metadata {
130
137
  }
131
138
  return this.meta.singleSignOnService;
132
139
  }
140
+ /**
141
+ * @desc Get the entity endpoint for single ArtifactResolutionService
142
+ * @param {string} binding protocol binding (e.g. redirect, post)
143
+ * @return {string/object} location
144
+ */
145
+ getArtifactResolutionService(binding) {
146
+ if (isString(binding)) {
147
+ const bindName = namespace.binding[binding];
148
+ const service = this.meta.artifactResolutionService[bindName];
149
+ if (service) {
150
+ return service;
151
+ }
152
+ }
153
+ return this.meta.artifactResolutionService;
154
+ }
133
155
  }
@@ -70,6 +70,20 @@ export class SpMetadata extends Metadata {
70
70
  descriptors.SingleLogoutService.push([{ _attr: attr }]);
71
71
  });
72
72
  }
73
+ if (isNonEmptyArray(artifactResolutionService)) {
74
+ let indexCount = 0;
75
+ artifactResolutionService.forEach(a => {
76
+ const attr = {
77
+ index: String(indexCount++),
78
+ Binding: a.Binding,
79
+ Location: a.Location,
80
+ };
81
+ if (a.isDefault) {
82
+ attr.isDefault = true;
83
+ }
84
+ descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
85
+ });
86
+ }
73
87
  if (isNonEmptyArray(assertionConsumerService)) {
74
88
  let indexCount = 0;
75
89
  assertionConsumerService.forEach(a => {
@@ -150,21 +164,6 @@ export class SpMetadata extends Metadata {
150
164
  descriptors.AttributeConsumingService.push(attrConsumingService);
151
165
  });
152
166
  }
153
- if (isNonEmptyArray(artifactResolutionService)) {
154
- artifactResolutionService.forEach(a => {
155
- const attr = {
156
- Binding: a.Binding,
157
- Location: a.Location,
158
- };
159
- if (a.isDefault) {
160
- attr.isDefault = true;
161
- }
162
- descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
163
- });
164
- }
165
- else {
166
- console.warn('Construct identity provider - missing endpoint of ArtifactResolutionService');
167
- }
168
167
  // handle element order
169
168
  const existedElements = elementsOrder.filter(name => isNonEmptyArray(descriptors[name]));
170
169
  existedElements.forEach(name => {
@@ -193,6 +192,11 @@ export class SpMetadata extends Metadata {
193
192
  key: 'assertionConsumerService',
194
193
  localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'],
195
194
  attributes: ['Binding', 'Location', 'isDefault', 'index'],
195
+ },
196
+ {
197
+ key: 'artifactResolutionService',
198
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'],
199
+ attributes: ['Binding', 'Location', 'isDefault', 'index'],
196
200
  }
197
201
  ]);
198
202
  }