mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
Fix sign-certificate logic and minor improvements on the certificates table
This commit is contained in:
@@ -2137,6 +2137,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const certificateV3Service = certificateV3ServiceFactory({
|
const certificateV3Service = certificateV3ServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
|
|||||||
@@ -1200,7 +1200,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificates: z.array(CertificatesSchema),
|
certificates: z.array(CertificatesSchema.extend({ hasPrivateKey: z.boolean() })),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
CertSubjectAlternativeNameType
|
CertSubjectAlternativeNameType
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
|
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
@@ -169,9 +170,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
.min(1, "TTL cannot be empty")
|
.min(1, "TTL cannot be empty")
|
||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional()
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -192,6 +191,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
||||||
|
|
||||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -203,9 +204,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
||||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
|
||||||
keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -217,7 +216,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId,
|
certificateId: data.certificateId,
|
||||||
profileName: data.profileName,
|
profileName: data.profileName,
|
||||||
commonName: ""
|
commonName: certificateRequest.commonName || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -1728,7 +1728,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
signatureAlgorithm: alg
|
||||||
});
|
});
|
||||||
|
|
||||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageOIDToName,
|
||||||
|
CertKeyAlgorithm,
|
||||||
|
CertKeyUsage,
|
||||||
|
CertSignatureAlgorithm,
|
||||||
|
mapLegacyAltNameType,
|
||||||
|
TAltNameMapping,
|
||||||
|
TAltNameType
|
||||||
|
} from "../certificate/certificate-types";
|
||||||
|
import { parseDistinguishedName } from "../certificate-authority/certificate-authority-fns";
|
||||||
|
import { validateAndMapAltNameType } from "../certificate-authority/certificate-authority-validators";
|
||||||
|
import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types";
|
||||||
|
import { mapLegacyExtendedKeyUsageToStandard, mapLegacyKeyUsageToStandard } from "./certificate-constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts certificate request data from a CSR string
|
||||||
|
* @param csr - The CSR in PEM format
|
||||||
|
* @returns TCertificateRequest object with parsed CSR data
|
||||||
|
*/
|
||||||
|
export const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
const subject = parseDistinguishedName(csrObj.subject);
|
||||||
|
|
||||||
|
const certificateRequest: TCertificateRequest = {
|
||||||
|
commonName: subject.commonName,
|
||||||
|
organization: subject.organization,
|
||||||
|
organizationUnit: subject.ou,
|
||||||
|
locality: subject.locality,
|
||||||
|
state: subject.province,
|
||||||
|
country: subject.country
|
||||||
|
};
|
||||||
|
|
||||||
|
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||||
|
);
|
||||||
|
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||||
|
if (csrExtendedKeyUsageExtension) {
|
||||||
|
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||||
|
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||||
|
);
|
||||||
|
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||||
|
if (sanExtension) {
|
||||||
|
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||||
|
const altNamesArray: TAltNameMapping[] = sanNames.items
|
||||||
|
.filter(
|
||||||
|
(value) =>
|
||||||
|
value.type === TAltNameType.EMAIL ||
|
||||||
|
value.type === TAltNameType.DNS ||
|
||||||
|
value.type === TAltNameType.IP ||
|
||||||
|
value.type === TAltNameType.URL
|
||||||
|
)
|
||||||
|
.map((name): TAltNameMapping => {
|
||||||
|
const altNameType = validateAndMapAltNameType(name.value);
|
||||||
|
if (!altNameType) {
|
||||||
|
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
||||||
|
}
|
||||||
|
return altNameType;
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
||||||
|
type: mapLegacyAltNameType(altName.type),
|
||||||
|
value: altName.value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the key algorithm and signature algorithm from a CSR
|
||||||
|
* @param csr - The CSR in PEM format
|
||||||
|
* @returns Object containing keyAlgorithm and signatureAlgorithm
|
||||||
|
*/
|
||||||
|
export const extractAlgorithmsFromCSR = (csr: string) => {
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
// Extract key algorithm from public key
|
||||||
|
const { publicKey } = csrObj;
|
||||||
|
let keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
|
||||||
|
if (publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
||||||
|
const rsaPublicKey = publicKey as unknown as { algorithm: { modulusLength: number } };
|
||||||
|
const keySize = rsaPublicKey.algorithm.modulusLength;
|
||||||
|
switch (keySize) {
|
||||||
|
case 2048:
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||||
|
break;
|
||||||
|
case 3072:
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_3072;
|
||||||
|
break;
|
||||||
|
case 4096:
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_4096;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported RSA key size in CSR: ${keySize}. Supported: 2048, 3072, 4096`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (publicKey.algorithm.name === "ECDSA") {
|
||||||
|
const ecPublicKey = publicKey as unknown as { algorithm: { namedCurve: string } };
|
||||||
|
const { namedCurve } = ecPublicKey.algorithm;
|
||||||
|
switch (namedCurve) {
|
||||||
|
case "P-256":
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.ECDSA_P256;
|
||||||
|
break;
|
||||||
|
case "P-384":
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.ECDSA_P384;
|
||||||
|
break;
|
||||||
|
case "P-521":
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.ECDSA_P521;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported ECDSA curve in CSR: ${namedCurve}. Supported: P-256, P-384, P-521`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported key algorithm in CSR: ${publicKey.algorithm.name}. Supported: RSASSA-PKCS1-v1_5, ECDSA`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureAlgorithm = csrObj.signatureAlgorithm.name;
|
||||||
|
const hashName = (csrObj.signatureAlgorithm as unknown as { hash?: { name: string } }).hash?.name;
|
||||||
|
|
||||||
|
let normalizedSignatureAlg: CertSignatureAlgorithm;
|
||||||
|
|
||||||
|
if (signatureAlgorithm === "RSASSA-PKCS1-v1_5") {
|
||||||
|
switch (hashName) {
|
||||||
|
case "SHA-256":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.RSA_SHA256;
|
||||||
|
break;
|
||||||
|
case "SHA-384":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.RSA_SHA384;
|
||||||
|
break;
|
||||||
|
case "SHA-512":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.RSA_SHA512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported RSA hash algorithm in CSR: ${hashName}. Supported: SHA-256, SHA-384, SHA-512`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (signatureAlgorithm === "ECDSA") {
|
||||||
|
switch (hashName) {
|
||||||
|
case "SHA-256":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.ECDSA_SHA256;
|
||||||
|
break;
|
||||||
|
case "SHA-384":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.ECDSA_SHA384;
|
||||||
|
break;
|
||||||
|
case "SHA-512":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.ECDSA_SHA512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported ECDSA hash algorithm in CSR: ${hashName}. Supported: SHA-256, SHA-384, SHA-512`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported signature algorithm in CSR: ${signatureAlgorithm}. Supported: RSASSA-PKCS1-v1_5, ECDSA`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyAlgorithm,
|
||||||
|
signatureAlgorithm: normalizedSignatureAlg
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,31 +3,15 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||||
import {
|
|
||||||
CertExtendedKeyUsageOIDToName,
|
|
||||||
CertKeyUsage,
|
|
||||||
mapLegacyAltNameType,
|
|
||||||
TAltNameMapping,
|
|
||||||
TAltNameType
|
|
||||||
} from "@app/services/certificate/certificate-types";
|
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import {
|
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
getCaCertChain,
|
|
||||||
getCaCertChains,
|
|
||||||
parseDistinguishedName
|
|
||||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
|
||||||
import { validateAndMapAltNameType } from "@app/services/certificate-authority/certificate-authority-validators";
|
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import {
|
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||||
mapLegacyExtendedKeyUsageToStandard,
|
|
||||||
mapLegacyKeyUsageToStandard
|
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
|
|
||||||
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -61,63 +45,6 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
estEnrollmentConfigDAL
|
estEnrollmentConfigDAL
|
||||||
}: TCertificateEstV3ServiceFactoryDep) => {
|
}: TCertificateEstV3ServiceFactoryDep) => {
|
||||||
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
|
||||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
|
||||||
const subject = parseDistinguishedName(csrObj.subject);
|
|
||||||
|
|
||||||
const certificateRequest: TCertificateRequest = {
|
|
||||||
commonName: subject.commonName,
|
|
||||||
organization: subject.organization,
|
|
||||||
organizationUnit: subject.ou,
|
|
||||||
locality: subject.locality,
|
|
||||||
state: subject.province,
|
|
||||||
country: subject.country
|
|
||||||
};
|
|
||||||
|
|
||||||
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
|
||||||
if (csrKeyUsageExtension) {
|
|
||||||
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
|
||||||
);
|
|
||||||
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
|
||||||
}
|
|
||||||
|
|
||||||
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
|
||||||
if (csrExtendedKeyUsageExtension) {
|
|
||||||
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
|
||||||
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
|
||||||
);
|
|
||||||
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
|
||||||
if (sanExtension) {
|
|
||||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
|
||||||
const altNamesArray: TAltNameMapping[] = sanNames.items
|
|
||||||
.filter(
|
|
||||||
(value) =>
|
|
||||||
value.type === TAltNameType.EMAIL ||
|
|
||||||
value.type === TAltNameType.DNS ||
|
|
||||||
value.type === TAltNameType.IP ||
|
|
||||||
value.type === TAltNameType.URL
|
|
||||||
)
|
|
||||||
.map((name): TAltNameMapping => {
|
|
||||||
const altNameType = validateAndMapAltNameType(name.value);
|
|
||||||
if (!altNameType) {
|
|
||||||
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
|
||||||
}
|
|
||||||
return altNameType;
|
|
||||||
});
|
|
||||||
|
|
||||||
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
|
||||||
type: mapLegacyAltNameType(altName.type),
|
|
||||||
value: altName.value
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificateRequest;
|
|
||||||
};
|
|
||||||
const simpleEnrollByProfile = async ({
|
const simpleEnrollByProfile = async ({
|
||||||
csr,
|
csr,
|
||||||
profileId,
|
profileId,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
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, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
@@ -24,8 +25,17 @@ import { EnrollmentType } from "@app/services/certificate-profile/certificate-pr
|
|||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||||
|
import {
|
||||||
|
extractAlgorithmsFromCSR,
|
||||||
|
extractCertificateRequestFromCSR
|
||||||
|
} from "../certificate-common/certificate-csr-utils";
|
||||||
import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
||||||
|
|
||||||
|
vi.mock("../certificate-common/certificate-csr-utils", () => ({
|
||||||
|
extractCertificateRequestFromCSR: vi.fn(),
|
||||||
|
extractAlgorithmsFromCSR: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
describe("CertificateV3Service", () => {
|
describe("CertificateV3Service", () => {
|
||||||
let service: TCertificateV3ServiceFactory;
|
let service: TCertificateV3ServiceFactory;
|
||||||
|
|
||||||
@@ -39,6 +49,10 @@ describe("CertificateV3Service", () => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne"> = {
|
||||||
|
findOne: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
||||||
findByIdWithAssociatedCa: vi.fn()
|
findByIdWithAssociatedCa: vi.fn()
|
||||||
};
|
};
|
||||||
@@ -101,8 +115,20 @@ describe("CertificateV3Service", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mocked(extractCertificateRequestFromCSR).mockReturnValue({
|
||||||
|
commonName: "test.example.com",
|
||||||
|
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||||
|
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH]
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(extractAlgorithmsFromCSR).mockReturnValue({
|
||||||
|
keyAlgorithm: "RSA_2048" as any,
|
||||||
|
signatureAlgorithm: "RSA-SHA256" as any
|
||||||
|
});
|
||||||
|
|
||||||
service = certificateV3ServiceFactory({
|
service = certificateV3ServiceFactory({
|
||||||
certificateDAL: mockCertificateDAL,
|
certificateDAL: mockCertificateDAL,
|
||||||
|
certificateSecretDAL: mockCertificateSecretDAL,
|
||||||
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
||||||
certificateProfileDAL: mockCertificateProfileDAL,
|
certificateProfileDAL: mockCertificateProfileDAL,
|
||||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||||
@@ -647,6 +673,11 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||||
@@ -1586,6 +1617,7 @@ describe("CertificateV3Service", () => {
|
|||||||
it("should successfully renew eligible certificate", async () => {
|
it("should successfully renew eligible certificate", async () => {
|
||||||
// Mock the initial findById call
|
// Mock the initial findById call
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
@@ -1650,6 +1682,7 @@ describe("CertificateV3Service", () => {
|
|||||||
errors: ["Subject alternative name not allowed"],
|
errors: ["Subject alternative name not allowed"],
|
||||||
warnings: []
|
warnings: []
|
||||||
});
|
});
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
// Mock updateById to handle the renewal error logging
|
// Mock updateById to handle the renewal error logging
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
||||||
@@ -1705,11 +1738,37 @@ describe("CertificateV3Service", () => {
|
|||||||
).rejects.toThrow("Only certificates issued from a profile can be renewed");
|
).rejects.toThrow("Only certificates issued from a profile can be renewed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if certificate was issued from CSR (external private key)", async () => {
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue(null as any);
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(ForbiddenRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("certificates issued from CSR (external private key) cannot be renewed");
|
||||||
|
});
|
||||||
|
|
||||||
it("should reject renewal if certificate is already renewed", async () => {
|
it("should reject renewal if certificate is already renewed", async () => {
|
||||||
const alreadyRenewedCert = { ...mockOriginalCert, renewedByCertificateId: "cert-456" };
|
const alreadyRenewedCert = { ...mockOriginalCert, renewedByCertificateId: "cert-456" };
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
// Mock updateById to handle the renewal error logging
|
// Mock updateById to handle the renewal error logging
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(alreadyRenewedCert);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(alreadyRenewedCert);
|
||||||
@@ -1743,6 +1802,7 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(expiredCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(expiredCert);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
// Mock updateById to handle the renewal error logging
|
// Mock updateById to handle the renewal error logging
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(expiredCert);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(expiredCert);
|
||||||
@@ -1776,6 +1836,7 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(revokedCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(revokedCert);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
// Mock updateById to handle the renewal error logging
|
// Mock updateById to handle the renewal error logging
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(revokedCert);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(revokedCert);
|
||||||
@@ -1806,6 +1867,7 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
// Mock updateById to handle the renewal error logging
|
// Mock updateById to handle the renewal error logging
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
||||||
@@ -1842,6 +1904,7 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
// Mock updateById to handle the renewal error logging
|
// Mock updateById to handle the renewal error logging
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
||||||
@@ -1873,6 +1936,7 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
@@ -1929,6 +1993,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCert as any);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCert as any);
|
||||||
|
|
||||||
const result = await service.updateRenewalConfig({
|
const result = await service.updateRenewalConfig({
|
||||||
@@ -2004,6 +2069,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.updateRenewalConfig({
|
service.updateRenewalConfig({
|
||||||
@@ -2048,6 +2114,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.updateRenewalConfig({
|
service.updateRenewalConfig({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
CertificateOrderStatus,
|
CertificateOrderStatus,
|
||||||
@@ -32,6 +33,10 @@ import { EnrollmentType } from "@app/services/certificate-profile/certificate-pr
|
|||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
||||||
|
import {
|
||||||
|
extractAlgorithmsFromCSR,
|
||||||
|
extractCertificateRequestFromCSR
|
||||||
|
} from "../certificate-common/certificate-csr-utils";
|
||||||
import {
|
import {
|
||||||
bufferToString,
|
bufferToString,
|
||||||
buildCertificateSubjectFromTemplate,
|
buildCertificateSubjectFromTemplate,
|
||||||
@@ -58,6 +63,7 @@ import {
|
|||||||
|
|
||||||
type TCertificateV3ServiceFactoryDep = {
|
type TCertificateV3ServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
||||||
certificateTemplateV2Service: Pick<
|
certificateTemplateV2Service: Pick<
|
||||||
@@ -296,8 +302,28 @@ const parseTtlToDays = (ttl: string): number => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateFinalRenewBeforeDays = (
|
||||||
|
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
||||||
|
ttl: string,
|
||||||
|
certificateExpiryDate: Date
|
||||||
|
): number | undefined => {
|
||||||
|
if (!profile.apiConfig?.autoRenew || !profile.apiConfig.renewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTtlInDays = parseTtlToDays(ttl);
|
||||||
|
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig.renewBeforeDays, certificateTtlInDays);
|
||||||
|
|
||||||
|
if (!renewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const certificateV3ServiceFactory = ({
|
export const certificateV3ServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
@@ -412,11 +438,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateTtlInDays = parseTtlToDays(certificateRequest.validity.ttl);
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
|
profile,
|
||||||
|
certificateRequest.validity.ttl,
|
||||||
const finalRenewBeforeDays =
|
new Date(cert.notAfter)
|
||||||
renewBeforeDays && isValidRenewalTiming(renewBeforeDays, new Date(cert.notAfter)) ? renewBeforeDays : undefined;
|
);
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, {
|
await certificateDAL.updateById(cert.id, {
|
||||||
profileId,
|
profileId,
|
||||||
@@ -442,8 +468,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
validity,
|
validity,
|
||||||
notBefore,
|
notBefore,
|
||||||
notAfter,
|
notAfter,
|
||||||
signatureAlgorithm,
|
|
||||||
keyAlgorithm,
|
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -480,22 +504,27 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(csr);
|
||||||
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||||
|
|
||||||
|
const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } =
|
||||||
|
extractAlgorithmsFromCSR(csr);
|
||||||
|
|
||||||
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
|
profile.certificateTemplateId,
|
||||||
|
mappedCertificateRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm = signatureAlgorithm;
|
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
|
||||||
const effectiveKeyAlgorithm = keyAlgorithm;
|
const effectiveKeyAlgorithm = extractedKeyAlgorithm;
|
||||||
|
|
||||||
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Key algorithm is required by template policy but not provided in request"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Signature algorithm is required by template policy but not provided in request"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||||
await internalCaService.signCertFromCa({
|
await internalCaService.signCertFromCa({
|
||||||
@@ -516,11 +545,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateTtlInDays = parseTtlToDays(validity.ttl);
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
||||||
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
|
|
||||||
|
|
||||||
const finalRenewBeforeDays =
|
|
||||||
renewBeforeDays && isValidRenewalTiming(renewBeforeDays, new Date(cert.notAfter)) ? renewBeforeDays : undefined;
|
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, {
|
await certificateDAL.updateById(cert.id, {
|
||||||
profileId,
|
profileId,
|
||||||
@@ -675,6 +700,14 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
|
||||||
|
if (!certificateSecret) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message:
|
||||||
|
"Certificate is not eligible for renewal: certificates issued from CSR (external private key) cannot be renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!internal) {
|
if (!internal) {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -791,8 +824,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
const notBefore = new Date();
|
const notBefore = new Date();
|
||||||
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const certificateTtlInDays = parseTtlToDays(ttl);
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, ttl, notAfter);
|
||||||
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
|
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||||
await internalCaService.issueCertFromCa({
|
await internalCaService.issueCertFromCa({
|
||||||
@@ -907,6 +939,14 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId: certificate.id });
|
||||||
|
if (!certificateSecret) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message:
|
||||||
|
"Certificate is not eligible for auto-renewal: certificates issued from CSR (external private key) cannot be auto-renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (certificate.status !== CertStatus.ACTIVE) {
|
if (certificate.status !== CertStatus.ACTIVE) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Certificate is not eligible for auto-renewal: certificate status is ${certificate.status}, must be active`
|
message: `Certificate is not eligible for auto-renewal: certificate status is ${certificate.status}, must be active`
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export type TSignCertificateFromProfileDTO = {
|
|||||||
};
|
};
|
||||||
notBefore?: Date;
|
notBefore?: Date;
|
||||||
notAfter?: Date;
|
notAfter?: Date;
|
||||||
signatureAlgorithm?: string;
|
|
||||||
keyAlgorithm?: string;
|
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TOrderCertificateFromProfileDTO = {
|
export type TOrderCertificateFromProfileDTO = {
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.Certificate}.profileId`,
|
`${TableName.Certificate}.profileId`,
|
||||||
`${TableName.PkiCertificateProfile}.id`
|
`${TableName.PkiCertificateProfile}.id`
|
||||||
)
|
)
|
||||||
|
.innerJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
||||||
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
|
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
|
||||||
.whereNull(`${TableName.Certificate}.renewedByCertificateId`)
|
.whereNull(`${TableName.Certificate}.renewedByCertificateId`)
|
||||||
.whereNull(`${TableName.Certificate}.renewalError`)
|
.whereNull(`${TableName.Certificate}.renewalError`)
|
||||||
@@ -157,6 +158,42 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findWithPrivateKeyInfo = async (
|
||||||
|
filter: Partial<TCertificates>,
|
||||||
|
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
|
||||||
|
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||||
|
try {
|
||||||
|
let query = db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
||||||
|
.select(selectAllTableCols(TableName.Certificate))
|
||||||
|
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
|
||||||
|
.where(filter);
|
||||||
|
|
||||||
|
if (options?.offset) {
|
||||||
|
query = query.offset(options.offset);
|
||||||
|
}
|
||||||
|
if (options?.limit) {
|
||||||
|
query = query.limit(options.limit);
|
||||||
|
}
|
||||||
|
if (options?.sort) {
|
||||||
|
options.sort.forEach(([column, direction]) => {
|
||||||
|
query = query.orderBy(column, direction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await query;
|
||||||
|
return results.map((row) => {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
hasPrivateKey: row.privateKeyRef !== null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find certificates with private key info" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...certificateOrm,
|
...certificateOrm,
|
||||||
countCertificatesInProject,
|
countCertificatesInProject,
|
||||||
@@ -164,6 +201,7 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
findLatestActiveCertForSubscriber,
|
findLatestActiveCertForSubscriber,
|
||||||
findAllActiveCertsForSubscriber,
|
findAllActiveCertsForSubscriber,
|
||||||
findExpiredSyncedCertificates,
|
findExpiredSyncedCertificates,
|
||||||
findCertificatesEligibleForRenewal
|
findCertificatesEligibleForRenewal,
|
||||||
|
findWithPrivateKeyInfo
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject" | "findWithPrivateKeyInfo">;
|
||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
@@ -938,7 +938,7 @@ export const projectServiceFactory = ({
|
|||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
const certificates = await certificateDAL.find(
|
const certificates = await certificateDAL.findWithPrivateKeyInfo(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
...(friendlyName && { friendlyName }),
|
...(friendlyName && { friendlyName }),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type TCertificate = {
|
|||||||
renewedFromCertificateId?: string;
|
renewedFromCertificateId?: string;
|
||||||
renewedByCertificateId?: string;
|
renewedByCertificateId?: string;
|
||||||
renewalError?: string;
|
renewalError?: string;
|
||||||
|
hasPrivateKey?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteCertDTO = {
|
export type TDeleteCertDTO = {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
|
|||||||
return {
|
return {
|
||||||
text: "Not Available",
|
text: "Not Available",
|
||||||
variant: "instance" as const,
|
variant: "instance" as const,
|
||||||
tooltip: "Auto-renewal is not available for revoked certificates"
|
tooltip: "Renewal is not available for revoked certificates"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
|
|||||||
return {
|
return {
|
||||||
text: "Not Available",
|
text: "Not Available",
|
||||||
variant: "instance" as const,
|
variant: "instance" as const,
|
||||||
tooltip: "Auto-renewal is not available for expired certificates"
|
tooltip: "Renewal is not available for expired certificates"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,15 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
|
|||||||
return {
|
return {
|
||||||
text: "Not Available",
|
text: "Not Available",
|
||||||
variant: "instance" as const,
|
variant: "instance" as const,
|
||||||
tooltip: "Auto-renewal requires a certificate profile"
|
tooltip: "Renewal requires a certificate profile"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.hasPrivateKey === false) {
|
||||||
|
return {
|
||||||
|
text: "Not Available",
|
||||||
|
variant: "instance" as const,
|
||||||
|
tooltip: "Renewal is not available for certificates with externally generated private keys"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +352,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{(() => {
|
{(() => {
|
||||||
const canManageRenewal =
|
const canManageRenewal =
|
||||||
certificate.profileId &&
|
certificate.profileId &&
|
||||||
|
certificate.hasPrivateKey !== false &&
|
||||||
!certificate.renewedByCertificateId &&
|
!certificate.renewedByCertificateId &&
|
||||||
!isRevoked &&
|
!isRevoked &&
|
||||||
!isExpired &&
|
!isExpired &&
|
||||||
@@ -407,6 +416,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{(() => {
|
{(() => {
|
||||||
const canDisableRenewal =
|
const canDisableRenewal =
|
||||||
certificate.profileId &&
|
certificate.profileId &&
|
||||||
|
certificate.hasPrivateKey !== false &&
|
||||||
!certificate.renewedByCertificateId &&
|
!certificate.renewedByCertificateId &&
|
||||||
!isRevoked &&
|
!isRevoked &&
|
||||||
!isExpired &&
|
!isExpired &&
|
||||||
@@ -445,6 +455,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{(() => {
|
{(() => {
|
||||||
const canRenew =
|
const canRenew =
|
||||||
certificate.profileId &&
|
certificate.profileId &&
|
||||||
|
certificate.hasPrivateKey !== false &&
|
||||||
!certificate.renewedByCertificateId &&
|
!certificate.renewedByCertificateId &&
|
||||||
!isRevoked &&
|
!isRevoked &&
|
||||||
!isExpired;
|
!isExpired;
|
||||||
|
|||||||
Reference in New Issue
Block a user