fix(SSO): SAMLのメタデータに使われる証明書を保存するように

This commit is contained in:
まっちゃとーにゅ 2024-03-18 01:19:16 +09:00
parent fa4db2c420
commit 29e8fe419f
No known key found for this signature in database
GPG Key ID: 6AFBBF529601C1DB
6 changed files with 85 additions and 52 deletions

View 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);
}

View File

@ -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,

View File

@ -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,

View File

@ -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, ''),
},
},
},

View File

@ -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)),
),

View File

@ -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>