samlesa 2.12.10 → 2.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of samlesa might be problematic. Click here for more details.
- package/build/src/binding-post.js +2 -2
- package/build/src/binding-post.js.map +1 -1
- package/build/src/binding-redirect.js +2 -2
- package/build/src/binding-redirect.js.map +1 -1
- package/build/src/binding-simplesign.js +2 -2
- package/build/src/binding-simplesign.js.map +1 -1
- package/build/src/entity-idp.js +17 -22
- package/build/src/entity-idp.js.map +1 -1
- package/build/src/extractor.js +1 -1
- package/build/src/extractor.js.map +1 -1
- package/build/src/libsaml.js +148 -103
- package/build/src/libsaml.js.map +1 -1
- package/build/src/validator.js.map +1 -1
- package/package.json +70 -70
- package/src/binding-post.ts +2 -2
- package/src/binding-redirect.ts +2 -2
- package/src/binding-simplesign.ts +2 -2
- package/src/entity-idp.ts +4 -4
- package/src/extractor.ts +1 -1
- package/src/libsaml.ts +271 -207
- package/src/validator.ts +2 -7
- package/types/src/binding-post.d.ts +1 -1
- package/types/src/binding-redirect.d.ts +1 -1
- package/types/src/binding-simplesign.d.ts +1 -1
- package/types/src/entity-idp.d.ts +1 -1
- package/types/src/libsaml.d.ts +56 -47
package/src/libsaml.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file SamlLib.js
|
|
3
|
-
* @author tngan
|
|
4
|
-
* @desc A simple library including some common functions
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
import utility, {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
2
|
+
* @file SamlLib.js
|
|
3
|
+
* @author tngan
|
|
4
|
+
* @desc A simple library including some common functions
|
|
5
|
+
*/
|
|
6
|
+
import {createSign, createPrivateKey, createVerify} from 'node:crypto';
|
|
7
|
+
import utility, {flattenDeep, isString} from './utility.js';
|
|
8
|
+
import {algorithms, wording, namespace} from './urn.js';
|
|
9
|
+
import {select} from 'xpath';
|
|
10
|
+
import {MetadataInterface} from './metadata.js';
|
|
11
|
+
import {create} from 'xmlbuilder2'
|
|
12
|
+
import {SignedXml} from 'xml-crypto';
|
|
13
13
|
import * as xmlenc from 'xml-encryption';
|
|
14
|
-
import {
|
|
14
|
+
import {extract} from './extractor.js';
|
|
15
15
|
import camelCase from 'camelcase';
|
|
16
|
-
import {
|
|
16
|
+
import {getContext} from './api.js';
|
|
17
17
|
import xmlEscape from 'xml-escape';
|
|
18
18
|
import * as fs from 'fs';
|
|
19
19
|
import {DOMParser} from '@xmldom/xmldom';
|
|
@@ -22,11 +22,12 @@ const signatureAlgorithms = algorithms.signature;
|
|
|
22
22
|
const digestAlgorithms = algorithms.digest;
|
|
23
23
|
const certUse = wording.certUse;
|
|
24
24
|
const urlParams = wording.urlParams;
|
|
25
|
+
|
|
25
26
|
/**
|
|
26
27
|
* 算法名称映射表 (兼容 X.509 和 SAML 规范)
|
|
27
28
|
*/
|
|
28
29
|
function mapSignAlgorithm(algorithm: string): string {
|
|
29
|
-
const algorithmMap =
|
|
30
|
+
const algorithmMap = {
|
|
30
31
|
'rsa-sha1': 'RSA-SHA1',
|
|
31
32
|
'rsa-sha256': 'RSA-SHA256',
|
|
32
33
|
'rsa-sha384': 'RSA-SHA384',
|
|
@@ -38,6 +39,15 @@ function mapSignAlgorithm(algorithm: string): string {
|
|
|
38
39
|
|
|
39
40
|
return algorithmMap[algorithm.toLowerCase()] || algorithm;
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 生成 SAML Attribute 元素(不带 XML 声明头)
|
|
46
|
+
* @param {Array} attributeData - 属性配置数据
|
|
47
|
+
* @returns {string} SAML Attribute XML 字符串
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
|
|
41
51
|
export interface SignatureConstructor {
|
|
42
52
|
rawSamlMessage: string;
|
|
43
53
|
referenceTagXPath?: string;
|
|
@@ -59,6 +69,7 @@ export interface SignatureVerifierOptions {
|
|
|
59
69
|
|
|
60
70
|
export interface ExtractorResult {
|
|
61
71
|
[key: string]: any;
|
|
72
|
+
|
|
62
73
|
signature?: string | string[];
|
|
63
74
|
issuer?: string | string[];
|
|
64
75
|
nameID?: string;
|
|
@@ -88,15 +99,21 @@ export interface LoginResponseTemplate extends BaseSamlTemplate {
|
|
|
88
99
|
attributes?: LoginResponseAttribute[];
|
|
89
100
|
additionalTemplates?: LoginResponseAdditionalTemplates;
|
|
90
101
|
}
|
|
91
|
-
export interface AttributeStatementTemplate extends BaseSamlTemplate { }
|
|
92
102
|
|
|
93
|
-
export interface
|
|
103
|
+
export interface AttributeStatementTemplate extends BaseSamlTemplate {
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AttributeTemplate extends BaseSamlTemplate {
|
|
107
|
+
}
|
|
94
108
|
|
|
95
|
-
export interface LoginRequestTemplate extends BaseSamlTemplate {
|
|
109
|
+
export interface LoginRequestTemplate extends BaseSamlTemplate {
|
|
110
|
+
}
|
|
96
111
|
|
|
97
|
-
export interface LogoutRequestTemplate extends BaseSamlTemplate {
|
|
112
|
+
export interface LogoutRequestTemplate extends BaseSamlTemplate {
|
|
113
|
+
}
|
|
98
114
|
|
|
99
|
-
export interface LogoutResponseTemplate extends BaseSamlTemplate {
|
|
115
|
+
export interface LogoutResponseTemplate extends BaseSamlTemplate {
|
|
116
|
+
}
|
|
100
117
|
|
|
101
118
|
export type KeyUse = 'signing' | 'encryption';
|
|
102
119
|
|
|
@@ -134,9 +151,9 @@ export interface LibSamlInterface {
|
|
|
134
151
|
const libSaml = () => {
|
|
135
152
|
|
|
136
153
|
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
* @desc helper function to get back the query param for redirect binding for SLO/SSO
|
|
155
|
+
* @type {string}
|
|
156
|
+
*/
|
|
140
157
|
function getQueryParamByType(type: string) {
|
|
141
158
|
if ([urlParams.logoutRequest, urlParams.samlRequest].indexOf(type) !== -1) {
|
|
142
159
|
return 'SAMLRequest';
|
|
@@ -146,6 +163,7 @@ const libSaml = () => {
|
|
|
146
163
|
}
|
|
147
164
|
throw new Error('ERR_UNDEFINED_QUERY_PARAMS');
|
|
148
165
|
}
|
|
166
|
+
|
|
149
167
|
/**
|
|
150
168
|
*
|
|
151
169
|
*/
|
|
@@ -160,40 +178,47 @@ const libSaml = () => {
|
|
|
160
178
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'RSA-SHA512',
|
|
161
179
|
};
|
|
162
180
|
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
* @desc Default login request template
|
|
182
|
+
* @type {LoginRequestTemplate}
|
|
183
|
+
*/
|
|
166
184
|
const defaultLoginRequestTemplate = {
|
|
167
185
|
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>',
|
|
168
186
|
};
|
|
169
187
|
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
* @desc Default logout request template
|
|
189
|
+
* @type {LogoutRequestTemplate}
|
|
190
|
+
*/
|
|
173
191
|
const defaultLogoutRequestTemplate = {
|
|
174
192
|
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>',
|
|
175
193
|
};
|
|
176
194
|
|
|
177
195
|
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
* @desc Default AttributeStatement template
|
|
197
|
+
* @type {AttributeStatementTemplate}
|
|
198
|
+
*/
|
|
181
199
|
const defaultAttributeStatementTemplate = {
|
|
182
200
|
context: '<saml:AttributeStatement>{Attributes}</saml:AttributeStatement>',
|
|
183
201
|
};
|
|
184
202
|
|
|
185
203
|
/**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
* @desc Default Attribute template
|
|
205
|
+
* @type {AttributeTemplate}
|
|
206
|
+
*/
|
|
189
207
|
const defaultAttributeTemplate = {
|
|
190
|
-
context: '<saml:Attribute Name="{Name}" NameFormat="{NameFormat}"
|
|
208
|
+
context: '<saml:Attribute Name="{Name}" NameFormat="{NameFormat}">{AttributeValues}</saml:Attribute>',
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* @desc Default AttributeValue template
|
|
212
|
+
* @type {AttributeTemplate}
|
|
213
|
+
*/
|
|
214
|
+
const defaultAttributeValueTemplate = {
|
|
215
|
+
context: '<saml:AttributeValue xmlns:xs="{ValueXmlnsXs}" xmlns:xsi="{ValueXmlnsXsi}" xsi:type="{ValueXsiType}">{Value}</saml:AttributeValue>',
|
|
191
216
|
};
|
|
192
217
|
|
|
193
218
|
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
219
|
+
* @desc Default login response template
|
|
220
|
+
* @type {LoginResponseTemplate}
|
|
221
|
+
*/
|
|
197
222
|
const defaultLoginResponseTemplate = {
|
|
198
223
|
context: '<samlp:Response 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><saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AuthnStatement}{AttributeStatement}</saml:Assertion></samlp:Response>',
|
|
199
224
|
attributes: [],
|
|
@@ -203,14 +228,14 @@ const libSaml = () => {
|
|
|
203
228
|
}
|
|
204
229
|
};
|
|
205
230
|
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
231
|
+
* @desc Default logout response template
|
|
232
|
+
* @type {LogoutResponseTemplate}
|
|
233
|
+
*/
|
|
209
234
|
const defaultLogoutResponseTemplate = {
|
|
210
235
|
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>',
|
|
211
236
|
};
|
|
212
237
|
|
|
213
|
-
function getSigningSchemeForNode(sigAlg?: string){
|
|
238
|
+
function getSigningSchemeForNode(sigAlg?: string) {
|
|
214
239
|
if (sigAlg) {
|
|
215
240
|
const algAlias = nrsaAliasMappingForNode[sigAlg];
|
|
216
241
|
if (!(algAlias === undefined)) {
|
|
@@ -219,22 +244,24 @@ const libSaml = () => {
|
|
|
219
244
|
}
|
|
220
245
|
return nrsaAliasMappingForNode[signatureAlgorithms.RSA_SHA256];
|
|
221
246
|
}
|
|
247
|
+
|
|
222
248
|
/**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
* @private
|
|
250
|
+
* @desc Get the digest algorithms by signature algorithms
|
|
251
|
+
* @param {string} sigAlg signature algorithm
|
|
252
|
+
* @return {string/undefined} digest algorithm
|
|
253
|
+
*/
|
|
228
254
|
function getDigestMethod(sigAlg: string): string | undefined {
|
|
229
255
|
return digestAlgorithms[sigAlg];
|
|
230
256
|
}
|
|
257
|
+
|
|
231
258
|
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
259
|
+
* @public
|
|
260
|
+
* @desc Create XPath
|
|
261
|
+
* @param {string/object} local parameters to create XPath
|
|
262
|
+
* @param {boolean} isExtractAll define whether returns whole content according to the XPath
|
|
263
|
+
* @return {string} xpath
|
|
264
|
+
*/
|
|
238
265
|
function createXPath(local, isExtractAll?: boolean): string {
|
|
239
266
|
if (isString(local)) {
|
|
240
267
|
return isExtractAll === true ? "//*[local-name(.)='" + local + "']/text()" : "//*[local-name(.)='" + local + "']";
|
|
@@ -273,13 +300,13 @@ const libSaml = () => {
|
|
|
273
300
|
defaultAttributeTemplate,
|
|
274
301
|
defaultLogoutRequestTemplate,
|
|
275
302
|
defaultLogoutResponseTemplate,
|
|
276
|
-
|
|
303
|
+
defaultAttributeValueTemplate,
|
|
277
304
|
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
305
|
+
* @desc Replace the tag (e.g. {tag}) inside the raw XML
|
|
306
|
+
* @param {string} rawXML raw XML string used to do keyword replacement
|
|
307
|
+
* @param {array} tagValues tag values
|
|
308
|
+
* @return {string}
|
|
309
|
+
*/
|
|
283
310
|
replaceTagsByValue(rawXML: string, tagValues: Record<string, unknown>): string {
|
|
284
311
|
Object.keys(tagValues).forEach(t => {
|
|
285
312
|
rawXML = rawXML.replace(
|
|
@@ -290,50 +317,87 @@ const libSaml = () => {
|
|
|
290
317
|
return rawXML;
|
|
291
318
|
},
|
|
292
319
|
/**
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
attributeStatementBuilder(
|
|
320
|
+
* @desc Helper function to build the AttributeStatement tag
|
|
321
|
+
* @param {LoginResponseAttribute} attributes an array of attribute configuration
|
|
322
|
+
* @param {AttributeTemplate} attributeTemplate the attribute tag template to be used
|
|
323
|
+
* @param {AttributeStatementTemplate} attributeStatementTemplate the attributeStatement tag template to be used
|
|
324
|
+
* @return {string}
|
|
325
|
+
*/
|
|
326
|
+
/* attributeStatementBuilder(
|
|
300
327
|
attributes: LoginResponseAttribute[],
|
|
301
328
|
attributeTemplate: AttributeTemplate = defaultAttributeTemplate,
|
|
302
329
|
attributeStatementTemplate: AttributeStatementTemplate = defaultAttributeStatementTemplate
|
|
303
330
|
): string {
|
|
304
|
-
const attr = attributes.map(({name, nameFormat, valueTag, valueXsiType,type, valueXmlnsXs, valueXmlnsXsi
|
|
331
|
+
const attr = attributes.map(({name, nameFormat, valueTag, valueXsiType, type, valueXmlnsXs, valueXmlnsXsi}) => {
|
|
305
332
|
const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema';
|
|
306
333
|
const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance';
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
let attributeLine = attributeTemplate.context;
|
|
335
|
+
if (attributeLine && typeof attributeLine === 'function') {
|
|
336
|
+
// 安全调用
|
|
337
|
+
// @ts-ignore
|
|
338
|
+
return attributeLine({
|
|
339
|
+
name,
|
|
340
|
+
nameFormat,
|
|
341
|
+
valueTag,
|
|
342
|
+
valueXsiType,
|
|
343
|
+
type,
|
|
344
|
+
valueXmlnsXs: valueXmlnsXs ?? defaultValueXmlnsXs,
|
|
345
|
+
valueXmlnsXsi: valueXmlnsXsi ?? defaultValueXmlnsXsi
|
|
346
|
+
})
|
|
347
|
+
} else {
|
|
348
|
+
attributeLine = attributeLine.replace('{Name}', name);
|
|
349
|
+
attributeLine = attributeLine.replace('{NameFormat}', nameFormat);
|
|
350
|
+
attributeLine = attributeLine.replace('{ValueXmlnsXs}', valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs);
|
|
351
|
+
attributeLine = attributeLine.replace('{ValueXmlnsXsi}', valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi);
|
|
352
|
+
attributeLine = attributeLine.replace('{ValueXsiType}', valueXsiType);
|
|
353
|
+
attributeLine = attributeLine.replace('{Value}', `{${tagging('attr', valueTag)}}`);
|
|
354
|
+
return attributeLine;
|
|
355
|
+
}
|
|
321
356
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
357
|
+
}).join('');
|
|
358
|
+
return attributeStatementTemplate.context.replace('{Attributes}', attr);
|
|
359
|
+
},*/
|
|
360
|
+
/** For Test */
|
|
361
|
+
attributeStatementBuilder(attributeData: any[]): string {
|
|
362
|
+
const root = create({
|
|
363
|
+
// 关键配置:关闭 XML 声明头和独立文档标识
|
|
364
|
+
headless: true
|
|
365
|
+
}).ele('saml:AttributeStatement', {
|
|
366
|
+
index: 1
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
attributeData.forEach(attr => {
|
|
370
|
+
const attribute = root.ele('saml:Attribute', {
|
|
371
|
+
Name: attr.Name,
|
|
372
|
+
NameFormat: attr.NameFormat,
|
|
373
|
+
FriendlyName: attr.FriendlyName
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
attr.valueArray.forEach(valueObj => {
|
|
377
|
+
const valueElement = attribute.ele('saml:AttributeValue');
|
|
325
378
|
|
|
379
|
+
// 根据 ValueType 添加数据类型
|
|
380
|
+
/* if (attr.ValueType === 1) {
|
|
381
|
+
valueElement.att('xsi:type', 'xs:string');
|
|
382
|
+
} // 可扩展其他类型...*/
|
|
383
|
+
|
|
384
|
+
valueElement.txt(valueObj.value);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return root.end({prettyPrint: true});
|
|
389
|
+
},
|
|
326
390
|
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
391
|
+
* @desc Construct the XML signature for POST binding
|
|
392
|
+
* @param {string} rawSamlMessage request/response xml string
|
|
393
|
+
* @param {string} referenceTagXPath reference uri
|
|
394
|
+
* @param {string} privateKey declares the private key
|
|
395
|
+
* @param {string} passphrase passphrase of the private key [optional]
|
|
396
|
+
* @param {string|buffer} signingCert signing certificate
|
|
397
|
+
* @param {string} signatureAlgorithm signature algorithm
|
|
398
|
+
* @param {string[]} transformationAlgorithms canonicalization and transformation Algorithms
|
|
399
|
+
* @return {string} base64 encoded string
|
|
400
|
+
*/
|
|
337
401
|
constructSAMLSignature(opts: SignatureConstructor) {
|
|
338
402
|
const {
|
|
339
403
|
rawSamlMessage,
|
|
@@ -382,15 +446,15 @@ const libSaml = () => {
|
|
|
382
446
|
return isBase64Output !== false ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml();
|
|
383
447
|
},
|
|
384
448
|
/**
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
449
|
+
* @desc Verify the XML signature
|
|
450
|
+
* @param {string} xml xml
|
|
451
|
+
* @param {SignatureVerifierOptions} opts cert declares the X509 certificate
|
|
388
452
|
* @return {[boolean, string | null]} - A tuple where:
|
|
389
453
|
* - The first element is `true` if the signature is valid, `false` otherwise.
|
|
390
454
|
* - The second element is the cryptographically authenticated assertion node as a string, or `null` if not found.
|
|
391
455
|
*/
|
|
392
456
|
verifySignature(xml: string, opts: SignatureVerifierOptions) {
|
|
393
|
-
const {
|
|
457
|
+
const {dom} = getContext();
|
|
394
458
|
const doc = dom.parseFromString(xml);
|
|
395
459
|
|
|
396
460
|
const docParser = new DOMParser();
|
|
@@ -423,7 +487,7 @@ const libSaml = () => {
|
|
|
423
487
|
|
|
424
488
|
|
|
425
489
|
// need to refactor later on
|
|
426
|
-
for (const signatureNode of selection){
|
|
490
|
+
for (const signatureNode of selection) {
|
|
427
491
|
const sig = new SignedXml();
|
|
428
492
|
let verified = false;
|
|
429
493
|
|
|
@@ -502,19 +566,19 @@ const libSaml = () => {
|
|
|
502
566
|
|
|
503
567
|
// try getting the Xml from the first assertion
|
|
504
568
|
const EncryptedAssertions = select(
|
|
505
|
-
|
|
506
|
-
|
|
569
|
+
"./*[local-name()='EncryptedAssertion']",
|
|
570
|
+
rootNode
|
|
507
571
|
);
|
|
508
572
|
const assertions = select(
|
|
509
|
-
|
|
510
|
-
|
|
573
|
+
"./*[local-name()='Assertion']",
|
|
574
|
+
rootNode
|
|
511
575
|
);
|
|
512
576
|
|
|
513
|
-
|
|
514
|
-
|
|
577
|
+
// now we can process the assertion as an assertion
|
|
578
|
+
if (EncryptedAssertions.length === 1) {
|
|
515
579
|
|
|
516
|
-
|
|
517
|
-
|
|
580
|
+
return [true, EncryptedAssertions[0].toString()];
|
|
581
|
+
}
|
|
518
582
|
|
|
519
583
|
if (assertions.length === 1) {
|
|
520
584
|
|
|
@@ -526,65 +590,65 @@ const libSaml = () => {
|
|
|
526
590
|
} else {
|
|
527
591
|
return [true, null]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
|
|
528
592
|
}
|
|
529
|
-
}
|
|
593
|
+
}
|
|
530
594
|
|
|
531
595
|
// something has gone seriously wrong if we are still here
|
|
532
596
|
throw new Error('ERR_ZERO_SIGNATURE');
|
|
533
597
|
|
|
534
598
|
// response must be signed, either entire document or assertion
|
|
535
599
|
// default we will take the assertion section under root
|
|
536
|
-
/* if (messageSignatureNode.length === 1) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
600
|
+
/* if (messageSignatureNode.length === 1) {
|
|
601
|
+
const node = select("/!*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/!*[local-name(.)='Assertion']", doc);
|
|
602
|
+
if (node.length === 1) {
|
|
603
|
+
assertionNode = node[0].toString();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
542
606
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
607
|
+
if (assertionSignatureNode.length === 1) {
|
|
608
|
+
const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{
|
|
609
|
+
key: 'refURI',
|
|
610
|
+
localPath: ['Signature', 'SignedInfo', 'Reference'],
|
|
611
|
+
attributes: ['URI']
|
|
612
|
+
}]);
|
|
613
|
+
// get the assertion supposed to be the one should be verified
|
|
614
|
+
const desiredAssertionInfo = extract(doc.toString(), [{
|
|
615
|
+
key: 'id',
|
|
616
|
+
localPath: ['~Response', 'Assertion'],
|
|
617
|
+
attributes: ['ID']
|
|
618
|
+
}]);
|
|
619
|
+
// 5.4.2 References
|
|
620
|
+
// SAML assertions and protocol messages MUST supply a value for the ID attribute on the root element of
|
|
621
|
+
// the assertion or protocol message being signed. The assertion’s or protocol message's root element may
|
|
622
|
+
// or may not be the root element of the actual XML document containing the signed assertion or protocol
|
|
623
|
+
// message (e.g., it might be contained within a SOAP envelope).
|
|
624
|
+
// Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID
|
|
625
|
+
// attribute value of the root element of the assertion or protocol message being signed. For example, if the
|
|
626
|
+
// ID attribute value is "foo", then the URI attribute in the <ds:Reference> element MUST be "#foo".
|
|
627
|
+
if (verifiedAssertionInfo.refURI !== `#${desiredAssertionInfo.id}`) {
|
|
628
|
+
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
629
|
+
}
|
|
630
|
+
const verifiedDoc = extract(doc.toString(), [{
|
|
631
|
+
key: 'assertion',
|
|
632
|
+
localPath: ['~Response', 'Assertion'],
|
|
633
|
+
attributes: [],
|
|
634
|
+
context: true
|
|
635
|
+
}]);
|
|
636
|
+
assertionNode = verifiedDoc.assertion.toString();
|
|
637
|
+
}
|
|
574
638
|
|
|
575
|
-
|
|
639
|
+
return [verified, assertionNode];*/
|
|
576
640
|
},
|
|
577
641
|
/**
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
642
|
+
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
|
|
643
|
+
* @param {string} use type of certificate (e.g. signing, encrypt)
|
|
644
|
+
* @param {string} certString declares the certificate String
|
|
645
|
+
* @return {object} object used in xml module
|
|
646
|
+
*/
|
|
583
647
|
createKeySection(use: KeyUse, certString: string | Buffer): KeyComponent {
|
|
584
648
|
return {
|
|
585
649
|
['KeyDescriptor']: [
|
|
586
650
|
{
|
|
587
|
-
_attr: {
|
|
651
|
+
_attr: {use},
|
|
588
652
|
},
|
|
589
653
|
{
|
|
590
654
|
['ds:KeyInfo']: [
|
|
@@ -615,38 +679,38 @@ const libSaml = () => {
|
|
|
615
679
|
|
|
616
680
|
constructMessageSignature(
|
|
617
681
|
octetString: string | Buffer,
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
): string | Buffer {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
682
|
+
key: string | Buffer,
|
|
683
|
+
passphrase?: string,
|
|
684
|
+
isBase64: boolean = true,
|
|
685
|
+
signingAlgorithm: string = nrsaAliasMappingForNode[signatureAlgorithms.RSA_SHA256]
|
|
686
|
+
): string | Buffer {
|
|
687
|
+
try {
|
|
688
|
+
// 1. 标准化输入数据
|
|
689
|
+
const inputData = Buffer.isBuffer(octetString)
|
|
690
|
+
? octetString
|
|
691
|
+
: Buffer.from(octetString, 'utf8');
|
|
692
|
+
// 2. 创建签名器并设置算
|
|
693
|
+
const signingAlgorithmValue = getSigningSchemeForNode(signingAlgorithm)
|
|
694
|
+
const signer = createSign(signingAlgorithmValue)
|
|
695
|
+
|
|
696
|
+
// 3. 加载私钥
|
|
697
|
+
const privateKey = createPrivateKey({
|
|
698
|
+
key: key,
|
|
699
|
+
format: 'pem',
|
|
700
|
+
passphrase: passphrase,
|
|
701
|
+
encoding: 'utf8'
|
|
702
|
+
});
|
|
703
|
+
signer.write(octetString);
|
|
704
|
+
signer.end();
|
|
705
|
+
const signature = signer.sign(privateKey, 'base64');
|
|
706
|
+
console.log(signature.toString());
|
|
707
|
+
console.log('dayingyixia')
|
|
708
|
+
// 5. 处理编码输出
|
|
709
|
+
return isBase64 ? signature.toString() : signature;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
throw new Error(`SAML 签名失败: ${error.message}`);
|
|
712
|
+
}
|
|
713
|
+
},
|
|
650
714
|
verifyMessageSignature(
|
|
651
715
|
metadata,
|
|
652
716
|
octetString: string,
|
|
@@ -657,19 +721,19 @@ const libSaml = () => {
|
|
|
657
721
|
const signingScheme = getSigningSchemeForNode(verifyAlgorithm);
|
|
658
722
|
const verifier = createVerify(signingScheme);
|
|
659
723
|
verifier.update(octetString);
|
|
660
|
-
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert),
|
|
724
|
+
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
661
725
|
console.log(isValid);
|
|
662
|
-
console.log('
|
|
726
|
+
console.log('-------------签名验证结果-------------')
|
|
663
727
|
return isValid
|
|
664
728
|
|
|
665
729
|
},
|
|
666
730
|
|
|
667
731
|
|
|
668
732
|
/**
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
733
|
+
* @desc Get the public key in string format
|
|
734
|
+
* @param {string} x509Certificate certificate
|
|
735
|
+
* @return {string} public key
|
|
736
|
+
*/
|
|
673
737
|
getKeyInfo(x509Certificate: string, signatureConfig: any = {}) {
|
|
674
738
|
const prefix = signatureConfig.prefix ? `${signatureConfig.prefix}:` : '';
|
|
675
739
|
return {
|
|
@@ -682,12 +746,12 @@ const libSaml = () => {
|
|
|
682
746
|
};
|
|
683
747
|
},
|
|
684
748
|
/**
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
749
|
+
* @desc Encrypt the assertion section in Response
|
|
750
|
+
* @param {Entity} sourceEntity source entity
|
|
751
|
+
* @param {Entity} targetEntity target entity
|
|
752
|
+
* @param {string} xml response in xml string format
|
|
753
|
+
* @return {Promise} a promise to resolve the finalized xml
|
|
754
|
+
*/
|
|
691
755
|
encryptAssertion(sourceEntity, targetEntity, xml?: string) {
|
|
692
756
|
// Implement encryption after signature if it has
|
|
693
757
|
return new Promise<string>((resolve, reject) => {
|
|
@@ -698,7 +762,7 @@ const libSaml = () => {
|
|
|
698
762
|
|
|
699
763
|
const sourceEntitySetting = sourceEntity.entitySetting;
|
|
700
764
|
const targetEntityMetadata = targetEntity.entityMeta;
|
|
701
|
-
const {
|
|
765
|
+
const {dom} = getContext();
|
|
702
766
|
const doc = dom.parseFromString(xml);
|
|
703
767
|
const assertions = select("//*[local-name(.)='Assertion']", doc) as Node[];
|
|
704
768
|
if (!Array.isArray(assertions) || assertions.length === 0) {
|
|
@@ -731,7 +795,7 @@ const libSaml = () => {
|
|
|
731
795
|
if (!res) {
|
|
732
796
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
733
797
|
}
|
|
734
|
-
const {
|
|
798
|
+
const {encryptedAssertion: encAssertionPrefix} = sourceEntitySetting.tagPrefix;
|
|
735
799
|
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion>`);
|
|
736
800
|
doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode);
|
|
737
801
|
return resolve(utility.base64Encode(doc.toString()));
|
|
@@ -742,13 +806,13 @@ const libSaml = () => {
|
|
|
742
806
|
});
|
|
743
807
|
},
|
|
744
808
|
/**
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
809
|
+
* @desc Decrypt the assertion section in Response
|
|
810
|
+
* @param {string} type only accept SAMLResponse to proceed decryption
|
|
811
|
+
* @param {Entity} here this entity
|
|
812
|
+
* @param {Entity} from from the entity where the message is sent
|
|
813
|
+
* @param {string} entireXML response in xml string format
|
|
814
|
+
* @return {function} a promise to get back the entire xml with decrypted assertion
|
|
815
|
+
*/
|
|
752
816
|
decryptAssertion(here, entireXML: string) {
|
|
753
817
|
return new Promise<[string, any]>((resolve, reject) => {
|
|
754
818
|
// Implement decryption first then check the signature
|
|
@@ -757,7 +821,7 @@ const libSaml = () => {
|
|
|
757
821
|
}
|
|
758
822
|
// Perform encryption depends on the setting of where the message is sent, default is false
|
|
759
823
|
const hereSetting = here.entitySetting;
|
|
760
|
-
const {
|
|
824
|
+
const {dom} = getContext();
|
|
761
825
|
const doc = dom.parseFromString(entireXML);
|
|
762
826
|
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc) as Node[];
|
|
763
827
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
@@ -789,7 +853,7 @@ const libSaml = () => {
|
|
|
789
853
|
async isValidXml(input: string) {
|
|
790
854
|
|
|
791
855
|
// check if global api contains the validate function
|
|
792
|
-
const {
|
|
856
|
+
const {validate} = getContext();
|
|
793
857
|
|
|
794
858
|
/**
|
|
795
859
|
* user can write a validate function that always returns
|