feat: added subscriber endpoint for fetching active cert

This commit is contained in:
Sheen Capadngan
2025-05-18 23:37:23 +08:00
parent 10ed6f6b52
commit b358f2dbb7
8 changed files with 227 additions and 6 deletions

View File

@@ -263,6 +263,7 @@ export enum EventType {
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
@@ -2061,6 +2062,16 @@ interface ListPkiSubscriberCerts {
};
}
interface GetSubscriberActiveCertBundle {
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE;
metadata: {
subscriberId: string;
subscriberName: string;
certId: string;
serialNumber: string;
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
@@ -3033,6 +3044,7 @@ export type Event =
| IssuePkiSubscriberCert
| SignPkiSubscriberCert
| ListPkiSubscriberCerts
| GetSubscriberActiveCertBundle
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent

View File

@@ -1755,6 +1755,14 @@ export const PKI_SUBSCRIBERS = {
subscriberName: "The name of the PKI subscriber to get.",
projectId: "The ID of the project to get the PKI subscriber for."
},
GET_ACTIVE_CERT_BUNDLE: {
subscriberName: "The name of the PKI subscriber to get the active certificate bundle for.",
projectId: "The ID of the project to get the active certificate bundle for.",
certificate: "The active certificate for the subscriber.",
certificateChain: "The certificate chain of the active certificate for the subscriber.",
privateKey: "The private key of the active certificate for the subscriber.",
serialNumber: "The serial number of the active certificate for the subscriber."
},
CREATE: {
projectId: "The ID of the project to create the PKI subscriber in.",
caId: "The ID of the CA that will issue certificates for the PKI subscriber.",

View File

@@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addNoCacheHeaders } from "@app/server/lib/caching";
import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -482,6 +483,72 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
}
});
server.route({
method: "GET",
url: "/:subscriberName/active-certificate/bundle",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Get active certificate bundle of a subscriber",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET_ACTIVE_CERT_BUNDLE.subscriberName)
}),
querystring: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.GET_ACTIVE_CERT_BUNDLE.projectId)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.GET_ACTIVE_CERT_BUNDLE.certificate),
certificateChain: z
.string()
.trim()
.nullable()
.describe(PKI_SUBSCRIBERS.GET_ACTIVE_CERT_BUNDLE.certificateChain),
privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.GET_ACTIVE_CERT_BUNDLE.privateKey),
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.GET_ACTIVE_CERT_BUNDLE.serialNumber)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, reply) => {
const { certificate, certificateChain, serialNumber, cert, privateKey, subscriber } =
await server.services.pkiSubscriber.getSubscriberActiveCertBundle({
subscriberName: req.params.subscriberName,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: cert.projectId,
event: {
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE,
metadata: {
subscriberId: subscriber.id,
subscriberName: subscriber.name,
certId: cert.id,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
return {
certificate,
certificateChain,
serialNumber,
privateKey
};
}
});
server.route({
method: "GET",
url: "/:subscriberName/certificates",

View File

@@ -3,11 +3,24 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { CertStatus } from "./certificate-types";
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate);
const findLatestActiveCertForSubscriber = async ({ subscriberId }: { subscriberId: string }) => {
const cert = await db
.replicaNode()(TableName.Certificate)
.where({ pkiSubscriberId: subscriberId, status: CertStatus.ACTIVE })
.where("notAfter", ">", new Date())
.orderBy("notBefore", "desc")
.first();
return cert;
};
const countCertificatesInProject = async ({
projectId,
friendlyName,
@@ -65,6 +78,7 @@ export const certificateDALFactory = (db: TDbClient) => {
return {
...certificateOrm,
countCertificatesInProject,
countCertificatesForPkiSubscriber
countCertificatesForPkiSubscriber,
findLatestActiveCertForSubscriber
};
};

View File

@@ -6,6 +6,7 @@ import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionCertificateActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
@@ -38,6 +39,7 @@ import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subsc
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCertificateCredentials } from "../certificate/certificate-fns";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue";
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
@@ -46,6 +48,7 @@ import {
TCreatePkiSubscriberDTO,
TDeletePkiSubscriberDTO,
TGetPkiSubscriberDTO,
TGetSubscriberActiveCertBundleDTO,
TIssuePkiSubscriberCertDTO,
TListPkiSubscriberCertsDTO,
TOrderPkiSubscriberCertDTO,
@@ -66,9 +69,12 @@ type TPkiSubscriberServiceFactoryDep = {
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityQueue: Pick<TCertificateAuthorityQueueFactory, "orderCertificateForSubscriber">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateDAL: Pick<
TCertificateDALFactory,
"create" | "transaction" | "countCertificatesForPkiSubscriber" | "findLatestActiveCertForSubscriber" | "find"
>;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create" | "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create" | "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -691,6 +697,110 @@ export const pkiSubscriberServiceFactory = ({
};
};
const getSubscriberActiveCertBundle = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetSubscriberActiveCertBundleDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.ListCerts,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const cert = await certificateDAL.findLatestActiveCertForSubscriber({
subscriberId: subscriber.id
});
if (!cert) {
throw new NotFoundError({ message: "No active certificate found for subscriber" });
}
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: cert.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate
});
const certObj = new x509.X509Certificate(decryptedCert);
const certificate = certObj.toString("pem");
let certificateChain = null;
// On newer certs the certBody.encryptedCertificateChain column will always exist.
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
} else if (cert.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: cert.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
certificate,
certificateChain,
privateKey: certPrivateKey,
serialNumber: cert.serialNumber,
cert,
subscriber
};
};
return {
createSubscriber,
getSubscriber,
@@ -699,6 +809,7 @@ export const pkiSubscriberServiceFactory = ({
issueSubscriberCert,
signSubscriberCert,
listSubscriberCerts,
orderSubscriberCert
orderSubscriberCert,
getSubscriberActiveCertBundle
};
};

View File

@@ -57,6 +57,10 @@ export type TListPkiSubscriberCertsDTO = {
limit: number;
} & TProjectPermission;
export type TGetSubscriberActiveCertBundleDTO = {
subscriberName: string;
} & TProjectPermission;
export enum SubscriberOperationStatus {
SUCCESS = "success",
FAILED = "failed"

View File

@@ -0,0 +1,4 @@
---
title: "Retrieve active certificate bundle"
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/active-certificate/bundle"
---

View File

@@ -1468,7 +1468,8 @@
"api-reference/endpoints/pki/subscribers/update",
"api-reference/endpoints/pki/subscribers/delete",
"api-reference/endpoints/pki/subscribers/issue-cert",
"api-reference/endpoints/pki/subscribers/sign-cert"
"api-reference/endpoints/pki/subscribers/sign-cert",
"api-reference/endpoints/pki/subscribers/get-active-cert-bundle"
]
},
{