mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: added subscriber endpoint for fetching active cert
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -57,6 +57,10 @@ export type TListPkiSubscriberCertsDTO = {
|
||||
limit: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetSubscriberActiveCertBundleDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export enum SubscriberOperationStatus {
|
||||
SUCCESS = "success",
|
||||
FAILED = "failed"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Retrieve active certificate bundle"
|
||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/active-certificate/bundle"
|
||||
---
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user