Issue cert

This commit is contained in:
Fang-Pen Lin
2025-10-31 21:32:27 -07:00
parent e652c2a5a3
commit 47ee70c6a0
8 changed files with 81 additions and 34 deletions

View File

@@ -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<TCertificateProfileDALFactory, "findById">;
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
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
);

View File

@@ -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,

View File

@@ -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({

View File

@@ -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"
}

View File

@@ -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> => {
try {
const query = (tx || db)(TableName.PkiCertificateProfile)
@@ -444,6 +461,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
updateById,
deleteById,
findById,
findByIdWithOwnerOrgId,
findByIdWithConfigs,
findBySlugAndProjectId,
findByProjectId,

View File

@@ -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");

View File

@@ -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<Omit<TCertificateFromProfileResponse, "privateKey">> => {
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);

View File

@@ -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<TProjectPermission, "projectId">;
export type TOrderCertificateFromProfileDTO = {