Issue cert

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:19:50 -08:00
parent e652c2a5a3
commit 47ee70c6a0
8 changed files with 81 additions and 34 deletions
@@ -63,10 +63,12 @@ import {
TRawJwsPayload, TRawJwsPayload,
TRespondToAcmeChallengeResponse TRespondToAcmeChallengeResponse
} from "./pki-acme-types"; } 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 = { type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId">;
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">; certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
acmeAccountDAL: Pick< acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory, TPkiAcmeAccountDALFactory,
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
@@ -86,7 +88,7 @@ type TPkiAcmeServiceFactoryDep = {
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
certificateProfileDAL, certificateProfileDAL,
internalCertificateAuthorityService, certificateV3Service,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
acmeAuthDAL, acmeAuthDAL,
@@ -507,7 +509,8 @@ export const pkiAcmeServiceFactory = ({
if (order.status === AcmeOrderStatus.Ready) { if (order.status === AcmeOrderStatus.Ready) {
order = await acmeOrderDAL.transaction(async (tx) => { order = await acmeOrderDAL.transaction(async (tx) => {
const order = (await acmeOrderDAL.findByIdForFinalization(orderId, 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) { if (order.status !== AcmeOrderStatus.Ready) {
throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" });
} }
@@ -516,20 +519,30 @@ export const pkiAcmeServiceFactory = ({
} }
const { csr } = payload; const { csr } = payload;
// TODO: validate the CSR and return badCSR error if it's invalid // TODO: validate the CSR and return badCSR error if it's invalid
const { certificate, certificateChain } = await internalCertificateAuthorityService.signCertFromCa({ // TODO: this should be the same transaction?
isInternal: true, const { certificate, certificateChain, certificateId } = await certificateV3Service.signCertificateFromProfile({
certificateTemplateId: profile.certificateTemplateId, actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
profileId,
csr, csr,
notBefore: order.notBefore?.toISOString(), notBefore: order.notBefore ? new Date(order.notBefore) : undefined,
notAfter: order.notAfter?.toISOString() 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( await acmeOrderDAL.updateById(
orderId, orderId,
{ {
status: AcmeOrderStatus.Valid, status: AcmeOrderStatus.Valid,
csr, csr,
certificateChain, certificateChain,
certificate: certificate.toString("pem") certificate
}, },
tx tx
); );
+14 -14
View File
@@ -1175,20 +1175,6 @@ export const registerRoutes = async (
projectDAL projectDAL
}); });
const acmeChallengeService = pkiAcmeChallengeServiceFactory({
acmeAuthDAL,
acmeChallengeDAL
});
const pkiAcmeService = pkiAcmeServiceFactory({
certificateProfileDAL,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,
acmeOrderAuthDAL,
acmeChallengeDAL,
acmeChallengeService
});
const pkiAlertService = pkiAlertServiceFactory({ const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL, pkiAlertDAL,
pkiCollectionDAL, pkiCollectionDAL,
@@ -2209,6 +2195,20 @@ export const registerRoutes = async (
estEnrollmentConfigDAL estEnrollmentConfigDAL
}); });
const acmeChallengeService = pkiAcmeChallengeServiceFactory({
acmeChallengeDAL
});
const pkiAcmeService = pkiAcmeServiceFactory({
certificateV3Service,
certificateProfileDAL,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,
acmeOrderAuthDAL,
acmeChallengeDAL,
acmeChallengeService
});
const pkiSubscriberService = pkiSubscriberServiceFactory({ const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL, pkiSubscriberDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -6,12 +6,6 @@ import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; 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 { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
@@ -20,7 +14,14 @@ import {
} 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 { 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 { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import {
ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
interface CertificateRequestForService { interface CertificateRequestForService {
commonName?: string; commonName?: string;
@@ -204,7 +205,8 @@ 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,
enrollmentType: EnrollmentType.API
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
+1
View File
@@ -41,6 +41,7 @@ export enum ActorType { // would extend to AWS, Azure, ...
IDENTITY = "identity", IDENTITY = "identity",
Machine = "machine", Machine = "machine",
SCIM_CLIENT = "scimClient", SCIM_CLIENT = "scimClient",
ACME_ACCOUNT = "acmeAccount",
UNKNOWN_USER = "unknownUser" UNKNOWN_USER = "unknownUser"
} }
@@ -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<TCertificateProfileWithConfigs | undefined> => { const findByIdWithConfigs = async (id: string, tx?: Knex): Promise<TCertificateProfileWithConfigs | undefined> => {
try { try {
const query = (tx || db)(TableName.PkiCertificateProfile) const query = (tx || db)(TableName.PkiCertificateProfile)
@@ -444,6 +461,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
updateById, updateById,
deleteById, deleteById,
findById, findById,
findByIdWithOwnerOrgId,
findByIdWithConfigs, findByIdWithConfigs,
findBySlugAndProjectId, findBySlugAndProjectId,
findByProjectId, findByProjectId,
@@ -697,6 +697,7 @@ describe("CertificateV3Service", () => {
profileId, profileId,
csr: mockCSR, csr: mockCSR,
validity: mockValidity, validity: mockValidity,
enrollmentType: EnrollmentType.API,
...mockActor ...mockActor
}); });
@@ -731,6 +732,7 @@ describe("CertificateV3Service", () => {
profileId, profileId,
csr: mockCSR, csr: mockCSR,
validity: mockValidity, validity: mockValidity,
enrollmentType: EnrollmentType.API,
...mockActor ...mockActor
}) })
).rejects.toThrow(ForbiddenRequestError); ).rejects.toThrow(ForbiddenRequestError);
@@ -740,6 +742,7 @@ describe("CertificateV3Service", () => {
profileId, profileId,
csr: mockCSR, csr: mockCSR,
validity: mockValidity, validity: mockValidity,
enrollmentType: EnrollmentType.API,
...mockActor ...mockActor
}) })
).rejects.toThrow("Profile is not configured for api enrollment"); ).rejects.toThrow("Profile is not configured for api enrollment");
@@ -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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -484,7 +491,8 @@ export const certificateV3ServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
enrollmentType
}: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => { }: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => {
const profile = await validateProfileAndPermissions( const profile = await validateProfileAndPermissions(
profileId, profileId,
@@ -494,7 +502,7 @@ export const certificateV3ServiceFactory = ({
actorOrgId, actorOrgId,
certificateProfileDAL, certificateProfileDAL,
permissionService, permissionService,
EnrollmentType.API enrollmentType
); );
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
@@ -1,11 +1,12 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
CertKeyUsageType, CertKeyUsageType,
CertSubjectAlternativeNameType CertSubjectAlternativeNameType
} from "../certificate-common/certificate-constants"; } from "../certificate-common/certificate-constants";
import { EnrollmentType } from "../certificate-profile/certificate-profile-types";
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
export type TIssueCertificateFromProfileDTO = { export type TIssueCertificateFromProfileDTO = {
profileId: string; profileId: string;
@@ -35,6 +36,7 @@ export type TSignCertificateFromProfileDTO = {
}; };
notBefore?: Date; notBefore?: Date;
notAfter?: Date; notAfter?: Date;
enrollmentType: EnrollmentType;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TOrderCertificateFromProfileDTO = { export type TOrderCertificateFromProfileDTO = {