diff --git a/packages/backend/package.json b/packages/backend/package.json index 5a438d29d6..83256149a2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -143,6 +143,7 @@ "nanoid": "5.0.6", "nested-property": "4.0.0", "node-fetch": "3.3.2", + "node-forge": "1.3.1", "nodemailer": "6.9.12", "nsfwjs": "2.4.2", "oauth": "0.10.0", @@ -213,6 +214,7 @@ "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", "@types/node": "20.11.27", + "@types/node-forge": "1.3.11", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", "@types/oauth2orize": "1.11.4", diff --git a/packages/backend/src/server/api/endpoints/admin/sso/create.ts b/packages/backend/src/server/api/endpoints/admin/sso/create.ts index 9ebb7b1c80..780fc59e74 100644 --- a/packages/backend/src/server/api/endpoints/admin/sso/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/sso/create.ts @@ -125,7 +125,7 @@ export default class extends Endpoint { // eslint- name: ps.name ? ps.name : null, type: ps.type, issuer: ps.issuer, - audience: ps.audience?.filter(i => !!i), + audience: ps.audience?.filter(i => i.length > 0) ?? [], acsUrl: ps.acsUrl, publicKey: publicKey, privateKey: privateKey, diff --git a/packages/backend/src/server/api/endpoints/admin/sso/list.ts b/packages/backend/src/server/api/endpoints/admin/sso/list.ts index b67ffec998..e68cfdb73f 100644 --- a/packages/backend/src/server/api/endpoints/admin/sso/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/sso/list.ts @@ -48,6 +48,10 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + useCertificate: { + type: 'boolean', + optional: false, nullable: false, + }, publicKey: { type: 'string', optional: false, nullable: false, @@ -100,6 +104,7 @@ export default class extends Endpoint { // eslint- issuer: service.issuer, audience: service.audience, acsUrl: service.acsUrl, + useCertificate: service.privateKey != null, publicKey: service.publicKey, signatureAlgorithm: service.signatureAlgorithm, cipherAlgorithm: service.cipherAlgorithm, diff --git a/packages/backend/src/server/api/endpoints/admin/sso/update.ts b/packages/backend/src/server/api/endpoints/admin/sso/update.ts index 909f4add6a..c72709f5ee 100644 --- a/packages/backend/src/server/api/endpoints/admin/sso/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/sso/update.ts @@ -64,7 +64,7 @@ export default class extends Endpoint { // eslint- await this.singleSignOnServiceProviderRepository.update(service.id, { name: ps.name !== '' ? ps.name : null, issuer: ps.issuer, - audience: ps.audience?.filter(i => !!i), + audience: ps.audience?.filter(i => i.length > 0), acsUrl: ps.acsUrl, publicKey: publicKey, privateKey: privateKey, diff --git a/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts b/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts index 4cdc828ab8..fee6e9f74b 100644 --- a/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts +++ b/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts @@ -1,5 +1,6 @@ import { fileURLToPath } from 'node:url'; import { randomUUID } from 'node:crypto'; +import forge from 'node-forge'; import * as jose from 'jose'; import * as Redis from 'ioredis'; import * as saml from 'samlify'; @@ -55,17 +56,33 @@ export class SAMLIdentifyProviderService { public async createIdPMetadataXml( provider: MiSingleSignOnServiceProvider, ): Promise { - const today = new Date(); - const publicKey = await jose - .importJWK(JSON.parse(provider.publicKey)) + const nowTime = new Date(); + const tenYearsLaterTime = new Date(nowTime.getTime()); + tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10); + const tenYearsLater = tenYearsLaterTime.toISOString(); + + const cert = forge.pki.createCertificate(); + cert.serialNumber = '01'; + cert.validity.notBefore = provider.createdAt; + cert.validity.notAfter = tenYearsLaterTime; + const attrs = [{ name: 'commonName', value: this.config.hostname }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.publicKey = await jose.importJWK(JSON.parse(provider.publicKey)) .then(k => jose.exportSPKI(k as jose.KeyLike)) - .then(k => k.replace(/-----(?:BEGIN|END) PUBLIC KEY-----|\s/g, '')); + .then(k => forge.pki.publicKeyFromPem(k)); + cert.sign( + await jose.importJWK(JSON.parse(provider.privateKey ?? '{}')) + .then(k => jose.exportPKCS8(k as jose.KeyLike)) + .then(k => forge.pki.privateKeyFromPem(k)), + forge.md.sha256.create(), + ); const nodes = { 'md:EntityDescriptor': { '@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', '@entityID': provider.issuer, - '@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(), + '@validUntil': tenYearsLater, 'md:IDPSSODescriptor': { '@WantAuthnRequestsSigned': provider.wantAuthnRequestsSigned, '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', @@ -75,7 +92,7 @@ export class SAMLIdentifyProviderService { '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', 'ds:X509Data': { 'ds:X509Certificate': { - '#text': publicKey, + '#text': forge.pki.certificateToPem(cert).replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''), }, }, }, @@ -88,10 +105,6 @@ export class SAMLIdentifyProviderService { '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', '@Location': `${this.config.url}/sso/saml/${provider.id}`, }, - { - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - '@Location': `${this.config.url}/sso/saml/${provider.id}`, - }, ], }, }, @@ -105,11 +118,28 @@ export class SAMLIdentifyProviderService { public async createSPMetadataXml( provider: MiSingleSignOnServiceProvider, ): Promise { - const today = new Date(); - const publicKey = await jose - .importJWK(JSON.parse(provider.publicKey)) + const nowTime = new Date(); + const tenYearsLaterTime = new Date(nowTime.getTime()); + tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10); + const tenYearsLater = tenYearsLaterTime.toISOString(); + + const cert = forge.pki.createCertificate(); + cert.serialNumber = '01'; + cert.validity.notBefore = provider.createdAt; + cert.validity.notAfter = tenYearsLaterTime; + const attrs = [{ name: 'commonName', value: this.config.hostname }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.publicKey = await jose.importJWK(JSON.parse(provider.publicKey)) .then(k => jose.exportSPKI(k as jose.KeyLike)) - .then(k => k.replace(/-----(?:BEGIN|END) PUBLIC KEY-----|\s/g, '')); + .then(k => forge.pki.publicKeyFromPem(k)); + cert.sign( + await jose.importJWK(JSON.parse(provider.privateKey ?? '{}')) + .then(k => jose.exportPKCS8(k as jose.KeyLike)) + .then(k => forge.pki.privateKeyFromPem(k)), + forge.md.sha256.create(), + ); + const x509 = forge.pki.certificateToPem(cert).replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''); const keyDescriptor: unknown[] = [ { @@ -118,7 +148,7 @@ export class SAMLIdentifyProviderService { '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', 'ds:X509Data': { 'ds:X509Certificate': { - '#text': publicKey, + '#text': x509, }, }, }, @@ -132,13 +162,13 @@ export class SAMLIdentifyProviderService { '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', 'ds:X509Data': { 'ds:X509Certificate': { - '#text': publicKey, + '#text': x509, }, }, }, - 'md:EncryptionMethod': { - '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc', - }, + 'md:EncryptionMethod': [ + { '@Algorithm': `http://www.w3.org/2001/04/xmlenc#${provider.cipherAlgorithm}` }, + ], }); } @@ -146,7 +176,7 @@ export class SAMLIdentifyProviderService { 'md:EntityDescriptor': { '@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', '@entityID': provider.issuer, - '@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(), + '@validUntil': tenYearsLater, 'md:SPSSODescriptor': { '@AuthnRequestsSigned': provider.wantAuthnRequestsSigned, '@WantAssertionsSigned': provider.wantAssertionsSigned, @@ -155,11 +185,13 @@ export class SAMLIdentifyProviderService { 'md:NameIDFormat': { '#text': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', }, - 'md:AssertionConsumerService': { - '@index': 1, - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - '@Location': provider.acsUrl, - }, + 'md:AssertionConsumerService': [ + { + '@index': 1, + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + '@Location': provider.acsUrl, + }, + ], }, }, }; @@ -203,7 +235,7 @@ export class SAMLIdentifyProviderService { Body?: { SAMLRequest?: string; RelayState?: string }; }>('/:serviceId', async (request, reply) => { const serviceId = request.params.serviceId; - const binding = request.query?.SAMLRequest ? 'redirect' : 'post'; + const binding = 'redirect'; // 今はリダイレクトのみ対応 request.query?.SAMLRequest ? 'redirect' : 'post'; const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest; const relayState = request.query?.RelayState ?? request.body?.RelayState; @@ -236,38 +268,63 @@ export class SAMLIdentifyProviderService { metadata: await this.createIdPMetadataXml(ssoServiceProvider), privateKey: await jose .importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}')) - .then(k => jose.exportPKCS8(k as jose.KeyLike)) - .then(k => k.replace(/-----(?:BEGIN|END) PRIVATE KEY-----|\s/g, '')), + .then(k => jose.exportPKCS8(k as jose.KeyLike)), }); const sp = saml.ServiceProvider({ metadata: await this.createSPMetadataXml(ssoServiceProvider), }); - const parsed = await idp.parseLoginRequest(sp, binding, { query: request.query, body: request.body }); - this.#logger.info('Parsed SAML request', { saml: parsed }); + try { + const parsed = await idp.parseLoginRequest(sp, binding, { query: request.query, body: request.body }); + this.#logger.info('Parsed SAML request', { saml: parsed }); - const transactionId = randomUUID(); - await this.redisClient.set( - `sso:saml:transaction:${transactionId}`, - JSON.stringify({ - serviceId: serviceId, - binding: binding, - flowResult: parsed, - relayState: relayState, - }), - 'EX', - 60 * 5, - ); + const transactionId = randomUUID(); + await this.redisClient.set( + `sso:saml:transaction:${transactionId}`, + JSON.stringify({ + serviceId: serviceId, + binding: binding, + flowResult: parsed, + relayState: relayState, + }), + 'EX', + 60 * 5, + ); - this.#logger.info(`Rendering authorization page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`); + this.#logger.info(`Rendering authorization page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`); - reply.header('Cache-Control', 'no-store'); - return await reply.view('sso', { - transactionId: transactionId, - serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer, - kind: 'saml', - }); + reply.header('Cache-Control', 'no-store'); + return await reply.view('sso', { + transactionId: transactionId, + serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer, + kind: 'saml', + }); + } catch (err) { + this.#logger.error('Failed to parse SAML request', { error: err }); + const traceableError = err as Error & {code?: string}; + + if (traceableError.code) { + reply.status(500).send({ + error: { + message: traceableError.message, + code: traceableError.code, + id: 'a8aa8e69-a4c3-4148-8efe-fe3e8cb89684', + kind: 'client', + }, + }); + return; + } + + reply.status(400).send({ + error: { + message: 'Invalid SAML Request', + code: 'INVALID_SAML_REQUEST', + id: '874b9cc2-71cb-4000-95c7-449391ee9861', + kind: 'client', + }, + }); + } }); fastify.get<{ Params: { serviceId: string } }>( @@ -371,8 +428,7 @@ export class SAMLIdentifyProviderService { metadata: await this.createIdPMetadataXml(ssoServiceProvider), privateKey: await jose .importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}')) - .then(k => jose.exportPKCS8(k as jose.KeyLike)) - .then(k => k.replace(/-----(?:BEGIN|END) PRIVATE KEY-----|\s/g, '')), + .then(k => jose.exportPKCS8(k as jose.KeyLike)), loginResponseTemplate: { context: 'ignored' }, }); @@ -380,7 +436,7 @@ export class SAMLIdentifyProviderService { metadata: await this.createSPMetadataXml(ssoServiceProvider), }); - const samlResponse = await idp.createLoginResponse( + const loginResponse = await idp.createLoginResponse( sp, flowResult, binding, @@ -422,8 +478,7 @@ export class SAMLIdentifyProviderService { }, 'saml:Subject': { 'saml:NameID': { - '@Format': - 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + '@Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', '#text': user.id, }, 'saml:SubjectConfirmation': { @@ -453,8 +508,7 @@ export class SAMLIdentifyProviderService { '@SessionNotOnOrAfter': fiveMinutesLater, 'saml:AuthnContext': { 'saml:AuthnContextClassRef': { - '#text': - 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + '#text': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', }, }, }, @@ -462,8 +516,7 @@ export class SAMLIdentifyProviderService { 'saml:Attribute': [ { '@Name': 'identityprovider', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': this.config.url, @@ -471,8 +524,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'uid', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': user.id, @@ -480,8 +532,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'displayname', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': user.name, @@ -489,8 +540,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'name', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': user.username, @@ -498,8 +548,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'preferred_username', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': user.username, @@ -507,8 +556,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'profile', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': `${this.config.url}/@${user.username}`, @@ -516,8 +564,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'picture', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': user.avatarUrl, @@ -525,8 +572,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'mail', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': profile.email, @@ -534,8 +580,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'email', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:string', '#text': profile.email, @@ -543,8 +588,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'email_verified', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:boolean', '#text': profile.emailVerified, @@ -552,8 +596,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'mfa_enabled', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:boolean', '#text': profile.twoFactorEnabled, @@ -561,8 +604,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'updated_at', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:integer', '#text': Math.floor((user.updatedAt?.getTime() ?? user.createdAt.getTime()) / 1000), @@ -570,8 +612,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'admin', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:boolean', '#text': isAdministrator, @@ -579,8 +620,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'moderator', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xsi:type': 'xs:boolean', '#text': isModerator, @@ -588,8 +628,7 @@ export class SAMLIdentifyProviderService { }, { '@Name': 'roles', - '@NameFormat': - 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': [ ...roles .filter((r) => r.isPublic) @@ -616,7 +655,7 @@ export class SAMLIdentifyProviderService { relayState, ); - this.#logger.info(`Rendering SAML response page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`, { + this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, { userId: user.id, ssoServiceProvider: ssoServiceProvider.id, acsUrl: ssoServiceProvider.acsUrl, @@ -624,11 +663,8 @@ export class SAMLIdentifyProviderService { }); reply.header('Cache-Control', 'no-store'); - return await reply.view('sso-saml-post', { - acsUrl: ssoServiceProvider.acsUrl, - samlResponse: samlResponse, - relyState: relayState ?? null, - }); + reply.redirect(loginResponse.context); + return; } catch (err) { this.#logger.error('Failed to create SAML response', { error: err }); const traceableError = err as Error & { code?: string }; diff --git a/packages/backend/src/server/web/views/sso-saml-post.pug b/packages/backend/src/server/web/views/sso-saml-post.pug deleted file mode 100644 index a3cf7e2391..0000000000 --- a/packages/backend/src/server/web/views/sso-saml-post.pug +++ /dev/null @@ -1,21 +0,0 @@ -html - body - noscript: p - | JavaScriptを有効にしてください - br - | Please turn on your JavaScript - - p - | Redirecting... - - form(id='sso', method='post', action=action autocomplete='off') - input(type='hidden', name='SAMLResponse', value=samlResponse) - - if relayState !== null - input(type='hidden', name='RelayState', value=relayState) - - button(type='submit') - | click here if you are not redirected. - - script. - document.forms[0].submit(); diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 5455f25a38..c0be7f91bd 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -427,12 +427,12 @@ function ssoServiceAddNew() { issuer: '', audience: '', acsUrl: '', + useCertificate: false, publicKey: '', signatureAlgorithm: 'HS256', cipherAlgorithm: '', wantAuthnRequestsSigned: false, wantAssertionsSigned: true, - useCertificate: false, regenerateCertificate: false, }); } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index faaaa4c60d..73c8de605d 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -10488,6 +10488,7 @@ export type operations = { issuer: string; audience: string[]; acsUrl: string; + useCertificate: boolean; publicKey: string; signatureAlgorithm: string; cipherAlgorithm?: string | null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 955f5a11e7..ce245ffb8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,6 +295,9 @@ importers: node-fetch: specifier: 3.3.2 version: 3.3.2 + node-forge: + specifier: 1.3.1 + version: 1.3.1 nodemailer: specifier: 6.9.12 version: 6.9.12 @@ -588,6 +591,9 @@ importers: '@types/node': specifier: 20.11.27 version: 20.11.27 + '@types/node-forge': + specifier: 1.3.11 + version: 1.3.11 '@types/nodemailer': specifier: 6.4.14 version: 6.4.14 @@ -7509,6 +7515,12 @@ packages: form-data: 4.0.0 dev: false + /@types/node-forge@1.3.11: + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + dependencies: + '@types/node': 20.11.27 + dev: true + /@types/node@18.19.24: resolution: {integrity: sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==} dependencies: