Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/keystore/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const KeyStorePrefixes = {

CertDashboardStats: (projectId: string) => `cert-dashboard-stats:${projectId}` as const,
CertActivityTrend: (projectId: string, range: string) => `cert-activity-trend:${projectId}:${range}` as const,
CertPqcTrend: (projectId: string, range: string) => `cert-pqc-trend:${projectId}:${range}` as const,
RefreshTokenGrace: (sessionId: string) => `refresh-token-grace:${sessionId}` as const,
InsightsCache: (projectId: string, endpoint: string) => `insights-cache:${projectId}:${endpoint}` as const
};
Expand Down
45 changes: 45 additions & 0 deletions backend/src/server/routes/v1/project-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,51 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});

server.route({
method: "GET",
url: "/:projectId/certificates/pqc-trend",
config: {
rateLimit: readLimit
},
schema: {
hide: true,
operationId: "getCertificatePqcTrend",
tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate PQC adoption trend over time.",
params: z.object({
projectId: z.string().trim()
}),
querystring: z.object({
range: z.enum(["7d", "30d", "6m"]).optional().default("30d")
}),
response: {
200: z.object({
periods: z.array(
z.object({
period: z.string(),
pqc: z.number(),
nonPqc: z.number()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.project.getPqcTrend({
filter: {
projectId: req.params.projectId,
type: ProjectFilterType.ID
},
range: req.query.range,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
}
});

server.route({
method: "GET",
url: "/:projectId/pki-alerts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TProjectPermission } from "@app/lib/types";

import { TPermissionServiceFactory } from "../../ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "../../ee/services/permission/project-permission";
import { NON_PQC_KEY_ALGORITHMS, PQC_KEY_ALGORITHMS } from "../certificate/certificate-dal";
import { TCertificateInventoryViewDALFactory } from "./certificate-inventory-view-dal";
import {
TCreateInventoryViewDTO,
Expand All @@ -17,6 +18,7 @@ import {
type TSystemViewFilters = {
status?: string[];
notAfterTo?: string;
keyAlgorithm?: string[];
};

type TSystemView = {
Expand Down Expand Up @@ -61,6 +63,22 @@ const SYSTEM_VIEWS: TSystemView[] = [
columns: null,
isSystem: true,
createdByUserId: null
},
{
id: "system-pqc",
name: "Post-Quantum (PQC)",
filters: { keyAlgorithm: PQC_KEY_ALGORITHMS },
columns: null,
isSystem: true,
createdByUserId: null
},
{
id: "system-non-pqc",
name: "Classical (Non-PQC)",
filters: { keyAlgorithm: NON_PQC_KEY_ALGORITHMS },
columns: null,
isSystem: true,
createdByUserId: null
}
];

Expand Down
92 changes: 90 additions & 2 deletions backend/src/services/certificate/certificate-dal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ import { isUuidV4 } from "@app/lib/validator";
import { applyMetadataFilter } from "@app/services/resource-metadata/resource-metadata-fns";

import { keySizeToAlgorithms } from "./certificate-fns";
import { CertStatus } from "./certificate-types";
import { CertKeyAlgorithm, CertStatus } from "./certificate-types";

// Scoped to UI-surfaced algorithms; intentionally narrower than pqc-utils.PQC_ALGORITHMS.
export const PQC_KEY_ALGORITHMS: string[] = [
CertKeyAlgorithm.ML_DSA_44,
CertKeyAlgorithm.ML_DSA_65,
CertKeyAlgorithm.ML_DSA_87
];

export const NON_PQC_KEY_ALGORITHMS: string[] = [
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_3072,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384,
CertKeyAlgorithm.ECDSA_P521
];

export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;

Expand Down Expand Up @@ -918,6 +934,77 @@ export const certificateDALFactory = (db: TDbClient) => {
}
};

const getPqcTrend = async (projectId: string, daysBack: number) => {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
startDate.setHours(0, 0, 0, 0);
const now = new Date();

const useDaily = daysBack <= 30;
if (!useDaily) {
startDate.setDate(1);
}
const truncUnit = useDaily ? "day" : "month";
const dateFormat = useDaily ? "YYYY-MM-DD" : "YYYY-MM";

const rows = await db
.replicaNode()(TableName.Certificate)
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
.where(`${TableName.CertificateAuthority}.projectId`, projectId)
.where(`${TableName.Certificate}.notBefore`, ">=", startDate)
.where(`${TableName.Certificate}.notBefore`, "<=", now)
.whereIn(`${TableName.Certificate}.keyAlgorithm`, [...PQC_KEY_ALGORITHMS, ...NON_PQC_KEY_ALGORITHMS])
Comment thread
saifsmailbox98 marked this conversation as resolved.
.select(
db.raw(`to_char(date_trunc(?, "${TableName.Certificate}"."notBefore"), ?) as period`, [truncUnit, dateFormat])
)
.select(
db.raw(
`CASE WHEN "${TableName.Certificate}"."keyAlgorithm" IN (${PQC_KEY_ALGORITHMS.map(() => "?").join(",")}) THEN 'pqc' ELSE 'nonPqc' END as bucket`,
[...PQC_KEY_ALGORITHMS]
)
)
.select(db.raw("count(*)::int as count"))
.groupBy("period", "bucket")
.orderBy("period");

interface PeriodBucketCount {
period: string;
bucket: "pqc" | "nonPqc";
count: string;
}
const typed = rows as unknown as PeriodBucketCount[];
const pqcMap = new Map<string, number>();
const nonPqcMap = new Map<string, number>();
typed.forEach((r) => {
if (r.bucket === "pqc") pqcMap.set(r.period, Number(r.count));
else nonPqcMap.set(r.period, Number(r.count));
});

const periods: Array<{ period: string; pqc: number; nonPqc: number }> = [];
const cursor = new Date(startDate);
while (cursor <= now) {
const periodKey = useDaily
? `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}-${String(cursor.getDate()).padStart(2, "0")}`
: `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}`;
periods.push({
period: periodKey,
pqc: pqcMap.get(periodKey) ?? 0,
nonPqc: nonPqcMap.get(periodKey) ?? 0
});
if (useDaily) {
cursor.setDate(cursor.getDate() + 1);
} else {
cursor.setMonth(cursor.getMonth() + 1);
}
}

return { periods };
} catch (error) {
throw new DatabaseError({ error, name: "Get PQC trend" });
}
};

return {
...certificateOrm,
countCertificatesInProject,
Expand All @@ -932,6 +1019,7 @@ export const certificateDALFactory = (db: TDbClient) => {
findWithPrivateKeyInfo,
findWithFullDetails,
getDashboardStats,
getActivityTrend
getActivityTrend,
getPqcTrend
};
};
38 changes: 38 additions & 0 deletions backend/src/services/project/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ type TProjectServiceFactoryDep = {
| "countActiveCertificatesForSync"
| "getDashboardStats"
| "getActivityTrend"
| "getPqcTrend"
>;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
Expand Down Expand Up @@ -1403,6 +1404,42 @@ export const projectServiceFactory = ({
});
};

const getPqcTrend = async ({
filter,
range = "30d",
actorId,
actorOrgId,
actorAuthMethod,
actor
}: TGetActivityTrendDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
const projectId = project.id;

const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});

ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);

const rangeDaysMap: Record<string, number> = { "7d": 7, "30d": 30, "6m": 180 };
const daysBack = rangeDaysMap[range];

return withCache({
keyStore,
key: KeyStorePrefixes.CertPqcTrend(projectId, range),
ttlSeconds: DASHBOARD_CACHE_TTL,
fetcher: () => certificateDAL.getPqcTrend(projectId, daysBack)
});
};

/**
* Return list of (PKI) alerts configured for project
*/
Expand Down Expand Up @@ -2433,6 +2470,7 @@ export const projectServiceFactory = ({
listProjectCertificates,
getDashboardStats,
getActivityTrend,
getPqcTrend,
listProjectAlerts,
listProjectPkiCollections,
listProjectCertificateTemplates,
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/hooks/api/certificates/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
[CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
[CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384",
[CertKeyAlgorithm.ECDSA_P521]: "ECDSA P521",
[CertKeyAlgorithm.ML_DSA_44]: "ML-DSA-44",
[CertKeyAlgorithm.ML_DSA_65]: "ML-DSA-65",
[CertKeyAlgorithm.ML_DSA_87]: "ML-DSA-87"
Expand All @@ -42,12 +43,30 @@
"ECDSA-SHA512": "ECDSA with SHA-512",
"ML-DSA-44": "ML-DSA-44",
"ML-DSA-65": "ML-DSA-65",
"ML-DSA-87": "ML-DSA-87"
};

export const PQC_KEY_ALGORITHMS = [
CertKeyAlgorithm.ML_DSA_44,
CertKeyAlgorithm.ML_DSA_65,
CertKeyAlgorithm.ML_DSA_87
] as const;
Comment thread
saifsmailbox98 marked this conversation as resolved.

export const NON_PQC_KEY_ALGORITHMS = [
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_3072,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384,
CertKeyAlgorithm.ECDSA_P521
] as const;

export const isPqcAlgorithm = (label: string): boolean =>
(PQC_KEY_ALGORITHMS as readonly string[]).includes(label);

export const certKeyAlgorithms = [
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_3072], value: CertKeyAlgorithm.RSA_3072 },

Check failure on line 69 in frontend/src/hooks/api/certificates/constants.tsx

View check run for this annotation

Claude / Claude Code Review

ECDSA_P521 missing from certKeyAlgorithms dropdown array

The `certKeyAlgorithms` array in `frontend/src/hooks/api/certificates/constants.tsx` is missing an entry for `ECDSA_P521`, so users cannot select EC P521 from algorithm-selection dropdowns in CA creation, certificate policy forms, and KMIP client certificate modals. Add `{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P521], value: CertKeyAlgorithm.ECDSA_P521 }` after the `ECDSA_P384` entry to restore selectability.
Comment thread
saifsmailbox98 marked this conversation as resolved.
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_4096], value: CertKeyAlgorithm.RSA_4096 },
{
label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P256],
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/api/certificates/enums.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum CertKeyAlgorithm {
RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1",
ECDSA_P384 = "EC_secp384r1",
ECDSA_P521 = "EC_secp521r1",
ML_DSA_44 = "ML-DSA-44",
ML_DSA_65 = "ML-DSA-65",
ML_DSA_87 = "ML-DSA-87"
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/hooks/api/certificates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
useGetCertDashboardStats,
useGetCertificateById,
useGetCertificateRequest,
useGetCertPqcTrend,
useListCertificateRequests
} from "./queries";
export type {
Expand All @@ -31,5 +32,7 @@ export type {
TExpirationBucket,
TListCertificateRequestsParams,
TListCertificateRequestsResponse,
TPqcTrendPoint,
TPqcTrendResponse,
TUpdateCertificateDTO
} from "./types";
22 changes: 20 additions & 2 deletions frontend/src/hooks/api/certificates/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
TCertificateRequestDetails,
TDashboardStats,
TListCertificateRequestsParams,
TListCertificateRequestsResponse
TListCertificateRequestsResponse,
TPqcTrendResponse
} from "./types";

export const certKeys = {
Expand Down Expand Up @@ -38,7 +39,9 @@ export const certKeys = {
],
getDashboardStats: (projectId: string) => ["cert-dashboard-stats", { projectId }] as const,
getActivityTrend: (projectId: string, range: string) =>
["cert-activity-trend", { projectId }, { range }] as const
["cert-activity-trend", { projectId }, { range }] as const,
getPqcTrend: (projectId: string, range: string) =>
["cert-pqc-trend", { projectId }, { range }] as const
};

export const useGetCert = (serialNumber: string) => {
Expand Down Expand Up @@ -175,3 +178,18 @@ export const useGetCertActivityTrend = (projectId: string, range = "6m") => {
staleTime: DASHBOARD_STALE_TIME
});
};

export const useGetCertPqcTrend = (projectId: string, range = "30d") => {
return useQuery({
queryKey: certKeys.getPqcTrend(projectId, range),
queryFn: async () => {
const { data } = await apiRequest.get<TPqcTrendResponse>(
`/api/v1/projects/${projectId}/certificates/pqc-trend`,
{ params: { range } }
);
return data;
},
enabled: Boolean(projectId),
staleTime: DASHBOARD_STALE_TIME
});
};
10 changes: 10 additions & 0 deletions frontend/src/hooks/api/certificates/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,13 @@ export type TActivityTrendPoint = {
export type TActivityTrendResponse = {
periods: TActivityTrendPoint[];
};

export type TPqcTrendPoint = {
period: string;
pqc: number;
nonPqc: number;
};

export type TPqcTrendResponse = {
periods: TPqcTrendPoint[];
};
Loading
Loading