mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-01-14 01:21:04 +09:00
fix(SSO): SAMLのメタデータに使われる証明書を保存するように
This commit is contained in:
parent
fa4db2c420
commit
29e8fe419f
33
packages/backend/src/misc/gen-x509-cert-from-jwk.ts
Normal file
33
packages/backend/src/misc/gen-x509-cert-from-jwk.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import forge from 'node-forge';
|
||||||
|
import * as jose from 'jose';
|
||||||
|
|
||||||
|
export async function genX509CertFromJWK(
|
||||||
|
hostname: string,
|
||||||
|
notBefore: Date,
|
||||||
|
notAfter: Date,
|
||||||
|
publicKey: string,
|
||||||
|
privateKey: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const cert = forge.pki.createCertificate();
|
||||||
|
cert.serialNumber = '01';
|
||||||
|
cert.validity.notBefore = notBefore;
|
||||||
|
cert.validity.notAfter = notAfter;
|
||||||
|
|
||||||
|
const attrs = [{ name: 'commonName', value: hostname }];
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
cert.publicKey = await jose
|
||||||
|
.importJWK(JSON.parse(publicKey))
|
||||||
|
.then((k) => jose.exportSPKI(k as jose.KeyLike))
|
||||||
|
.then((k) => forge.pki.publicKeyFromPem(k));
|
||||||
|
|
||||||
|
cert.sign(
|
||||||
|
await jose
|
||||||
|
.importJWK(JSON.parse(privateKey))
|
||||||
|
.then((k) => jose.exportPKCS8(k as jose.KeyLike))
|
||||||
|
.then((k) => forge.pki.privateKeyFromPem(k)),
|
||||||
|
forge.md.sha256.create(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return forge.pki.certificateToPem(cert);
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as jose from 'jose';
|
import * as jose from 'jose';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { genX509CertFromJWK } from '@/misc/gen-x509-cert-from-jwk.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@ -108,6 +110,8 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||||
|
|
||||||
@ -125,16 +129,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}))
|
}))
|
||||||
: { publicKey: ps.secret ?? randomUUID(), privateKey: null };
|
: { publicKey: ps.secret ?? randomUUID(), privateKey: null };
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const tenYearsLaterTime = new Date(now.getTime());
|
||||||
|
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
||||||
|
|
||||||
|
const x509Cert = ps.type === 'saml' && ps.useCertificate ? await genX509CertFromJWK(
|
||||||
|
this.config.hostname,
|
||||||
|
now,
|
||||||
|
tenYearsLaterTime,
|
||||||
|
publicKey,
|
||||||
|
privateKey ?? '',
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.insert({
|
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.insert({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
createdAt: new Date(),
|
createdAt: now,
|
||||||
name: ps.name ? ps.name : null,
|
name: ps.name ? ps.name : null,
|
||||||
type: ps.type,
|
type: ps.type,
|
||||||
issuer: ps.issuer,
|
issuer: ps.issuer,
|
||||||
audience: ps.audience?.filter(i => i.length > 0) ?? [],
|
audience: ps.audience?.filter(i => i.length > 0) ?? [],
|
||||||
binding: ps.binding,
|
binding: ps.binding,
|
||||||
acsUrl: ps.acsUrl,
|
acsUrl: ps.acsUrl,
|
||||||
publicKey: publicKey,
|
publicKey: ps.type === 'saml' && ps.useCertificate ? x509Cert : publicKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
signatureAlgorithm: ps.signatureAlgorithm,
|
signatureAlgorithm: ps.signatureAlgorithm,
|
||||||
cipherAlgorithm: ps.cipherAlgorithm ? ps.cipherAlgorithm : null,
|
cipherAlgorithm: ps.cipherAlgorithm ? ps.cipherAlgorithm : null,
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import * as jose from 'jose';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
import * as jose from 'jose';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { genX509CertFromJWK } from '@/misc/gen-x509-cert-from-jwk.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@ -44,6 +46,8 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||||
|
|
||||||
@ -62,13 +66,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}))
|
}))
|
||||||
: { publicKey: ps.secret ?? undefined, privateKey: undefined };
|
: { publicKey: ps.secret ?? undefined, privateKey: undefined };
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const tenYearsLaterTime = new Date(now.getTime());
|
||||||
|
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
||||||
|
|
||||||
|
const x509Cert = service.type === 'saml' && ps.regenerateCertificate ? await genX509CertFromJWK(
|
||||||
|
this.config.hostname,
|
||||||
|
now,
|
||||||
|
tenYearsLaterTime,
|
||||||
|
publicKey ?? '',
|
||||||
|
privateKey ?? '',
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
await this.singleSignOnServiceProviderRepository.update(service.id, {
|
await this.singleSignOnServiceProviderRepository.update(service.id, {
|
||||||
name: ps.name !== '' ? ps.name : null,
|
name: ps.name !== '' ? ps.name : null,
|
||||||
|
createdAt: service.type === 'saml' && ps.regenerateCertificate ? now : undefined,
|
||||||
issuer: ps.issuer,
|
issuer: ps.issuer,
|
||||||
audience: ps.audience?.filter(i => i.length > 0),
|
audience: ps.audience?.filter(i => i.length > 0),
|
||||||
binding: ps.binding,
|
binding: ps.binding,
|
||||||
acsUrl: ps.acsUrl,
|
acsUrl: ps.acsUrl,
|
||||||
publicKey: publicKey,
|
publicKey: service.type === 'saml' && ps.regenerateCertificate ? x509Cert : publicKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
signatureAlgorithm: ps.signatureAlgorithm,
|
signatureAlgorithm: ps.signatureAlgorithm,
|
||||||
cipherAlgorithm: ps.cipherAlgorithm !== '' ? ps.cipherAlgorithm : null,
|
cipherAlgorithm: ps.cipherAlgorithm !== '' ? ps.cipherAlgorithm : null,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import forge from 'node-forge';
|
|
||||||
import * as jose from 'jose';
|
import * as jose from 'jose';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import * as saml from 'samlify';
|
import * as saml from 'samlify';
|
||||||
@ -56,28 +55,10 @@ export class SAMLIdentifyProviderService {
|
|||||||
public async createIdPMetadataXml(
|
public async createIdPMetadataXml(
|
||||||
provider: MiSingleSignOnServiceProvider,
|
provider: MiSingleSignOnServiceProvider,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const nowTime = new Date();
|
const tenYearsLaterTime = new Date(provider.createdAt.getTime());
|
||||||
const tenYearsLaterTime = new Date(nowTime.getTime());
|
|
||||||
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
||||||
const tenYearsLater = tenYearsLaterTime.toISOString();
|
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 => 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 = {
|
const nodes = {
|
||||||
'md:EntityDescriptor': {
|
'md:EntityDescriptor': {
|
||||||
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||||
@ -92,7 +73,7 @@ export class SAMLIdentifyProviderService {
|
|||||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||||
'ds:X509Data': {
|
'ds:X509Data': {
|
||||||
'ds:X509Certificate': {
|
'ds:X509Certificate': {
|
||||||
'#text': forge.pki.certificateToPem(cert).replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''),
|
'#text': provider.publicKey.replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -123,29 +104,10 @@ export class SAMLIdentifyProviderService {
|
|||||||
public async createSPMetadataXml(
|
public async createSPMetadataXml(
|
||||||
provider: MiSingleSignOnServiceProvider,
|
provider: MiSingleSignOnServiceProvider,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const nowTime = new Date();
|
const tenYearsLaterTime = new Date(provider.createdAt.getTime());
|
||||||
const tenYearsLaterTime = new Date(nowTime.getTime());
|
|
||||||
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
||||||
const tenYearsLater = tenYearsLaterTime.toISOString();
|
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 => 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[] = [
|
const keyDescriptor: unknown[] = [
|
||||||
{
|
{
|
||||||
'@use': 'signing',
|
'@use': 'signing',
|
||||||
@ -153,7 +115,7 @@ export class SAMLIdentifyProviderService {
|
|||||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||||
'ds:X509Data': {
|
'ds:X509Data': {
|
||||||
'ds:X509Certificate': {
|
'ds:X509Certificate': {
|
||||||
'#text': x509,
|
'#text': provider.publicKey.replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -167,7 +129,7 @@ export class SAMLIdentifyProviderService {
|
|||||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||||
'ds:X509Data': {
|
'ds:X509Data': {
|
||||||
'ds:X509Certificate': {
|
'ds:X509Certificate': {
|
||||||
'#text': x509,
|
'#text': provider.publicKey.replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,10 @@ export default defineComponent({
|
|||||||
modelValue: {
|
modelValue: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const value = ref(props.modelValue);
|
const value = ref(props.modelValue);
|
||||||
@ -41,6 +45,7 @@ export default defineComponent({
|
|||||||
key: option.key as string,
|
key: option.key as string,
|
||||||
value: option.props?.value,
|
value: option.props?.value,
|
||||||
modelValue: value.value,
|
modelValue: value.value,
|
||||||
|
disabled: props.disabled,
|
||||||
'onUpdate:modelValue': _v => value.value = _v,
|
'onUpdate:modelValue': _v => value.value = _v,
|
||||||
}, () => option.children)),
|
}, () => option.children)),
|
||||||
),
|
),
|
||||||
|
@ -204,9 +204,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkInput v-model="service.acsUrl">
|
<MkInput v-model="service.acsUrl">
|
||||||
<template #label>Assertion Consumer Service URL</template>
|
<template #label>Assertion Consumer Service URL</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="service.publicKey">
|
<MkTextarea v-model="service.publicKey">
|
||||||
<template #label>{{ service['useCertificate'] ? 'Public Key' : 'Secret' }}</template>
|
<template #label>{{ service['useCertificate'] ? 'Public Key' : 'Secret' }}</template>
|
||||||
</MkInput>
|
</MkTextarea>
|
||||||
<MkInput v-model="service.signatureAlgorithm">
|
<MkInput v-model="service.signatureAlgorithm">
|
||||||
<template #label>Signature Algorithm</template>
|
<template #label>Signature Algorithm</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
Loading…
Reference in New Issue
Block a user