diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 458beefae..a3c00a513 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -63,10 +63,12 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; type TPkiAcmeServiceFactoryDep = { - certificateProfileDAL: Pick; - internalCertificateAuthorityService: Pick; + certificateProfileDAL: Pick; + certificateV3Service: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" @@ -86,7 +88,7 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ certificateProfileDAL, - internalCertificateAuthorityService, + certificateV3Service, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, @@ -507,7 +509,8 @@ export const pkiAcmeServiceFactory = ({ if (order.status === AcmeOrderStatus.Ready) { order = await acmeOrderDAL.transaction(async (tx) => { const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; - const profile = (await certificateProfileDAL.findById(profileId, tx))!; + // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead + const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; if (order.status !== AcmeOrderStatus.Ready) { throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); } @@ -516,20 +519,30 @@ export const pkiAcmeServiceFactory = ({ } const { csr } = payload; // TODO: validate the CSR and return badCSR error if it's invalid - const { certificate, certificateChain } = await internalCertificateAuthorityService.signCertFromCa({ - isInternal: true, - certificateTemplateId: profile.certificateTemplateId, + // TODO: this should be the same transaction? + const { certificate, certificateChain, certificateId } = await certificateV3Service.signCertificateFromProfile({ + actor: ActorType.ACME_ACCOUNT, + actorId: accountId, + actorAuthMethod: null, + actorOrgId, + profileId, csr, - notBefore: order.notBefore?.toISOString(), - notAfter: order.notAfter?.toISOString() + notBefore: order.notBefore ? new Date(order.notBefore) : undefined, + notAfter: order.notAfter ? new Date(order.notAfter) : undefined, + validity: { + // TODO: read config from the profile to get the expiration time instead + ttl: (24 * 60 * 60 * 1000).toString() + }, + enrollmentType: EnrollmentType.ACME }); + // TODO: associate the certificate with the order await acmeOrderDAL.updateById( orderId, { status: AcmeOrderStatus.Valid, csr, certificateChain, - certificate: certificate.toString("pem") + certificate }, tx ); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a3355ada3..13a5aee2c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1175,20 +1175,6 @@ export const registerRoutes = async ( projectDAL }); - const acmeChallengeService = pkiAcmeChallengeServiceFactory({ - acmeAuthDAL, - acmeChallengeDAL - }); - const pkiAcmeService = pkiAcmeServiceFactory({ - certificateProfileDAL, - acmeAccountDAL, - acmeOrderDAL, - acmeAuthDAL, - acmeOrderAuthDAL, - acmeChallengeDAL, - acmeChallengeService - }); - const pkiAlertService = pkiAlertServiceFactory({ pkiAlertDAL, pkiCollectionDAL, @@ -2209,6 +2195,20 @@ export const registerRoutes = async ( estEnrollmentConfigDAL }); + const acmeChallengeService = pkiAcmeChallengeServiceFactory({ + acmeChallengeDAL + }); + const pkiAcmeService = pkiAcmeServiceFactory({ + certificateV3Service, + certificateProfileDAL, + acmeAccountDAL, + acmeOrderDAL, + acmeAuthDAL, + acmeOrderAuthDAL, + acmeChallengeDAL, + acmeChallengeService + }); + const pkiSubscriberService = pkiSubscriberServiceFactory({ pkiSubscriberDAL, certificateAuthorityDAL, diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/certificates-router.ts index d2d696596..9aa4b198b 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/certificates-router.ts @@ -6,12 +6,6 @@ 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 { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { CertExtendedKeyUsageType, @@ -20,7 +14,14 @@ import { } 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 { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; +import { + ACMESANType, + CertificateOrderStatus, + CertKeyAlgorithm, + CertSignatureAlgorithm +} from "@app/services/certificate/certificate-types"; interface CertificateRequestForService { commonName?: string; @@ -204,7 +205,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => ttl: req.body.ttl }, 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, + enrollmentType: EnrollmentType.API }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 497414a60..ef54ac0be 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -41,6 +41,7 @@ export enum ActorType { // would extend to AWS, Azure, ... IDENTITY = "identity", Machine = "machine", SCIM_CLIENT = "scimClient", + ACME_ACCOUNT = "acmeAccount", UNKNOWN_USER = "unknownUser" } diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 7029d4b37..dcf6ca4e6 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -65,6 +65,23 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } }; + const findByIdWithOwnerOrgId = async ( + id: string, + tx?: Knex + ): Promise<(TCertificateProfile & { ownerOrgId: string }) | undefined> => { + try { + const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile) + .join(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`) + .select(selectAllTableCols(TableName.PkiCertificateProfile)) + .select(db.ref("orgId").withSchema(TableName.Project).as("ownerOrgId")) + .where({ id }) + .first()) as (TCertificateProfile & { ownerOrgId: string }) | undefined; + return certificateProfile; + } catch (error) { + throw new DatabaseError({ error, name: "Find certificate profile by id with owner org id" }); + } + }; + const findByIdWithConfigs = async (id: string, tx?: Knex): Promise => { try { const query = (tx || db)(TableName.PkiCertificateProfile) @@ -444,6 +461,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { updateById, deleteById, findById, + findByIdWithOwnerOrgId, findByIdWithConfigs, findBySlugAndProjectId, findByProjectId, diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index d11cce056..36823671f 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -697,6 +697,7 @@ describe("CertificateV3Service", () => { profileId, csr: mockCSR, validity: mockValidity, + enrollmentType: EnrollmentType.API, ...mockActor }); @@ -731,6 +732,7 @@ describe("CertificateV3Service", () => { profileId, csr: mockCSR, validity: mockValidity, + enrollmentType: EnrollmentType.API, ...mockActor }) ).rejects.toThrow(ForbiddenRequestError); @@ -740,6 +742,7 @@ describe("CertificateV3Service", () => { profileId, csr: mockCSR, validity: mockValidity, + enrollmentType: EnrollmentType.API, ...mockActor }) ).rejects.toThrow("Profile is not configured for api enrollment"); diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 51c79f135..5ccd02698 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -107,6 +107,13 @@ const validateProfileAndPermissions = async ( }); } + // XXX: NOT SURE IF THIS IS SECURE TO BY PASS THE PERMISSION CHECK FOR ACME ACCOUNTS + // may need to consider this carefully + // TODO: check actor/profile ownership as well + if (actor === ActorType.ACME_ACCOUNT && requiredEnrollmentType === EnrollmentType.ACME) { + return profile; + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -484,7 +491,8 @@ export const certificateV3ServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + enrollmentType }: TSignCertificateFromProfileDTO): Promise> => { const profile = await validateProfileAndPermissions( profileId, @@ -494,7 +502,7 @@ export const certificateV3ServiceFactory = ({ actorOrgId, certificateProfileDAL, permissionService, - EnrollmentType.API + enrollmentType ); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); diff --git a/backend/src/services/certificate-v3/certificate-v3-types.ts b/backend/src/services/certificate-v3/certificate-v3-types.ts index a62a25b73..8685d5f30 100644 --- a/backend/src/services/certificate-v3/certificate-v3-types.ts +++ b/backend/src/services/certificate-v3/certificate-v3-types.ts @@ -1,11 +1,12 @@ import { TProjectPermission } from "@app/lib/types"; -import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; import { CertExtendedKeyUsageType, CertKeyUsageType, CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants"; +import { EnrollmentType } from "../certificate-profile/certificate-profile-types"; +import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; export type TIssueCertificateFromProfileDTO = { profileId: string; @@ -35,6 +36,7 @@ export type TSignCertificateFromProfileDTO = { }; notBefore?: Date; notAfter?: Date; + enrollmentType: EnrollmentType; } & Omit; export type TOrderCertificateFromProfileDTO = {