mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 02:51:51 +00:00
Start work on PkiSubscriberDetailsByIDPage
This commit is contained in:
@@ -20,6 +20,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
TUpdateSecretSyncDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
import { CertKeyUsage, CertExtendedKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||
@@ -242,6 +242,8 @@ export enum EventType {
|
||||
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
|
||||
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
|
||||
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
@@ -1946,6 +1948,22 @@ interface GetPkiSubscriber {
|
||||
};
|
||||
}
|
||||
|
||||
interface IssuePkiSubscriberCert {
|
||||
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignPkiSubscriberCert {
|
||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
@@ -2908,6 +2926,8 @@ export type Event =
|
||||
| UpdatePkiSubscriber
|
||||
| DeletePkiSubscriber
|
||||
| GetPkiSubscriber
|
||||
| IssuePkiSubscriberCert
|
||||
| SignPkiSubscriberCert
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
|
||||
@@ -54,7 +54,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false,
|
||||
sshHostGroups: false
|
||||
sshHostGroups: true
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
||||
@@ -179,13 +179,41 @@ export const sshHostGroupServiceFactory = ({
|
||||
});
|
||||
|
||||
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||
await sshHostGroupDAL.updateById(
|
||||
sshHostGroupId,
|
||||
{
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (name) {
|
||||
// (dangtony98): room to optimize check to ensure that
|
||||
// the SSH host group name is unique across the whole org
|
||||
const project = await projectDAL.findById(sshHostGroup.projectId, tx);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` });
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: project.orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingSshHostGroup = await sshHostGroupDAL.find(
|
||||
{
|
||||
name,
|
||||
$in: {
|
||||
projectId: projects.map((p) => p.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingSshHostGroup.length) {
|
||||
throw new BadRequestError({
|
||||
message: `SSH host group with name '${name}' already exists in the organization`
|
||||
});
|
||||
}
|
||||
await sshHostGroupDAL.updateById(
|
||||
sshHostGroupId,
|
||||
{
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
|
||||
@@ -954,6 +954,15 @@ export const registerRoutes = async (
|
||||
|
||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { validateAltNameField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -31,7 +33,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.getPkiSubscriberById({
|
||||
const subscriber = await server.services.pkiSubscriber.getSubscriberById({
|
||||
subscriberId: req.params.subscriberId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -103,7 +105,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.createPkiSubscriber({
|
||||
const subscriber = await server.services.pkiSubscriber.createSubscriber({
|
||||
...req.body,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -185,7 +187,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.updatePkiSubscriber({
|
||||
const subscriber = await server.services.pkiSubscriber.updateSubscriber({
|
||||
subscriberId: req.params.subscriberId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -235,7 +237,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.deletePkiSubscriber({
|
||||
const subscriber = await server.services.pkiSubscriber.deleteSubscriber({
|
||||
subscriberId: req.params.subscriberId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -264,7 +266,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
@@ -283,37 +285,43 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
// TODO: reuse issueCertFromCa fn (or not since we are adding support for external CAs?)
|
||||
// const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } =
|
||||
// await server.services.pkiSubscriber.issuePkiSubscriberCertificate({
|
||||
// subscriberId: req.params.subscriberId,
|
||||
// actor: req.permission.type,
|
||||
// actorId: req.permission.id,
|
||||
// actorAuthMethod: req.permission.authMethod,
|
||||
// actorOrgId: req.permission.orgId
|
||||
// });
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, subscriber } =
|
||||
await server.services.pkiSubscriber.issueSubscriberCert({
|
||||
subscriberId: req.params.subscriberId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ISSUE_SSH_HOST_USER_CERT,
|
||||
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
|
||||
metadata: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
loginUser: req.body.loginUser,
|
||||
principals,
|
||||
ttl: host.userCertTtl
|
||||
subscriberId: subscriber.id,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
signedKey: signedPublicKey,
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
publicKey,
|
||||
keyAlgorithm
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -324,7 +332,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
@@ -333,7 +341,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
subscriberId: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberId)
|
||||
}),
|
||||
body: z.object({
|
||||
csr: z.string().trim().min(1).
|
||||
csr: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -345,36 +353,43 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
// TODO: reuse issueCertFromCa fn (or not since we are adding support for external CAs?)
|
||||
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } =
|
||||
await server.services.pkiSubscriber.issuePkiSubscriberCertificate({
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, subscriber } =
|
||||
await server.services.pkiSubscriber.signSubscriberCert({
|
||||
subscriberId: req.params.subscriberId,
|
||||
csr: req.body.csr,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// orgId: req.permission.orgId,
|
||||
// event: {
|
||||
// type: EventType.ISSUE_SSH_HOST_USER_CERT,
|
||||
// metadata: {
|
||||
// sshHostId: req.params.sshHostId,
|
||||
// hostname: host.hostname,
|
||||
// loginUser: req.body.loginUser,
|
||||
// principals,
|
||||
// ttl: host.userCertTtl
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT,
|
||||
metadata: {
|
||||
subscriberId: subscriber.id,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
signedKey: signedPublicKey,
|
||||
publicKey,
|
||||
keyAlgorithm
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto, { KeyObject } from "crypto";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
@@ -11,10 +13,26 @@ import {
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { getCaCredentials, keyAlgorithmToAlgCfg } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import {
|
||||
createSerialNumber,
|
||||
getCaCertChain,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg,
|
||||
parseDistinguishedName
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
@@ -31,32 +49,41 @@ import {
|
||||
} from "./pki-subscriber-types";
|
||||
|
||||
type TPkiSubscriberServiceFactoryDep = {
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "create" | "findById" | "updateById" | "deleteById">;
|
||||
pkiSubscriberDAL: Pick<
|
||||
TPkiSubscriberDALFactory,
|
||||
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find"
|
||||
>;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
|
||||
|
||||
// TODO: bind subscribers to CA
|
||||
|
||||
export const pkiSubscriberServiceFactory = ({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
}: TPkiSubscriberServiceFactoryDep) => {
|
||||
const createPkiSubscriber = async ({
|
||||
const createSubscriber = async ({
|
||||
name,
|
||||
commonName,
|
||||
caId, // (dangtony98) consider by CA name instead (newly-introduced field)
|
||||
caId,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
@@ -82,21 +109,55 @@ export const pkiSubscriberServiceFactory = ({
|
||||
ProjectPermissionSub.PkiSubscribers
|
||||
);
|
||||
|
||||
const newSubscriber = await pkiSubscriberDAL.create({
|
||||
caId,
|
||||
projectId,
|
||||
name,
|
||||
commonName,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
const newSubscriber = await pkiSubscriberDAL.transaction(async (tx) => {
|
||||
// (dangtony98): room to optimize check to ensure that
|
||||
// the PKI subscriber name is unique across the whole org
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: project.orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingPkiSubscriber = await pkiSubscriberDAL.find(
|
||||
{
|
||||
name,
|
||||
$in: {
|
||||
projectId: projects.map((p) => p.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingPkiSubscriber.length) {
|
||||
throw new BadRequestError({
|
||||
message: `PKI subscriber with name '${name}' already exists in the organization`
|
||||
});
|
||||
}
|
||||
|
||||
const subscriber = await pkiSubscriberDAL.create(
|
||||
{
|
||||
caId,
|
||||
projectId,
|
||||
name,
|
||||
commonName,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return subscriber;
|
||||
});
|
||||
|
||||
return newSubscriber;
|
||||
};
|
||||
|
||||
const getPkiSubscriberById = async ({
|
||||
const getSubscriberById = async ({
|
||||
subscriberId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -124,7 +185,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
return subscriber;
|
||||
};
|
||||
|
||||
const updatePkiSubscriber = async ({
|
||||
const updateSubscriber = async ({
|
||||
subscriberId,
|
||||
name,
|
||||
commonName,
|
||||
@@ -138,13 +199,13 @@ export const pkiSubscriberServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdatePkiSubscriberDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber with ID '${subscriberId}' not found` });
|
||||
const foundSubscriber = await pkiSubscriberDAL.findById(subscriberId);
|
||||
if (!foundSubscriber) throw new NotFoundError({ message: `PKI subscriber with ID '${subscriberId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
projectId: foundSubscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@@ -156,20 +217,57 @@ export const pkiSubscriberServiceFactory = ({
|
||||
ProjectPermissionSub.PkiSubscribers
|
||||
);
|
||||
|
||||
const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriberId, {
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
const updatedSubscriber = await pkiSubscriberDAL.transaction(async (tx) => {
|
||||
if (name) {
|
||||
// (dangtony98): room to optimize check to ensure that
|
||||
// the PKI subscriber name is unique across the whole org
|
||||
const project = await projectDAL.findById(foundSubscriber.projectId, tx);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${foundSubscriber.projectId}' not found` });
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: project.orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingPkiSubscriber = await pkiSubscriberDAL.find(
|
||||
{
|
||||
name,
|
||||
$in: {
|
||||
projectId: projects.map((p) => p.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingPkiSubscriber.length) {
|
||||
throw new BadRequestError({
|
||||
message: `PKI subscriber with name '${name}' already exists in the organization`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const subscriber = await pkiSubscriberDAL.updateById(
|
||||
subscriberId,
|
||||
{
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return subscriber;
|
||||
});
|
||||
|
||||
return updatedSubscriber;
|
||||
};
|
||||
|
||||
const deletePkiSubscriber = async ({
|
||||
const deleteSubscriber = async ({
|
||||
subscriberId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -199,7 +297,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
return subscriber;
|
||||
};
|
||||
|
||||
const issuePkiSubscriberCert = async ({
|
||||
const issueSubscriberCert = async ({
|
||||
subscriberId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -300,15 +398,111 @@ export const pkiSubscriberServiceFactory = ({
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[];
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: Buffer.from(skLeaf)
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: caCert.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: certificateChainPem,
|
||||
issuingCaCertificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber,
|
||||
ca,
|
||||
subscriber
|
||||
};
|
||||
};
|
||||
|
||||
const signPkiSubscriberCert = async ({
|
||||
const signSubscriberCert = async ({
|
||||
subscriberId,
|
||||
csr,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignPkiSubscriberCertDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber with ID '${subscriberId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
|
||||
@@ -328,14 +522,237 @@ export const pkiSubscriberServiceFactory = ({
|
||||
ProjectPermissionPkiSubscriberActions.SignCert,
|
||||
ProjectPermissionSub.PkiSubscribers
|
||||
);
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
if (ca.requireTemplateForIssuance) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
|
||||
const dn = parseDistinguishedName(csrObj.subject);
|
||||
const cn = dn.commonName;
|
||||
if (cn !== subscriber.commonName) {
|
||||
throw new BadRequestError({ message: "Common name (CN) in the CSR does not match the subscriber's common name" });
|
||||
}
|
||||
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
// handle key usages
|
||||
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||
let csrKeyUsages: CertKeyUsage[] = [];
|
||||
if (csrKeyUsageExtension) {
|
||||
csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||
|
||||
if (csrKeyUsages.some((keyUsage) => !selectedKeyUsages.includes(keyUsage))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid key usage value based on subscriber's specified key usages"
|
||||
});
|
||||
}
|
||||
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
// handle extended key usages
|
||||
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||
let csrExtendedKeyUsages: CertExtendedKeyUsage[] = [];
|
||||
if (csrExtendedKeyUsageExtension) {
|
||||
csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||
);
|
||||
}
|
||||
|
||||
const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[];
|
||||
if (csrExtendedKeyUsages.some((eku) => !selectedExtendedKeyUsages.includes(eku))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid extended key usage value based on subscriber's specified extended key usages"
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedExtendedKeyUsages.length) {
|
||||
extensions.push(
|
||||
new x509.ExtendedKeyUsageExtension(
|
||||
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// attempt to read from CSR if altNames is not explicitly provided
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
|
||||
altNamesArray = sanNames.items
|
||||
.filter((value) => value.type === "email" || value.type === "dns")
|
||||
.map((name) => ({
|
||||
type: name.type as "email" | "dns",
|
||||
value: name.value
|
||||
}));
|
||||
}
|
||||
|
||||
if (
|
||||
altNamesArray
|
||||
.map((altName) => altName.value)
|
||||
.some((altName) => !subscriber.subjectAlternativeNames.includes(altName))
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid subject alternative name based on subscriber's specified subject alternative names"
|
||||
});
|
||||
}
|
||||
|
||||
if (altNamesArray.length) {
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return cert;
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert,
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
ca,
|
||||
commonName: subscriber.commonName,
|
||||
subscriber
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createPkiSubscriber,
|
||||
getPkiSubscriberById,
|
||||
updatePkiSubscriber,
|
||||
deletePkiSubscriber,
|
||||
issuePkiSubscriberCert,
|
||||
signPkiSubscriberCert
|
||||
createSubscriber,
|
||||
getSubscriberById,
|
||||
updateSubscriber,
|
||||
deleteSubscriber,
|
||||
issueSubscriberCert,
|
||||
signSubscriberCert
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,4 +37,5 @@ export type TIssuePkiSubscriberCertDTO = {
|
||||
|
||||
export type TSignPkiSubscriberCertDTO = {
|
||||
subscriberId: string;
|
||||
csr: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -188,6 +188,7 @@ export type TSignCertificateEvent = {
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
subscriberId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
@@ -198,6 +199,7 @@ export type TIssueCertificateEvent = {
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
subscriberId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user