mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Address PR comments and improvements
This commit is contained in:
@@ -388,6 +388,9 @@ export enum EventType {
|
||||
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
|
||||
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
|
||||
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
|
||||
CREATE_CERTIFICATE_REQUEST = "create-certificate-request",
|
||||
GET_CERTIFICATE_REQUEST = "get-certificate-request",
|
||||
GET_CERTIFICATE_FROM_REQUEST = "get-certificate-from-request",
|
||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
@@ -2846,7 +2849,6 @@ interface OrderCertificateFromProfile {
|
||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
|
||||
metadata: {
|
||||
certificateProfileId: string;
|
||||
orderId: string;
|
||||
profileName: string;
|
||||
};
|
||||
}
|
||||
@@ -4196,6 +4198,31 @@ interface DisableCertificateRenewalConfigEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCertificateRequestEvent {
|
||||
type: EventType.CREATE_CERTIFICATE_REQUEST;
|
||||
metadata: {
|
||||
certificateRequestId: string;
|
||||
profileId?: string;
|
||||
caId?: string;
|
||||
commonName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateRequestEvent {
|
||||
type: EventType.GET_CERTIFICATE_REQUEST;
|
||||
metadata: {
|
||||
certificateRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateFromRequestEvent {
|
||||
type: EventType.GET_CERTIFICATE_FROM_REQUEST;
|
||||
metadata: {
|
||||
certificateRequestId: string;
|
||||
certificateId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| CreateSubOrganizationEvent
|
||||
| UpdateSubOrganizationEvent
|
||||
@@ -4575,6 +4602,9 @@ export type Event =
|
||||
| PamResourceDeleteEvent
|
||||
| UpdateCertificateRenewalConfigEvent
|
||||
| DisableCertificateRenewalConfigEvent
|
||||
| CreateCertificateRequestEvent
|
||||
| GetCertificateRequestEvent
|
||||
| GetCertificateFromRequestEvent
|
||||
| AutomatedRenewCertificate
|
||||
| AutomatedRenewCertificateFailed
|
||||
| UserLoginEvent
|
||||
|
||||
@@ -2259,7 +2259,8 @@ export const registerRoutes = async (
|
||||
kmsService,
|
||||
projectDAL,
|
||||
certificateBodyDAL,
|
||||
certificateIssuanceQueue
|
||||
certificateIssuanceQueue,
|
||||
certificateRequestService
|
||||
});
|
||||
|
||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||
|
||||
@@ -665,15 +665,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
const response = await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
|
||||
@@ -299,7 +299,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
certificateAuthority: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
projectId: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
status: z.string(),
|
||||
name: z.string(),
|
||||
isExternal: z.boolean().optional(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema, TCertificateRequests } from "@app/db/schemas";
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
@@ -13,7 +13,6 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
ACMESANType,
|
||||
CertificateOrderStatus,
|
||||
CertKeyAlgorithm,
|
||||
CertSignatureAlgorithm,
|
||||
CrlReason
|
||||
@@ -30,9 +29,21 @@ import { mapEnumsForValidation } from "@app/services/certificate-common/certific
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
import { TCertificateFromProfileResponse } from "@app/services/certificate-v3/certificate-v3-types";
|
||||
|
||||
import { booleanSchema } from "../sanitizedSchemas";
|
||||
|
||||
type CertificateServiceResponse = TCertificateFromProfileResponse | Omit<TCertificateFromProfileResponse, "privateKey">;
|
||||
|
||||
const extractCertificateData = (data: CertificateServiceResponse) => ({
|
||||
certificate: data.certificate,
|
||||
issuingCaCertificate: data.issuingCaCertificate,
|
||||
certificateChain: data.certificateChain,
|
||||
privateKey: "privateKey" in data ? data.privateKey : undefined,
|
||||
serialNumber: data.serialNumber,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
|
||||
interface CertificateRequestForService {
|
||||
commonName?: string;
|
||||
keyUsages?: CertKeyUsageType[];
|
||||
@@ -50,13 +61,32 @@ interface CertificateRequestForService {
|
||||
keyAlgorithm?: string;
|
||||
}
|
||||
|
||||
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
|
||||
const validateTtlAndDateFields = (data: {
|
||||
attributes?: { notBefore?: string; notAfter?: string; ttl?: string };
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
ttl?: string;
|
||||
}) => {
|
||||
if (data.attributes) {
|
||||
const hasDateFields = data.attributes.notBefore || data.attributes.notAfter;
|
||||
const hasTtl = data.attributes.ttl;
|
||||
return !(hasDateFields && hasTtl);
|
||||
}
|
||||
const hasDateFields = data.notBefore || data.notAfter;
|
||||
const hasTtl = data.ttl;
|
||||
return !(hasDateFields && hasTtl);
|
||||
};
|
||||
|
||||
const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
|
||||
const validateDateOrder = (data: {
|
||||
attributes?: { notBefore?: string; notAfter?: string };
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
}) => {
|
||||
if (data.attributes?.notBefore && data.attributes?.notAfter) {
|
||||
const notBefore = new Date(data.attributes.notBefore);
|
||||
const notAfter = new Date(data.attributes.notAfter);
|
||||
return notBefore < notAfter;
|
||||
}
|
||||
if (data.notBefore && data.notAfter) {
|
||||
const notBefore = new Date(data.notBefore);
|
||||
const notAfter = new Date(data.notAfter);
|
||||
@@ -65,20 +95,6 @@ const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateCertificateRequestFlow = (data: {
|
||||
csr?: string;
|
||||
subjectAlternativeNames?: Array<{ type: ACMESANType; value: string }>;
|
||||
commonName?: string;
|
||||
altNames?: Array<{ type: CertSubjectAlternativeNameType; value: string }>;
|
||||
}) => {
|
||||
const hasCSR = !!data.csr;
|
||||
const hasSANs = !!data.subjectAlternativeNames?.length;
|
||||
const hasStandardFields = !!(data.commonName || data.altNames?.length);
|
||||
|
||||
const flowCount = Number(hasCSR) + Number(hasSANs) + Number(hasStandardFields);
|
||||
return flowCount === 1;
|
||||
};
|
||||
|
||||
export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@@ -92,45 +108,48 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
body: z
|
||||
.object({
|
||||
profileId: z.string().uuid(),
|
||||
projectId: z.string().uuid(),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||
altNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
value: z.string().min(1, "SAN value cannot be empty")
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
|
||||
csr: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "CSR cannot be empty")
|
||||
.max(4096, "CSR cannot exceed 4096 characters")
|
||||
.optional(),
|
||||
subjectAlternativeNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "SAN value cannot be empty")
|
||||
.max(255, "SAN value must be less than 255 characters")
|
||||
})
|
||||
)
|
||||
attributes: z
|
||||
.object({
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||
altNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
value: z.string().min(1, "SAN value cannot be empty")
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional(),
|
||||
subjectAlternativeNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "SAN value cannot be empty")
|
||||
.max(255, "SAN value must be less than 255 characters")
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional()
|
||||
})
|
||||
.optional(),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
removeRootsFromChain: booleanSchema.default(false).optional()
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
@@ -139,264 +158,177 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.refine(validateDateOrder, {
|
||||
message: "notBefore must be earlier than notAfter"
|
||||
})
|
||||
.refine(validateCertificateRequestFlow, {
|
||||
message:
|
||||
"Must specify exactly one of: csr (for signing), subjectAlternativeNames (for ACME), or commonName/altNames (for issuance)"
|
||||
}),
|
||||
response: {
|
||||
200: z.union([
|
||||
z.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
privateKey: z.string().trim().optional(),
|
||||
serialNumber: z.string().trim(),
|
||||
certificateId: z.string(),
|
||||
certificateRequestId: z.string()
|
||||
}),
|
||||
z.object({
|
||||
certificateRequestId: z.string(),
|
||||
status: z.string(),
|
||||
orderId: z.string().optional(),
|
||||
subjectAlternativeNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus)
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
authorizations: z
|
||||
.array(
|
||||
z.object({
|
||||
identifier: z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string()
|
||||
}),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
expires: z.string().optional(),
|
||||
challenges: z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
url: z.string(),
|
||||
token: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
finalize: z.string().optional()
|
||||
})
|
||||
])
|
||||
200: z.object({
|
||||
certificate: z
|
||||
.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
privateKey: z.string().trim().optional(),
|
||||
serialNumber: z.string().trim(),
|
||||
certificateId: z.string()
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
certificateRequestId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { csr, subjectAlternativeNames, ...requestBody } = req.body;
|
||||
|
||||
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||
const { csr, attributes, ...requestBody } = req.body;
|
||||
const profile = await server.services.certificateProfile.getProfileById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.body.projectId,
|
||||
profileId: requestBody.profileId,
|
||||
csr,
|
||||
commonName: requestBody.commonName,
|
||||
altNames: requestBody.altNames ? JSON.stringify(requestBody.altNames) : undefined,
|
||||
keyUsages: requestBody.keyUsages,
|
||||
extendedKeyUsages: requestBody.extendedKeyUsages,
|
||||
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
|
||||
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
|
||||
keyAlgorithm: requestBody.keyAlgorithm,
|
||||
signatureAlgorithm: requestBody.signatureAlgorithm,
|
||||
metadata: JSON.stringify({
|
||||
ttl: requestBody.ttl,
|
||||
removeRootsFromChain: requestBody.removeRootsFromChain,
|
||||
subjectAlternativeNames
|
||||
})
|
||||
profileId: requestBody.profileId
|
||||
});
|
||||
|
||||
try {
|
||||
if (csr) {
|
||||
const extractedCsrData = extractCertificateRequestFromCSR(csr);
|
||||
|
||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: requestBody.profileId,
|
||||
csr,
|
||||
validity: { ttl: requestBody.ttl },
|
||||
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
|
||||
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
|
||||
enrollmentType: EnrollmentType.API,
|
||||
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.certificateRequest.attachCertificateToRequest({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: requestBody.profileId,
|
||||
certificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: extractedCsrData.commonName || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
};
|
||||
}
|
||||
|
||||
const profile = await server.services.certificateProfile.getProfileById({
|
||||
let useOrderFlow = false;
|
||||
if (profile?.caId) {
|
||||
const ca = await server.services.certificateAuthority.getCaById({
|
||||
caId: profile.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: requestBody.profileId
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
useOrderFlow = caType !== CaType.INTERNAL;
|
||||
}
|
||||
|
||||
let useOrderFlow = false;
|
||||
if (profile?.caId) {
|
||||
const ca = await server.services.certificateAuthority.getCaById({
|
||||
caId: profile.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
useOrderFlow = caType !== CaType.INTERNAL;
|
||||
if (attributes?.subjectAlternativeNames?.length || useOrderFlow) {
|
||||
let acmeAltNames: Array<{ type: ACMESANType; value: string }> | undefined = attributes?.subjectAlternativeNames;
|
||||
if (useOrderFlow && !attributes?.subjectAlternativeNames && attributes?.altNames?.length) {
|
||||
acmeAltNames = attributes.altNames.map((alt: { type: CertSubjectAlternativeNameType; value: string }) => ({
|
||||
type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME
|
||||
? ACMESANType.DNS
|
||||
: ACMESANType.IP) as ACMESANType,
|
||||
value: alt.value
|
||||
}));
|
||||
}
|
||||
|
||||
if (subjectAlternativeNames?.length || useOrderFlow) {
|
||||
let acmeAltNames = subjectAlternativeNames;
|
||||
if (useOrderFlow && !subjectAlternativeNames && requestBody.altNames?.length) {
|
||||
acmeAltNames = requestBody.altNames.map((alt) => ({
|
||||
type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME
|
||||
? ACMESANType.DNS
|
||||
: ACMESANType.IP) as ACMESANType,
|
||||
value: alt.value
|
||||
}));
|
||||
}
|
||||
|
||||
const certificateOrderObject = {
|
||||
altNames: acmeAltNames || [],
|
||||
validity: { ttl: requestBody.ttl },
|
||||
commonName: requestBody.commonName,
|
||||
keyUsages: requestBody.keyUsages,
|
||||
extendedKeyUsages: requestBody.extendedKeyUsages,
|
||||
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
|
||||
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
|
||||
signatureAlgorithm: requestBody.signatureAlgorithm,
|
||||
keyAlgorithm: requestBody.keyAlgorithm
|
||||
};
|
||||
|
||||
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: requestBody.profileId,
|
||||
certificateOrder: certificateOrderObject,
|
||||
removeRootsFromChain: requestBody.removeRootsFromChain,
|
||||
certificateRequestId: certificateRequest.id
|
||||
});
|
||||
|
||||
await server.services.certificateRequest.updateCertificateRequestStatus({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
status: CertificateRequestStatus.PENDING,
|
||||
errorMessage: undefined
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: requestBody.profileId,
|
||||
orderId: data.orderId,
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificateRequestId: certificateRequest.id,
|
||||
...data
|
||||
};
|
||||
}
|
||||
const certificateRequestForService: CertificateRequestForService = {
|
||||
commonName: requestBody.commonName,
|
||||
keyUsages: requestBody.keyUsages,
|
||||
extendedKeyUsages: requestBody.extendedKeyUsages,
|
||||
altNames: requestBody.altNames,
|
||||
validity: { ttl: requestBody.ttl },
|
||||
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
|
||||
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
|
||||
signatureAlgorithm: requestBody.signatureAlgorithm,
|
||||
keyAlgorithm: requestBody.keyAlgorithm
|
||||
const certificateOrderObject = {
|
||||
altNames: acmeAltNames || [],
|
||||
validity: { ttl: attributes?.ttl || "" },
|
||||
commonName: attributes?.commonName,
|
||||
keyUsages: attributes?.keyUsages,
|
||||
extendedKeyUsages: attributes?.extendedKeyUsages,
|
||||
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
|
||||
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
|
||||
signatureAlgorithm: attributes?.signatureAlgorithm,
|
||||
keyAlgorithm: attributes?.keyAlgorithm,
|
||||
csr
|
||||
};
|
||||
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
|
||||
|
||||
const data = await server.services.certificateV3.issueCertificateFromProfile({
|
||||
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: requestBody.profileId,
|
||||
certificateRequest: mappedCertificateRequest,
|
||||
certificateOrder: certificateOrderObject,
|
||||
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.certificateRequest.attachCertificateToRequest({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
|
||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: requestBody.profileId,
|
||||
certificateId: data.certificateId,
|
||||
commonName: requestBody.commonName || "",
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
certificate: null,
|
||||
certificateRequestId: data.certificateRequestId
|
||||
};
|
||||
} catch (error) {
|
||||
await server.services.certificateRequest.updateCertificateRequestStatus({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
status: CertificateRequestStatus.FAILED,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error"
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (csr) {
|
||||
const extractedCsrData = extractCertificateRequestFromCSR(csr);
|
||||
|
||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: requestBody.profileId,
|
||||
csr,
|
||||
validity: { ttl: attributes?.ttl || "" },
|
||||
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
|
||||
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
|
||||
enrollmentType: EnrollmentType.API,
|
||||
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: requestBody.profileId,
|
||||
certificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: extractedCsrData.commonName || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
certificate: extractCertificateData(data),
|
||||
certificateRequestId: data.certificateRequestId
|
||||
};
|
||||
}
|
||||
|
||||
const certificateRequestForService: CertificateRequestForService = {
|
||||
commonName: attributes?.commonName,
|
||||
keyUsages: attributes?.keyUsages,
|
||||
extendedKeyUsages: attributes?.extendedKeyUsages,
|
||||
altNames: attributes?.altNames,
|
||||
validity: { ttl: attributes?.ttl || "" },
|
||||
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
|
||||
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
|
||||
signatureAlgorithm: attributes?.signatureAlgorithm,
|
||||
keyAlgorithm: attributes?.keyAlgorithm
|
||||
};
|
||||
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
|
||||
|
||||
const data = await server.services.certificateV3.issueCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: requestBody.profileId,
|
||||
certificateRequest: mappedCertificateRequest,
|
||||
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: requestBody.profileId,
|
||||
certificateId: data.certificateId,
|
||||
commonName: attributes?.commonName || "",
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
certificate: extractCertificateData(data),
|
||||
certificateRequestId: data.certificateRequestId
|
||||
};
|
||||
}
|
||||
});
|
||||
server.route({
|
||||
@@ -437,21 +369,18 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
certificateRequestId: req.params.requestId
|
||||
});
|
||||
|
||||
if (data.certificate && data.serialNumber) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: (req.query as { projectId: string }).projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT,
|
||||
metadata: {
|
||||
certId: req.params.requestId,
|
||||
cn: "",
|
||||
serialNumber: data.serialNumber || ""
|
||||
}
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: (req.query as { projectId: string }).projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT,
|
||||
metadata: {
|
||||
certId: req.params.requestId,
|
||||
cn: "",
|
||||
serialNumber: data.serialNumber || ""
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
@@ -539,28 +468,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
removeRootsFromChain: req.body.removeRootsFromChain
|
||||
});
|
||||
|
||||
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: data.projectId,
|
||||
profileId: req.body.profileId,
|
||||
commonName: req.body.commonName,
|
||||
altNames: req.body.altNames?.map((altName) => `${altName.type}:${altName.value}`).join(","),
|
||||
keyUsages: req.body.keyUsages,
|
||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
keyAlgorithm: req.body.keyAlgorithm,
|
||||
signatureAlgorithm: req.body.signatureAlgorithm
|
||||
});
|
||||
|
||||
await server.services.certificateRequest.attachCertificateToRequest({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
@@ -575,10 +482,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
};
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -644,29 +548,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr);
|
||||
|
||||
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: data.projectId,
|
||||
profileId: req.body.profileId,
|
||||
csr: req.body.csr,
|
||||
commonName: certificateRequestData.commonName,
|
||||
altNames: certificateRequestData.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","),
|
||||
keyUsages: certificateRequestData.keyUsages,
|
||||
extendedKeyUsages: certificateRequestData.extendedKeyUsages,
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
keyAlgorithm: certificateRequestData.keyAlgorithm,
|
||||
signatureAlgorithm: certificateRequestData.signatureAlgorithm
|
||||
});
|
||||
|
||||
await server.services.certificateRequest.attachCertificateToRequest({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
@@ -681,10 +562,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
};
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -735,34 +613,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
orderId: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
subjectAlternativeNames: z.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus)
|
||||
})
|
||||
),
|
||||
authorizations: z.array(
|
||||
z.object({
|
||||
identifier: z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string()
|
||||
}),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
expires: z.string().optional(),
|
||||
challenges: z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
url: z.string(),
|
||||
token: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
finalize: z.string(),
|
||||
certificate: z.string().optional(),
|
||||
certificateRequestId: z.string()
|
||||
})
|
||||
@@ -794,23 +644,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
removeRootsFromChain: req.body.removeRootsFromChain
|
||||
});
|
||||
|
||||
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: data.projectId,
|
||||
profileId: req.body.profileId,
|
||||
commonName: req.body.commonName,
|
||||
altNames: req.body.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","),
|
||||
keyUsages: req.body.keyUsages,
|
||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||
keyAlgorithm: req.body.keyAlgorithm
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
@@ -818,16 +651,12 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: req.body.profileId,
|
||||
orderId: data.orderId,
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
};
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -862,80 +691,41 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
let certificateRequest: TCertificateRequests | undefined;
|
||||
|
||||
try {
|
||||
const originalCertificate = await server.services.certificate.getCert({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
if (!originalCertificate) {
|
||||
throw new NotFoundError({ message: "Original certificate not found" });
|
||||
}
|
||||
|
||||
certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: originalCertificate.cert.projectId,
|
||||
profileId: originalCertificate.cert.profileId || undefined,
|
||||
caId: originalCertificate.cert.caId ?? undefined,
|
||||
metadata: JSON.stringify({
|
||||
operation: "renewal",
|
||||
originalCertificateId: req.params.id,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain
|
||||
})
|
||||
});
|
||||
|
||||
const data = await server.services.certificateV3.renewCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.id,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain,
|
||||
certificateRequestId: certificateRequest.id
|
||||
});
|
||||
|
||||
if (data.certificate && data.certificate.trim() !== "") {
|
||||
await server.services.certificateRequest.attachCertificateToRequest({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CERTIFICATE,
|
||||
metadata: {
|
||||
originalCertificateId: req.params.id,
|
||||
newCertificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
};
|
||||
} catch (error) {
|
||||
if (certificateRequest) {
|
||||
await server.services.certificateRequest.updateCertificateRequestStatus({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
status: CertificateRequestStatus.FAILED,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error during certificate renewal"
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
const originalCertificate = await server.services.certificate.getCert({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
if (!originalCertificate) {
|
||||
throw new NotFoundError({ message: "Original certificate not found" });
|
||||
}
|
||||
|
||||
const data = await server.services.certificateV3.renewCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.id,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CERTIFICATE,
|
||||
metadata: {
|
||||
originalCertificateId: req.params.id,
|
||||
newCertificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -242,14 +242,15 @@ export const registerDeprecatedCertRouter = async (server: FastifyZodProvider) =
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
const response = await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateRequests } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
@@ -8,12 +7,7 @@ import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
ACMESANType,
|
||||
CertificateOrderStatus,
|
||||
CertKeyAlgorithm,
|
||||
CertSignatureAlgorithm
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { ACMESANType, CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
@@ -23,7 +17,6 @@ import {
|
||||
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
import { booleanSchema } from "../sanitizedSchemas";
|
||||
@@ -340,34 +333,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
orderId: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
subjectAlternativeNames: z.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus)
|
||||
})
|
||||
),
|
||||
authorizations: z.array(
|
||||
z.object({
|
||||
identifier: z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string()
|
||||
}),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
expires: z.string().optional(),
|
||||
challenges: z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
url: z.string(),
|
||||
token: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
finalize: z.string(),
|
||||
certificate: z.string().optional(),
|
||||
certificateRequestId: z.string()
|
||||
})
|
||||
@@ -423,7 +388,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: req.body.profileId,
|
||||
orderId: data.orderId,
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
@@ -467,80 +431,41 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
let certificateRequest: TCertificateRequests | undefined;
|
||||
|
||||
try {
|
||||
const originalCertificate = await server.services.certificate.getCert({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.certificateId
|
||||
});
|
||||
if (!originalCertificate) {
|
||||
throw new NotFoundError({ message: "Original certificate not found" });
|
||||
}
|
||||
|
||||
certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: originalCertificate.cert.projectId,
|
||||
profileId: originalCertificate.cert.profileId || undefined,
|
||||
caId: originalCertificate.cert.caId ?? undefined,
|
||||
metadata: JSON.stringify({
|
||||
operation: "renewal",
|
||||
originalCertificateId: req.params.certificateId,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain
|
||||
})
|
||||
});
|
||||
|
||||
const data = await server.services.certificateV3.renewCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.certificateId,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain,
|
||||
certificateRequestId: certificateRequest.id
|
||||
});
|
||||
|
||||
if (data.certificate && data.certificate.trim() !== "") {
|
||||
await server.services.certificateRequest.attachCertificateToRequest({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
certificateId: data.certificateId
|
||||
});
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CERTIFICATE,
|
||||
metadata: {
|
||||
originalCertificateId: req.params.certificateId,
|
||||
newCertificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
certificateRequestId: certificateRequest.id
|
||||
};
|
||||
} catch (error) {
|
||||
if (certificateRequest) {
|
||||
await server.services.certificateRequest.updateCertificateRequestStatus({
|
||||
certificateRequestId: certificateRequest.id,
|
||||
status: CertificateRequestStatus.FAILED,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error during certificate renewal"
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
const originalCertificate = await server.services.certificate.getCert({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.certificateId
|
||||
});
|
||||
if (!originalCertificate) {
|
||||
throw new NotFoundError({ message: "Original certificate not found" });
|
||||
}
|
||||
|
||||
const data = await server.services.certificateV3.renewCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.certificateId,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CERTIFICATE,
|
||||
metadata: {
|
||||
originalCertificateId: req.params.certificateId,
|
||||
newCertificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import acme from "acme-client";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, TQueueServiceFactory } from "@app/queue";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -15,6 +14,7 @@ import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
|
||||
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import { CertKeyAlgorithm } from "../certificate-common/certificate-constants";
|
||||
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
|
||||
import { CertificateRequestStatus } from "../certificate-request/certificate-request-types";
|
||||
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||
@@ -41,6 +41,7 @@ export type TIssueCertificateFromProfileJobData = {
|
||||
isRenewal?: boolean;
|
||||
originalCertificateId?: string;
|
||||
certificateRequestId?: string;
|
||||
csr?: string;
|
||||
};
|
||||
|
||||
type TCertificateIssuanceQueueFactoryDep = {
|
||||
@@ -86,40 +87,6 @@ export const certificateIssuanceQueueFactory = ({
|
||||
certificateProfileDAL,
|
||||
certificateRequestService
|
||||
}: TCertificateIssuanceQueueFactoryDep) => {
|
||||
const validateKeyUsages = (keyUsages: unknown): CertKeyUsage[] => {
|
||||
if (!keyUsages) return [];
|
||||
const validKeyUsages = Object.values(CertKeyUsage);
|
||||
|
||||
if (Array.isArray(keyUsages)) {
|
||||
return keyUsages.filter(
|
||||
(usage): usage is CertKeyUsage => typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsage)
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const validateExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => {
|
||||
if (!extendedKeyUsages) return [];
|
||||
const validExtendedKeyUsages = Object.values(CertExtendedKeyUsage);
|
||||
|
||||
if (Array.isArray(extendedKeyUsages)) {
|
||||
return extendedKeyUsages.filter(
|
||||
(usage): usage is CertExtendedKeyUsage =>
|
||||
typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsage)
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const validateKeyAlgorithm = (keyAlgorithm: unknown): CertKeyAlgorithm | undefined => {
|
||||
if (typeof keyAlgorithm !== "string") return undefined;
|
||||
const validKeyAlgorithms = Object.values(CertKeyAlgorithm);
|
||||
return validKeyAlgorithms.includes(keyAlgorithm as CertKeyAlgorithm)
|
||||
? (keyAlgorithm as CertKeyAlgorithm)
|
||||
: undefined;
|
||||
};
|
||||
const acmeFns = AcmeCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
@@ -168,7 +135,8 @@ export const certificateIssuanceQueueFactory = ({
|
||||
extendedKeyUsages,
|
||||
isRenewal,
|
||||
originalCertificateId,
|
||||
certificateRequestId
|
||||
certificateRequestId,
|
||||
csr
|
||||
}: TIssueCertificateFromProfileJobData) => {
|
||||
const jobData: TIssueCertificateFromProfileJobData = {
|
||||
certificateId,
|
||||
@@ -183,7 +151,8 @@ export const certificateIssuanceQueueFactory = ({
|
||||
extendedKeyUsages,
|
||||
isRenewal,
|
||||
originalCertificateId,
|
||||
certificateRequestId
|
||||
certificateRequestId,
|
||||
csr
|
||||
};
|
||||
|
||||
await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, {
|
||||
@@ -210,7 +179,8 @@ export const certificateIssuanceQueueFactory = ({
|
||||
extendedKeyUsages,
|
||||
isRenewal,
|
||||
originalCertificateId,
|
||||
certificateRequestId
|
||||
certificateRequestId,
|
||||
csr
|
||||
} = data;
|
||||
|
||||
try {
|
||||
@@ -225,32 +195,36 @@ export const certificateIssuanceQueueFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
|
||||
if (ca.externalCa?.type === CaType.ACME) {
|
||||
const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm);
|
||||
if (!validatedKeyAlgorithm) {
|
||||
throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` });
|
||||
}
|
||||
const keyAlg = keyAlgorithmToAlgCfg(validatedKeyAlgorithm);
|
||||
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]);
|
||||
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
let certificateCsr: string;
|
||||
let skLeaf: string = "";
|
||||
|
||||
const [, certificateCsr] = await acme.crypto.createCsr(
|
||||
{
|
||||
altNames: altNames || [],
|
||||
commonName: commonName || ""
|
||||
},
|
||||
skLeaf
|
||||
);
|
||||
if (csr) {
|
||||
certificateCsr = csr;
|
||||
} else {
|
||||
const keyAlg = keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]);
|
||||
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
|
||||
skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const [, generatedCsr] = await acme.crypto.createCsr(
|
||||
{
|
||||
altNames: altNames || [],
|
||||
commonName: commonName || ""
|
||||
},
|
||||
skLeaf
|
||||
);
|
||||
certificateCsr = generatedCsr.toString();
|
||||
}
|
||||
|
||||
const acmeResult = await acmeFns.orderCertificateFromProfile({
|
||||
caId,
|
||||
profileId,
|
||||
commonName: commonName || "",
|
||||
altNames: altNames || [],
|
||||
csr: certificateCsr,
|
||||
csr: Buffer.from(certificateCsr),
|
||||
csrPrivateKey: skLeaf,
|
||||
keyUsages: validateKeyUsages(keyUsages),
|
||||
extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages),
|
||||
keyUsages: keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
|
||||
ttl,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
@@ -306,24 +280,20 @@ export const certificateIssuanceQueueFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm);
|
||||
if (!validatedKeyAlgorithm) {
|
||||
throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` });
|
||||
}
|
||||
|
||||
const azureParams = {
|
||||
caId,
|
||||
profileId,
|
||||
commonName: commonName || "",
|
||||
altNames: altNames || [],
|
||||
keyUsages: validateKeyUsages(keyUsages),
|
||||
extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages),
|
||||
keyUsages: keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
|
||||
validity: { ttl },
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm: validatedKeyAlgorithm,
|
||||
keyAlgorithm: keyAlgorithm as CertKeyAlgorithm,
|
||||
isRenewal,
|
||||
originalCertificateId,
|
||||
template
|
||||
template,
|
||||
...(csr && { csr })
|
||||
};
|
||||
|
||||
const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams);
|
||||
@@ -383,8 +353,6 @@ export const certificateIssuanceQueueFactory = ({
|
||||
};
|
||||
|
||||
const initializeCertificateIssuanceQueue = async () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
await queueService.startPg(
|
||||
QueueJobs.CaIssueCertificateFromProfile,
|
||||
async ([job]) => {
|
||||
@@ -392,8 +360,9 @@ export const certificateIssuanceQueueFactory = ({
|
||||
await processCertificateIssuanceJobs(data);
|
||||
},
|
||||
{
|
||||
workerCount: appCfg.NODE_ENV === "production" ? 3 : 1,
|
||||
batchSize: 1
|
||||
workerCount: 2,
|
||||
batchSize: 1,
|
||||
pollingIntervalSeconds: 1
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
TGetCaDTO,
|
||||
TImportCertToCaDTO,
|
||||
TIssueCertFromCaDTO,
|
||||
TIssueCertFromCaResponse,
|
||||
TRenewCaCertDTO,
|
||||
TSignCertFromCaDTO,
|
||||
TSignIntermediateDTO,
|
||||
@@ -1198,7 +1199,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
isFromProfile,
|
||||
internal = false,
|
||||
tx
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
}: TIssueCertFromCaDTO): Promise<TIssueCertFromCaResponse> => {
|
||||
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
let collectionId = pkiCollectionId;
|
||||
@@ -1532,10 +1533,11 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
return cert;
|
||||
};
|
||||
|
||||
let cert;
|
||||
if (tx) {
|
||||
await executeIssueCertOperations(tx);
|
||||
cert = await executeIssueCertOperations(tx);
|
||||
} else {
|
||||
await certificateDAL.transaction(executeIssueCertOperations);
|
||||
cert = await certificateDAL.transaction(executeIssueCertOperations);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1544,6 +1546,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
issuingCaCertificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
commonName,
|
||||
ca: expandInternalCa(ca)
|
||||
};
|
||||
};
|
||||
@@ -1905,8 +1909,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
const cert = await certificateDAL.transaction(async (tx) => {
|
||||
const newCert = await certificateDAL.create(
|
||||
{
|
||||
caId: (ca as TCertificateAuthorities).id,
|
||||
caCertId: caCert.id,
|
||||
@@ -1929,7 +1933,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
certId: newCert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
@@ -1940,13 +1944,13 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
await pkiCollectionItemDAL.create(
|
||||
{
|
||||
pkiCollectionId: collectionId,
|
||||
certId: cert.id
|
||||
certId: newCert.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cert;
|
||||
return newCert;
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1954,6 +1958,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
certificateChain: certificateChainPem,
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
ca: expandInternalCa(ca),
|
||||
commonName: cn
|
||||
};
|
||||
|
||||
@@ -248,3 +248,20 @@ export type TIssueCertWithTemplateDTO = {
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
type TCaReference = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
dn: string;
|
||||
};
|
||||
|
||||
export type TIssueCertFromCaResponse = {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
issuingCaCertificate: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
certificateId: string;
|
||||
ca: TCaReference;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||
};
|
||||
certificateAuthority?: {
|
||||
id: string;
|
||||
projectId?: string;
|
||||
projectId: string;
|
||||
status: string;
|
||||
name: string;
|
||||
isExternal?: boolean;
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TCertificateRequests } from "@app/db/schemas";
|
||||
import { TableName, TCertificateRequests, TCertificates } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
type TCertificateRequestWithCertificateFlat = TCertificateRequests & {
|
||||
certificateId?: string | null;
|
||||
certificateSerialNumber?: string | null;
|
||||
certificateFriendlyName?: string | null;
|
||||
certificateCommonName?: string | null;
|
||||
certificateAltNames?: string | null;
|
||||
certificateStatus?: string | null;
|
||||
certificateNotBefore?: Date | null;
|
||||
certificateNotAfter?: Date | null;
|
||||
certificateKeyUsages?: string[] | null;
|
||||
certificateExtendedKeyUsages?: string[] | null;
|
||||
};
|
||||
|
||||
type TCertificateInfo = {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
friendlyName: string | null;
|
||||
commonName: string;
|
||||
altNames: string | null;
|
||||
status: string;
|
||||
notBefore: Date;
|
||||
notAfter: Date;
|
||||
keyUsages: string[] | null;
|
||||
extendedKeyUsages: string[] | null;
|
||||
};
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
type TCertificateRequestWithCertificate = TCertificateRequests & {
|
||||
certificate: TCertificateInfo | null;
|
||||
certificate: TCertificates | null;
|
||||
};
|
||||
|
||||
export type TCertificateRequestDALFactory = ReturnType<typeof certificateRequestDALFactory>;
|
||||
@@ -40,61 +16,24 @@ export const certificateRequestDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findByIdWithCertificate = async (id: string): Promise<TCertificateRequestWithCertificate | null> => {
|
||||
try {
|
||||
const certificateRequest = (await db(TableName.CertificateRequests)
|
||||
.leftJoin(
|
||||
TableName.Certificate,
|
||||
`${TableName.CertificateRequests}.certificateId`,
|
||||
`${TableName.Certificate}.id`
|
||||
)
|
||||
.where(`${TableName.CertificateRequests}.id`, id)
|
||||
.select(
|
||||
`${TableName.CertificateRequests}.*`,
|
||||
`${TableName.Certificate}.id as certificateId`,
|
||||
`${TableName.Certificate}.serialNumber as certificateSerialNumber`,
|
||||
`${TableName.Certificate}.friendlyName as certificateFriendlyName`,
|
||||
`${TableName.Certificate}.commonName as certificateCommonName`,
|
||||
`${TableName.Certificate}.altNames as certificateAltNames`,
|
||||
`${TableName.Certificate}.status as certificateStatus`,
|
||||
`${TableName.Certificate}.notBefore as certificateNotBefore`,
|
||||
`${TableName.Certificate}.notAfter as certificateNotAfter`,
|
||||
`${TableName.Certificate}.keyUsages as certificateKeyUsages`,
|
||||
`${TableName.Certificate}.extendedKeyUsages as certificateExtendedKeyUsages`
|
||||
)
|
||||
.first()) as TCertificateRequestWithCertificateFlat | undefined;
|
||||
|
||||
const certificateRequest = await certificateRequestOrm.findById(id);
|
||||
if (!certificateRequest) return null;
|
||||
|
||||
// Transform the flat result into nested structure
|
||||
const {
|
||||
certificateId,
|
||||
certificateSerialNumber,
|
||||
certificateFriendlyName,
|
||||
certificateCommonName,
|
||||
certificateAltNames,
|
||||
certificateStatus,
|
||||
certificateNotBefore,
|
||||
certificateNotAfter,
|
||||
certificateKeyUsages,
|
||||
certificateExtendedKeyUsages,
|
||||
...certificateRequestData
|
||||
} = certificateRequest;
|
||||
if (!certificateRequest.certificateId) {
|
||||
return {
|
||||
...certificateRequest,
|
||||
certificate: null
|
||||
};
|
||||
}
|
||||
|
||||
const certificate = await db(TableName.Certificate)
|
||||
.where("id", certificateRequest.certificateId)
|
||||
.select(selectAllTableCols(TableName.Certificate))
|
||||
.first();
|
||||
|
||||
return {
|
||||
...certificateRequestData,
|
||||
certificate: certificateId
|
||||
? {
|
||||
id: certificateId,
|
||||
serialNumber: certificateSerialNumber as string,
|
||||
friendlyName: certificateFriendlyName || null,
|
||||
commonName: certificateCommonName as string,
|
||||
altNames: certificateAltNames || null,
|
||||
status: certificateStatus as string,
|
||||
notBefore: certificateNotBefore as Date,
|
||||
notAfter: certificateNotAfter as Date,
|
||||
keyUsages: certificateKeyUsages || null,
|
||||
extendedKeyUsages: certificateExtendedKeyUsages || null
|
||||
}
|
||||
: null
|
||||
...certificateRequest,
|
||||
certificate: certificate || null
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate request by ID with certificate" });
|
||||
@@ -111,24 +50,33 @@ export const certificateRequestDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = async (id: string, status: string, errorMessage?: string): Promise<TCertificateRequests> => {
|
||||
const updateStatus = async (
|
||||
id: string,
|
||||
status: string,
|
||||
errorMessage?: string,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateRequests> => {
|
||||
try {
|
||||
const updateData: Partial<TCertificateRequests> = { status };
|
||||
if (errorMessage !== undefined) {
|
||||
updateData.errorMessage = errorMessage;
|
||||
}
|
||||
return await certificateRequestOrm.updateById(id, updateData);
|
||||
return await certificateRequestOrm.updateById(id, updateData, tx);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update certificate request status" });
|
||||
}
|
||||
};
|
||||
|
||||
const attachCertificate = async (id: string, certificateId: string): Promise<TCertificateRequests> => {
|
||||
const attachCertificate = async (id: string, certificateId: string, tx?: Knex): Promise<TCertificateRequests> => {
|
||||
try {
|
||||
return await certificateRequestOrm.updateById(id, {
|
||||
certificateId,
|
||||
status: "issued"
|
||||
});
|
||||
return await certificateRequestOrm.updateById(
|
||||
id,
|
||||
{
|
||||
certificateId,
|
||||
status: "issued"
|
||||
},
|
||||
tx
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Attach certificate to request" });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { ActorType, AuthMethod } from "@app/services/auth/auth-type";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
@@ -202,7 +202,7 @@ describe("CertificateRequestService", () => {
|
||||
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||
|
||||
await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(BadRequestError);
|
||||
await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -257,14 +257,14 @@ describe("CertificateRequestService", () => {
|
||||
"550e8400-e29b-41d4-a716-446655440005"
|
||||
);
|
||||
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
|
||||
serialNumber: "123456",
|
||||
id: "550e8400-e29b-41d4-a716-446655440006",
|
||||
actor: ActorType.USER,
|
||||
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
actorAuthMethod: AuthMethod.EMAIL,
|
||||
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||
});
|
||||
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
|
||||
serialNumber: "123456",
|
||||
id: "550e8400-e29b-41d4-a716-446655440006",
|
||||
actor: ActorType.USER,
|
||||
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
actorAuthMethod: AuthMethod.EMAIL,
|
||||
@@ -354,14 +354,14 @@ describe("CertificateRequestService", () => {
|
||||
"550e8400-e29b-41d4-a716-446655440005"
|
||||
);
|
||||
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
|
||||
serialNumber: "123456",
|
||||
id: "550e8400-e29b-41d4-a716-446655440008",
|
||||
actor: ActorType.USER,
|
||||
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
actorAuthMethod: AuthMethod.EMAIL,
|
||||
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||
});
|
||||
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
|
||||
serialNumber: "123456",
|
||||
id: "550e8400-e29b-41d4-a716-446655440008",
|
||||
actor: ActorType.USER,
|
||||
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
actorAuthMethod: AuthMethod.EMAIL,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
@@ -30,13 +31,12 @@ type TCertificateRequestServiceFactoryDep = {
|
||||
|
||||
export type TCertificateRequestServiceFactory = ReturnType<typeof certificateRequestServiceFactory>;
|
||||
|
||||
// Input validation schemas
|
||||
const certificateRequestDataSchema = z
|
||||
.object({
|
||||
profileId: z.string().uuid().optional(),
|
||||
caId: z.string().uuid().optional(),
|
||||
csr: z.string().min(1).optional(),
|
||||
commonName: z.string().min(1).max(255).optional(),
|
||||
commonName: z.string().max(255).optional(),
|
||||
altNames: z.string().max(1000).optional(),
|
||||
keyUsages: z.array(z.string()).max(20).optional(),
|
||||
extendedKeyUsages: z.array(z.string()).max(20).optional(),
|
||||
@@ -93,8 +93,9 @@ export const certificateRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
tx,
|
||||
...requestData
|
||||
}: TCreateCertificateRequestDTO) => {
|
||||
}: TCreateCertificateRequestDTO & { tx?: Knex }) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -112,11 +113,20 @@ export const certificateRequestServiceFactory = ({
|
||||
// Validate input data before creating the request
|
||||
const validatedData = validateCertificateRequestData(requestData);
|
||||
|
||||
const certificateRequest = await certificateRequestDAL.create({
|
||||
status: CertificateRequestStatus.PENDING,
|
||||
projectId,
|
||||
...validatedData
|
||||
});
|
||||
const certificateRequest = tx
|
||||
? await certificateRequestDAL.create(
|
||||
{
|
||||
status: CertificateRequestStatus.PENDING,
|
||||
projectId,
|
||||
...validatedData
|
||||
},
|
||||
tx
|
||||
)
|
||||
: await certificateRequestDAL.create({
|
||||
status: CertificateRequestStatus.PENDING,
|
||||
projectId,
|
||||
...validatedData
|
||||
});
|
||||
|
||||
return certificateRequest;
|
||||
};
|
||||
@@ -149,7 +159,7 @@ export const certificateRequestServiceFactory = ({
|
||||
}
|
||||
|
||||
if (certificateRequest.projectId !== projectId) {
|
||||
throw new BadRequestError({ message: "Certificate request does not belong to this project" });
|
||||
throw new NotFoundError({ message: "Certificate request not found" });
|
||||
}
|
||||
|
||||
return certificateRequest;
|
||||
@@ -183,7 +193,7 @@ export const certificateRequestServiceFactory = ({
|
||||
}
|
||||
|
||||
if (certificateRequest.projectId !== projectId) {
|
||||
throw new BadRequestError({ message: "Certificate request does not belong to this project" });
|
||||
throw new NotFoundError({ message: "Certificate request not found" });
|
||||
}
|
||||
|
||||
// If no certificate is attached, return basic info
|
||||
@@ -201,7 +211,7 @@ export const certificateRequestServiceFactory = ({
|
||||
|
||||
// Get certificate body (PEM data)
|
||||
const certBody = await certificateService.getCertBody({
|
||||
serialNumber: certificateRequest.certificate.serialNumber,
|
||||
id: certificateRequest.certificate.id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -212,7 +222,7 @@ export const certificateRequestServiceFactory = ({
|
||||
let privateKey: string | null = null;
|
||||
try {
|
||||
const certPrivateKey = await certificateService.getCertPrivateKey({
|
||||
serialNumber: certificateRequest.certificate.serialNumber,
|
||||
id: certificateRequest.certificate.id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-ac
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types";
|
||||
import { ACMESANType, CertStatus } from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
@@ -198,6 +198,9 @@ describe("CertificateV3Service", () => {
|
||||
} as any,
|
||||
certificateIssuanceQueue: {
|
||||
queueCertificateIssuance: vi.fn().mockResolvedValue(undefined)
|
||||
},
|
||||
certificateRequestService: {
|
||||
createCertificateRequest: vi.fn().mockResolvedValue({ id: "cert-req-123" })
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -287,6 +290,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "issuing-ca",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
certificateId: "cert-1",
|
||||
commonName: "test.example.com",
|
||||
ca: {
|
||||
id: "ca-123",
|
||||
projectId: "project-123",
|
||||
@@ -346,8 +351,13 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
|
||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
const result = await service.issueCertificateFromProfile({
|
||||
profileId,
|
||||
certificateRequest: mockCertificateRequest,
|
||||
@@ -481,6 +491,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "issuing-ca",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
certificateId: "cert-1",
|
||||
commonName: "test.example.com",
|
||||
ca: {
|
||||
id: "ca-123",
|
||||
projectId: "project-123",
|
||||
@@ -523,8 +535,34 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any);
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||
id: "cert-1",
|
||||
serialNumber: "123456",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
commonName: "test.example.com",
|
||||
friendlyName: "Test Algorithm Cert",
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(),
|
||||
caId: "ca-1",
|
||||
certificateTemplateId: "template-1",
|
||||
revokedAt: null,
|
||||
altNames: null,
|
||||
caCertId: null,
|
||||
keyUsages: null,
|
||||
extendedKeyUsages: null,
|
||||
revocationReason: null,
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
await service.issueCertificateFromProfile({
|
||||
profileId,
|
||||
certificateRequest: camelCaseRequest,
|
||||
@@ -749,8 +787,13 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
|
||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
const result = await service.signCertificateFromProfile({
|
||||
profileId,
|
||||
csr: mockCSR,
|
||||
@@ -819,168 +862,6 @@ describe("CertificateV3Service", () => {
|
||||
keyAlgorithm: "RSA_2048"
|
||||
};
|
||||
|
||||
it("should create order successfully for API enrollment profile", async () => {
|
||||
const profileId = "profile-123";
|
||||
const mockProfile = {
|
||||
id: profileId,
|
||||
projectId: "project-123",
|
||||
enrollmentType: EnrollmentType.API,
|
||||
issuerType: IssuerType.CA,
|
||||
caId: "ca-123",
|
||||
certificateTemplateId: "template-123",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
slug: "test-profile-order",
|
||||
description: "Test order profile",
|
||||
estConfigId: null,
|
||||
apiConfigId: null
|
||||
};
|
||||
|
||||
const mockCA = {
|
||||
id: "ca-123",
|
||||
projectId: "project-123",
|
||||
externalCa: undefined,
|
||||
internalCa: {
|
||||
id: "internal-ca-123",
|
||||
parentCaId: null,
|
||||
type: "ROOT",
|
||||
friendlyName: "Test CA",
|
||||
organization: "Test Org",
|
||||
ou: "Test OU",
|
||||
country: "US",
|
||||
province: "CA",
|
||||
locality: "SF",
|
||||
commonName: "Test CA",
|
||||
dn: "CN=Test CA",
|
||||
serialNumber: "123",
|
||||
maxPathLength: null,
|
||||
keyAlgorithm: "RSA_2048",
|
||||
notBefore: undefined,
|
||||
notAfter: undefined,
|
||||
activeCaCertId: "cert-123",
|
||||
caId: "ca-123"
|
||||
},
|
||||
name: "Test CA",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
enableDirectIssuance: true
|
||||
};
|
||||
|
||||
const mockTemplate = {
|
||||
id: "template-123",
|
||||
name: "Test Order Template",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-123",
|
||||
description: "Test template for ordering certificates",
|
||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
],
|
||||
subject: undefined,
|
||||
sans: undefined,
|
||||
keyUsages: undefined,
|
||||
extendedKeyUsages: undefined,
|
||||
algorithms: undefined,
|
||||
validity: undefined
|
||||
};
|
||||
|
||||
const mockCertificateResult = {
|
||||
certificate: "cert",
|
||||
certificateChain: "chain",
|
||||
issuingCaCertificate: "issuing-ca",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
ca: {
|
||||
id: "ca-123",
|
||||
projectId: "project-123",
|
||||
name: "Test CA",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
enableDirectIssuance: true,
|
||||
externalCa: undefined,
|
||||
internalCa: {
|
||||
id: "internal-ca-123",
|
||||
parentCaId: null,
|
||||
type: "ROOT",
|
||||
friendlyName: "Test CA",
|
||||
organization: "Test Org",
|
||||
ou: "Test OU",
|
||||
country: "US",
|
||||
province: "CA",
|
||||
locality: "SF",
|
||||
commonName: "Test CA",
|
||||
dn: "CN=Test CA",
|
||||
serialNumber: "123",
|
||||
maxPathLength: null,
|
||||
keyAlgorithm: "RSA_2048",
|
||||
notBefore: null,
|
||||
notAfter: null,
|
||||
activeCaCertId: "cert-123",
|
||||
caId: "ca-123"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockCertRecord = {
|
||||
id: "cert-123",
|
||||
serialNumber: "123456",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-123",
|
||||
commonName: "example.com",
|
||||
friendlyName: "Test Order Cert",
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(),
|
||||
caId: "ca-123",
|
||||
certificateTemplateId: "template-123",
|
||||
revokedAt: null,
|
||||
altNames: JSON.stringify([{ type: "DNS", value: "example.com" }]),
|
||||
caCertId: null,
|
||||
keyUsages: ["DIGITAL_SIGNATURE"],
|
||||
extendedKeyUsages: ["SERVER_AUTH"],
|
||||
revocationReason: null,
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
};
|
||||
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||
|
||||
const result = await service.orderCertificateFromProfile({
|
||||
profileId,
|
||||
certificateOrder: mockCertificateOrder,
|
||||
...mockActor
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("orderId");
|
||||
expect(result).toHaveProperty("status", "valid");
|
||||
expect(result).toHaveProperty("certificate");
|
||||
expect(result.subjectAlternativeNames).toHaveLength(1);
|
||||
expect(result.subjectAlternativeNames[0]).toEqual({
|
||||
type: ACMESANType.DNS,
|
||||
value: "example.com",
|
||||
status: CertificateOrderStatus.VALID
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => {
|
||||
const profileId = "profile-123";
|
||||
const mockProfile = {
|
||||
@@ -1117,6 +998,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "ca-cert",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
certificateId: "cert-1",
|
||||
commonName: "test.example.com",
|
||||
ca: rsaCa as any
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||
@@ -1163,6 +1046,31 @@ describe("CertificateV3Service", () => {
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||
id: "cert-1",
|
||||
serialNumber: "123456",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
commonName: "test.example.com",
|
||||
friendlyName: "Test Algorithm Cert",
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(),
|
||||
caId: "ca-1",
|
||||
certificateTemplateId: "template-1",
|
||||
revokedAt: null,
|
||||
altNames: null,
|
||||
caCertId: null,
|
||||
keyUsages: null,
|
||||
extendedKeyUsages: null,
|
||||
revocationReason: null,
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
// Should not throw - RSA CA is compatible with RSA signature algorithms
|
||||
await expect(
|
||||
@@ -1249,6 +1157,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "ca-cert",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
certificateId: "cert-1",
|
||||
commonName: "test.example.com",
|
||||
ca: ecCa as any
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||
@@ -1295,6 +1205,31 @@ describe("CertificateV3Service", () => {
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||
id: "cert-1",
|
||||
serialNumber: "123456",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
commonName: "test.example.com",
|
||||
friendlyName: "Test Algorithm Cert",
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(),
|
||||
caId: "ca-1",
|
||||
certificateTemplateId: "template-1",
|
||||
revokedAt: null,
|
||||
altNames: null,
|
||||
caCertId: null,
|
||||
keyUsages: null,
|
||||
extendedKeyUsages: null,
|
||||
revocationReason: null,
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
// Should not throw - EC CA is compatible with ECDSA signature algorithms
|
||||
await expect(
|
||||
@@ -1381,6 +1316,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "ca-cert",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
certificateId: "cert-1",
|
||||
commonName: "test.example.com",
|
||||
ca: rsa8192Ca as any
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||
@@ -1427,6 +1364,31 @@ describe("CertificateV3Service", () => {
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||
id: "cert-1",
|
||||
serialNumber: "123456",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
commonName: "test.example.com",
|
||||
friendlyName: "Test Algorithm Cert",
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(),
|
||||
caId: "ca-1",
|
||||
certificateTemplateId: "template-1",
|
||||
revokedAt: null,
|
||||
altNames: null,
|
||||
caCertId: null,
|
||||
keyUsages: null,
|
||||
extendedKeyUsages: null,
|
||||
revocationReason: null,
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
// Should not throw - dynamic check supports new RSA key sizes
|
||||
await expect(
|
||||
@@ -1513,6 +1475,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "ca-cert",
|
||||
privateKey: "key",
|
||||
serialNumber: "123456",
|
||||
certificateId: "cert-1",
|
||||
commonName: "test.example.com",
|
||||
ca: newEcCa as any
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||
@@ -1559,6 +1523,31 @@ describe("CertificateV3Service", () => {
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||
id: "cert-1",
|
||||
serialNumber: "123456",
|
||||
status: "ACTIVE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
commonName: "test.example.com",
|
||||
friendlyName: "Test Algorithm Cert",
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(),
|
||||
caId: "ca-1",
|
||||
certificateTemplateId: "template-1",
|
||||
revokedAt: null,
|
||||
altNames: null,
|
||||
caCertId: null,
|
||||
keyUsages: null,
|
||||
extendedKeyUsages: null,
|
||||
revocationReason: null,
|
||||
pkiSubscriberId: null,
|
||||
profileId: null
|
||||
});
|
||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||
return callback(undefined as any);
|
||||
});
|
||||
|
||||
// Should not throw - dynamic check supports new EC curves
|
||||
await expect(
|
||||
@@ -1692,8 +1681,9 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
|
||||
it("should successfully renew eligible certificate", async () => {
|
||||
// Mock the initial findById call
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||
vi.mocked(mockCertificateDAL.findById)
|
||||
.mockResolvedValueOnce(mockOriginalCert)
|
||||
.mockResolvedValueOnce({ ...mockOriginalCert, id: "cert-456", serialNumber: "789012" });
|
||||
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||
@@ -1709,6 +1699,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "issuing-ca",
|
||||
privateKey: "private-key",
|
||||
serialNumber: "789012",
|
||||
certificateId: "cert-456",
|
||||
commonName: "test.example.com",
|
||||
ca: mockCA
|
||||
});
|
||||
|
||||
@@ -2026,6 +2018,8 @@ describe("CertificateV3Service", () => {
|
||||
issuingCaCertificate: "issuing-ca",
|
||||
privateKey: "private-key",
|
||||
serialNumber: "789012",
|
||||
certificateId: "cert-456",
|
||||
commonName: "test.example.com",
|
||||
ca: mockCA
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { TCertificateDALFactory } from "@app/services/certificate/certificate-da
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertificateOrderStatus,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyType,
|
||||
CertKeyUsage,
|
||||
@@ -40,11 +39,7 @@ import {
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
import {
|
||||
EnrollmentType,
|
||||
IssuerType,
|
||||
TCertificateProfileWithConfigs
|
||||
} from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -71,7 +66,9 @@ import {
|
||||
normalizeDateForApi,
|
||||
removeRootCaFromChain
|
||||
} from "../certificate-common/certificate-utils";
|
||||
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
|
||||
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||
import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types";
|
||||
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
|
||||
@@ -122,6 +119,7 @@ type TCertificateV3ServiceFactoryDep = {
|
||||
import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory,
|
||||
"queueCertificateIssuance"
|
||||
>;
|
||||
certificateRequestService: Pick<TCertificateRequestServiceFactory, "createCertificateRequest">;
|
||||
};
|
||||
|
||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||
@@ -861,7 +859,8 @@ export const certificateV3ServiceFactory = ({
|
||||
pkiSyncQueue,
|
||||
kmsService,
|
||||
projectDAL,
|
||||
certificateIssuanceQueue
|
||||
certificateIssuanceQueue,
|
||||
certificateRequestService
|
||||
}: TCertificateV3ServiceFactoryDep) => {
|
||||
const issueCertificateFromProfile = async ({
|
||||
profileId,
|
||||
@@ -945,7 +944,7 @@ export const certificateV3ServiceFactory = ({
|
||||
const result = await certificateDAL.transaction(async (tx) => {
|
||||
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
||||
|
||||
return processSelfSignedCertificate({
|
||||
const selfSignedResult = await processSelfSignedCertificate({
|
||||
certificateRequest,
|
||||
template,
|
||||
profile,
|
||||
@@ -957,9 +956,29 @@ export const certificateV3ServiceFactory = ({
|
||||
projectDAL,
|
||||
tx
|
||||
});
|
||||
|
||||
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId: profile.projectId,
|
||||
tx,
|
||||
profileId: profile.id,
|
||||
commonName: certificateRequest.commonName,
|
||||
altNames: certificateRequest.altNames?.map((san) => san.value).join(","),
|
||||
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages),
|
||||
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages),
|
||||
notBefore: certificateRequest.notBefore,
|
||||
notAfter: certificateRequest.notAfter,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm
|
||||
});
|
||||
|
||||
return { ...selfSignedResult, certificateRequestId: certRequestResult.id };
|
||||
});
|
||||
|
||||
const { selfSignedResult, certificateData } = result;
|
||||
const { selfSignedResult, certificateData, certificateRequestId } = result;
|
||||
|
||||
const subjectCommonName =
|
||||
(selfSignedResult.certificateSubject.common_name as string) ||
|
||||
@@ -985,6 +1004,7 @@ export const certificateV3ServiceFactory = ({
|
||||
privateKey: selfSignedResult.privateKey.toString("utf8"),
|
||||
serialNumber: selfSignedResult.serialNumber,
|
||||
certificateId: certificateData.id,
|
||||
certificateRequestId,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug,
|
||||
commonName: subjectCommonName
|
||||
@@ -1003,8 +1023,16 @@ export const certificateV3ServiceFactory = ({
|
||||
validateCaSupport(ca, "direct certificate issuance");
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||
await internalCaService.issueCertFromCa({
|
||||
const {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber,
|
||||
cert,
|
||||
certificateRequestId
|
||||
} = await certificateDAL.transaction(async (tx) => {
|
||||
const certResult = await internalCaService.issueCertFromCa({
|
||||
caId: ca.id,
|
||||
friendlyName: certificateSubject.common_name || "Certificate",
|
||||
commonName: certificateSubject.common_name || "",
|
||||
@@ -1023,22 +1051,43 @@ export const certificateV3ServiceFactory = ({
|
||||
isFromProfile: true
|
||||
});
|
||||
|
||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
||||
if (!cert) {
|
||||
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
||||
}
|
||||
const certificateRecord = await certificateDAL.findById(certResult.certificateId);
|
||||
if (!certificateRecord) {
|
||||
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
||||
}
|
||||
|
||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||
profile,
|
||||
certificateRequest.validity.ttl,
|
||||
new Date(cert.notAfter)
|
||||
);
|
||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||
profile,
|
||||
certificateRequest.validity.ttl,
|
||||
new Date(certificateRecord.notAfter)
|
||||
);
|
||||
|
||||
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||
if (finalRenewBeforeDays !== undefined) {
|
||||
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||
}
|
||||
await certificateDAL.updateById(cert.id, updateData);
|
||||
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||
if (finalRenewBeforeDays !== undefined) {
|
||||
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||
}
|
||||
await certificateDAL.updateById(certificateRecord.id, updateData);
|
||||
|
||||
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId: profile.projectId,
|
||||
tx,
|
||||
profileId: profile.id,
|
||||
commonName: certificateRequest.commonName,
|
||||
altNames: certificateRequest.altNames?.map((san) => san.value).join(","),
|
||||
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages),
|
||||
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages),
|
||||
notBefore: certificateRequest.notBefore,
|
||||
notAfter: certificateRequest.notAfter,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm
|
||||
});
|
||||
|
||||
return { ...certResult, cert: certificateRecord, certificateRequestId: certRequestResult.id };
|
||||
});
|
||||
|
||||
let finalCertificateChain = bufferToString(certificateChain);
|
||||
if (removeRootsFromChain) {
|
||||
@@ -1052,6 +1101,7 @@ export const certificateV3ServiceFactory = ({
|
||||
privateKey: bufferToString(privateKey),
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
certificateRequestId,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug,
|
||||
commonName: cert.commonName || ""
|
||||
@@ -1135,33 +1185,60 @@ export const certificateV3ServiceFactory = ({
|
||||
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
|
||||
const effectiveKeyAlgorithm = extractedKeyAlgorithm;
|
||||
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||
await internalCaService.signCertFromCa({
|
||||
isInternal: true,
|
||||
caId: ca.id,
|
||||
csr,
|
||||
ttl: validity.ttl,
|
||||
altNames: undefined,
|
||||
notBefore: normalizeDateForApi(notBefore),
|
||||
notAfter: normalizeDateForApi(notAfter),
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
isFromProfile: true
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, cert, certificateRequestId } =
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const certResult = await internalCaService.signCertFromCa({
|
||||
isInternal: true,
|
||||
caId: ca.id,
|
||||
csr,
|
||||
ttl: validity.ttl,
|
||||
altNames: undefined,
|
||||
notBefore: normalizeDateForApi(notBefore),
|
||||
notAfter: normalizeDateForApi(notAfter),
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
isFromProfile: true
|
||||
});
|
||||
|
||||
const signedCertRecord = await certificateDAL.findById(certResult.certificateId);
|
||||
if (!signedCertRecord) {
|
||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||
}
|
||||
|
||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||
profile,
|
||||
validity.ttl,
|
||||
new Date(signedCertRecord.notAfter)
|
||||
);
|
||||
|
||||
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||
if (finalRenewBeforeDays !== undefined) {
|
||||
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||
}
|
||||
await certificateDAL.updateById(signedCertRecord.id, updateData);
|
||||
|
||||
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId: profile.projectId,
|
||||
tx,
|
||||
profileId: profile.id,
|
||||
csr,
|
||||
commonName: mappedCertificateRequest.commonName,
|
||||
altNames: mappedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","),
|
||||
keyUsages: convertKeyUsageArrayToLegacy(mappedCertificateRequest.keyUsages),
|
||||
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(mappedCertificateRequest.extendedKeyUsages),
|
||||
notBefore,
|
||||
notAfter,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm
|
||||
});
|
||||
|
||||
return { ...certResult, cert: signedCertRecord, certificateRequestId: certRequestResult.id };
|
||||
});
|
||||
|
||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
||||
if (!cert) {
|
||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||
}
|
||||
|
||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
||||
|
||||
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||
if (finalRenewBeforeDays !== undefined) {
|
||||
updateData2.renewBeforeDays = finalRenewBeforeDays;
|
||||
}
|
||||
await certificateDAL.updateById(cert.id, updateData2);
|
||||
|
||||
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
||||
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
||||
if (removeRootsFromChain) {
|
||||
@@ -1174,6 +1251,7 @@ export const certificateV3ServiceFactory = ({
|
||||
certificateChain: certificateChainString,
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
certificateRequestId,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug,
|
||||
commonName: cert.commonName || ""
|
||||
@@ -1186,9 +1264,7 @@ export const certificateV3ServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
removeRootsFromChain,
|
||||
certificateRequestId
|
||||
actorOrgId
|
||||
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
|
||||
const profile = await validateProfileAndPermissions(
|
||||
profileId,
|
||||
@@ -1202,37 +1278,59 @@ export const certificateV3ServiceFactory = ({
|
||||
EnrollmentType.API
|
||||
);
|
||||
|
||||
const certificateRequest = {
|
||||
commonName: certificateOrder.commonName,
|
||||
keyUsages: certificateOrder.keyUsages,
|
||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||
subjectAlternativeNames: certificateOrder.altNames?.map((san) => {
|
||||
let certType: CertSubjectAlternativeNameType;
|
||||
switch (san.type) {
|
||||
case "dns":
|
||||
certType = CertSubjectAlternativeNameType.DNS_NAME;
|
||||
break;
|
||||
case "ip":
|
||||
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: certType,
|
||||
value: san.value
|
||||
};
|
||||
}),
|
||||
validity: certificateOrder.validity,
|
||||
notBefore: certificateOrder.notBefore,
|
||||
notAfter: certificateOrder.notAfter,
|
||||
signatureAlgorithm: certificateOrder.signatureAlgorithm,
|
||||
keyAlgorithm: certificateOrder.keyAlgorithm
|
||||
};
|
||||
let certificateRequest: TCertificateRequest;
|
||||
let extractedKeyAlgorithm: string | undefined;
|
||||
let extractedSignatureAlgorithm: string | undefined;
|
||||
|
||||
if (certificateOrder.csr) {
|
||||
certificateRequest = extractCertificateRequestFromCSR(certificateOrder.csr);
|
||||
const algorithms = extractAlgorithmsFromCSR(certificateOrder.csr);
|
||||
extractedKeyAlgorithm = algorithms.keyAlgorithm;
|
||||
extractedSignatureAlgorithm = algorithms.signatureAlgorithm;
|
||||
certificateRequest.validity = certificateOrder.validity;
|
||||
if (certificateOrder.notBefore && certificateOrder.notAfter) {
|
||||
certificateRequest.notBefore = certificateOrder.notBefore;
|
||||
certificateRequest.notAfter = certificateOrder.notAfter;
|
||||
}
|
||||
} else {
|
||||
certificateRequest = {
|
||||
commonName: certificateOrder.commonName,
|
||||
keyUsages: certificateOrder.keyUsages,
|
||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||
subjectAlternativeNames: certificateOrder.altNames?.map((san) => {
|
||||
let certType: CertSubjectAlternativeNameType;
|
||||
switch (san.type) {
|
||||
case "dns":
|
||||
certType = CertSubjectAlternativeNameType.DNS_NAME;
|
||||
break;
|
||||
case "ip":
|
||||
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: certType,
|
||||
value: san.value
|
||||
};
|
||||
}),
|
||||
validity: certificateOrder.validity,
|
||||
notBefore: certificateOrder.notBefore,
|
||||
notAfter: certificateOrder.notAfter,
|
||||
signatureAlgorithm: certificateOrder.signatureAlgorithm,
|
||||
keyAlgorithm: certificateOrder.keyAlgorithm
|
||||
};
|
||||
}
|
||||
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||
|
||||
if (certificateOrder.csr) {
|
||||
mappedCertificateRequest.keyAlgorithm = extractedKeyAlgorithm;
|
||||
mappedCertificateRequest.signatureAlgorithm = extractedSignatureAlgorithm;
|
||||
}
|
||||
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
mappedCertificateRequest
|
||||
@@ -1258,64 +1356,53 @@ export const certificateV3ServiceFactory = ({
|
||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
|
||||
if (caType === CaType.INTERNAL) {
|
||||
const certificateResult = await issueCertificateFromProfile({
|
||||
profileId,
|
||||
certificateRequest,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
removeRootsFromChain
|
||||
throw new BadRequestError({
|
||||
message: "Certificate ordering is not supported for the specified CA type"
|
||||
});
|
||||
|
||||
const orderId = randomUUID();
|
||||
|
||||
return {
|
||||
orderId,
|
||||
status: CertificateOrderStatus.VALID,
|
||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
||||
type: san.type,
|
||||
value: san.value,
|
||||
status: CertificateOrderStatus.VALID
|
||||
})),
|
||||
authorizations: [],
|
||||
finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`,
|
||||
certificate: certificateResult.certificate,
|
||||
projectId: certificateResult.projectId,
|
||||
profileName: certificateResult.profileName
|
||||
};
|
||||
}
|
||||
|
||||
if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) {
|
||||
const orderId = randomUUID();
|
||||
|
||||
const certRequest = await certificateRequestService.createCertificateRequest({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId: profile.projectId,
|
||||
profileId: profile.id,
|
||||
commonName: certificateOrder.commonName || "",
|
||||
keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [],
|
||||
extendedKeyUsages: certificateOrder.extendedKeyUsages
|
||||
? convertEnumsToStringArray(certificateOrder.extendedKeyUsages)
|
||||
: [],
|
||||
keyAlgorithm: certificateOrder.keyAlgorithm || "",
|
||||
signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
|
||||
altNames: certificateOrder.altNames?.map((san) => san.value).join(",") || "",
|
||||
notBefore: certificateOrder.notBefore,
|
||||
notAfter: certificateOrder.notAfter
|
||||
});
|
||||
|
||||
await certificateIssuanceQueue.queueCertificateIssuance({
|
||||
certificateId: orderId,
|
||||
profileId: profile.id,
|
||||
caId: profile.caId || "",
|
||||
ttl: certificateOrder.validity?.ttl || "1y",
|
||||
signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
|
||||
keyAlgorithm: certificateOrder.keyAlgorithm || "",
|
||||
commonName: certificateOrder.commonName || "",
|
||||
altNames: certificateOrder.altNames?.map((san) => san.value) || [],
|
||||
keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [],
|
||||
extendedKeyUsages: certificateOrder.extendedKeyUsages
|
||||
? convertEnumsToStringArray(certificateOrder.extendedKeyUsages)
|
||||
keyAlgorithm: certificateRequest.keyAlgorithm || "",
|
||||
commonName: certificateRequest.commonName || "",
|
||||
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value) || [],
|
||||
keyUsages: certificateRequest.keyUsages ? convertEnumsToStringArray(certificateRequest.keyUsages) : [],
|
||||
extendedKeyUsages: certificateRequest.extendedKeyUsages
|
||||
? convertEnumsToStringArray(certificateRequest.extendedKeyUsages)
|
||||
: [],
|
||||
certificateRequestId
|
||||
certificateRequestId: certRequest.id,
|
||||
csr: certificateOrder.csr
|
||||
});
|
||||
|
||||
return {
|
||||
orderId,
|
||||
status: CertificateOrderStatus.PENDING,
|
||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
||||
type: san.type,
|
||||
value: san.value,
|
||||
status: CertificateOrderStatus.PENDING
|
||||
})),
|
||||
authorizations: [],
|
||||
finalize: `/api/v3/pki/certificates/orders/${orderId}/finalize`,
|
||||
projectId: profile.projectId,
|
||||
certificateRequestId: certRequest.id,
|
||||
projectId: certRequest.projectId,
|
||||
profileName: profile.slug
|
||||
};
|
||||
}
|
||||
@@ -1325,30 +1412,6 @@ export const certificateV3ServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
// Type for internal CA renewal result
|
||||
type TInternalRenewalData = {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
issuingCaCertificate: string;
|
||||
serialNumber: string;
|
||||
newCert: TCertificates;
|
||||
originalCert: TCertificates;
|
||||
profile: TCertificateProfileWithConfigs | null;
|
||||
};
|
||||
|
||||
// Type for external CA renewal result
|
||||
type TExternalRenewalData = {
|
||||
isExternalCA: true;
|
||||
ca: TCertificateAuthorityWithAssociatedCa;
|
||||
profile: TCertificateProfileWithConfigs | null;
|
||||
originalCert: TCertificates;
|
||||
originalSignatureAlgorithm: CertSignatureAlgorithm;
|
||||
originalKeyAlgorithm: CertKeyAlgorithm;
|
||||
ttl: string;
|
||||
};
|
||||
|
||||
type TRenewalTransactionResult = TInternalRenewalData | TExternalRenewalData;
|
||||
|
||||
const renewCertificate = async ({
|
||||
certificateId,
|
||||
actor,
|
||||
@@ -1356,10 +1419,11 @@ export const certificateV3ServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
internal = false,
|
||||
removeRootsFromChain,
|
||||
certificateRequestId
|
||||
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
|
||||
const renewalResult: TRenewalTransactionResult = await certificateDAL.transaction(async (tx) => {
|
||||
removeRootsFromChain
|
||||
}: Omit<TRenewCertificateDTO, "certificateRequestId"> & {
|
||||
internal?: boolean;
|
||||
}): Promise<TCertificateFromProfileResponse> => {
|
||||
const renewalResult = await certificateDAL.transaction(async (tx) => {
|
||||
const originalCert = await certificateDAL.findById(certificateId, tx);
|
||||
if (!originalCert) {
|
||||
throw new NotFoundError({ message: "Certificate not found" });
|
||||
@@ -1576,7 +1640,7 @@ export const certificateV3ServiceFactory = ({
|
||||
issuingCaCertificate = caResult.issuingCaCertificate;
|
||||
serialNumber = caResult.serialNumber;
|
||||
|
||||
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
||||
const foundCert = await certificateDAL.findById(caResult.certificateId, tx);
|
||||
if (!foundCert) {
|
||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||
}
|
||||
@@ -1665,6 +1729,27 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
|
||||
|
||||
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId: originalCert.projectId,
|
||||
tx,
|
||||
profileId: originalCert.profileId || undefined,
|
||||
commonName: originalCert.commonName || undefined,
|
||||
altNames: originalCert.altNames || undefined,
|
||||
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
|
||||
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
|
||||
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
|
||||
),
|
||||
notBefore: new Date(newCert.notBefore),
|
||||
notAfter: new Date(newCert.notAfter),
|
||||
keyAlgorithm: originalKeyAlgorithm,
|
||||
signatureAlgorithm: originalSignatureAlgorithm,
|
||||
metadata: `Renewed from certificate ID: ${originalCert.id}`
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
@@ -1672,10 +1757,13 @@ export const certificateV3ServiceFactory = ({
|
||||
serialNumber,
|
||||
newCert,
|
||||
originalCert,
|
||||
profile
|
||||
profile,
|
||||
certRequestResult
|
||||
};
|
||||
});
|
||||
|
||||
let certificateRequestId: string = renewalResult.certRequestResult?.id || "";
|
||||
|
||||
// Handle external CA renewals separately
|
||||
if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) {
|
||||
const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult;
|
||||
@@ -1685,6 +1773,27 @@ export const certificateV3ServiceFactory = ({
|
||||
? originalCert.altNames.split(",").map((san: string) => san.trim())
|
||||
: [];
|
||||
|
||||
const certificateRequest = await certificateRequestService.createCertificateRequest({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId: originalCert.projectId,
|
||||
profileId: profile?.id,
|
||||
caId: ca.id,
|
||||
commonName: originalCert.commonName || undefined,
|
||||
altNames: originalCert.altNames || undefined,
|
||||
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
|
||||
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
|
||||
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
|
||||
),
|
||||
keyAlgorithm: originalKeyAlgorithm,
|
||||
signatureAlgorithm: originalSignatureAlgorithm,
|
||||
metadata: `Renewed from certificate ID: ${originalCert.id}`
|
||||
});
|
||||
|
||||
certificateRequestId = certificateRequest.id;
|
||||
|
||||
await certificateIssuanceQueue.queueCertificateIssuance({
|
||||
certificateId: renewalOrderId,
|
||||
profileId: profile?.id || "",
|
||||
@@ -1698,7 +1807,7 @@ export const certificateV3ServiceFactory = ({
|
||||
extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)),
|
||||
isRenewal: true,
|
||||
originalCertificateId: certificateId,
|
||||
certificateRequestId
|
||||
certificateRequestId: certificateRequest.id
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1707,6 +1816,7 @@ export const certificateV3ServiceFactory = ({
|
||||
issuingCaCertificate: "",
|
||||
serialNumber: "",
|
||||
certificateId: renewalOrderId,
|
||||
certificateRequestId: certificateRequest.id,
|
||||
projectId: originalCert.projectId,
|
||||
profileName: profile?.slug || "External CA Profile",
|
||||
commonName: originalCert.commonName || ""
|
||||
@@ -1734,6 +1844,7 @@ export const certificateV3ServiceFactory = ({
|
||||
certificateChain: finalCertificateChain,
|
||||
serialNumber: renewalResult.serialNumber,
|
||||
certificateId: renewalResult.newCert.id,
|
||||
certificateRequestId,
|
||||
projectId: renewalResult.originalCert.projectId,
|
||||
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
|
||||
commonName: renewalResult.originalCert.commonName || ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
|
||||
import { ACMESANType } from "../certificate/certificate-types";
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
@@ -59,9 +59,9 @@ export type TOrderCertificateFromProfileDTO = {
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
template?: string;
|
||||
csr?: string;
|
||||
};
|
||||
removeRootsFromChain?: boolean;
|
||||
certificateRequestId?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCertificateFromProfileResponse = {
|
||||
@@ -71,34 +71,14 @@ export type TCertificateFromProfileResponse = {
|
||||
privateKey?: string;
|
||||
serialNumber: string;
|
||||
certificateId: string;
|
||||
certificateRequestId: string;
|
||||
projectId: string;
|
||||
profileName: string;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
export type TCertificateOrderResponse = {
|
||||
orderId: string;
|
||||
status: CertificateOrderStatus;
|
||||
subjectAlternativeNames: Array<{
|
||||
type: ACMESANType;
|
||||
value: string;
|
||||
status: CertificateOrderStatus;
|
||||
}>;
|
||||
authorizations: Array<{
|
||||
identifier: {
|
||||
type: ACMESANType;
|
||||
value: string;
|
||||
};
|
||||
status: CertificateOrderStatus;
|
||||
expires?: string;
|
||||
challenges: Array<{
|
||||
type: string;
|
||||
status: CertificateOrderStatus;
|
||||
url: string;
|
||||
token: string;
|
||||
}>;
|
||||
}>;
|
||||
finalize: string;
|
||||
certificateRequestId: string;
|
||||
certificate?: string;
|
||||
projectId: string;
|
||||
profileName: string;
|
||||
|
||||
@@ -83,32 +83,40 @@ export type TDownloadPkcs12DTO = {
|
||||
|
||||
export type TUnifiedCertificateIssuanceDTO = {
|
||||
projectSlug: string;
|
||||
profileId: string;
|
||||
projectId: string;
|
||||
profileId?: string;
|
||||
caId?: string;
|
||||
csr?: string;
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
keyUsages?: string[];
|
||||
extendedKeyUsages?: string[];
|
||||
notBefore?: Date;
|
||||
notAfter?: Date;
|
||||
keyAlgorithm?: string;
|
||||
signatureAlgorithm?: string;
|
||||
ttl?: string;
|
||||
friendlyName?: string;
|
||||
pkiCollectionId?: string;
|
||||
issuerType?: string;
|
||||
attributes?: {
|
||||
commonName?: string;
|
||||
keyUsages?: string[];
|
||||
extendedKeyUsages?: string[];
|
||||
altNames?: Array<{
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
signatureAlgorithm: string;
|
||||
keyAlgorithm: string;
|
||||
subjectAlternativeNames?: Array<{
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
};
|
||||
removeRootsFromChain?: boolean;
|
||||
};
|
||||
|
||||
export type TUnifiedCertificateResponse = {
|
||||
certificate: string;
|
||||
issuingCaCertificate: string;
|
||||
certificateChain: string;
|
||||
privateKey?: string;
|
||||
serialNumber: string;
|
||||
certificateId: string;
|
||||
projectId: string;
|
||||
certificate: {
|
||||
certificate: string;
|
||||
issuingCaCertificate: string;
|
||||
certificateChain: string;
|
||||
privateKey?: string;
|
||||
serialNumber: string;
|
||||
certificateId: string;
|
||||
};
|
||||
certificateRequestId: string;
|
||||
};
|
||||
|
||||
export type TCertificateRequestResponse = {
|
||||
|
||||
@@ -107,6 +107,7 @@ type TCertificateDetails = {
|
||||
certificate?: string;
|
||||
certificateChain?: string;
|
||||
privateKey?: string;
|
||||
issuingCaCertificate?: string;
|
||||
};
|
||||
|
||||
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
||||
@@ -280,41 +281,42 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
profileId: formProfileId,
|
||||
projectSlug: currentProject.slug,
|
||||
projectId: currentProject.id,
|
||||
ttl,
|
||||
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||
attributes: {
|
||||
ttl,
|
||||
signatureAlgorithm: signatureAlgorithm || "",
|
||||
keyAlgorithm: keyAlgorithm || "",
|
||||
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||
}
|
||||
};
|
||||
|
||||
if (constraints.shouldShowSubjectSection && commonName) {
|
||||
request.commonName = commonName;
|
||||
}
|
||||
|
||||
if (signatureAlgorithm) {
|
||||
request.signatureAlgorithm = signatureAlgorithm;
|
||||
}
|
||||
|
||||
if (keyAlgorithm) {
|
||||
request.keyAlgorithm = keyAlgorithm;
|
||||
request.attributes.commonName = commonName;
|
||||
}
|
||||
|
||||
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
||||
if (formattedSans && formattedSans.length > 0) {
|
||||
request.altNames = formattedSans;
|
||||
request.attributes.altNames = formattedSans;
|
||||
request.attributes.subjectAlternativeNames = formattedSans;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await issueCertificate(request);
|
||||
|
||||
// Handle certificate issuance response
|
||||
if ("certificate" in response) {
|
||||
// Immediate certificate issuance
|
||||
setCertificateDetails({
|
||||
serialNumber: response.serialNumber,
|
||||
certificate: response.certificate,
|
||||
certificateChain: response.certificateChain,
|
||||
privateKey: response.privateKey
|
||||
});
|
||||
|
||||
if ("certificate" in response && response.certificate) {
|
||||
const certData = response.certificate;
|
||||
const certificateDetailsToSet = {
|
||||
serialNumber: certData.serialNumber || "",
|
||||
certificate: certData.certificate || "",
|
||||
certificateChain: certData.certificateChain || "",
|
||||
privateKey: certData.privateKey || "",
|
||||
issuingCaCertificate: certData.issuingCaCertificate || ""
|
||||
};
|
||||
|
||||
setCertificateDetails(certificateDetailsToSet);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
|
||||
Reference in New Issue
Block a user