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 { Inject, Injectable } from '@nestjs/common';
|
||||
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 type { Config } from '@/config.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.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';
|
||||
|
||||
export const meta = {
|
||||
@ -108,6 +110,8 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
|
||||
@ -125,16 +129,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}))
|
||||
: { 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({
|
||||
id: randomUUID(),
|
||||
createdAt: new Date(),
|
||||
createdAt: now,
|
||||
name: ps.name ? ps.name : null,
|
||||
type: ps.type,
|
||||
issuer: ps.issuer,
|
||||
audience: ps.audience?.filter(i => i.length > 0) ?? [],
|
||||
binding: ps.binding,
|
||||
acsUrl: ps.acsUrl,
|
||||
publicKey: publicKey,
|
||||
publicKey: ps.type === 'saml' && ps.useCertificate ? x509Cert : publicKey,
|
||||
privateKey: privateKey,
|
||||
signatureAlgorithm: ps.signatureAlgorithm,
|
||||
cipherAlgorithm: ps.cipherAlgorithm ? ps.cipherAlgorithm : null,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import * as jose from 'jose';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||
import * as jose from 'jose';
|
||||
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 { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { genX509CertFromJWK } from '@/misc/gen-x509-cert-from-jwk.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@ -44,6 +46,8 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
|
||||
@ -62,13 +66,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}))
|
||||
: { 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, {
|
||||
name: ps.name !== '' ? ps.name : null,
|
||||
createdAt: service.type === 'saml' && ps.regenerateCertificate ? now : undefined,
|
||||
issuer: ps.issuer,
|
||||
audience: ps.audience?.filter(i => i.length > 0),
|
||||
binding: ps.binding,
|
||||
acsUrl: ps.acsUrl,
|
||||
publicKey: publicKey,
|
||||
publicKey: service.type === 'saml' && ps.regenerateCertificate ? x509Cert : publicKey,
|
||||
privateKey: privateKey,
|
||||
signatureAlgorithm: ps.signatureAlgorithm,
|
||||
cipherAlgorithm: ps.cipherAlgorithm !== '' ? ps.cipherAlgorithm : null,
|
||||
|
@ -1,6 +1,5 @@
|
||||
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';
|
||||
@ -56,28 +55,10 @@ export class SAMLIdentifyProviderService {
|
||||
public async createIdPMetadataXml(
|
||||
provider: MiSingleSignOnServiceProvider,
|
||||
): Promise<string> {
|
||||
const nowTime = new Date();
|
||||
const tenYearsLaterTime = new Date(nowTime.getTime());
|
||||
const tenYearsLaterTime = new Date(provider.createdAt.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 => 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',
|
||||
@ -92,7 +73,7 @@ export class SAMLIdentifyProviderService {
|
||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'ds:X509Data': {
|
||||
'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(
|
||||
provider: MiSingleSignOnServiceProvider,
|
||||
): Promise<string> {
|
||||
const nowTime = new Date();
|
||||
const tenYearsLaterTime = new Date(nowTime.getTime());
|
||||
const tenYearsLaterTime = new Date(provider.createdAt.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 => 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[] = [
|
||||
{
|
||||
'@use': 'signing',
|
||||
@ -153,7 +115,7 @@ export class SAMLIdentifyProviderService {
|
||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'ds:X509Data': {
|
||||
'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#',
|
||||
'ds:X509Data': {
|
||||
'ds:X509Certificate': {
|
||||
'#text': x509,
|
||||
'#text': provider.publicKey.replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -12,6 +12,10 @@ export default defineComponent({
|
||||
modelValue: {
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const value = ref(props.modelValue);
|
||||
@ -41,6 +45,7 @@ export default defineComponent({
|
||||
key: option.key as string,
|
||||
value: option.props?.value,
|
||||
modelValue: value.value,
|
||||
disabled: props.disabled,
|
||||
'onUpdate:modelValue': _v => value.value = _v,
|
||||
}, () => option.children)),
|
||||
),
|
||||
|
@ -204,9 +204,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput v-model="service.acsUrl">
|
||||
<template #label>Assertion Consumer Service URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="service.publicKey">
|
||||
<MkTextarea v-model="service.publicKey">
|
||||
<template #label>{{ service['useCertificate'] ? 'Public Key' : 'Secret' }}</template>
|
||||
</MkInput>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="service.signatureAlgorithm">
|
||||
<template #label>Signature Algorithm</template>
|
||||
</MkInput>
|
||||
|
Loading…
Reference in New Issue
Block a user